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