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 15 package com.android.systemui.qs; 16 17 import static android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS; 18 19 import static com.android.systemui.util.InjectionInflationController.VIEW_CONTEXT; 20 21 import android.annotation.ColorInt; 22 import android.app.ActivityManager; 23 import android.app.AlarmManager; 24 import android.content.BroadcastReceiver; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.IntentFilter; 28 import android.content.res.ColorStateList; 29 import android.content.res.Configuration; 30 import android.content.res.Resources; 31 import android.graphics.Color; 32 import android.graphics.Rect; 33 import android.media.AudioManager; 34 import android.os.Handler; 35 import android.provider.AlarmClock; 36 import android.provider.Settings; 37 import android.service.notification.ZenModeConfig; 38 import android.text.format.DateUtils; 39 import android.util.AttributeSet; 40 import android.util.Log; 41 import android.util.Pair; 42 import android.view.ContextThemeWrapper; 43 import android.view.DisplayCutout; 44 import android.view.View; 45 import android.view.WindowInsets; 46 import android.widget.FrameLayout; 47 import android.widget.ImageView; 48 import android.widget.RelativeLayout; 49 import android.widget.TextView; 50 51 import androidx.annotation.VisibleForTesting; 52 53 import com.android.settingslib.Utils; 54 import com.android.systemui.BatteryMeterView; 55 import com.android.systemui.DualToneHandler; 56 import com.android.systemui.R; 57 import com.android.systemui.plugins.ActivityStarter; 58 import com.android.systemui.plugins.DarkIconDispatcher; 59 import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver; 60 import com.android.systemui.qs.QSDetail.Callback; 61 import com.android.systemui.statusbar.phone.PhoneStatusBarView; 62 import com.android.systemui.statusbar.phone.StatusBarIconController; 63 import com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconManager; 64 import com.android.systemui.statusbar.phone.StatusIconContainer; 65 import com.android.systemui.statusbar.policy.Clock; 66 import com.android.systemui.statusbar.policy.DateView; 67 import com.android.systemui.statusbar.policy.NextAlarmController; 68 import com.android.systemui.statusbar.policy.ZenModeController; 69 70 import java.util.Locale; 71 import java.util.Objects; 72 73 import javax.inject.Inject; 74 import javax.inject.Named; 75 76 /** 77 * View that contains the top-most bits of the screen (primarily the status bar with date, time, and 78 * battery) and also contains the {@link QuickQSPanel} along with some of the panel's inner 79 * contents. 80 */ 81 public class QuickStatusBarHeader extends RelativeLayout implements 82 View.OnClickListener, NextAlarmController.NextAlarmChangeCallback, 83 ZenModeController.Callback { 84 private static final String TAG = "QuickStatusBarHeader"; 85 private static final boolean DEBUG = false; 86 87 /** Delay for auto fading out the long press tooltip after it's fully visible (in ms). */ 88 private static final long AUTO_FADE_OUT_DELAY_MS = DateUtils.SECOND_IN_MILLIS * 6; 89 private static final int FADE_ANIMATION_DURATION_MS = 300; 90 private static final int TOOLTIP_NOT_YET_SHOWN_COUNT = 0; 91 public static final int MAX_TOOLTIP_SHOWN_COUNT = 2; 92 93 private final Handler mHandler = new Handler(); 94 private final NextAlarmController mAlarmController; 95 private final ZenModeController mZenController; 96 private final StatusBarIconController mStatusBarIconController; 97 private final ActivityStarter mActivityStarter; 98 99 private QSPanel mQsPanel; 100 101 private boolean mExpanded; 102 private boolean mListening; 103 private boolean mQsDisabled; 104 105 private QSCarrierGroup mCarrierGroup; 106 protected QuickQSPanel mHeaderQsPanel; 107 protected QSTileHost mHost; 108 private TintedIconManager mIconManager; 109 private TouchAnimator mStatusIconsAlphaAnimator; 110 private TouchAnimator mHeaderTextContainerAlphaAnimator; 111 private DualToneHandler mDualToneHandler; 112 113 private View mSystemIconsView; 114 private View mQuickQsStatusIcons; 115 private View mHeaderTextContainerView; 116 117 private int mRingerMode = AudioManager.RINGER_MODE_NORMAL; 118 private AlarmManager.AlarmClockInfo mNextAlarm; 119 120 private ImageView mNextAlarmIcon; 121 /** {@link TextView} containing the actual text indicating when the next alarm will go off. */ 122 private TextView mNextAlarmTextView; 123 private View mNextAlarmContainer; 124 private View mStatusSeparator; 125 private ImageView mRingerModeIcon; 126 private TextView mRingerModeTextView; 127 private View mRingerContainer; 128 private Clock mClockView; 129 private DateView mDateView; 130 private BatteryMeterView mBatteryRemainingIcon; 131 132 private final BroadcastReceiver mRingerReceiver = new BroadcastReceiver() { 133 @Override 134 public void onReceive(Context context, Intent intent) { 135 mRingerMode = intent.getIntExtra(AudioManager.EXTRA_RINGER_MODE, -1); 136 updateStatusText(); 137 } 138 }; 139 private boolean mHasTopCutout = false; 140 141 @Inject QuickStatusBarHeader(@amedVIEW_CONTEXT) Context context, AttributeSet attrs, NextAlarmController nextAlarmController, ZenModeController zenModeController, StatusBarIconController statusBarIconController, ActivityStarter activityStarter)142 public QuickStatusBarHeader(@Named(VIEW_CONTEXT) Context context, AttributeSet attrs, 143 NextAlarmController nextAlarmController, ZenModeController zenModeController, 144 StatusBarIconController statusBarIconController, 145 ActivityStarter activityStarter) { 146 super(context, attrs); 147 mAlarmController = nextAlarmController; 148 mZenController = zenModeController; 149 mStatusBarIconController = statusBarIconController; 150 mActivityStarter = activityStarter; 151 mDualToneHandler = new DualToneHandler( 152 new ContextThemeWrapper(context, R.style.QSHeaderTheme)); 153 } 154 155 @Override onFinishInflate()156 protected void onFinishInflate() { 157 super.onFinishInflate(); 158 159 mHeaderQsPanel = findViewById(R.id.quick_qs_panel); 160 mSystemIconsView = findViewById(R.id.quick_status_bar_system_icons); 161 mQuickQsStatusIcons = findViewById(R.id.quick_qs_status_icons); 162 StatusIconContainer iconContainer = findViewById(R.id.statusIcons); 163 iconContainer.setShouldRestrictIcons(false); 164 mIconManager = new TintedIconManager(iconContainer); 165 166 // Views corresponding to the header info section (e.g. ringer and next alarm). 167 mHeaderTextContainerView = findViewById(R.id.header_text_container); 168 mStatusSeparator = findViewById(R.id.status_separator); 169 mNextAlarmIcon = findViewById(R.id.next_alarm_icon); 170 mNextAlarmTextView = findViewById(R.id.next_alarm_text); 171 mNextAlarmContainer = findViewById(R.id.alarm_container); 172 mNextAlarmContainer.setOnClickListener(this::onClick); 173 mRingerModeIcon = findViewById(R.id.ringer_mode_icon); 174 mRingerModeTextView = findViewById(R.id.ringer_mode_text); 175 mRingerContainer = findViewById(R.id.ringer_container); 176 mCarrierGroup = findViewById(R.id.carrier_group); 177 178 179 updateResources(); 180 181 Rect tintArea = new Rect(0, 0, 0, 0); 182 int colorForeground = Utils.getColorAttrDefaultColor(getContext(), 183 android.R.attr.colorForeground); 184 float intensity = getColorIntensity(colorForeground); 185 int fillColor = mDualToneHandler.getSingleColor(intensity); 186 187 // Set light text on the header icons because they will always be on a black background 188 applyDarkness(R.id.clock, tintArea, 0, DarkIconDispatcher.DEFAULT_ICON_TINT); 189 190 // Set the correct tint for the status icons so they contrast 191 mIconManager.setTint(fillColor); 192 mNextAlarmIcon.setImageTintList(ColorStateList.valueOf(fillColor)); 193 mRingerModeIcon.setImageTintList(ColorStateList.valueOf(fillColor)); 194 195 mClockView = findViewById(R.id.clock); 196 mClockView.setOnClickListener(this); 197 mDateView = findViewById(R.id.date); 198 199 // Tint for the battery icons are handled in setupHost() 200 mBatteryRemainingIcon = findViewById(R.id.batteryRemainingIcon); 201 // Don't need to worry about tuner settings for this icon 202 mBatteryRemainingIcon.setIgnoreTunerUpdates(true); 203 // QS will always show the estimate, and BatteryMeterView handles the case where 204 // it's unavailable or charging 205 mBatteryRemainingIcon.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE); 206 mRingerModeTextView.setSelected(true); 207 mNextAlarmTextView.setSelected(true); 208 } 209 updateStatusText()210 private void updateStatusText() { 211 boolean changed = updateRingerStatus() || updateAlarmStatus(); 212 213 if (changed) { 214 boolean alarmVisible = mNextAlarmTextView.getVisibility() == View.VISIBLE; 215 boolean ringerVisible = mRingerModeTextView.getVisibility() == View.VISIBLE; 216 mStatusSeparator.setVisibility(alarmVisible && ringerVisible ? View.VISIBLE 217 : View.GONE); 218 } 219 } 220 updateRingerStatus()221 private boolean updateRingerStatus() { 222 boolean isOriginalVisible = mRingerModeTextView.getVisibility() == View.VISIBLE; 223 CharSequence originalRingerText = mRingerModeTextView.getText(); 224 225 boolean ringerVisible = false; 226 if (!ZenModeConfig.isZenOverridingRinger(mZenController.getZen(), 227 mZenController.getConsolidatedPolicy())) { 228 if (mRingerMode == AudioManager.RINGER_MODE_VIBRATE) { 229 mRingerModeIcon.setImageResource(R.drawable.ic_volume_ringer_vibrate); 230 mRingerModeTextView.setText(R.string.qs_status_phone_vibrate); 231 ringerVisible = true; 232 } else if (mRingerMode == AudioManager.RINGER_MODE_SILENT) { 233 mRingerModeIcon.setImageResource(R.drawable.ic_volume_ringer_mute); 234 mRingerModeTextView.setText(R.string.qs_status_phone_muted); 235 ringerVisible = true; 236 } 237 } 238 mRingerModeIcon.setVisibility(ringerVisible ? View.VISIBLE : View.GONE); 239 mRingerModeTextView.setVisibility(ringerVisible ? View.VISIBLE : View.GONE); 240 mRingerContainer.setVisibility(ringerVisible ? View.VISIBLE : View.GONE); 241 242 return isOriginalVisible != ringerVisible || 243 !Objects.equals(originalRingerText, mRingerModeTextView.getText()); 244 } 245 updateAlarmStatus()246 private boolean updateAlarmStatus() { 247 boolean isOriginalVisible = mNextAlarmTextView.getVisibility() == View.VISIBLE; 248 CharSequence originalAlarmText = mNextAlarmTextView.getText(); 249 250 boolean alarmVisible = false; 251 if (mNextAlarm != null) { 252 alarmVisible = true; 253 mNextAlarmTextView.setText(formatNextAlarm(mNextAlarm)); 254 } 255 mNextAlarmIcon.setVisibility(alarmVisible ? View.VISIBLE : View.GONE); 256 mNextAlarmTextView.setVisibility(alarmVisible ? View.VISIBLE : View.GONE); 257 mNextAlarmContainer.setVisibility(alarmVisible ? View.VISIBLE : View.GONE); 258 259 return isOriginalVisible != alarmVisible || 260 !Objects.equals(originalAlarmText, mNextAlarmTextView.getText()); 261 } 262 applyDarkness(int id, Rect tintArea, float intensity, int color)263 private void applyDarkness(int id, Rect tintArea, float intensity, int color) { 264 View v = findViewById(id); 265 if (v instanceof DarkReceiver) { 266 ((DarkReceiver) v).onDarkChanged(tintArea, intensity, color); 267 } 268 } 269 270 @Override onConfigurationChanged(Configuration newConfig)271 protected void onConfigurationChanged(Configuration newConfig) { 272 super.onConfigurationChanged(newConfig); 273 updateResources(); 274 275 // Update color schemes in landscape to use wallpaperTextColor 276 boolean shouldUseWallpaperTextColor = 277 newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE; 278 mClockView.useWallpaperTextColor(shouldUseWallpaperTextColor); 279 } 280 281 @Override onRtlPropertiesChanged(int layoutDirection)282 public void onRtlPropertiesChanged(int layoutDirection) { 283 super.onRtlPropertiesChanged(layoutDirection); 284 updateResources(); 285 } 286 287 /** 288 * The height of QQS should always be the status bar height + 128dp. This is normally easy, but 289 * when there is a notch involved the status bar can remain a fixed pixel size. 290 */ updateMinimumHeight()291 private void updateMinimumHeight() { 292 int sbHeight = mContext.getResources().getDimensionPixelSize( 293 com.android.internal.R.dimen.status_bar_height); 294 int qqsHeight = mContext.getResources().getDimensionPixelSize( 295 R.dimen.qs_quick_header_panel_height); 296 297 setMinimumHeight(sbHeight + qqsHeight); 298 } 299 updateResources()300 private void updateResources() { 301 Resources resources = mContext.getResources(); 302 updateMinimumHeight(); 303 304 // Update height for a few views, especially due to landscape mode restricting space. 305 mHeaderTextContainerView.getLayoutParams().height = 306 resources.getDimensionPixelSize(R.dimen.qs_header_tooltip_height); 307 mHeaderTextContainerView.setLayoutParams(mHeaderTextContainerView.getLayoutParams()); 308 309 mSystemIconsView.getLayoutParams().height = resources.getDimensionPixelSize( 310 com.android.internal.R.dimen.quick_qs_offset_height); 311 mSystemIconsView.setLayoutParams(mSystemIconsView.getLayoutParams()); 312 313 FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams(); 314 if (mQsDisabled) { 315 lp.height = resources.getDimensionPixelSize( 316 com.android.internal.R.dimen.quick_qs_offset_height); 317 } else { 318 lp.height = Math.max(getMinimumHeight(), 319 resources.getDimensionPixelSize( 320 com.android.internal.R.dimen.quick_qs_total_height)); 321 } 322 323 setLayoutParams(lp); 324 325 updateStatusIconAlphaAnimator(); 326 updateHeaderTextContainerAlphaAnimator(); 327 } 328 updateStatusIconAlphaAnimator()329 private void updateStatusIconAlphaAnimator() { 330 mStatusIconsAlphaAnimator = new TouchAnimator.Builder() 331 .addFloat(mQuickQsStatusIcons, "alpha", 1, 0, 0) 332 .build(); 333 } 334 updateHeaderTextContainerAlphaAnimator()335 private void updateHeaderTextContainerAlphaAnimator() { 336 mHeaderTextContainerAlphaAnimator = new TouchAnimator.Builder() 337 .addFloat(mHeaderTextContainerView, "alpha", 0, 0, 1) 338 .build(); 339 } 340 setExpanded(boolean expanded)341 public void setExpanded(boolean expanded) { 342 if (mExpanded == expanded) return; 343 mExpanded = expanded; 344 mHeaderQsPanel.setExpanded(expanded); 345 updateEverything(); 346 } 347 348 /** 349 * Animates the inner contents based on the given expansion details. 350 * 351 * @param forceExpanded whether we should show the state expanded forcibly 352 * @param expansionFraction how much the QS panel is expanded/pulled out (up to 1f) 353 * @param panelTranslationY how much the panel has physically moved down vertically (required 354 * for keyguard animations only) 355 */ setExpansion(boolean forceExpanded, float expansionFraction, float panelTranslationY)356 public void setExpansion(boolean forceExpanded, float expansionFraction, 357 float panelTranslationY) { 358 final float keyguardExpansionFraction = forceExpanded ? 1f : expansionFraction; 359 if (mStatusIconsAlphaAnimator != null) { 360 mStatusIconsAlphaAnimator.setPosition(keyguardExpansionFraction); 361 } 362 363 if (forceExpanded) { 364 // If the keyguard is showing, we want to offset the text so that it comes in at the 365 // same time as the panel as it slides down. 366 mHeaderTextContainerView.setTranslationY(panelTranslationY); 367 } else { 368 mHeaderTextContainerView.setTranslationY(0f); 369 } 370 371 if (mHeaderTextContainerAlphaAnimator != null) { 372 mHeaderTextContainerAlphaAnimator.setPosition(keyguardExpansionFraction); 373 if (keyguardExpansionFraction > 0) { 374 mHeaderTextContainerView.setVisibility(VISIBLE); 375 } else { 376 mHeaderTextContainerView.setVisibility(INVISIBLE); 377 } 378 } 379 } 380 disable(int state1, int state2, boolean animate)381 public void disable(int state1, int state2, boolean animate) { 382 final boolean disabled = (state2 & DISABLE2_QUICK_SETTINGS) != 0; 383 if (disabled == mQsDisabled) return; 384 mQsDisabled = disabled; 385 mHeaderQsPanel.setDisabledByPolicy(disabled); 386 mHeaderTextContainerView.setVisibility(mQsDisabled ? View.GONE : View.VISIBLE); 387 mQuickQsStatusIcons.setVisibility(mQsDisabled ? View.GONE : View.VISIBLE); 388 updateResources(); 389 } 390 391 @Override onAttachedToWindow()392 public void onAttachedToWindow() { 393 super.onAttachedToWindow(); 394 mStatusBarIconController.addIconGroup(mIconManager); 395 requestApplyInsets(); 396 } 397 398 @Override onApplyWindowInsets(WindowInsets insets)399 public WindowInsets onApplyWindowInsets(WindowInsets insets) { 400 DisplayCutout cutout = insets.getDisplayCutout(); 401 Pair<Integer, Integer> padding = PhoneStatusBarView.cornerCutoutMargins( 402 cutout, getDisplay()); 403 if (padding == null) { 404 mSystemIconsView.setPaddingRelative( 405 getResources().getDimensionPixelSize(R.dimen.status_bar_padding_start), 406 getResources().getDimensionPixelSize(R.dimen.status_bar_padding_top), 407 getResources().getDimensionPixelSize(R.dimen.status_bar_padding_end), 408 0); 409 } else { 410 mSystemIconsView.setPadding( 411 padding.first, 412 getResources().getDimensionPixelSize(R.dimen.status_bar_padding_top), 413 padding.second, 0); 414 415 } 416 return super.onApplyWindowInsets(insets); 417 } 418 419 @Override 420 @VisibleForTesting onDetachedFromWindow()421 public void onDetachedFromWindow() { 422 setListening(false); 423 mStatusBarIconController.removeIconGroup(mIconManager); 424 super.onDetachedFromWindow(); 425 } 426 setListening(boolean listening)427 public void setListening(boolean listening) { 428 if (listening == mListening) { 429 return; 430 } 431 mHeaderQsPanel.setListening(listening); 432 mListening = listening; 433 mCarrierGroup.setListening(mListening); 434 435 if (listening) { 436 mZenController.addCallback(this); 437 mAlarmController.addCallback(this); 438 mContext.registerReceiver(mRingerReceiver, 439 new IntentFilter(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION)); 440 } else { 441 mZenController.removeCallback(this); 442 mAlarmController.removeCallback(this); 443 mContext.unregisterReceiver(mRingerReceiver); 444 } 445 } 446 447 @Override onClick(View v)448 public void onClick(View v) { 449 if (v == mClockView) { 450 mActivityStarter.postStartActivityDismissingKeyguard(new Intent( 451 AlarmClock.ACTION_SHOW_ALARMS), 0); 452 } else if (v == mNextAlarmContainer && mNextAlarmContainer.isVisibleToUser()) { 453 if (mNextAlarm.getShowIntent() != null) { 454 mActivityStarter.postStartActivityDismissingKeyguard( 455 mNextAlarm.getShowIntent()); 456 } else { 457 Log.d(TAG, "No PendingIntent for next alarm. Using default intent"); 458 mActivityStarter.postStartActivityDismissingKeyguard(new Intent( 459 AlarmClock.ACTION_SHOW_ALARMS), 0); 460 } 461 } else if (v == mRingerContainer && mRingerContainer.isVisibleToUser()) { 462 mActivityStarter.postStartActivityDismissingKeyguard(new Intent( 463 Settings.ACTION_SOUND_SETTINGS), 0); 464 } 465 } 466 467 @Override onNextAlarmChanged(AlarmManager.AlarmClockInfo nextAlarm)468 public void onNextAlarmChanged(AlarmManager.AlarmClockInfo nextAlarm) { 469 mNextAlarm = nextAlarm; 470 updateStatusText(); 471 } 472 473 @Override onZenChanged(int zen)474 public void onZenChanged(int zen) { 475 updateStatusText(); 476 } 477 478 @Override onConfigChanged(ZenModeConfig config)479 public void onConfigChanged(ZenModeConfig config) { 480 updateStatusText(); 481 } 482 updateEverything()483 public void updateEverything() { 484 post(() -> setClickable(!mExpanded)); 485 } 486 setQSPanel(final QSPanel qsPanel)487 public void setQSPanel(final QSPanel qsPanel) { 488 mQsPanel = qsPanel; 489 setupHost(qsPanel.getHost()); 490 } 491 setupHost(final QSTileHost host)492 public void setupHost(final QSTileHost host) { 493 mHost = host; 494 //host.setHeaderView(mExpandIndicator); 495 mHeaderQsPanel.setQSPanelAndHeader(mQsPanel, this); 496 mHeaderQsPanel.setHost(host, null /* No customization in header */); 497 498 499 Rect tintArea = new Rect(0, 0, 0, 0); 500 int colorForeground = Utils.getColorAttrDefaultColor(getContext(), 501 android.R.attr.colorForeground); 502 float intensity = getColorIntensity(colorForeground); 503 int fillColor = mDualToneHandler.getSingleColor(intensity); 504 mBatteryRemainingIcon.onDarkChanged(tintArea, intensity, fillColor); 505 } 506 setCallback(Callback qsPanelCallback)507 public void setCallback(Callback qsPanelCallback) { 508 mHeaderQsPanel.setCallback(qsPanelCallback); 509 } 510 formatNextAlarm(AlarmManager.AlarmClockInfo info)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 getColorIntensity(@olorInt int color)522 public static float getColorIntensity(@ColorInt int color) { 523 return color == Color.WHITE ? 0 : 1; 524 } 525 setMargins(int sideMargins)526 public void setMargins(int sideMargins) { 527 for (int i = 0; i < getChildCount(); i++) { 528 View v = getChildAt(i); 529 // Prevents these views from getting set a margin. 530 // The Icon views all have the same padding set in XML to be aligned. 531 if (v == mSystemIconsView || v == mQuickQsStatusIcons || v == mHeaderQsPanel 532 || v == mHeaderTextContainerView) { 533 continue; 534 } 535 RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) v.getLayoutParams(); 536 lp.leftMargin = sideMargins; 537 lp.rightMargin = sideMargins; 538 } 539 } 540 } 541