package com.android.systemui.statusbar; /* * Copyright (C) 2017 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 */ import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper; import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.content.Context; import android.content.res.Resources; import android.graphics.Typeface; import android.metrics.LogMaker; import android.os.Bundle; import android.provider.Settings; import android.service.notification.SnoozeCriterion; import android.service.notification.StatusBarNotification; import android.text.SpannableString; import android.text.style.StyleSpan; import android.util.AttributeSet; import android.util.KeyValueListParser; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import com.android.systemui.Interpolators; import com.android.systemui.R; public class NotificationSnooze extends LinearLayout implements NotificationGuts.GutsContent, View.OnClickListener { private static final String TAG = "NotificationSnooze"; /** * If this changes more number increases, more assistant action resId's should be defined for * accessibility purposes, see {@link #setSnoozeOptions(List)} */ private static final int MAX_ASSISTANT_SUGGESTIONS = 1; private static final String KEY_DEFAULT_SNOOZE = "default"; private static final String KEY_OPTIONS = "options_array"; private static final LogMaker OPTIONS_OPEN_LOG = new LogMaker(MetricsEvent.NOTIFICATION_SNOOZE_OPTIONS) .setType(MetricsEvent.TYPE_OPEN); private static final LogMaker OPTIONS_CLOSE_LOG = new LogMaker(MetricsEvent.NOTIFICATION_SNOOZE_OPTIONS) .setType(MetricsEvent.TYPE_CLOSE); private static final LogMaker UNDO_LOG = new LogMaker(MetricsEvent.NOTIFICATION_UNDO_SNOOZE) .setType(MetricsEvent.TYPE_ACTION); private NotificationGuts mGutsContainer; private NotificationSwipeActionHelper mSnoozeListener; private StatusBarNotification mSbn; private TextView mSelectedOptionText; private TextView mUndoButton; private ImageView mExpandButton; private View mDivider; private ViewGroup mSnoozeOptionContainer; private List mSnoozeOptions; private int mCollapsedHeight; private SnoozeOption mDefaultOption; private SnoozeOption mSelectedOption; private boolean mSnoozing; private boolean mExpanded; private AnimatorSet mExpandAnimation; private KeyValueListParser mParser; private final static int[] sAccessibilityActions = { R.id.action_snooze_shorter, R.id.action_snooze_short, R.id.action_snooze_long, R.id.action_snooze_longer, }; private MetricsLogger mMetricsLogger = new MetricsLogger(); public NotificationSnooze(Context context, AttributeSet attrs) { super(context, attrs); mParser = new KeyValueListParser(','); } @VisibleForTesting SnoozeOption getDefaultOption() { return mDefaultOption; } @VisibleForTesting void setKeyValueListParser(KeyValueListParser parser) { mParser = parser; } @Override protected void onFinishInflate() { super.onFinishInflate(); mCollapsedHeight = getResources().getDimensionPixelSize(R.dimen.snooze_snackbar_min_height); findViewById(R.id.notification_snooze).setOnClickListener(this); mSelectedOptionText = (TextView) findViewById(R.id.snooze_option_default); mUndoButton = (TextView) findViewById(R.id.undo); mUndoButton.setOnClickListener(this); mExpandButton = (ImageView) findViewById(R.id.expand_button); mDivider = findViewById(R.id.divider); mDivider.setAlpha(0f); mSnoozeOptionContainer = (ViewGroup) findViewById(R.id.snooze_options); mSnoozeOptionContainer.setVisibility(View.INVISIBLE); mSnoozeOptionContainer.setAlpha(0f); // Create the different options based on list mSnoozeOptions = getDefaultSnoozeOptions(); createOptionViews(); setSelected(mDefaultOption, false); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); logOptionSelection(MetricsEvent.NOTIFICATION_SNOOZE_CLICKED, mDefaultOption); } @Override public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); if (mGutsContainer != null && mGutsContainer.isExposed()) { if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { event.getText().add(mSelectedOptionText.getText()); } } } @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); info.addAction(new AccessibilityAction(R.id.action_snooze_undo, getResources().getString(R.string.snooze_undo))); int count = mSnoozeOptions.size(); for (int i = 0; i < count; i++) { AccessibilityAction action = mSnoozeOptions.get(i).getAccessibilityAction(); if (action != null) { info.addAction(action); } } } @Override public boolean performAccessibilityActionInternal(int action, Bundle arguments) { if (super.performAccessibilityActionInternal(action, arguments)) { return true; } if (action == R.id.action_snooze_undo) { undoSnooze(mUndoButton); return true; } for (int i = 0; i < mSnoozeOptions.size(); i++) { SnoozeOption so = mSnoozeOptions.get(i); if (so.getAccessibilityAction() != null && so.getAccessibilityAction().getId() == action) { setSelected(so, true); return true; } } return false; } public void setSnoozeOptions(final List snoozeList) { if (snoozeList == null) { return; } mSnoozeOptions.clear(); mSnoozeOptions = getDefaultSnoozeOptions(); final int count = Math.min(MAX_ASSISTANT_SUGGESTIONS, snoozeList.size()); for (int i = 0; i < count; i++) { SnoozeCriterion sc = snoozeList.get(i); AccessibilityAction action = new AccessibilityAction( R.id.action_snooze_assistant_suggestion_1, sc.getExplanation()); mSnoozeOptions.add(new NotificationSnoozeOption(sc, 0, sc.getExplanation(), sc.getConfirmation(), action)); } createOptionViews(); } public boolean isExpanded() { return mExpanded; } public void setSnoozeListener(NotificationSwipeActionHelper listener) { mSnoozeListener = listener; } public void setStatusBarNotification(StatusBarNotification sbn) { mSbn = sbn; } @VisibleForTesting ArrayList getDefaultSnoozeOptions() { final Resources resources = getContext().getResources(); ArrayList options = new ArrayList<>(); try { final String config = Settings.Global.getString(getContext().getContentResolver(), Settings.Global.NOTIFICATION_SNOOZE_OPTIONS); mParser.setString(config); } catch (IllegalArgumentException e) { Log.e(TAG, "Bad snooze constants"); } final int defaultSnooze = mParser.getInt(KEY_DEFAULT_SNOOZE, resources.getInteger(R.integer.config_notification_snooze_time_default)); final int[] snoozeTimes = parseIntArray(KEY_OPTIONS, resources.getIntArray(R.array.config_notification_snooze_times)); for (int i = 0; i < snoozeTimes.length && i < sAccessibilityActions.length; i++) { int snoozeTime = snoozeTimes[i]; SnoozeOption option = createOption(snoozeTime, sAccessibilityActions[i]); if (i == 0 || snoozeTime == defaultSnooze) { mDefaultOption = option; } options.add(option); } return options; } @VisibleForTesting int[] parseIntArray(final String key, final int[] defaultArray) { final String value = mParser.getString(key, null); if (value != null) { try { return Arrays.stream(value.split(":")).map(String::trim).mapToInt( Integer::parseInt).toArray(); } catch (NumberFormatException e) { return defaultArray; } } else { return defaultArray; } } private SnoozeOption createOption(int minutes, int accessibilityActionId) { Resources res = getResources(); boolean showInHours = minutes >= 60; int pluralResId = showInHours ? R.plurals.snoozeHourOptions : R.plurals.snoozeMinuteOptions; int count = showInHours ? (minutes / 60) : minutes; String description = res.getQuantityString(pluralResId, count, count); String resultText = String.format(res.getString(R.string.snoozed_for_time), description); SpannableString string = new SpannableString(resultText); string.setSpan(new StyleSpan(Typeface.BOLD), resultText.length() - description.length(), resultText.length(), 0 /* flags */); AccessibilityAction action = new AccessibilityAction(accessibilityActionId, description); return new NotificationSnoozeOption(null, minutes, description, string, action); } private void createOptionViews() { mSnoozeOptionContainer.removeAllViews(); LayoutInflater inflater = (LayoutInflater) getContext().getSystemService( Context.LAYOUT_INFLATER_SERVICE); for (int i = 0; i < mSnoozeOptions.size(); i++) { SnoozeOption option = mSnoozeOptions.get(i); TextView tv = (TextView) inflater.inflate(R.layout.notification_snooze_option, mSnoozeOptionContainer, false); mSnoozeOptionContainer.addView(tv); tv.setText(option.getDescription()); tv.setTag(option); tv.setOnClickListener(this); } } private void hideSelectedOption() { final int childCount = mSnoozeOptionContainer.getChildCount(); for (int i = 0; i < childCount; i++) { final View child = mSnoozeOptionContainer.getChildAt(i); child.setVisibility(child.getTag() == mSelectedOption ? View.GONE : View.VISIBLE); } } private void showSnoozeOptions(boolean show) { int drawableId = show ? com.android.internal.R.drawable.ic_collapse_notification : com.android.internal.R.drawable.ic_expand_notification; mExpandButton.setImageResource(drawableId); if (mExpanded != show) { mExpanded = show; animateSnoozeOptions(show); if (mGutsContainer != null) { mGutsContainer.onHeightChanged(); } } } private void animateSnoozeOptions(boolean show) { if (mExpandAnimation != null) { mExpandAnimation.cancel(); } ObjectAnimator dividerAnim = ObjectAnimator.ofFloat(mDivider, View.ALPHA, mDivider.getAlpha(), show ? 1f : 0f); ObjectAnimator optionAnim = ObjectAnimator.ofFloat(mSnoozeOptionContainer, View.ALPHA, mSnoozeOptionContainer.getAlpha(), show ? 1f : 0f); mSnoozeOptionContainer.setVisibility(View.VISIBLE); mExpandAnimation = new AnimatorSet(); mExpandAnimation.playTogether(dividerAnim, optionAnim); mExpandAnimation.setDuration(150); mExpandAnimation.setInterpolator(show ? Interpolators.ALPHA_IN : Interpolators.ALPHA_OUT); mExpandAnimation.addListener(new AnimatorListenerAdapter() { boolean cancelled = false; @Override public void onAnimationCancel(Animator animation) { cancelled = true; } @Override public void onAnimationEnd(Animator animation) { if (!show && !cancelled) { mSnoozeOptionContainer.setVisibility(View.INVISIBLE); mSnoozeOptionContainer.setAlpha(0f); } } }); mExpandAnimation.start(); } private void setSelected(SnoozeOption option, boolean userAction) { mSelectedOption = option; mSelectedOptionText.setText(option.getConfirmation()); showSnoozeOptions(false); hideSelectedOption(); sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); if (userAction) { logOptionSelection(MetricsEvent.NOTIFICATION_SELECT_SNOOZE, option); } } private void logOptionSelection(int category, SnoozeOption option) { int index = mSnoozeOptions.indexOf(option); long duration = TimeUnit.MINUTES.toMillis(option.getMinutesToSnoozeFor()); mMetricsLogger.write(new LogMaker(category) .setType(MetricsEvent.TYPE_ACTION) .addTaggedData(MetricsEvent.FIELD_NOTIFICATION_SNOOZE_INDEX, index) .addTaggedData(MetricsEvent.FIELD_NOTIFICATION_SNOOZE_DURATION_MS, duration)); } @Override public void onClick(View v) { if (mGutsContainer != null) { mGutsContainer.resetFalsingCheck(); } final int id = v.getId(); final SnoozeOption tag = (SnoozeOption) v.getTag(); if (tag != null) { setSelected(tag, true); } else if (id == R.id.notification_snooze) { // Toggle snooze options showSnoozeOptions(!mExpanded); mMetricsLogger.write(!mExpanded ? OPTIONS_OPEN_LOG : OPTIONS_CLOSE_LOG); } else { // Undo snooze was selected undoSnooze(v); mMetricsLogger.write(UNDO_LOG); } } private void undoSnooze(View v) { mSelectedOption = null; int[] parentLoc = new int[2]; int[] targetLoc = new int[2]; mGutsContainer.getLocationOnScreen(parentLoc); v.getLocationOnScreen(targetLoc); final int centerX = v.getWidth() / 2; final int centerY = v.getHeight() / 2; final int x = targetLoc[0] - parentLoc[0] + centerX; final int y = targetLoc[1] - parentLoc[1] + centerY; showSnoozeOptions(false); mGutsContainer.closeControls(x, y, false /* save */, false /* force */); } @Override public int getActualHeight() { return mExpanded ? getHeight() : mCollapsedHeight; } @Override public boolean willBeRemoved() { return mSnoozing; } @Override public View getContentView() { // Reset the view before use setSelected(mDefaultOption, false); return this; } @Override public void setGutsParent(NotificationGuts guts) { mGutsContainer = guts; } @Override public boolean handleCloseControls(boolean save, boolean force) { if (mExpanded && !force) { // Collapse expanded state on outside touch showSnoozeOptions(false); return true; } else if (mSnoozeListener != null && mSelectedOption != null) { // Snooze option selected so commit it mSnoozing = true; mSnoozeListener.snooze(mSbn, mSelectedOption); return true; } else { // The view should actually be closed setSelected(mSnoozeOptions.get(0), false); return false; // Return false here so that guts handles closing the view } } @Override public boolean isLeavebehind() { return true; } public class NotificationSnoozeOption implements SnoozeOption { private SnoozeCriterion mCriterion; private int mMinutesToSnoozeFor; private CharSequence mDescription; private CharSequence mConfirmation; private AccessibilityAction mAction; public NotificationSnoozeOption(SnoozeCriterion sc, int minToSnoozeFor, CharSequence description, CharSequence confirmation, AccessibilityAction action) { mCriterion = sc; mMinutesToSnoozeFor = minToSnoozeFor; mDescription = description; mConfirmation = confirmation; mAction = action; } @Override public SnoozeCriterion getSnoozeCriterion() { return mCriterion; } @Override public CharSequence getDescription() { return mDescription; } @Override public CharSequence getConfirmation() { return mConfirmation; } @Override public int getMinutesToSnoozeFor() { return mMinutesToSnoozeFor; } @Override public AccessibilityAction getAccessibilityAction() { return mAction; } } }