1/*
2 * Copyright (C) 2011 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.keyguard;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ObjectAnimator;
22import android.content.ContentResolver;
23import android.content.Context;
24import android.os.BatteryManager;
25import android.os.Handler;
26import android.os.IBinder;
27import android.os.Looper;
28import android.os.RemoteException;
29import android.os.ServiceManager;
30import android.os.SystemClock;
31import android.os.UserHandle;
32import android.provider.Settings;
33import android.text.TextUtils;
34import android.util.AttributeSet;
35import android.util.Slog;
36import android.view.View;
37import android.widget.TextView;
38
39import libcore.util.MutableInt;
40
41import java.lang.ref.WeakReference;
42
43import com.android.internal.widget.LockPatternUtils;
44
45/***
46 * Manages a number of views inside of the given layout. See below for a list of widgets.
47 */
48class KeyguardMessageArea extends TextView {
49    /** Handler token posted with accessibility announcement runnables. */
50    private static final Object ANNOUNCE_TOKEN = new Object();
51
52    /**
53     * Delay before speaking an accessibility announcement. Used to prevent
54     * lift-to-type from interrupting itself.
55     */
56    private static final long ANNOUNCEMENT_DELAY = 250;
57
58    static final int CHARGING_ICON = 0; //R.drawable.ic_lock_idle_charging;
59    static final int BATTERY_LOW_ICON = 0; //R.drawable.ic_lock_idle_low_battery;
60
61    static final int SECURITY_MESSAGE_DURATION = 5000;
62    protected static final int FADE_DURATION = 750;
63
64    private static final String TAG = "KeyguardMessageArea";
65
66    // are we showing battery information?
67    boolean mShowingBatteryInfo = false;
68
69    // is the bouncer up?
70    boolean mShowingBouncer = false;
71
72    // last known plugged in state
73    boolean mCharging = false;
74
75    // last known battery level
76    int mBatteryLevel = 100;
77
78    KeyguardUpdateMonitor mUpdateMonitor;
79
80    // Timeout before we reset the message to show charging/owner info
81    long mTimeout = SECURITY_MESSAGE_DURATION;
82
83    // Shadowed text values
84    protected boolean mBatteryCharged;
85    protected boolean mBatteryIsLow;
86
87    private Handler mHandler;
88
89    CharSequence mMessage;
90    boolean mShowingMessage;
91    private CharSequence mSeparator;
92    private LockPatternUtils mLockPatternUtils;
93
94    Runnable mClearMessageRunnable = new Runnable() {
95        @Override
96        public void run() {
97            mMessage = null;
98            mShowingMessage = false;
99            if (mShowingBouncer) {
100                hideMessage(FADE_DURATION, true);
101            } else {
102                update();
103            }
104        }
105    };
106
107    public static class Helper implements SecurityMessageDisplay {
108        KeyguardMessageArea mMessageArea;
109        Helper(View v) {
110            mMessageArea = (KeyguardMessageArea) v.findViewById(R.id.keyguard_message_area);
111            if (mMessageArea == null) {
112                throw new RuntimeException("Can't find keyguard_message_area in " + v.getClass());
113            }
114        }
115
116        public void setMessage(CharSequence msg, boolean important) {
117            if (!TextUtils.isEmpty(msg) && important) {
118                mMessageArea.mMessage = msg;
119                mMessageArea.securityMessageChanged();
120            }
121        }
122
123        public void setMessage(int resId, boolean important) {
124            if (resId != 0 && important) {
125                mMessageArea.mMessage = mMessageArea.getContext().getResources().getText(resId);
126                mMessageArea.securityMessageChanged();
127            }
128        }
129
130        public void setMessage(int resId, boolean important, Object... formatArgs) {
131            if (resId != 0 && important) {
132                mMessageArea.mMessage = mMessageArea.getContext().getString(resId, formatArgs);
133                mMessageArea.securityMessageChanged();
134            }
135        }
136
137        @Override
138        public void showBouncer(int duration) {
139            mMessageArea.hideMessage(duration, false);
140            mMessageArea.mShowingBouncer = true;
141        }
142
143        @Override
144        public void hideBouncer(int duration) {
145            mMessageArea.showMessage(duration);
146            mMessageArea.mShowingBouncer = false;
147        }
148
149        @Override
150        public void setTimeout(int timeoutMs) {
151            mMessageArea.mTimeout = timeoutMs;
152        }
153    }
154
155    private KeyguardUpdateMonitorCallback mInfoCallback = new KeyguardUpdateMonitorCallback() {
156        @Override
157        public void onRefreshBatteryInfo(KeyguardUpdateMonitor.BatteryStatus status) {
158            mShowingBatteryInfo = status.isPluggedIn() || status.isBatteryLow();
159            mCharging = status.status == BatteryManager.BATTERY_STATUS_CHARGING
160                     || status.status == BatteryManager.BATTERY_STATUS_FULL;
161            mBatteryLevel = status.level;
162            mBatteryCharged = status.isCharged();
163            mBatteryIsLow = status.isBatteryLow();
164            update();
165        }
166        public void onScreenTurnedOff(int why) {
167            setSelected(false);
168        };
169        public void onScreenTurnedOn() {
170            setSelected(true);
171        };
172    };
173
174    public KeyguardMessageArea(Context context) {
175        this(context, null);
176    }
177
178    public KeyguardMessageArea(Context context, AttributeSet attrs) {
179        super(context, attrs);
180        setLayerType(LAYER_TYPE_HARDWARE, null); // work around nested unclipped SaveLayer bug
181
182        mLockPatternUtils = new LockPatternUtils(context);
183
184        // Registering this callback immediately updates the battery state, among other things.
185        mUpdateMonitor = KeyguardUpdateMonitor.getInstance(getContext());
186        mUpdateMonitor.registerCallback(mInfoCallback);
187        mHandler = new Handler(Looper.myLooper());
188
189        mSeparator = getResources().getString(R.string.kg_text_message_separator);
190
191        update();
192    }
193
194    @Override
195    protected void onFinishInflate() {
196        final boolean screenOn = KeyguardUpdateMonitor.getInstance(mContext).isScreenOn();
197        setSelected(screenOn); // This is required to ensure marquee works
198    }
199
200    public void securityMessageChanged() {
201        setAlpha(1f);
202        mShowingMessage = true;
203        update();
204        mHandler.removeCallbacks(mClearMessageRunnable);
205        if (mTimeout > 0) {
206            mHandler.postDelayed(mClearMessageRunnable, mTimeout);
207        }
208        mHandler.removeCallbacksAndMessages(ANNOUNCE_TOKEN);
209        mHandler.postAtTime(new AnnounceRunnable(this, getText()), ANNOUNCE_TOKEN,
210                (SystemClock.uptimeMillis() + ANNOUNCEMENT_DELAY));
211    }
212
213    /**
214     * Update the status lines based on these rules:
215     * AlarmStatus: Alarm state always gets it's own line.
216     * Status1 is shared between help, battery status and generic unlock instructions,
217     * prioritized in that order.
218     * @param showStatusLines status lines are shown if true
219     */
220    void update() {
221        MutableInt icon = new MutableInt(0);
222        CharSequence status = concat(getChargeInfo(icon), getOwnerInfo(), getCurrentMessage());
223        setCompoundDrawablesWithIntrinsicBounds(icon.value, 0, 0, 0);
224        setText(status);
225    }
226
227    private CharSequence concat(CharSequence... args) {
228        StringBuilder b = new StringBuilder();
229        if (!TextUtils.isEmpty(args[0])) {
230            b.append(args[0]);
231        }
232        for (int i = 1; i < args.length; i++) {
233            CharSequence text = args[i];
234            if (!TextUtils.isEmpty(text)) {
235                if (b.length() > 0) {
236                    b.append(mSeparator);
237                }
238                b.append(text);
239            }
240        }
241        return b.toString();
242    }
243
244    CharSequence getCurrentMessage() {
245        return mShowingMessage ? mMessage : null;
246    }
247
248    String getOwnerInfo() {
249        ContentResolver res = getContext().getContentResolver();
250        String info = null;
251        final boolean ownerInfoEnabled = mLockPatternUtils.isOwnerInfoEnabled();
252        if (ownerInfoEnabled && !mShowingMessage) {
253            info = mLockPatternUtils.getOwnerInfo(mLockPatternUtils.getCurrentUser());
254        }
255        return info;
256    }
257
258    private CharSequence getChargeInfo(MutableInt icon) {
259        CharSequence string = null;
260        if (mShowingBatteryInfo && !mShowingMessage) {
261            // Battery status
262            if (mCharging) {
263                // Charging, charged or waiting to charge.
264                string = getContext().getString(mBatteryCharged
265                        ? R.string.keyguard_charged
266                        : R.string.keyguard_plugged_in, mBatteryLevel);
267                icon.value = CHARGING_ICON;
268            } else if (mBatteryIsLow) {
269                // Battery is low
270                string = getContext().getString(R.string.keyguard_low_battery);
271                icon.value = BATTERY_LOW_ICON;
272            }
273        }
274        return string;
275    }
276
277    private void hideMessage(int duration, boolean thenUpdate) {
278        if (duration > 0) {
279            Animator anim = ObjectAnimator.ofFloat(this, "alpha", 0f);
280            anim.setDuration(duration);
281            if (thenUpdate) {
282                anim.addListener(new AnimatorListenerAdapter() {
283                        @Override
284                            public void onAnimationEnd(Animator animation) {
285                            update();
286                        }
287                });
288            }
289            anim.start();
290        } else {
291            setAlpha(0f);
292            if (thenUpdate) {
293                update();
294            }
295        }
296    }
297
298    private void showMessage(int duration) {
299        if (duration > 0) {
300            Animator anim = ObjectAnimator.ofFloat(this, "alpha", 1f);
301            anim.setDuration(duration);
302            anim.start();
303        } else {
304            setAlpha(1f);
305        }
306    }
307
308    /**
309     * Runnable used to delay accessibility announcements.
310     */
311    private static class AnnounceRunnable implements Runnable {
312        private final WeakReference<View> mHost;
313        private final CharSequence mTextToAnnounce;
314
315        public AnnounceRunnable(View host, CharSequence textToAnnounce) {
316            mHost = new WeakReference<View>(host);
317            mTextToAnnounce = textToAnnounce;
318        }
319
320        @Override
321        public void run() {
322            final View host = mHost.get();
323            if (host != null) {
324                host.announceForAccessibility(mTextToAnnounce);
325            }
326        }
327    }
328}
329