/TalkBack/src/com/google/android/marvin/talkback/formatter/NotificationFormatter.java
Java | 239 lines | 123 code | 43 blank | 73 comment | 28 complexity | 0a7f45c484ccc4403846c0f0df4ad9d8 MD5 | raw file
1/* 2 * Copyright (C) 2009 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 17package com.google.android.marvin.talkback.formatter; 18 19import android.app.Notification; 20import android.content.Context; 21import android.os.Bundle; 22import android.os.Parcelable; 23import android.os.SystemClock; 24import android.text.TextUtils; 25import android.view.accessibility.AccessibilityEvent; 26import android.widget.RemoteViews; 27 28import com.google.android.marvin.talkback.Formatter; 29import com.google.android.marvin.talkback.NotificationType; 30import com.google.android.marvin.talkback.Utils; 31import com.google.android.marvin.talkback.Utterance; 32 33import java.util.Iterator; 34import java.util.LinkedList; 35 36/** 37 * Formatter that returns an utterance to announce text replacement. 38 * 39 * @author svetoslavganov@google.conm (Svetoslav R. Ganov) 40 * @author alanv@google.com (Alan Viverette) 41 */ 42public class NotificationFormatter implements Formatter { 43 private static final String SEPARATOR = " "; 44 45 /** The maximum number of history items to keep track of. */ 46 private static final int MAX_HISTORY_SIZE = 1; 47 48 /** 49 * The maximum age of a history item before it's flushed. Repeating 50 * notifications will be spoken every {@link #MAX_HISTORY_AGE} milliseconds. 51 */ 52 private static final long MAX_HISTORY_AGE = (60 * 1000); 53 54 /** The notification history. Used to detect duplicate notifications. */ 55 private final LinkedList<Notification> mNotificationHistory = new LinkedList<Notification>(); 56 57 @Override 58 public boolean format(AccessibilityEvent event, Context context, Utterance utterance, 59 Bundle args) { 60 if (isRecentNotification(event)) { 61 return false; 62 } 63 64 final StringBuilder utteranceText = utterance.getText(); 65 final CharSequence typeText = getTypeText(context, event); 66 67 if (!TextUtils.isEmpty(typeText)) { 68 utteranceText.append(typeText); 69 utteranceText.append(SEPARATOR); 70 } 71 72 final CharSequence eventText = Utils.getEventText(context, event); 73 74 if (!TextUtils.isEmpty(eventText)) { 75 utteranceText.append(eventText); 76 utteranceText.append(SEPARATOR); 77 } 78 79 return true; 80 } 81 82 /** 83 * Manages the notification history and returns whether a notification event 84 * is recent. Returns false if the {@link AccessibilityEvent} does not 85 * contain a {@link Notification}. 86 * 87 * @param event The accessibility event that contains the notification. 88 * @return {@code true} if the notification is recent. 89 */ 90 private synchronized boolean isRecentNotification(AccessibilityEvent event) { 91 final Parcelable parcelable = event.getParcelableData(); 92 93 if (!(parcelable instanceof Notification)) { 94 return false; 95 } 96 97 final Notification notification = (Notification) parcelable; 98 final Notification foundInHistory = removeFromHistory(notification); 99 100 // If we didn't find the notification in history, set the notification 101 // time to now. Otherwise, keep the event time of the previous entry. 102 if (foundInHistory != null) { 103 notification.when = foundInHistory.when; 104 } else { 105 notification.when = SystemClock.uptimeMillis(); 106 } 107 108 addToHistory(notification); 109 110 return (foundInHistory != null); 111 } 112 113 /** 114 * Searches for and removes a {@link Notification} from history. 115 * 116 * @param notification The notification to remove from history. 117 * @return {@code true} if the notification was found and removed. 118 */ 119 private Notification removeFromHistory(Notification notification) { 120 final Iterator<Notification> historyIterator = mNotificationHistory.iterator(); 121 122 while (historyIterator.hasNext()) { 123 final Notification recentNotification = historyIterator.next(); 124 final long age = SystemClock.uptimeMillis() - recentNotification.when; 125 126 // Don't compare against old entries, just remove them. 127 if (age > MAX_HISTORY_AGE) { 128 historyIterator.remove(); 129 continue; 130 } 131 132 if (notificationsAreEqual(notification, recentNotification)) { 133 historyIterator.remove(); 134 return recentNotification; 135 } 136 } 137 138 return null; 139 } 140 141 /** 142 * Adds the specified {@link Notification} to history and ensures the size 143 * of history does not exceed the specified limit. 144 * 145 * @param notification The notification to add. 146 */ 147 private void addToHistory(Notification notification) { 148 mNotificationHistory.addFirst(notification); 149 150 if (mNotificationHistory.size() > MAX_HISTORY_SIZE) { 151 mNotificationHistory.removeLast(); 152 } 153 } 154 155 /** 156 * Compares two {@link Notification}s. 157 * 158 * @param first The first notification to compare. 159 * @param second The second notification to compare. 160 * @return {@code true} is the notifications are equal. 161 */ 162 private boolean notificationsAreEqual(Notification first, Notification second) { 163 if (!TextUtils.equals(first.tickerText, second.tickerText)) { 164 return false; 165 } 166 167 final RemoteViews firstView = first.contentView; 168 final RemoteViews secondView = second.contentView; 169 170 if (!remoteViewsAreEqual(firstView, secondView)) { 171 return false; 172 } 173 174 return true; 175 } 176 177 /** 178 * Compares to {@link RemoteViews} objects. 179 * 180 * @param firstView The first view to compare. 181 * @param secondView The second view to compare. 182 */ 183 private boolean remoteViewsAreEqual(RemoteViews firstView, RemoteViews secondView) { 184 if (firstView == secondView) { 185 return true; 186 } 187 188 if (firstView == null || secondView == null) { 189 return false; 190 } 191 192 final String firstPackage = firstView.getPackage(); 193 final String secondPackage = secondView.getPackage(); 194 195 if (!TextUtils.equals(firstPackage, secondPackage)) { 196 return false; 197 } 198 199 final int firstLayoutId = firstView.getLayoutId(); 200 final int secondLayoutId = secondView.getLayoutId(); 201 202 if (firstLayoutId != secondLayoutId) { 203 return false; 204 } 205 206 return true; 207 } 208 209 /** 210 * Returns text describing the type of {@link Notification} contained within 211 * an {@link AccessibilityEvent}. 212 * 213 * @param context The parent context. 214 * @param event The event containing the notification. 215 * @return Text describing the type of notification. 216 */ 217 private CharSequence getTypeText(Context context, AccessibilityEvent event) { 218 final Parcelable parcelable = event.getParcelableData(); 219 if (!(parcelable instanceof Notification)) { 220 return null; 221 } 222 223 final Notification notification = (Notification) parcelable; 224 final int icon = notification.icon; 225 226 // TODO(alanv): This may be obsolete now that we keep track of duplicate 227 // notifications. 228 if (icon == NotificationType.ICON_PHONE_CALL) { 229 return null; 230 } 231 232 final NotificationType type = NotificationType.getNotificationTypeFromIcon(icon); 233 if (type == null) { 234 return null; 235 } 236 237 return context.getResources().getString(type.getValue()); 238 } 239}