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.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.app.ActivityManager;
22 import android.app.IActivityManager;
23 import android.content.Context;
24 import android.content.res.Resources;
25 import android.graphics.Color;
26 import android.graphics.Paint;
27 import android.os.Handler;
28 import android.os.Looper;
29 import android.os.RemoteException;
30 import android.os.UserHandle;
31 import android.support.v4.graphics.ColorUtils;
32 import android.text.TextUtils;
33 import android.text.format.DateFormat;
34 import android.util.ArraySet;
35 import android.util.AttributeSet;
36 import android.util.Log;
37 import android.util.Slog;
38 import android.util.TypedValue;
39 import android.view.View;
40 import android.widget.GridLayout;
41 import android.widget.RelativeLayout;
42 import android.widget.TextClock;
43 import android.widget.TextView;
44 
45 import com.android.internal.widget.LockPatternUtils;
46 import com.android.internal.widget.ViewClippingUtil;
47 import com.android.systemui.Dependency;
48 import com.android.systemui.Interpolators;
49 import com.android.systemui.statusbar.policy.ConfigurationController;
50 import com.android.systemui.util.wakelock.KeepAwakeAnimationListener;
51 
52 import com.google.android.collect.Sets;
53 
54 import java.util.Locale;
55 
56 public class KeyguardStatusView extends GridLayout implements
57         ConfigurationController.ConfigurationListener, View.OnLayoutChangeListener {
58     private static final boolean DEBUG = KeyguardConstants.DEBUG;
59     private static final String TAG = "KeyguardStatusView";
60     private static final int MARQUEE_DELAY_MS = 2000;
61 
62     private final LockPatternUtils mLockPatternUtils;
63     private final IActivityManager mIActivityManager;
64     private final float mSmallClockScale;
65 
66     private TextView mLogoutView;
67     private TextClock mClockView;
68     private View mClockSeparator;
69     private TextView mOwnerInfo;
70     private KeyguardSliceView mKeyguardSlice;
71     private Runnable mPendingMarqueeStart;
72     private Handler mHandler;
73 
74     private ArraySet<View> mVisibleInDoze;
75     private boolean mPulsing;
76     private float mDarkAmount = 0;
77     private int mTextColor;
78     private float mWidgetPadding;
79     private int mLastLayoutHeight;
80 
81     private KeyguardUpdateMonitorCallback mInfoCallback = new KeyguardUpdateMonitorCallback() {
82 
83         @Override
84         public void onTimeChanged() {
85             refreshTime();
86         }
87 
88         @Override
89         public void onKeyguardVisibilityChanged(boolean showing) {
90             if (showing) {
91                 if (DEBUG) Slog.v(TAG, "refresh statusview showing:" + showing);
92                 refreshTime();
93                 updateOwnerInfo();
94                 updateLogoutView();
95             }
96         }
97 
98         @Override
99         public void onStartedWakingUp() {
100             setEnableMarquee(true);
101         }
102 
103         @Override
104         public void onFinishedGoingToSleep(int why) {
105             setEnableMarquee(false);
106         }
107 
108         @Override
109         public void onUserSwitchComplete(int userId) {
110             refreshFormat();
111             updateOwnerInfo();
112             updateLogoutView();
113         }
114 
115         @Override
116         public void onLogoutEnabledChanged() {
117             updateLogoutView();
118         }
119     };
120 
KeyguardStatusView(Context context)121     public KeyguardStatusView(Context context) {
122         this(context, null, 0);
123     }
124 
KeyguardStatusView(Context context, AttributeSet attrs)125     public KeyguardStatusView(Context context, AttributeSet attrs) {
126         this(context, attrs, 0);
127     }
128 
KeyguardStatusView(Context context, AttributeSet attrs, int defStyle)129     public KeyguardStatusView(Context context, AttributeSet attrs, int defStyle) {
130         super(context, attrs, defStyle);
131         mIActivityManager = ActivityManager.getService();
132         mLockPatternUtils = new LockPatternUtils(getContext());
133         mHandler = new Handler(Looper.myLooper());
134         mSmallClockScale = getResources().getDimension(R.dimen.widget_small_font_size)
135                 / getResources().getDimension(R.dimen.widget_big_font_size);
136         onDensityOrFontScaleChanged();
137     }
138 
setEnableMarquee(boolean enabled)139     private void setEnableMarquee(boolean enabled) {
140         if (DEBUG) Log.v(TAG, "Schedule setEnableMarquee: " + (enabled ? "Enable" : "Disable"));
141         if (enabled) {
142             if (mPendingMarqueeStart == null) {
143                 mPendingMarqueeStart = () -> {
144                     setEnableMarqueeImpl(true);
145                     mPendingMarqueeStart = null;
146                 };
147                 mHandler.postDelayed(mPendingMarqueeStart, MARQUEE_DELAY_MS);
148             }
149         } else {
150             if (mPendingMarqueeStart != null) {
151                 mHandler.removeCallbacks(mPendingMarqueeStart);
152                 mPendingMarqueeStart = null;
153             }
154             setEnableMarqueeImpl(false);
155         }
156     }
157 
setEnableMarqueeImpl(boolean enabled)158     private void setEnableMarqueeImpl(boolean enabled) {
159         if (DEBUG) Log.v(TAG, (enabled ? "Enable" : "Disable") + " transport text marquee");
160         if (mOwnerInfo != null) mOwnerInfo.setSelected(enabled);
161     }
162 
163     @Override
onFinishInflate()164     protected void onFinishInflate() {
165         super.onFinishInflate();
166         mLogoutView = findViewById(R.id.logout);
167         if (mLogoutView != null) {
168             mLogoutView.setOnClickListener(this::onLogoutClicked);
169         }
170 
171         mClockView = findViewById(R.id.clock_view);
172         mClockView.setShowCurrentUserTime(true);
173         if (KeyguardClockAccessibilityDelegate.isNeeded(mContext)) {
174             mClockView.setAccessibilityDelegate(new KeyguardClockAccessibilityDelegate(mContext));
175         }
176         mOwnerInfo = findViewById(R.id.owner_info);
177         mKeyguardSlice = findViewById(R.id.keyguard_status_area);
178         mClockSeparator = findViewById(R.id.clock_separator);
179         mVisibleInDoze = Sets.newArraySet(mClockView, mKeyguardSlice);
180         mTextColor = mClockView.getCurrentTextColor();
181 
182         int clockStroke = getResources().getDimensionPixelSize(R.dimen.widget_small_font_stroke);
183         mClockView.getPaint().setStrokeWidth(clockStroke);
184         mClockView.addOnLayoutChangeListener(this);
185         mClockSeparator.addOnLayoutChangeListener(this);
186         mKeyguardSlice.setContentChangeListener(this::onSliceContentChanged);
187         onSliceContentChanged();
188 
189         boolean shouldMarquee = KeyguardUpdateMonitor.getInstance(mContext).isDeviceInteractive();
190         setEnableMarquee(shouldMarquee);
191         refreshFormat();
192         updateOwnerInfo();
193         updateLogoutView();
194         updateDark();
195 
196         // Disable elegant text height because our fancy colon makes the ymin value huge for no
197         // reason.
198         mClockView.setElegantTextHeight(false);
199     }
200 
onSliceContentChanged()201     private void onSliceContentChanged() {
202         boolean smallClock = mKeyguardSlice.hasHeader() || mPulsing;
203         float clockScale = smallClock ? mSmallClockScale : 1;
204 
205         RelativeLayout.LayoutParams layoutParams =
206                 (RelativeLayout.LayoutParams) mClockView.getLayoutParams();
207         int height = mClockView.getHeight();
208         layoutParams.bottomMargin = (int) -(height - (clockScale * height));
209         mClockView.setLayoutParams(layoutParams);
210 
211         layoutParams = (RelativeLayout.LayoutParams) mClockSeparator.getLayoutParams();
212         layoutParams.topMargin = smallClock ? (int) mWidgetPadding : 0;
213         layoutParams.bottomMargin = layoutParams.topMargin;
214         mClockSeparator.setLayoutParams(layoutParams);
215     }
216 
217     /**
218      * Animate clock and its separator when necessary.
219      */
220     @Override
onLayoutChange(View view, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom)221     public void onLayoutChange(View view, int left, int top, int right, int bottom,
222             int oldLeft, int oldTop, int oldRight, int oldBottom) {
223         int heightOffset = mPulsing ? 0 : getHeight() - mLastLayoutHeight;
224         boolean hasHeader = mKeyguardSlice.hasHeader();
225         boolean smallClock = hasHeader || mPulsing;
226         long duration = KeyguardSliceView.DEFAULT_ANIM_DURATION;
227         long delay = smallClock ? 0 : duration / 4;
228 
229         boolean shouldAnimate = mKeyguardSlice.getLayoutTransition() != null
230                 && mKeyguardSlice.getLayoutTransition().isRunning();
231         if (view == mClockView) {
232             float clockScale = smallClock ? mSmallClockScale : 1;
233             Paint.Style style = smallClock ? Paint.Style.FILL_AND_STROKE : Paint.Style.FILL;
234             mClockView.animate().cancel();
235             if (shouldAnimate) {
236                 mClockView.setY(oldTop + heightOffset);
237                 mClockView.animate()
238                         .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
239                         .setDuration(duration)
240                         .setListener(new ClipChildrenAnimationListener())
241                         .setStartDelay(delay)
242                         .y(top)
243                         .scaleX(clockScale)
244                         .scaleY(clockScale)
245                         .withEndAction(() -> {
246                             mClockView.getPaint().setStyle(style);
247                             mClockView.invalidate();
248                         })
249                         .start();
250             } else {
251                 mClockView.setY(top);
252                 mClockView.setScaleX(clockScale);
253                 mClockView.setScaleY(clockScale);
254                 mClockView.getPaint().setStyle(style);
255                 mClockView.invalidate();
256             }
257         } else if (view == mClockSeparator) {
258             boolean hasSeparator = hasHeader && !mPulsing;
259             float alpha = hasSeparator ? 1 : 0;
260             mClockSeparator.animate().cancel();
261             if (shouldAnimate) {
262                 boolean isAwake = mDarkAmount != 0;
263                 mClockSeparator.setY(oldTop + heightOffset);
264                 mClockSeparator.animate()
265                         .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
266                         .setDuration(duration)
267                         .setListener(isAwake ? null : new KeepAwakeAnimationListener(getContext()))
268                         .setStartDelay(delay)
269                         .y(top)
270                         .alpha(alpha)
271                         .start();
272             } else {
273                 mClockSeparator.setY(top);
274                 mClockSeparator.setAlpha(alpha);
275             }
276         }
277     }
278 
279     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)280     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
281         super.onLayout(changed, left, top, right, bottom);
282         mClockView.setPivotX(mClockView.getWidth() / 2);
283         mClockView.setPivotY(0);
284         mLastLayoutHeight = getHeight();
285         layoutOwnerInfo();
286     }
287 
288     @Override
onDensityOrFontScaleChanged()289     public void onDensityOrFontScaleChanged() {
290         mWidgetPadding = getResources().getDimension(R.dimen.widget_vertical_padding);
291         if (mClockView != null) {
292             mClockView.setTextSize(TypedValue.COMPLEX_UNIT_PX,
293                     getResources().getDimensionPixelSize(R.dimen.widget_big_font_size));
294             mClockView.getPaint().setStrokeWidth(
295                     getResources().getDimensionPixelSize(R.dimen.widget_small_font_stroke));
296         }
297         if (mOwnerInfo != null) {
298             mOwnerInfo.setTextSize(TypedValue.COMPLEX_UNIT_PX,
299                     getResources().getDimensionPixelSize(R.dimen.widget_label_font_size));
300         }
301     }
302 
dozeTimeTick()303     public void dozeTimeTick() {
304         refreshTime();
305         mKeyguardSlice.refresh();
306     }
307 
refreshTime()308     private void refreshTime() {
309         mClockView.refresh();
310     }
311 
refreshFormat()312     private void refreshFormat() {
313         Patterns.update(mContext);
314         mClockView.setFormat12Hour(Patterns.clockView12);
315         mClockView.setFormat24Hour(Patterns.clockView24);
316     }
317 
getLogoutButtonHeight()318     public int getLogoutButtonHeight() {
319         if (mLogoutView == null) {
320             return 0;
321         }
322         return mLogoutView.getVisibility() == VISIBLE ? mLogoutView.getHeight() : 0;
323     }
324 
getClockTextSize()325     public float getClockTextSize() {
326         return mClockView.getTextSize();
327     }
328 
updateLogoutView()329     private void updateLogoutView() {
330         if (mLogoutView == null) {
331             return;
332         }
333         mLogoutView.setVisibility(shouldShowLogout() ? VISIBLE : GONE);
334         // Logout button will stay in language of user 0 if we don't set that manually.
335         mLogoutView.setText(mContext.getResources().getString(
336                 com.android.internal.R.string.global_action_logout));
337     }
338 
updateOwnerInfo()339     private void updateOwnerInfo() {
340         if (mOwnerInfo == null) return;
341         String info = mLockPatternUtils.getDeviceOwnerInfo();
342         if (info == null) {
343             // Use the current user owner information if enabled.
344             final boolean ownerInfoEnabled = mLockPatternUtils.isOwnerInfoEnabled(
345                     KeyguardUpdateMonitor.getCurrentUser());
346             if (ownerInfoEnabled) {
347                 info = mLockPatternUtils.getOwnerInfo(KeyguardUpdateMonitor.getCurrentUser());
348             }
349         }
350         mOwnerInfo.setText(info);
351     }
352 
353     @Override
onAttachedToWindow()354     protected void onAttachedToWindow() {
355         super.onAttachedToWindow();
356         KeyguardUpdateMonitor.getInstance(mContext).registerCallback(mInfoCallback);
357         Dependency.get(ConfigurationController.class).addCallback(this);
358     }
359 
360     @Override
onDetachedFromWindow()361     protected void onDetachedFromWindow() {
362         super.onDetachedFromWindow();
363         KeyguardUpdateMonitor.getInstance(mContext).removeCallback(mInfoCallback);
364         Dependency.get(ConfigurationController.class).removeCallback(this);
365     }
366 
367     @Override
onLocaleListChanged()368     public void onLocaleListChanged() {
369         refreshFormat();
370     }
371 
372     @Override
hasOverlappingRendering()373     public boolean hasOverlappingRendering() {
374         return false;
375     }
376 
377     // DateFormat.getBestDateTimePattern is extremely expensive, and refresh is called often.
378     // This is an optimization to ensure we only recompute the patterns when the inputs change.
379     private static final class Patterns {
380         static String clockView12;
381         static String clockView24;
382         static String cacheKey;
383 
update(Context context)384         static void update(Context context) {
385             final Locale locale = Locale.getDefault();
386             final Resources res = context.getResources();
387             final String clockView12Skel = res.getString(R.string.clock_12hr_format);
388             final String clockView24Skel = res.getString(R.string.clock_24hr_format);
389             final String key = locale.toString() + clockView12Skel + clockView24Skel;
390             if (key.equals(cacheKey)) return;
391 
392             clockView12 = DateFormat.getBestDateTimePattern(locale, clockView12Skel);
393             // CLDR insists on adding an AM/PM indicator even though it wasn't in the skeleton
394             // format.  The following code removes the AM/PM indicator if we didn't want it.
395             if (!clockView12Skel.contains("a")) {
396                 clockView12 = clockView12.replaceAll("a", "").trim();
397             }
398 
399             clockView24 = DateFormat.getBestDateTimePattern(locale, clockView24Skel);
400 
401             // Use fancy colon.
402             clockView24 = clockView24.replace(':', '\uee01');
403             clockView12 = clockView12.replace(':', '\uee01');
404 
405             cacheKey = key;
406         }
407     }
408 
setDarkAmount(float darkAmount)409     public void setDarkAmount(float darkAmount) {
410         if (mDarkAmount == darkAmount) {
411             return;
412         }
413         mDarkAmount = darkAmount;
414         updateDark();
415     }
416 
updateDark()417     private void updateDark() {
418         boolean dark = mDarkAmount == 1;
419         if (mLogoutView != null) {
420             mLogoutView.setAlpha(dark ? 0 : 1);
421         }
422 
423         if (mOwnerInfo != null) {
424             boolean hasText = !TextUtils.isEmpty(mOwnerInfo.getText());
425             mOwnerInfo.setVisibility(hasText ? VISIBLE : GONE);
426             layoutOwnerInfo();
427         }
428 
429         final int blendedTextColor = ColorUtils.blendARGB(mTextColor, Color.WHITE, mDarkAmount);
430         updateDozeVisibleViews();
431         mKeyguardSlice.setDarkAmount(mDarkAmount);
432         mClockView.setTextColor(blendedTextColor);
433         mClockSeparator.setBackgroundColor(blendedTextColor);
434     }
435 
layoutOwnerInfo()436     private void layoutOwnerInfo() {
437         if (mOwnerInfo != null && mOwnerInfo.getVisibility() != GONE) {
438             // Animate owner info during wake-up transition
439             mOwnerInfo.setAlpha(1f - mDarkAmount);
440 
441             float ratio = mDarkAmount;
442             // Calculate how much of it we should crop in order to have a smooth transition
443             int collapsed = mOwnerInfo.getTop() - mOwnerInfo.getPaddingTop();
444             int expanded = mOwnerInfo.getBottom() + mOwnerInfo.getPaddingBottom();
445             int toRemove = (int) ((expanded - collapsed) * ratio);
446             setBottom(getMeasuredHeight() - toRemove);
447         }
448     }
449 
setPulsing(boolean pulsing, boolean animate)450     public void setPulsing(boolean pulsing, boolean animate) {
451         mPulsing = pulsing;
452         mKeyguardSlice.setPulsing(pulsing, animate);
453         updateDozeVisibleViews();
454     }
455 
updateDozeVisibleViews()456     private void updateDozeVisibleViews() {
457         for (View child : mVisibleInDoze) {
458             child.setAlpha(mDarkAmount == 1 && mPulsing ? 0.8f : 1);
459         }
460     }
461 
shouldShowLogout()462     private boolean shouldShowLogout() {
463         return KeyguardUpdateMonitor.getInstance(mContext).isLogoutEnabled()
464                 && KeyguardUpdateMonitor.getCurrentUser() != UserHandle.USER_SYSTEM;
465     }
466 
onLogoutClicked(View view)467     private void onLogoutClicked(View view) {
468         int currentUserId = KeyguardUpdateMonitor.getCurrentUser();
469         try {
470             mIActivityManager.switchUser(UserHandle.USER_SYSTEM);
471             mIActivityManager.stopUser(currentUserId, true /*force*/, null);
472         } catch (RemoteException re) {
473             Log.e(TAG, "Failed to logout user", re);
474         }
475     }
476 
477     private class ClipChildrenAnimationListener extends AnimatorListenerAdapter implements
478             ViewClippingUtil.ClippingParameters {
479 
ClipChildrenAnimationListener()480         ClipChildrenAnimationListener() {
481             ViewClippingUtil.setClippingDeactivated(mClockView, true /* deactivated */,
482                     this /* clippingParams */);
483         }
484 
485         @Override
onAnimationEnd(Animator animation)486         public void onAnimationEnd(Animator animation) {
487             ViewClippingUtil.setClippingDeactivated(mClockView, false /* deactivated */,
488                     this /* clippingParams */);
489         }
490 
491         @Override
shouldFinish(View view)492         public boolean shouldFinish(View view) {
493             return view == getParent();
494         }
495     }
496 }
497