Java tutorial
/* * Copyright 2014 Google Inc. All rights reserved. * * 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 fr.paug.droidcon.service; import android.app.*; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.support.v4.app.NotificationCompat; import android.support.v4.app.TaskStackBuilder; import android.util.Log; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.common.api.ResultCallback; import com.google.android.gms.wearable.DataApi; import com.google.android.gms.wearable.PutDataMapRequest; import com.google.android.gms.wearable.PutDataRequest; import com.google.android.gms.wearable.Wearable; import fr.paug.droidcon.R; import fr.paug.droidcon.provider.ScheduleContract; import fr.paug.droidcon.ui.BrowseSessionsActivity; import fr.paug.droidcon.ui.MyScheduleActivity; import fr.paug.droidcon.util.FeedbackUtils; import fr.paug.droidcon.util.PrefUtils; import fr.paug.droidcon.util.UIUtils; import java.util.ArrayList; import java.util.Date; import java.util.concurrent.TimeUnit; import static fr.paug.droidcon.util.LogUtils.LOGD; import static fr.paug.droidcon.util.LogUtils.makeLogTag; /** * Background service to handle scheduling of starred session notification via * {@link android.app.AlarmManager}. */ public class SessionAlarmService extends IntentService implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener { private static final String TAG = makeLogTag(SessionAlarmService.class); public static final String ACTION_NOTIFY_SESSION = "fr.paug.droidcon.action.NOTIFY_SESSION"; public static final String ACTION_SCHEDULE_STARRED_BLOCK = "fr.paug.droidcon.action.SCHEDULE_STARRED_BLOCK"; public static final String ACTION_SCHEDULE_ALL_STARRED_BLOCKS = "fr.paug.droidcon.action.SCHEDULE_ALL_STARRED_BLOCKS"; public static final String EXTRA_SESSION_START = "fr.paug.droidcon.extra.SESSION_START"; public static final String EXTRA_SESSION_END = "fr.paug.droidcon.extra.SESSION_END"; public static final String EXTRA_SESSION_ALARM_OFFSET = "fr.paug.droidcon.extra.SESSION_ALARM_OFFSET"; public static final String EXTRA_SESSION_ID = "fr.paug.droidcon.extra.SESSION_ID"; public static final String EXTRA_SESSION_TITLE = "fr.paug.droidcon.extra.SESSION_TITLE"; public static final String EXTRA_SESSION_ROOM = "fr.paug.droidcon.extra.SESSION_ROOM"; public static final String EXTRA_SESSION_SPEAKERS = "fr.paug.droidcon.extra.SESSION_SPEAKERS"; public static final int NOTIFICATION_ID = 100; public static final int FEEDBACK_NOTIFICATION_ID = 101; // pulsate every 1 second, indicating a relatively high degree of urgency private static final int NOTIFICATION_LED_ON_MS = 100; private static final int NOTIFICATION_LED_OFF_MS = 1000; private static final int NOTIFICATION_ARGB_COLOR = 0xff0088ff; // cyan private static final long MILLI_TEN_MINUTES = 600000; private static final long MILLI_FIVE_MINUTES = 300000; private static final long MILLI_ONE_MINUTE = 60000; private static final long UNDEFINED_ALARM_OFFSET = -1; private static final long UNDEFINED_VALUE = -1; public static final String ACTION_NOTIFICATION_DISMISSAL = "com.google.sample.apps.iosched.ACTION_NOTIFICATION_DISMISSAL"; private GoogleApiClient mGoogleApiClient; public static final String KEY_SESSION_ID = "session-id"; private static final String KEY_SESSION_NAME = "session-name"; private static final String KEY_SPEAKER_NAME = "speaker-name"; private static final String KEY_SESSION_ROOM = "session-room"; public static final String PATH_FEEDBACK = "/iowear/feedback"; // special session ID that identifies a debug notification public static final String DEBUG_SESSION_ID = "debug-session-id"; public SessionAlarmService() { super(TAG); } @Override public void onCreate() { super.onCreate(); mGoogleApiClient = new GoogleApiClient.Builder(this).addApi(Wearable.API).addConnectionCallbacks(this) .addOnConnectionFailedListener(this).build(); } @Override protected void onHandleIntent(Intent intent) { mGoogleApiClient.blockingConnect(2000, TimeUnit.MILLISECONDS); final String action = intent.getAction(); LOGD(TAG, "SessionAlarmService handling " + action); if (ACTION_SCHEDULE_ALL_STARRED_BLOCKS.equals(action)) { LOGD(TAG, "Scheduling all starred blocks."); scheduleAllStarredBlocks(); return; } final long sessionEnd = intent.getLongExtra(SessionAlarmService.EXTRA_SESSION_END, UNDEFINED_VALUE); if (sessionEnd == UNDEFINED_VALUE) { LOGD(TAG, "IGNORING ACTION -- missing sessionEnd parameter"); return; } final long sessionAlarmOffset = intent.getLongExtra(SessionAlarmService.EXTRA_SESSION_ALARM_OFFSET, UNDEFINED_ALARM_OFFSET); LOGD(TAG, "Session alarm offset is: " + sessionAlarmOffset); final long sessionStart = intent.getLongExtra(SessionAlarmService.EXTRA_SESSION_START, UNDEFINED_VALUE); if (sessionStart == UNDEFINED_VALUE) { LOGD(TAG, "IGNORING ACTION -- no session start parameter."); return; } if (ACTION_NOTIFY_SESSION.equals(action)) { LOGD(TAG, "Notifying about sessions starting at " + sessionStart + " = " + (new Date(sessionStart)).toString()); LOGD(TAG, "-> Alarm offset: " + sessionAlarmOffset); notifySession(sessionStart, sessionAlarmOffset); } else if (ACTION_SCHEDULE_STARRED_BLOCK.equals(action)) { LOGD(TAG, "Scheduling session alarm."); LOGD(TAG, "-> Session start: " + sessionStart + " = " + (new Date(sessionStart)).toString()); LOGD(TAG, "-> Session end: " + sessionEnd + " = " + (new Date(sessionEnd)).toString()); LOGD(TAG, "-> Alarm offset: " + sessionAlarmOffset); scheduleAlarm(sessionStart, sessionEnd, sessionAlarmOffset); } } private void scheduleAlarm(final long sessionStart, final long sessionEnd, final long alarmOffset) { NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); nm.cancel(NOTIFICATION_ID); final long currentTime = UIUtils.getCurrentTime(this); // If the session is already started, do not schedule system notification. if (currentTime > sessionStart) { LOGD(TAG, "Not scheduling alarm because target time is in the past: " + sessionStart); return; } // By default, sets alarm to go off at 10 minutes before session start time. If alarm // offset is provided, alarm is set to go off by that much time from now. long alarmTime; if (alarmOffset == UNDEFINED_ALARM_OFFSET) { alarmTime = sessionStart - MILLI_TEN_MINUTES; } else { alarmTime = currentTime + alarmOffset; } LOGD(TAG, "Scheduling alarm for " + alarmTime + " = " + (new Date(alarmTime)).toString()); final Intent notifIntent = new Intent(ACTION_NOTIFY_SESSION, null, this, SessionAlarmService.class); // Setting data to ensure intent's uniqueness for different session start times. notifIntent.setData( new Uri.Builder().authority("fr.paug.droidcon").path(String.valueOf(sessionStart)).build()); notifIntent.putExtra(SessionAlarmService.EXTRA_SESSION_START, sessionStart); LOGD(TAG, "-> Intent extra: session start " + sessionStart); notifIntent.putExtra(SessionAlarmService.EXTRA_SESSION_END, sessionEnd); LOGD(TAG, "-> Intent extra: session end " + sessionEnd); notifIntent.putExtra(SessionAlarmService.EXTRA_SESSION_ALARM_OFFSET, alarmOffset); LOGD(TAG, "-> Intent extra: session alarm offset " + alarmOffset); PendingIntent pi = PendingIntent.getService(this, 0, notifIntent, PendingIntent.FLAG_CANCEL_CURRENT); final AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE); // Schedule an alarm to be fired to notify user of added sessions are about to begin. LOGD(TAG, "-> Scheduling RTC_WAKEUP alarm at " + alarmTime); am.set(AlarmManager.RTC_WAKEUP, alarmTime, pi); } // // A starred session is about to end; notify the user to provide session feedback. // // Constructs and triggers a system notification. Does nothing if the session has already // // concluded. // private void notifySessionFeedback(final String sessionId, final long sessionEnd, // final String sessionTitle, final String sessionRoom, final String sessionSpeakers) { // LOGD(TAG, "Considering firing notification for feedback for session: " + sessionTitle); // boolean isDebug = DEBUG_SESSION_ID.equals(sessionId); // // if (isDebug) { // LOGD(TAG, "Note: this is a debug notification."); // } // // // Don't fire notification if this feature is disabled in settings // if (!PrefUtils.shouldShowSessionFeedbackReminders(this)) { // LOGD(TAG, "Skipping session feedback notification for session " + sessionId + " (" // + sessionTitle + "). Disabled in settings."); // return; // } // // // Avoid repeated notifications. // if (!isDebug && UIUtils.isFeedbackNotificationFiredForSession(this, sessionId)) { // LOGD(TAG, "Skipping repeated session feedback notification for session '" // + sessionTitle + "'"); // return; // } // // // If the session is no longer is MY_SCHEDULE, don't notify for it. // final Uri myScheduleUri = ScheduleContract.MySchedule.buildMyScheduleUri(this); // final Cursor c = getContentResolver().query( // myScheduleUri, MySessionsExistenceQuery.PROJECTION, // MySessionsExistenceQuery.WHERE_CLAUSE, new String[]{sessionId}, null); // if (!isDebug && (c == null || !c.moveToFirst())) { // // no longer in MY_SCHEDULE // return; // } // // LOGD(TAG, "Going forward with session feedback notification for: " + sessionTitle); // final Uri sessionUri = ScheduleContract.Sessions.buildSessionUri(sessionId); // // final Resources res = getResources(); // String contentText = res.getString(R.string.session_feedback_notification_text, // sessionTitle); // // PendingIntent pi = TaskStackBuilder.create(this) // .addNextIntent(new Intent(this, MyScheduleActivity.class)) // .addNextIntent(new Intent(Intent.ACTION_VIEW, sessionUri, this, // SessionFeedbackActivity.class)) // .getPendingIntent(1, PendingIntent.FLAG_CANCEL_CURRENT); // // // this is used to synchronize deletion of notifications on phone and wear // Intent dismissalIntent = new Intent(ACTION_NOTIFICATION_DISMISSAL); // dismissalIntent.putExtra(KEY_SESSION_ID, sessionId); // PendingIntent dismissalPendingIntent = PendingIntent // .getService(this, (int) new Date().getTime(), dismissalIntent, // PendingIntent.FLAG_UPDATE_CURRENT); // // NotificationCompat.Builder notifBuilder = new NotificationCompat.Builder(this) // .setContentTitle(sessionTitle) // .setContentText(contentText) // //.setColor(getResources().getColor(R.color.theme_primary)) // // Note: setColor() is available in the support lib v21+. // // We commented it out because we want the source to compile // // against support lib v20. If you are using support lib // // v21 or above on Android L, uncomment this line. // .setTicker(res.getString(R.string.session_feedback_notification_ticker)) // .setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE) // .setLights( // SessionAlarmService.NOTIFICATION_ARGB_COLOR, // SessionAlarmService.NOTIFICATION_LED_ON_MS, // SessionAlarmService.NOTIFICATION_LED_OFF_MS) // .setSmallIcon(R.drawable.ic_stat_notification) // .setContentIntent(pi) // .setPriority(Notification.PRIORITY_MAX) // .setLocalOnly(true) // make it local to the phone // .setDeleteIntent(dismissalPendingIntent) // .setAutoCancel(true); // NotificationManager nm = (NotificationManager) getSystemService( // Context.NOTIFICATION_SERVICE); // LOGD(TAG, "Now showing session feedback notification!"); // nm.notify(sessionId, FEEDBACK_NOTIFICATION_ID, notifBuilder.build()); // setupNotificationOnWear(sessionId, sessionRoom, sessionTitle, sessionSpeakers); // } /** * Builds corresponding notification for the Wear device that is paired to this handset. This * is done by adding a Data Item to teh Data Store; the Wear device will be notified to build a * local notification. */ private void setupNotificationOnWear(String sessionId, String sessionRoom, String sessionName, String speaker) { if (!mGoogleApiClient.isConnected()) { Log.e(TAG, "setupNotificationOnWear(): Failed to send data item since there was no " + "connectivity to Google API Client"); return; } PutDataMapRequest putDataMapRequest = PutDataMapRequest.create(FeedbackUtils.getFeedbackPath(sessionId)); putDataMapRequest.getDataMap().putLong("time", new Date().getTime()); putDataMapRequest.getDataMap().putString(KEY_SESSION_ID, sessionId); putDataMapRequest.getDataMap().putString(KEY_SESSION_NAME, sessionName); putDataMapRequest.getDataMap().putString(KEY_SPEAKER_NAME, speaker); putDataMapRequest.getDataMap().putString(KEY_SESSION_ROOM, sessionRoom); PutDataRequest request = putDataMapRequest.asPutDataRequest(); Wearable.DataApi.putDataItem(mGoogleApiClient, request) .setResultCallback(new ResultCallback<DataApi.DataItemResult>() { @Override public void onResult(DataApi.DataItemResult dataItemResult) { LOGD(TAG, "setupNotificationOnWear(): Sending notification result success:" + dataItemResult.getStatus().isSuccess()); } }); } // Starred sessions are about to begin. Constructs and triggers system notification. private void notifySession(final long sessionStart, final long alarmOffset) { long currentTime = UIUtils.getCurrentTime(this); final long intervalEnd = sessionStart + MILLI_TEN_MINUTES; LOGD(TAG, "Considering notifying for time interval."); LOGD(TAG, " Interval start: " + sessionStart + "=" + (new Date(sessionStart)).toString()); LOGD(TAG, " Interval end: " + intervalEnd + "=" + (new Date(intervalEnd)).toString()); LOGD(TAG, " Current time is: " + currentTime + "=" + (new Date(currentTime)).toString()); if (sessionStart < currentTime) { LOGD(TAG, "Skipping session notification (too late -- time interval already started)"); return; } if (!PrefUtils.shouldShowSessionReminders(this)) { // skip if disabled in settings LOGD(TAG, "Skipping session notification for sessions. Disabled in settings."); return; } // Avoid repeated notifications. if (alarmOffset == UNDEFINED_ALARM_OFFSET && UIUtils.isNotificationFiredForBlock(this, ScheduleContract.Blocks.generateBlockId(sessionStart, intervalEnd))) { LOGD(TAG, "Skipping session notification (already notified)"); return; } final ContentResolver cr = getContentResolver(); LOGD(TAG, "Looking for sessions in interval " + sessionStart + " - " + intervalEnd); Cursor c = cr.query(ScheduleContract.Sessions.CONTENT_MY_SCHEDULE_URI, SessionDetailQuery.PROJECTION, ScheduleContract.Sessions.STARTING_AT_TIME_INTERVAL_SELECTION, ScheduleContract.Sessions.buildAtTimeIntervalArgs(sessionStart, intervalEnd), null); int starredCount = c.getCount(); LOGD(TAG, "# starred sessions in that interval: " + c.getCount()); String singleSessionId = null; String singleSessionRoomId = null; ArrayList<String> starredSessionTitles = new ArrayList<String>(); while (c.moveToNext()) { singleSessionId = c.getString(SessionDetailQuery.SESSION_ID); singleSessionRoomId = c.getString(SessionDetailQuery.ROOM_ID); starredSessionTitles.add(c.getString(SessionDetailQuery.SESSION_TITLE)); LOGD(TAG, "-> Title: " + c.getString(SessionDetailQuery.SESSION_TITLE)); } if (starredCount < 1) { return; } // Generates the pending intent which gets fired when the user taps on the notification. // NOTE: Use TaskStackBuilder to comply with Android's design guidelines // related to navigation from notifications. Intent baseIntent = new Intent(this, MyScheduleActivity.class); baseIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); TaskStackBuilder taskBuilder = TaskStackBuilder.create(this).addNextIntent(baseIntent); // For a single session, tapping the notification should open the session details (b/15350787) if (starredCount == 1) { taskBuilder.addNextIntent( new Intent(Intent.ACTION_VIEW, ScheduleContract.Sessions.buildSessionUri(singleSessionId))); } PendingIntent pi = taskBuilder.getPendingIntent(0, PendingIntent.FLAG_CANCEL_CURRENT); final Resources res = getResources(); String contentText; int minutesLeft = (int) (sessionStart - currentTime + 59000) / 60000; if (minutesLeft < 1) { minutesLeft = 1; } if (starredCount == 1) { contentText = res.getString(R.string.session_notification_text_1, minutesLeft); } else { contentText = res.getQuantityString(R.plurals.session_notification_text, starredCount - 1, minutesLeft, starredCount - 1); } NotificationCompat.Builder notifBuilder = new NotificationCompat.Builder(this) .setContentTitle(starredSessionTitles.get(0)).setContentText(contentText) //.setColor(getResources().getColor(R.color.theme_primary)) // Note: setColor() is available in the support lib v21+. // We commented it out because we want the source to compile // against support lib v20. If you are using support lib // v21 or above on Android L, uncomment this line. .setTicker(res.getQuantityString(R.plurals.session_notification_ticker, starredCount, starredCount)) .setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE) .setLights(SessionAlarmService.NOTIFICATION_ARGB_COLOR, SessionAlarmService.NOTIFICATION_LED_ON_MS, SessionAlarmService.NOTIFICATION_LED_OFF_MS) .setSmallIcon(R.drawable.ic_stat_notification).setContentIntent(pi) .setPriority(Notification.PRIORITY_MAX).setAutoCancel(true); if (minutesLeft > 5) { notifBuilder.addAction(R.drawable.ic_alarm_holo_dark, String.format(res.getString(R.string.snooze_x_min), 5), createSnoozeIntent(sessionStart, intervalEnd, 5)); } String bigContentTitle; if (starredCount == 1 && starredSessionTitles.size() > 0) { bigContentTitle = starredSessionTitles.get(0); } else { bigContentTitle = res.getQuantityString(R.plurals.session_notification_title, starredCount, minutesLeft, starredCount); } NotificationCompat.InboxStyle richNotification = new NotificationCompat.InboxStyle(notifBuilder) .setBigContentTitle(bigContentTitle); // Adds starred sessions starting at this time block to the notification. for (int i = 0; i < starredCount; i++) { richNotification.addLine(starredSessionTitles.get(i)); } NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); LOGD(TAG, "Now showing notification."); nm.notify(NOTIFICATION_ID, richNotification.build()); } private PendingIntent createSnoozeIntent(final long sessionStart, final long sessionEnd, final int snoozeMinutes) { Intent scheduleIntent = new Intent(SessionAlarmService.ACTION_SCHEDULE_STARRED_BLOCK, null, this, SessionAlarmService.class); scheduleIntent.putExtra(SessionAlarmService.EXTRA_SESSION_START, sessionStart); scheduleIntent.putExtra(SessionAlarmService.EXTRA_SESSION_END, sessionEnd); scheduleIntent.putExtra(SessionAlarmService.EXTRA_SESSION_ALARM_OFFSET, snoozeMinutes * MILLI_ONE_MINUTE); return PendingIntent.getService(this, 0, scheduleIntent, PendingIntent.FLAG_CANCEL_CURRENT); } private void scheduleAllStarredBlocks() { final ContentResolver cr = getContentResolver(); final Cursor c = cr.query(ScheduleContract.Sessions.CONTENT_MY_SCHEDULE_URI, new String[] { "distinct " + ScheduleContract.Sessions.SESSION_START, ScheduleContract.Sessions.SESSION_END, ScheduleContract.Sessions.SESSION_IN_MY_SCHEDULE }, null, null, null); if (c == null) { return; } while (c.moveToNext()) { final long sessionStart = c.getLong(0); final long sessionEnd = c.getLong(1); scheduleAlarm(sessionStart, sessionEnd, UNDEFINED_ALARM_OFFSET); } } public interface SessionDetailQuery { String[] PROJECTION = { ScheduleContract.Sessions.SESSION_ID, ScheduleContract.Sessions.SESSION_TITLE, ScheduleContract.Sessions.ROOM_ID, ScheduleContract.Sessions.SESSION_IN_MY_SCHEDULE }; int SESSION_ID = 0; int SESSION_TITLE = 1; int ROOM_ID = 2; } public interface MySessionsExistenceQuery { String[] PROJECTION = { ScheduleContract.MySchedule.SESSION_ID }; int SESSION_ID = 0; public static final String WHERE_CLAUSE = ScheduleContract.MySchedule.SESSION_ID + "=?"; } @Override public void onConnected(Bundle connectionHint) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Connected to Google Api Service"); } } @Override public void onConnectionSuspended(int cause) { // Ignore } @Override public void onConnectionFailed(ConnectionResult result) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Disconnected from Google Api Service"); } } }