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 17 package com.android.keyguard; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.ObjectAnimator; 22 import android.content.ContentResolver; 23 import android.content.Context; 24 import android.os.BatteryManager; 25 import android.os.Handler; 26 import android.os.IBinder; 27 import android.os.Looper; 28 import android.os.RemoteException; 29 import android.os.ServiceManager; 30 import android.os.SystemClock; 31 import android.os.UserHandle; 32 import android.provider.Settings; 33 import android.text.TextUtils; 34 import android.util.AttributeSet; 35 import android.util.Slog; 36 import android.view.View; 37 import android.widget.TextView; 38 39 import libcore.util.MutableInt; 40 41 import java.lang.ref.WeakReference; 42 43 import 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 */ 48 class 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