1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.example.android.wearable.quiz; 18 19 import static com.example.android.wearable.quiz.Constants.ANSWERS; 20 import static com.example.android.wearable.quiz.Constants.CONNECT_TIMEOUT_MS; 21 import static com.example.android.wearable.quiz.Constants.CORRECT_ANSWER_INDEX; 22 import static com.example.android.wearable.quiz.Constants.NUM_CORRECT; 23 import static com.example.android.wearable.quiz.Constants.NUM_INCORRECT; 24 import static com.example.android.wearable.quiz.Constants.NUM_SKIPPED; 25 import static com.example.android.wearable.quiz.Constants.QUESTION; 26 import static com.example.android.wearable.quiz.Constants.QUESTION_INDEX; 27 import static com.example.android.wearable.quiz.Constants.QUESTION_WAS_ANSWERED; 28 import static com.example.android.wearable.quiz.Constants.QUESTION_WAS_DELETED; 29 import static com.example.android.wearable.quiz.Constants.QUIZ_ENDED_PATH; 30 import static com.example.android.wearable.quiz.Constants.QUIZ_EXITED_PATH; 31 32 import android.app.Notification; 33 import android.app.NotificationManager; 34 import android.app.PendingIntent; 35 import android.content.Intent; 36 import android.net.Uri; 37 import android.text.SpannableStringBuilder; 38 import android.text.style.ForegroundColorSpan; 39 import android.util.Log; 40 41 import com.google.android.gms.common.ConnectionResult; 42 import com.google.android.gms.common.api.GoogleApiClient; 43 import com.google.android.gms.common.data.FreezableUtils; 44 import com.google.android.gms.wearable.DataEvent; 45 import com.google.android.gms.wearable.DataEventBuffer; 46 import com.google.android.gms.wearable.DataItem; 47 import com.google.android.gms.wearable.DataMap; 48 import com.google.android.gms.wearable.DataMapItem; 49 import com.google.android.gms.wearable.MessageEvent; 50 import com.google.android.gms.wearable.Wearable; 51 import com.google.android.gms.wearable.WearableListenerService; 52 53 import java.util.Collections; 54 import java.util.HashMap; 55 import java.util.List; 56 import java.util.Map; 57 import java.util.concurrent.TimeUnit; 58 59 /** 60 * Listens to changes in DataItems, which represent quiz questions. 61 * If a new question is created, this builds a new notification for it. 62 * Otherwise, if a question is deleted, this cancels the corresponding notification. 63 * 64 * When the quiz ends, this listener receives a message telling it to create an end-of-quiz report. 65 */ 66 public class QuizListenerService extends WearableListenerService { 67 private static final String TAG = "QuizSample"; 68 private static final int QUIZ_REPORT_NOTIF_ID = -1; // Never used by question notifications. 69 private static final Map<Integer, Integer> questionNumToDrawableId; 70 71 static { 72 Map<Integer, Integer> temp = new HashMap<Integer, Integer>(4); 73 temp.put(0, R.drawable.ic_choice_a); 74 temp.put(1, R.drawable.ic_choice_b); 75 temp.put(2, R.drawable.ic_choice_c); 76 temp.put(3, R.drawable.ic_choice_d); 77 questionNumToDrawableId = Collections.unmodifiableMap(temp); 78 } 79 80 @Override 81 public void onDataChanged(DataEventBuffer dataEvents) { 82 final List<DataEvent> events = FreezableUtils.freezeIterable(dataEvents); 83 dataEvents.close(); 84 85 GoogleApiClient googleApiClient = new GoogleApiClient.Builder(this) 86 .addApi(Wearable.API) 87 .build(); 88 89 ConnectionResult connectionResult = googleApiClient.blockingConnect(CONNECT_TIMEOUT_MS, 90 TimeUnit.MILLISECONDS); 91 if (!connectionResult.isSuccess()) { 92 Log.e(TAG, "QuizListenerService failed to connect to GoogleApiClient."); 93 return; 94 } 95 96 for (DataEvent event : events) { 97 if (event.getType() == DataEvent.TYPE_CHANGED) { 98 DataItem dataItem = event.getDataItem(); 99 DataMap dataMap = DataMapItem.fromDataItem(dataItem).getDataMap(); 100 if (dataMap.getBoolean(QUESTION_WAS_ANSWERED) 101 || dataMap.getBoolean(QUESTION_WAS_DELETED)) { 102 // Ignore the change in data; it is used in MainActivity to update 103 // the question's status (i.e. was the answer right or wrong or left blank). 104 continue; 105 } 106 String question = dataMap.getString(QUESTION); 107 int questionIndex = dataMap.getInt(QUESTION_INDEX); 108 int questionNum = questionIndex + 1; 109 String[] answers = dataMap.getStringArray(ANSWERS); 110 int correctAnswerIndex = dataMap.getInt(CORRECT_ANSWER_INDEX); 111 Intent deleteOperation = new Intent(this, DeleteQuestionService.class); 112 deleteOperation.setData(dataItem.getUri()); 113 PendingIntent deleteIntent = PendingIntent.getService(this, 0, 114 deleteOperation, PendingIntent.FLAG_UPDATE_CURRENT); 115 // First page of notification contains question as Big Text. 116 Notification.BigTextStyle bigTextStyle = new Notification.BigTextStyle() 117 .setBigContentTitle(getString(R.string.question, questionNum)) 118 .bigText(question); 119 Notification.Builder builder = new Notification.Builder(this) 120 .setStyle(bigTextStyle) 121 .setSmallIcon(R.drawable.ic_launcher) 122 .setLocalOnly(true) 123 .setDeleteIntent(deleteIntent); 124 125 // Add answers as actions. 126 Notification.WearableExtender wearableOptions = new Notification.WearableExtender(); 127 for (int i = 0; i < answers.length; i++) { 128 Notification answerPage = new Notification.Builder(this) 129 .setContentTitle(question) 130 .setContentText(answers[i]) 131 .extend(new Notification.WearableExtender() 132 .setContentAction(i)) 133 .build(); 134 135 boolean correct = (i == correctAnswerIndex); 136 Intent updateOperation = new Intent(this, UpdateQuestionService.class); 137 // Give each intent a unique action. 138 updateOperation.setAction("question_" + questionIndex + "_answer_" + i); 139 updateOperation.setData(dataItem.getUri()); 140 updateOperation.putExtra(UpdateQuestionService.EXTRA_QUESTION_INDEX, 141 questionIndex); 142 updateOperation.putExtra(UpdateQuestionService.EXTRA_QUESTION_CORRECT, correct); 143 PendingIntent updateIntent = PendingIntent.getService(this, 0, updateOperation, 144 PendingIntent.FLAG_UPDATE_CURRENT); 145 Notification.Action action = new Notification.Action.Builder( 146 questionNumToDrawableId.get(i), null, updateIntent) 147 .build(); 148 wearableOptions.addAction(action).addPage(answerPage); 149 } 150 builder.extend(wearableOptions); 151 Notification notification = builder.build(); 152 ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)) 153 .notify(questionIndex, notification); 154 } else if (event.getType() == DataEvent.TYPE_DELETED) { 155 Uri uri = event.getDataItem().getUri(); 156 // URI's are of the form "/question/0", "/question/1" etc. 157 // We use the question index as the notification id. 158 int notificationId = Integer.parseInt(uri.getLastPathSegment()); 159 ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)) 160 .cancel(notificationId); 161 } 162 // Delete the quiz report, if it exists. 163 ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)) 164 .cancel(QUIZ_REPORT_NOTIF_ID); 165 } 166 googleApiClient.disconnect(); 167 } 168 169 @Override 170 public void onMessageReceived(MessageEvent messageEvent) { 171 String path = messageEvent.getPath(); 172 if (path.equals(QUIZ_EXITED_PATH)) { 173 // Remove any lingering question notifications. 174 ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)).cancelAll(); 175 } 176 if (path.equals(QUIZ_ENDED_PATH) || path.equals(QUIZ_EXITED_PATH)) { 177 // Quiz ended - display overall results. 178 DataMap dataMap = DataMap.fromByteArray(messageEvent.getData()); 179 int numCorrect = dataMap.getInt(NUM_CORRECT); 180 int numIncorrect = dataMap.getInt(NUM_INCORRECT); 181 int numSkipped = dataMap.getInt(NUM_SKIPPED); 182 183 Notification.Builder builder = new Notification.Builder(this) 184 .setContentTitle(getString(R.string.quiz_report)) 185 .setSmallIcon(R.drawable.ic_launcher) 186 .setLocalOnly(true); 187 SpannableStringBuilder quizReportText = new SpannableStringBuilder(); 188 appendColored(quizReportText, String.valueOf(numCorrect), R.color.dark_green); 189 quizReportText.append(" " + getString(R.string.correct) + "\n"); 190 appendColored(quizReportText, String.valueOf(numIncorrect), R.color.dark_red); 191 quizReportText.append(" " + getString(R.string.incorrect) + "\n"); 192 appendColored(quizReportText, String.valueOf(numSkipped), R.color.dark_yellow); 193 quizReportText.append(" " + getString(R.string.skipped) + "\n"); 194 195 builder.setContentText(quizReportText); 196 if (!path.equals(QUIZ_EXITED_PATH)) { 197 // Don't add reset option if user exited quiz (there might not be a quiz to reset!). 198 builder.addAction(R.drawable.ic_launcher, 199 getString(R.string.reset_quiz), getResetQuizPendingIntent()); 200 } 201 ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)) 202 .notify(QUIZ_REPORT_NOTIF_ID, builder.build()); 203 } 204 } 205 206 private void appendColored(SpannableStringBuilder builder, String text, int colorResId) { 207 builder.append(text).setSpan(new ForegroundColorSpan(getResources().getColor(colorResId)), 208 builder.length() - text.length(), builder.length(), 0); 209 } 210 211 /** 212 * Returns a PendingIntent that will send a message to the phone to reset the quiz when fired. 213 */ 214 private PendingIntent getResetQuizPendingIntent() { 215 Intent intent = new Intent(QuizReportActionService.ACTION_RESET_QUIZ) 216 .setClass(this, QuizReportActionService.class); 217 return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); 218 } 219 } 220