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 NotificationSnooze(Context context, AttributeSet attrs)105 public NotificationSnooze(Context context, AttributeSet attrs) { 106 super(context, attrs); 107 mParser = new KeyValueListParser(','); 108 } 109 110 @VisibleForTesting getDefaultOption()111 SnoozeOption getDefaultOption() 112 { 113 return mDefaultOption; 114 } 115 116 @VisibleForTesting setKeyValueListParser(KeyValueListParser parser)117 void setKeyValueListParser(KeyValueListParser parser) { 118 mParser = parser; 119 } 120 121 @Override onFinishInflate()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 onAttachedToWindow()144 protected void onAttachedToWindow() { 145 super.onAttachedToWindow(); 146 logOptionSelection(MetricsEvent.NOTIFICATION_SNOOZE_CLICKED, mDefaultOption); 147 } 148 149 @Override onInitializeAccessibilityEvent(AccessibilityEvent event)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 onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)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 performAccessibilityActionInternal(int action, Bundle arguments)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 setSnoozeOptions(final List<SnoozeCriterion> snoozeList)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 isExpanded()210 public boolean isExpanded() { 211 return mExpanded; 212 } 213 setSnoozeListener(NotificationSwipeActionHelper listener)214 public void setSnoozeListener(NotificationSwipeActionHelper listener) { 215 mSnoozeListener = listener; 216 } 217 setStatusBarNotification(StatusBarNotification sbn)218 public void setStatusBarNotification(StatusBarNotification sbn) { 219 mSbn = sbn; 220 } 221 222 @VisibleForTesting getDefaultSnoozeOptions()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 createOption(int minutes, int accessibilityActionId)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 createOptionViews()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 hideSelectedOption()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 showSnoozeOptions(boolean show)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 animateSnoozeOptions(boolean show)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 setSelected(SnoozeOption option, boolean userAction)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 logOptionSelection(int category, SnoozeOption option)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 onClick(View v)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 undoSnooze(View v)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 getActualHeight()390 public int getActualHeight() { 391 return mExpanded ? getHeight() : mCollapsedHeight; 392 } 393 394 @Override willBeRemoved()395 public boolean willBeRemoved() { 396 return mSnoozing; 397 } 398 399 @Override getContentView()400 public View getContentView() { 401 // Reset the view before use 402 setSelected(mDefaultOption, false); 403 return this; 404 } 405 406 @Override setGutsParent(NotificationGuts guts)407 public void setGutsParent(NotificationGuts guts) { 408 mGutsContainer = guts; 409 } 410 411 @Override handleCloseControls(boolean save, boolean force)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 isLeavebehind()430 public boolean isLeavebehind() { 431 return true; 432 } 433 434 @Override shouldBeSaved()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 NotificationSnoozeOption(SnoozeCriterion sc, int minToSnoozeFor, CharSequence description, CharSequence confirmation, AccessibilityAction action)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 getSnoozeCriterion()457 public SnoozeCriterion getSnoozeCriterion() { 458 return mCriterion; 459 } 460 461 @Override getDescription()462 public CharSequence getDescription() { 463 return mDescription; 464 } 465 466 @Override getConfirmation()467 public CharSequence getConfirmation() { 468 return mConfirmation; 469 } 470 471 @Override getMinutesToSnoozeFor()472 public int getMinutesToSnoozeFor() { 473 return mMinutesToSnoozeFor; 474 } 475 476 @Override getAccessibilityAction()477 public AccessibilityAction getAccessibilityAction() { 478 return mAction; 479 } 480 481 } 482 } 483