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