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