/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.embeddedlog.LightUpDroid.timer;
import android.app.AlarmManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import com.embeddedlog.LightUpDroid.DeskClock;
import com.embeddedlog.LightUpDroid.R;
import com.embeddedlog.LightUpDroid.TimerRingService;
import com.embeddedlog.LightUpDroid.Utils;
import java.util.ArrayList;
import java.util.Iterator;
public class TimerReceiver extends BroadcastReceiver {
private static final String TAG = "TimerReceiver";
// Make this a large number to avoid the alarm ID's which seem to be 1, 2, ...
// Must also be different than StopwatchService.NOTIFICATION_ID
private static final int IN_USE_NOTIFICATION_ID = Integer.MAX_VALUE - 2;
ArrayList<TimerObj> mTimers;
@Override
public void onReceive(final Context context, final Intent intent) {
if (Timers.LOGGING) {
Log.v(TAG, "Received intent " + intent.toString());
}
String actionType = intent.getAction();
// This action does not need the timers data
if (Timers.NOTIF_IN_USE_CANCEL.equals(actionType)) {
cancelInUseNotification(context);
return;
}
// Get the updated timers data.
if (mTimers == null) {
mTimers = new ArrayList<TimerObj> ();
}
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
TimerObj.getTimersFromSharedPrefs(prefs, mTimers);
// These actions do not provide a timer ID, but do use the timers data
if (Timers.NOTIF_IN_USE_SHOW.equals(actionType)) {
showInUseNotification(context);
return;
} else if (Timers.NOTIF_TIMES_UP_SHOW.equals(actionType)) {
showTimesUpNotification(context);
return;
} else if (Timers.NOTIF_TIMES_UP_CANCEL.equals(actionType)) {
cancelTimesUpNotification(context);
return;
}
// Remaining actions provide a timer Id
if (!intent.hasExtra(Timers.TIMER_INTENT_EXTRA)) {
// No data to work with, do nothing
Log.e(TAG, "got intent without Timer data");
return;
}
// Get the timer out of the Intent
int timerId = intent.getIntExtra(Timers.TIMER_INTENT_EXTRA, -1);
if (timerId == -1) {
Log.d(TAG, "OnReceive:intent without Timer data for " + actionType);
}
TimerObj t = Timers.findTimer(mTimers, timerId);
if (Timers.TIMES_UP.equals(actionType)) {
// Find the timer (if it doesn't exists, it was probably deleted).
if (t == null) {
Log.d(TAG, " timer not found in list - do nothing");
return;
}
t.mState = TimerObj.STATE_TIMESUP;
t.writeToSharedPref(prefs);
// Play ringtone by using TimerRingService service with a default alarm.
Log.d(TAG, "playing ringtone");
Intent si = new Intent();
si.setClass(context, TimerRingService.class);
context.startService(si);
// Update the in-use notification
if (getNextRunningTimer(mTimers, false, Utils.getTimeNow()) == null) {
// Found no running timers.
cancelInUseNotification(context);
} else {
showInUseNotification(context);
}
// Start the TimerAlertFullScreen activity.
Intent timersAlert = new Intent(context, TimerAlertFullScreen.class);
timersAlert.setFlags(
Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NO_USER_ACTION);
context.startActivity(timersAlert);
} else if (Timers.TIMER_RESET.equals(actionType)
|| Timers.DELETE_TIMER.equals(actionType)
|| Timers.TIMER_DONE.equals(actionType)) {
// Stop Ringtone if all timers are not in times-up status
stopRingtoneIfNoTimesup(context);
} else if (Timers.NOTIF_TIMES_UP_STOP.equals(actionType)) {
// Find the timer (if it doesn't exists, it was probably deleted).
if (t == null) {
Log.d(TAG, "timer to stop not found in list - do nothing");
return;
} else if (t.mState != TimerObj.STATE_TIMESUP) {
Log.d(TAG, "action to stop but timer not in times-up state - do nothing");
return;
}
// Update timer state
t.mState = t.getDeleteAfterUse() ? TimerObj.STATE_DELETED : TimerObj.STATE_DONE;
t.mTimeLeft = t.mOriginalLength - (Utils.getTimeNow() - t.mStartTime);
t.writeToSharedPref(prefs);
// Flag to tell DeskClock to re-sync with the database
prefs.edit().putBoolean(Timers.FROM_NOTIFICATION, true).apply();
cancelTimesUpNotification(context, t);
// Done with timer - delete from data base
if (t.getDeleteAfterUse()) {
t.deleteFromSharedPref(prefs);
}
// Stop Ringtone if no timers are in times-up status
stopRingtoneIfNoTimesup(context);
} else if (Timers.NOTIF_TIMES_UP_PLUS_ONE.equals(actionType)) {
// Find the timer (if it doesn't exists, it was probably deleted).
if (t == null) {
Log.d(TAG, "timer to +1m not found in list - do nothing");
return;
} else if (t.mState != TimerObj.STATE_TIMESUP) {
Log.d(TAG, "action to +1m but timer not in times up state - do nothing");
return;
}
// Restarting the timer with 1 minute left.
t.mState = TimerObj.STATE_RUNNING;
t.mStartTime = Utils.getTimeNow();
t.mTimeLeft = t. mOriginalLength = TimerObj.MINUTE_IN_MILLIS;
t.writeToSharedPref(prefs);
// Flag to tell DeskClock to re-sync with the database
prefs.edit().putBoolean(Timers.FROM_NOTIFICATION, true).apply();
cancelTimesUpNotification(context, t);
// If the app is not open, refresh the in-use notification
if (!prefs.getBoolean(Timers.NOTIF_APP_OPEN, false)) {
showInUseNotification(context);
}
// Stop Ringtone if no timers are in times-up status
stopRingtoneIfNoTimesup(context);
} else if (Timers.TIMER_UPDATE.equals(actionType)) {
// Refresh buzzing notification
if (t.mState == TimerObj.STATE_TIMESUP) {
// Must cancel the previous notification to get all updates displayed correctly
cancelTimesUpNotification(context, t);
showTimesUpNotification(context, t);
}
}
// Update the next "Times up" alarm
updateNextTimesup(context);
}
private void stopRingtoneIfNoTimesup(final Context context) {
if (Timers.findExpiredTimer(mTimers) == null) {
// Stop ringtone
Log.d(TAG, "stopping ringtone");
Intent si = new Intent();
si.setClass(context, TimerRingService.class);
context.stopService(si);
}
}
// Scan all timers and find the one that will expire next.
// Tell AlarmManager to send a "Time's up" message to this receiver when this timer expires.
// If no timer exists, clear "time's up" message.
private void updateNextTimesup(Context context) {
TimerObj t = getNextRunningTimer(mTimers, false, Utils.getTimeNow());
long nextTimesup = (t == null) ? -1 : t.getTimesupTime();
int timerId = (t == null) ? -1 : t.mTimerId;
Intent intent = new Intent();
intent.setAction(Timers.TIMES_UP);
intent.setClass(context, TimerReceiver.class);
if (!mTimers.isEmpty()) {
intent.putExtra(Timers.TIMER_INTENT_EXTRA, timerId);
}
AlarmManager mngr = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
PendingIntent p = PendingIntent.getBroadcast(context,
0, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
if (t != null) {
if (Utils.isKitKatOrLater()) {
mngr.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextTimesup, p);
} else {
mngr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextTimesup, p);
}
if (Timers.LOGGING) {
Log.d(TAG, "Setting times up to " + nextTimesup);
}
} else {
mngr.cancel(p);
if (Timers.LOGGING) {
Log.v(TAG, "no next times up");
}
}
}
private void showInUseNotification(final Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
boolean appOpen = prefs.getBoolean(Timers.NOTIF_APP_OPEN, false);
ArrayList<TimerObj> timersInUse = Timers.timersInUse(mTimers);
int numTimersInUse = timersInUse.size();
if (appOpen || numTimersInUse == 0) {
return;
}
String title, contentText;
Long nextBroadcastTime = null;
long now = Utils.getTimeNow();
if (timersInUse.size() == 1) {
TimerObj timer = timersInUse.get(0);
boolean timerIsTicking = timer.isTicking();
String label = timer.getLabelOrDefault(context);
title = timerIsTicking ? label : context.getString(R.string.timer_stopped);
long timeLeft = timerIsTicking ? timer.getTimesupTime() - now : timer.mTimeLeft;
contentText = buildTimeRemaining(context, timeLeft);
if (timerIsTicking && timeLeft > TimerObj.MINUTE_IN_MILLIS) {
nextBroadcastTime = getBroadcastTime(now, timeLeft);
}
} else {
TimerObj timer = getNextRunningTimer(timersInUse, false, now);
if (timer == null) {
// No running timers.
title = String.format(
context.getString(R.string.timers_stopped), numTimersInUse);
contentText = context.getString(R.string.all_timers_stopped_notif);
} else {
// We have at least one timer running and other timers stopped.
title = String.format(
context.getString(R.string.timers_in_use), numTimersInUse);
long completionTime = timer.getTimesupTime();
long timeLeft = completionTime - now;
contentText = String.format(context.getString(R.string.next_timer_notif),
buildTimeRemaining(context, timeLeft));
if (timeLeft <= TimerObj.MINUTE_IN_MILLIS) {
TimerObj timerWithUpdate = getNextRunningTimer(timersInUse, true, now);
if (timerWithUpdate != null) {
completionTime = timerWithUpdate.getTimesupTime();
timeLeft = completionTime - now;
nextBroadcastTime = getBroadcastTime(now, timeLeft);
}
} else {
nextBroadcastTime = getBroadcastTime(now, timeLeft);
}
}
}
showCollapsedNotificationWithNext(context, title, contentText, nextBroadcastTime);
}
private long getBroadcastTime(long now, long timeUntilBroadcast) {
long seconds = timeUntilBroadcast / 1000;
seconds = seconds - ( (seconds / 60) * 60 );
return now + (seconds * 1000);
}
private void showCollapsedNotificationWithNext(
final Context context, String title, String text, Long nextBroadcastTime) {
Intent activityIntent = new Intent(context, DeskClock.class);
activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
activityIntent.putExtra(DeskClock.SELECT_TAB_INTENT_EXTRA, DeskClock.TIMER_TAB_INDEX);
PendingIntent pendingActivityIntent = PendingIntent.getActivity(context, 0, activityIntent,
PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
showCollapsedNotification(context, title, text, Notification.PRIORITY_HIGH,
pendingActivityIntent, IN_USE_NOTIFICATION_ID, false);
if (nextBroadcastTime == null) {
return;
}
Intent nextBroadcast = new Intent();
nextBroadcast.setAction(Timers.NOTIF_IN_USE_SHOW);
PendingIntent pendingNextBroadcast =
PendingIntent.getBroadcast(context, 0, nextBroadcast, 0);
AlarmManager alarmManager =
(AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
if (Utils.isKitKatOrLater()) {
alarmManager.setExact(AlarmManager.ELAPSED_REALTIME, nextBroadcastTime, pendingNextBroadcast);
} else {
alarmManager.set(AlarmManager.ELAPSED_REALTIME, nextBroadcastTime, pendingNextBroadcast);
}
}
private static void showCollapsedNotification(final Context context, String title, String text,
int priority, PendingIntent pendingIntent, int notificationId, boolean showTicker) {
NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
.setAutoCancel(false)
.setContentTitle(title)
.setContentText(text)
.setDeleteIntent(pendingIntent)
.setOngoing(true)
.setPriority(priority)
.setSmallIcon(R.drawable.stat_notify_timer);
if (showTicker) {
builder.setTicker(text);
}
Notification notification = builder.build();
notification.contentIntent = pendingIntent;
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(notificationId, notification);
}
private String buildTimeRemaining(Context context, long timeLeft) {
if (timeLeft < 0) {
// We should never be here...
Log.v(TAG, "Will not show notification for timer already expired.");
return null;
}
long hundreds, seconds, minutes, hours;
seconds = timeLeft / 1000;
minutes = seconds / 60;
seconds = seconds - minutes * 60;
hours = minutes / 60;
minutes = minutes - hours * 60;
if (hours > 99) {
hours = 0;
}
String hourSeq = (hours == 0) ? "" :
( (hours == 1) ? context.getString(R.string.hour) :
context.getString(R.string.hours, Long.toString(hours)) );
String minSeq = (minutes == 0) ? "" :
( (minutes == 1) ? context.getString(R.string.minute) :
context.getString(R.string.minutes, Long.toString(minutes)) );
boolean dispHour = hours > 0;
boolean dispMinute = minutes > 0;
int index = (dispHour ? 1 : 0) | (dispMinute ? 2 : 0);
String[] formats = context.getResources().getStringArray(R.array.timer_notifications);
return String.format(formats[index], hourSeq, minSeq);
}
private TimerObj getNextRunningTimer(
ArrayList<TimerObj> timers, boolean requireNextUpdate, long now) {
long nextTimesup = Long.MAX_VALUE;
boolean nextTimerFound = false;
Iterator<TimerObj> i = timers.iterator();
TimerObj t = null;
while(i.hasNext()) {
TimerObj tmp = i.next();
if (tmp.mState == TimerObj.STATE_RUNNING) {
long timesupTime = tmp.getTimesupTime();
long timeLeft = timesupTime - now;
if (timesupTime < nextTimesup && (!requireNextUpdate || timeLeft > 60) ) {
nextTimesup = timesupTime;
nextTimerFound = true;
t = tmp;
}
}
}
if (nextTimerFound) {
return t;
} else {
return null;
}
}
private void cancelInUseNotification(final Context context) {
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(IN_USE_NOTIFICATION_ID);
}
private void showTimesUpNotification(final Context context) {
for (TimerObj timerObj : Timers.timersInTimesUp(mTimers) ) {
showTimesUpNotification(context, timerObj);
}
}
private void showTimesUpNotification(final Context context, TimerObj timerObj) {
// Content Intent. When clicked will show the timer full screen
PendingIntent contentIntent = PendingIntent.getActivity(context, timerObj.mTimerId,
new Intent(context, TimerAlertFullScreen.class).putExtra(
Timers.TIMER_INTENT_EXTRA, timerObj.mTimerId),
PendingIntent.FLAG_UPDATE_CURRENT);
// Add one minute action button
PendingIntent addOneMinuteAction = PendingIntent.getBroadcast(context, timerObj.mTimerId,
new Intent(Timers.NOTIF_TIMES_UP_PLUS_ONE)
.putExtra(Timers.TIMER_INTENT_EXTRA, timerObj.mTimerId),
PendingIntent.FLAG_UPDATE_CURRENT);
// Add stop/done action button
PendingIntent stopAction = PendingIntent.getBroadcast(context, timerObj.mTimerId,
new Intent(Timers.NOTIF_TIMES_UP_STOP)
.putExtra(Timers.TIMER_INTENT_EXTRA, timerObj.mTimerId),
PendingIntent.FLAG_UPDATE_CURRENT);
// Notification creation
Notification notification = new Notification.Builder(context)
.setContentIntent(contentIntent)
.addAction(R.drawable.ic_menu_add,
context.getResources().getString(R.string.timer_plus_1_min),
addOneMinuteAction)
.addAction(
timerObj.getDeleteAfterUse()
? android.R.drawable.ic_menu_close_clear_cancel
: R.drawable.ic_stop_normal,
timerObj.getDeleteAfterUse()
? context.getResources().getString(R.string.timer_done)
: context.getResources().getString(R.string.timer_stop),
stopAction)
.setContentTitle(timerObj.getLabelOrDefault(context))
.setContentText(context.getResources().getString(R.string.timer_times_up))
.setSmallIcon(R.drawable.stat_notify_timer)
.setOngoing(true)
.setAutoCancel(false)
.setPriority(Notification.PRIORITY_MAX)
.setDefaults(Notification.DEFAULT_LIGHTS)
.setWhen(0)
.build();
// Send the notification using the timer's id to identify the
// correct notification
((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)).notify(
timerObj.mTimerId, notification);
if (Timers.LOGGING) {
Log.v(TAG, "Setting times-up notification for "
+ timerObj.getLabelOrDefault(context) + " #" + timerObj.mTimerId);
}
}
private void cancelTimesUpNotification(final Context context) {
for (TimerObj timerObj : Timers.timersInTimesUp(mTimers) ) {
cancelTimesUpNotification(context, timerObj);
}
}
private void cancelTimesUpNotification(final Context context, TimerObj timerObj) {
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(timerObj.mTimerId);
if (Timers.LOGGING) {
Log.v(TAG, "Canceling times-up notification for "
+ timerObj.getLabelOrDefault(context) + " #" + timerObj.mTimerId);
}
}
}