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