简体   繁体   中英

android Bound Service gets frozen when screen is off

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

  • Galaxy C7 (Oreo)
  • Galaxy Tab A3 (Nougat)
  • Galaxy A3 (Lollipop)

I investigated the issue thoroughly and followed the recommendation to switch to Foreground Service which solved my problem perfectly.

Tested with Lollipop , Nougat , Oreo .


Foreground Service aproach

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.

How to broadcast events from Service

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM