1 /* 2 * Copyright (C) 2015 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.phone; 18 19 import android.animation.ArgbEvaluator; 20 import android.animation.ValueAnimator; 21 import android.content.Context; 22 import android.content.res.ColorStateList; 23 import android.content.res.Resources; 24 import android.graphics.Color; 25 import android.graphics.Rect; 26 import android.graphics.drawable.Icon; 27 import android.os.Bundle; 28 import android.os.Handler; 29 import android.os.SystemClock; 30 import android.os.UserHandle; 31 import android.text.TextUtils; 32 import android.util.ArraySet; 33 import android.util.TypedValue; 34 import android.view.Gravity; 35 import android.view.View; 36 import android.view.ViewGroup; 37 import android.widget.ImageView; 38 import android.widget.LinearLayout; 39 import android.widget.TextView; 40 import com.android.internal.statusbar.StatusBarIcon; 41 import com.android.systemui.BatteryMeterView; 42 import com.android.systemui.FontSizeUtils; 43 import com.android.systemui.Interpolators; 44 import com.android.systemui.R; 45 import com.android.systemui.SystemUIFactory; 46 import com.android.systemui.statusbar.NotificationData; 47 import com.android.systemui.statusbar.SignalClusterView; 48 import com.android.systemui.statusbar.StatusBarIconView; 49 import com.android.systemui.tuner.TunerService; 50 import com.android.systemui.tuner.TunerService.Tunable; 51 52 import java.io.PrintWriter; 53 import java.util.ArrayList; 54 55 /** 56 * Controls everything regarding the icons in the status bar and on Keyguard, including, but not 57 * limited to: notification icons, signal cluster, additional status icons, and clock in the status 58 * bar. 59 */ 60 public class StatusBarIconController extends StatusBarIconList implements Tunable { 61 62 public static final long DEFAULT_TINT_ANIMATION_DURATION = 120; 63 public static final String ICON_BLACKLIST = "icon_blacklist"; 64 public static final int DEFAULT_ICON_TINT = Color.WHITE; 65 66 private Context mContext; 67 private PhoneStatusBar mPhoneStatusBar; 68 private DemoStatusIcons mDemoStatusIcons; 69 70 private LinearLayout mSystemIconArea; 71 private LinearLayout mStatusIcons; 72 private SignalClusterView mSignalCluster; 73 private LinearLayout mStatusIconsKeyguard; 74 75 private NotificationIconAreaController mNotificationIconAreaController; 76 private View mNotificationIconAreaInner; 77 78 private BatteryMeterView mBatteryMeterView; 79 private BatteryMeterView mBatteryMeterViewKeyguard; 80 private TextView mClock; 81 82 private int mIconSize; 83 private int mIconHPadding; 84 85 private int mIconTint = DEFAULT_ICON_TINT; 86 private float mDarkIntensity; 87 private final Rect mTintArea = new Rect(); 88 private static final Rect sTmpRect = new Rect(); 89 private static final int[] sTmpInt2 = new int[2]; 90 91 private boolean mTransitionPending; 92 private boolean mTintChangePending; 93 private float mPendingDarkIntensity; 94 private ValueAnimator mTintAnimator; 95 96 private int mDarkModeIconColorSingleTone; 97 private int mLightModeIconColorSingleTone; 98 99 private final Handler mHandler; 100 private boolean mTransitionDeferring; 101 private long mTransitionDeferringStartTime; 102 private long mTransitionDeferringDuration; 103 104 private final ArraySet<String> mIconBlacklist = new ArraySet<>(); 105 106 private final Runnable mTransitionDeferringDoneRunnable = new Runnable() { 107 @Override 108 public void run() { 109 mTransitionDeferring = false; 110 } 111 }; 112 113 public StatusBarIconController(Context context, View statusBar, View keyguardStatusBar, 114 PhoneStatusBar phoneStatusBar) { 115 super(context.getResources().getStringArray( 116 com.android.internal.R.array.config_statusBarIcons)); 117 mContext = context; 118 mPhoneStatusBar = phoneStatusBar; 119 mSystemIconArea = (LinearLayout) statusBar.findViewById(R.id.system_icon_area); 120 mStatusIcons = (LinearLayout) statusBar.findViewById(R.id.statusIcons); 121 mSignalCluster = (SignalClusterView) statusBar.findViewById(R.id.signal_cluster); 122 123 mNotificationIconAreaController = SystemUIFactory.getInstance() 124 .createNotificationIconAreaController(context, phoneStatusBar); 125 mNotificationIconAreaInner = 126 mNotificationIconAreaController.getNotificationInnerAreaView(); 127 128 ViewGroup notificationIconArea = 129 (ViewGroup) statusBar.findViewById(R.id.notification_icon_area); 130 notificationIconArea.addView(mNotificationIconAreaInner); 131 132 mStatusIconsKeyguard = (LinearLayout) keyguardStatusBar.findViewById(R.id.statusIcons); 133 134 mBatteryMeterView = (BatteryMeterView) statusBar.findViewById(R.id.battery); 135 mBatteryMeterViewKeyguard = (BatteryMeterView) keyguardStatusBar.findViewById(R.id.battery); 136 scaleBatteryMeterViews(context); 137 138 mClock = (TextView) statusBar.findViewById(R.id.clock); 139 mDarkModeIconColorSingleTone = context.getColor(R.color.dark_mode_icon_color_single_tone); 140 mLightModeIconColorSingleTone = context.getColor(R.color.light_mode_icon_color_single_tone); 141 mHandler = new Handler(); 142 loadDimens(); 143 144 TunerService.get(mContext).addTunable(this, ICON_BLACKLIST); 145 } 146 147 public void setSignalCluster(SignalClusterView signalCluster) { 148 mSignalCluster = signalCluster; 149 } 150 151 /** 152 * Looks up the scale factor for status bar icons and scales the battery view by that amount. 153 */ 154 private void scaleBatteryMeterViews(Context context) { 155 Resources res = context.getResources(); 156 TypedValue typedValue = new TypedValue(); 157 158 res.getValue(R.dimen.status_bar_icon_scale_factor, typedValue, true); 159 float iconScaleFactor = typedValue.getFloat(); 160 161 int batteryHeight = res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_height); 162 int batteryWidth = res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_width); 163 int marginBottom = res.getDimensionPixelSize(R.dimen.battery_margin_bottom); 164 165 LinearLayout.LayoutParams scaledLayoutParams = new LinearLayout.LayoutParams( 166 (int) (batteryWidth * iconScaleFactor), (int) (batteryHeight * iconScaleFactor)); 167 scaledLayoutParams.setMarginsRelative(0, 0, 0, marginBottom); 168 169 mBatteryMeterView.setLayoutParams(scaledLayoutParams); 170 mBatteryMeterViewKeyguard.setLayoutParams(scaledLayoutParams); 171 } 172 173 @Override 174 public void onTuningChanged(String key, String newValue) { 175 if (!ICON_BLACKLIST.equals(key)) { 176 return; 177 } 178 mIconBlacklist.clear(); 179 mIconBlacklist.addAll(getIconBlacklist(newValue)); 180 ArrayList<StatusBarIconView> views = new ArrayList<StatusBarIconView>(); 181 // Get all the current views. 182 for (int i = 0; i < mStatusIcons.getChildCount(); i++) { 183 views.add((StatusBarIconView) mStatusIcons.getChildAt(i)); 184 } 185 // Remove all the icons. 186 for (int i = views.size() - 1; i >= 0; i--) { 187 removeIcon(views.get(i).getSlot()); 188 } 189 // Add them all back 190 for (int i = 0; i < views.size(); i++) { 191 setIcon(views.get(i).getSlot(), views.get(i).getStatusBarIcon()); 192 } 193 } 194 private void loadDimens() { 195 mIconSize = mContext.getResources().getDimensionPixelSize( 196 com.android.internal.R.dimen.status_bar_icon_size); 197 mIconHPadding = mContext.getResources().getDimensionPixelSize( 198 R.dimen.status_bar_icon_padding); 199 } 200 201 private void addSystemIcon(int index, StatusBarIcon icon) { 202 String slot = getSlot(index); 203 int viewIndex = getViewIndex(index); 204 boolean blocked = mIconBlacklist.contains(slot); 205 StatusBarIconView view = new StatusBarIconView(mContext, slot, null, blocked); 206 view.set(icon); 207 208 LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( 209 ViewGroup.LayoutParams.WRAP_CONTENT, mIconSize); 210 lp.setMargins(mIconHPadding, 0, mIconHPadding, 0); 211 mStatusIcons.addView(view, viewIndex, lp); 212 213 view = new StatusBarIconView(mContext, slot, null, blocked); 214 view.set(icon); 215 mStatusIconsKeyguard.addView(view, viewIndex, new LinearLayout.LayoutParams( 216 ViewGroup.LayoutParams.WRAP_CONTENT, mIconSize)); 217 applyIconTint(); 218 } 219 220 public void setIcon(String slot, int resourceId, CharSequence contentDescription) { 221 int index = getSlotIndex(slot); 222 StatusBarIcon icon = getIcon(index); 223 if (icon == null) { 224 icon = new StatusBarIcon(UserHandle.SYSTEM, mContext.getPackageName(), 225 Icon.createWithResource(mContext, resourceId), 0, 0, contentDescription); 226 setIcon(slot, icon); 227 } else { 228 icon.icon = Icon.createWithResource(mContext, resourceId); 229 icon.contentDescription = contentDescription; 230 handleSet(index, icon); 231 } 232 } 233 234 public void setExternalIcon(String slot) { 235 int viewIndex = getViewIndex(getSlotIndex(slot)); 236 int height = mContext.getResources().getDimensionPixelSize( 237 R.dimen.status_bar_icon_drawing_size); 238 ImageView imageView = (ImageView) mStatusIcons.getChildAt(viewIndex); 239 imageView.setScaleType(ImageView.ScaleType.FIT_CENTER); 240 imageView.setAdjustViewBounds(true); 241 setHeightAndCenter(imageView, height); 242 imageView = (ImageView) mStatusIconsKeyguard.getChildAt(viewIndex); 243 imageView.setScaleType(ImageView.ScaleType.FIT_CENTER); 244 imageView.setAdjustViewBounds(true); 245 setHeightAndCenter(imageView, height); 246 } 247 248 private void setHeightAndCenter(ImageView imageView, int height) { 249 ViewGroup.LayoutParams params = imageView.getLayoutParams(); 250 params.height = height; 251 if (params instanceof LinearLayout.LayoutParams) { 252 ((LinearLayout.LayoutParams) params).gravity = Gravity.CENTER_VERTICAL; 253 } 254 imageView.setLayoutParams(params); 255 } 256 257 public void setIcon(String slot, StatusBarIcon icon) { 258 setIcon(getSlotIndex(slot), icon); 259 } 260 261 public void removeIcon(String slot) { 262 int index = getSlotIndex(slot); 263 removeIcon(index); 264 } 265 266 public void setIconVisibility(String slot, boolean visibility) { 267 int index = getSlotIndex(slot); 268 StatusBarIcon icon = getIcon(index); 269 if (icon == null || icon.visible == visibility) { 270 return; 271 } 272 icon.visible = visibility; 273 handleSet(index, icon); 274 } 275 276 @Override 277 public void removeIcon(int index) { 278 if (getIcon(index) == null) { 279 return; 280 } 281 super.removeIcon(index); 282 int viewIndex = getViewIndex(index); 283 mStatusIcons.removeViewAt(viewIndex); 284 mStatusIconsKeyguard.removeViewAt(viewIndex); 285 } 286 287 @Override 288 public void setIcon(int index, StatusBarIcon icon) { 289 if (icon == null) { 290 removeIcon(index); 291 return; 292 } 293 boolean isNew = getIcon(index) == null; 294 super.setIcon(index, icon); 295 if (isNew) { 296 addSystemIcon(index, icon); 297 } else { 298 handleSet(index, icon); 299 } 300 } 301 302 private void handleSet(int index, StatusBarIcon icon) { 303 int viewIndex = getViewIndex(index); 304 StatusBarIconView view = (StatusBarIconView) mStatusIcons.getChildAt(viewIndex); 305 view.set(icon); 306 view = (StatusBarIconView) mStatusIconsKeyguard.getChildAt(viewIndex); 307 view.set(icon); 308 applyIconTint(); 309 } 310 311 public void updateNotificationIcons(NotificationData notificationData) { 312 mNotificationIconAreaController.updateNotificationIcons(notificationData); 313 } 314 315 public void hideSystemIconArea(boolean animate) { 316 animateHide(mSystemIconArea, animate); 317 } 318 319 public void showSystemIconArea(boolean animate) { 320 animateShow(mSystemIconArea, animate); 321 } 322 323 public void hideNotificationIconArea(boolean animate) { 324 animateHide(mNotificationIconAreaInner, animate); 325 } 326 327 public void showNotificationIconArea(boolean animate) { 328 animateShow(mNotificationIconAreaInner, animate); 329 } 330 331 public void setClockVisibility(boolean visible) { 332 mClock.setVisibility(visible ? View.VISIBLE : View.GONE); 333 } 334 335 public void dump(PrintWriter pw) { 336 int N = mStatusIcons.getChildCount(); 337 pw.println(" icon views: " + N); 338 for (int i=0; i<N; i++) { 339 StatusBarIconView ic = (StatusBarIconView) mStatusIcons.getChildAt(i); 340 pw.println(" [" + i + "] icon=" + ic); 341 } 342 super.dump(pw); 343 } 344 345 public void dispatchDemoCommand(String command, Bundle args) { 346 if (mDemoStatusIcons == null) { 347 mDemoStatusIcons = new DemoStatusIcons(mStatusIcons, mIconSize); 348 } 349 mDemoStatusIcons.dispatchDemoCommand(command, args); 350 } 351 352 /** 353 * Hides a view. 354 */ 355 private void animateHide(final View v, boolean animate) { 356 v.animate().cancel(); 357 if (!animate) { 358 v.setAlpha(0f); 359 v.setVisibility(View.INVISIBLE); 360 return; 361 } 362 v.animate() 363 .alpha(0f) 364 .setDuration(160) 365 .setStartDelay(0) 366 .setInterpolator(Interpolators.ALPHA_OUT) 367 .withEndAction(new Runnable() { 368 @Override 369 public void run() { 370 v.setVisibility(View.INVISIBLE); 371 } 372 }); 373 } 374 375 /** 376 * Shows a view, and synchronizes the animation with Keyguard exit animations, if applicable. 377 */ 378 private void animateShow(View v, boolean animate) { 379 v.animate().cancel(); 380 v.setVisibility(View.VISIBLE); 381 if (!animate) { 382 v.setAlpha(1f); 383 return; 384 } 385 v.animate() 386 .alpha(1f) 387 .setDuration(320) 388 .setInterpolator(Interpolators.ALPHA_IN) 389 .setStartDelay(50) 390 391 // We need to clean up any pending end action from animateHide if we call 392 // both hide and show in the same frame before the animation actually gets started. 393 // cancel() doesn't really remove the end action. 394 .withEndAction(null); 395 396 // Synchronize the motion with the Keyguard fading if necessary. 397 if (mPhoneStatusBar.isKeyguardFadingAway()) { 398 v.animate() 399 .setDuration(mPhoneStatusBar.getKeyguardFadingAwayDuration()) 400 .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN) 401 .setStartDelay(mPhoneStatusBar.getKeyguardFadingAwayDelay()) 402 .start(); 403 } 404 } 405 406 /** 407 * Sets the dark area so {@link #setIconsDark} only affects the icons in the specified area. 408 * 409 * @param darkArea the area in which icons should change it's tint, in logical screen 410 * coordinates 411 */ 412 public void setIconsDarkArea(Rect darkArea) { 413 if (darkArea == null && mTintArea.isEmpty()) { 414 return; 415 } 416 if (darkArea == null) { 417 mTintArea.setEmpty(); 418 } else { 419 mTintArea.set(darkArea); 420 } 421 applyIconTint(); 422 mNotificationIconAreaController.setTintArea(darkArea); 423 } 424 425 public void setIconsDark(boolean dark, boolean animate) { 426 if (!animate) { 427 setIconTintInternal(dark ? 1.0f : 0.0f); 428 } else if (mTransitionPending) { 429 deferIconTintChange(dark ? 1.0f : 0.0f); 430 } else if (mTransitionDeferring) { 431 animateIconTint(dark ? 1.0f : 0.0f, 432 Math.max(0, mTransitionDeferringStartTime - SystemClock.uptimeMillis()), 433 mTransitionDeferringDuration); 434 } else { 435 animateIconTint(dark ? 1.0f : 0.0f, 0 /* delay */, DEFAULT_TINT_ANIMATION_DURATION); 436 } 437 } 438 439 private void animateIconTint(float targetDarkIntensity, long delay, 440 long duration) { 441 if (mTintAnimator != null) { 442 mTintAnimator.cancel(); 443 } 444 if (mDarkIntensity == targetDarkIntensity) { 445 return; 446 } 447 mTintAnimator = ValueAnimator.ofFloat(mDarkIntensity, targetDarkIntensity); 448 mTintAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 449 @Override 450 public void onAnimationUpdate(ValueAnimator animation) { 451 setIconTintInternal((Float) animation.getAnimatedValue()); 452 } 453 }); 454 mTintAnimator.setDuration(duration); 455 mTintAnimator.setStartDelay(delay); 456 mTintAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); 457 mTintAnimator.start(); 458 } 459 460 private void setIconTintInternal(float darkIntensity) { 461 mDarkIntensity = darkIntensity; 462 mIconTint = (int) ArgbEvaluator.getInstance().evaluate(darkIntensity, 463 mLightModeIconColorSingleTone, mDarkModeIconColorSingleTone); 464 mNotificationIconAreaController.setIconTint(mIconTint); 465 applyIconTint(); 466 } 467 468 private void deferIconTintChange(float darkIntensity) { 469 if (mTintChangePending && darkIntensity == mPendingDarkIntensity) { 470 return; 471 } 472 mTintChangePending = true; 473 mPendingDarkIntensity = darkIntensity; 474 } 475 476 /** 477 * @return the tint to apply to {@param view} depending on the desired tint {@param color} and 478 * the screen {@param tintArea} in which to apply that tint 479 */ 480 public static int getTint(Rect tintArea, View view, int color) { 481 if (isInArea(tintArea, view)) { 482 return color; 483 } else { 484 return DEFAULT_ICON_TINT; 485 } 486 } 487 488 /** 489 * @return the dark intensity to apply to {@param view} depending on the desired dark 490 * {@param intensity} and the screen {@param tintArea} in which to apply that intensity 491 */ 492 public static float getDarkIntensity(Rect tintArea, View view, float intensity) { 493 if (isInArea(tintArea, view)) { 494 return intensity; 495 } else { 496 return 0f; 497 } 498 } 499 500 /** 501 * @return true if more than half of the {@param view} area are in {@param area}, false 502 * otherwise 503 */ 504 private static boolean isInArea(Rect area, View view) { 505 if (area.isEmpty()) { 506 return true; 507 } 508 sTmpRect.set(area); 509 view.getLocationOnScreen(sTmpInt2); 510 int left = sTmpInt2[0]; 511 512 int intersectStart = Math.max(left, area.left); 513 int intersectEnd = Math.min(left + view.getWidth(), area.right); 514 int intersectAmount = Math.max(0, intersectEnd - intersectStart); 515 516 boolean coversFullStatusBar = area.top <= 0; 517 boolean majorityOfWidth = 2 * intersectAmount > view.getWidth(); 518 return majorityOfWidth && coversFullStatusBar; 519 } 520 521 private void applyIconTint() { 522 for (int i = 0; i < mStatusIcons.getChildCount(); i++) { 523 StatusBarIconView v = (StatusBarIconView) mStatusIcons.getChildAt(i); 524 v.setImageTintList(ColorStateList.valueOf(getTint(mTintArea, v, mIconTint))); 525 } 526 mSignalCluster.setIconTint(mIconTint, mDarkIntensity, mTintArea); 527 mBatteryMeterView.setDarkIntensity( 528 isInArea(mTintArea, mBatteryMeterView) ? mDarkIntensity : 0); 529 mClock.setTextColor(getTint(mTintArea, mClock, mIconTint)); 530 } 531 532 public void appTransitionPending() { 533 mTransitionPending = true; 534 } 535 536 public void appTransitionCancelled() { 537 if (mTransitionPending && mTintChangePending) { 538 mTintChangePending = false; 539 animateIconTint(mPendingDarkIntensity, 0 /* delay */, DEFAULT_TINT_ANIMATION_DURATION); 540 } 541 mTransitionPending = false; 542 } 543 544 public void appTransitionStarting(long startTime, long duration) { 545 if (mTransitionPending && mTintChangePending) { 546 mTintChangePending = false; 547 animateIconTint(mPendingDarkIntensity, 548 Math.max(0, startTime - SystemClock.uptimeMillis()), 549 duration); 550 551 } else if (mTransitionPending) { 552 553 // If we don't have a pending tint change yet, the change might come in the future until 554 // startTime is reached. 555 mTransitionDeferring = true; 556 mTransitionDeferringStartTime = startTime; 557 mTransitionDeferringDuration = duration; 558 mHandler.removeCallbacks(mTransitionDeferringDoneRunnable); 559 mHandler.postAtTime(mTransitionDeferringDoneRunnable, startTime); 560 } 561 mTransitionPending = false; 562 } 563 564 public static ArraySet<String> getIconBlacklist(String blackListStr) { 565 ArraySet<String> ret = new ArraySet<String>(); 566 if (blackListStr == null) { 567 blackListStr = "rotate,headset"; 568 } 569 String[] blacklist = blackListStr.split(","); 570 for (String slot : blacklist) { 571 if (!TextUtils.isEmpty(slot)) { 572 ret.add(slot); 573 } 574 } 575 return ret; 576 } 577 578 public void onDensityOrFontScaleChanged() { 579 loadDimens(); 580 mNotificationIconAreaController.onDensityOrFontScaleChanged(mContext); 581 updateClock(); 582 for (int i = 0; i < mStatusIcons.getChildCount(); i++) { 583 View child = mStatusIcons.getChildAt(i); 584 LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( 585 ViewGroup.LayoutParams.WRAP_CONTENT, mIconSize); 586 lp.setMargins(mIconHPadding, 0, mIconHPadding, 0); 587 child.setLayoutParams(lp); 588 } 589 for (int i = 0; i < mStatusIconsKeyguard.getChildCount(); i++) { 590 View child = mStatusIconsKeyguard.getChildAt(i); 591 LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( 592 ViewGroup.LayoutParams.WRAP_CONTENT, mIconSize); 593 child.setLayoutParams(lp); 594 } 595 scaleBatteryMeterViews(mContext); 596 } 597 598 private void updateClock() { 599 FontSizeUtils.updateFontSize(mClock, R.dimen.status_bar_clock_size); 600 mClock.setPaddingRelative( 601 mContext.getResources().getDimensionPixelSize( 602 R.dimen.status_bar_clock_starting_padding), 603 0, 604 mContext.getResources().getDimensionPixelSize( 605 R.dimen.status_bar_clock_end_padding), 606 0); 607 } 608 } 609