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