Программирование

Голосовое чтение уведомлений в Android

Для своего первого Android приложения я выбрал не самую легкую задачу, но мне хотелось не просто написать «Привет мир», а сделать что-то реально полезное. Итак, программа умеет озвучивать все входящие уведомления, что особенно помогает в моих покатушках: больше не надо слезать с велосипеда во время затяжного подъема или быстрого спуска ради очередного «Ты где? Как дела?». =)

Сразу скажу, что этот код я писал для себя и своего девятого Андрюши, что он наверняка содержит кучу ошибок и уж точно не запустится под 10 или 11 версию с их постоянным устареванием методов/нововведениями, но он прямо сейчас и уже целую неделю без перерывов работает на моем смартфоне, чему я очень рад. Думаю, при желании, вы сможете легко переделать его под свои требования. Поехали.

Первым делом разберемся с Google Text-to-Speech. Для этого нам потребуется его инициализировать, установить язык и написать 2 простых функции: озвучки и паузы.

private TextToSpeech tts;
    private boolean isLoaded = false;

    public void init(Context context) {
        try {
            tts = new TextToSpeech(context, onInitListener);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private TextToSpeech.OnInitListener onInitListener = new TextToSpeech.OnInitListener() {

        @Override
        public void onInit(int status) {
            if (status == TextToSpeech.SUCCESS) {
                Locale locale = new Locale("ru");
                int result = tts.setLanguage(locale);
                isLoaded = true;

                if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) {
                    Log.e("error", "Русский язык не поддерживается");
                }
            } else {
                Log.e("error", "Ошибка инициализации!");
            }
        }

    };

    public void speak(String text) {
        if (isLoaded)
            tts.speak(text, TextToSpeech.QUEUE_ADD, null, null);
        else
            Log.e("error", "TTS не инициализирован!");
    }

    public void pause(int duration) {
        if (isLoaded)
            tts.playSilentUtterance(duration, TextToSpeech.QUEUE_ADD, null);
        else
            Log.e("error", "TTS не инициализирован!");
    }

Кстати, изменить голос с грубого мужского на томный женский можно в настройках самого приложения. =)

Далее напишем небольшой сервис для работы с уведомлениями, в котором будем получать название приложения, заголовок и текст отправленного сообщения.

Context context;

    @Override
    public void onCreate() {

        super.onCreate();
        context = getApplicationContext();

    }

    @Override
    public void onNotificationPosted(StatusBarNotification sbn) {

        String pack = sbn.getPackageName();
        Bundle extras = sbn.getNotification().extras;
        if (extras.getCharSequence(Notification.EXTRA_TEXT) != null) {
            String title = extras.getCharSequence(Notification.EXTRA_TITLE).toString();
            String text = extras.getCharSequence(Notification.EXTRA_TEXT).toString();

            Log.i("Package", pack);
            Log.i("Title", title);
            Log.i("Text", text);

            Intent msgrcv = new Intent("Msg");
            msgrcv.putExtra("package", pack);
            msgrcv.putExtra("title", title);
            msgrcv.putExtra("text", text);

            if (sbn.isClearable())
                LocalBroadcastManager.getInstance(context).sendBroadcast(msgrcv);
        }

    }

    @Override
    public void onNotificationRemoved(StatusBarNotification sbn) {

        Log.i("Msg", "Уведомление удалено");

    }

Тут все еще проще и единственная загвоздка была со сложными YouTube уведомлениями. Именно поэтому пришлось использовать Notification.EXTRA_TEXT вместо android.text, который всегда был равен нулю.

Теперь напишем код моего главного класса — MainActivity. В нем мы будем проверять разрешение на чтение уведомлений при запуске и если такое не установлено, то сразу открывать настройки. Также мы создадим нескрываемое уведомление для постоянного отображения иконки приложения в статус баре и удобного его открытия по клику на текст сообщения. Ну и напоследок будем выводить уведомления в табличку.

TableLayout tab;
    NReader s;
    String buf = "";
    boolean notif = false;
    NotificationManager mNotifyMgr;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        s = new NReader();
        s.init(this);

        tab = (TableLayout)findViewById(R.id.tab);
        TableRow tr = new TableRow(getApplicationContext());
        tr.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.WRAP_CONTENT));
        TextView textview = new TextView(getApplicationContext());
        textview.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT, TableRow.LayoutParams.WRAP_CONTENT, 1.0f));
        textview.setTextSize(20);
        textview.setTextColor(Color.parseColor("#0B0719"));
        textview.setText(HtmlCompat.fromHtml("Список уведомлений для озвучки:", HtmlCompat.FROM_HTML_MODE_LEGACY));
        tr.addView(textview);
        tab.addView(tr);
        LocalBroadcastManager.getInstance(this).registerReceiver(onNotice, new IntentFilter("Msg"));

        boolean isNotificationServiceRunning = isNotificationServiceRunning();
        if (!isNotificationServiceRunning)
            startActivity(new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS));

        if (!notif)
            applyStatusBar("Я просто озвучиваю уведомления.", 1002);
    }

    private void applyStatusBar(String iconTitle, int notificationId) {
        NotificationManager NotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

        NotificationChannel channel = new NotificationChannel("1001", "Канал озвучки",
                NotificationManager.IMPORTANCE_LOW);
        channel.setDescription("Озвучка голосом всех входящих уведомлений");
        channel.enableLights(true);
        channel.setLightColor(Color.RED);
        channel.enableVibration(false);
        NotificationManager.createNotificationChannel(channel);

        NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this, "1001")
                .setSmallIcon(R.drawable.ic_stat_headset_mic)
                .setContentTitle(iconTitle);
        Intent resultIntent = new Intent(this, MainActivity.class);
        PendingIntent resultPendingIntent = PendingIntent.getActivity(this, 0, resultIntent, PendingIntent.FLAG_UPDATE_CURRENT);
        mBuilder.setContentIntent(resultPendingIntent);
        Notification notification = mBuilder.build();
        notification.flags |= Notification.FLAG_NO_CLEAR|Notification.FLAG_ONGOING_EVENT;

        mNotifyMgr = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        mNotifyMgr.notify(notificationId, notification);
        notif = true;
    }

    private boolean isNotificationServiceRunning() {
        ContentResolver contentResolver = getContentResolver();
        String enabledNotificationListeners =
                Settings.Secure.getString(contentResolver, "enabled_notification_listeners");
        String packageName = getPackageName();
        return enabledNotificationListeners != null && enabledNotificationListeners.contains(packageName);
    }

    private BroadcastReceiver onNotice= new BroadcastReceiver() {

        @Override
        public void onReceive(Context context, Intent intent) {

            String pack = intent.getStringExtra("package");
            String title = intent.getStringExtra("title");
            String text = intent.getStringExtra("text");

            if (!text.equalsIgnoreCase(buf) && !text.matches(".*\\d+\\s+новых\\s+сообщени.*")) {
                TableRow tr = new TableRow(getApplicationContext());
                tr.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.WRAP_CONTENT));
                TextView textview = new TextView(getApplicationContext());
                textview.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT, TableRow.LayoutParams.WRAP_CONTENT, 1.0f));
                textview.setTextSize(20);
                textview.setTextColor(Color.parseColor("#0B0719"));
                textview.setText(HtmlCompat.fromHtml(pack + "<br><b>" + title + ": </b>" + text, HtmlCompat.FROM_HTML_MODE_LEGACY));
                tr.addView(textview);
                tab.addView(tr);

                s.speak(pack.replaceAll(".*\\.", "").replaceAll("\\..*", ""));
                s.pause(500);
                s.speak(title);
                s.pause(500);
                s.speak(text);

                buf = text;
            }

        }
    };

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (isFinishing())
            mNotifyMgr.cancel(1002);
    }

Правда пришлось немного попотеть с WhatsApp’ом, который зачем-то цепляет к каждому последующему уведомлению своё надоевшее «У вас 2-3-4 новых сообщения» и перестать озвучивать это.

Если Вам нужно, то вот мой activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <TableLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/tab" />
    </ScrollView>
</LinearLayout>

и AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="ru.webwalker.nreader" >

<application
    android:allowBackup="true"
    android:label="@string/app_name"
    android:icon="@mipmap/ic_launcher"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:theme="@style/AppTheme" >
    <activity
        android:screenOrientation="portrait"
        android:configChanges="orientation|keyboardHidden"
        android:name="ru.webwalker.nreader.MainActivity"
        android:launchMode="singleInstance"
        android:label="@string/app_name" >
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
    <service android:name="ru.webwalker.nreader.NotificationService"
        android:label="@string/app_name"
        android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
        <intent-filter>
            <action android:name="android.service.notification.NotificationListenerService" />
        </intent-filter>
    </service>
</application>

</manifest>

Из интересного здесь есть разве что строчка

android:launchMode="singleInstance"

которая не дает запускать новые «копии».

В итоге у меня получилось полностью рабочее приложение для моего смартфона на Андроид 9, которым я пользуюсь ежедневно. По-моему, для первого раза вышло неплохо.

P. S. Кстати, программирование — довольно простая задача, так как большинство проблем уже решены до нас и временами мне только и оставалось, что копировать некоторые куски кода, делая минимальные правки.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *