1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 package com.android.systemui.statusbar.phone;
18 
19 import static com.android.systemui.Flags.centralizedStatusBarHeightFix;
20 import static com.android.systemui.ScreenDecorations.DisplayCutoutView.boundsFromDirection;
21 import static com.android.systemui.util.Utils.getStatusBarHeaderHeightKeyguard;
22 
23 import android.annotation.ColorInt;
24 import android.content.Context;
25 import android.content.res.Configuration;
26 import android.content.res.Resources;
27 import android.graphics.Color;
28 import android.graphics.Insets;
29 import android.graphics.Rect;
30 import android.graphics.drawable.Drawable;
31 import android.os.Trace;
32 import android.util.AttributeSet;
33 import android.util.TypedValue;
34 import android.view.DisplayCutout;
35 import android.view.Gravity;
36 import android.view.View;
37 import android.view.ViewGroup;
38 import android.view.WindowInsets;
39 import android.widget.ImageView;
40 import android.widget.LinearLayout;
41 import android.widget.RelativeLayout;
42 import android.widget.TextView;
43 
44 import androidx.annotation.Nullable;
45 import androidx.annotation.VisibleForTesting;
46 
47 import com.android.settingslib.Utils;
48 import com.android.systemui.battery.BatteryMeterView;
49 import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
50 import com.android.systemui.res.R;
51 import com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher.DarkChange;
52 import com.android.systemui.statusbar.phone.ui.TintedIconManager;
53 import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer;
54 import com.android.systemui.user.ui.binder.StatusBarUserChipViewBinder;
55 import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel;
56 
57 import kotlinx.coroutines.flow.FlowKt;
58 import kotlinx.coroutines.flow.MutableStateFlow;
59 import kotlinx.coroutines.flow.StateFlow;
60 import kotlinx.coroutines.flow.StateFlowKt;
61 
62 import java.io.PrintWriter;
63 import java.util.ArrayList;
64 
65 /**
66  * The header group on Keyguard.
67  */
68 public class KeyguardStatusBarView extends RelativeLayout {
69 
70     private static final int LAYOUT_NONE = 0;
71     private static final int LAYOUT_CUTOUT = 1;
72     private static final int LAYOUT_NO_CUTOUT = 2;
73 
74     private final ArrayList<Rect> mEmptyTintRect = new ArrayList<>();
75 
76     private boolean mShowPercentAvailable;
77     private boolean mBatteryCharging;
78 
79     private TextView mCarrierLabel;
80     private ImageView mMultiUserAvatar;
81     private BatteryMeterView mBatteryView;
82     private StatusIconContainer mStatusIconContainer;
83     private StatusBarUserSwitcherContainer mUserSwitcherContainer;
84 
85     private boolean mKeyguardUserSwitcherEnabled;
86     private boolean mKeyguardUserAvatarEnabled;
87 
88     private boolean mIsPrivacyDotEnabled;
89     private int mSystemIconsSwitcherHiddenExpandedMargin;
90     private int mStatusBarPaddingEnd;
91     private int mMinDotWidth;
92     private View mSystemIconsContainer;
93     private View mSystemIcons;
94     private final MutableStateFlow<DarkChange> mDarkChange = StateFlowKt.MutableStateFlow(
95             DarkChange.EMPTY);
96 
97     private View mCutoutSpace;
98     private ViewGroup mStatusIconArea;
99     private int mLayoutState = LAYOUT_NONE;
100 
101     /**
102      * Draw this many pixels into the left/right side of the cutout to optimally use the space
103      */
104     private int mCutoutSideNudge = 0;
105 
106     private DisplayCutout mDisplayCutout;
107     private int mRoundedCornerPadding = 0;
108     // right and left padding applied to this view to account for cutouts and rounded corners
109     private Insets mPadding = Insets.of(0, 0, 0, 0);
110 
111     /**
112      * The clipping on the top
113      */
114     private int mTopClipping;
115     private final Rect mClipRect = new Rect(0, 0, 0, 0);
116     private boolean mIsUserSwitcherEnabled;
117 
KeyguardStatusBarView(Context context, AttributeSet attrs)118     public KeyguardStatusBarView(Context context, AttributeSet attrs) {
119         super(context, attrs);
120     }
121 
122     @Override
onFinishInflate()123     protected void onFinishInflate() {
124         super.onFinishInflate();
125         mSystemIconsContainer = findViewById(R.id.system_icons_container);
126         mSystemIcons = findViewById(R.id.system_icons);
127         mMultiUserAvatar = findViewById(R.id.multi_user_avatar);
128         mCarrierLabel = findViewById(R.id.keyguard_carrier_text);
129         mBatteryView = mSystemIconsContainer.findViewById(R.id.battery);
130         mCutoutSpace = findViewById(R.id.cutout_space_view);
131         mStatusIconArea = findViewById(R.id.status_icon_area);
132         mStatusIconContainer = findViewById(R.id.statusIcons);
133         mUserSwitcherContainer = findViewById(R.id.user_switcher_container);
134         mIsPrivacyDotEnabled = mContext.getResources().getBoolean(R.bool.config_enablePrivacyDot);
135         loadDimens();
136         if (!centralizedStatusBarHeightFix()) {
137             setGravity(Gravity.CENTER_VERTICAL);
138         }
139     }
140 
141     /**
142      * Should only be called from {@link KeyguardStatusBarViewController}
143      * @param viewModel view model for the status bar user chip
144      */
init(StatusBarUserChipViewModel viewModel)145     void init(StatusBarUserChipViewModel viewModel) {
146         StatusBarUserChipViewBinder.bind(mUserSwitcherContainer, viewModel);
147     }
148 
149     @Override
onConfigurationChanged(Configuration newConfig)150     protected void onConfigurationChanged(Configuration newConfig) {
151         super.onConfigurationChanged(newConfig);
152         loadDimens();
153 
154         MarginLayoutParams lp = (MarginLayoutParams) mMultiUserAvatar.getLayoutParams();
155         lp.width = lp.height = getResources().getDimensionPixelSize(
156                 R.dimen.multi_user_avatar_keyguard_size);
157         mMultiUserAvatar.setLayoutParams(lp);
158 
159         // System icons
160         updateSystemIconsLayoutParams();
161 
162         // mStatusIconArea
163         mStatusIconArea.setPaddingRelative(
164                 mStatusIconArea.getPaddingStart(),
165                 getResources().getDimensionPixelSize(R.dimen.status_bar_padding_top),
166                 mStatusIconArea.getPaddingEnd(),
167                 mStatusIconArea.getPaddingBottom()
168         );
169 
170         // mStatusIconContainer
171         mStatusIconContainer.setPaddingRelative(
172                 mStatusIconContainer.getPaddingStart(),
173                 mStatusIconContainer.getPaddingTop(),
174                 getResources().getDimensionPixelSize(R.dimen.signal_cluster_battery_padding),
175                 mStatusIconContainer.getPaddingBottom()
176         );
177 
178         mSystemIcons.setPaddingRelative(
179                 getResources().getDimensionPixelSize(R.dimen.status_bar_icons_padding_start),
180                 getResources().getDimensionPixelSize(R.dimen.status_bar_icons_padding_top),
181                 getResources().getDimensionPixelSize(R.dimen.status_bar_icons_padding_end),
182                 getResources().getDimensionPixelSize(R.dimen.status_bar_icons_padding_bottom)
183         );
184 
185         // Respect font size setting.
186         mCarrierLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX,
187                 getResources().getDimensionPixelSize(
188                         com.android.internal.R.dimen.text_size_small_material));
189         lp = (MarginLayoutParams) mCarrierLabel.getLayoutParams();
190 
191         int marginStart = calculateMargin(
192                 getResources().getDimensionPixelSize(R.dimen.keyguard_carrier_text_margin),
193                 mPadding.left);
194         lp.setMarginStart(marginStart);
195 
196         mCarrierLabel.setLayoutParams(lp);
197         updateKeyguardStatusBarHeight();
198     }
199 
setUserSwitcherEnabled(boolean enabled)200     public void setUserSwitcherEnabled(boolean enabled) {
201         mIsUserSwitcherEnabled = enabled;
202     }
203 
updateKeyguardStatusBarHeight()204     private void updateKeyguardStatusBarHeight() {
205         ViewGroup.LayoutParams lp = (ViewGroup.LayoutParams) getLayoutParams();
206         lp.height = getStatusBarHeaderHeightKeyguard(mContext);
207         setLayoutParams(lp);
208     }
209 
loadDimens()210     void loadDimens() {
211         Resources res = getResources();
212         mSystemIconsSwitcherHiddenExpandedMargin = res.getDimensionPixelSize(
213                 R.dimen.system_icons_switcher_hidden_expanded_margin);
214         mStatusBarPaddingEnd = res.getDimensionPixelSize(
215                 R.dimen.status_bar_padding_end);
216         mMinDotWidth = res.getDimensionPixelSize(
217                 R.dimen.ongoing_appops_dot_min_padding);
218         mCutoutSideNudge = getResources().getDimensionPixelSize(
219                 R.dimen.display_cutout_margin_consumption);
220         mShowPercentAvailable = getContext().getResources().getBoolean(
221                 com.android.internal.R.bool.config_battery_percentage_setting_available);
222         mRoundedCornerPadding = res.getDimensionPixelSize(
223                 R.dimen.rounded_corner_content_padding);
224     }
225 
updateVisibilities()226     private void updateVisibilities() {
227         // Multi user avatar is disabled in favor of the user switcher chip
228         if (!mKeyguardUserAvatarEnabled) {
229             if (mMultiUserAvatar.getParent() == mStatusIconArea) {
230                 mStatusIconArea.removeView(mMultiUserAvatar);
231             } else if (mMultiUserAvatar.getParent() != null) {
232                 getOverlay().remove(mMultiUserAvatar);
233             }
234 
235             return;
236         }
237 
238         if (mMultiUserAvatar.getParent() != mStatusIconArea
239                 && !mKeyguardUserSwitcherEnabled) {
240             if (mMultiUserAvatar.getParent() != null) {
241                 getOverlay().remove(mMultiUserAvatar);
242             }
243             mStatusIconArea.addView(mMultiUserAvatar, 0);
244         } else if (mMultiUserAvatar.getParent() == mStatusIconArea
245                 && mKeyguardUserSwitcherEnabled) {
246             mStatusIconArea.removeView(mMultiUserAvatar);
247         }
248         if (!mKeyguardUserSwitcherEnabled) {
249             // If we have no keyguard switcher, the screen width is under 600dp. In this case,
250             // we only show the multi-user switch if it's enabled through UserManager as well as
251             // by the user.
252             if (mIsUserSwitcherEnabled) {
253                 mMultiUserAvatar.setVisibility(View.VISIBLE);
254             } else {
255                 mMultiUserAvatar.setVisibility(View.GONE);
256             }
257         }
258         mBatteryView.setForceShowPercent(mBatteryCharging && mShowPercentAvailable);
259     }
260 
updateSystemIconsLayoutParams()261     private void updateSystemIconsLayoutParams() {
262         LinearLayout.LayoutParams lp =
263                 (LinearLayout.LayoutParams) mSystemIconsContainer.getLayoutParams();
264 
265         // Use status_bar_padding_end to replace original
266         // system_icons_super_container_avatarless_margin_end to prevent different end alignment
267         // between PhoneStatusBarView and KeyguardStatusBarView
268         int baseMarginEnd = mStatusBarPaddingEnd;
269         int marginEnd =
270                 mKeyguardUserSwitcherEnabled ? mSystemIconsSwitcherHiddenExpandedMargin
271                         : baseMarginEnd;
272 
273         // Align PhoneStatusBar right margin/padding, only use
274         // 1. status bar layout: mPadding(consider round_corner + privacy dot)
275         // 2. icon container: R.dimen.status_bar_padding_end
276 
277         if (marginEnd != lp.getMarginEnd()) {
278             lp.setMarginEnd(marginEnd);
279             mSystemIconsContainer.setLayoutParams(lp);
280         }
281     }
282 
283     /** Should only be called from {@link KeyguardStatusBarViewController}. */
updateWindowInsets( WindowInsets insets, StatusBarContentInsetsProvider insetsProvider)284     WindowInsets updateWindowInsets(
285             WindowInsets insets,
286             StatusBarContentInsetsProvider insetsProvider) {
287         mLayoutState = LAYOUT_NONE;
288         if (updateLayoutConsideringCutout(insetsProvider)) {
289             requestLayout();
290         }
291         return super.onApplyWindowInsets(insets);
292     }
293 
updateLayoutConsideringCutout(StatusBarContentInsetsProvider insetsProvider)294     private boolean updateLayoutConsideringCutout(StatusBarContentInsetsProvider insetsProvider) {
295         return setDisplayCutout(
296                 getRootWindowInsets().getDisplayCutout(),
297                 insetsProvider);
298     }
299 
300     /** Sets the {@link DisplayCutout}, updating the view to render around the cutout. */
setDisplayCutout( @ullable DisplayCutout displayCutout, StatusBarContentInsetsProvider insetsProvider)301     public boolean setDisplayCutout(
302             @Nullable DisplayCutout displayCutout,
303             StatusBarContentInsetsProvider insetsProvider) {
304         mDisplayCutout = displayCutout;
305         updateKeyguardStatusBarHeight();
306         updatePadding(insetsProvider);
307         if (mDisplayCutout == null || insetsProvider.currentRotationHasCornerCutout()) {
308             return updateLayoutParamsNoCutout();
309         } else {
310             return updateLayoutParamsForCutout();
311         }
312     }
313 
updatePadding(StatusBarContentInsetsProvider insetsProvider)314     private void updatePadding(StatusBarContentInsetsProvider insetsProvider) {
315         final int waterfallTop =
316                 mDisplayCutout == null ? 0 : mDisplayCutout.getWaterfallInsets().top;
317         mPadding = insetsProvider.getStatusBarContentInsetsForCurrentRotation();
318 
319         // consider privacy dot space
320         final int minLeft = (isLayoutRtl() && mIsPrivacyDotEnabled)
321                 ? Math.max(mMinDotWidth, mPadding.left) : mPadding.left;
322         final int minRight = (!isLayoutRtl() && mIsPrivacyDotEnabled)
323                 ? Math.max(mMinDotWidth, mPadding.right) : mPadding.right;
324 
325         int top = centralizedStatusBarHeightFix() ? waterfallTop + mPadding.top : waterfallTop;
326         setPadding(minLeft, top, minRight, 0);
327     }
328 
updateLayoutParamsNoCutout()329     private boolean updateLayoutParamsNoCutout() {
330         if (mLayoutState == LAYOUT_NO_CUTOUT) {
331             return false;
332         }
333         mLayoutState = LAYOUT_NO_CUTOUT;
334 
335         if (mCutoutSpace != null) {
336             mCutoutSpace.setVisibility(View.GONE);
337         }
338 
339         RelativeLayout.LayoutParams lp = (LayoutParams) mCarrierLabel.getLayoutParams();
340         lp.addRule(RelativeLayout.START_OF, R.id.status_icon_area);
341 
342         lp = (LayoutParams) mStatusIconArea.getLayoutParams();
343         lp.removeRule(RelativeLayout.RIGHT_OF);
344         lp.width = LayoutParams.WRAP_CONTENT;
345         lp.setMarginStart(getResources().getDimensionPixelSize(
346                 R.dimen.system_icons_super_container_margin_start));
347         return true;
348     }
349 
updateLayoutParamsForCutout()350     private boolean updateLayoutParamsForCutout() {
351         if (mLayoutState == LAYOUT_CUTOUT) {
352             return false;
353         }
354         mLayoutState = LAYOUT_CUTOUT;
355 
356         if (mCutoutSpace == null) {
357             updateLayoutParamsNoCutout();
358         }
359 
360         Rect bounds = new Rect();
361         boundsFromDirection(mDisplayCutout, Gravity.TOP, bounds);
362 
363         mCutoutSpace.setVisibility(View.VISIBLE);
364         RelativeLayout.LayoutParams lp = (LayoutParams) mCutoutSpace.getLayoutParams();
365         bounds.left = bounds.left + mCutoutSideNudge;
366         bounds.right = bounds.right - mCutoutSideNudge;
367         lp.width = bounds.width();
368         lp.height = bounds.height();
369         lp.addRule(RelativeLayout.CENTER_IN_PARENT);
370 
371         lp = (LayoutParams) mCarrierLabel.getLayoutParams();
372         lp.addRule(RelativeLayout.START_OF, R.id.cutout_space_view);
373 
374         lp = (LayoutParams) mStatusIconArea.getLayoutParams();
375         lp.addRule(RelativeLayout.RIGHT_OF, R.id.cutout_space_view);
376         lp.width = LayoutParams.MATCH_PARENT;
377         lp.setMarginStart(0);
378         return true;
379     }
380 
381     /** Should only be called from {@link KeyguardStatusBarViewController}. */
onUserInfoChanged(Drawable picture)382     void onUserInfoChanged(Drawable picture) {
383         mMultiUserAvatar.setImageDrawable(picture);
384     }
385 
386     /**
387      * Should only be called from {@link KeyguardStatusBarViewController} or
388      * {@link com.android.systemui.statusbar.ui.binder.KeyguardStatusBarViewBinder}.
389      */
onBatteryChargingChanged(boolean charging)390     public void onBatteryChargingChanged(boolean charging) {
391         if (mBatteryCharging != charging) {
392             mBatteryCharging = charging;
393             updateVisibilities();
394         }
395     }
396 
397     /**
398      * Should only be called from {@link KeyguardStatusBarViewController} or
399      * {@link com.android.systemui.statusbar.ui.binder.KeyguardStatusBarViewBinder}.
400      */
setKeyguardUserSwitcherEnabled(boolean enabled)401     public void setKeyguardUserSwitcherEnabled(boolean enabled) {
402         mKeyguardUserSwitcherEnabled = enabled;
403     }
404 
setKeyguardUserAvatarEnabled(boolean enabled)405     void setKeyguardUserAvatarEnabled(boolean enabled) {
406         mKeyguardUserAvatarEnabled = enabled;
407         updateVisibilities();
408     }
409 
410     @VisibleForTesting
isKeyguardUserAvatarEnabled()411     boolean isKeyguardUserAvatarEnabled() {
412         return mKeyguardUserAvatarEnabled;
413     }
414 
415     @Override
setVisibility(int visibility)416     public void setVisibility(int visibility) {
417         super.setVisibility(visibility);
418         if (visibility != View.VISIBLE) {
419             mSystemIconsContainer.animate().cancel();
420             mSystemIconsContainer.setTranslationX(0);
421             mMultiUserAvatar.animate().cancel();
422             mMultiUserAvatar.setAlpha(1f);
423         } else {
424             updateVisibilities();
425             updateSystemIconsLayoutParams();
426         }
427     }
428 
429     @Override
hasOverlappingRendering()430     public boolean hasOverlappingRendering() {
431         return false;
432     }
433 
434     /** Should only be called from {@link KeyguardStatusBarViewController}. */
onThemeChanged(TintedIconManager iconManager)435     void onThemeChanged(TintedIconManager iconManager) {
436         mBatteryView.setColorsFromContext(mContext);
437         updateIconsAndTextColors(iconManager);
438     }
439 
440     /** Should only be called from {@link KeyguardStatusBarViewController}. */
onOverlayChanged()441     void onOverlayChanged() {
442         int theme = Utils.getThemeAttr(mContext, com.android.internal.R.attr.textAppearanceSmall);
443         mCarrierLabel.setTextAppearance(theme);
444         mBatteryView.updatePercentView();
445 
446         TextView userSwitcherName = mUserSwitcherContainer.findViewById(R.id.current_user_name);
447         if (userSwitcherName != null) {
448             userSwitcherName.setTextAppearance(theme);
449         }
450     }
451 
updateIconsAndTextColors(TintedIconManager iconManager)452     private void updateIconsAndTextColors(TintedIconManager iconManager) {
453         @ColorInt int textColor = Utils.getColorAttrDefaultColor(mContext,
454                 R.attr.wallpaperTextColor);
455         float luminance = Color.luminance(textColor);
456         @ColorInt int iconColor = Utils.getColorStateListDefaultColor(mContext,
457                     luminance < 0.5
458                         ? com.android.settingslib.R.color.dark_mode_icon_color_single_tone
459                         : com.android.settingslib.R.color.light_mode_icon_color_single_tone);
460         @ColorInt int contrastColor = luminance < 0.5
461                 ? DarkIconDispatcherImpl.DEFAULT_ICON_TINT
462                 : DarkIconDispatcherImpl.DEFAULT_INVERSE_ICON_TINT;
463         float intensity = textColor == Color.WHITE ? 0 : 1;
464         mCarrierLabel.setTextColor(iconColor);
465 
466         TextView userSwitcherName = mUserSwitcherContainer.findViewById(R.id.current_user_name);
467         if (userSwitcherName != null) {
468             userSwitcherName.setTextColor(Utils.getColorStateListDefaultColor(
469                     mContext,
470                     com.android.settingslib.R.color.light_mode_icon_color_single_tone));
471         }
472 
473         if (iconManager != null) {
474             iconManager.setTint(iconColor, contrastColor);
475         }
476 
477         mDarkChange.setValue(new DarkChange(mEmptyTintRect, intensity, iconColor));
478         applyDarkness(R.id.battery, mEmptyTintRect, intensity, iconColor);
479         applyDarkness(R.id.clock, mEmptyTintRect, intensity, iconColor);
480     }
481 
482     private void applyDarkness(int id, ArrayList<Rect> tintAreas, float intensity, int color) {
483         View v = findViewById(id);
484         if (v instanceof DarkReceiver) {
485             ((DarkReceiver) v).onDarkChanged(tintAreas, intensity, color);
486         }
487     }
488 
489     /**
490      * Calculates the margin that isn't already accounted for in the view's padding.
491      */
492     private int calculateMargin(int margin, int padding) {
493         if (padding >= margin) {
494             return 0;
495         } else {
496             return margin - padding;
497         }
498     }
499 
500     /** Should only be called from {@link KeyguardStatusBarViewController}. */
501     void dump(PrintWriter pw, String[] args) {
502         pw.println("KeyguardStatusBarView:");
503         pw.println("  mBatteryCharging: " + mBatteryCharging);
504         pw.println("  mLayoutState: " + mLayoutState);
505         pw.println("  mKeyguardUserSwitcherEnabled: " + mKeyguardUserSwitcherEnabled);
506         if (mBatteryView != null) {
507             mBatteryView.dump(pw, args);
508         }
509     }
510 
511     @Override
512     protected void onLayout(boolean changed, int l, int t, int r, int b) {
513         super.onLayout(changed, l, t, r, b);
514         updateClipping();
515     }
516 
517     /**
518      * Set the clipping on the top of the view.
519      *
520      * Should only be called from {@link KeyguardStatusBarViewController}.
521      */
522     void setTopClipping(int topClipping) {
523         if (topClipping != mTopClipping) {
524             mTopClipping = topClipping;
525             updateClipping();
526         }
527     }
528 
529     private void updateClipping() {
530         mClipRect.set(0, mTopClipping, getWidth(), getHeight());
531         setClipBounds(mClipRect);
532     }
533 
534     @Override
535     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
536         Trace.beginSection("KeyguardStatusBarView#onMeasure");
537         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
538         Trace.endSection();
539     }
540 
541     public StateFlow<DarkChange> darkChangeFlow() {
542         return FlowKt.asStateFlow(mDarkChange);
543     }
544 }
545