/*
* Copyright (C) 2009 The Android Open Source Project
*
* 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 com.android.talkback.formatter;
import android.app.Notification;
import android.content.Context;
import android.os.Parcelable;
import android.os.SystemClock;
import android.text.TextUtils;
import android.view.accessibility.AccessibilityEvent;
import android.widget.RemoteViews;
import com.android.talkback.R;
import com.google.android.marvin.talkback.TalkBackService;
import com.android.talkback.Utterance;
import java.util.Iterator;
import java.util.LinkedList;
/**
* Formatter that returns an utterance to announce text replacement.
*/
public class NotificationFormatter implements EventSpeechRule.AccessibilityEventFormatter {
/** The maximum number of history items to keep track of. */
private static final int MAX_HISTORY_SIZE = 1;
private static final long MAX_HISTORY_AGE = (60 * 1000);
/** The notification history. Used to detect duplicate notifications. */
private final LinkedList<Notification> mNotificationHistory = new LinkedList<>();
@Override
public boolean format(AccessibilityEvent event, TalkBackService context, Utterance utterance) {
final Notification notification = extractNotification(event);
if (notification == null) return false;
if (isRecent(notification)) return false;
final CharSequence typeText = getTypeText(context, notification);
final CharSequence tickerText = notification.tickerText;
if (!TextUtils.isEmpty(typeText)) utterance.addSpoken(typeText);
if (!TextUtils.isEmpty(tickerText)) utterance.addSpoken(tickerText);
return !utterance.getSpoken().isEmpty();
}
/**
* Extracts a {@link Notification} from an {@link AccessibilityEvent}.
*
* @param event The event to extract from.
* @return The extracted Notification, or {@code null} on error.
*/
private Notification extractNotification(AccessibilityEvent event) {
final Parcelable parcelable = event.getParcelableData();
if (!(parcelable instanceof Notification)) {
return null;
}
return (Notification) parcelable;
}
/**
* Manages the notification history and returns whether a notification event
* is recent. Returns false if the {@link AccessibilityEvent} does not
* contain a {@link Notification}.
*
* @param notification The notification.
* @return {@code true} if the notification is recent.
*/
private synchronized boolean isRecent(Notification notification) {
final Notification foundInHistory = removeFromHistory(notification);
// If we didn't find the notification in history, set the notification
// time to now. Otherwise, keep the event time of the previous entry.
if (foundInHistory != null) {
notification.when = foundInHistory.when;
} else {
notification.when = SystemClock.uptimeMillis();
}
addToHistory(notification);
return (foundInHistory != null);
}
/**
* Searches for and removes a {@link Notification} from history.
*
* @param notification The notification to remove from history.
* @return {@code true} if the notification was found and removed.
*/
private Notification removeFromHistory(Notification notification) {
final Iterator<Notification> historyIterator = mNotificationHistory.iterator();
while (historyIterator.hasNext()) {
final Notification recentNotification = historyIterator.next();
final long age = SystemClock.uptimeMillis() - recentNotification.when;
// Don't compare against old entries, just remove them.
if (age > MAX_HISTORY_AGE) {
historyIterator.remove();
continue;
}
if (notificationsAreEqual(notification, recentNotification)) {
historyIterator.remove();
return recentNotification;
}
}
return null;
}
/**
* Adds the specified {@link Notification} to history and ensures the size
* of history does not exceed the specified limit.
*
* @param notification The notification to add.
*/
private void addToHistory(Notification notification) {
mNotificationHistory.addFirst(notification);
if (mNotificationHistory.size() > MAX_HISTORY_SIZE) {
mNotificationHistory.removeLast();
}
}
/**
* Compares two {@link Notification}s.
*
* @param first The first notification to compare.
* @param second The second notification to compare.
* @return {@code true} is the notifications are equal.
*/
private boolean notificationsAreEqual(Notification first, Notification second) {
if (!TextUtils.equals(first.tickerText, second.tickerText)) {
return false;
}
final RemoteViews firstView = first.contentView;
final RemoteViews secondView = second.contentView;
return remoteViewsAreEqual(firstView, secondView);
}
/**
* Compares to {@link RemoteViews} objects.
*
* @param firstView The first view to compare.
* @param secondView The second view to compare.
*/
private boolean remoteViewsAreEqual(RemoteViews firstView, RemoteViews secondView) {
if (firstView == secondView) {
return true;
}
if (firstView == null || secondView == null) {
return false;
}
final String firstPackage = firstView.getPackage();
final String secondPackage = secondView.getPackage();
if (!TextUtils.equals(firstPackage, secondPackage)) {
return false;
}
final int firstLayoutId = firstView.getLayoutId();
final int secondLayoutId = secondView.getLayoutId();
return firstLayoutId == secondLayoutId;
}
/**
* Returns text describing the type of {@link Notification} contained within
* an {@link AccessibilityEvent}.
*
* @param context The parent context.
* @param notification The notification.
* @return Text describing the type of notification.
*/
private CharSequence getTypeText(Context context, Notification notification) {
switch (notification.icon) {
case android.R.drawable.stat_notify_missed_call:
return context.getResources().getString(R.string.notification_type_missed_call);
case android.R.drawable.stat_notify_call_mute:
return context.getResources().getString(R.string.notification_type_status_mute);
case android.R.drawable.stat_notify_chat:
return context.getResources().getString(R.string.notification_type_status_chat);
case android.R.drawable.stat_notify_error:
return context.getResources().getString(R.string.notification_type_status_error);
case android.R.drawable.stat_notify_more:
return context.getResources().getString(R.string.notification_type_status_more);
case android.R.drawable.stat_notify_sdcard:
return context.getResources().getString(R.string.notification_type_status_sdcard);
case android.R.drawable.stat_notify_sdcard_usb:
return context.getResources().getString(
R.string.notification_type_status_sdcard_usb);
case android.R.drawable.stat_notify_sync:
return context.getResources().getString(R.string.notification_status_sync);
case android.R.drawable.stat_notify_sync_noanim:
return context.getResources().getString(
R.string.notification_type_status_sync_noanim);
case android.R.drawable.stat_notify_voicemail:
return context.getResources().getString(
R.string.notification_type_status_voicemail);
default:
return null;
}
}
}