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
17package com.android.systemui.statusbar;
18
19import android.app.ActivityManager;
20import android.app.admin.DevicePolicyManager;
21import android.content.BroadcastReceiver;
22import android.content.Context;
23import android.content.Intent;
24import android.content.IntentFilter;
25import android.content.res.Resources;
26import android.graphics.Color;
27import android.hardware.fingerprint.FingerprintManager;
28import android.os.BatteryManager;
29import android.os.BatteryStats;
30import android.os.Handler;
31import android.os.Message;
32import android.os.RemoteException;
33import android.os.ServiceManager;
34import android.os.UserHandle;
35import android.os.UserManager;
36import android.text.TextUtils;
37import android.text.format.Formatter;
38import android.util.Log;
39import android.view.View;
40import android.view.ViewGroup;
41
42import com.android.internal.annotations.VisibleForTesting;
43import com.android.internal.app.IBatteryStats;
44import com.android.keyguard.KeyguardUpdateMonitor;
45import com.android.keyguard.KeyguardUpdateMonitorCallback;
46import com.android.settingslib.Utils;
47import com.android.systemui.Dependency;
48import com.android.systemui.R;
49import com.android.systemui.statusbar.phone.KeyguardIndicationTextView;
50import com.android.systemui.statusbar.phone.LockIcon;
51import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
52import com.android.systemui.statusbar.policy.UserInfoController;
53import com.android.systemui.util.wakelock.SettableWakeLock;
54import com.android.systemui.util.wakelock.WakeLock;
55
56/**
57 * Controls the indications and error messages shown on the Keyguard
58 */
59public class KeyguardIndicationController {
60
61    private static final String TAG = "KeyguardIndication";
62    private static final boolean DEBUG_CHARGING_SPEED = false;
63
64    private static final int MSG_HIDE_TRANSIENT = 1;
65    private static final int MSG_CLEAR_FP_MSG = 2;
66    private static final long TRANSIENT_FP_ERROR_TIMEOUT = 1300;
67
68    private final Context mContext;
69    private final ViewGroup mIndicationArea;
70    private final KeyguardIndicationTextView mTextView;
71    private final KeyguardIndicationTextView mDisclosure;
72    private final UserManager mUserManager;
73    private final IBatteryStats mBatteryInfo;
74    private final SettableWakeLock mWakeLock;
75
76    private final int mSlowThreshold;
77    private final int mFastThreshold;
78    private final LockIcon mLockIcon;
79    private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
80
81    private String mRestingIndication;
82    private String mTransientIndication;
83    private int mTransientTextColor;
84    private boolean mVisible;
85
86    private boolean mPowerPluggedIn;
87    private boolean mPowerCharged;
88    private int mChargingSpeed;
89    private int mChargingWattage;
90    private String mMessageToShowOnScreenOn;
91
92    private KeyguardUpdateMonitorCallback mUpdateMonitor;
93
94    private final DevicePolicyManager mDevicePolicyManager;
95    private boolean mDozing;
96
97    /**
98     * Creates a new KeyguardIndicationController and registers callbacks.
99     */
100    public KeyguardIndicationController(Context context, ViewGroup indicationArea,
101            LockIcon lockIcon) {
102        this(context, indicationArea, lockIcon,
103                WakeLock.createPartial(context, "Doze:KeyguardIndication"));
104
105        registerCallbacks(KeyguardUpdateMonitor.getInstance(context));
106    }
107
108    /**
109     * Creates a new KeyguardIndicationController for testing. Does *not* register callbacks.
110     */
111    @VisibleForTesting
112    KeyguardIndicationController(Context context, ViewGroup indicationArea, LockIcon lockIcon,
113                WakeLock wakeLock) {
114        mContext = context;
115        mIndicationArea = indicationArea;
116        mTextView = (KeyguardIndicationTextView) indicationArea.findViewById(
117                R.id.keyguard_indication_text);
118        mDisclosure = (KeyguardIndicationTextView) indicationArea.findViewById(
119                R.id.keyguard_indication_enterprise_disclosure);
120        mLockIcon = lockIcon;
121        mWakeLock = new SettableWakeLock(wakeLock);
122
123        Resources res = context.getResources();
124        mSlowThreshold = res.getInteger(R.integer.config_chargingSlowlyThreshold);
125        mFastThreshold = res.getInteger(R.integer.config_chargingFastThreshold);
126
127        mUserManager = context.getSystemService(UserManager.class);
128        mBatteryInfo = IBatteryStats.Stub.asInterface(
129                ServiceManager.getService(BatteryStats.SERVICE_NAME));
130
131        mDevicePolicyManager = (DevicePolicyManager) context.getSystemService(
132                Context.DEVICE_POLICY_SERVICE);
133
134        updateDisclosure();
135    }
136
137    private void registerCallbacks(KeyguardUpdateMonitor monitor) {
138        monitor.registerCallback(getKeyguardCallback());
139
140        mContext.registerReceiverAsUser(mTickReceiver, UserHandle.SYSTEM,
141                new IntentFilter(Intent.ACTION_TIME_TICK), null,
142                Dependency.get(Dependency.TIME_TICK_HANDLER));
143    }
144
145    /**
146     * Gets the {@link KeyguardUpdateMonitorCallback} instance associated with this
147     * {@link KeyguardIndicationController}.
148     *
149     * <p>Subclasses may override this method to extend or change the callback behavior by extending
150     * the {@link BaseKeyguardCallback}.
151     *
152     * @return A KeyguardUpdateMonitorCallback. Multiple calls to this method <b>must</b> return the
153     * same instance.
154     */
155    protected KeyguardUpdateMonitorCallback getKeyguardCallback() {
156        if (mUpdateMonitor == null) {
157            mUpdateMonitor = new BaseKeyguardCallback();
158        }
159        return mUpdateMonitor;
160    }
161
162    private void updateDisclosure() {
163        if (mDevicePolicyManager == null) {
164            return;
165        }
166
167        if (!mDozing && mDevicePolicyManager.isDeviceManaged()) {
168            final CharSequence organizationName =
169                    mDevicePolicyManager.getDeviceOwnerOrganizationName();
170            if (organizationName != null) {
171                mDisclosure.switchIndication(mContext.getResources().getString(
172                        R.string.do_disclosure_with_name, organizationName));
173            } else {
174                mDisclosure.switchIndication(R.string.do_disclosure_generic);
175            }
176            mDisclosure.setVisibility(View.VISIBLE);
177        } else {
178            mDisclosure.setVisibility(View.GONE);
179        }
180    }
181
182    public void setVisible(boolean visible) {
183        mVisible = visible;
184        mIndicationArea.setVisibility(visible ? View.VISIBLE : View.GONE);
185        if (visible) {
186            hideTransientIndication();
187            updateIndication();
188        }
189    }
190
191    /**
192     * Sets the indication that is shown if nothing else is showing.
193     */
194    public void setRestingIndication(String restingIndication) {
195        mRestingIndication = restingIndication;
196        updateIndication();
197    }
198
199    /**
200     * Sets the active controller managing changes and callbacks to user information.
201     */
202    public void setUserInfoController(UserInfoController userInfoController) {
203    }
204
205    /**
206     * Hides transient indication in {@param delayMs}.
207     */
208    public void hideTransientIndicationDelayed(long delayMs) {
209        mHandler.sendMessageDelayed(
210                mHandler.obtainMessage(MSG_HIDE_TRANSIENT), delayMs);
211    }
212
213    /**
214     * Shows {@param transientIndication} until it is hidden by {@link #hideTransientIndication}.
215     */
216    public void showTransientIndication(int transientIndication) {
217        showTransientIndication(mContext.getResources().getString(transientIndication));
218    }
219
220    /**
221     * Shows {@param transientIndication} until it is hidden by {@link #hideTransientIndication}.
222     */
223    public void showTransientIndication(String transientIndication) {
224        showTransientIndication(transientIndication, Color.WHITE);
225    }
226
227    /**
228     * Shows {@param transientIndication} until it is hidden by {@link #hideTransientIndication}.
229     */
230    public void showTransientIndication(String transientIndication, int textColor) {
231        mTransientIndication = transientIndication;
232        mTransientTextColor = textColor;
233        mHandler.removeMessages(MSG_HIDE_TRANSIENT);
234        if (mDozing && !TextUtils.isEmpty(mTransientIndication)) {
235            // Make sure this doesn't get stuck and burns in. Acquire wakelock until its cleared.
236            mWakeLock.setAcquired(true);
237            hideTransientIndicationDelayed(BaseKeyguardCallback.HIDE_DELAY_MS);
238        }
239        updateIndication();
240    }
241
242    /**
243     * Hides transient indication.
244     */
245    public void hideTransientIndication() {
246        if (mTransientIndication != null) {
247            mTransientIndication = null;
248            mHandler.removeMessages(MSG_HIDE_TRANSIENT);
249            updateIndication();
250        }
251    }
252
253    private void updateIndication() {
254        if (TextUtils.isEmpty(mTransientIndication)) {
255            mWakeLock.setAcquired(false);
256        }
257
258        if (mVisible) {
259            // Walk down a precedence-ordered list of what should indication
260            // should be shown based on user or device state
261            if (mDozing) {
262                // If we're dozing, never show a persistent indication.
263                if (!TextUtils.isEmpty(mTransientIndication)) {
264                    mTextView.switchIndication(mTransientIndication);
265                    mTextView.setTextColor(mTransientTextColor);
266
267                } else {
268                    mTextView.switchIndication(null);
269                }
270                return;
271            }
272
273            if (!mUserManager.isUserUnlocked(KeyguardUpdateMonitor.getCurrentUser())) {
274                mTextView.switchIndication(com.android.internal.R.string.lockscreen_storage_locked);
275                mTextView.setTextColor(Color.WHITE);
276
277            } else if (!TextUtils.isEmpty(mTransientIndication)) {
278                mTextView.switchIndication(mTransientIndication);
279                mTextView.setTextColor(mTransientTextColor);
280
281            } else if (mPowerPluggedIn) {
282                String indication = computePowerIndication();
283                if (DEBUG_CHARGING_SPEED) {
284                    indication += ",  " + (mChargingWattage / 1000) + " mW";
285                }
286                mTextView.switchIndication(indication);
287                mTextView.setTextColor(Color.WHITE);
288
289            } else {
290                mTextView.switchIndication(mRestingIndication);
291                mTextView.setTextColor(Color.WHITE);
292            }
293        }
294    }
295
296    private String computePowerIndication() {
297        if (mPowerCharged) {
298            return mContext.getResources().getString(R.string.keyguard_charged);
299        }
300
301        // Try fetching charging time from battery stats.
302        long chargingTimeRemaining = 0;
303        try {
304            chargingTimeRemaining = mBatteryInfo.computeChargeTimeRemaining();
305
306        } catch (RemoteException e) {
307            Log.e(TAG, "Error calling IBatteryStats: ", e);
308        }
309        final boolean hasChargingTime = chargingTimeRemaining > 0;
310
311        int chargingId;
312        switch (mChargingSpeed) {
313            case KeyguardUpdateMonitor.BatteryStatus.CHARGING_FAST:
314                chargingId = hasChargingTime
315                        ? R.string.keyguard_indication_charging_time_fast
316                        : R.string.keyguard_plugged_in_charging_fast;
317                break;
318            case KeyguardUpdateMonitor.BatteryStatus.CHARGING_SLOWLY:
319                chargingId = hasChargingTime
320                        ? R.string.keyguard_indication_charging_time_slowly
321                        : R.string.keyguard_plugged_in_charging_slowly;
322                break;
323            default:
324                chargingId = hasChargingTime
325                        ? R.string.keyguard_indication_charging_time
326                        : R.string.keyguard_plugged_in;
327                break;
328        }
329
330        if (hasChargingTime) {
331            String chargingTimeFormatted = Formatter.formatShortElapsedTimeRoundingUpToMinutes(
332                    mContext, chargingTimeRemaining);
333            return mContext.getResources().getString(chargingId, chargingTimeFormatted);
334        } else {
335            return mContext.getResources().getString(chargingId);
336        }
337    }
338
339    public void setStatusBarKeyguardViewManager(
340            StatusBarKeyguardViewManager statusBarKeyguardViewManager) {
341        mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
342    }
343
344    private final BroadcastReceiver mTickReceiver = new BroadcastReceiver() {
345        @Override
346        public void onReceive(Context context, Intent intent) {
347            mHandler.post(() -> {
348                if (mVisible) {
349                    updateIndication();
350                }
351            });
352        }
353    };
354
355    private final Handler mHandler = new Handler() {
356        @Override
357        public void handleMessage(Message msg) {
358            if (msg.what == MSG_HIDE_TRANSIENT) {
359                hideTransientIndication();
360            } else if (msg.what == MSG_CLEAR_FP_MSG) {
361                mLockIcon.setTransientFpError(false);
362                hideTransientIndication();
363            }
364        }
365    };
366
367    public void setDozing(boolean dozing) {
368        if (mDozing == dozing) {
369            return;
370        }
371        mDozing = dozing;
372        updateIndication();
373        updateDisclosure();
374    }
375
376    protected class BaseKeyguardCallback extends KeyguardUpdateMonitorCallback {
377        public static final int HIDE_DELAY_MS = 5000;
378        private int mLastSuccessiveErrorMessage = -1;
379
380        @Override
381        public void onRefreshBatteryInfo(KeyguardUpdateMonitor.BatteryStatus status) {
382            boolean isChargingOrFull = status.status == BatteryManager.BATTERY_STATUS_CHARGING
383                    || status.status == BatteryManager.BATTERY_STATUS_FULL;
384            boolean wasPluggedIn = mPowerPluggedIn;
385            mPowerPluggedIn = status.isPluggedIn() && isChargingOrFull;
386            mPowerCharged = status.isCharged();
387            mChargingWattage = status.maxChargingWattage;
388            mChargingSpeed = status.getChargingSpeed(mSlowThreshold, mFastThreshold);
389            updateIndication();
390            if (mDozing) {
391                if (!wasPluggedIn && mPowerPluggedIn) {
392                    showTransientIndication(computePowerIndication());
393                    hideTransientIndicationDelayed(HIDE_DELAY_MS);
394                } else if (wasPluggedIn && !mPowerPluggedIn) {
395                    hideTransientIndication();
396                }
397            }
398        }
399
400        @Override
401        public void onKeyguardVisibilityChanged(boolean showing) {
402            if (showing) {
403                updateDisclosure();
404            }
405        }
406
407        @Override
408        public void onFingerprintHelp(int msgId, String helpString) {
409            KeyguardUpdateMonitor updateMonitor = KeyguardUpdateMonitor.getInstance(mContext);
410            if (!updateMonitor.isUnlockingWithFingerprintAllowed()) {
411                return;
412            }
413            int errorColor = Utils.getColorError(mContext);
414            if (mStatusBarKeyguardViewManager.isBouncerShowing()) {
415                mStatusBarKeyguardViewManager.showBouncerMessage(helpString, errorColor);
416            } else if (updateMonitor.isDeviceInteractive()
417                    || mDozing && updateMonitor.isScreenOn()) {
418                mLockIcon.setTransientFpError(true);
419                showTransientIndication(helpString, errorColor);
420                mHandler.removeMessages(MSG_CLEAR_FP_MSG);
421                mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_CLEAR_FP_MSG),
422                        TRANSIENT_FP_ERROR_TIMEOUT);
423            }
424            // Help messages indicate that there was actually a try since the last error, so those
425            // are not two successive error messages anymore.
426            mLastSuccessiveErrorMessage = -1;
427        }
428
429        @Override
430        public void onFingerprintError(int msgId, String errString) {
431            KeyguardUpdateMonitor updateMonitor = KeyguardUpdateMonitor.getInstance(mContext);
432            if (!updateMonitor.isUnlockingWithFingerprintAllowed()
433                    || msgId == FingerprintManager.FINGERPRINT_ERROR_CANCELED) {
434                return;
435            }
436            int errorColor = Utils.getColorError(mContext);
437            if (mStatusBarKeyguardViewManager.isBouncerShowing()) {
438                // When swiping up right after receiving a fingerprint error, the bouncer calls
439                // authenticate leading to the same message being shown again on the bouncer.
440                // We want to avoid this, as it may confuse the user when the message is too
441                // generic.
442                if (mLastSuccessiveErrorMessage != msgId) {
443                    mStatusBarKeyguardViewManager.showBouncerMessage(errString, errorColor);
444                }
445            } else if (updateMonitor.isDeviceInteractive()) {
446                showTransientIndication(errString, errorColor);
447                // We want to keep this message around in case the screen was off
448                hideTransientIndicationDelayed(HIDE_DELAY_MS);
449            } else {
450                mMessageToShowOnScreenOn = errString;
451            }
452            mLastSuccessiveErrorMessage = msgId;
453        }
454
455        @Override
456        public void onScreenTurnedOn() {
457            if (mMessageToShowOnScreenOn != null) {
458                int errorColor = Utils.getColorError(mContext);
459                showTransientIndication(mMessageToShowOnScreenOn, errorColor);
460                // We want to keep this message around in case the screen was off
461                hideTransientIndicationDelayed(HIDE_DELAY_MS);
462                mMessageToShowOnScreenOn = null;
463            }
464        }
465
466        @Override
467        public void onFingerprintRunningStateChanged(boolean running) {
468            if (running) {
469                mMessageToShowOnScreenOn = null;
470            }
471        }
472
473        @Override
474        public void onFingerprintAuthenticated(int userId) {
475            super.onFingerprintAuthenticated(userId);
476            mLastSuccessiveErrorMessage = -1;
477        }
478
479        @Override
480        public void onFingerprintAuthFailed() {
481            super.onFingerprintAuthFailed();
482            mLastSuccessiveErrorMessage = -1;
483        }
484
485        @Override
486        public void onUserUnlocked() {
487            if (mVisible) {
488                updateIndication();
489            }
490        }
491    };
492}
493