1 /*
2  * Copyright (C) 2012 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.keyguard;
18 
19 import android.app.ActivityManager;
20 import android.app.IActivityManager;
21 import android.content.Context;
22 import android.content.res.Resources;
23 import android.graphics.Color;
24 import android.os.Handler;
25 import android.os.Looper;
26 import android.os.RemoteException;
27 import android.os.UserHandle;
28 import android.text.TextUtils;
29 import android.text.format.DateFormat;
30 import android.util.AttributeSet;
31 import android.util.Log;
32 import android.util.Slog;
33 import android.util.TypedValue;
34 import android.view.View;
35 import android.widget.GridLayout;
36 import android.widget.LinearLayout;
37 import android.widget.TextView;
38 
39 import androidx.core.graphics.ColorUtils;
40 
41 import com.android.internal.widget.LockPatternUtils;
42 import com.android.systemui.Dependency;
43 import com.android.systemui.statusbar.policy.ConfigurationController;
44 
45 import java.io.FileDescriptor;
46 import java.io.PrintWriter;
47 import java.util.Locale;
48 import java.util.TimeZone;
49 
50 public class KeyguardStatusView extends GridLayout implements
51         ConfigurationController.ConfigurationListener {
52     private static final boolean DEBUG = KeyguardConstants.DEBUG;
53     private static final String TAG = "KeyguardStatusView";
54     private static final int MARQUEE_DELAY_MS = 2000;
55 
56     private final LockPatternUtils mLockPatternUtils;
57     private final IActivityManager mIActivityManager;
58 
59     private LinearLayout mStatusViewContainer;
60     private TextView mLogoutView;
61     private KeyguardClockSwitch mClockView;
62     private TextView mOwnerInfo;
63     private KeyguardSliceView mKeyguardSlice;
64     private Runnable mPendingMarqueeStart;
65     private Handler mHandler;
66 
67     private boolean mPulsing;
68     private float mDarkAmount = 0;
69     private int mTextColor;
70 
71     /**
72      * Bottom margin that defines the margin between bottom of smart space and top of notification
73      * icons on AOD.
74      */
75     private int mBottomMargin;
76     private int mBottomMarginWithHeader;
77     private boolean mShowingHeader;
78 
79     private KeyguardUpdateMonitorCallback mInfoCallback = new KeyguardUpdateMonitorCallback() {
80 
81         @Override
82         public void onTimeChanged() {
83             refreshTime();
84         }
85 
86         @Override
87         public void onTimeZoneChanged(TimeZone timeZone) {
88             updateTimeZone(timeZone);
89         }
90 
91         @Override
92         public void onKeyguardVisibilityChanged(boolean showing) {
93             if (showing) {
94                 if (DEBUG) Slog.v(TAG, "refresh statusview showing:" + showing);
95                 refreshTime();
96                 updateOwnerInfo();
97                 updateLogoutView();
98             }
99         }
100 
101         @Override
102         public void onStartedWakingUp() {
103             setEnableMarquee(true);
104         }
105 
106         @Override
107         public void onFinishedGoingToSleep(int why) {
108             setEnableMarquee(false);
109         }
110 
111         @Override
112         public void onUserSwitchComplete(int userId) {
113             refreshFormat();
114             updateOwnerInfo();
115             updateLogoutView();
116         }
117 
118         @Override
119         public void onLogoutEnabledChanged() {
120             updateLogoutView();
121         }
122     };
123 
KeyguardStatusView(Context context)124     public KeyguardStatusView(Context context) {
125         this(context, null, 0);
126     }
127 
KeyguardStatusView(Context context, AttributeSet attrs)128     public KeyguardStatusView(Context context, AttributeSet attrs) {
129         this(context, attrs, 0);
130     }
131 
KeyguardStatusView(Context context, AttributeSet attrs, int defStyle)132     public KeyguardStatusView(Context context, AttributeSet attrs, int defStyle) {
133         super(context, attrs, defStyle);
134         mIActivityManager = ActivityManager.getService();
135         mLockPatternUtils = new LockPatternUtils(getContext());
136         mHandler = new Handler(Looper.myLooper());
137         onDensityOrFontScaleChanged();
138     }
139 
140     /**
141      * If we're presenting a custom clock of just the default one.
142      */
hasCustomClock()143     public boolean hasCustomClock() {
144         return mClockView.hasCustomClock();
145     }
146 
setEnableMarquee(boolean enabled)147     private void setEnableMarquee(boolean enabled) {
148         if (DEBUG) Log.v(TAG, "Schedule setEnableMarquee: " + (enabled ? "Enable" : "Disable"));
149         if (enabled) {
150             if (mPendingMarqueeStart == null) {
151                 mPendingMarqueeStart = () -> {
152                     setEnableMarqueeImpl(true);
153                     mPendingMarqueeStart = null;
154                 };
155                 mHandler.postDelayed(mPendingMarqueeStart, MARQUEE_DELAY_MS);
156             }
157         } else {
158             if (mPendingMarqueeStart != null) {
159                 mHandler.removeCallbacks(mPendingMarqueeStart);
160                 mPendingMarqueeStart = null;
161             }
162             setEnableMarqueeImpl(false);
163         }
164     }
165 
setEnableMarqueeImpl(boolean enabled)166     private void setEnableMarqueeImpl(boolean enabled) {
167         if (DEBUG) Log.v(TAG, (enabled ? "Enable" : "Disable") + " transport text marquee");
168         if (mOwnerInfo != null) mOwnerInfo.setSelected(enabled);
169     }
170 
171     @Override
onFinishInflate()172     protected void onFinishInflate() {
173         super.onFinishInflate();
174         mStatusViewContainer = findViewById(R.id.status_view_container);
175         mLogoutView = findViewById(R.id.logout);
176         if (mLogoutView != null) {
177             mLogoutView.setOnClickListener(this::onLogoutClicked);
178         }
179 
180         mClockView = findViewById(R.id.keyguard_clock_container);
181         mClockView.setShowCurrentUserTime(true);
182         if (KeyguardClockAccessibilityDelegate.isNeeded(mContext)) {
183             mClockView.setAccessibilityDelegate(new KeyguardClockAccessibilityDelegate(mContext));
184         }
185         mOwnerInfo = findViewById(R.id.owner_info);
186         mKeyguardSlice = findViewById(R.id.keyguard_status_area);
187         mTextColor = mClockView.getCurrentTextColor();
188 
189         mKeyguardSlice.setContentChangeListener(this::onSliceContentChanged);
190         onSliceContentChanged();
191 
192         boolean shouldMarquee = KeyguardUpdateMonitor.getInstance(mContext).isDeviceInteractive();
193         setEnableMarquee(shouldMarquee);
194         refreshFormat();
195         updateOwnerInfo();
196         updateLogoutView();
197         updateDark();
198     }
199 
200     /**
201      * Moves clock, adjusting margins when slice content changes.
202      */
onSliceContentChanged()203     private void onSliceContentChanged() {
204         final boolean hasHeader = mKeyguardSlice.hasHeader();
205         mClockView.setKeyguardShowingHeader(hasHeader);
206         if (mShowingHeader == hasHeader) {
207             return;
208         }
209         mShowingHeader = hasHeader;
210         // Update bottom margin since header has appeared/disappeared.
211         if (mStatusViewContainer != null) {
212             MarginLayoutParams params = (MarginLayoutParams) mStatusViewContainer.getLayoutParams();
213             params.setMargins(params.leftMargin, params.topMargin, params.rightMargin,
214                     hasHeader ? mBottomMarginWithHeader : mBottomMargin);
215             mStatusViewContainer.setLayoutParams(params);
216         }
217     }
218 
219     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)220     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
221         super.onLayout(changed, left, top, right, bottom);
222         layoutOwnerInfo();
223     }
224 
225     @Override
onDensityOrFontScaleChanged()226     public void onDensityOrFontScaleChanged() {
227         if (mClockView != null) {
228             mClockView.setTextSize(TypedValue.COMPLEX_UNIT_PX,
229                     getResources().getDimensionPixelSize(R.dimen.widget_big_font_size));
230         }
231         if (mOwnerInfo != null) {
232             mOwnerInfo.setTextSize(TypedValue.COMPLEX_UNIT_PX,
233                     getResources().getDimensionPixelSize(R.dimen.widget_label_font_size));
234         }
235         loadBottomMargin();
236     }
237 
dozeTimeTick()238     public void dozeTimeTick() {
239         refreshTime();
240         mKeyguardSlice.refresh();
241     }
242 
refreshTime()243     private void refreshTime() {
244         mClockView.refresh();
245     }
246 
updateTimeZone(TimeZone timeZone)247     private void updateTimeZone(TimeZone timeZone) {
248         mClockView.onTimeZoneChanged(timeZone);
249     }
250 
refreshFormat()251     private void refreshFormat() {
252         Patterns.update(mContext);
253         mClockView.setFormat12Hour(Patterns.clockView12);
254         mClockView.setFormat24Hour(Patterns.clockView24);
255     }
256 
getLogoutButtonHeight()257     public int getLogoutButtonHeight() {
258         if (mLogoutView == null) {
259             return 0;
260         }
261         return mLogoutView.getVisibility() == VISIBLE ? mLogoutView.getHeight() : 0;
262     }
263 
getClockTextSize()264     public float getClockTextSize() {
265         return mClockView.getTextSize();
266     }
267 
268     /**
269      * Returns the preferred Y position of the clock.
270      *
271      * @param totalHeight The height available to position the clock.
272      * @return Y position of clock.
273      */
getClockPreferredY(int totalHeight)274     public int getClockPreferredY(int totalHeight) {
275         return mClockView.getPreferredY(totalHeight);
276     }
277 
updateLogoutView()278     private void updateLogoutView() {
279         if (mLogoutView == null) {
280             return;
281         }
282         mLogoutView.setVisibility(shouldShowLogout() ? VISIBLE : GONE);
283         // Logout button will stay in language of user 0 if we don't set that manually.
284         mLogoutView.setText(mContext.getResources().getString(
285                 com.android.internal.R.string.global_action_logout));
286     }
287 
updateOwnerInfo()288     private void updateOwnerInfo() {
289         if (mOwnerInfo == null) return;
290         String info = mLockPatternUtils.getDeviceOwnerInfo();
291         if (info == null) {
292             // Use the current user owner information if enabled.
293             final boolean ownerInfoEnabled = mLockPatternUtils.isOwnerInfoEnabled(
294                     KeyguardUpdateMonitor.getCurrentUser());
295             if (ownerInfoEnabled) {
296                 info = mLockPatternUtils.getOwnerInfo(KeyguardUpdateMonitor.getCurrentUser());
297             }
298         }
299         mOwnerInfo.setText(info);
300         updateDark();
301     }
302 
303     @Override
onAttachedToWindow()304     protected void onAttachedToWindow() {
305         super.onAttachedToWindow();
306         KeyguardUpdateMonitor.getInstance(mContext).registerCallback(mInfoCallback);
307         Dependency.get(ConfigurationController.class).addCallback(this);
308     }
309 
310     @Override
onDetachedFromWindow()311     protected void onDetachedFromWindow() {
312         super.onDetachedFromWindow();
313         KeyguardUpdateMonitor.getInstance(mContext).removeCallback(mInfoCallback);
314         Dependency.get(ConfigurationController.class).removeCallback(this);
315     }
316 
317     @Override
onLocaleListChanged()318     public void onLocaleListChanged() {
319         refreshFormat();
320     }
321 
dump(FileDescriptor fd, PrintWriter pw, String[] args)322     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
323         pw.println("KeyguardStatusView:");
324         pw.println("  mOwnerInfo: " + (mOwnerInfo == null
325                 ? "null" : mOwnerInfo.getVisibility() == VISIBLE));
326         pw.println("  mPulsing: " + mPulsing);
327         pw.println("  mDarkAmount: " + mDarkAmount);
328         pw.println("  mTextColor: " + Integer.toHexString(mTextColor));
329         if (mLogoutView != null) {
330             pw.println("  logout visible: " + (mLogoutView.getVisibility() == VISIBLE));
331         }
332         if (mClockView != null) {
333             mClockView.dump(fd, pw, args);
334         }
335         if (mKeyguardSlice != null) {
336             mKeyguardSlice.dump(fd, pw, args);
337         }
338     }
339 
loadBottomMargin()340     private void loadBottomMargin() {
341         mBottomMargin = getResources().getDimensionPixelSize(R.dimen.widget_vertical_padding);
342         mBottomMarginWithHeader = getResources().getDimensionPixelSize(
343                 R.dimen.widget_vertical_padding_with_header);
344     }
345 
346     // DateFormat.getBestDateTimePattern is extremely expensive, and refresh is called often.
347     // This is an optimization to ensure we only recompute the patterns when the inputs change.
348     private static final class Patterns {
349         static String clockView12;
350         static String clockView24;
351         static String cacheKey;
352 
update(Context context)353         static void update(Context context) {
354             final Locale locale = Locale.getDefault();
355             final Resources res = context.getResources();
356             final String clockView12Skel = res.getString(R.string.clock_12hr_format);
357             final String clockView24Skel = res.getString(R.string.clock_24hr_format);
358             final String key = locale.toString() + clockView12Skel + clockView24Skel;
359             if (key.equals(cacheKey)) return;
360 
361             clockView12 = DateFormat.getBestDateTimePattern(locale, clockView12Skel);
362             // CLDR insists on adding an AM/PM indicator even though it wasn't in the skeleton
363             // format.  The following code removes the AM/PM indicator if we didn't want it.
364             if (!clockView12Skel.contains("a")) {
365                 clockView12 = clockView12.replaceAll("a", "").trim();
366             }
367 
368             clockView24 = DateFormat.getBestDateTimePattern(locale, clockView24Skel);
369 
370             // Use fancy colon.
371             clockView24 = clockView24.replace(':', '\uee01');
372             clockView12 = clockView12.replace(':', '\uee01');
373 
374             cacheKey = key;
375         }
376     }
377 
setDarkAmount(float darkAmount)378     public void setDarkAmount(float darkAmount) {
379         if (mDarkAmount == darkAmount) {
380             return;
381         }
382         mDarkAmount = darkAmount;
383         mClockView.setDarkAmount(darkAmount);
384         updateDark();
385     }
386 
updateDark()387     private void updateDark() {
388         boolean dark = mDarkAmount == 1;
389         if (mLogoutView != null) {
390             mLogoutView.setAlpha(dark ? 0 : 1);
391         }
392 
393         if (mOwnerInfo != null) {
394             boolean hasText = !TextUtils.isEmpty(mOwnerInfo.getText());
395             mOwnerInfo.setVisibility(hasText ? VISIBLE : GONE);
396             layoutOwnerInfo();
397         }
398 
399         final int blendedTextColor = ColorUtils.blendARGB(mTextColor, Color.WHITE, mDarkAmount);
400         mKeyguardSlice.setDarkAmount(mDarkAmount);
401         mClockView.setTextColor(blendedTextColor);
402     }
403 
layoutOwnerInfo()404     private void layoutOwnerInfo() {
405         if (mOwnerInfo != null && mOwnerInfo.getVisibility() != GONE) {
406             // Animate owner info during wake-up transition
407             mOwnerInfo.setAlpha(1f - mDarkAmount);
408 
409             float ratio = mDarkAmount;
410             // Calculate how much of it we should crop in order to have a smooth transition
411             int collapsed = mOwnerInfo.getTop() - mOwnerInfo.getPaddingTop();
412             int expanded = mOwnerInfo.getBottom() + mOwnerInfo.getPaddingBottom();
413             int toRemove = (int) ((expanded - collapsed) * ratio);
414             setBottom(getMeasuredHeight() - toRemove);
415         }
416     }
417 
setPulsing(boolean pulsing)418     public void setPulsing(boolean pulsing) {
419         if (mPulsing == pulsing) {
420             return;
421         }
422         mPulsing = pulsing;
423     }
424 
shouldShowLogout()425     private boolean shouldShowLogout() {
426         return KeyguardUpdateMonitor.getInstance(mContext).isLogoutEnabled()
427                 && KeyguardUpdateMonitor.getCurrentUser() != UserHandle.USER_SYSTEM;
428     }
429 
onLogoutClicked(View view)430     private void onLogoutClicked(View view) {
431         int currentUserId = KeyguardUpdateMonitor.getCurrentUser();
432         try {
433             mIActivityManager.switchUser(UserHandle.USER_SYSTEM);
434             mIActivityManager.stopUser(currentUserId, true /*force*/, null);
435         } catch (RemoteException re) {
436             Log.e(TAG, "Failed to logout user", re);
437         }
438     }
439 }
440