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