Приложение читает RSS ленту и синтезированным голосом читает заголовки новостей. MainActivity.java package com.example.rss2audio; import android.Manifest; import android.content.pm.PackageManager; import android.os.Bundle; import android.os.Handler; import android.speech.tts.TextToSpeech; import android.speech.tts.UtteranceProgressListener; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserFactory; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; import java.util.ArrayList; import java.util.Locale; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class MainActivity extends AppCompatActivity { private static final int PERMISSION_REQUEST_CODE = 1; private static final int MAX_ITEMS = 40; private static final int READ_INTERVAL_MS = 15 * 60 * 1000; // 15 минут private TextView newsTextView; private Button exitButton; private TextToSpeech textToSpeech; private Handler handler = new Handler(); private ExecutorService executorService = Executors.newSingleThreadExecutor(); private ArrayList newsTitles = new ArrayList<>(); private int currentNewsIndex = 0; private boolean isSpeaking = false; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); newsTextView = findViewById(R.id.newsTextView); exitButton = findViewById(R.id.exitButton); // Инициализация TextToSpeech initTextToSpeech(); // Настройка кнопки выхода exitButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { finish(); } }); // Запрос разрешений checkPermissions(); // Запрашиваем разрешения сразу при запуске requestPermissions(); } private void requestPermissions() { if (ContextCompat.checkSelfPermission(this, Manifest.permission.INTERNET) != PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_NETWORK_STATE) != PackageManager.PERMISSION_GRANTED) { // Показываем диалог с объяснением необходимости разрешений if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.INTERNET)) { new AlertDialog.Builder(this) .setTitle("Необходимы разрешения") .setMessage("Приложению нужны разрешения на доступ к интернету для загрузки новостей") .setPositiveButton("OK", (dialog, which) -> { ActivityCompat.requestPermissions(MainActivity.this, new String[]{ Manifest.permission.INTERNET, Manifest.permission.ACCESS_NETWORK_STATE }, PERMISSION_REQUEST_CODE); }) .setNegativeButton("Отмена", null) .create() .show(); } else { // Запрашиваем разрешения без объяснения ActivityCompat.requestPermissions(this, new String[]{ Manifest.permission.INTERNET, Manifest.permission.ACCESS_NETWORK_STATE }, PERMISSION_REQUEST_CODE); } } else { // Разрешения уже есть, начинаем работу Toast.makeText(this, "Разрешения получены", Toast.LENGTH_SHORT).show(); startRSSReading(); } } // Инициализация синтезатора речи private void initTextToSpeech() { textToSpeech = new TextToSpeech(this, new TextToSpeech.OnInitListener() { @Override public void onInit(int status) { if (status == TextToSpeech.SUCCESS) { int result = textToSpeech.setLanguage(new Locale("ru", "RU")); if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) { textToSpeech.setLanguage(Locale.getDefault()); } // Установка слушателя для отслеживания окончания произнесения текста textToSpeech.setOnUtteranceProgressListener(new UtteranceProgressListener() { @Override public void onStart(String utteranceId) { runOnUiThread(() -> { isSpeaking = true; newsTextView.setText(newsTitles.get(currentNewsIndex)); }); } @Override public void onDone(String utteranceId) { runOnUiThread(() -> { isSpeaking = false; currentNewsIndex++; if (currentNewsIndex < newsTitles.size()) { // Пауза 2 секунды перед следующим заголовком handler.postDelayed(() -> speakNextTitle(), 2000); } else { // Все заголовки произнесены, запускаем повтор через 15 минут startPeriodicReading(); } }); } @Override public void onError(String utteranceId) { runOnUiThread(() -> { isSpeaking = false; Toast.makeText(MainActivity.this, "Ошибка синтеза речи", Toast.LENGTH_SHORT).show(); }); } }); Toast.makeText(MainActivity.this, "TTS инициализирован", Toast.LENGTH_SHORT).show(); } else { Toast.makeText(MainActivity.this, "Ошибка инициализации TTS", Toast.LENGTH_SHORT).show(); } } }); } // Проверка и запрос разрешений private void checkPermissions() { String[] permissions = { Manifest.permission.INTERNET, Manifest.permission.ACCESS_NETWORK_STATE }; boolean allGranted = true; for (String permission : permissions) { if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) { allGranted = false; break; } } if (!allGranted) { ActivityCompat.requestPermissions(this, permissions, PERMISSION_REQUEST_CODE); } else { // Разрешения уже есть, начинаем работу startRSSReading(); } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == PERMISSION_REQUEST_CODE) { boolean allGranted = true; for (int result : grantResults) { if (result != PackageManager.PERMISSION_GRANTED) { allGranted = false; break; } } if (allGranted) { Toast.makeText(this, "Разрешения предоставлены", Toast.LENGTH_SHORT).show(); startRSSReading(); } else { Toast.makeText(this, "Без разрешений приложение не может работать", Toast.LENGTH_LONG).show(); newsTextView.setText("Необходимы разрешения для работы"); // Предлагаем снова запросить разрешения new Handler().postDelayed(() -> requestPermissions(), 2000); } } } // Начало чтения RSS private void startRSSReading() { // Проверяем разрешения перед началом работы if (ContextCompat.checkSelfPermission(this, Manifest.permission.INTERNET) != PackageManager.PERMISSION_GRANTED) { Toast.makeText(this, "Сначала предоставьте разрешения", Toast.LENGTH_SHORT).show(); return; } // Проверка подключения к интернету if (!isNetworkAvailable()) { Toast.makeText(this, "Интернет не доступен. Проверьте подключение.", Toast.LENGTH_LONG).show(); newsTextView.setText("Нет подключения к интернету"); return; } newsTextView.setText("Загрузка новостей..."); // Запуск парсинга в фоновом потоке executorService.execute(this::parseRSS); } // Парсинг RSS private void parseRSS() { try { URL url = new URL("https://lenta.ru/rss"); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET"); connection.connect(); InputStream inputStream = connection.getInputStream(); XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); XmlPullParser parser = factory.newPullParser(); parser.setInput(inputStream, null); int eventType = parser.getEventType(); boolean inItem = false; int itemCount = 0; ArrayList titles = new ArrayList<>(); StringBuilder currentTitle = new StringBuilder(); while (eventType != XmlPullParser.END_DOCUMENT && itemCount < MAX_ITEMS) { String tagName = parser.getName(); switch (eventType) { case XmlPullParser.START_TAG: if ("item".equals(tagName)) { inItem = true; } else if (inItem && "title".equals(tagName)) { currentTitle = new StringBuilder(); } break; case XmlPullParser.TEXT: if (inItem && currentTitle != null) { currentTitle.append(parser.getText()); } break; case XmlPullParser.END_TAG: if ("item".equals(tagName)) { inItem = false; } else if (inItem && "title".equals(tagName)) { String title = currentTitle.toString().trim(); if (!title.isEmpty()) { titles.add(title); itemCount++; Log.d("RSSParser", "Найден заголовок: " + title); } } break; } eventType = parser.next(); } inputStream.close(); runOnUiThread(new Runnable() { @Override public void run() { if (titles.isEmpty()) { Toast.makeText(MainActivity.this, "Не удалось получить заголовки новостей", Toast.LENGTH_LONG).show(); newsTextView.setText("Не удалось загрузить новости"); } else { newsTitles.clear(); newsTitles.addAll(titles); currentNewsIndex = 0; // Вывод количества найденных заголовков для отладки Toast.makeText(MainActivity.this, "Загружено заголовков: " + newsTitles.size(), Toast.LENGTH_SHORT).show(); speakNextTitle(); } } }); } catch (Exception e) { e.printStackTrace(); runOnUiThread(() -> { Toast.makeText(MainActivity.this, "Ошибка при загрузке RSS: " + e.getMessage(), Toast.LENGTH_LONG).show(); newsTextView.setText("Ошибка загрузки: " + e.getMessage()); }); } } // Произнесение следующего заголовка private void speakNextTitle() { if (currentNewsIndex < newsTitles.size() && textToSpeech != null) { String title = newsTitles.get(currentNewsIndex); // Используем параметры по умолчанию textToSpeech.speak(title, TextToSpeech.QUEUE_FLUSH, null, "tts_" + currentNewsIndex); } } // Запуск периодического чтения private void startPeriodicReading() { handler.postDelayed(new Runnable() { @Override public void run() { if (!isSpeaking) { startRSSReading(); } // Планируем следующий запуск startPeriodicReading(); } }, READ_INTERVAL_MS); } // Проверка доступности сети private boolean isNetworkAvailable() { android.net.ConnectivityManager cm = (android.net.ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE); return cm.getActiveNetworkInfo() != null && cm.getActiveNetworkInfo().isConnected(); } @Override protected void onDestroy() { super.onDestroy(); if (textToSpeech != null) { textToSpeech.stop(); textToSpeech.shutdown(); } handler.removeCallbacksAndMessages(null); executorService.shutdown(); } } Activity_main.xml