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