/* * Copyright (C) 2012 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.keyguard; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.app.ActivityManager; import android.app.IActivityManager; import android.content.Context; import android.content.res.Resources; import android.graphics.Color; import android.graphics.Paint; import android.os.Handler; import android.os.Looper; import android.os.RemoteException; import android.os.UserHandle; import android.text.TextUtils; import android.text.format.DateFormat; import android.util.ArraySet; import android.util.AttributeSet; import android.util.Log; import android.util.Slog; import android.util.TypedValue; import android.view.View; import android.widget.GridLayout; import android.widget.RelativeLayout; import android.widget.TextView; import androidx.core.graphics.ColorUtils; import com.android.internal.widget.LockPatternUtils; import com.android.internal.widget.ViewClippingUtil; import com.android.systemui.Dependency; import com.android.systemui.Interpolators; import com.android.systemui.statusbar.policy.ConfigurationController; import com.google.android.collect.Sets; import java.util.Locale; public class KeyguardStatusView extends GridLayout implements ConfigurationController.ConfigurationListener, View.OnLayoutChangeListener { private static final boolean DEBUG = KeyguardConstants.DEBUG; private static final String TAG = "KeyguardStatusView"; private static final int MARQUEE_DELAY_MS = 2000; private final LockPatternUtils mLockPatternUtils; private final IActivityManager mIActivityManager; private final float mSmallClockScale; private TextView mLogoutView; private KeyguardClockSwitch mClockView; private TextView mOwnerInfo; private KeyguardSliceView mKeyguardSlice; private Runnable mPendingMarqueeStart; private Handler mHandler; private ArraySet mVisibleInDoze; private boolean mPulsing; private boolean mWasPulsing; private float mDarkAmount = 0; private int mTextColor; private int mLastLayoutHeight; private int mSmallClockPadding; private KeyguardUpdateMonitorCallback mInfoCallback = new KeyguardUpdateMonitorCallback() { @Override public void onTimeChanged() { refreshTime(); } @Override public void onKeyguardVisibilityChanged(boolean showing) { if (showing) { if (DEBUG) Slog.v(TAG, "refresh statusview showing:" + showing); refreshTime(); updateOwnerInfo(); updateLogoutView(); } } @Override public void onStartedWakingUp() { setEnableMarquee(true); } @Override public void onFinishedGoingToSleep(int why) { setEnableMarquee(false); } @Override public void onUserSwitchComplete(int userId) { refreshFormat(); updateOwnerInfo(); updateLogoutView(); } @Override public void onLogoutEnabledChanged() { updateLogoutView(); } }; public KeyguardStatusView(Context context) { this(context, null, 0); } public KeyguardStatusView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public KeyguardStatusView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mIActivityManager = ActivityManager.getService(); mLockPatternUtils = new LockPatternUtils(getContext()); mHandler = new Handler(Looper.myLooper()); mSmallClockScale = getResources().getDimension(R.dimen.widget_small_font_size) / getResources().getDimension(R.dimen.widget_big_font_size); onDensityOrFontScaleChanged(); } private void setEnableMarquee(boolean enabled) { if (DEBUG) Log.v(TAG, "Schedule setEnableMarquee: " + (enabled ? "Enable" : "Disable")); if (enabled) { if (mPendingMarqueeStart == null) { mPendingMarqueeStart = () -> { setEnableMarqueeImpl(true); mPendingMarqueeStart = null; }; mHandler.postDelayed(mPendingMarqueeStart, MARQUEE_DELAY_MS); } } else { if (mPendingMarqueeStart != null) { mHandler.removeCallbacks(mPendingMarqueeStart); mPendingMarqueeStart = null; } setEnableMarqueeImpl(false); } } private void setEnableMarqueeImpl(boolean enabled) { if (DEBUG) Log.v(TAG, (enabled ? "Enable" : "Disable") + " transport text marquee"); if (mOwnerInfo != null) mOwnerInfo.setSelected(enabled); } @Override protected void onFinishInflate() { super.onFinishInflate(); mLogoutView = findViewById(R.id.logout); if (mLogoutView != null) { mLogoutView.setOnClickListener(this::onLogoutClicked); } mClockView = findViewById(R.id.clock_view); mClockView.setShowCurrentUserTime(true); if (KeyguardClockAccessibilityDelegate.isNeeded(mContext)) { mClockView.setAccessibilityDelegate(new KeyguardClockAccessibilityDelegate(mContext)); } mOwnerInfo = findViewById(R.id.owner_info); mKeyguardSlice = findViewById(R.id.keyguard_status_area); mVisibleInDoze = Sets.newArraySet(mClockView, mKeyguardSlice); mTextColor = mClockView.getCurrentTextColor(); int clockStroke = getResources().getDimensionPixelSize(R.dimen.widget_small_font_stroke); mClockView.getPaint().setStrokeWidth(clockStroke); mClockView.addOnLayoutChangeListener(this); mKeyguardSlice.setContentChangeListener(this::onSliceContentChanged); onSliceContentChanged(); boolean shouldMarquee = KeyguardUpdateMonitor.getInstance(mContext).isDeviceInteractive(); setEnableMarquee(shouldMarquee); refreshFormat(); updateOwnerInfo(); updateLogoutView(); updateDark(); // Disable elegant text height because our fancy colon makes the ymin value huge for no // reason. mClockView.setElegantTextHeight(false); } /** * Moves clock, adjusting margins when slice content changes. */ private void onSliceContentChanged() { RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) mClockView.getLayoutParams(); layoutParams.bottomMargin = mPulsing ? mSmallClockPadding : 0; mClockView.setLayoutParams(layoutParams); } /** * Animate clock when necessary. */ @Override public void onLayoutChange(View view, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { int heightOffset = mPulsing || mWasPulsing ? 0 : getHeight() - mLastLayoutHeight; long duration = KeyguardSliceView.DEFAULT_ANIM_DURATION; long delay = mPulsing || mWasPulsing ? 0 : duration / 4; mWasPulsing = false; boolean shouldAnimate = mKeyguardSlice.getLayoutTransition() != null && mKeyguardSlice.getLayoutTransition().isRunning(); if (view == mClockView) { float clockScale = mPulsing ? mSmallClockScale : 1; Paint.Style style = mPulsing ? Paint.Style.FILL_AND_STROKE : Paint.Style.FILL; mClockView.animate().cancel(); if (shouldAnimate) { mClockView.setY(oldTop + heightOffset); mClockView.animate() .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) .setDuration(duration) .setListener(new ClipChildrenAnimationListener()) .setStartDelay(delay) .y(top) .scaleX(clockScale) .scaleY(clockScale) .withEndAction(() -> { mClockView.setStyle(style); mClockView.invalidate(); }) .start(); } else { mClockView.setY(top); mClockView.setScaleX(clockScale); mClockView.setScaleY(clockScale); mClockView.setStyle(style); mClockView.invalidate(); } } } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); mClockView.setPivotX(mClockView.getWidth() / 2); mClockView.setPivotY(0); mLastLayoutHeight = getHeight(); layoutOwnerInfo(); } @Override public void onDensityOrFontScaleChanged() { mSmallClockPadding = getResources() .getDimensionPixelSize(R.dimen.widget_small_clock_padding); if (mClockView != null) { mClockView.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimensionPixelSize(R.dimen.widget_big_font_size)); mClockView.getPaint().setStrokeWidth( getResources().getDimensionPixelSize(R.dimen.widget_small_font_stroke)); } if (mOwnerInfo != null) { mOwnerInfo.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimensionPixelSize(R.dimen.widget_label_font_size)); } } public void dozeTimeTick() { refreshTime(); mKeyguardSlice.refresh(); mClockView.dozeTimeTick(); } private void refreshTime() { mClockView.refresh(); } private void refreshFormat() { Patterns.update(mContext); mClockView.setFormat12Hour(Patterns.clockView12); mClockView.setFormat24Hour(Patterns.clockView24); } public int getLogoutButtonHeight() { if (mLogoutView == null) { return 0; } return mLogoutView.getVisibility() == VISIBLE ? mLogoutView.getHeight() : 0; } public float getClockTextSize() { return mClockView.getTextSize(); } private void updateLogoutView() { if (mLogoutView == null) { return; } mLogoutView.setVisibility(shouldShowLogout() ? VISIBLE : GONE); // Logout button will stay in language of user 0 if we don't set that manually. mLogoutView.setText(mContext.getResources().getString( com.android.internal.R.string.global_action_logout)); } private void updateOwnerInfo() { if (mOwnerInfo == null) return; String info = mLockPatternUtils.getDeviceOwnerInfo(); if (info == null) { // Use the current user owner information if enabled. final boolean ownerInfoEnabled = mLockPatternUtils.isOwnerInfoEnabled( KeyguardUpdateMonitor.getCurrentUser()); if (ownerInfoEnabled) { info = mLockPatternUtils.getOwnerInfo(KeyguardUpdateMonitor.getCurrentUser()); } } mOwnerInfo.setText(info); updateDark(); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); KeyguardUpdateMonitor.getInstance(mContext).registerCallback(mInfoCallback); Dependency.get(ConfigurationController.class).addCallback(this); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); KeyguardUpdateMonitor.getInstance(mContext).removeCallback(mInfoCallback); Dependency.get(ConfigurationController.class).removeCallback(this); } @Override public void onLocaleListChanged() { refreshFormat(); } @Override public boolean hasOverlappingRendering() { return false; } // DateFormat.getBestDateTimePattern is extremely expensive, and refresh is called often. // This is an optimization to ensure we only recompute the patterns when the inputs change. private static final class Patterns { static String clockView12; static String clockView24; static String cacheKey; static void update(Context context) { final Locale locale = Locale.getDefault(); final Resources res = context.getResources(); final String clockView12Skel = res.getString(R.string.clock_12hr_format); final String clockView24Skel = res.getString(R.string.clock_24hr_format); final String key = locale.toString() + clockView12Skel + clockView24Skel; if (key.equals(cacheKey)) return; clockView12 = DateFormat.getBestDateTimePattern(locale, clockView12Skel); // CLDR insists on adding an AM/PM indicator even though it wasn't in the skeleton // format. The following code removes the AM/PM indicator if we didn't want it. if (!clockView12Skel.contains("a")) { clockView12 = clockView12.replaceAll("a", "").trim(); } clockView24 = DateFormat.getBestDateTimePattern(locale, clockView24Skel); // Use fancy colon. clockView24 = clockView24.replace(':', '\uee01'); clockView12 = clockView12.replace(':', '\uee01'); cacheKey = key; } } public void setDarkAmount(float darkAmount) { if (mDarkAmount == darkAmount) { return; } mDarkAmount = darkAmount; mClockView.setDarkAmount(darkAmount); updateDark(); } private void updateDark() { boolean dark = mDarkAmount == 1; if (mLogoutView != null) { mLogoutView.setAlpha(dark ? 0 : 1); } if (mOwnerInfo != null) { boolean hasText = !TextUtils.isEmpty(mOwnerInfo.getText()); mOwnerInfo.setVisibility(hasText ? VISIBLE : GONE); layoutOwnerInfo(); } final int blendedTextColor = ColorUtils.blendARGB(mTextColor, Color.WHITE, mDarkAmount); updateDozeVisibleViews(); mKeyguardSlice.setDarkAmount(mDarkAmount); mClockView.setTextColor(blendedTextColor); } private void layoutOwnerInfo() { if (mOwnerInfo != null && mOwnerInfo.getVisibility() != GONE) { // Animate owner info during wake-up transition mOwnerInfo.setAlpha(1f - mDarkAmount); float ratio = mDarkAmount; // Calculate how much of it we should crop in order to have a smooth transition int collapsed = mOwnerInfo.getTop() - mOwnerInfo.getPaddingTop(); int expanded = mOwnerInfo.getBottom() + mOwnerInfo.getPaddingBottom(); int toRemove = (int) ((expanded - collapsed) * ratio); setBottom(getMeasuredHeight() - toRemove); } } public void setPulsing(boolean pulsing, boolean animate) { if (mPulsing == pulsing) { return; } if (mPulsing) { mWasPulsing = true; } mPulsing = pulsing; mKeyguardSlice.setPulsing(pulsing, animate); updateDozeVisibleViews(); } private void updateDozeVisibleViews() { for (View child : mVisibleInDoze) { child.setAlpha(mDarkAmount == 1 && mPulsing ? 0.8f : 1); } } private boolean shouldShowLogout() { return KeyguardUpdateMonitor.getInstance(mContext).isLogoutEnabled() && KeyguardUpdateMonitor.getCurrentUser() != UserHandle.USER_SYSTEM; } private void onLogoutClicked(View view) { int currentUserId = KeyguardUpdateMonitor.getCurrentUser(); try { mIActivityManager.switchUser(UserHandle.USER_SYSTEM); mIActivityManager.stopUser(currentUserId, true /*force*/, null); } catch (RemoteException re) { Log.e(TAG, "Failed to logout user", re); } } private class ClipChildrenAnimationListener extends AnimatorListenerAdapter implements ViewClippingUtil.ClippingParameters { ClipChildrenAnimationListener() { ViewClippingUtil.setClippingDeactivated(mClockView, true /* deactivated */, this /* clippingParams */); } @Override public void onAnimationEnd(Animator animation) { ViewClippingUtil.setClippingDeactivated(mClockView, false /* deactivated */, this /* clippingParams */); } @Override public boolean shouldFinish(View view) { return view == getParent(); } } }