Home | History | Annotate | Download | only in statusbar
      1 package com.android.systemui.statusbar;
      2 /*
      3  * Copyright (C) 2017 The Android Open Source Project
      4  *
      5  * Licensed under the Apache License, Version 2.0 (the "License");
      6  * you may not use this file except in compliance with the License.
      7  * You may obtain a copy of the License at
      8  *
      9  *      http://www.apache.org/licenses/LICENSE-2.0
     10  *
     11  * Unless required by applicable law or agreed to in writing, software
     12  * distributed under the License is distributed on an "AS IS" BASIS,
     13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  * See the License for the specific language governing permissions and
     15  * limitations under the License
     16  */
     17 
     18 import java.util.ArrayList;
     19 import java.util.List;
     20 import java.util.concurrent.TimeUnit;
     21 
     22 import com.android.internal.annotations.VisibleForTesting;
     23 import com.android.internal.logging.MetricsLogger;
     24 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
     25 import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
     26 import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption;
     27 
     28 import android.animation.Animator;
     29 import android.animation.AnimatorListenerAdapter;
     30 import android.animation.AnimatorSet;
     31 import android.animation.ObjectAnimator;
     32 import android.content.Context;
     33 import android.content.res.Resources;
     34 import android.graphics.Typeface;
     35 import android.metrics.LogMaker;
     36 import android.os.Bundle;
     37 import android.provider.Settings;
     38 import android.service.notification.SnoozeCriterion;
     39 import android.service.notification.StatusBarNotification;
     40 import android.text.SpannableString;
     41 import android.text.style.StyleSpan;
     42 import android.util.AttributeSet;
     43 import android.util.KeyValueListParser;
     44 import android.util.Log;
     45 import android.view.LayoutInflater;
     46 import android.view.View;
     47 import android.view.ViewGroup;
     48 import android.view.accessibility.AccessibilityEvent;
     49 import android.view.accessibility.AccessibilityNodeInfo;
     50 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
     51 import android.widget.ImageView;
     52 import android.widget.LinearLayout;
     53 import android.widget.TextView;
     54 
     55 import com.android.systemui.Interpolators;
     56 import com.android.systemui.R;
     57 
     58 public class NotificationSnooze extends LinearLayout
     59         implements NotificationGuts.GutsContent, View.OnClickListener {
     60 
     61     private static final String TAG = "NotificationSnooze";
     62     /**
     63      * If this changes more number increases, more assistant action resId's should be defined for
     64      * accessibility purposes, see {@link #setSnoozeOptions(List)}
     65      */
     66     private static final int MAX_ASSISTANT_SUGGESTIONS = 1;
     67     private static final String KEY_DEFAULT_SNOOZE = "default";
     68     private static final String KEY_OPTIONS = "options_array";
     69     private static final LogMaker OPTIONS_OPEN_LOG =
     70             new LogMaker(MetricsEvent.NOTIFICATION_SNOOZE_OPTIONS)
     71                     .setType(MetricsEvent.TYPE_OPEN);
     72     private static final LogMaker OPTIONS_CLOSE_LOG =
     73             new LogMaker(MetricsEvent.NOTIFICATION_SNOOZE_OPTIONS)
     74                     .setType(MetricsEvent.TYPE_CLOSE);
     75     private static final LogMaker UNDO_LOG =
     76             new LogMaker(MetricsEvent.NOTIFICATION_UNDO_SNOOZE)
     77                     .setType(MetricsEvent.TYPE_ACTION);
     78     private NotificationGuts mGutsContainer;
     79     private NotificationSwipeActionHelper mSnoozeListener;
     80     private StatusBarNotification mSbn;
     81 
     82     private TextView mSelectedOptionText;
     83     private TextView mUndoButton;
     84     private ImageView mExpandButton;
     85     private View mDivider;
     86     private ViewGroup mSnoozeOptionContainer;
     87     private List<SnoozeOption> mSnoozeOptions;
     88     private int mCollapsedHeight;
     89     private SnoozeOption mDefaultOption;
     90     private SnoozeOption mSelectedOption;
     91     private boolean mSnoozing;
     92     private boolean mExpanded;
     93     private AnimatorSet mExpandAnimation;
     94     private KeyValueListParser mParser;
     95 
     96     private final static int[] sAccessibilityActions = {
     97             R.id.action_snooze_shorter,
     98             R.id.action_snooze_short,
     99             R.id.action_snooze_long,
    100             R.id.action_snooze_longer,
    101     };
    102 
    103     private MetricsLogger mMetricsLogger = new MetricsLogger();
    104 
    105     public NotificationSnooze(Context context, AttributeSet attrs) {
    106         super(context, attrs);
    107         mParser = new KeyValueListParser(',');
    108     }
    109 
    110     @VisibleForTesting
    111     SnoozeOption getDefaultOption()
    112     {
    113         return mDefaultOption;
    114     }
    115 
    116     @VisibleForTesting
    117     void setKeyValueListParser(KeyValueListParser parser) {
    118         mParser = parser;
    119     }
    120 
    121     @Override
    122     protected void onFinishInflate() {
    123         super.onFinishInflate();
    124         mCollapsedHeight = getResources().getDimensionPixelSize(R.dimen.snooze_snackbar_min_height);
    125         findViewById(R.id.notification_snooze).setOnClickListener(this);
    126         mSelectedOptionText = (TextView) findViewById(R.id.snooze_option_default);
    127         mUndoButton = (TextView) findViewById(R.id.undo);
    128         mUndoButton.setOnClickListener(this);
    129         mExpandButton = (ImageView) findViewById(R.id.expand_button);
    130         mDivider = findViewById(R.id.divider);
    131         mDivider.setAlpha(0f);
    132         mSnoozeOptionContainer = (ViewGroup) findViewById(R.id.snooze_options);
    133         mSnoozeOptionContainer.setVisibility(View.INVISIBLE);
    134         mSnoozeOptionContainer.setAlpha(0f);
    135 
    136         // Create the different options based on list
    137         mSnoozeOptions = getDefaultSnoozeOptions();
    138         createOptionViews();
    139 
    140         setSelected(mDefaultOption, false);
    141     }
    142 
    143     @Override
    144     protected void onAttachedToWindow() {
    145         super.onAttachedToWindow();
    146         logOptionSelection(MetricsEvent.NOTIFICATION_SNOOZE_CLICKED, mDefaultOption);
    147     }
    148 
    149     @Override
    150     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
    151         super.onInitializeAccessibilityEvent(event);
    152         if (mGutsContainer != null && mGutsContainer.isExposed()) {
    153             if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
    154                 event.getText().add(mSelectedOptionText.getText());
    155             }
    156         }
    157     }
    158 
    159     @Override
    160     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
    161         super.onInitializeAccessibilityNodeInfo(info);
    162         info.addAction(new AccessibilityAction(R.id.action_snooze_undo,
    163                 getResources().getString(R.string.snooze_undo)));
    164         int count = mSnoozeOptions.size();
    165         for (int i = 0; i < count; i++) {
    166             AccessibilityAction action = mSnoozeOptions.get(i).getAccessibilityAction();
    167             if (action != null) {
    168                 info.addAction(action);
    169             }
    170         }
    171     }
    172 
    173     @Override
    174     public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
    175         if (super.performAccessibilityActionInternal(action, arguments)) {
    176             return true;
    177         }
    178         if (action == R.id.action_snooze_undo) {
    179             undoSnooze(mUndoButton);
    180             return true;
    181         }
    182         for (int i = 0; i < mSnoozeOptions.size(); i++) {
    183             SnoozeOption so = mSnoozeOptions.get(i);
    184             if (so.getAccessibilityAction() != null
    185                     && so.getAccessibilityAction().getId() == action) {
    186                 setSelected(so, true);
    187                 return true;
    188             }
    189         }
    190         return false;
    191     }
    192 
    193     public void setSnoozeOptions(final List<SnoozeCriterion> snoozeList) {
    194         if (snoozeList == null) {
    195             return;
    196         }
    197         mSnoozeOptions.clear();
    198         mSnoozeOptions = getDefaultSnoozeOptions();
    199         final int count = Math.min(MAX_ASSISTANT_SUGGESTIONS, snoozeList.size());
    200         for (int i = 0; i < count; i++) {
    201             SnoozeCriterion sc = snoozeList.get(i);
    202             AccessibilityAction action = new AccessibilityAction(
    203                     R.id.action_snooze_assistant_suggestion_1, sc.getExplanation());
    204             mSnoozeOptions.add(new NotificationSnoozeOption(sc, 0, sc.getExplanation(),
    205                     sc.getConfirmation(), action));
    206         }
    207         createOptionViews();
    208     }
    209 
    210     public boolean isExpanded() {
    211         return mExpanded;
    212     }
    213 
    214     public void setSnoozeListener(NotificationSwipeActionHelper listener) {
    215         mSnoozeListener = listener;
    216     }
    217 
    218     public void setStatusBarNotification(StatusBarNotification sbn) {
    219         mSbn = sbn;
    220     }
    221 
    222     @VisibleForTesting
    223     ArrayList<SnoozeOption> getDefaultSnoozeOptions() {
    224         final Resources resources = getContext().getResources();
    225         ArrayList<SnoozeOption> options = new ArrayList<>();
    226         try {
    227             final String config = Settings.Global.getString(getContext().getContentResolver(),
    228                     Settings.Global.NOTIFICATION_SNOOZE_OPTIONS);
    229             mParser.setString(config);
    230         } catch (IllegalArgumentException e) {
    231             Log.e(TAG, "Bad snooze constants");
    232         }
    233 
    234         final int defaultSnooze = mParser.getInt(KEY_DEFAULT_SNOOZE,
    235                 resources.getInteger(R.integer.config_notification_snooze_time_default));
    236         final int[] snoozeTimes = mParser.getIntArray(KEY_OPTIONS,
    237                 resources.getIntArray(R.array.config_notification_snooze_times));
    238 
    239         for (int i = 0; i < snoozeTimes.length && i < sAccessibilityActions.length; i++) {
    240             int snoozeTime = snoozeTimes[i];
    241             SnoozeOption option = createOption(snoozeTime, sAccessibilityActions[i]);
    242             if (i == 0 || snoozeTime == defaultSnooze) {
    243                 mDefaultOption = option;
    244             }
    245             options.add(option);
    246         }
    247         return options;
    248     }
    249 
    250     private SnoozeOption createOption(int minutes, int accessibilityActionId) {
    251         Resources res = getResources();
    252         boolean showInHours = minutes >= 60;
    253         int pluralResId = showInHours
    254                 ? R.plurals.snoozeHourOptions
    255                 : R.plurals.snoozeMinuteOptions;
    256         int count = showInHours ? (minutes / 60) : minutes;
    257         String description = res.getQuantityString(pluralResId, count, count);
    258         String resultText = String.format(res.getString(R.string.snoozed_for_time), description);
    259         SpannableString string = new SpannableString(resultText);
    260         string.setSpan(new StyleSpan(Typeface.BOLD),
    261                 resultText.length() - description.length(), resultText.length(), 0 /* flags */);
    262         AccessibilityAction action = new AccessibilityAction(accessibilityActionId, description);
    263         return new NotificationSnoozeOption(null, minutes, description, string,
    264                 action);
    265     }
    266 
    267     private void createOptionViews() {
    268         mSnoozeOptionContainer.removeAllViews();
    269         LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(
    270                 Context.LAYOUT_INFLATER_SERVICE);
    271         for (int i = 0; i < mSnoozeOptions.size(); i++) {
    272             SnoozeOption option = mSnoozeOptions.get(i);
    273             TextView tv = (TextView) inflater.inflate(R.layout.notification_snooze_option,
    274                     mSnoozeOptionContainer, false);
    275             mSnoozeOptionContainer.addView(tv);
    276             tv.setText(option.getDescription());
    277             tv.setTag(option);
    278             tv.setOnClickListener(this);
    279         }
    280     }
    281 
    282     private void hideSelectedOption() {
    283         final int childCount = mSnoozeOptionContainer.getChildCount();
    284         for (int i = 0; i < childCount; i++) {
    285             final View child = mSnoozeOptionContainer.getChildAt(i);
    286             child.setVisibility(child.getTag() == mSelectedOption ? View.GONE : View.VISIBLE);
    287         }
    288     }
    289 
    290     private void showSnoozeOptions(boolean show) {
    291         int drawableId = show ? com.android.internal.R.drawable.ic_collapse_notification
    292                 : com.android.internal.R.drawable.ic_expand_notification;
    293         mExpandButton.setImageResource(drawableId);
    294         if (mExpanded != show) {
    295             mExpanded = show;
    296             animateSnoozeOptions(show);
    297             if (mGutsContainer != null) {
    298                 mGutsContainer.onHeightChanged();
    299             }
    300         }
    301     }
    302 
    303     private void animateSnoozeOptions(boolean show) {
    304         if (mExpandAnimation != null) {
    305             mExpandAnimation.cancel();
    306         }
    307         ObjectAnimator dividerAnim = ObjectAnimator.ofFloat(mDivider, View.ALPHA,
    308                 mDivider.getAlpha(), show ? 1f : 0f);
    309         ObjectAnimator optionAnim = ObjectAnimator.ofFloat(mSnoozeOptionContainer, View.ALPHA,
    310                 mSnoozeOptionContainer.getAlpha(), show ? 1f : 0f);
    311         mSnoozeOptionContainer.setVisibility(View.VISIBLE);
    312         mExpandAnimation = new AnimatorSet();
    313         mExpandAnimation.playTogether(dividerAnim, optionAnim);
    314         mExpandAnimation.setDuration(150);
    315         mExpandAnimation.setInterpolator(show ? Interpolators.ALPHA_IN : Interpolators.ALPHA_OUT);
    316         mExpandAnimation.addListener(new AnimatorListenerAdapter() {
    317             boolean cancelled = false;
    318 
    319             @Override
    320             public void onAnimationCancel(Animator animation) {
    321                 cancelled = true;
    322             }
    323 
    324             @Override
    325             public void onAnimationEnd(Animator animation) {
    326                 if (!show && !cancelled) {
    327                     mSnoozeOptionContainer.setVisibility(View.INVISIBLE);
    328                     mSnoozeOptionContainer.setAlpha(0f);
    329                 }
    330             }
    331         });
    332         mExpandAnimation.start();
    333     }
    334 
    335     private void setSelected(SnoozeOption option, boolean userAction) {
    336         mSelectedOption = option;
    337         mSelectedOptionText.setText(option.getConfirmation());
    338         showSnoozeOptions(false);
    339         hideSelectedOption();
    340         sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
    341         if (userAction) {
    342             logOptionSelection(MetricsEvent.NOTIFICATION_SELECT_SNOOZE, option);
    343         }
    344     }
    345 
    346     private void logOptionSelection(int category, SnoozeOption option) {
    347         int index = mSnoozeOptions.indexOf(option);
    348         long duration = TimeUnit.MINUTES.toMillis(option.getMinutesToSnoozeFor());
    349         mMetricsLogger.write(new LogMaker(category)
    350                 .setType(MetricsEvent.TYPE_ACTION)
    351                 .addTaggedData(MetricsEvent.FIELD_NOTIFICATION_SNOOZE_INDEX, index)
    352                 .addTaggedData(MetricsEvent.FIELD_NOTIFICATION_SNOOZE_DURATION_MS, duration));
    353     }
    354 
    355     @Override
    356     public void onClick(View v) {
    357         if (mGutsContainer != null) {
    358             mGutsContainer.resetFalsingCheck();
    359         }
    360         final int id = v.getId();
    361         final SnoozeOption tag = (SnoozeOption) v.getTag();
    362         if (tag != null) {
    363             setSelected(tag, true);
    364         } else if (id == R.id.notification_snooze) {
    365             // Toggle snooze options
    366             showSnoozeOptions(!mExpanded);
    367             mMetricsLogger.write(!mExpanded ? OPTIONS_OPEN_LOG : OPTIONS_CLOSE_LOG);
    368         } else {
    369             // Undo snooze was selected
    370             undoSnooze(v);
    371             mMetricsLogger.write(UNDO_LOG);
    372         }
    373     }
    374 
    375     private void undoSnooze(View v) {
    376         mSelectedOption = null;
    377         int[] parentLoc = new int[2];
    378         int[] targetLoc = new int[2];
    379         mGutsContainer.getLocationOnScreen(parentLoc);
    380         v.getLocationOnScreen(targetLoc);
    381         final int centerX = v.getWidth() / 2;
    382         final int centerY = v.getHeight() / 2;
    383         final int x = targetLoc[0] - parentLoc[0] + centerX;
    384         final int y = targetLoc[1] - parentLoc[1] + centerY;
    385         showSnoozeOptions(false);
    386         mGutsContainer.closeControls(x, y, false /* save */, false /* force */);
    387     }
    388 
    389     @Override
    390     public int getActualHeight() {
    391         return mExpanded ? getHeight() : mCollapsedHeight;
    392     }
    393 
    394     @Override
    395     public boolean willBeRemoved() {
    396         return mSnoozing;
    397     }
    398 
    399     @Override
    400     public View getContentView() {
    401         // Reset the view before use
    402         setSelected(mDefaultOption, false);
    403         return this;
    404     }
    405 
    406     @Override
    407     public void setGutsParent(NotificationGuts guts) {
    408         mGutsContainer = guts;
    409     }
    410 
    411     @Override
    412     public boolean handleCloseControls(boolean save, boolean force) {
    413         if (mExpanded && !force) {
    414             // Collapse expanded state on outside touch
    415             showSnoozeOptions(false);
    416             return true;
    417         } else if (mSnoozeListener != null && mSelectedOption != null) {
    418             // Snooze option selected so commit it
    419             mSnoozing = true;
    420             mSnoozeListener.snooze(mSbn, mSelectedOption);
    421             return true;
    422         } else {
    423             // The view should actually be closed
    424             setSelected(mSnoozeOptions.get(0), false);
    425             return false; // Return false here so that guts handles closing the view
    426         }
    427     }
    428 
    429     @Override
    430     public boolean isLeavebehind() {
    431         return true;
    432     }
    433 
    434     @Override
    435     public boolean shouldBeSaved() {
    436         return true;
    437     }
    438 
    439     public class NotificationSnoozeOption implements SnoozeOption {
    440         private SnoozeCriterion mCriterion;
    441         private int mMinutesToSnoozeFor;
    442         private CharSequence mDescription;
    443         private CharSequence mConfirmation;
    444         private AccessibilityAction mAction;
    445 
    446         public NotificationSnoozeOption(SnoozeCriterion sc, int minToSnoozeFor,
    447                 CharSequence description,
    448                 CharSequence confirmation, AccessibilityAction action) {
    449             mCriterion = sc;
    450             mMinutesToSnoozeFor = minToSnoozeFor;
    451             mDescription = description;
    452             mConfirmation = confirmation;
    453             mAction = action;
    454         }
    455 
    456         @Override
    457         public SnoozeCriterion getSnoozeCriterion() {
    458             return mCriterion;
    459         }
    460 
    461         @Override
    462         public CharSequence getDescription() {
    463             return mDescription;
    464         }
    465 
    466         @Override
    467         public CharSequence getConfirmation() {
    468             return mConfirmation;
    469         }
    470 
    471         @Override
    472         public int getMinutesToSnoozeFor() {
    473             return mMinutesToSnoozeFor;
    474         }
    475 
    476         @Override
    477         public AccessibilityAction getAccessibilityAction() {
    478             return mAction;
    479         }
    480 
    481     }
    482 }
    483