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.wearable.DataEvent; 44 import com.google.android.gms.wearable.DataEventBuffer; 45 import com.google.android.gms.wearable.DataItem; 46 import com.google.android.gms.wearable.DataMap; 47 import com.google.android.gms.wearable.DataMapItem; 48 import com.google.android.gms.wearable.MessageEvent; 49 import com.google.android.gms.wearable.Wearable; 50 import com.google.android.gms.wearable.WearableListenerService; 51 52 import java.util.Collections; 53 import java.util.HashMap; 54 import java.util.List; 55 import java.util.Map; 56 import java.util.concurrent.TimeUnit; 57 58 /** 59 * Listens to changes in DataItems, which represent quiz questions. 60 * If a new question is created, this builds a new notification for it. 61 * Otherwise, if a question is deleted, this cancels the corresponding notification. 62 * 63 * When the quiz ends, this listener receives a message telling it to create an end-of-quiz report. 64 */ 65 public class QuizListenerService extends WearableListenerService { 66 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 GoogleApiClient googleApiClient = new GoogleApiClient.Builder(this) 83 .addApi(Wearable.API) 84 .build(); 85 86 ConnectionResult connectionResult = googleApiClient.blockingConnect(CONNECT_TIMEOUT_MS, 87 TimeUnit.MILLISECONDS); 88 if (!connectionResult.isSuccess()) { 89 Log.e(TAG, "QuizListenerService failed to connect to GoogleApiClient."); 90 return; 91 } 92 93 for (DataEvent event : dataEvents) { 94 if (event.getType() == DataEvent.TYPE_CHANGED) { 95 DataItem dataItem = event.getDataItem(); 96 DataMap dataMap = DataMapItem.fromDataItem(dataItem).getDataMap(); 97 if (dataMap.getBoolean(QUESTION_WAS_ANSWERED) 98 || dataMap.getBoolean(QUESTION_WAS_DELETED)) { 99 // Ignore the change in data; it is used in MainActivity to update 100 // the question's status (i.e. was the answer right or wrong or left blank). 101 continue; 102 } 103 String question = dataMap.getString(QUESTION); 104 int questionIndex = dataMap.getInt(QUESTION_INDEX); 105 int questionNum = questionIndex + 1; 106 String[] answers = dataMap.getStringArray(ANSWERS); 107 int correctAnswerIndex = dataMap.getInt(CORRECT_ANSWER_INDEX); 108 Intent deleteOperation = new Intent(this, DeleteQuestionService.class); 109 deleteOperation.setData(dataItem.getUri()); 110 PendingIntent deleteIntent = PendingIntent.getService(this, 0, 111 deleteOperation, PendingIntent.FLAG_UPDATE_CURRENT); 112 // First page of notification contains question as Big Text. 113 Notification.BigTextStyle bigTextStyle = new Notification.BigTextStyle() 114 .setBigContentTitle(getString(R.string.question, questionNum)) 115 .bigText(question); 116 Notification.Builder builder = new Notification.Builder(this) 117 .setStyle(bigTextStyle) 118 .setSmallIcon(R.drawable.ic_launcher) 119 .setLocalOnly(true) 120 .setDeleteIntent(deleteIntent); 121 122 // Add answers as actions. 123 Notification.WearableExtender wearableOptions = new Notification.WearableExtender(); 124 for (int i = 0; i < answers.length; i++) { 125 Notification answerPage = new Notification.Builder(this) 126 .setContentTitle(question) 127 .setContentText(answers[i]) 128 .extend(new Notification.WearableExtender() 129 .setContentAction(i)) 130 .build(); 131 132 boolean correct = (i == correctAnswerIndex); 133 Intent updateOperation = new Intent(this, UpdateQuestionService.class); 134 // Give each intent a unique action. 135 updateOperation.setAction("question_" + questionIndex + "_answer_" + i); 136 updateOperation.setData(dataItem.getUri()); 137 updateOperation.putExtra(UpdateQuestionService.EXTRA_QUESTION_INDEX, 138 questionIndex); 139 updateOperation.putExtra(UpdateQuestionService.EXTRA_QUESTION_CORRECT, correct); 140 PendingIntent updateIntent = PendingIntent.getService(this, 0, updateOperation, 141 PendingIntent.FLAG_UPDATE_CURRENT); 142 Notification.Action action = new Notification.Action.Builder( 143 questionNumToDrawableId.get(i), null, updateIntent) 144 .build(); 145 wearableOptions.addAction(action).addPage(answerPage); 146 } 147 builder.extend(wearableOptions); 148 Notification notification = builder.build(); 149 ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)) 150 .notify(questionIndex, notification); 151 } else if (event.getType() == DataEvent.TYPE_DELETED) { 152 Uri uri = event.getDataItem().getUri(); 153 // URI's are of the form "/question/0", "/question/1" etc. 154 // We use the question index as the notification id. 155 int notificationId = Integer.parseInt(uri.getLastPathSegment()); 156 ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)) 157 .cancel(notificationId); 158 } 159 // Delete the quiz report, if it exists. 160 ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)) 161 .cancel(QUIZ_REPORT_NOTIF_ID); 162 } 163 googleApiClient.disconnect(); 164 } 165 166 @Override 167 public void onMessageReceived(MessageEvent messageEvent) { 168 String path = messageEvent.getPath(); 169 if (path.equals(QUIZ_EXITED_PATH)) { 170 // Remove any lingering question notifications. 171 ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)).cancelAll(); 172 } 173 if (path.equals(QUIZ_ENDED_PATH) || path.equals(QUIZ_EXITED_PATH)) { 174 // Quiz ended - display overall results. 175 DataMap dataMap = DataMap.fromByteArray(messageEvent.getData()); 176 int numCorrect = dataMap.getInt(NUM_CORRECT); 177 int numIncorrect = dataMap.getInt(NUM_INCORRECT); 178 int numSkipped = dataMap.getInt(NUM_SKIPPED); 179 180 Notification.Builder builder = new Notification.Builder(this) 181 .setContentTitle(getString(R.string.quiz_report)) 182 .setSmallIcon(R.drawable.ic_launcher) 183 .setLocalOnly(true); 184 SpannableStringBuilder quizReportText = new SpannableStringBuilder(); 185 appendColored(quizReportText, String.valueOf(numCorrect), R.color.dark_green); 186 quizReportText.append(" " + getString(R.string.correct) + "\n"); 187 appendColored(quizReportText, String.valueOf(numIncorrect), R.color.dark_red); 188 quizReportText.append(" " + getString(R.string.incorrect) + "\n"); 189 appendColored(quizReportText, String.valueOf(numSkipped), R.color.dark_yellow); 190 quizReportText.append(" " + getString(R.string.skipped) + "\n"); 191 192 builder.setContentText(quizReportText); 193 if (!path.equals(QUIZ_EXITED_PATH)) { 194 // Don't add reset option if user exited quiz (there might not be a quiz to reset!). 195 builder.addAction(R.drawable.ic_launcher, 196 getString(R.string.reset_quiz), getResetQuizPendingIntent()); 197 } 198 ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)) 199 .notify(QUIZ_REPORT_NOTIF_ID, builder.build()); 200 } 201 } 202 203 private void appendColored(SpannableStringBuilder builder, String text, int colorResId) { 204 builder.append(text).setSpan(new ForegroundColorSpan(getResources().getColor(colorResId)), 205 builder.length() - text.length(), builder.length(), 0); 206 } 207 208 /** 209 * Returns a PendingIntent that will send a message to the phone to reset the quiz when fired. 210 */ 211 private PendingIntent getResetQuizPendingIntent() { 212 Intent intent = new Intent(QuizReportActionService.ACTION_RESET_QUIZ) 213 .setClass(this, QuizReportActionService.class); 214 return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); 215 } 216 } 217