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