1 /* 2 * Copyright (C) 2013 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; 18 19 import android.content.Context; 20 import android.graphics.drawable.AnimatedVectorDrawable; 21 import android.graphics.drawable.AnimationDrawable; 22 import android.graphics.drawable.Drawable; 23 import android.service.notification.StatusBarNotification; 24 import android.util.AttributeSet; 25 import android.view.MotionEvent; 26 import android.view.View; 27 import android.view.ViewStub; 28 import android.view.accessibility.AccessibilityEvent; 29 import android.widget.ImageView; 30 import com.android.systemui.R; 31 32 public class ExpandableNotificationRow extends ActivatableNotificationView { 33 private int mRowMinHeight; 34 private int mRowMaxHeight; 35 36 /** Does this row contain layouts that can adapt to row expansion */ 37 private boolean mExpandable; 38 /** Has the user actively changed the expansion state of this row */ 39 private boolean mHasUserChangedExpansion; 40 /** If {@link #mHasUserChangedExpansion}, has the user expanded this row */ 41 private boolean mUserExpanded; 42 /** Is the user touching this row */ 43 private boolean mUserLocked; 44 /** Are we showing the "public" version */ 45 private boolean mShowingPublic; 46 private boolean mSensitive; 47 private boolean mShowingPublicInitialized; 48 private boolean mShowingPublicForIntrinsicHeight; 49 50 /** 51 * Is this notification expanded by the system. The expansion state can be overridden by the 52 * user expansion. 53 */ 54 private boolean mIsSystemExpanded; 55 56 /** 57 * Whether the notification expansion is disabled. This is the case on Keyguard. 58 */ 59 private boolean mExpansionDisabled; 60 61 private NotificationContentView mPublicLayout; 62 private NotificationContentView mPrivateLayout; 63 private int mMaxExpandHeight; 64 private View mVetoButton; 65 private boolean mClearable; 66 private ExpansionLogger mLogger; 67 private String mLoggingKey; 68 private boolean mWasReset; 69 private NotificationGuts mGuts; 70 71 private StatusBarNotification mStatusBarNotification; 72 private boolean mIsHeadsUp; 73 setIconAnimationRunning(boolean running)74 public void setIconAnimationRunning(boolean running) { 75 setIconAnimationRunning(running, mPublicLayout); 76 setIconAnimationRunning(running, mPrivateLayout); 77 } 78 setIconAnimationRunning(boolean running, NotificationContentView layout)79 private void setIconAnimationRunning(boolean running, NotificationContentView layout) { 80 if (layout != null) { 81 View contractedChild = layout.getContractedChild(); 82 View expandedChild = layout.getExpandedChild(); 83 setIconAnimationRunningForChild(running, contractedChild); 84 setIconAnimationRunningForChild(running, expandedChild); 85 } 86 } 87 setIconAnimationRunningForChild(boolean running, View child)88 private void setIconAnimationRunningForChild(boolean running, View child) { 89 if (child != null) { 90 ImageView icon = (ImageView) child.findViewById(com.android.internal.R.id.icon); 91 setIconRunning(icon, running); 92 ImageView rightIcon = (ImageView) child.findViewById( 93 com.android.internal.R.id.right_icon); 94 setIconRunning(rightIcon, running); 95 } 96 } 97 setIconRunning(ImageView imageView, boolean running)98 private void setIconRunning(ImageView imageView, boolean running) { 99 if (imageView != null) { 100 Drawable drawable = imageView.getDrawable(); 101 if (drawable instanceof AnimationDrawable) { 102 AnimationDrawable animationDrawable = (AnimationDrawable) drawable; 103 if (running) { 104 animationDrawable.start(); 105 } else { 106 animationDrawable.stop(); 107 } 108 } else if (drawable instanceof AnimatedVectorDrawable) { 109 AnimatedVectorDrawable animationDrawable = (AnimatedVectorDrawable) drawable; 110 if (running) { 111 animationDrawable.start(); 112 } else { 113 animationDrawable.stop(); 114 } 115 } 116 } 117 } 118 setStatusBarNotification(StatusBarNotification statusBarNotification)119 public void setStatusBarNotification(StatusBarNotification statusBarNotification) { 120 mStatusBarNotification = statusBarNotification; 121 updateVetoButton(); 122 } 123 getStatusBarNotification()124 public StatusBarNotification getStatusBarNotification() { 125 return mStatusBarNotification; 126 } 127 setHeadsUp(boolean isHeadsUp)128 public void setHeadsUp(boolean isHeadsUp) { 129 mIsHeadsUp = isHeadsUp; 130 } 131 132 public interface ExpansionLogger { logNotificationExpansion(String key, boolean userAction, boolean expanded)133 public void logNotificationExpansion(String key, boolean userAction, boolean expanded); 134 } 135 ExpandableNotificationRow(Context context, AttributeSet attrs)136 public ExpandableNotificationRow(Context context, AttributeSet attrs) { 137 super(context, attrs); 138 } 139 140 /** 141 * Resets this view so it can be re-used for an updated notification. 142 */ 143 @Override reset()144 public void reset() { 145 super.reset(); 146 mRowMinHeight = 0; 147 final boolean wasExpanded = isExpanded(); 148 mRowMaxHeight = 0; 149 mExpandable = false; 150 mHasUserChangedExpansion = false; 151 mUserLocked = false; 152 mShowingPublic = false; 153 mSensitive = false; 154 mShowingPublicInitialized = false; 155 mIsSystemExpanded = false; 156 mExpansionDisabled = false; 157 mPublicLayout.reset(mIsHeadsUp); 158 mPrivateLayout.reset(mIsHeadsUp); 159 resetHeight(); 160 logExpansionEvent(false, wasExpanded); 161 } 162 resetHeight()163 public void resetHeight() { 164 if (mIsHeadsUp) { 165 resetActualHeight(); 166 } 167 mMaxExpandHeight = 0; 168 mWasReset = true; 169 onHeightReset(); 170 requestLayout(); 171 } 172 173 @Override filterMotionEvent(MotionEvent event)174 protected boolean filterMotionEvent(MotionEvent event) { 175 return mIsHeadsUp || super.filterMotionEvent(event); 176 } 177 178 @Override onFinishInflate()179 protected void onFinishInflate() { 180 super.onFinishInflate(); 181 mPublicLayout = (NotificationContentView) findViewById(R.id.expandedPublic); 182 mPrivateLayout = (NotificationContentView) findViewById(R.id.expanded); 183 ViewStub gutsStub = (ViewStub) findViewById(R.id.notification_guts_stub); 184 gutsStub.setOnInflateListener(new ViewStub.OnInflateListener() { 185 @Override 186 public void onInflate(ViewStub stub, View inflated) { 187 mGuts = (NotificationGuts) inflated; 188 mGuts.setClipTopAmount(getClipTopAmount()); 189 mGuts.setActualHeight(getActualHeight()); 190 } 191 }); 192 mVetoButton = findViewById(R.id.veto); 193 } 194 195 @Override onRequestSendAccessibilityEvent(View child, AccessibilityEvent event)196 public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) { 197 if (super.onRequestSendAccessibilityEvent(child, event)) { 198 // Add a record for the entire layout since its content is somehow small. 199 // The event comes from a leaf view that is interacted with. 200 AccessibilityEvent record = AccessibilityEvent.obtain(); 201 onInitializeAccessibilityEvent(record); 202 dispatchPopulateAccessibilityEvent(record); 203 event.appendRecord(record); 204 return true; 205 } 206 return false; 207 } 208 209 @Override setDark(boolean dark, boolean fade, long delay)210 public void setDark(boolean dark, boolean fade, long delay) { 211 super.setDark(dark, fade, delay); 212 final NotificationContentView showing = getShowingLayout(); 213 if (showing != null) { 214 showing.setDark(dark, fade, delay); 215 } 216 } 217 setHeightRange(int rowMinHeight, int rowMaxHeight)218 public void setHeightRange(int rowMinHeight, int rowMaxHeight) { 219 mRowMinHeight = rowMinHeight; 220 mRowMaxHeight = rowMaxHeight; 221 } 222 isExpandable()223 public boolean isExpandable() { 224 return mExpandable; 225 } 226 setExpandable(boolean expandable)227 public void setExpandable(boolean expandable) { 228 mExpandable = expandable; 229 } 230 231 /** 232 * @return whether the user has changed the expansion state 233 */ hasUserChangedExpansion()234 public boolean hasUserChangedExpansion() { 235 return mHasUserChangedExpansion; 236 } 237 isUserExpanded()238 public boolean isUserExpanded() { 239 return mUserExpanded; 240 } 241 242 /** 243 * Set this notification to be expanded by the user 244 * 245 * @param userExpanded whether the user wants this notification to be expanded 246 */ setUserExpanded(boolean userExpanded)247 public void setUserExpanded(boolean userExpanded) { 248 if (userExpanded && !mExpandable) return; 249 final boolean wasExpanded = isExpanded(); 250 mHasUserChangedExpansion = true; 251 mUserExpanded = userExpanded; 252 logExpansionEvent(true, wasExpanded); 253 } 254 resetUserExpansion()255 public void resetUserExpansion() { 256 mHasUserChangedExpansion = false; 257 mUserExpanded = false; 258 } 259 isUserLocked()260 public boolean isUserLocked() { 261 return mUserLocked; 262 } 263 setUserLocked(boolean userLocked)264 public void setUserLocked(boolean userLocked) { 265 mUserLocked = userLocked; 266 } 267 268 /** 269 * @return has the system set this notification to be expanded 270 */ isSystemExpanded()271 public boolean isSystemExpanded() { 272 return mIsSystemExpanded; 273 } 274 275 /** 276 * Set this notification to be expanded by the system. 277 * 278 * @param expand whether the system wants this notification to be expanded. 279 */ setSystemExpanded(boolean expand)280 public void setSystemExpanded(boolean expand) { 281 if (expand != mIsSystemExpanded) { 282 final boolean wasExpanded = isExpanded(); 283 mIsSystemExpanded = expand; 284 notifyHeightChanged(); 285 logExpansionEvent(false, wasExpanded); 286 } 287 } 288 289 /** 290 * @param expansionDisabled whether to prevent notification expansion 291 */ setExpansionDisabled(boolean expansionDisabled)292 public void setExpansionDisabled(boolean expansionDisabled) { 293 if (expansionDisabled != mExpansionDisabled) { 294 final boolean wasExpanded = isExpanded(); 295 mExpansionDisabled = expansionDisabled; 296 logExpansionEvent(false, wasExpanded); 297 if (wasExpanded != isExpanded()) { 298 notifyHeightChanged(); 299 } 300 } 301 } 302 303 /** 304 * @return Can the underlying notification be cleared? 305 */ isClearable()306 public boolean isClearable() { 307 return mStatusBarNotification != null && mStatusBarNotification.isClearable(); 308 } 309 310 /** 311 * Apply an expansion state to the layout. 312 */ applyExpansionToLayout()313 public void applyExpansionToLayout() { 314 boolean expand = isExpanded(); 315 if (expand && mExpandable) { 316 setActualHeight(mMaxExpandHeight); 317 } else { 318 setActualHeight(mRowMinHeight); 319 } 320 } 321 322 @Override getIntrinsicHeight()323 public int getIntrinsicHeight() { 324 if (isUserLocked()) { 325 return getActualHeight(); 326 } 327 boolean inExpansionState = isExpanded(); 328 if (!inExpansionState) { 329 // not expanded, so we return the collapsed size 330 return mRowMinHeight; 331 } 332 333 return mShowingPublicForIntrinsicHeight ? mRowMinHeight : getMaxExpandHeight(); 334 } 335 336 /** 337 * Check whether the view state is currently expanded. This is given by the system in {@link 338 * #setSystemExpanded(boolean)} and can be overridden by user expansion or 339 * collapsing in {@link #setUserExpanded(boolean)}. Note that the visual appearance of this 340 * view can differ from this state, if layout params are modified from outside. 341 * 342 * @return whether the view state is currently expanded. 343 */ isExpanded()344 private boolean isExpanded() { 345 return !mExpansionDisabled 346 && (!hasUserChangedExpansion() && isSystemExpanded() || isUserExpanded()); 347 } 348 349 @Override onLayout(boolean changed, int left, int top, int right, int bottom)350 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 351 super.onLayout(changed, left, top, right, bottom); 352 boolean updateExpandHeight = mMaxExpandHeight == 0 && !mWasReset; 353 updateMaxExpandHeight(); 354 if (updateExpandHeight) { 355 applyExpansionToLayout(); 356 } 357 mWasReset = false; 358 } 359 updateMaxExpandHeight()360 private void updateMaxExpandHeight() { 361 int intrinsicBefore = getIntrinsicHeight(); 362 mMaxExpandHeight = mPrivateLayout.getMaxHeight(); 363 if (intrinsicBefore != getIntrinsicHeight()) { 364 notifyHeightChanged(); 365 } 366 } 367 setSensitive(boolean sensitive)368 public void setSensitive(boolean sensitive) { 369 mSensitive = sensitive; 370 } 371 setHideSensitiveForIntrinsicHeight(boolean hideSensitive)372 public void setHideSensitiveForIntrinsicHeight(boolean hideSensitive) { 373 mShowingPublicForIntrinsicHeight = mSensitive && hideSensitive; 374 } 375 setHideSensitive(boolean hideSensitive, boolean animated, long delay, long duration)376 public void setHideSensitive(boolean hideSensitive, boolean animated, long delay, 377 long duration) { 378 boolean oldShowingPublic = mShowingPublic; 379 mShowingPublic = mSensitive && hideSensitive; 380 if (mShowingPublicInitialized && mShowingPublic == oldShowingPublic) { 381 return; 382 } 383 384 // bail out if no public version 385 if (mPublicLayout.getChildCount() == 0) return; 386 387 if (!animated) { 388 mPublicLayout.animate().cancel(); 389 mPrivateLayout.animate().cancel(); 390 mPublicLayout.setAlpha(1f); 391 mPrivateLayout.setAlpha(1f); 392 mPublicLayout.setVisibility(mShowingPublic ? View.VISIBLE : View.INVISIBLE); 393 mPrivateLayout.setVisibility(mShowingPublic ? View.INVISIBLE : View.VISIBLE); 394 } else { 395 animateShowingPublic(delay, duration); 396 } 397 398 updateVetoButton(); 399 mShowingPublicInitialized = true; 400 } 401 animateShowingPublic(long delay, long duration)402 private void animateShowingPublic(long delay, long duration) { 403 final View source = mShowingPublic ? mPrivateLayout : mPublicLayout; 404 View target = mShowingPublic ? mPublicLayout : mPrivateLayout; 405 source.setVisibility(View.VISIBLE); 406 target.setVisibility(View.VISIBLE); 407 target.setAlpha(0f); 408 source.animate().cancel(); 409 target.animate().cancel(); 410 source.animate() 411 .alpha(0f) 412 .setStartDelay(delay) 413 .setDuration(duration) 414 .withEndAction(new Runnable() { 415 @Override 416 public void run() { 417 source.setVisibility(View.INVISIBLE); 418 } 419 }); 420 target.animate() 421 .alpha(1f) 422 .setStartDelay(delay) 423 .setDuration(duration); 424 } 425 updateVetoButton()426 private void updateVetoButton() { 427 // public versions cannot be dismissed 428 mVetoButton.setVisibility(isClearable() && !mShowingPublic ? View.VISIBLE : View.GONE); 429 } 430 getMaxExpandHeight()431 public int getMaxExpandHeight() { 432 return mShowingPublicForIntrinsicHeight ? mRowMinHeight : mMaxExpandHeight; 433 } 434 435 @Override isContentExpandable()436 public boolean isContentExpandable() { 437 NotificationContentView showingLayout = getShowingLayout(); 438 return showingLayout.isContentExpandable(); 439 } 440 441 @Override setActualHeight(int height, boolean notifyListeners)442 public void setActualHeight(int height, boolean notifyListeners) { 443 mPrivateLayout.setActualHeight(height); 444 mPublicLayout.setActualHeight(height); 445 if (mGuts != null) { 446 mGuts.setActualHeight(height); 447 } 448 invalidate(); 449 super.setActualHeight(height, notifyListeners); 450 } 451 452 @Override getMaxHeight()453 public int getMaxHeight() { 454 NotificationContentView showingLayout = getShowingLayout(); 455 return showingLayout.getMaxHeight(); 456 } 457 458 @Override getMinHeight()459 public int getMinHeight() { 460 NotificationContentView showingLayout = getShowingLayout(); 461 return showingLayout.getMinHeight(); 462 } 463 464 @Override setClipTopAmount(int clipTopAmount)465 public void setClipTopAmount(int clipTopAmount) { 466 super.setClipTopAmount(clipTopAmount); 467 mPrivateLayout.setClipTopAmount(clipTopAmount); 468 mPublicLayout.setClipTopAmount(clipTopAmount); 469 if (mGuts != null) { 470 mGuts.setClipTopAmount(clipTopAmount); 471 } 472 } 473 notifyContentUpdated()474 public void notifyContentUpdated() { 475 mPublicLayout.notifyContentUpdated(); 476 mPrivateLayout.notifyContentUpdated(); 477 } 478 isMaxExpandHeightInitialized()479 public boolean isMaxExpandHeightInitialized() { 480 return mMaxExpandHeight != 0; 481 } 482 getShowingLayout()483 private NotificationContentView getShowingLayout() { 484 return mShowingPublic ? mPublicLayout : mPrivateLayout; 485 } 486 setExpansionLogger(ExpansionLogger logger, String key)487 public void setExpansionLogger(ExpansionLogger logger, String key) { 488 mLogger = logger; 489 mLoggingKey = key; 490 } 491 492 logExpansionEvent(boolean userAction, boolean wasExpanded)493 private void logExpansionEvent(boolean userAction, boolean wasExpanded) { 494 final boolean nowExpanded = isExpanded(); 495 if (wasExpanded != nowExpanded && mLogger != null) { 496 mLogger.logNotificationExpansion(mLoggingKey, userAction, nowExpanded) ; 497 } 498 } 499 } 500