diff --git a/app/src/amazon/AndroidManifest.xml b/app/src/amazon/AndroidManifest.xml index a24555a517..b37d0987e7 100644 --- a/app/src/amazon/AndroidManifest.xml +++ b/app/src/amazon/AndroidManifest.xml @@ -15,6 +15,7 @@ + @@ -630,6 +631,11 @@ android:name=".ServiceUI" android:exported="false" /> + + + @@ -637,6 +638,11 @@ android:name=".ServiceUI" android:exported="false" /> + + + @@ -636,6 +637,11 @@ android:name=".ServiceUI" android:exported="false" /> + + + @@ -636,6 +637,11 @@ android:name=".ServiceUI" android:exported="false" /> + + + @@ -631,6 +632,11 @@ android:name=".ServiceUI" android:exported="false" /> + + . + + Copyright 2018-2024 by Marcel Bokhorst (M66B) +*/ + +import android.app.Notification; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; +import android.os.OperationCanceledException; +import android.speech.tts.TextToSpeech; +import android.speech.tts.UtteranceProgressListener; + +import androidx.annotation.Nullable; +import androidx.core.app.NotificationCompat; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public class ServiceTTS extends ServiceBase { + private Integer status = null; + private TextToSpeech instance = null; + private final List queue = new ArrayList<>(); + private final Object lock = new Object(); + + static final String EXTRA_FLUSH = "flush"; + static final String EXTRA_TEXT = "text"; + static final String EXTRA_LANGUAGE = "language"; + static final String EXTRA_UTTERANCE_ID = "utterance"; + + static final String ACTION_TTS_COMPLETED = BuildConfig.APPLICATION_ID + ".TTS"; + + @Override + public void onCreate() { + Log.i("Service TTS create"); + super.onCreate(); + try { + startForeground(NotificationHelper.NOTIFICATION_TTS, getNotification()); + } catch (Throwable ex) { + if (Helper.isPlayStoreInstall()) + Log.i(ex); + else + Log.e(ex); + } + } + + @Override + public void onTimeout(int startId) { + String msg = "onTimeout" + + " class=" + this.getClass().getName() + + " ignoring=" + Helper.isIgnoringOptimizations(this); + Log.e(new Throwable(msg)); + EntityLog.log(this, EntityLog.Type.Debug3, msg); + stopSelf(); + } + + @Override + public void onDestroy() { + Log.i("Service TTS destroy"); + stopForeground(true); + super.onDestroy(); + CoalMine.watch(this, this.getClass().getName() + "#onDestroy"); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + EntityLog.log(this, "Service TTS intent=" + intent); + Log.logExtras(intent); + + super.onStartCommand(intent, flags, startId); + + try { + startForeground(NotificationHelper.NOTIFICATION_TTS, getNotification()); + } catch (Throwable ex) { + if (Helper.isPlayStoreInstall()) + Log.i(ex); + else + Log.e(ex); + } + + if (intent == null) + return START_NOT_STICKY; + + onTts(intent); + + return START_NOT_STICKY; + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return null; + } + + private Notification getNotification() { + NotificationCompat.Builder builder = + new NotificationCompat.Builder(this, "service") + .setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_DEFAULT) + .setSmallIcon(R.drawable.twotone_play_arrow_24) + .setContentTitle(getString(R.string.title_rule_tts)) + .setContentIntent(getPendingIntent(this)) + .setAutoCancel(false) + .setShowWhen(false) + .setDefaults(0) // disable sound on pre Android 8 + .setPriority(NotificationCompat.PRIORITY_MIN) + .setCategory(NotificationCompat.CATEGORY_SERVICE) + .setVisibility(NotificationCompat.VISIBILITY_SECRET) + .setLocalOnly(true) + .setOngoing(true); + + Notification notification = builder.build(); + notification.flags |= Notification.FLAG_NO_CLEAR; + return notification; + } + + private static PendingIntent getPendingIntent(Context context) { + Intent view = new Intent(context, ActivityView.class); + view.setAction("unified"); + view.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + return PendingIntentCompat.getActivity( + context, ActivityView.PI_UNIFIED, view, PendingIntent.FLAG_UPDATE_CURRENT); + } + + void onTts(Intent intent) { + final boolean flush = intent.getBooleanExtra(EXTRA_FLUSH, false); + final String text = intent.getStringExtra(EXTRA_TEXT); + final String language = intent.getStringExtra(EXTRA_LANGUAGE); + final String utteranceId = intent.getStringExtra(EXTRA_UTTERANCE_ID); + + final Locale locale = (language == null ? Locale.getDefault() : new Locale(language)); + + final Runnable speak = new RunnableEx("tts") { + @Override + public void delegate() { + boolean available = (instance.setLanguage(locale) >= 0); + EntityLog.log(ServiceTTS.this, "TTS queued" + + " language=" + locale + + " available=" + available + + " utterance=" + utteranceId + + " text=" + text); + int error = instance.speak(text, flush ? TextToSpeech.QUEUE_FLUSH : TextToSpeech.QUEUE_ADD, null, utteranceId); + if (error != TextToSpeech.SUCCESS) + throw new OperationCanceledException("TTS error=" + error); + } + }; + + final LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(ServiceTTS.this); + + synchronized (lock) { + if (status == null) { + queue.add(speak); + if (instance == null) + try { + Log.i("TTS init"); + instance = new TextToSpeech(ServiceTTS.this, new TextToSpeech.OnInitListener() { + @Override + public void onInit(int initStatus) { + instance.setOnUtteranceProgressListener(new UtteranceProgressListener() { + @Override + public void onStart(String utteranceId) { + Log.i("TTS start=" + utteranceId); + } + + @Override + public void onDone(String utteranceId) { + Log.i("TTS done=" + utteranceId); + report(utteranceId); + synchronized (lock) { + if (!instance.isSpeaking()) + try { + Log.i("TTS shutdown"); + instance.shutdown(); + } catch (Throwable ex) { + Log.e(ex); + } finally { + status = null; + instance = null; + ServiceTTS.this.stopSelf(); + } + } + } + + @Override + public void onError(String utteranceId) { + Log.i("TTS error=" + utteranceId); + report(utteranceId); + } + + private void report(String utteranceId) { + lbm.sendBroadcast(new Intent(ACTION_TTS_COMPLETED) + .putExtra(EXTRA_UTTERANCE_ID, utteranceId)); + } + }); + + synchronized (lock) { + status = initStatus; + Log.i("TTS status=" + status + " queued=" + queue.size()); + if (status == TextToSpeech.SUCCESS) + for (Runnable speak : queue) + speak.run(); + queue.clear(); + } + } + }); + } catch (Throwable ex) { + Log.e(ex); + status = TextToSpeech.ERROR; + } + } else if (status == TextToSpeech.SUCCESS) + speak.run(); + } + } + + static int getMaxTextSize() { + return TextToSpeech.getMaxSpeechInputLength(); + } +} diff --git a/app/src/main/java/eu/faircode/email/TTSHelper.java b/app/src/main/java/eu/faircode/email/TTSHelper.java deleted file mode 100644 index d093bdd4cb..0000000000 --- a/app/src/main/java/eu/faircode/email/TTSHelper.java +++ /dev/null @@ -1,159 +0,0 @@ -package eu.faircode.email; - -/* - This file is part of FairEmail. - - FairEmail is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - FairEmail is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with FairEmail. If not, see . - - Copyright 2018-2024 by Marcel Bokhorst (M66B) -*/ - -import android.content.Context; -import android.os.OperationCanceledException; -import android.os.PowerManager; -import android.speech.tts.TextToSpeech; -import android.speech.tts.UtteranceProgressListener; - -import androidx.annotation.NonNull; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; - -public class TTSHelper { - private static Integer status = null; - private static TextToSpeech instance = null; - private static PowerManager.WakeLock wl = null; - private static final List queue = new ArrayList<>(); - private static final Map listeners = new HashMap<>(); - private static final Object lock = new Object(); - - private static final long MAX_WAKELOCK_DURATION = 3 * 60 * 1000L; // milliseconds - - // https://developer.android.com/reference/android/speech/tts/TextToSpeech - // https://android-developers.googleblog.com/2009/09/introduction-to-text-to-speech-in.html - - static void speak( - @NonNull final Context context, - @NonNull final String utteranceId, - @NonNull final String text, - final String language, - final boolean flush, - final Runnable listener) { - - if (wl == null) { - PowerManager pm = Helper.getSystemService(context, PowerManager.class); - wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, BuildConfig.APPLICATION_ID + ":tts"); - } - - Locale locale = (language == null ? Locale.getDefault() : new Locale(language)); - - final Runnable speak = new Runnable() { - @Override - public void run() { - try { - boolean available = (instance.setLanguage(locale) >= 0); - EntityLog.log(context, "TTS queued" + - " language=" + locale + - " available=" + available + - " utterance=" + utteranceId + - " text=" + text); - int error = instance.speak(text, flush ? TextToSpeech.QUEUE_FLUSH : TextToSpeech.QUEUE_ADD, null, utteranceId); - if (error != TextToSpeech.SUCCESS) - throw new OperationCanceledException("TTS error=" + error); - } catch (Throwable ex) { - Log.e(ex); - } - } - }; - - synchronized (lock) { - if (listener != null) - listeners.put(utteranceId, listener); - - if (status == null) { - queue.add(speak); - if (instance == null) - try { - Log.i("TTS init"); - instance = new TextToSpeech(context, new TextToSpeech.OnInitListener() { - @Override - public void onInit(int initStatus) { - instance.setOnUtteranceProgressListener(new UtteranceProgressListener() { - @Override - public void onStart(String utteranceId) { - Log.i("TTS start=" + utteranceId); - } - - @Override - public void onDone(String utteranceId) { - Log.i("TTS done=" + utteranceId); - report(utteranceId); - synchronized (lock) { - if (queue.isEmpty()) - try { - Log.i("TTS shutdown"); - instance.shutdown(); - } catch (Throwable ex) { - Log.e(ex); - } finally { - status = null; - instance = null; - wl.release(); - } - } - } - - @Override - public void onError(String utteranceId) { - Log.i("TTS error=" + utteranceId); - report(utteranceId); - } - - private void report(String utteranceId) { - synchronized (lock) { - Runnable listener = listeners.remove(utteranceId); - if (listener != null) - ApplicationEx.getMainHandler().post(listener); - } - } - }); - - synchronized (lock) { - status = initStatus; - boolean ok = (status == TextToSpeech.SUCCESS); - Log.i("TTS status=" + status + " ok=" + ok + " queued=" + queue.size()); - if (ok) - for (Runnable speak : queue) - speak.run(); - queue.clear(); - } - } - }); - wl.acquire(MAX_WAKELOCK_DURATION); - } catch (Throwable ex) { - Log.e(ex); - status = TextToSpeech.ERROR; - } - } else if (status == TextToSpeech.SUCCESS) - speak.run(); - } - } - - static int getMaxTextSize() { - return TextToSpeech.getMaxSpeechInputLength(); - } -} diff --git a/app/src/play/AndroidManifest.xml b/app/src/play/AndroidManifest.xml index 2ff3995e69..a9cea48f63 100644 --- a/app/src/play/AndroidManifest.xml +++ b/app/src/play/AndroidManifest.xml @@ -15,6 +15,7 @@ + @@ -630,6 +631,11 @@ android:name=".ServiceUI" android:exported="false" /> + +