QuickStatusBarHeader.java revision 20b87bf0ae8c880a76d0de859b3665b7d4f2e144
1/* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 * except in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the 10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 11 * KIND, either express or implied. See the License for the specific language governing 12 * permissions and limitations under the License. 13 */ 14 15package com.android.systemui.qs; 16 17import static android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS; 18 19import android.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.annotation.ColorInt; 22import android.app.ActivityManager; 23import android.app.AlarmManager; 24import android.content.BroadcastReceiver; 25import android.content.Context; 26import android.content.Intent; 27import android.content.IntentFilter; 28import android.content.res.Configuration; 29import android.content.res.Resources; 30import android.graphics.Color; 31import android.graphics.Rect; 32import android.media.AudioManager; 33import android.os.Handler; 34import android.provider.AlarmClock; 35import android.support.annotation.VisibleForTesting; 36import android.text.format.DateUtils; 37import android.util.AttributeSet; 38import android.util.Log; 39import android.util.Pair; 40import android.view.View; 41import android.view.WindowInsets; 42import android.widget.ImageView; 43import android.widget.RelativeLayout; 44import android.widget.TextView; 45 46import com.android.settingslib.Utils; 47import com.android.systemui.BatteryMeterView; 48import com.android.systemui.Dependency; 49import com.android.systemui.Prefs; 50import com.android.systemui.R; 51import com.android.systemui.SysUiServiceProvider; 52import com.android.systemui.plugins.ActivityStarter; 53import com.android.systemui.qs.QSDetail.Callback; 54import com.android.systemui.statusbar.CommandQueue; 55import com.android.systemui.statusbar.phone.PhoneStatusBarView; 56import com.android.systemui.statusbar.phone.StatusBarIconController; 57import com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconManager; 58import com.android.systemui.statusbar.policy.Clock; 59import com.android.systemui.statusbar.phone.StatusIconContainer; 60import com.android.systemui.statusbar.policy.DarkIconDispatcher; 61import com.android.systemui.statusbar.policy.DarkIconDispatcher.DarkReceiver; 62import com.android.systemui.statusbar.policy.DateView; 63import com.android.systemui.statusbar.policy.NextAlarmController; 64 65import java.util.Locale; 66 67/** 68 * View that contains the top-most bits of the screen (primarily the status bar with date, time, and 69 * battery) and also contains the {@link QuickQSPanel} along with some of the panel's inner 70 * contents. 71 */ 72public class QuickStatusBarHeader extends RelativeLayout implements CommandQueue.Callbacks, 73 View.OnClickListener, NextAlarmController.NextAlarmChangeCallback { 74 private static final String TAG = "QuickStatusBarHeader"; 75 private static final boolean DEBUG = false; 76 77 /** Delay for auto fading out the long press tooltip after it's fully visible (in ms). */ 78 private static final long AUTO_FADE_OUT_DELAY_MS = DateUtils.SECOND_IN_MILLIS * 6; 79 private static final int FADE_ANIMATION_DURATION_MS = 300; 80 private static final int TOOLTIP_NOT_YET_SHOWN_COUNT = 0; 81 public static final int MAX_TOOLTIP_SHOWN_COUNT = 2; 82 83 private final Handler mHandler = new Handler(); 84 85 private QSPanel mQsPanel; 86 87 private boolean mExpanded; 88 private boolean mListening; 89 private boolean mQsDisabled; 90 91 protected QuickQSPanel mHeaderQsPanel; 92 protected QSTileHost mHost; 93 private TintedIconManager mIconManager; 94 private TouchAnimator mStatusIconsAlphaAnimator; 95 private TouchAnimator mHeaderTextContainerAlphaAnimator; 96 97 private View mSystemIconsView; 98 private View mQuickQsStatusIcons; 99 private View mDate; 100 private View mHeaderTextContainerView; 101 /** View containing the next alarm and ringer mode info. */ 102 private View mStatusContainer; 103 /** Tooltip for educating users that they can long press on icons to see more details. */ 104 private View mLongPressTooltipView; 105 106 private int mRingerMode = AudioManager.RINGER_MODE_NORMAL; 107 private AlarmManager.AlarmClockInfo mNextAlarm; 108 109 private ImageView mNextAlarmIcon; 110 /** {@link TextView} containing the actual text indicating when the next alarm will go off. */ 111 private TextView mNextAlarmTextView; 112 private View mStatusSeparator; 113 private ImageView mRingerModeIcon; 114 private TextView mRingerModeTextView; 115 private BatteryMeterView mBatteryMeterView; 116 private Clock mClockView; 117 private DateView mDateView; 118 119 private NextAlarmController mAlarmController; 120 /** Counts how many times the long press tooltip has been shown to the user. */ 121 private int mShownCount; 122 123 private final BroadcastReceiver mRingerReceiver = new BroadcastReceiver() { 124 @Override 125 public void onReceive(Context context, Intent intent) { 126 mRingerMode = intent.getIntExtra(AudioManager.EXTRA_RINGER_MODE, -1); 127 updateStatusText(); 128 } 129 }; 130 131 /** 132 * Runnable for automatically fading out the long press tooltip (as if it were animating away). 133 */ 134 private final Runnable mAutoFadeOutTooltipRunnable = () -> hideLongPressTooltip(false); 135 136 public QuickStatusBarHeader(Context context, AttributeSet attrs) { 137 super(context, attrs); 138 mAlarmController = Dependency.get(NextAlarmController.class); 139 mShownCount = getStoredShownCount(); 140 } 141 142 @Override 143 protected void onFinishInflate() { 144 super.onFinishInflate(); 145 146 mHeaderQsPanel = findViewById(R.id.quick_qs_panel); 147 mDate = findViewById(R.id.date); 148 mDate.setOnClickListener(this); 149 mSystemIconsView = findViewById(R.id.quick_status_bar_system_icons); 150 mQuickQsStatusIcons = findViewById(R.id.quick_qs_status_icons); 151 StatusIconContainer iconContainer = findViewById(R.id.statusIcons); 152 iconContainer.setShouldRestrictIcons(false); 153 mIconManager = new TintedIconManager(iconContainer); 154 155 // Views corresponding to the header info section (e.g. tooltip and next alarm). 156 mHeaderTextContainerView = findViewById(R.id.header_text_container); 157 mLongPressTooltipView = findViewById(R.id.long_press_tooltip); 158 mStatusContainer = findViewById(R.id.status_container); 159 mStatusSeparator = findViewById(R.id.status_separator); 160 mNextAlarmIcon = findViewById(R.id.next_alarm_icon); 161 mNextAlarmTextView = findViewById(R.id.next_alarm_text); 162 mRingerModeIcon = findViewById(R.id.ringer_mode_icon); 163 mRingerModeTextView = findViewById(R.id.ringer_mode_text); 164 165 updateResources(); 166 167 Rect tintArea = new Rect(0, 0, 0, 0); 168 int colorForeground = Utils.getColorAttr(getContext(), android.R.attr.colorForeground); 169 float intensity = getColorIntensity(colorForeground); 170 int fillColor = fillColorForIntensity(intensity, getContext()); 171 172 // Set light text on the header icons because they will always be on a black background 173 applyDarkness(R.id.clock, tintArea, 0, DarkIconDispatcher.DEFAULT_ICON_TINT); 174 175 // Set the correct tint for the status icons so they contrast 176 mIconManager.setTint(fillColor); 177 178 mBatteryMeterView = findViewById(R.id.battery); 179 mBatteryMeterView.setForceShowPercent(true); 180 mClockView = findViewById(R.id.clock); 181 mDateView = findViewById(R.id.date); 182 } 183 184 private void updateStatusText() { 185 boolean ringerVisible = false; 186 if (mRingerMode == AudioManager.RINGER_MODE_VIBRATE) { 187 mRingerModeIcon.setImageResource(R.drawable.stat_sys_ringer_vibrate); 188 mRingerModeTextView.setText(R.string.qs_status_phone_vibrate); 189 ringerVisible = true; 190 } else if (mRingerMode == AudioManager.RINGER_MODE_SILENT) { 191 mRingerModeIcon.setImageResource(R.drawable.stat_sys_ringer_silent); 192 mRingerModeTextView.setText(R.string.qs_status_phone_muted); 193 ringerVisible = true; 194 } 195 mRingerModeIcon.setVisibility(ringerVisible ? View.VISIBLE : View.GONE); 196 mRingerModeTextView.setVisibility(ringerVisible ? View.VISIBLE : View.GONE); 197 198 boolean alarmVisible = false; 199 if (mNextAlarm != null) { 200 alarmVisible = true; 201 mNextAlarmTextView.setText(formatNextAlarm(mNextAlarm)); 202 } 203 mNextAlarmIcon.setVisibility(alarmVisible ? View.VISIBLE : View.GONE); 204 mNextAlarmTextView.setVisibility(alarmVisible ? View.VISIBLE : View.GONE); 205 mStatusSeparator.setVisibility(alarmVisible && ringerVisible ? View.VISIBLE : View.GONE); 206 updateTooltipShow(); 207 } 208 209 210 private void applyDarkness(int id, Rect tintArea, float intensity, int color) { 211 View v = findViewById(id); 212 if (v instanceof DarkReceiver) { 213 ((DarkReceiver) v).onDarkChanged(tintArea, intensity, color); 214 } 215 } 216 217 private int fillColorForIntensity(float intensity, Context context) { 218 if (intensity == 0) { 219 return context.getColor(R.color.light_mode_icon_color_dual_tone_fill); 220 } 221 return context.getColor(R.color.dark_mode_icon_color_dual_tone_fill); 222 } 223 224 @Override 225 protected void onConfigurationChanged(Configuration newConfig) { 226 super.onConfigurationChanged(newConfig); 227 updateResources(); 228 229 // Update color schemes in landscape to use wallpaperTextColor 230 boolean shouldUseWallpaperTextColor = 231 newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE; 232 mBatteryMeterView.useWallpaperTextColor(shouldUseWallpaperTextColor); 233 mClockView.useWallpaperTextColor(shouldUseWallpaperTextColor); 234 mDateView.useWallpaperTextColor(shouldUseWallpaperTextColor); 235 } 236 237 @Override 238 public void onRtlPropertiesChanged(int layoutDirection) { 239 super.onRtlPropertiesChanged(layoutDirection); 240 updateResources(); 241 } 242 243 private void updateResources() { 244 Resources resources = mContext.getResources(); 245 246 // Update height for a few views, especially due to landscape mode restricting space. 247 mHeaderTextContainerView.getLayoutParams().height = 248 resources.getDimensionPixelSize(R.dimen.qs_header_tooltip_height); 249 mHeaderTextContainerView.setLayoutParams(mHeaderTextContainerView.getLayoutParams()); 250 251 mSystemIconsView.getLayoutParams().height = resources.getDimensionPixelSize( 252 com.android.internal.R.dimen.quick_qs_offset_height); 253 mSystemIconsView.setLayoutParams(mSystemIconsView.getLayoutParams()); 254 255 getLayoutParams().height = 256 resources.getDimensionPixelSize(com.android.internal.R.dimen.quick_qs_total_height); 257 setLayoutParams(getLayoutParams()); 258 259 updateStatusIconAlphaAnimator(); 260 updateHeaderTextContainerAlphaAnimator(); 261 } 262 263 private void updateStatusIconAlphaAnimator() { 264 mStatusIconsAlphaAnimator = new TouchAnimator.Builder() 265 .addFloat(mQuickQsStatusIcons, "alpha", 1, 0) 266 .build(); 267 } 268 269 private void updateHeaderTextContainerAlphaAnimator() { 270 mHeaderTextContainerAlphaAnimator = new TouchAnimator.Builder() 271 .addFloat(mHeaderTextContainerView, "alpha", 0, 1) 272 .setStartDelay(.5f) 273 .build(); 274 } 275 276 public void setExpanded(boolean expanded) { 277 if (mExpanded == expanded) return; 278 mExpanded = expanded; 279 mHeaderQsPanel.setExpanded(expanded); 280 updateEverything(); 281 } 282 283 /** 284 * Animates the inner contents based on the given expansion details. 285 * 286 * @param isKeyguardShowing whether or not we're showing the keyguard (a.k.a. lockscreen) 287 * @param expansionFraction how much the QS panel is expanded/pulled out (up to 1f) 288 * @param panelTranslationY how much the panel has physically moved down vertically (required 289 * for keyguard animations only) 290 */ 291 public void setExpansion(boolean isKeyguardShowing, float expansionFraction, 292 float panelTranslationY) { 293 final float keyguardExpansionFraction = isKeyguardShowing ? 1f : expansionFraction; 294 if (mStatusIconsAlphaAnimator != null) { 295 mStatusIconsAlphaAnimator.setPosition(keyguardExpansionFraction); 296 } 297 298 if (isKeyguardShowing) { 299 // If the keyguard is showing, we want to offset the text so that it comes in at the 300 // same time as the panel as it slides down. 301 mHeaderTextContainerView.setTranslationY(panelTranslationY); 302 } else { 303 mHeaderTextContainerView.setTranslationY(0f); 304 } 305 306 if (mHeaderTextContainerAlphaAnimator != null) { 307 mHeaderTextContainerAlphaAnimator.setPosition(keyguardExpansionFraction); 308 } 309 310 // Check the original expansion fraction - we don't want to show the tooltip until the 311 // panel is pulled all the way out. 312 if (expansionFraction == 1f) { 313 // QS is fully expanded, bring in the tooltip. 314 showLongPressTooltip(); 315 } 316 } 317 318 /** Returns the latest stored tooltip shown count from SharedPreferences. */ 319 private int getStoredShownCount() { 320 return Prefs.getInt( 321 mContext, 322 Prefs.Key.QS_LONG_PRESS_TOOLTIP_SHOWN_COUNT, 323 TOOLTIP_NOT_YET_SHOWN_COUNT); 324 } 325 326 @Override 327 public void disable(int state1, int state2, boolean animate) { 328 final boolean disabled = (state2 & DISABLE2_QUICK_SETTINGS) != 0; 329 if (disabled == mQsDisabled) return; 330 mQsDisabled = disabled; 331 mHeaderQsPanel.setDisabledByPolicy(disabled); 332 final int rawHeight = (int) getResources().getDimension( 333 com.android.internal.R.dimen.quick_qs_total_height); 334 getLayoutParams().height = disabled ? (rawHeight - mHeaderQsPanel.getHeight()) : rawHeight; 335 } 336 337 @Override 338 public void onAttachedToWindow() { 339 SysUiServiceProvider.getComponent(getContext(), CommandQueue.class).addCallbacks(this); 340 Dependency.get(StatusBarIconController.class).addIconGroup(mIconManager); 341 requestApplyInsets(); 342 } 343 344 @Override 345 public WindowInsets onApplyWindowInsets(WindowInsets insets) { 346 Pair<Integer, Integer> padding = PhoneStatusBarView.cornerCutoutMargins( 347 insets.getDisplayCutout(), getDisplay()); 348 if (padding == null) { 349 setPadding(0, 0, 0, 0); 350 } else { 351 setPadding(padding.first, 0, padding.second, 0); 352 } 353 return super.onApplyWindowInsets(insets); 354 } 355 356 @Override 357 @VisibleForTesting 358 public void onDetachedFromWindow() { 359 setListening(false); 360 SysUiServiceProvider.getComponent(getContext(), CommandQueue.class).removeCallbacks(this); 361 Dependency.get(StatusBarIconController.class).removeIconGroup(mIconManager); 362 super.onDetachedFromWindow(); 363 } 364 365 public void setListening(boolean listening) { 366 if (listening == mListening) { 367 return; 368 } 369 mHeaderQsPanel.setListening(listening); 370 mListening = listening; 371 372 if (listening) { 373 mAlarmController.addCallback(this); 374 mContext.registerReceiver(mRingerReceiver, 375 new IntentFilter(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION)); 376 } else { 377 mAlarmController.removeCallback(this); 378 mContext.unregisterReceiver(mRingerReceiver); 379 } 380 } 381 382 @Override 383 public void onClick(View v) { 384 if(v == mDate){ 385 Dependency.get(ActivityStarter.class).postStartActivityDismissingKeyguard(new Intent( 386 AlarmClock.ACTION_SHOW_ALARMS),0); 387 } 388 } 389 390 @Override 391 public void onNextAlarmChanged(AlarmManager.AlarmClockInfo nextAlarm) { 392 mNextAlarm = nextAlarm; 393 updateStatusText(); 394 } 395 396 private void updateTooltipShow() { 397 if (hasStatusText()) { 398 hideLongPressTooltip(true /* shouldShowStatusText */); 399 } else { 400 hideStatusText(); 401 } 402 updateHeaderTextContainerAlphaAnimator(); 403 } 404 405 private boolean hasStatusText() { 406 return mNextAlarmTextView.getVisibility() == View.VISIBLE 407 || mRingerModeTextView.getVisibility() == View.VISIBLE; 408 } 409 410 /** 411 * Animates in the long press tooltip (as long as the next alarm text isn't currently occupying 412 * the space). 413 */ 414 public void showLongPressTooltip() { 415 // If we have status text to show, don't bother fading in the tooltip. 416 if (hasStatusText()) { 417 return; 418 } 419 420 if (mShownCount < MAX_TOOLTIP_SHOWN_COUNT) { 421 mLongPressTooltipView.animate().cancel(); 422 mLongPressTooltipView.setVisibility(View.VISIBLE); 423 mLongPressTooltipView.animate() 424 .alpha(1f) 425 .setDuration(FADE_ANIMATION_DURATION_MS) 426 .setListener(new AnimatorListenerAdapter() { 427 @Override 428 public void onAnimationEnd(Animator animation) { 429 mHandler.postDelayed( 430 mAutoFadeOutTooltipRunnable, AUTO_FADE_OUT_DELAY_MS); 431 } 432 }) 433 .start(); 434 435 // Increment and drop the shown count in prefs for the next time we're deciding to 436 // fade in the tooltip. We first sanity check that the tooltip count hasn't changed yet 437 // in prefs (say, from a long press). 438 if (getStoredShownCount() <= mShownCount) { 439 Prefs.putInt(mContext, Prefs.Key.QS_LONG_PRESS_TOOLTIP_SHOWN_COUNT, ++mShownCount); 440 } 441 } 442 } 443 444 /** 445 * Fades out the long press tooltip if it's partially visible - short circuits any running 446 * animation. Additionally has the ability to fade in the status info text. 447 * 448 * @param shouldShowStatusText whether we should fade in the status text 449 */ 450 private void hideLongPressTooltip(boolean shouldShowStatusText) { 451 mLongPressTooltipView.animate().cancel(); 452 if (mLongPressTooltipView.getVisibility() == View.VISIBLE 453 && mLongPressTooltipView.getAlpha() != 0f) { 454 mHandler.removeCallbacks(mAutoFadeOutTooltipRunnable); 455 mLongPressTooltipView.animate() 456 .alpha(0f) 457 .setDuration(FADE_ANIMATION_DURATION_MS) 458 .setListener(new AnimatorListenerAdapter() { 459 @Override 460 public void onAnimationEnd(Animator animation) { 461 if (DEBUG) Log.d(TAG, "hideLongPressTooltip: Hid long press tip"); 462 mLongPressTooltipView.setVisibility(View.INVISIBLE); 463 464 if (shouldShowStatusText) { 465 showStatus(); 466 } 467 } 468 }) 469 .start(); 470 } else { 471 mLongPressTooltipView.setVisibility(View.INVISIBLE); 472 if (shouldShowStatusText) { 473 showStatus(); 474 } 475 } 476 } 477 478 /** 479 * Fades in the updated status text. Note that if there's already a status showing, this will 480 * immediately hide it and fade in the updated status. 481 */ 482 private void showStatus() { 483 mStatusContainer.setAlpha(0f); 484 mStatusContainer.setVisibility(View.VISIBLE); 485 486 // Animate the alarm back in. Make sure to clear the animator listener for the animation! 487 mStatusContainer.animate() 488 .alpha(1f) 489 .setDuration(FADE_ANIMATION_DURATION_MS) 490 .setListener(null) 491 .start(); 492 } 493 494 /** Fades out and hides the status text. */ 495 private void hideStatusText() { 496 if (mStatusContainer.getVisibility() == View.VISIBLE) { 497 mStatusContainer.animate() 498 .alpha(0f) 499 .setListener(new AnimatorListenerAdapter() { 500 @Override 501 public void onAnimationEnd(Animator animation) { 502 if (DEBUG) Log.d(TAG, "hideAlarmText: Hid alarm text"); 503 504 // Reset the alpha regardless of how the animation ends for the next 505 // time we show this view/want to animate it. 506 mStatusContainer.setVisibility(View.INVISIBLE); 507 mStatusContainer.setAlpha(1f); 508 } 509 }) 510 .start(); 511 } 512 } 513 514 public void updateEverything() { 515 post(() -> setClickable(false)); 516 } 517 518 public void setQSPanel(final QSPanel qsPanel) { 519 mQsPanel = qsPanel; 520 setupHost(qsPanel.getHost()); 521 } 522 523 public void setupHost(final QSTileHost host) { 524 mHost = host; 525 //host.setHeaderView(mExpandIndicator); 526 mHeaderQsPanel.setQSPanelAndHeader(mQsPanel, this); 527 mHeaderQsPanel.setHost(host, null /* No customization in header */); 528 529 // Use SystemUI context to get battery meter colors, and let it use the default tint (white) 530 mBatteryMeterView.setColorsFromContext(mHost.getContext()); 531 mBatteryMeterView.onDarkChanged(new Rect(), 0, DarkIconDispatcher.DEFAULT_ICON_TINT); 532 } 533 534 public void setCallback(Callback qsPanelCallback) { 535 mHeaderQsPanel.setCallback(qsPanelCallback); 536 } 537 538 private String formatNextAlarm(AlarmManager.AlarmClockInfo info) { 539 if (info == null) { 540 return ""; 541 } 542 String skeleton = android.text.format.DateFormat 543 .is24HourFormat(mContext, ActivityManager.getCurrentUser()) ? "EHm" : "Ehma"; 544 String pattern = android.text.format.DateFormat 545 .getBestDateTimePattern(Locale.getDefault(), skeleton); 546 return android.text.format.DateFormat.format(pattern, info.getTriggerTime()).toString(); 547 } 548 549 public static float getColorIntensity(@ColorInt int color) { 550 return color == Color.WHITE ? 0 : 1; 551 } 552 553} 554