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