I created a very basic Service for dictionary playback using TTS ( see the complete code below ) and running into the same issue on all my 3 android devices (android versions 5, 7 and 8).
Gist: The app plays vocabulary entries, definitions and examples. Between each of them the app takes pause.
Symptoms:
The issue is happening mostly when I use 8 seconds for pause and the app is in the background mode (the screen is turned off). The playback simply gets frozen.
Sometimes the playback continues on its own with screen turned off after a lengthy pause sometimes being up to 20 - 30 minutes or even longer ( but then the next entry is played after a very lenghty pause too, provided that we haven't activated screen ). Could be some other process partly waking the phone?
Also, playback continues straight after I pressed Power button and screen turns on.
Debug info:
I was reckoning to press pause in Visual Studio after the app got frozen in order to see which bit of code is the cause - unfortunately the debugger seems to keep the device awake and this issue is extremely difficult to reveal .
In order to prevent my app from being frozen I acquire Partial WakeLock in my service (but this still doesn't help, even though app manifest contains permission for WAKE_LOCK
)
private void AcquireWakeLock(MainActivity activity)
{
var mgr = (PowerManager)activity.ApplicationContext.GetSystemService(Context.PowerService);
WakeLock = mgr.NewWakeLock(WakeLockFlags.Partial, "myWakeLock");
WakeLock.Acquire();
}
My app also has Play/Pause button and I use TaskCompletionSource
for the app to wait until I resume playback
public async Task PlayPause(bool isChecked, MainActivity mainActivity)
{
if (isChecked)
{
ReleaseWakeLock();
AppSuspended = new TaskCompletionSource<bool>();
Tts.Stop();
}
else
{
AcquireWakeLock(mainActivity);
AppSuspended.TrySetResult(true);
}
}
Then, before each next word/phrase is about to be played I use the following code for my app to wait for my resuming playback
await AppSuspended.Task;
Complete code
[Service(Name = "com.my_app.service.PlaybackService")]
public class PlaybackService : Service, TextToSpeech.IOnInitListener, TextToSpeech.IOnUtteranceCompletedListener
{
public IBinder Binder { get; private set; }
private Java.Util.Locale Lang;
private bool Playing;
private int EntryIndex;
private int DefinitionIndex;
private DictionaryDto Dictionary;
private EntryDto CurrentEntry;
private DefinitionDto CurrentDefinition;
private TaskCompletionSource<bool> AppSuspended;
protected TextToSpeech Tts;
private TaskCompletionSource<bool> PlaybackFinished;
private WakeLock WakeLock;
public override void OnCreate()
{
base.OnCreate();
Tts = new TextToSpeech(this, this);
Lang = Tts.DefaultLanguage;
AppSuspended = new TaskCompletionSource<bool>();
AppSuspended.TrySetResult(true);
}
public override IBinder OnBind(Intent intent)
{
Binder = new PlaybackBinder(this);
return Binder;
}
public override bool OnUnbind(Intent intent)
{
return base.OnUnbind(intent);
}
public override void OnDestroy()
{
Binder = null;
base.OnDestroy();
}
void TextToSpeech.IOnUtteranceCompletedListener.OnUtteranceCompleted(string utteranceId)
{
if (utteranceId.Equals("PlaybackFinished")) { PlaybackFinished.TrySetResult(true); }
}
void TextToSpeech.IOnInitListener.OnInit(OperationResult status)
{
// if we get an error, default to the default language
if (status == OperationResult.Error)
Tts.SetLanguage(Java.Util.Locale.Default);
// if the listener is ok, set the lang
if (status == OperationResult.Success)
{
Tts.SetLanguage(Lang);
Tts.SetOnUtteranceCompletedListener(this);
}
}
public async Task Play(string text)
{
Dictionary<string, string> myHashRender = new Dictionary<string, string>();
myHashRender.Add(TextToSpeech.Engine.KeyParamUtteranceId, "PlaybackFinished");
PlaybackFinished = new TaskCompletionSource<bool>();
Tts.Speak(text, QueueMode.Flush, myHashRender);
await PlaybackFinished.Task;
}
public async Task PlaySilence(long ms)
{
Dictionary<string, string> myHashRender = new Dictionary<string, string>();
myHashRender.Add(TextToSpeech.Engine.KeyParamUtteranceId, "PlaybackFinished");
PlaybackFinished = new TaskCompletionSource<bool>();
Tts.PlaySilence(ms, QueueMode.Flush, myHashRender);
await PlaybackFinished.Task;
}
private async Task PlayDictionary(MainActivity activity)
{
EntryIndex = 0;
for (; EntryIndex < Dictionary.Entries.Count;)
{
CurrentEntry = Dictionary.Entries.ElementAt(EntryIndex);
await AppSuspended.Task;
if (!Playing) { return; }
if (!string.IsNullOrEmpty(CurrentEntry.Text))
{
await AppSuspended.Task;
if (!Playing) { return; }
await Play(CurrentEntry.Text);
}
DefinitionIndex = 0;
for (; DefinitionIndex < CurrentEntry.Definitions.Count();)
{
CurrentDefinition = CurrentEntry.Definitions.ElementAt(DefinitionIndex);
await PlayDefinition();
await PlayExamples();
DefinitionIndex++;
}
if (Playing)
{
DefinitionIndex++;
}
EntryIndex++;
}
}
private async Task PlayExamples()
{
if (!Playing) { return; }
foreach (var example in CurrentDefinition.Examples)
{
if (!string.IsNullOrEmpty(example))
{
await AppSuspended.Task;
if (!Playing) { return; }
await Play(example);
if (Playing)
{
await PlaySilence((long)TimeSpan.FromSeconds(8).TotalMilliseconds);
}
}
}
}
private async Task PlayDefinition()
{
if (!Playing) { return; }
if (!string.IsNullOrEmpty(CurrentEntry.Definitions.ElementAt(DefinitionIndex).Text))
{
await AppSuspended.Task;
if (!Playing) { return; }
await PlayDefinitionText();
if (Playing)
{
await PlaySilence((long)TimeSpan.FromSeconds(7).TotalMilliseconds);
}
}
}
private async Task PlayDefinitionText()
{
await AppSuspended.Task;
await Play($"{CurrentEntry.Definitions.ElementAt(DefinitionIndex).Text}");
}
private void ReleaseWakeLock()
{
if (WakeLock != null)
{
WakeLock.Release();
}
}
private void AcquireWakeLock(MainActivity activity)
{
var mgr = (PowerManager)activity.ApplicationContext.GetSystemService(Context.PowerService);
WakeLock = mgr.NewWakeLock(WakeLockFlags.Partial, "myWakeLock");
WakeLock.Acquire();
}
public async Task PlayPause(bool isChecked, MainActivity mainActivity)
{
if (isChecked)
{
ReleaseWakeLock();
AppSuspended = new TaskCompletionSource<bool>();
Tts.Stop();
}
else
{
AcquireWakeLock(mainActivity);
AppSuspended.TrySetResult(true);
}
}
}
Additional info:
The issue happens on all of my devices
I investigated the issue thoroughly and followed the recommendation to switch to Foreground Service which solved my problem perfectly.
Tested with Lollipop , Nougat , Oreo .
Put the following method in your MainActivity
class
public void StartForegroundServiceSafely(Intent intent)
{
if (Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.O)
{
StartForegroundService(intent);
}
else
{
StartService(intent);
}
}
You then start your service via Intent
public void PlayFromFile(Android.Net.Uri uri)
{
AcquireWakeLock();
Intent startIntent = new Intent(this, typeof(PlaybackService));
startIntent.SetAction(PlaybackConsts.Start);
startIntent.PutExtra("uri", uri.ToString());
StartForegroundServiceSafely(startIntent);
}
Implement OnStartCommand
method in your service
public class PlaybackService : Service, TextToSpeech.IOnInitListener, TextToSpeech.IOnUtteranceCompletedListener
[return: GeneratedEnum]
public override StartCommandResult OnStartCommand(Intent intent, [GeneratedEnum] StartCommandFlags flags, int startId)
{
if (intent.Action.Equals(PlaybackConsts.Start))
{
var notification =
new Notification.Builder(this)
.SetContentTitle(Resources.GetString(Resource.String.ApplicationName))
.SetContentText("HELLO WORLD")
.SetOngoing(true)
.Build();
StartForeground(SERVICE_RUNNING_NOTIFICATION_ID, notification);
}
if (intent.Action.Equals(PlaybackConsts.Start))
{
var uri = Android.Net.Uri.Parse(intent.GetStringExtra("uri"));
var content = MiscellaneousHelper.GetTextFromStream(ContentResolver.OpenInputStream(uri));
Dictionary = DictionaryFactory.Get(content);
Playing = true;
Task.Factory.StartNew(async () =>
{
await PlayDictionary();
});
}
if (intent.Action.Equals(PlaybackConsts.PlayPause))
{
bool isChecked = intent.GetBooleanExtra("isChecked", false);
PlayPause(isChecked);
}
if (intent.Action.Equals(PlaybackConsts.NextEntry))
{
NextEntry();
}
if (intent.Action.Equals(PlaybackConsts.PrevEntry))
{
PrevEntry();
}
if (intent.Action.Equals(PlaybackConsts.Stop))
{
Task.Factory.StartNew(async () =>
{
await Stop();
});
StopForeground(true);
StopSelf();
}
return StartCommandResult.Sticky;
}
From the code above we've learned how to trigger service's functionality in OnStartCommand
method.
Define your BroadcastReceiver
[BroadcastReceiver(Enabled = true, Exported = false)]
public class PlaybackBroadcastReceiver : BroadcastReceiver
{
public override void OnReceive(Context context, Intent intent)
{
var activity = MainActivity.GetInstance(); // if you need your activity here, see further code below
if (intent.Action == "renderEntry")
{
string entryHtml = intent.GetStringExtra("html");
// omitting code to keep example concise
}
}
}
Declare receiver field in your MainActivity
class.
Also encase you need your activity in BroadcastReceiver
class you can declare GetInstance
method (singleton approach).
public class MainActivity : AppCompatActivity
{
PlaybackBroadcastReceiver receiver;
protected DrawerLayout drawerLayout;
protected NavigationView navigationView;
protected WakeLock WakeLock;
private static MainActivity instance;
public static MainActivity GetInstance()
{
return instance;
}
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
receiver = new PlaybackBroadcastReceiver();
instance = this;
}
protected override void OnStart()
{
base.OnStart();
RegisterReceiver(receiver, new IntentFilter("renderEntry"));
}
In order to unregister receiver use the following line:
UnregisterReceiver(receiver);
Broadcasting events from service
In your service you must also use intent
private void SendRenderEntryBroadcast(EntryDto entry)
{
Intent intent = new Intent("renderEntry");
intent.PutExtra("html", GetEntryHtml(entry));
SendBroadcast(intent);
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.