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