From 53d5eed67059e9e4f19b037f6c1b736d3e1efe4a Mon Sep 17 00:00:00 2001 From: Phillip Hsu Date: Sun, 5 Jun 2016 00:08:54 -0700 Subject: [PATCH] Fixed alarm not ringing because of RingtoneActivity being resumed on next launch and not created --- app/src/main/AndroidManifest.xml | 2 - .../clock2/ringtone/RingtoneActivity.java | 45 ++++++++++++++++++- .../clock2/ringtone/RingtoneService.java | 38 +++++++++++++++- 3 files changed, 80 insertions(+), 5 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fcb158f..d07d5c9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -24,8 +24,6 @@ android:label="@string/title_activity_ringtone" android:theme="@style/AppTheme.NoActionBar" android:excludeFromRecents="true" - android:finishOnTaskLaunch="true" - android:launchMode="singleTask" android:taskAffinity="com.philliphsu.clock2.RingtoneActivity"> diff --git a/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneActivity.java b/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneActivity.java index e915b81..ff34611 100644 --- a/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneActivity.java +++ b/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneActivity.java @@ -1,7 +1,11 @@ package com.philliphsu.clock2.ringtone; +import android.content.ComponentName; +import android.content.Context; import android.content.Intent; +import android.content.ServiceConnection; import android.os.Bundle; +import android.os.IBinder; import android.support.v7.app.AppCompatActivity; import android.view.View; import android.widget.Button; @@ -20,7 +24,8 @@ import static com.philliphsu.clock2.util.Preconditions.checkNotNull; * TODO: Make this abstract and make appropriate subclasses for Alarms and Timers. * TODO: Implement dismiss and extend logic here. */ -public class RingtoneActivity extends AppCompatActivity { +public class RingtoneActivity extends AppCompatActivity implements RingtoneService.RingtoneCallback { + private static final String TAG = "RingtoneActivity"; // Shared with RingtoneService public static final String EXTRA_ITEM_ID = "com.philliphsu.clock2.ringtone.extra.ITEM_ID"; @@ -37,12 +42,15 @@ public class RingtoneActivity extends AppCompatActivity { } mAlarm = checkNotNull(AlarmsRepository.getInstance(this).getItem(id)); + // TODO: If the upcoming alarm notification isn't present, verify other notifications aren't affected. + // This could be the case if we're starting a new instance of this activity after leaving the first launch. AlarmUtils.removeUpcomingAlarmNotification(this, mAlarm); // Play the ringtone Intent intent = new Intent(this, RingtoneService.class) .putExtra(EXTRA_ITEM_ID, mAlarm.id()); startService(intent); + bindService(intent, mConnection, Context.BIND_AUTO_CREATE); Button snooze = (Button) findViewById(R.id.btn_snooze); snooze.setOnClickListener(new View.OnClickListener() { @@ -84,6 +92,24 @@ public class RingtoneActivity extends AppCompatActivity { } } + @Override + public void onBackPressed() { + // Capture the back press and return. We want to limit the user's options for leaving + // this activity as much as possible. + } + + @Override + protected void onDestroy() { + super.onDestroy(); + unbindService(mConnection); + } + + @Override + public void onAutoSilence() { + // Service should have stopped itself by this point + finish(); + } + private void snooze() { mAlarm.snooze(1); // TODO: Read snooze duration from prefs AlarmUtils.scheduleAlarm(this, mAlarm); @@ -97,4 +123,19 @@ public class RingtoneActivity extends AppCompatActivity { // TODO: Do we need to cancel the PendingIntent and the alarm in AlarmManager? finish(); } -} + + private RingtoneService mBoundService; // TODO: Don't need? Only used locally in ServiceConnection. + + private ServiceConnection mConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + mBoundService = ((RingtoneService.RingtoneBinder) service).getService(); + mBoundService.setRingtoneCallback(RingtoneActivity.this); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + mBoundService = null; + } + }; +} \ No newline at end of file diff --git a/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneService.java b/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneService.java index 7e4cf46..0393444 100644 --- a/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneService.java +++ b/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneService.java @@ -9,6 +9,7 @@ import android.media.AudioManager; import android.media.Ringtone; import android.media.RingtoneManager; import android.net.Uri; +import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.support.v4.app.NotificationCompat; @@ -28,6 +29,17 @@ import static com.philliphsu.clock2.util.Preconditions.checkNotNull; * of the RingtoneService will be tied to that of its RingtoneActivity because users are not likely to * navigate away from the Activity without making an action. But if they do accidentally navigate away, * they have plenty of time to make the desired action via the notification. + * + * This is both a started and bound service. See https://developer.android.com/guide/components/bound-services.html#Lifecycle + * "... the service runs until the service stops itself with stopSelf() or another component + * calls stopService(), regardless of whether it is bound to any clients." + * The regardless phrase didn't work for me. I had to unbind in RingtoneActivity first before calling + * stopSelf() on this service for the ringtone to stop playing. + * TODO: Consider making this purely a bound service, so you don't have to bind/unbind AND start/stop + * manually. Instead of implementing onStartCommand() and calling startService(), you would write a public + * method that the activity calls to start playing the ringtone. When the activity calls its onDestroy(), it unbinds + * itself from this service, and the system will know to destroy this service instead of you manually + * calling stopSelf() or stopService(). */ public class RingtoneService extends Service { // TODO: abstract this, make subclasses private static final String TAG = "RingtoneService"; @@ -37,6 +49,7 @@ public class RingtoneService extends Service { // TODO: abstract this, make subc private Alarm mAlarm; private String mNormalRingTime; private boolean mAutoSilenced = false; + private RingtoneCallback mRingtoneCallback; // TODO: Using Handler for this is ill-suited? Alarm ringing could outlast the // application's life. Use AlarmManager API instead. private final Handler mSilenceHandler = new Handler(); @@ -44,9 +57,17 @@ public class RingtoneService extends Service { // TODO: abstract this, make subc @Override public void run() { mAutoSilenced = true; + if (mRingtoneCallback != null) { + // Finish the activity, which fires onDestroy() and then unbinds itself from this service. + // All clients must be unbound before stopSelf() (and stopService()?) will succeed. + // See https://developer.android.com/guide/components/bound-services.html#Lifecycle + // Figure 1 regarding the lifecycle of started and bound services. + mRingtoneCallback.onAutoSilence(); + } stopSelf(); } }; + private final IBinder mBinder = new RingtoneBinder(); // TODO: Apply the setting for "Silence after" here by using an AlarmManager to // schedule an alarm in the future to stop this service, and also update the foreground @@ -121,7 +142,22 @@ public class RingtoneService extends Service { // TODO: abstract this, make subc @Override public IBinder onBind(Intent intent) { - return null; // Binding to this service is not supported + return mBinder; + } + + public void setRingtoneCallback(RingtoneCallback callback) { + mRingtoneCallback = callback; + } + + // Needed so clients can get the Service instance and e.g. call setRingtoneCallback(). + public class RingtoneBinder extends Binder { + RingtoneService getService() { + return RingtoneService.this; + } + } + + public interface RingtoneCallback { + void onAutoSilence(); } private void scheduleAutoSilence() {