Created TimerNotificationService, TimesUpActivity. Schedule alarms with AlarmManager for timers.
This commit is contained in:
parent
6f8d22f15b
commit
9e4369282d
@ -21,13 +21,13 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<!--<activity
|
||||||
android:name=".ringtone.RingtoneActivity"
|
android:name=".ringtone.RingtoneActivity"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:label="@string/title_activity_ringtone"
|
android:label="@string/title_activity_ringtone"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:taskAffinity="com.philliphsu.clock2.RingtoneActivity">
|
android:taskAffinity="com.philliphsu.clock2.RingtoneActivity">
|
||||||
</activity>
|
</activity>-->
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".ringtone.RingtoneService"
|
android:name=".ringtone.RingtoneService"
|
||||||
@ -79,14 +79,33 @@
|
|||||||
android:exported="false">
|
android:exported="false">
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<activity android:name=".edittimer.EditTimerActivity"
|
<activity
|
||||||
android:label="@string/title_activity_create_timer"
|
android:name=".edittimer.EditTimerActivity"
|
||||||
android:parentActivityName=".MainActivity"
|
android:label="@string/title_activity_create_timer"
|
||||||
android:windowSoftInputMode="adjustNothing">
|
android:parentActivityName=".MainActivity"
|
||||||
|
android:windowSoftInputMode="adjustNothing">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="com.philliphsu.clock2.MainActivity"/>
|
android:value="com.philliphsu.clock2.MainActivity"/>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".timers.TimerNotificationService"
|
||||||
|
android:exported="false">
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<activity android:name=".timers.TimesUpActivity"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:label="@string/title_activity_ringtone"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:taskAffinity="com.philliphsu.clock2.RingtoneActivity">
|
||||||
|
</activity>
|
||||||
|
<activity android:name=".alarms.AlarmActivity"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:label="@string/title_activity_ringtone"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:taskAffinity="com.philliphsu.clock2.RingtoneActivity">
|
||||||
|
</activity>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@ -203,8 +203,9 @@ public abstract class Alarm extends ObjectWithId implements JsonSerializable, Pa
|
|||||||
return !ignoreUpcomingRingTime && ringsIn() <= TimeUnit.HOURS.toMillis(hours);
|
return !ignoreUpcomingRingTime && ringsIn() <= TimeUnit.HOURS.toMillis(hours);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Rename to getIntId() so usages refer to ObjectWithId#getIntId(), then delete this method.
|
||||||
public int intId() {
|
public int intId() {
|
||||||
return (int) getId();
|
return getIntId();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Remove method signature from JsonSerializable interface.
|
// TODO: Remove method signature from JsonSerializable interface.
|
||||||
|
|||||||
@ -1,14 +1,22 @@
|
|||||||
package com.philliphsu.clock2;
|
package com.philliphsu.clock2;
|
||||||
|
|
||||||
|
import android.app.AlarmManager;
|
||||||
|
import android.app.PendingIntent;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.SystemClock;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import com.philliphsu.clock2.alarms.ScrollHandler;
|
import com.philliphsu.clock2.alarms.ScrollHandler;
|
||||||
import com.philliphsu.clock2.model.TimersTableManager;
|
import com.philliphsu.clock2.model.TimersTableManager;
|
||||||
|
import com.philliphsu.clock2.timers.TimerNotificationService;
|
||||||
|
import com.philliphsu.clock2.timers.TimesUpActivity;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by Phillip Hsu on 8/2/2016.
|
* Created by Phillip Hsu on 8/2/2016.
|
||||||
*/
|
*/
|
||||||
public final class AsyncTimersTableUpdateHandler extends AsyncDatabaseTableUpdateHandler<Timer, TimersTableManager> {
|
public final class AsyncTimersTableUpdateHandler extends AsyncDatabaseTableUpdateHandler<Timer, TimersTableManager> {
|
||||||
|
private static final String TAG = "TimersTableUpdater"; // TAG max 23 chars
|
||||||
|
|
||||||
public AsyncTimersTableUpdateHandler(Context context, ScrollHandler scrollHandler) {
|
public AsyncTimersTableUpdateHandler(Context context, ScrollHandler scrollHandler) {
|
||||||
super(context, scrollHandler);
|
super(context, scrollHandler);
|
||||||
@ -21,16 +29,55 @@ public final class AsyncTimersTableUpdateHandler extends AsyncDatabaseTableUpdat
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPostAsyncDelete(Integer result, Timer timer) {
|
protected void onPostAsyncDelete(Integer result, Timer timer) {
|
||||||
// TODO: Cancel the alarm scheduled for this timer
|
cancelAlarm(timer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPostAsyncInsert(Long result, Timer timer) {
|
protected void onPostAsyncInsert(Long result, Timer timer) {
|
||||||
// TODO: if running, schedule alarm
|
Log.d(TAG, "onPostAsyncInsert()");
|
||||||
|
if (timer.isRunning()) {
|
||||||
|
Log.d(TAG, "Scheduling alarm for timer launch");
|
||||||
|
scheduleAlarm(timer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPostAsyncUpdate(Long result, Timer timer) {
|
protected void onPostAsyncUpdate(Long result, Timer timer) {
|
||||||
// TODO: cancel and reschedule
|
if (timer.isRunning()) {
|
||||||
|
// We don't need to cancel the previous alarm, because this one
|
||||||
|
// will remove and replace it.
|
||||||
|
scheduleAlarm(timer);
|
||||||
|
} else {
|
||||||
|
cancelAlarm(timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Consider changing to just a long id param
|
||||||
|
private PendingIntent createTimesUpIntent(Timer timer) {
|
||||||
|
Intent intent = new Intent(getContext(), TimesUpActivity.class);
|
||||||
|
// intent.putExtra(TimesUpActivity.EXTRA_ITEM_ID, timer.getId());
|
||||||
|
// There's no point to determining whether to retrieve a previous instance, because
|
||||||
|
// we chose to ignore it since we had issues with NPEs. TODO: Perhaps these issues
|
||||||
|
// were caused by you using the same reference variable for every Intent/PI that
|
||||||
|
// needed to be recreated, and you reassigning the reference each time you were done with
|
||||||
|
// one of them, which leaves the one before unreferenced and hence eligible for GC.
|
||||||
|
return PendingIntent.getActivity(getContext(), timer.getIntId(), intent, PendingIntent.FLAG_CANCEL_CURRENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scheduleAlarm(Timer timer) {
|
||||||
|
Log.d(TAG, String.format("now = %d, endTime = %d", SystemClock.elapsedRealtime(), timer.endTime()));
|
||||||
|
AlarmManager am = (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE);
|
||||||
|
am.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, timer.endTime(), createTimesUpIntent(timer));
|
||||||
|
TimerNotificationService.showNotification(getContext(), timer.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cancelAlarm(Timer timer) {
|
||||||
|
// Cancel the alarm scheduled. If one was never scheduled, does nothing.
|
||||||
|
AlarmManager am = (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE);
|
||||||
|
PendingIntent pi = createTimesUpIntent(timer);
|
||||||
|
// Now can't be null
|
||||||
|
am.cancel(pi);
|
||||||
|
pi.cancel();
|
||||||
|
TimerNotificationService.cancelNotification(getContext(), timer.getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,78 @@
|
|||||||
|
package com.philliphsu.clock2.alarms;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.v4.content.Loader;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Button;
|
||||||
|
|
||||||
|
import com.philliphsu.clock2.Alarm;
|
||||||
|
import com.philliphsu.clock2.R;
|
||||||
|
import com.philliphsu.clock2.model.AlarmLoader;
|
||||||
|
import com.philliphsu.clock2.ringtone.RingtoneActivity;
|
||||||
|
import com.philliphsu.clock2.util.AlarmController;
|
||||||
|
|
||||||
|
public class AlarmActivity extends RingtoneActivity<Alarm> {
|
||||||
|
|
||||||
|
private AlarmController mAlarmController;
|
||||||
|
// TODO: Write a getter method instead in the base class?
|
||||||
|
private Alarm mAlarm;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
mAlarmController = new AlarmController(this, null);
|
||||||
|
// TODO: Butterknife binding
|
||||||
|
Button snooze = (Button) findViewById(R.id.btn_snooze);
|
||||||
|
snooze.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
snooze();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Button dismiss = (Button) findViewById(R.id.btn_dismiss);
|
||||||
|
dismiss.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
dismiss();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Loader<Alarm> onCreateLoader(long id) {
|
||||||
|
return new AlarmLoader(this, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoadFinished(Loader<Alarm> loader, Alarm data) {
|
||||||
|
super.onLoadFinished(loader, data);
|
||||||
|
mAlarm = data;
|
||||||
|
if (data != null) {
|
||||||
|
// 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.
|
||||||
|
mAlarmController.removeUpcomingAlarmNotification(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int layoutResource() {
|
||||||
|
return R.layout.activity_ringtone;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void snooze() {
|
||||||
|
if (mAlarm != null) {
|
||||||
|
mAlarmController.snoozeAlarm(mAlarm);
|
||||||
|
}
|
||||||
|
// Can't call dismiss() because we don't want to also call cancelAlarm()! Why? For example,
|
||||||
|
// we don't want the alarm, if it has no recurrence, to be turned off right now.
|
||||||
|
stopAndFinish();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void dismiss() {
|
||||||
|
if (mAlarm != null) {
|
||||||
|
// TODO do we really need to cancel the intent and alarm?
|
||||||
|
mAlarmController.cancelAlarm(mAlarm, false);
|
||||||
|
}
|
||||||
|
stopAndFinish();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -28,8 +28,8 @@ import com.philliphsu.clock2.BaseActivity;
|
|||||||
import com.philliphsu.clock2.DaysOfWeek;
|
import com.philliphsu.clock2.DaysOfWeek;
|
||||||
import com.philliphsu.clock2.R;
|
import com.philliphsu.clock2.R;
|
||||||
import com.philliphsu.clock2.SharedPreferencesHelper;
|
import com.philliphsu.clock2.SharedPreferencesHelper;
|
||||||
|
import com.philliphsu.clock2.alarms.AlarmActivity;
|
||||||
import com.philliphsu.clock2.model.AlarmLoader;
|
import com.philliphsu.clock2.model.AlarmLoader;
|
||||||
import com.philliphsu.clock2.ringtone.RingtoneActivity;
|
|
||||||
import com.philliphsu.clock2.util.AlarmController;
|
import com.philliphsu.clock2.util.AlarmController;
|
||||||
import com.philliphsu.clock2.util.AlarmUtils;
|
import com.philliphsu.clock2.util.AlarmUtils;
|
||||||
import com.philliphsu.clock2.util.DateFormatUtils;
|
import com.philliphsu.clock2.util.DateFormatUtils;
|
||||||
@ -543,8 +543,8 @@ public class EditAlarmActivity extends BaseActivity implements
|
|||||||
@Override
|
@Override
|
||||||
public void cancelAlarm(Alarm alarm, boolean showToast) {
|
public void cancelAlarm(Alarm alarm, boolean showToast) {
|
||||||
new AlarmController(this, mMainContent).cancelAlarm(alarm, true);
|
new AlarmController(this, mMainContent).cancelAlarm(alarm, true);
|
||||||
if (RingtoneActivity.isAlive()) {
|
if (AlarmActivity.isAlive()) {
|
||||||
LocalBroadcastHelper.sendBroadcast(this, RingtoneActivity.ACTION_FINISH);
|
LocalBroadcastHelper.sendBroadcast(this, AlarmActivity.ACTION_FINISH);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,8 @@ public class AlarmLoader extends DataLoader<Alarm> {
|
|||||||
|
|
||||||
private long mAlarmId;
|
private long mAlarmId;
|
||||||
|
|
||||||
|
// TODO: Consider writing a super ctor that has the id param, so
|
||||||
|
// subclasses don't need to write their own.
|
||||||
public AlarmLoader(Context context, long alarmId) {
|
public AlarmLoader(Context context, long alarmId) {
|
||||||
super(context);
|
super(context);
|
||||||
mAlarmId = alarmId;
|
mAlarmId = alarmId;
|
||||||
|
|||||||
@ -6,6 +6,9 @@ import android.support.v4.content.AsyncTaskLoader;
|
|||||||
/**
|
/**
|
||||||
* Created by Phillip Hsu on 6/30/2016.
|
* Created by Phillip Hsu on 6/30/2016.
|
||||||
*/
|
*/
|
||||||
|
// TODO: Consider adding a DatabaseTableManager type param, so we can then
|
||||||
|
// implement loadInBackground for subclasses. You would, however, need to write
|
||||||
|
// an abstract method getTableManager() that subclasses implement for us.
|
||||||
public abstract class DataLoader<D> extends AsyncTaskLoader<D> {
|
public abstract class DataLoader<D> extends AsyncTaskLoader<D> {
|
||||||
|
|
||||||
private D mData;
|
private D mData;
|
||||||
|
|||||||
@ -15,4 +15,8 @@ public abstract class ObjectWithId {
|
|||||||
public final void setId(long id) {
|
public final void setId(long id) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final int getIntId() {
|
||||||
|
return (int) id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,25 @@
|
|||||||
|
package com.philliphsu.clock2.model;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import com.philliphsu.clock2.Timer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by Phillip Hsu on 8/3/2016.
|
||||||
|
*/
|
||||||
|
public class TimerLoader extends DataLoader<Timer> {
|
||||||
|
|
||||||
|
private long mTimerId;
|
||||||
|
|
||||||
|
// TODO: Consider writing a super ctor that has the id param, so
|
||||||
|
// subclasses don't need to write their own.
|
||||||
|
public TimerLoader(Context context, long timerId) {
|
||||||
|
super(context);
|
||||||
|
mTimerId = timerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Timer loadInBackground() {
|
||||||
|
return new TimersTableManager(getContext()).queryItem(mTimerId).getItem();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,26 +4,20 @@ import android.content.BroadcastReceiver;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.support.annotation.LayoutRes;
|
||||||
|
import android.support.v4.app.LoaderManager.LoaderCallbacks;
|
||||||
import android.support.v4.content.Loader;
|
import android.support.v4.content.Loader;
|
||||||
import android.support.v7.app.AppCompatActivity;
|
import android.support.v7.app.AppCompatActivity;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
import android.widget.Button;
|
|
||||||
|
|
||||||
import com.philliphsu.clock2.Alarm;
|
|
||||||
import com.philliphsu.clock2.R;
|
|
||||||
import com.philliphsu.clock2.model.AlarmLoader;
|
|
||||||
import com.philliphsu.clock2.util.AlarmController;
|
|
||||||
import com.philliphsu.clock2.util.LocalBroadcastHelper;
|
import com.philliphsu.clock2.util.LocalBroadcastHelper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An example full-screen activity that shows and hides the system UI (i.e.
|
* An example full-screen activity that shows and hides the system UI (i.e.
|
||||||
* status bar and navigation/system bar) with user interaction.
|
* status bar and navigation/system bar) with user interaction.
|
||||||
*
|
|
||||||
* TODO: Make this abstract and make appropriate subclasses for Alarms and Timers.
|
|
||||||
*/
|
*/
|
||||||
public class RingtoneActivity extends AppCompatActivity implements
|
public abstract class RingtoneActivity<T> extends AppCompatActivity implements LoaderCallbacks<T> {
|
||||||
android.support.v4.app.LoaderManager.LoaderCallbacks<Alarm> {
|
|
||||||
private static final String TAG = "RingtoneActivity";
|
private static final String TAG = "RingtoneActivity";
|
||||||
|
|
||||||
// Shared with RingtoneService
|
// Shared with RingtoneService
|
||||||
@ -32,14 +26,19 @@ public class RingtoneActivity extends AppCompatActivity implements
|
|||||||
|
|
||||||
private static boolean sIsAlive = false;
|
private static boolean sIsAlive = false;
|
||||||
|
|
||||||
private long mAlarmId;
|
private long mItemId;
|
||||||
private Alarm mAlarm;
|
private T mItem;
|
||||||
private AlarmController mAlarmController;
|
|
||||||
|
public abstract Loader<T> onCreateLoader(long itemId);
|
||||||
|
|
||||||
|
// TODO: Should we extend from BaseActivity instead?
|
||||||
|
@LayoutRes
|
||||||
|
public abstract int layoutResource();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_ringtone);
|
setContentView(layoutResource());
|
||||||
sIsAlive = true;
|
sIsAlive = true;
|
||||||
|
|
||||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
|
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
|
||||||
@ -47,8 +46,8 @@ public class RingtoneActivity extends AppCompatActivity implements
|
|||||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
|
getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
|
||||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
|
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
|
||||||
|
|
||||||
mAlarmId = getIntent().getLongExtra(EXTRA_ITEM_ID, -1);
|
mItemId = getIntent().getLongExtra(EXTRA_ITEM_ID, -1);
|
||||||
if (mAlarmId < 0) {
|
if (mItemId < 0) {
|
||||||
throw new IllegalStateException("Cannot start RingtoneActivity without item's id");
|
throw new IllegalStateException("Cannot start RingtoneActivity without item's id");
|
||||||
}
|
}
|
||||||
// The reason we don't use a thread to load the alarm is because this is an
|
// The reason we don't use a thread to load the alarm is because this is an
|
||||||
@ -57,43 +56,31 @@ public class RingtoneActivity extends AppCompatActivity implements
|
|||||||
getSupportLoaderManager().initLoader(0, null, this);
|
getSupportLoaderManager().initLoader(0, null, this);
|
||||||
|
|
||||||
Intent intent = new Intent(this, RingtoneService.class)
|
Intent intent = new Intent(this, RingtoneService.class)
|
||||||
.putExtra(EXTRA_ITEM_ID, mAlarmId);
|
.putExtra(EXTRA_ITEM_ID, mItemId);
|
||||||
startService(intent);
|
startService(intent);
|
||||||
|
|
||||||
mAlarmController = new AlarmController(this, null);
|
|
||||||
|
|
||||||
// TODO: Butterknife binding
|
|
||||||
Button snooze = (Button) findViewById(R.id.btn_snooze);
|
|
||||||
snooze.setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
snooze();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Button dismiss = (Button) findViewById(R.id.btn_dismiss);
|
|
||||||
dismiss.setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
dismiss();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
// TODO: Do we need this anymore? I think this broadcast was only sent from
|
||||||
|
// EditAlarmActivity?
|
||||||
LocalBroadcastHelper.registerReceiver(this, mFinishReceiver, ACTION_FINISH);
|
LocalBroadcastHelper.registerReceiver(this, mFinishReceiver, ACTION_FINISH);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPause() {
|
protected void onPause() {
|
||||||
super.onPause();
|
super.onPause();
|
||||||
|
// TODO: Do we need this anymore? I think this broadcast was only sent from
|
||||||
|
// EditAlarmActivity?
|
||||||
LocalBroadcastHelper.unregisterReceiver(this, mFinishReceiver);
|
LocalBroadcastHelper.unregisterReceiver(this, mFinishReceiver);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onNewIntent(Intent intent) {
|
protected void onNewIntent(Intent intent) {
|
||||||
//super.onNewIntent(intent); // Not needed since no fragments hosted?
|
//super.onNewIntent(intent); // Not needed since no fragments hosted?
|
||||||
|
// TODO: Do we need this anymore? I think the broadcast that calls through to
|
||||||
|
// this was only sent from EditAlarmActivity?
|
||||||
|
|
||||||
// Notifies alarm missed and stops the service
|
// Notifies alarm missed and stops the service
|
||||||
LocalBroadcastHelper.sendBroadcast(this, RingtoneService.ACTION_NOTIFY_MISSED);
|
LocalBroadcastHelper.sendBroadcast(this, RingtoneService.ACTION_NOTIFY_MISSED);
|
||||||
@ -138,22 +125,17 @@ public class RingtoneActivity extends AppCompatActivity implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Loader<Alarm> onCreateLoader(int id, Bundle args) {
|
public Loader<T> onCreateLoader(int id, Bundle args) {
|
||||||
return new AlarmLoader(this, mAlarmId);
|
return onCreateLoader(mItemId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoadFinished(Loader<Alarm> loader, Alarm data) {
|
public void onLoadFinished(Loader<T> loader, T data) {
|
||||||
mAlarm = data;
|
mItem = data;
|
||||||
if (mAlarm != null) {
|
|
||||||
// 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.
|
|
||||||
mAlarmController.removeUpcomingAlarmNotification(mAlarm);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoaderReset(Loader<Alarm> loader) {
|
public void onLoaderReset(Loader<T> loader) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,28 +143,17 @@ public class RingtoneActivity extends AppCompatActivity implements
|
|||||||
return sIsAlive;
|
return sIsAlive;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void snooze() {
|
/**
|
||||||
if (mAlarm != null) {
|
* Exposed to subclasses so they can force us to stop the
|
||||||
mAlarmController.snoozeAlarm(mAlarm);
|
* ringtone and finish us.
|
||||||
}
|
*/
|
||||||
// Can't call dismiss() because we don't want to also call cancelAlarm()! Why? For example,
|
protected final void stopAndFinish() {
|
||||||
// we don't want the alarm, if it has no recurrence, to be turned off right now.
|
|
||||||
stopAndFinish();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void dismiss() {
|
|
||||||
if (mAlarm != null) {
|
|
||||||
// TODO do we really need to cancel the intent and alarm?
|
|
||||||
mAlarmController.cancelAlarm(mAlarm, false);
|
|
||||||
}
|
|
||||||
stopAndFinish();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void stopAndFinish() {
|
|
||||||
stopService(new Intent(this, RingtoneService.class));
|
stopService(new Intent(this, RingtoneService.class));
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Do we need this anymore? I think this broadcast was only sent from
|
||||||
|
// EditAlarmActivity?
|
||||||
private final BroadcastReceiver mFinishReceiver = new BroadcastReceiver() {
|
private final BroadcastReceiver mFinishReceiver = new BroadcastReceiver() {
|
||||||
@Override
|
@Override
|
||||||
public void onReceive(Context context, Intent intent) {
|
public void onReceive(Context context, Intent intent) {
|
||||||
|
|||||||
@ -220,6 +220,10 @@ public class RingtoneService extends Service { // TODO: abstract this, make subc
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void finishActivity() {
|
private void finishActivity() {
|
||||||
|
// I think this will be received by all instances of RingtoneActivity
|
||||||
|
// subclasses in memory.. but since we realistically expect only one
|
||||||
|
// instance alive at any given time, we don't need to worry about having
|
||||||
|
// to restrict the broadcast to only the subclass that's alive.
|
||||||
LocalBroadcastHelper.sendBroadcast(this, RingtoneActivity.ACTION_FINISH);
|
LocalBroadcastHelper.sendBroadcast(this, RingtoneActivity.ACTION_FINISH);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
package com.philliphsu.clock2.timers;
|
package com.philliphsu.clock2.timers;
|
||||||
|
|
||||||
import android.widget.ImageButton;
|
|
||||||
|
|
||||||
import com.philliphsu.clock2.Timer;
|
import com.philliphsu.clock2.Timer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -9,27 +7,26 @@ import com.philliphsu.clock2.Timer;
|
|||||||
*/
|
*/
|
||||||
public class TimerController {
|
public class TimerController {
|
||||||
private final Timer mTimer;
|
private final Timer mTimer;
|
||||||
private final CountdownChronometer mChronometer;
|
|
||||||
private final ImageButton mAddOneMinute;
|
|
||||||
private final ImageButton mStartPause;
|
|
||||||
private final ImageButton mStop;
|
|
||||||
|
|
||||||
public TimerController(Timer timer, CountdownChronometer chronometer, ImageButton addOneMinute,
|
/**
|
||||||
ImageButton startPause, ImageButton stop) {
|
* Calls the appropriate state on the given Timer, based on
|
||||||
mTimer = timer;
|
* its current state.
|
||||||
mChronometer = chronometer;
|
*/
|
||||||
mAddOneMinute = addOneMinute;
|
public static void startPause(Timer timer) {
|
||||||
mStartPause = startPause;
|
if (timer.hasStarted()) {
|
||||||
mStop = stop;
|
if (timer.isRunning()) {
|
||||||
|
timer.pause();
|
||||||
// init();
|
} else {
|
||||||
|
timer.resume();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
timer.start();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// private void init() {
|
public TimerController(Timer timer) {
|
||||||
// mChronometer.setBase(SystemClock.elapsedRealtime() + mTimer.duration());
|
mTimer = timer;
|
||||||
// updateStartPauseIcon();
|
}
|
||||||
// setSecondaryButtonsVisible(false);
|
|
||||||
// }
|
|
||||||
|
|
||||||
public void start() {
|
public void start() {
|
||||||
mTimer.start();
|
mTimer.start();
|
||||||
|
|||||||
@ -0,0 +1,171 @@
|
|||||||
|
package com.philliphsu.clock2.timers;
|
||||||
|
|
||||||
|
import android.app.IntentService;
|
||||||
|
import android.app.NotificationManager;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.support.annotation.DrawableRes;
|
||||||
|
import android.support.v4.app.NotificationCompat;
|
||||||
|
|
||||||
|
import com.philliphsu.clock2.MainActivity;
|
||||||
|
import com.philliphsu.clock2.R;
|
||||||
|
import com.philliphsu.clock2.Timer;
|
||||||
|
import com.philliphsu.clock2.model.TimersTableManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An {@link IntentService} subclass for handling asynchronous task requests in
|
||||||
|
* a service on a separate handler thread.
|
||||||
|
* <p/>
|
||||||
|
* TODO: Customize class - update intent actions, extra parameters and static
|
||||||
|
* helper methods.
|
||||||
|
*/
|
||||||
|
public class TimerNotificationService extends IntentService {
|
||||||
|
private static final String TAG = "TimerNotificationService";
|
||||||
|
|
||||||
|
public static final String ACTION_ADD_ONE_MINUTE = "com.philliphsu.clock2.timers.action.ADD_ONE_MINUTE";
|
||||||
|
public static final String ACTION_START_PAUSE = "com.philliphsu.clock2.timers.action.START_PAUSE";
|
||||||
|
public static final String ACTION_STOP = "com.philliphsu.clock2.timers.action.STOP";
|
||||||
|
|
||||||
|
public static final String EXTRA_TIMER_ID = "com.philliphsu.clock2.timers.extra.TIMER_ID";
|
||||||
|
|
||||||
|
private TimersTableManager mTableManager;
|
||||||
|
|
||||||
|
public TimerNotificationService() {
|
||||||
|
super("TimerNotificationService");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to start this Service for its default action: to show
|
||||||
|
* the notification for the Timer with the given id.
|
||||||
|
*/
|
||||||
|
public static void showNotification(Context context, long timerId) {
|
||||||
|
Intent intent = new Intent(context, TimerNotificationService.class);
|
||||||
|
intent.putExtra(EXTRA_TIMER_ID, timerId);
|
||||||
|
context.startService(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to cancel the notification previously shown from calling
|
||||||
|
* {@link #showNotification(Context, long)}. This does NOT start the Service
|
||||||
|
* and call through to {@link #onHandleIntent(Intent)}.
|
||||||
|
* @param timerId the id of the Timer associated with the notification
|
||||||
|
* you want to cancel
|
||||||
|
*/
|
||||||
|
public static void cancelNotification(Context context, long timerId) {
|
||||||
|
NotificationManager nm = (NotificationManager)
|
||||||
|
context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
nm.cancel(TAG, (int) timerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
mTableManager = new TimersTableManager(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onHandleIntent(Intent intent) {
|
||||||
|
if (intent != null) {
|
||||||
|
final long timerId = intent.getLongExtra(EXTRA_TIMER_ID, -1);
|
||||||
|
if (timerId == -1) {
|
||||||
|
throw new IllegalStateException("Did not pass in timer id");
|
||||||
|
}
|
||||||
|
final String action = intent.getAction();
|
||||||
|
if (action == null) {
|
||||||
|
showNotification(timerId);
|
||||||
|
} else if (ACTION_ADD_ONE_MINUTE.equals(action)) {
|
||||||
|
handleAddOneMinute(timerId);
|
||||||
|
} else if (ACTION_START_PAUSE.equals(action)) {
|
||||||
|
handleStartPause(timerId);
|
||||||
|
} else if (ACTION_STOP.equals(action)) {
|
||||||
|
handleStop(timerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showNotification(long timerId) {
|
||||||
|
Timer timer = getTimer(timerId);
|
||||||
|
|
||||||
|
// Base note
|
||||||
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
|
||||||
|
// TODO: correct icon
|
||||||
|
.setSmallIcon(R.drawable.ic_half_day_1_black_24dp)
|
||||||
|
.setShowWhen(false)
|
||||||
|
.setOngoing(true);
|
||||||
|
// TODO: Set content intent so that when clicked, we launch
|
||||||
|
// TimersFragment and scroll to the given timer id. The following
|
||||||
|
// is merely pseudocode.
|
||||||
|
Intent contentIntent = new Intent(this, MainActivity.class);
|
||||||
|
contentIntent.putExtra(null/*TODO:MainActivity.EXTRA_SHOW_PAGE*/,
|
||||||
|
1/*TODO:The tab index of the timers page*/);
|
||||||
|
contentIntent.putExtra(null/*TODO:MainActivity.EXTRA_SCROLL_TO_ID*/,
|
||||||
|
timerId);
|
||||||
|
builder.setContentIntent(PendingIntent.getActivity(
|
||||||
|
this,
|
||||||
|
0, // TODO: Request code not needed? Since any multiple notifications
|
||||||
|
// should be able to use the same PendingIntent for this action....
|
||||||
|
// unless the underlying *Intent* and its id extra are overwritten
|
||||||
|
// per notification when retrieving the PendingIntent..
|
||||||
|
contentIntent,
|
||||||
|
0/*Shouldn't need a flag..*/));
|
||||||
|
// TODO: Use a handler to continually update the countdown text
|
||||||
|
|
||||||
|
String title = timer.label();
|
||||||
|
if (title.isEmpty()) {
|
||||||
|
title = getString(R.string.timer);
|
||||||
|
}
|
||||||
|
builder.setContentTitle(title);
|
||||||
|
|
||||||
|
addAction(builder, ACTION_ADD_ONE_MINUTE,
|
||||||
|
timer.getId(), R.drawable.ic_add_circle_24dp/*TODO: correct icon*/);
|
||||||
|
addAction(builder, ACTION_START_PAUSE,
|
||||||
|
timer.getId(), R.drawable.ic_add_circle_24dp/*TODO: correct icon*/);
|
||||||
|
addAction(builder, ACTION_STOP,
|
||||||
|
timer.getId(), R.drawable.ic_add_circle_24dp/*TODO: correct icon*/);
|
||||||
|
|
||||||
|
NotificationManager nm = (NotificationManager)
|
||||||
|
getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
nm.notify(TAG, timer.getIntId(), builder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds and adds the specified action to the notification's builder.
|
||||||
|
*/
|
||||||
|
private void addAction(NotificationCompat.Builder noteBuilder, String action,
|
||||||
|
long timerId, @DrawableRes int icon) {
|
||||||
|
Intent intent = new Intent(this, TimerNotificationService.class)
|
||||||
|
.setAction(action)
|
||||||
|
.putExtra(EXTRA_TIMER_ID, timerId);
|
||||||
|
PendingIntent pi = PendingIntent.getService(this,
|
||||||
|
(int) timerId, intent, 0/*no flags*/);
|
||||||
|
noteBuilder.addAction(icon, ""/*no action title*/, pi);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleAddOneMinute(long timerId) {
|
||||||
|
Timer timer = getTimer(timerId);
|
||||||
|
timer.addOneMinute();
|
||||||
|
updateTimer(timer);
|
||||||
|
// TODO: Verify the notification countdown is extended by one minute.
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleStartPause(long timerId) {
|
||||||
|
Timer t = getTimer(timerId);
|
||||||
|
TimerController.startPause(t);
|
||||||
|
updateTimer(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleStop(long timerId) {
|
||||||
|
Timer t = getTimer(timerId);
|
||||||
|
t.stop();
|
||||||
|
updateTimer(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateTimer(Timer timer) {
|
||||||
|
mTableManager.updateItem(timer.getId(), timer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Timer getTimer(long timerId) {
|
||||||
|
return mTableManager.queryItem(timerId).getItem();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,5 @@
|
|||||||
package com.philliphsu.clock2.timers;
|
package com.philliphsu.clock2.timers;
|
||||||
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
@ -52,17 +51,7 @@ public class TimerViewHolder extends BaseViewHolder<Timer> {
|
|||||||
|
|
||||||
@OnClick(R.id.start_pause)
|
@OnClick(R.id.start_pause)
|
||||||
void startPause() {
|
void startPause() {
|
||||||
Timer t = getItem();
|
TimerController.startPause(getItem());
|
||||||
if (t.isRunning()) {
|
|
||||||
// mController.pause();
|
|
||||||
t.pause();
|
|
||||||
} else {
|
|
||||||
if (t.hasStarted()) {
|
|
||||||
t.resume();
|
|
||||||
} else {
|
|
||||||
t.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Persist value changes
|
// Persist value changes
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,22 @@
|
|||||||
|
package com.philliphsu.clock2.timers;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.v7.app.AppCompatActivity;
|
||||||
|
|
||||||
|
public class TimesUpActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Override
|
||||||
|
// public Loader<Timer> onCreateLoader(long itemId) {
|
||||||
|
// return new TimerLoader(this, itemId);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// @Override
|
||||||
|
// public int layoutResource() {
|
||||||
|
// return R.layout.activity_ringtone;
|
||||||
|
// }
|
||||||
|
}
|
||||||
@ -12,8 +12,8 @@ import com.philliphsu.clock2.Alarm;
|
|||||||
import com.philliphsu.clock2.PendingAlarmScheduler;
|
import com.philliphsu.clock2.PendingAlarmScheduler;
|
||||||
import com.philliphsu.clock2.R;
|
import com.philliphsu.clock2.R;
|
||||||
import com.philliphsu.clock2.UpcomingAlarmReceiver;
|
import com.philliphsu.clock2.UpcomingAlarmReceiver;
|
||||||
|
import com.philliphsu.clock2.alarms.AlarmActivity;
|
||||||
import com.philliphsu.clock2.model.AlarmsTableManager;
|
import com.philliphsu.clock2.model.AlarmsTableManager;
|
||||||
import com.philliphsu.clock2.ringtone.RingtoneActivity;
|
|
||||||
import com.philliphsu.clock2.ringtone.RingtoneService;
|
import com.philliphsu.clock2.ringtone.RingtoneService;
|
||||||
|
|
||||||
import static android.app.PendingIntent.FLAG_CANCEL_CURRENT;
|
import static android.app.PendingIntent.FLAG_CANCEL_CURRENT;
|
||||||
@ -183,8 +183,8 @@ public final class AlarmController {
|
|||||||
|
|
||||||
private PendingIntent alarmIntent(Alarm alarm, boolean retrievePrevious) {
|
private PendingIntent alarmIntent(Alarm alarm, boolean retrievePrevious) {
|
||||||
// TODO: Use appropriate subclass instead
|
// TODO: Use appropriate subclass instead
|
||||||
Intent intent = new Intent(mAppContext, RingtoneActivity.class)
|
Intent intent = new Intent(mAppContext, AlarmActivity.class)
|
||||||
.putExtra(RingtoneActivity.EXTRA_ITEM_ID, alarm.id());
|
.putExtra(AlarmActivity.EXTRA_ITEM_ID, alarm.id());
|
||||||
int flag = retrievePrevious ? FLAG_NO_CREATE : FLAG_CANCEL_CURRENT;
|
int flag = retrievePrevious ? FLAG_NO_CREATE : FLAG_CANCEL_CURRENT;
|
||||||
PendingIntent pi = getActivity(mAppContext, alarm.intId(), intent, flag);
|
PendingIntent pi = getActivity(mAppContext, alarm.intId(), intent, flag);
|
||||||
// Even when we try to retrieve a previous instance that actually did exist,
|
// Even when we try to retrieve a previous instance that actually did exist,
|
||||||
|
|||||||
@ -13,8 +13,8 @@ import com.philliphsu.clock2.Alarm;
|
|||||||
import com.philliphsu.clock2.PendingAlarmScheduler;
|
import com.philliphsu.clock2.PendingAlarmScheduler;
|
||||||
import com.philliphsu.clock2.R;
|
import com.philliphsu.clock2.R;
|
||||||
import com.philliphsu.clock2.UpcomingAlarmReceiver;
|
import com.philliphsu.clock2.UpcomingAlarmReceiver;
|
||||||
|
import com.philliphsu.clock2.alarms.AlarmActivity;
|
||||||
import com.philliphsu.clock2.model.AlarmsTableManager;
|
import com.philliphsu.clock2.model.AlarmsTableManager;
|
||||||
import com.philliphsu.clock2.ringtone.RingtoneActivity;
|
|
||||||
import com.philliphsu.clock2.ringtone.RingtoneService;
|
import com.philliphsu.clock2.ringtone.RingtoneService;
|
||||||
|
|
||||||
import static android.app.PendingIntent.FLAG_CANCEL_CURRENT;
|
import static android.app.PendingIntent.FLAG_CANCEL_CURRENT;
|
||||||
@ -180,8 +180,8 @@ public final class AlarmUtils {
|
|||||||
|
|
||||||
private static PendingIntent alarmIntent(Context context, Alarm alarm, boolean retrievePrevious) {
|
private static PendingIntent alarmIntent(Context context, Alarm alarm, boolean retrievePrevious) {
|
||||||
// TODO: Use appropriate subclass instead
|
// TODO: Use appropriate subclass instead
|
||||||
Intent intent = new Intent(context, RingtoneActivity.class)
|
Intent intent = new Intent(context, AlarmActivity.class)
|
||||||
.putExtra(RingtoneActivity.EXTRA_ITEM_ID, alarm.id());
|
.putExtra(AlarmActivity.EXTRA_ITEM_ID, alarm.id());
|
||||||
int flag = retrievePrevious ? FLAG_NO_CREATE : FLAG_CANCEL_CURRENT;
|
int flag = retrievePrevious ? FLAG_NO_CREATE : FLAG_CANCEL_CURRENT;
|
||||||
PendingIntent pi = getActivity(context, alarm.intId(), intent, flag);
|
PendingIntent pi = getActivity(context, alarm.intId(), intent, flag);
|
||||||
// Even when we try to retrieve a previous instance that actually did exist,
|
// Even when we try to retrieve a previous instance that actually did exist,
|
||||||
|
|||||||
@ -191,4 +191,6 @@
|
|||||||
|
|
||||||
<!-- TODO: Remove or change this placeholder text -->
|
<!-- TODO: Remove or change this placeholder text -->
|
||||||
<string name="title_activity_create_timer">CreateTimerActivity</string>
|
<string name="title_activity_create_timer">CreateTimerActivity</string>
|
||||||
|
|
||||||
|
<string name="timer">Timer</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user