PageRenderTime 23ms CodeModel.GetById 12ms app.highlight 8ms RepoModel.GetById 1ms app.codeStats 0ms

/TalkBack/src/com/google/android/marvin/talkback/formatter/NotificationFormatter.java

http://eyes-free.googlecode.com/
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}