blob: d65f2c53598c3398ccf36f54f4851537d1c905cf [file] [log] [blame]
/*
* 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
*/
package com.android.systemui.statusbar.notification.row;
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.internal.annotations.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.systemui.Interpolators;
import com.android.systemui.R;
import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
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<SnoozeOption> 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<SnoozeCriterion> 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<SnoozeOption> getDefaultSnoozeOptions() {
final Resources resources = getContext().getResources();
ArrayList<SnoozeOption> 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 = mParser.getIntArray(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;
}
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);
AccessibilityAction action = new AccessibilityAction(accessibilityActionId, description);
final int index = resultText.indexOf(description);
if (index == -1) {
return new NotificationSnoozeOption(null, minutes, description, resultText, action);
}
SpannableString string = new SpannableString(resultText);
string.setSpan(new StyleSpan(Typeface.BOLD),
index, index + description.length(), 0 /* flags */);
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;
}
@Override
public boolean shouldBeSaved() {
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;
}
}
}