diff options
author | Jason Monk <jmonk@google.com> | 2017-01-31 14:29:32 -0500 |
---|---|---|
committer | Jason Monk <jmonk@google.com> | 2017-02-10 07:45:58 -0800 |
commit | 23f85ec14dab49b2c525dc266d2a1f74f7f9d07c (patch) | |
tree | f7543adafe98131df4e94785f26f3cb3dab1ba16 /packages/SystemUI/src | |
parent | efdb4289597ad1594eb906aeafd2ebdf8854bdc7 (diff) |
Move Keyguard to SystemUI
Test: make
Change-Id: I3abb67e2b022737d2aa0226bb07f3966ad68fff7
Diffstat (limited to 'packages/SystemUI/src')
33 files changed, 8784 insertions, 0 deletions
diff --git a/packages/SystemUI/src/com/android/keyguard/AlphaOptimizedImageButton.java b/packages/SystemUI/src/com/android/keyguard/AlphaOptimizedImageButton.java new file mode 100644 index 000000000000..58c79b41df3e --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/AlphaOptimizedImageButton.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2014 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.content.Context; +import android.util.AttributeSet; +import android.widget.ImageButton; + +/** + * A frame layout which does not have overlapping renderings commands and therefore does not need a + * layer when alpha is changed. + */ +public class AlphaOptimizedImageButton extends ImageButton { + + public AlphaOptimizedImageButton(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/AlphaOptimizedLinearLayout.java b/packages/SystemUI/src/com/android/keyguard/AlphaOptimizedLinearLayout.java new file mode 100644 index 000000000000..2c6c4fa341ca --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/AlphaOptimizedLinearLayout.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2014 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.content.Context; +import android.util.AttributeSet; +import android.widget.LinearLayout; + +/** + * A linear layout which does not have overlapping renderings commands and therefore does not need a + * layer when alpha is changed. + */ +public class AlphaOptimizedLinearLayout extends LinearLayout +{ + public AlphaOptimizedLinearLayout(Context context) { + super(context); + } + + public AlphaOptimizedLinearLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public AlphaOptimizedLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public AlphaOptimizedLinearLayout(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/AlphaOptimizedRelativeLayout.java b/packages/SystemUI/src/com/android/keyguard/AlphaOptimizedRelativeLayout.java new file mode 100644 index 000000000000..200b1162655e --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/AlphaOptimizedRelativeLayout.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2014 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.content.Context; +import android.util.AttributeSet; +import android.widget.RelativeLayout; + +/** + * A frame layout which does not have overlapping renderings commands and therefore does not need a + * layer when alpha is changed. + */ +public class AlphaOptimizedRelativeLayout extends RelativeLayout { + + public AlphaOptimizedRelativeLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/CarrierText.java b/packages/SystemUI/src/com/android/keyguard/CarrierText.java new file mode 100644 index 000000000000..159ac4cc6cbd --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/CarrierText.java @@ -0,0 +1,390 @@ +/* + * 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 java.util.List; +import java.util.Locale; +import java.util.Objects; + +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.TypedArray; +import android.net.ConnectivityManager; +import android.net.wifi.WifiManager; +import android.telephony.ServiceState; +import android.telephony.SubscriptionInfo; +import android.text.TextUtils; +import android.text.method.SingleLineTransformationMethod; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.TextView; + +import com.android.internal.telephony.IccCardConstants; +import com.android.internal.telephony.IccCardConstants.State; +import com.android.internal.telephony.TelephonyIntents; +import com.android.settingslib.WirelessUtils; + +public class CarrierText extends TextView { + private static final boolean DEBUG = KeyguardConstants.DEBUG; + private static final String TAG = "CarrierText"; + + private static CharSequence mSeparator; + + private final boolean mIsEmergencyCallCapable; + + private KeyguardUpdateMonitor mKeyguardUpdateMonitor; + + private WifiManager mWifiManager; + + private KeyguardUpdateMonitorCallback mCallback = new KeyguardUpdateMonitorCallback() { + @Override + public void onRefreshCarrierInfo() { + updateCarrierText(); + } + + public void onFinishedGoingToSleep(int why) { + setSelected(false); + }; + + public void onStartedWakingUp() { + setSelected(true); + }; + }; + /** + * The status of this lock screen. Primarily used for widgets on LockScreen. + */ + private static enum StatusMode { + Normal, // Normal case (sim card present, it's not locked) + NetworkLocked, // SIM card is 'network locked'. + SimMissing, // SIM card is missing. + SimMissingLocked, // SIM card is missing, and device isn't provisioned; don't allow access + SimPukLocked, // SIM card is PUK locked because SIM entered wrong too many times + SimLocked, // SIM card is currently locked + SimPermDisabled, // SIM card is permanently disabled due to PUK unlock failure + SimNotReady; // SIM is not ready yet. May never be on devices w/o a SIM. + } + + public CarrierText(Context context) { + this(context, null); + } + + public CarrierText(Context context, AttributeSet attrs) { + super(context, attrs); + mIsEmergencyCallCapable = context.getResources().getBoolean( + com.android.internal.R.bool.config_voice_capable); + boolean useAllCaps; + TypedArray a = context.getTheme().obtainStyledAttributes( + attrs, R.styleable.CarrierText, 0, 0); + try { + useAllCaps = a.getBoolean(R.styleable.CarrierText_allCaps, false); + } finally { + a.recycle(); + } + setTransformationMethod(new CarrierTextTransformationMethod(mContext, useAllCaps)); + + mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + } + + protected void updateCarrierText() { + boolean allSimsMissing = true; + boolean anySimReadyAndInService = false; + CharSequence displayText = null; + + List<SubscriptionInfo> subs = mKeyguardUpdateMonitor.getSubscriptionInfo(false); + final int N = subs.size(); + if (DEBUG) Log.d(TAG, "updateCarrierText(): " + N); + for (int i = 0; i < N; i++) { + int subId = subs.get(i).getSubscriptionId(); + State simState = mKeyguardUpdateMonitor.getSimState(subId); + CharSequence carrierName = subs.get(i).getCarrierName(); + CharSequence carrierTextForSimState = getCarrierTextForSimState(simState, carrierName); + if (DEBUG) { + Log.d(TAG, "Handling (subId=" + subId + "): " + simState + " " + carrierName); + } + if (carrierTextForSimState != null) { + allSimsMissing = false; + displayText = concatenate(displayText, carrierTextForSimState); + } + if (simState == IccCardConstants.State.READY) { + ServiceState ss = mKeyguardUpdateMonitor.mServiceStates.get(subId); + if (ss != null && ss.getDataRegState() == ServiceState.STATE_IN_SERVICE) { + // hack for WFC (IWLAN) not turning off immediately once + // Wi-Fi is disassociated or disabled + if (ss.getRilDataRadioTechnology() != ServiceState.RIL_RADIO_TECHNOLOGY_IWLAN + || (mWifiManager.isWifiEnabled() + && mWifiManager.getConnectionInfo() != null + && mWifiManager.getConnectionInfo().getBSSID() != null)) { + if (DEBUG) { + Log.d(TAG, "SIM ready and in service: subId=" + subId + ", ss=" + ss); + } + anySimReadyAndInService = true; + } + } + } + } + if (allSimsMissing) { + if (N != 0) { + // Shows "No SIM card | Emergency calls only" on devices that are voice-capable. + // This depends on mPlmn containing the text "Emergency calls only" when the radio + // has some connectivity. Otherwise, it should be null or empty and just show + // "No SIM card" + // Grab the first subscripton, because they all should contain the emergency text, + // described above. + displayText = makeCarrierStringOnEmergencyCapable( + getContext().getText(R.string.keyguard_missing_sim_message_short), + subs.get(0).getCarrierName()); + } else { + // We don't have a SubscriptionInfo to get the emergency calls only from. + // Grab it from the old sticky broadcast if possible instead. We can use it + // here because no subscriptions are active, so we don't have + // to worry about MSIM clashing. + CharSequence text = + getContext().getText(com.android.internal.R.string.emergency_calls_only); + Intent i = getContext().registerReceiver(null, + new IntentFilter(TelephonyIntents.SPN_STRINGS_UPDATED_ACTION)); + if (i != null) { + String spn = ""; + String plmn = ""; + if (i.getBooleanExtra(TelephonyIntents.EXTRA_SHOW_SPN, false)) { + spn = i.getStringExtra(TelephonyIntents.EXTRA_SPN); + } + if (i.getBooleanExtra(TelephonyIntents.EXTRA_SHOW_PLMN, false)) { + plmn = i.getStringExtra(TelephonyIntents.EXTRA_PLMN); + } + if (DEBUG) Log.d(TAG, "Getting plmn/spn sticky brdcst " + plmn + "/" + spn); + if (Objects.equals(plmn, spn)) { + text = plmn; + } else { + text = concatenate(plmn, spn); + } + } + displayText = makeCarrierStringOnEmergencyCapable( + getContext().getText(R.string.keyguard_missing_sim_message_short), text); + } + } + + // APM (airplane mode) != no carrier state. There are carrier services + // (e.g. WFC = Wi-Fi calling) which may operate in APM. + if (!anySimReadyAndInService && WirelessUtils.isAirplaneModeOn(mContext)) { + displayText = getContext().getString(R.string.airplane_mode); + } + setText(displayText); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mSeparator = getResources().getString( + com.android.internal.R.string.kg_text_message_separator); + boolean shouldMarquee = KeyguardUpdateMonitor.getInstance(mContext).isDeviceInteractive(); + setSelected(shouldMarquee); // Allow marquee to work. + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (ConnectivityManager.from(mContext).isNetworkSupported( + ConnectivityManager.TYPE_MOBILE)) { + mKeyguardUpdateMonitor = KeyguardUpdateMonitor.getInstance(mContext); + mKeyguardUpdateMonitor.registerCallback(mCallback); + } else { + // Don't listen and clear out the text when the device isn't a phone. + mKeyguardUpdateMonitor = null; + setText(""); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (mKeyguardUpdateMonitor != null) { + mKeyguardUpdateMonitor.removeCallback(mCallback); + } + } + + /** + * Top-level function for creating carrier text. Makes text based on simState, PLMN + * and SPN as well as device capabilities, such as being emergency call capable. + * + * @param simState + * @param text + * @param spn + * @return Carrier text if not in missing state, null otherwise. + */ + private CharSequence getCarrierTextForSimState(IccCardConstants.State simState, + CharSequence text) { + CharSequence carrierText = null; + StatusMode status = getStatusForIccState(simState); + switch (status) { + case Normal: + carrierText = text; + break; + + case SimNotReady: + // Null is reserved for denoting missing, in this case we have nothing to display. + carrierText = ""; // nothing to display yet. + break; + + case NetworkLocked: + carrierText = makeCarrierStringOnEmergencyCapable( + mContext.getText(R.string.keyguard_network_locked_message), text); + break; + + case SimMissing: + carrierText = null; + break; + + case SimPermDisabled: + carrierText = getContext().getText( + R.string.keyguard_permanent_disabled_sim_message_short); + break; + + case SimMissingLocked: + carrierText = null; + break; + + case SimLocked: + carrierText = makeCarrierStringOnEmergencyCapable( + getContext().getText(R.string.keyguard_sim_locked_message), + text); + break; + + case SimPukLocked: + carrierText = makeCarrierStringOnEmergencyCapable( + getContext().getText(R.string.keyguard_sim_puk_locked_message), + text); + break; + } + + return carrierText; + } + + /* + * Add emergencyCallMessage to carrier string only if phone supports emergency calls. + */ + private CharSequence makeCarrierStringOnEmergencyCapable( + CharSequence simMessage, CharSequence emergencyCallMessage) { + if (mIsEmergencyCallCapable) { + return concatenate(simMessage, emergencyCallMessage); + } + return simMessage; + } + + /** + * Determine the current status of the lock screen given the SIM state and other stuff. + */ + private StatusMode getStatusForIccState(IccCardConstants.State simState) { + // Since reading the SIM may take a while, we assume it is present until told otherwise. + if (simState == null) { + return StatusMode.Normal; + } + + final boolean missingAndNotProvisioned = + !KeyguardUpdateMonitor.getInstance(mContext).isDeviceProvisioned() + && (simState == IccCardConstants.State.ABSENT || + simState == IccCardConstants.State.PERM_DISABLED); + + // Assume we're NETWORK_LOCKED if not provisioned + simState = missingAndNotProvisioned ? IccCardConstants.State.NETWORK_LOCKED : simState; + switch (simState) { + case ABSENT: + return StatusMode.SimMissing; + case NETWORK_LOCKED: + return StatusMode.SimMissingLocked; + case NOT_READY: + return StatusMode.SimNotReady; + case PIN_REQUIRED: + return StatusMode.SimLocked; + case PUK_REQUIRED: + return StatusMode.SimPukLocked; + case READY: + return StatusMode.Normal; + case PERM_DISABLED: + return StatusMode.SimPermDisabled; + case UNKNOWN: + return StatusMode.SimMissing; + } + return StatusMode.SimMissing; + } + + private static CharSequence concatenate(CharSequence plmn, CharSequence spn) { + final boolean plmnValid = !TextUtils.isEmpty(plmn); + final boolean spnValid = !TextUtils.isEmpty(spn); + if (plmnValid && spnValid) { + return new StringBuilder().append(plmn).append(mSeparator).append(spn).toString(); + } else if (plmnValid) { + return plmn; + } else if (spnValid) { + return spn; + } else { + return ""; + } + } + + private CharSequence getCarrierHelpTextForSimState(IccCardConstants.State simState, + String plmn, String spn) { + int carrierHelpTextId = 0; + StatusMode status = getStatusForIccState(simState); + switch (status) { + case NetworkLocked: + carrierHelpTextId = R.string.keyguard_instructions_when_pattern_disabled; + break; + + case SimMissing: + carrierHelpTextId = R.string.keyguard_missing_sim_instructions_long; + break; + + case SimPermDisabled: + carrierHelpTextId = R.string.keyguard_permanent_disabled_sim_instructions; + break; + + case SimMissingLocked: + carrierHelpTextId = R.string.keyguard_missing_sim_instructions; + break; + + case Normal: + case SimLocked: + case SimPukLocked: + break; + } + + return mContext.getText(carrierHelpTextId); + } + + private class CarrierTextTransformationMethod extends SingleLineTransformationMethod { + private final Locale mLocale; + private final boolean mAllCaps; + + public CarrierTextTransformationMethod(Context context, boolean allCaps) { + mLocale = context.getResources().getConfiguration().locale; + mAllCaps = allCaps; + } + + @Override + public CharSequence getTransformation(CharSequence source, View view) { + source = super.getTransformation(source, view); + + if (mAllCaps && source != null) { + source = source.toString().toUpperCase(mLocale); + } + + return source; + } + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/EmergencyButton.java b/packages/SystemUI/src/com/android/keyguard/EmergencyButton.java new file mode 100644 index 000000000000..e3ab05d81de7 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/EmergencyButton.java @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2008 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.app.ActivityManager; +import android.app.ActivityOptions; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.os.PowerManager; +import android.os.RemoteException; +import android.os.SystemClock; +import android.os.UserHandle; +import android.telecom.TelecomManager; +import android.util.AttributeSet; +import android.util.Slog; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.widget.Button; + +import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.internal.telephony.IccCardConstants.State; +import com.android.internal.widget.LockPatternUtils; +import com.android.internal.policy.EmergencyAffordanceManager; + +/** + * This class implements a smart emergency button that updates itself based + * on telephony state. When the phone is idle, it is an emergency call button. + * When there's a call in progress, it presents an appropriate message and + * allows the user to return to the call. + */ +public class EmergencyButton extends Button { + private static final Intent INTENT_EMERGENCY_DIAL = new Intent() + .setAction("com.android.phone.EmergencyDialer.DIAL") + .setPackage("com.android.phone") + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS + | Intent.FLAG_ACTIVITY_CLEAR_TOP); + + private static final String LOG_TAG = "EmergencyButton"; + private final EmergencyAffordanceManager mEmergencyAffordanceManager; + + private int mDownX; + private int mDownY; + KeyguardUpdateMonitorCallback mInfoCallback = new KeyguardUpdateMonitorCallback() { + + @Override + public void onSimStateChanged(int subId, int slotId, State simState) { + updateEmergencyCallButton(); + } + + @Override + public void onPhoneStateChanged(int phoneState) { + updateEmergencyCallButton(); + } + }; + private boolean mLongPressWasDragged; + + public interface EmergencyButtonCallback { + public void onEmergencyButtonClickedWhenInCall(); + } + + private LockPatternUtils mLockPatternUtils; + private PowerManager mPowerManager; + private EmergencyButtonCallback mEmergencyButtonCallback; + + private final boolean mIsVoiceCapable; + private final boolean mEnableEmergencyCallWhileSimLocked; + + public EmergencyButton(Context context) { + this(context, null); + } + + public EmergencyButton(Context context, AttributeSet attrs) { + super(context, attrs); + mIsVoiceCapable = context.getResources().getBoolean( + com.android.internal.R.bool.config_voice_capable); + mEnableEmergencyCallWhileSimLocked = mContext.getResources().getBoolean( + com.android.internal.R.bool.config_enable_emergency_call_while_sim_locked); + mEmergencyAffordanceManager = new EmergencyAffordanceManager(context); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + KeyguardUpdateMonitor.getInstance(mContext).registerCallback(mInfoCallback); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + KeyguardUpdateMonitor.getInstance(mContext).removeCallback(mInfoCallback); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mLockPatternUtils = new LockPatternUtils(mContext); + mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); + setOnClickListener(new OnClickListener() { + public void onClick(View v) { + takeEmergencyCallAction(); + } + }); + setOnLongClickListener(new OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + if (!mLongPressWasDragged + && mEmergencyAffordanceManager.needsEmergencyAffordance()) { + mEmergencyAffordanceManager.performEmergencyCall(); + return true; + } + return false; + } + }); + updateEmergencyCallButton(); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + final int x = (int) event.getX(); + final int y = (int) event.getY(); + if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { + mDownX = x; + mDownY = y; + mLongPressWasDragged = false; + } else { + final int xDiff = Math.abs(x - mDownX); + final int yDiff = Math.abs(y - mDownY); + int touchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop(); + if (Math.abs(yDiff) > touchSlop || Math.abs(xDiff) > touchSlop) { + mLongPressWasDragged = true; + } + } + return super.onTouchEvent(event); + } + + @Override + public boolean performLongClick() { + return super.performLongClick(); + } + + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + updateEmergencyCallButton(); + } + + /** + * Shows the emergency dialer or returns the user to the existing call. + */ + public void takeEmergencyCallAction() { + MetricsLogger.action(mContext, MetricsEvent.ACTION_EMERGENCY_CALL); + // TODO: implement a shorter timeout once new PowerManager API is ready. + // should be the equivalent to the old userActivity(EMERGENCY_CALL_TIMEOUT) + mPowerManager.userActivity(SystemClock.uptimeMillis(), true); + try { + ActivityManager.getService().stopSystemLockTaskMode(); + } catch (RemoteException e) { + Slog.w(LOG_TAG, "Failed to stop app pinning"); + } + if (isInCall()) { + resumeCall(); + if (mEmergencyButtonCallback != null) { + mEmergencyButtonCallback.onEmergencyButtonClickedWhenInCall(); + } + } else { + KeyguardUpdateMonitor.getInstance(mContext).reportEmergencyCallAction( + true /* bypassHandler */); + getContext().startActivityAsUser(INTENT_EMERGENCY_DIAL, + ActivityOptions.makeCustomAnimation(getContext(), 0, 0).toBundle(), + new UserHandle(KeyguardUpdateMonitor.getCurrentUser())); + } + } + + private void updateEmergencyCallButton() { + boolean visible = false; + if (mIsVoiceCapable) { + // Emergency calling requires voice capability. + if (isInCall()) { + visible = true; // always show "return to call" if phone is off-hook + } else { + final boolean simLocked = KeyguardUpdateMonitor.getInstance(mContext) + .isSimPinVoiceSecure(); + if (simLocked) { + // Some countries can't handle emergency calls while SIM is locked. + visible = mEnableEmergencyCallWhileSimLocked; + } else { + // Only show if there is a secure screen (pin/pattern/SIM pin/SIM puk); + visible = mLockPatternUtils.isSecure(KeyguardUpdateMonitor.getCurrentUser()); + } + } + } + if (visible) { + setVisibility(View.VISIBLE); + + int textId; + if (isInCall()) { + textId = com.android.internal.R.string.lockscreen_return_to_call; + } else { + textId = com.android.internal.R.string.lockscreen_emergency_call; + } + setText(textId); + } else { + setVisibility(View.GONE); + } + } + + public void setCallback(EmergencyButtonCallback callback) { + mEmergencyButtonCallback = callback; + } + + /** + * Resumes a call in progress. + */ + private void resumeCall() { + getTelecommManager().showInCallScreen(false); + } + + /** + * @return {@code true} if there is a call currently in progress. + */ + private boolean isInCall() { + return getTelecommManager().isInCall(); + } + + private TelecomManager getTelecommManager() { + return (TelecomManager) mContext.getSystemService(Context.TELECOM_SERVICE); + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/EmergencyCarrierArea.java b/packages/SystemUI/src/com/android/keyguard/EmergencyCarrierArea.java new file mode 100644 index 000000000000..0a89d9b15e26 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/EmergencyCarrierArea.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2013 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.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; + +public class EmergencyCarrierArea extends AlphaOptimizedLinearLayout { + + private CarrierText mCarrierText; + private EmergencyButton mEmergencyButton; + + public EmergencyCarrierArea(Context context) { + super(context); + } + + public EmergencyCarrierArea(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mCarrierText = (CarrierText) findViewById(R.id.carrier_text); + mEmergencyButton = (EmergencyButton) findViewById(R.id.emergency_call_button); + + // The emergency button overlaps the carrier text, only noticeable when highlighted. + // So temporarily hide the carrier text while the emergency button is pressed. + mEmergencyButton.setOnTouchListener(new OnTouchListener(){ + @Override + public boolean onTouch(View v, MotionEvent event) { + if (mCarrierText.getVisibility() != View.VISIBLE) return false; + switch(event.getAction()) { + case MotionEvent.ACTION_DOWN: + mCarrierText.animate().alpha(0); + break; + case MotionEvent.ACTION_UP: + mCarrierText.animate().alpha(1); + break; + } + return false; + }}); + } + + public void setCarrierTextVisible(boolean visible) { + mCarrierText.setVisibility(visible ? View.VISIBLE : View.GONE); + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputView.java new file mode 100644 index 000000000000..5aa673b40124 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputView.java @@ -0,0 +1,310 @@ +/* + * 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 static com.android.keyguard.LatencyTracker.ACTION_CHECK_CREDENTIAL; +import static com.android.keyguard.LatencyTracker.ACTION_CHECK_CREDENTIAL_UNLOCKED; + +import android.content.Context; +import android.os.AsyncTask; +import android.os.CountDownTimer; +import android.os.SystemClock; +import android.util.AttributeSet; +import android.view.HapticFeedbackConstants; +import android.view.KeyEvent; +import android.view.View; +import android.widget.LinearLayout; + +import com.android.internal.widget.LockPatternChecker; +import com.android.internal.widget.LockPatternUtils; + +/** + * Base class for PIN and password unlock screens. + */ +public abstract class KeyguardAbsKeyInputView extends LinearLayout + implements KeyguardSecurityView, EmergencyButton.EmergencyButtonCallback { + protected KeyguardSecurityCallback mCallback; + protected LockPatternUtils mLockPatternUtils; + protected AsyncTask<?, ?, ?> mPendingLockCheck; + protected SecurityMessageDisplay mSecurityMessageDisplay; + protected View mEcaView; + protected boolean mEnableHaptics; + private boolean mDismissing; + private CountDownTimer mCountdownTimer = null; + + // To avoid accidental lockout due to events while the device in in the pocket, ignore + // any passwords with length less than or equal to this length. + protected static final int MINIMUM_PASSWORD_LENGTH_BEFORE_REPORT = 3; + + public KeyguardAbsKeyInputView(Context context) { + this(context, null); + } + + public KeyguardAbsKeyInputView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void setKeyguardCallback(KeyguardSecurityCallback callback) { + mCallback = callback; + } + + @Override + public void setLockPatternUtils(LockPatternUtils utils) { + mLockPatternUtils = utils; + mEnableHaptics = mLockPatternUtils.isTactileFeedbackEnabled(); + } + + @Override + public void reset() { + // start fresh + mDismissing = false; + resetPasswordText(false /* animate */, false /* announce */); + // if the user is currently locked out, enforce it. + long deadline = mLockPatternUtils.getLockoutAttemptDeadline( + KeyguardUpdateMonitor.getCurrentUser()); + if (shouldLockout(deadline)) { + handleAttemptLockout(deadline); + } else { + resetState(); + } + } + + // Allow subclasses to override this behavior + protected boolean shouldLockout(long deadline) { + return deadline != 0; + } + + protected abstract int getPasswordTextViewId(); + protected abstract void resetState(); + + @Override + protected void onFinishInflate() { + mLockPatternUtils = new LockPatternUtils(mContext); + mSecurityMessageDisplay = KeyguardMessageArea.findSecurityMessageDisplay(this); + mEcaView = findViewById(R.id.keyguard_selector_fade_container); + + EmergencyButton button = (EmergencyButton) findViewById(R.id.emergency_call_button); + if (button != null) { + button.setCallback(this); + } + } + + @Override + public void onEmergencyButtonClickedWhenInCall() { + mCallback.reset(); + } + + /* + * Override this if you have a different string for "wrong password" + * + * Note that PIN/PUK have their own implementation of verifyPasswordAndUnlock and so don't need this + */ + protected int getWrongPasswordStringId() { + return R.string.kg_wrong_password; + } + + protected void verifyPasswordAndUnlock() { + if (mDismissing) return; // already verified but haven't been dismissed; don't do it again. + + final String entry = getPasswordText(); + setPasswordEntryInputEnabled(false); + if (mPendingLockCheck != null) { + mPendingLockCheck.cancel(false); + } + + final int userId = KeyguardUpdateMonitor.getCurrentUser(); + if (entry.length() <= MINIMUM_PASSWORD_LENGTH_BEFORE_REPORT) { + // to avoid accidental lockout, only count attempts that are long enough to be a + // real password. This may require some tweaking. + setPasswordEntryInputEnabled(true); + onPasswordChecked(userId, false /* matched */, 0, false /* not valid - too short */); + return; + } + + if (LatencyTracker.isEnabled(mContext)) { + LatencyTracker.getInstance(mContext).onActionStart(ACTION_CHECK_CREDENTIAL); + LatencyTracker.getInstance(mContext).onActionStart(ACTION_CHECK_CREDENTIAL_UNLOCKED); + } + mPendingLockCheck = LockPatternChecker.checkPassword( + mLockPatternUtils, + entry, + userId, + new LockPatternChecker.OnCheckCallback() { + + @Override + public void onEarlyMatched() { + if (LatencyTracker.isEnabled(mContext)) { + LatencyTracker.getInstance(mContext).onActionEnd( + ACTION_CHECK_CREDENTIAL); + } + onPasswordChecked(userId, true /* matched */, 0 /* timeoutMs */, + true /* isValidPassword */); + } + + @Override + public void onChecked(boolean matched, int timeoutMs) { + if (LatencyTracker.isEnabled(mContext)) { + LatencyTracker.getInstance(mContext).onActionEnd( + ACTION_CHECK_CREDENTIAL_UNLOCKED); + } + setPasswordEntryInputEnabled(true); + mPendingLockCheck = null; + if (!matched) { + onPasswordChecked(userId, false /* matched */, timeoutMs, + true /* isValidPassword */); + } + } + + @Override + public void onCancelled() { + // We already got dismissed with the early matched callback, so we cancelled + // the check. However, we still need to note down the latency. + if (LatencyTracker.isEnabled(mContext)) { + LatencyTracker.getInstance(mContext).onActionEnd( + ACTION_CHECK_CREDENTIAL_UNLOCKED); + } + } + }); + } + + private void onPasswordChecked(int userId, boolean matched, int timeoutMs, + boolean isValidPassword) { + boolean dismissKeyguard = KeyguardUpdateMonitor.getCurrentUser() == userId; + if (matched) { + mCallback.reportUnlockAttempt(userId, true, 0); + if (dismissKeyguard) { + mDismissing = true; + mCallback.dismiss(true, userId); + } + } else { + if (isValidPassword) { + mCallback.reportUnlockAttempt(userId, false, timeoutMs); + if (timeoutMs > 0) { + long deadline = mLockPatternUtils.setLockoutAttemptDeadline( + userId, timeoutMs); + handleAttemptLockout(deadline); + } + } + if (timeoutMs == 0) { + mSecurityMessageDisplay.setMessage(getWrongPasswordStringId()); + } + } + resetPasswordText(true /* animate */, !matched /* announce deletion if no match */); + } + + protected abstract void resetPasswordText(boolean animate, boolean announce); + protected abstract String getPasswordText(); + protected abstract void setPasswordEntryEnabled(boolean enabled); + protected abstract void setPasswordEntryInputEnabled(boolean enabled); + + // Prevent user from using the PIN/Password entry until scheduled deadline. + protected void handleAttemptLockout(long elapsedRealtimeDeadline) { + setPasswordEntryEnabled(false); + long elapsedRealtime = SystemClock.elapsedRealtime(); + long secondsInFuture = (long) Math.ceil( + (elapsedRealtimeDeadline - elapsedRealtime) / 1000.0); + mCountdownTimer = new CountDownTimer(secondsInFuture * 1000, 1000) { + + @Override + public void onTick(long millisUntilFinished) { + int secondsRemaining = (int) Math.round(millisUntilFinished / 1000.0); + mSecurityMessageDisplay.formatMessage( + R.string.kg_too_many_failed_attempts_countdown, secondsRemaining); + } + + @Override + public void onFinish() { + mSecurityMessageDisplay.setMessage(""); + resetState(); + } + }.start(); + } + + protected void onUserInput() { + if (mCallback != null) { + mCallback.userActivity(); + } + mSecurityMessageDisplay.setMessage(""); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + onUserInput(); + return false; + } + + @Override + public boolean needsInput() { + return false; + } + + @Override + public void onPause() { + if (mCountdownTimer != null) { + mCountdownTimer.cancel(); + mCountdownTimer = null; + } + if (mPendingLockCheck != null) { + mPendingLockCheck.cancel(false); + mPendingLockCheck = null; + } + } + + @Override + public void onResume(int reason) { + reset(); + } + + @Override + public KeyguardSecurityCallback getCallback() { + return mCallback; + } + + @Override + public void showPromptReason(int reason) { + if (reason != PROMPT_REASON_NONE) { + int promtReasonStringRes = getPromtReasonStringRes(reason); + if (promtReasonStringRes != 0) { + mSecurityMessageDisplay.setMessage(promtReasonStringRes); + } + } + } + + @Override + public void showMessage(String message, int color) { + mSecurityMessageDisplay.setNextMessageColor(color); + mSecurityMessageDisplay.setMessage(message); + } + + protected abstract int getPromtReasonStringRes(int reason); + + // Cause a VIRTUAL_KEY vibration + public void doHapticKeyClick() { + if (mEnableHaptics) { + performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, + HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING + | HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING); + } + } + + @Override + public boolean startDisappearAnimation(Runnable finishRunnable) { + return false; + } +} + diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardConstants.java b/packages/SystemUI/src/com/android/keyguard/KeyguardConstants.java new file mode 100644 index 000000000000..39271224d943 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardConstants.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2014 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; + +/** + * Defines constants for the Keyguard. + */ +public class KeyguardConstants { + + /** + * Turns on debugging information for the whole Keyguard. This is very verbose and should only + * be used temporarily for debugging. + */ + public static final boolean DEBUG = false; + public static final boolean DEBUG_SIM_STATES = false; + public static final boolean DEBUG_FP_WAKELOCK = true; +} diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java b/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java new file mode 100644 index 000000000000..8de1d317c5ed --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2013 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.app.Presentation; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnDismissListener; +import android.graphics.Point; +import android.media.MediaRouter; +import android.media.MediaRouter.RouteInfo; +import android.os.Bundle; +import android.util.Slog; +import android.view.Display; +import android.view.View; +import android.view.WindowManager; + +public class KeyguardDisplayManager { + protected static final String TAG = "KeyguardDisplayManager"; + private static boolean DEBUG = KeyguardConstants.DEBUG; + Presentation mPresentation; + private MediaRouter mMediaRouter; + private Context mContext; + private boolean mShowing; + + public KeyguardDisplayManager(Context context) { + mContext = context; + mMediaRouter = (MediaRouter) mContext.getSystemService(Context.MEDIA_ROUTER_SERVICE); + } + + public void show() { + if (!mShowing) { + if (DEBUG) Slog.v(TAG, "show"); + mMediaRouter.addCallback(MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY, + mMediaRouterCallback, MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY); + updateDisplays(true); + } + mShowing = true; + } + + public void hide() { + if (mShowing) { + if (DEBUG) Slog.v(TAG, "hide"); + mMediaRouter.removeCallback(mMediaRouterCallback); + updateDisplays(false); + } + mShowing = false; + } + + private final MediaRouter.SimpleCallback mMediaRouterCallback = + new MediaRouter.SimpleCallback() { + @Override + public void onRouteSelected(MediaRouter router, int type, RouteInfo info) { + if (DEBUG) Slog.d(TAG, "onRouteSelected: type=" + type + ", info=" + info); + updateDisplays(mShowing); + } + + @Override + public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) { + if (DEBUG) Slog.d(TAG, "onRouteUnselected: type=" + type + ", info=" + info); + updateDisplays(mShowing); + } + + @Override + public void onRoutePresentationDisplayChanged(MediaRouter router, RouteInfo info) { + if (DEBUG) Slog.d(TAG, "onRoutePresentationDisplayChanged: info=" + info); + updateDisplays(mShowing); + } + }; + + private OnDismissListener mOnDismissListener = new OnDismissListener() { + + @Override + public void onDismiss(DialogInterface dialog) { + mPresentation = null; + } + }; + + protected void updateDisplays(boolean showing) { + if (showing) { + MediaRouter.RouteInfo route = mMediaRouter.getSelectedRoute( + MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY); + boolean useDisplay = route != null + && route.getPlaybackType() == MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE; + Display presentationDisplay = useDisplay ? route.getPresentationDisplay() : null; + + if (mPresentation != null && mPresentation.getDisplay() != presentationDisplay) { + if (DEBUG) Slog.v(TAG, "Display gone: " + mPresentation.getDisplay()); + mPresentation.dismiss(); + mPresentation = null; + } + + if (mPresentation == null && presentationDisplay != null) { + if (DEBUG) Slog.i(TAG, "Keyguard enabled on display: " + presentationDisplay); + mPresentation = new KeyguardPresentation(mContext, presentationDisplay, + R.style.keyguard_presentation_theme); + mPresentation.setOnDismissListener(mOnDismissListener); + try { + mPresentation.show(); + } catch (WindowManager.InvalidDisplayException ex) { + Slog.w(TAG, "Invalid display:", ex); + mPresentation = null; + } + } + } else { + if (mPresentation != null) { + mPresentation.dismiss(); + mPresentation = null; + } + } + } + + private final static class KeyguardPresentation extends Presentation { + private static final int VIDEO_SAFE_REGION = 80; // Percentage of display width & height + private static final int MOVE_CLOCK_TIMEOUT = 10000; // 10s + private View mClock; + private int mUsableWidth; + private int mUsableHeight; + private int mMarginTop; + private int mMarginLeft; + Runnable mMoveTextRunnable = new Runnable() { + @Override + public void run() { + int x = mMarginLeft + (int) (Math.random() * (mUsableWidth - mClock.getWidth())); + int y = mMarginTop + (int) (Math.random() * (mUsableHeight - mClock.getHeight())); + mClock.setTranslationX(x); + mClock.setTranslationY(y); + mClock.postDelayed(mMoveTextRunnable, MOVE_CLOCK_TIMEOUT); + } + }; + + public KeyguardPresentation(Context context, Display display, int theme) { + super(context, display, theme); + getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); + } + + @Override + public void onDetachedFromWindow() { + mClock.removeCallbacks(mMoveTextRunnable); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Point p = new Point(); + getDisplay().getSize(p); + mUsableWidth = VIDEO_SAFE_REGION * p.x/100; + mUsableHeight = VIDEO_SAFE_REGION * p.y/100; + mMarginLeft = (100 - VIDEO_SAFE_REGION) * p.x / 200; + mMarginTop = (100 - VIDEO_SAFE_REGION) * p.y / 200; + + setContentView(R.layout.keyguard_presentation); + mClock = findViewById(R.id.clock); + + // Avoid screen burn in + mClock.post(mMoveTextRunnable); + } + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardHostView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardHostView.java new file mode 100644 index 000000000000..dd5544d2cdad --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardHostView.java @@ -0,0 +1,443 @@ +/* + * Copyright (C) 2007 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.app.Activity; +import android.app.ActivityManager; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.media.AudioManager; +import android.os.SystemClock; +import android.service.trust.TrustAgentService; +import android.telephony.TelephonyManager; +import android.util.AttributeSet; +import android.util.Log; +import android.view.KeyEvent; +import android.view.accessibility.AccessibilityEvent; +import android.widget.FrameLayout; + +import com.android.internal.widget.LockPatternUtils; +import com.android.keyguard.KeyguardSecurityContainer.SecurityCallback; +import com.android.keyguard.KeyguardSecurityModel.SecurityMode; + +import java.io.File; + +/** + * Base class for keyguard view. {@link #reset} is where you should + * reset the state of your view. Use the {@link KeyguardViewCallback} via + * {@link #getCallback()} to send information back (such as poking the wake lock, + * or finishing the keyguard). + * + * Handles intercepting of media keys that still work when the keyguard is + * showing. + */ +public class KeyguardHostView extends FrameLayout implements SecurityCallback { + + public interface OnDismissAction { + /** + * @return true if the dismiss should be deferred + */ + boolean onDismiss(); + } + + private AudioManager mAudioManager; + private TelephonyManager mTelephonyManager = null; + protected ViewMediatorCallback mViewMediatorCallback; + protected LockPatternUtils mLockPatternUtils; + private OnDismissAction mDismissAction; + private Runnable mCancelAction; + + private final KeyguardUpdateMonitorCallback mUpdateCallback = + new KeyguardUpdateMonitorCallback() { + + @Override + public void onUserSwitchComplete(int userId) { + getSecurityContainer().showPrimarySecurityScreen(false /* turning off */); + } + + @Override + public void onTrustGrantedWithFlags(int flags, int userId) { + if (userId != KeyguardUpdateMonitor.getCurrentUser()) return; + if (!isAttachedToWindow()) return; + boolean bouncerVisible = isVisibleToUser(); + boolean initiatedByUser = + (flags & TrustAgentService.FLAG_GRANT_TRUST_INITIATED_BY_USER) != 0; + boolean dismissKeyguard = + (flags & TrustAgentService.FLAG_GRANT_TRUST_DISMISS_KEYGUARD) != 0; + + if (initiatedByUser || dismissKeyguard) { + if (mViewMediatorCallback.isScreenOn() && (bouncerVisible || dismissKeyguard)) { + if (!bouncerVisible) { + // The trust agent dismissed the keyguard without the user proving + // that they are present (by swiping up to show the bouncer). That's fine if + // the user proved presence via some other way to the trust agent. + Log.i(TAG, "TrustAgent dismissed Keyguard."); + } + dismiss(false /* authenticated */, userId); + } else { + mViewMediatorCallback.playTrustedSound(); + } + } + } + }; + + // Whether the volume keys should be handled by keyguard. If true, then + // they will be handled here for specific media types such as music, otherwise + // the audio service will bring up the volume dialog. + private static final boolean KEYGUARD_MANAGES_VOLUME = false; + public static final boolean DEBUG = KeyguardConstants.DEBUG; + private static final String TAG = "KeyguardViewBase"; + + private KeyguardSecurityContainer mSecurityContainer; + + public KeyguardHostView(Context context) { + this(context, null); + } + + public KeyguardHostView(Context context, AttributeSet attrs) { + super(context, attrs); + KeyguardUpdateMonitor.getInstance(context).registerCallback(mUpdateCallback); + } + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + if (mViewMediatorCallback != null) { + mViewMediatorCallback.keyguardDoneDrawing(); + } + } + + /** + * Sets an action to run when keyguard finishes. + * + * @param action + */ + public void setOnDismissAction(OnDismissAction action, Runnable cancelAction) { + if (mCancelAction != null) { + mCancelAction.run(); + mCancelAction = null; + } + mDismissAction = action; + mCancelAction = cancelAction; + } + + public void cancelDismissAction() { + setOnDismissAction(null, null); + } + + @Override + protected void onFinishInflate() { + mSecurityContainer = + (KeyguardSecurityContainer) findViewById(R.id.keyguard_security_container); + mLockPatternUtils = new LockPatternUtils(mContext); + mSecurityContainer.setLockPatternUtils(mLockPatternUtils); + mSecurityContainer.setSecurityCallback(this); + mSecurityContainer.showPrimarySecurityScreen(false); + // mSecurityContainer.updateSecurityViews(false /* not bouncing */); + } + + /** + * Called when the view needs to be shown. + */ + public void showPrimarySecurityScreen() { + if (DEBUG) Log.d(TAG, "show()"); + mSecurityContainer.showPrimarySecurityScreen(false); + } + + /** + * Show a string explaining why the security view needs to be solved. + * + * @param reason a flag indicating which string should be shown, see + * {@link KeyguardSecurityView#PROMPT_REASON_NONE}, + * {@link KeyguardSecurityView#PROMPT_REASON_RESTART} and + * {@link KeyguardSecurityView#PROMPT_REASON_TIMEOUT}. + */ + public void showPromptReason(int reason) { + mSecurityContainer.showPromptReason(reason); + } + + public void showMessage(String message, int color) { + mSecurityContainer.showMessage(message, color); + } + + /** + * Dismisses the keyguard by going to the next screen or making it gone. + * @param targetUserId a user that needs to be the foreground user at the dismissal completion. + * @return True if the keyguard is done. + */ + public boolean dismiss(int targetUserId) { + return dismiss(false, targetUserId); + } + + public boolean handleBackKey() { + if (mSecurityContainer.getCurrentSecuritySelection() != SecurityMode.None) { + mSecurityContainer.dismiss(false, KeyguardUpdateMonitor.getCurrentUser()); + return true; + } + return false; + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { + event.getText().add(mSecurityContainer.getCurrentSecurityModeContentDescription()); + return true; + } else { + return super.dispatchPopulateAccessibilityEvent(event); + } + } + + protected KeyguardSecurityContainer getSecurityContainer() { + return mSecurityContainer; + } + + @Override + public boolean dismiss(boolean authenticated, int targetUserId) { + return mSecurityContainer.showNextSecurityScreenOrFinish(authenticated, targetUserId); + } + + /** + * Authentication has happened and it's time to dismiss keyguard. This function + * should clean up and inform KeyguardViewMediator. + * + * @param strongAuth whether the user has authenticated with strong authentication like + * pattern, password or PIN but not by trust agents or fingerprint + * @param targetUserId a user that needs to be the foreground user at the dismissal completion. + */ + @Override + public void finish(boolean strongAuth, int targetUserId) { + // If there's a pending runnable because the user interacted with a widget + // and we're leaving keyguard, then run it. + boolean deferKeyguardDone = false; + if (mDismissAction != null) { + deferKeyguardDone = mDismissAction.onDismiss(); + mDismissAction = null; + mCancelAction = null; + } + if (mViewMediatorCallback != null) { + if (deferKeyguardDone) { + mViewMediatorCallback.keyguardDonePending(strongAuth, targetUserId); + } else { + mViewMediatorCallback.keyguardDone(strongAuth, targetUserId); + } + } + } + + @Override + public void reset() { + mViewMediatorCallback.resetKeyguard(); + } + + @Override + public void onSecurityModeChanged(SecurityMode securityMode, boolean needsInput) { + if (mViewMediatorCallback != null) { + mViewMediatorCallback.setNeedsInput(needsInput); + } + } + + public void userActivity() { + if (mViewMediatorCallback != null) { + mViewMediatorCallback.userActivity(); + } + } + + /** + * Called when the Keyguard is not actively shown anymore on the screen. + */ + public void onPause() { + if (DEBUG) Log.d(TAG, String.format("screen off, instance %s at %s", + Integer.toHexString(hashCode()), SystemClock.uptimeMillis())); + mSecurityContainer.showPrimarySecurityScreen(true); + mSecurityContainer.onPause(); + clearFocus(); + } + + /** + * Called when the Keyguard is actively shown on the screen. + */ + public void onResume() { + if (DEBUG) Log.d(TAG, "screen on, instance " + Integer.toHexString(hashCode())); + mSecurityContainer.onResume(KeyguardSecurityView.SCREEN_ON); + requestFocus(); + } + + /** + * Starts the animation when the Keyguard gets shown. + */ + public void startAppearAnimation() { + mSecurityContainer.startAppearAnimation(); + } + + public void startDisappearAnimation(Runnable finishRunnable) { + if (!mSecurityContainer.startDisappearAnimation(finishRunnable) && finishRunnable != null) { + finishRunnable.run(); + } + } + + /** + * Called before this view is being removed. + */ + public void cleanUp() { + getSecurityContainer().onPause(); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (interceptMediaKey(event)) { + return true; + } + return super.dispatchKeyEvent(event); + } + + /** + * Allows the media keys to work when the keyguard is showing. + * The media keys should be of no interest to the actual keyguard view(s), + * so intercepting them here should not be of any harm. + * @param event The key event + * @return whether the event was consumed as a media key. + */ + public boolean interceptMediaKey(KeyEvent event) { + final int keyCode = event.getKeyCode(); + if (event.getAction() == KeyEvent.ACTION_DOWN) { + switch (keyCode) { + case KeyEvent.KEYCODE_MEDIA_PLAY: + case KeyEvent.KEYCODE_MEDIA_PAUSE: + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + /* Suppress PLAY/PAUSE toggle when phone is ringing or + * in-call to avoid music playback */ + if (mTelephonyManager == null) { + mTelephonyManager = (TelephonyManager) getContext().getSystemService( + Context.TELEPHONY_SERVICE); + } + if (mTelephonyManager != null && + mTelephonyManager.getCallState() != TelephonyManager.CALL_STATE_IDLE) { + return true; // suppress key event + } + case KeyEvent.KEYCODE_MUTE: + case KeyEvent.KEYCODE_HEADSETHOOK: + case KeyEvent.KEYCODE_MEDIA_STOP: + case KeyEvent.KEYCODE_MEDIA_NEXT: + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + case KeyEvent.KEYCODE_MEDIA_REWIND: + case KeyEvent.KEYCODE_MEDIA_RECORD: + case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: + case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK: { + handleMediaKeyEvent(event); + return true; + } + + case KeyEvent.KEYCODE_VOLUME_UP: + case KeyEvent.KEYCODE_VOLUME_DOWN: + case KeyEvent.KEYCODE_VOLUME_MUTE: { + if (KEYGUARD_MANAGES_VOLUME) { + synchronized (this) { + if (mAudioManager == null) { + mAudioManager = (AudioManager) getContext().getSystemService( + Context.AUDIO_SERVICE); + } + } + // Volume buttons should only function for music (local or remote). + // TODO: Actually handle MUTE. + mAudioManager.adjustSuggestedStreamVolume( + keyCode == KeyEvent.KEYCODE_VOLUME_UP + ? AudioManager.ADJUST_RAISE + : AudioManager.ADJUST_LOWER /* direction */, + AudioManager.STREAM_MUSIC /* stream */, 0 /* flags */); + // Don't execute default volume behavior + return true; + } else { + return false; + } + } + } + } else if (event.getAction() == KeyEvent.ACTION_UP) { + switch (keyCode) { + case KeyEvent.KEYCODE_MUTE: + case KeyEvent.KEYCODE_HEADSETHOOK: + case KeyEvent.KEYCODE_MEDIA_PLAY: + case KeyEvent.KEYCODE_MEDIA_PAUSE: + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + case KeyEvent.KEYCODE_MEDIA_STOP: + case KeyEvent.KEYCODE_MEDIA_NEXT: + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + case KeyEvent.KEYCODE_MEDIA_REWIND: + case KeyEvent.KEYCODE_MEDIA_RECORD: + case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: + case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK: { + handleMediaKeyEvent(event); + return true; + } + } + } + return false; + } + + private void handleMediaKeyEvent(KeyEvent keyEvent) { + synchronized (this) { + if (mAudioManager == null) { + mAudioManager = (AudioManager) getContext().getSystemService( + Context.AUDIO_SERVICE); + } + } + mAudioManager.dispatchMediaKeyEvent(keyEvent); + } + + @Override + public void dispatchSystemUiVisibilityChanged(int visibility) { + super.dispatchSystemUiVisibilityChanged(visibility); + + if (!(mContext instanceof Activity)) { + setSystemUiVisibility(STATUS_BAR_DISABLE_BACK); + } + } + + /** + * In general, we enable unlocking the insecure keyguard with the menu key. However, there are + * some cases where we wish to disable it, notably when the menu button placement or technology + * is prone to false positives. + * + * @return true if the menu key should be enabled + */ + private static final String ENABLE_MENU_KEY_FILE = "/data/local/enable_menu_key"; + public boolean shouldEnableMenuKey() { + final Resources res = getResources(); + final boolean configDisabled = res.getBoolean(R.bool.config_disableMenuKeyInLockScreen); + final boolean isTestHarness = ActivityManager.isRunningInTestHarness(); + final boolean fileOverride = (new File(ENABLE_MENU_KEY_FILE)).exists(); + return !configDisabled || isTestHarness || fileOverride; + } + + public void setViewMediatorCallback(ViewMediatorCallback viewMediatorCallback) { + mViewMediatorCallback = viewMediatorCallback; + // Update ViewMediator with the current input method requirements + mViewMediatorCallback.setNeedsInput(mSecurityContainer.needsInput()); + } + + public void setLockPatternUtils(LockPatternUtils utils) { + mLockPatternUtils = utils; + mSecurityContainer.setLockPatternUtils(utils); + } + + public SecurityMode getSecurityMode() { + return mSecurityContainer.getSecurityMode(); + } + + public SecurityMode getCurrentSecurityMode() { + return mSecurityContainer.getCurrentSecurityMode(); + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageArea.java b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageArea.java new file mode 100644 index 000000000000..d19821fb9723 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageArea.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2011 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.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.View; +import android.widget.TextView; + +import java.lang.ref.WeakReference; + +/*** + * Manages a number of views inside of the given layout. See below for a list of widgets. + */ +class KeyguardMessageArea extends TextView implements SecurityMessageDisplay { + /** Handler token posted with accessibility announcement runnables. */ + private static final Object ANNOUNCE_TOKEN = new Object(); + + /** + * Delay before speaking an accessibility announcement. Used to prevent + * lift-to-type from interrupting itself. + */ + private static final long ANNOUNCEMENT_DELAY = 250; + private static final int DEFAULT_COLOR = -1; + + private final Handler mHandler; + private final int mDefaultColor; + + private CharSequence mMessage; + private int mNextMessageColor = DEFAULT_COLOR; + + private KeyguardUpdateMonitorCallback mInfoCallback = new KeyguardUpdateMonitorCallback() { + public void onFinishedGoingToSleep(int why) { + setSelected(false); + }; + public void onStartedWakingUp() { + setSelected(true); + }; + }; + + public KeyguardMessageArea(Context context) { + this(context, null); + } + + public KeyguardMessageArea(Context context, AttributeSet attrs) { + this(context, attrs, KeyguardUpdateMonitor.getInstance(context)); + } + + public KeyguardMessageArea(Context context, AttributeSet attrs, KeyguardUpdateMonitor monitor) { + super(context, attrs); + setLayerType(LAYER_TYPE_HARDWARE, null); // work around nested unclipped SaveLayer bug + + monitor.registerCallback(mInfoCallback); + mHandler = new Handler(Looper.myLooper()); + + mDefaultColor = getCurrentTextColor(); + update(); + } + + @Override + public void setNextMessageColor(int color) { + mNextMessageColor = color; + } + + @Override + public void setMessage(CharSequence msg) { + if (!TextUtils.isEmpty(msg)) { + securityMessageChanged(msg); + } else { + clearMessage(); + } + } + + @Override + public void setMessage(int resId) { + CharSequence message = null; + if (resId != 0) { + message = getContext().getResources().getText(resId); + } + setMessage(message); + } + + @Override + public void formatMessage(int resId, Object... formatArgs) { + CharSequence message = null; + if (resId != 0) { + message = getContext().getString(resId, formatArgs); + } + setMessage(message); + } + + public static SecurityMessageDisplay findSecurityMessageDisplay(View v) { + KeyguardMessageArea messageArea = (KeyguardMessageArea) v.findViewById( + R.id.keyguard_message_area); + if (messageArea == null) { + throw new RuntimeException("Can't find keyguard_message_area in " + v.getClass()); + } + return messageArea; + } + + @Override + protected void onFinishInflate() { + boolean shouldMarquee = KeyguardUpdateMonitor.getInstance(mContext).isDeviceInteractive(); + setSelected(shouldMarquee); // This is required to ensure marquee works + } + + private void securityMessageChanged(CharSequence message) { + mMessage = message; + update(); + mHandler.removeCallbacksAndMessages(ANNOUNCE_TOKEN); + mHandler.postAtTime(new AnnounceRunnable(this, getText()), ANNOUNCE_TOKEN, + (SystemClock.uptimeMillis() + ANNOUNCEMENT_DELAY)); + } + + private void clearMessage() { + mMessage = null; + update(); + } + + private void update() { + CharSequence status = mMessage; + setVisibility(TextUtils.isEmpty(status) ? INVISIBLE : VISIBLE); + setText(status); + int color = mDefaultColor; + if (mNextMessageColor != DEFAULT_COLOR) { + color = mNextMessageColor; + mNextMessageColor = DEFAULT_COLOR; + } + setTextColor(color); + } + + + /** + * Runnable used to delay accessibility announcements. + */ + private static class AnnounceRunnable implements Runnable { + private final WeakReference<View> mHost; + private final CharSequence mTextToAnnounce; + + AnnounceRunnable(View host, CharSequence textToAnnounce) { + mHost = new WeakReference<View>(host); + mTextToAnnounce = textToAnnounce; + } + + @Override + public void run() { + final View host = mHost.get(); + if (host != null) { + host.announceForAccessibility(mTextToAnnounce); + } + } + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java new file mode 100644 index 000000000000..590d8d5de672 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java @@ -0,0 +1,173 @@ +/* + * 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.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AnimationUtils; + +import com.android.settingslib.animation.AppearAnimationUtils; +import com.android.settingslib.animation.DisappearAnimationUtils; + +/** + * Displays a PIN pad for unlocking. + */ +public class KeyguardPINView extends KeyguardPinBasedInputView { + + private final AppearAnimationUtils mAppearAnimationUtils; + private final DisappearAnimationUtils mDisappearAnimationUtils; + private final DisappearAnimationUtils mDisappearAnimationUtilsLocked; + private ViewGroup mContainer; + private ViewGroup mRow0; + private ViewGroup mRow1; + private ViewGroup mRow2; + private ViewGroup mRow3; + private View mDivider; + private int mDisappearYTranslation; + private View[][] mViews; + private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; + + public KeyguardPINView(Context context) { + this(context, null); + } + + public KeyguardPINView(Context context, AttributeSet attrs) { + super(context, attrs); + mAppearAnimationUtils = new AppearAnimationUtils(context); + mDisappearAnimationUtils = new DisappearAnimationUtils(context, + 125, 0.6f /* translationScale */, + 0.45f /* delayScale */, AnimationUtils.loadInterpolator( + mContext, android.R.interpolator.fast_out_linear_in)); + mDisappearAnimationUtilsLocked = new DisappearAnimationUtils(context, + (long) (125 * KeyguardPatternView.DISAPPEAR_MULTIPLIER_LOCKED), + 0.6f /* translationScale */, + 0.45f /* delayScale */, AnimationUtils.loadInterpolator( + mContext, android.R.interpolator.fast_out_linear_in)); + mDisappearYTranslation = getResources().getDimensionPixelSize( + R.dimen.disappear_y_translation); + mKeyguardUpdateMonitor = KeyguardUpdateMonitor.getInstance(context); + } + + @Override + protected void resetState() { + super.resetState(); + mSecurityMessageDisplay.setMessage(""); + } + + @Override + protected int getPasswordTextViewId() { + return R.id.pinEntry; + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + mContainer = (ViewGroup) findViewById(R.id.container); + mRow0 = (ViewGroup) findViewById(R.id.row0); + mRow1 = (ViewGroup) findViewById(R.id.row1); + mRow2 = (ViewGroup) findViewById(R.id.row2); + mRow3 = (ViewGroup) findViewById(R.id.row3); + mDivider = findViewById(R.id.divider); + mViews = new View[][]{ + new View[]{ + mRow0, null, null + }, + new View[]{ + findViewById(R.id.key1), findViewById(R.id.key2), + findViewById(R.id.key3) + }, + new View[]{ + findViewById(R.id.key4), findViewById(R.id.key5), + findViewById(R.id.key6) + }, + new View[]{ + findViewById(R.id.key7), findViewById(R.id.key8), + findViewById(R.id.key9) + }, + new View[]{ + null, findViewById(R.id.key0), findViewById(R.id.key_enter) + }, + new View[]{ + null, mEcaView, null + }}; + } + + @Override + public void showUsabilityHint() { + } + + @Override + public int getWrongPasswordStringId() { + return R.string.kg_wrong_pin; + } + + @Override + public void startAppearAnimation() { + enableClipping(false); + setAlpha(1f); + setTranslationY(mAppearAnimationUtils.getStartTranslation()); + AppearAnimationUtils.startTranslationYAnimation(this, 0 /* delay */, 500 /* duration */, + 0, mAppearAnimationUtils.getInterpolator()); + mAppearAnimationUtils.startAnimation2d(mViews, + new Runnable() { + @Override + public void run() { + enableClipping(true); + } + }); + } + + @Override + public boolean startDisappearAnimation(final Runnable finishRunnable) { + enableClipping(false); + setTranslationY(0); + AppearAnimationUtils.startTranslationYAnimation(this, 0 /* delay */, 280 /* duration */, + mDisappearYTranslation, mDisappearAnimationUtils.getInterpolator()); + DisappearAnimationUtils disappearAnimationUtils = mKeyguardUpdateMonitor + .needsSlowUnlockTransition() + ? mDisappearAnimationUtilsLocked + : mDisappearAnimationUtils; + disappearAnimationUtils.startAnimation2d(mViews, + new Runnable() { + @Override + public void run() { + enableClipping(true); + if (finishRunnable != null) { + finishRunnable.run(); + } + } + }); + return true; + } + + private void enableClipping(boolean enable) { + mContainer.setClipToPadding(enable); + mContainer.setClipChildren(enable); + mRow1.setClipToPadding(enable); + mRow2.setClipToPadding(enable); + mRow3.setClipToPadding(enable); + setClipChildren(enable); + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java new file mode 100644 index 000000000000..d49ff975473d --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java @@ -0,0 +1,364 @@ +/* + * 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.content.Context; +import android.graphics.Rect; +import android.text.Editable; +import android.text.InputType; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.text.method.TextKeyListener; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.View; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.InputMethodSubtype; +import android.widget.TextView; +import android.widget.TextView.OnEditorActionListener; + +import com.android.internal.widget.TextViewInputDisabler; + +import java.util.List; +/** + * Displays an alphanumeric (latin-1) key entry for the user to enter + * an unlock password + */ +public class KeyguardPasswordView extends KeyguardAbsKeyInputView + implements KeyguardSecurityView, OnEditorActionListener, TextWatcher { + + private final boolean mShowImeAtScreenOn; + private final int mDisappearYTranslation; + + // A delay constant to be used in a workaround for the situation where InputMethodManagerService + // is not switched to the new user yet. + // TODO: Remove this by ensuring such a race condition never happens. + private static final int DELAY_MILLIS_TO_REEVALUATE_IME_SWITCH_ICON = 500; // 500ms + + InputMethodManager mImm; + private TextView mPasswordEntry; + private TextViewInputDisabler mPasswordEntryDisabler; + private View mSwitchImeButton; + + private Interpolator mLinearOutSlowInInterpolator; + private Interpolator mFastOutLinearInInterpolator; + + public KeyguardPasswordView(Context context) { + this(context, null); + } + + public KeyguardPasswordView(Context context, AttributeSet attrs) { + super(context, attrs); + mShowImeAtScreenOn = context.getResources(). + getBoolean(R.bool.kg_show_ime_at_screen_on); + mDisappearYTranslation = getResources().getDimensionPixelSize( + R.dimen.disappear_y_translation); + mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator( + context, android.R.interpolator.linear_out_slow_in); + mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator( + context, android.R.interpolator.fast_out_linear_in); + } + + @Override + protected void resetState() { + mSecurityMessageDisplay.setMessage(""); + final boolean wasDisabled = mPasswordEntry.isEnabled(); + setPasswordEntryEnabled(true); + setPasswordEntryInputEnabled(true); + if (wasDisabled) { + mImm.showSoftInput(mPasswordEntry, InputMethodManager.SHOW_IMPLICIT); + } + } + + @Override + protected int getPasswordTextViewId() { + return R.id.passwordEntry; + } + + @Override + public boolean needsInput() { + return true; + } + + @Override + public void onResume(final int reason) { + super.onResume(reason); + + // Wait a bit to focus the field so the focusable flag on the window is already set then. + post(new Runnable() { + @Override + public void run() { + if (isShown() && mPasswordEntry.isEnabled()) { + mPasswordEntry.requestFocus(); + if (reason != KeyguardSecurityView.SCREEN_ON || mShowImeAtScreenOn) { + mImm.showSoftInput(mPasswordEntry, InputMethodManager.SHOW_IMPLICIT); + } + } + } + }); + } + + @Override + protected int getPromtReasonStringRes(int reason) { + switch (reason) { + case PROMPT_REASON_RESTART: + return R.string.kg_prompt_reason_restart_password; + case PROMPT_REASON_TIMEOUT: + return R.string.kg_prompt_reason_timeout_password; + case PROMPT_REASON_DEVICE_ADMIN: + return R.string.kg_prompt_reason_device_admin; + case PROMPT_REASON_USER_REQUEST: + return R.string.kg_prompt_reason_user_request; + case PROMPT_REASON_NONE: + return 0; + default: + return R.string.kg_prompt_reason_timeout_password; + } + } + + @Override + public void onPause() { + super.onPause(); + mImm.hideSoftInputFromWindow(getWindowToken(), 0); + } + + @Override + public void reset() { + super.reset(); + mPasswordEntry.requestFocus(); + } + + private void updateSwitchImeButton() { + // If there's more than one IME, enable the IME switcher button + final boolean wasVisible = mSwitchImeButton.getVisibility() == View.VISIBLE; + final boolean shouldBeVisible = hasMultipleEnabledIMEsOrSubtypes(mImm, false); + if (wasVisible != shouldBeVisible) { + mSwitchImeButton.setVisibility(shouldBeVisible ? View.VISIBLE : View.GONE); + } + + // TODO: Check if we still need this hack. + // If no icon is visible, reset the start margin on the password field so the text is + // still centered. + if (mSwitchImeButton.getVisibility() != View.VISIBLE) { + android.view.ViewGroup.LayoutParams params = mPasswordEntry.getLayoutParams(); + if (params instanceof MarginLayoutParams) { + final MarginLayoutParams mlp = (MarginLayoutParams) params; + mlp.setMarginStart(0); + mPasswordEntry.setLayoutParams(params); + } + } + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + mImm = (InputMethodManager) getContext().getSystemService( + Context.INPUT_METHOD_SERVICE); + + mPasswordEntry = (TextView) findViewById(getPasswordTextViewId()); + mPasswordEntryDisabler = new TextViewInputDisabler(mPasswordEntry); + mPasswordEntry.setKeyListener(TextKeyListener.getInstance()); + mPasswordEntry.setInputType(InputType.TYPE_CLASS_TEXT + | InputType.TYPE_TEXT_VARIATION_PASSWORD); + mPasswordEntry.setOnEditorActionListener(this); + mPasswordEntry.addTextChangedListener(this); + + // Poke the wakelock any time the text is selected or modified + mPasswordEntry.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mCallback.userActivity(); + } + }); + + // Set selected property on so the view can send accessibility events. + mPasswordEntry.setSelected(true); + + mPasswordEntry.requestFocus(); + + mSwitchImeButton = findViewById(R.id.switch_ime_button); + mSwitchImeButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mCallback.userActivity(); // Leave the screen on a bit longer + // Do not show auxiliary subtypes in password lock screen. + mImm.showInputMethodPicker(false /* showAuxiliarySubtypes */); + } + }); + + // If there's more than one IME, enable the IME switcher button + updateSwitchImeButton(); + + // When we the current user is switching, InputMethodManagerService sometimes has not + // switched internal state yet here. As a quick workaround, we check the keyboard state + // again. + // TODO: Remove this workaround by ensuring such a race condition never happens. + postDelayed(new Runnable() { + @Override + public void run() { + updateSwitchImeButton(); + } + }, DELAY_MILLIS_TO_REEVALUATE_IME_SWITCH_ICON); + } + + @Override + protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { + // send focus to the password field + return mPasswordEntry.requestFocus(direction, previouslyFocusedRect); + } + + @Override + protected void resetPasswordText(boolean animate, boolean announce) { + mPasswordEntry.setText(""); + } + + @Override + protected String getPasswordText() { + return mPasswordEntry.getText().toString(); + } + + @Override + protected void setPasswordEntryEnabled(boolean enabled) { + mPasswordEntry.setEnabled(enabled); + } + + @Override + protected void setPasswordEntryInputEnabled(boolean enabled) { + mPasswordEntryDisabler.setInputEnabled(enabled); + } + + /** + * Method adapted from com.android.inputmethod.latin.Utils + * + * @param imm The input method manager + * @param shouldIncludeAuxiliarySubtypes + * @return true if we have multiple IMEs to choose from + */ + private boolean hasMultipleEnabledIMEsOrSubtypes(InputMethodManager imm, + final boolean shouldIncludeAuxiliarySubtypes) { + final List<InputMethodInfo> enabledImis = imm.getEnabledInputMethodList(); + + // Number of the filtered IMEs + int filteredImisCount = 0; + + for (InputMethodInfo imi : enabledImis) { + // We can return true immediately after we find two or more filtered IMEs. + if (filteredImisCount > 1) return true; + final List<InputMethodSubtype> subtypes = + imm.getEnabledInputMethodSubtypeList(imi, true); + // IMEs that have no subtypes should be counted. + if (subtypes.isEmpty()) { + ++filteredImisCount; + continue; + } + + int auxCount = 0; + for (InputMethodSubtype subtype : subtypes) { + if (subtype.isAuxiliary()) { + ++auxCount; + } + } + final int nonAuxCount = subtypes.size() - auxCount; + + // IMEs that have one or more non-auxiliary subtypes should be counted. + // If shouldIncludeAuxiliarySubtypes is true, IMEs that have two or more auxiliary + // subtypes should be counted as well. + if (nonAuxCount > 0 || (shouldIncludeAuxiliarySubtypes && auxCount > 1)) { + ++filteredImisCount; + continue; + } + } + + return filteredImisCount > 1 + // imm.getEnabledInputMethodSubtypeList(null, false) will return the current IME's enabled + // input method subtype (The current IME should be LatinIME.) + || imm.getEnabledInputMethodSubtypeList(null, false).size() > 1; + } + + @Override + public void showUsabilityHint() { + } + + @Override + public int getWrongPasswordStringId() { + return R.string.kg_wrong_password; + } + + @Override + public void startAppearAnimation() { + setAlpha(0f); + setTranslationY(0f); + animate() + .alpha(1) + .withLayer() + .setDuration(300) + .setInterpolator(mLinearOutSlowInInterpolator); + } + + @Override + public boolean startDisappearAnimation(Runnable finishRunnable) { + animate() + .alpha(0f) + .translationY(mDisappearYTranslation) + .setInterpolator(mFastOutLinearInInterpolator) + .setDuration(100) + .withEndAction(finishRunnable); + return true; + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + if (mCallback != null) { + mCallback.userActivity(); + } + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(Editable s) { + // Poor man's user edit detection, assuming empty text is programmatic and everything else + // is from the user. + if (!TextUtils.isEmpty(s)) { + onUserInput(); + } + } + + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + // Check if this was the result of hitting the enter key + final boolean isSoftImeEvent = event == null + && (actionId == EditorInfo.IME_NULL + || actionId == EditorInfo.IME_ACTION_DONE + || actionId == EditorInfo.IME_ACTION_NEXT); + final boolean isKeyboardEnterKey = event != null + && KeyEvent.isConfirmKey(event.getKeyCode()) + && event.getAction() == KeyEvent.ACTION_DOWN; + if (isSoftImeEvent || isKeyboardEnterKey) { + verifyPasswordAndUnlock(); + return true; + } + return false; + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java new file mode 100644 index 000000000000..c2b57ffa6113 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java @@ -0,0 +1,492 @@ +/* + * 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 static com.android.keyguard.LatencyTracker.ACTION_CHECK_CREDENTIAL; +import static com.android.keyguard.LatencyTracker.ACTION_CHECK_CREDENTIAL_UNLOCKED; + +import android.content.Context; +import android.graphics.Rect; +import android.os.AsyncTask; +import android.os.CountDownTimer; +import android.os.SystemClock; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; +import android.widget.LinearLayout; + +import com.android.internal.widget.LockPatternChecker; +import com.android.internal.widget.LockPatternUtils; +import com.android.internal.widget.LockPatternView; +import com.android.settingslib.animation.AppearAnimationCreator; +import com.android.settingslib.animation.AppearAnimationUtils; +import com.android.settingslib.animation.DisappearAnimationUtils; + +import java.util.List; + +public class KeyguardPatternView extends LinearLayout implements KeyguardSecurityView, + AppearAnimationCreator<LockPatternView.CellState>, + EmergencyButton.EmergencyButtonCallback { + + private static final String TAG = "SecurityPatternView"; + private static final boolean DEBUG = KeyguardConstants.DEBUG; + + // how long before we clear the wrong pattern + private static final int PATTERN_CLEAR_TIMEOUT_MS = 2000; + + // how long we stay awake after each key beyond MIN_PATTERN_BEFORE_POKE_WAKELOCK + private static final int UNLOCK_PATTERN_WAKE_INTERVAL_MS = 7000; + + // how many cells the user has to cross before we poke the wakelock + private static final int MIN_PATTERN_BEFORE_POKE_WAKELOCK = 2; + + // How much we scale up the duration of the disappear animation when the current user is locked + public static final float DISAPPEAR_MULTIPLIER_LOCKED = 1.5f; + + private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; + private final AppearAnimationUtils mAppearAnimationUtils; + private final DisappearAnimationUtils mDisappearAnimationUtils; + private final DisappearAnimationUtils mDisappearAnimationUtilsLocked; + + private CountDownTimer mCountdownTimer = null; + private LockPatternUtils mLockPatternUtils; + private AsyncTask<?, ?, ?> mPendingLockCheck; + private LockPatternView mLockPatternView; + private KeyguardSecurityCallback mCallback; + + /** + * Keeps track of the last time we poked the wake lock during dispatching of the touch event. + * Initialized to something guaranteed to make us poke the wakelock when the user starts + * drawing the pattern. + * @see #dispatchTouchEvent(android.view.MotionEvent) + */ + private long mLastPokeTime = -UNLOCK_PATTERN_WAKE_INTERVAL_MS; + + /** + * Useful for clearing out the wrong pattern after a delay + */ + private Runnable mCancelPatternRunnable = new Runnable() { + @Override + public void run() { + mLockPatternView.clearPattern(); + } + }; + private Rect mTempRect = new Rect(); + private KeyguardMessageArea mSecurityMessageDisplay; + private View mEcaView; + private ViewGroup mContainer; + private int mDisappearYTranslation; + + enum FooterMode { + Normal, + ForgotLockPattern, + VerifyUnlocked + } + + public KeyguardPatternView(Context context) { + this(context, null); + } + + public KeyguardPatternView(Context context, AttributeSet attrs) { + super(context, attrs); + mKeyguardUpdateMonitor = KeyguardUpdateMonitor.getInstance(mContext); + mAppearAnimationUtils = new AppearAnimationUtils(context, + AppearAnimationUtils.DEFAULT_APPEAR_DURATION, 1.5f /* translationScale */, + 2.0f /* delayScale */, AnimationUtils.loadInterpolator( + mContext, android.R.interpolator.linear_out_slow_in)); + mDisappearAnimationUtils = new DisappearAnimationUtils(context, + 125, 1.2f /* translationScale */, + 0.6f /* delayScale */, AnimationUtils.loadInterpolator( + mContext, android.R.interpolator.fast_out_linear_in)); + mDisappearAnimationUtilsLocked = new DisappearAnimationUtils(context, + (long) (125 * DISAPPEAR_MULTIPLIER_LOCKED), 1.2f /* translationScale */, + 0.6f /* delayScale */, AnimationUtils.loadInterpolator( + mContext, android.R.interpolator.fast_out_linear_in)); + mDisappearYTranslation = getResources().getDimensionPixelSize( + R.dimen.disappear_y_translation); + } + + @Override + public void setKeyguardCallback(KeyguardSecurityCallback callback) { + mCallback = callback; + } + + @Override + public void setLockPatternUtils(LockPatternUtils utils) { + mLockPatternUtils = utils; + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mLockPatternUtils = mLockPatternUtils == null + ? new LockPatternUtils(mContext) : mLockPatternUtils; + + mLockPatternView = (LockPatternView) findViewById(R.id.lockPatternView); + mLockPatternView.setSaveEnabled(false); + mLockPatternView.setOnPatternListener(new UnlockPatternListener()); + + // vibrate mode will be the same for the life of this screen + mLockPatternView.setTactileFeedbackEnabled(mLockPatternUtils.isTactileFeedbackEnabled()); + + mSecurityMessageDisplay = + (KeyguardMessageArea) KeyguardMessageArea.findSecurityMessageDisplay(this); + mEcaView = findViewById(R.id.keyguard_selector_fade_container); + mContainer = (ViewGroup) findViewById(R.id.container); + + EmergencyButton button = (EmergencyButton) findViewById(R.id.emergency_call_button); + if (button != null) { + button.setCallback(this); + } + } + + @Override + public void onEmergencyButtonClickedWhenInCall() { + mCallback.reset(); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + boolean result = super.onTouchEvent(ev); + // as long as the user is entering a pattern (i.e sending a touch event that was handled + // by this screen), keep poking the wake lock so that the screen will stay on. + final long elapsed = SystemClock.elapsedRealtime() - mLastPokeTime; + if (result && (elapsed > (UNLOCK_PATTERN_WAKE_INTERVAL_MS - 100))) { + mLastPokeTime = SystemClock.elapsedRealtime(); + } + mTempRect.set(0, 0, 0, 0); + offsetRectIntoDescendantCoords(mLockPatternView, mTempRect); + ev.offsetLocation(mTempRect.left, mTempRect.top); + result = mLockPatternView.dispatchTouchEvent(ev) || result; + ev.offsetLocation(-mTempRect.left, -mTempRect.top); + return result; + } + + @Override + public void reset() { + // reset lock pattern + mLockPatternView.setInStealthMode(!mLockPatternUtils.isVisiblePatternEnabled( + KeyguardUpdateMonitor.getCurrentUser())); + mLockPatternView.enableInput(); + mLockPatternView.setEnabled(true); + mLockPatternView.clearPattern(); + + // if the user is currently locked out, enforce it. + long deadline = mLockPatternUtils.getLockoutAttemptDeadline( + KeyguardUpdateMonitor.getCurrentUser()); + if (deadline != 0) { + handleAttemptLockout(deadline); + } else { + displayDefaultSecurityMessage(); + } + } + + private void displayDefaultSecurityMessage() { + mSecurityMessageDisplay.setMessage(""); + } + + @Override + public void showUsabilityHint() { + } + + /** TODO: hook this up */ + public void cleanUp() { + if (DEBUG) Log.v(TAG, "Cleanup() called on " + this); + mLockPatternUtils = null; + mLockPatternView.setOnPatternListener(null); + } + + private class UnlockPatternListener implements LockPatternView.OnPatternListener { + + @Override + public void onPatternStart() { + mLockPatternView.removeCallbacks(mCancelPatternRunnable); + mSecurityMessageDisplay.setMessage(""); + } + + @Override + public void onPatternCleared() { + } + + @Override + public void onPatternCellAdded(List<LockPatternView.Cell> pattern) { + mCallback.userActivity(); + } + + @Override + public void onPatternDetected(final List<LockPatternView.Cell> pattern) { + mLockPatternView.disableInput(); + if (mPendingLockCheck != null) { + mPendingLockCheck.cancel(false); + } + + final int userId = KeyguardUpdateMonitor.getCurrentUser(); + if (pattern.size() < LockPatternUtils.MIN_PATTERN_REGISTER_FAIL) { + mLockPatternView.enableInput(); + onPatternChecked(userId, false, 0, false /* not valid - too short */); + return; + } + + if (LatencyTracker.isEnabled(mContext)) { + LatencyTracker.getInstance(mContext).onActionStart(ACTION_CHECK_CREDENTIAL); + LatencyTracker.getInstance(mContext).onActionStart(ACTION_CHECK_CREDENTIAL_UNLOCKED); + } + mPendingLockCheck = LockPatternChecker.checkPattern( + mLockPatternUtils, + pattern, + userId, + new LockPatternChecker.OnCheckCallback() { + + @Override + public void onEarlyMatched() { + if (LatencyTracker.isEnabled(mContext)) { + LatencyTracker.getInstance(mContext).onActionEnd( + ACTION_CHECK_CREDENTIAL); + } + onPatternChecked(userId, true /* matched */, 0 /* timeoutMs */, + true /* isValidPattern */); + } + + @Override + public void onChecked(boolean matched, int timeoutMs) { + if (LatencyTracker.isEnabled(mContext)) { + LatencyTracker.getInstance(mContext).onActionEnd( + ACTION_CHECK_CREDENTIAL_UNLOCKED); + } + mLockPatternView.enableInput(); + mPendingLockCheck = null; + if (!matched) { + onPatternChecked(userId, false /* matched */, timeoutMs, + true /* isValidPattern */); + } + } + + @Override + public void onCancelled() { + // We already got dismissed with the early matched callback, so we + // cancelled the check. However, we still need to note down the latency. + if (LatencyTracker.isEnabled(mContext)) { + LatencyTracker.getInstance(mContext).onActionEnd( + ACTION_CHECK_CREDENTIAL_UNLOCKED); + } + } + }); + if (pattern.size() > MIN_PATTERN_BEFORE_POKE_WAKELOCK) { + mCallback.userActivity(); + } + } + + private void onPatternChecked(int userId, boolean matched, int timeoutMs, + boolean isValidPattern) { + boolean dismissKeyguard = KeyguardUpdateMonitor.getCurrentUser() == userId; + if (matched) { + mCallback.reportUnlockAttempt(userId, true, 0); + if (dismissKeyguard) { + mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Correct); + mCallback.dismiss(true, userId); + } + } else { + mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong); + if (isValidPattern) { + mCallback.reportUnlockAttempt(userId, false, timeoutMs); + if (timeoutMs > 0) { + long deadline = mLockPatternUtils.setLockoutAttemptDeadline( + userId, timeoutMs); + handleAttemptLockout(deadline); + } + } + if (timeoutMs == 0) { + mSecurityMessageDisplay.setMessage(R.string.kg_wrong_pattern); + mLockPatternView.postDelayed(mCancelPatternRunnable, PATTERN_CLEAR_TIMEOUT_MS); + } + } + } + } + + private void handleAttemptLockout(long elapsedRealtimeDeadline) { + mLockPatternView.clearPattern(); + mLockPatternView.setEnabled(false); + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long secondsInFuture = (long) Math.ceil( + (elapsedRealtimeDeadline - elapsedRealtime) / 1000.0); + mCountdownTimer = new CountDownTimer(secondsInFuture * 1000, 1000) { + + @Override + public void onTick(long millisUntilFinished) { + final int secondsRemaining = (int) Math.round(millisUntilFinished / 1000.0); + mSecurityMessageDisplay.formatMessage( + R.string.kg_too_many_failed_attempts_countdown, secondsRemaining); + } + + @Override + public void onFinish() { + mLockPatternView.setEnabled(true); + displayDefaultSecurityMessage(); + } + + }.start(); + } + + @Override + public boolean needsInput() { + return false; + } + + @Override + public void onPause() { + if (mCountdownTimer != null) { + mCountdownTimer.cancel(); + mCountdownTimer = null; + } + if (mPendingLockCheck != null) { + mPendingLockCheck.cancel(false); + mPendingLockCheck = null; + } + } + + @Override + public void onResume(int reason) { + reset(); + } + + @Override + public KeyguardSecurityCallback getCallback() { + return mCallback; + } + + @Override + public void showPromptReason(int reason) { + switch (reason) { + case PROMPT_REASON_RESTART: + mSecurityMessageDisplay.setMessage(R.string.kg_prompt_reason_restart_pattern); + break; + case PROMPT_REASON_TIMEOUT: + mSecurityMessageDisplay.setMessage(R.string.kg_prompt_reason_timeout_pattern); + break; + case PROMPT_REASON_DEVICE_ADMIN: + mSecurityMessageDisplay.setMessage(R.string.kg_prompt_reason_device_admin); + break; + case PROMPT_REASON_USER_REQUEST: + mSecurityMessageDisplay.setMessage(R.string.kg_prompt_reason_user_request); + break; + case PROMPT_REASON_NONE: + break; + default: + mSecurityMessageDisplay.setMessage(R.string.kg_prompt_reason_timeout_pattern); + break; + } + } + + @Override + public void showMessage(String message, int color) { + mSecurityMessageDisplay.setNextMessageColor(color); + mSecurityMessageDisplay.setMessage(message); + } + + @Override + public void startAppearAnimation() { + enableClipping(false); + setAlpha(1f); + setTranslationY(mAppearAnimationUtils.getStartTranslation()); + AppearAnimationUtils.startTranslationYAnimation(this, 0 /* delay */, 500 /* duration */, + 0, mAppearAnimationUtils.getInterpolator()); + mAppearAnimationUtils.startAnimation2d( + mLockPatternView.getCellStates(), + new Runnable() { + @Override + public void run() { + enableClipping(true); + } + }, + this); + if (!TextUtils.isEmpty(mSecurityMessageDisplay.getText())) { + mAppearAnimationUtils.createAnimation(mSecurityMessageDisplay, 0, + AppearAnimationUtils.DEFAULT_APPEAR_DURATION, + mAppearAnimationUtils.getStartTranslation(), + true /* appearing */, + mAppearAnimationUtils.getInterpolator(), + null /* finishRunnable */); + } + } + + @Override + public boolean startDisappearAnimation(final Runnable finishRunnable) { + float durationMultiplier = mKeyguardUpdateMonitor.needsSlowUnlockTransition() + ? DISAPPEAR_MULTIPLIER_LOCKED + : 1f; + mLockPatternView.clearPattern(); + enableClipping(false); + setTranslationY(0); + AppearAnimationUtils.startTranslationYAnimation(this, 0 /* delay */, + (long) (300 * durationMultiplier), + -mDisappearAnimationUtils.getStartTranslation(), + mDisappearAnimationUtils.getInterpolator()); + + DisappearAnimationUtils disappearAnimationUtils = mKeyguardUpdateMonitor + .needsSlowUnlockTransition() + ? mDisappearAnimationUtilsLocked + : mDisappearAnimationUtils; + disappearAnimationUtils.startAnimation2d(mLockPatternView.getCellStates(), + () -> { + enableClipping(true); + if (finishRunnable != null) { + finishRunnable.run(); + } + }, KeyguardPatternView.this); + if (!TextUtils.isEmpty(mSecurityMessageDisplay.getText())) { + mDisappearAnimationUtils.createAnimation(mSecurityMessageDisplay, 0, + (long) (200 * durationMultiplier), + - mDisappearAnimationUtils.getStartTranslation() * 3, + false /* appearing */, + mDisappearAnimationUtils.getInterpolator(), + null /* finishRunnable */); + } + return true; + } + + private void enableClipping(boolean enable) { + setClipChildren(enable); + mContainer.setClipToPadding(enable); + mContainer.setClipChildren(enable); + } + + @Override + public void createAnimation(final LockPatternView.CellState animatedCell, long delay, + long duration, float translationY, final boolean appearing, + Interpolator interpolator, + final Runnable finishListener) { + mLockPatternView.startCellStateAnimation(animatedCell, + 1f, appearing ? 1f : 0f, /* alpha */ + appearing ? translationY : 0f, appearing ? 0f : translationY, /* translation */ + appearing ? 0f : 1f, 1f /* scale */, + delay, duration, interpolator, finishListener); + if (finishListener != null) { + // Also animate the Emergency call + mAppearAnimationUtils.createAnimation(mEcaView, delay, duration, translationY, + appearing, interpolator, null); + } + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java new file mode 100644 index 000000000000..108b466e44af --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2014 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.content.Context; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; + +/** + * A Pin based Keyguard input view + */ +public abstract class KeyguardPinBasedInputView extends KeyguardAbsKeyInputView + implements View.OnKeyListener, View.OnTouchListener { + + protected PasswordTextView mPasswordEntry; + private View mOkButton; + private View mDeleteButton; + private View mButton0; + private View mButton1; + private View mButton2; + private View mButton3; + private View mButton4; + private View mButton5; + private View mButton6; + private View mButton7; + private View mButton8; + private View mButton9; + + public KeyguardPinBasedInputView(Context context) { + this(context, null); + } + + public KeyguardPinBasedInputView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void reset() { + mPasswordEntry.requestFocus(); + super.reset(); + } + + @Override + protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { + // send focus to the password field + return mPasswordEntry.requestFocus(direction, previouslyFocusedRect); + } + + @Override + protected void resetState() { + setPasswordEntryEnabled(true); + } + + @Override + protected void setPasswordEntryEnabled(boolean enabled) { + mPasswordEntry.setEnabled(enabled); + mOkButton.setEnabled(enabled); + } + + @Override + protected void setPasswordEntryInputEnabled(boolean enabled) { + mPasswordEntry.setEnabled(enabled); + mOkButton.setEnabled(enabled); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (KeyEvent.isConfirmKey(keyCode)) { + performClick(mOkButton); + return true; + } else if (keyCode == KeyEvent.KEYCODE_DEL) { + performClick(mDeleteButton); + return true; + } + if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) { + int number = keyCode - KeyEvent.KEYCODE_0; + performNumberClick(number); + return true; + } + if (keyCode >= KeyEvent.KEYCODE_NUMPAD_0 && keyCode <= KeyEvent.KEYCODE_NUMPAD_9) { + int number = keyCode - KeyEvent.KEYCODE_NUMPAD_0; + performNumberClick(number); + return true; + } + return super.onKeyDown(keyCode, event); + } + + @Override + protected int getPromtReasonStringRes(int reason) { + switch (reason) { + case PROMPT_REASON_RESTART: + return R.string.kg_prompt_reason_restart_pin; + case PROMPT_REASON_TIMEOUT: + return R.string.kg_prompt_reason_timeout_pin; + case PROMPT_REASON_DEVICE_ADMIN: + return R.string.kg_prompt_reason_device_admin; + case PROMPT_REASON_USER_REQUEST: + return R.string.kg_prompt_reason_user_request; + case PROMPT_REASON_NONE: + return 0; + default: + return R.string.kg_prompt_reason_timeout_pin; + } + } + + private void performClick(View view) { + view.performClick(); + } + + private void performNumberClick(int number) { + switch (number) { + case 0: + performClick(mButton0); + break; + case 1: + performClick(mButton1); + break; + case 2: + performClick(mButton2); + break; + case 3: + performClick(mButton3); + break; + case 4: + performClick(mButton4); + break; + case 5: + performClick(mButton5); + break; + case 6: + performClick(mButton6); + break; + case 7: + performClick(mButton7); + break; + case 8: + performClick(mButton8); + break; + case 9: + performClick(mButton9); + break; + } + } + + @Override + protected void resetPasswordText(boolean animate, boolean announce) { + mPasswordEntry.reset(animate, announce); + } + + @Override + protected String getPasswordText() { + return mPasswordEntry.getText(); + } + + @Override + protected void onFinishInflate() { + mPasswordEntry = (PasswordTextView) findViewById(getPasswordTextViewId()); + mPasswordEntry.setOnKeyListener(this); + + // Set selected property on so the view can send accessibility events. + mPasswordEntry.setSelected(true); + + mPasswordEntry.setUserActivityListener(new PasswordTextView.UserActivityListener() { + @Override + public void onUserActivity() { + onUserInput(); + } + }); + + mOkButton = findViewById(R.id.key_enter); + if (mOkButton != null) { + mOkButton.setOnTouchListener(this); + mOkButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (mPasswordEntry.isEnabled()) { + verifyPasswordAndUnlock(); + } + } + }); + mOkButton.setOnHoverListener(new LiftToActivateListener(getContext())); + } + + mDeleteButton = findViewById(R.id.delete_button); + mDeleteButton.setVisibility(View.VISIBLE); + mDeleteButton.setOnTouchListener(this); + mDeleteButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + // check for time-based lockouts + if (mPasswordEntry.isEnabled()) { + mPasswordEntry.deleteLastChar(); + } + } + }); + mDeleteButton.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + // check for time-based lockouts + if (mPasswordEntry.isEnabled()) { + resetPasswordText(true /* animate */, true /* announce */); + } + doHapticKeyClick(); + return true; + } + }); + + mButton0 = findViewById(R.id.key0); + mButton1 = findViewById(R.id.key1); + mButton2 = findViewById(R.id.key2); + mButton3 = findViewById(R.id.key3); + mButton4 = findViewById(R.id.key4); + mButton5 = findViewById(R.id.key5); + mButton6 = findViewById(R.id.key6); + mButton7 = findViewById(R.id.key7); + mButton8 = findViewById(R.id.key8); + mButton9 = findViewById(R.id.key9); + + mPasswordEntry.requestFocus(); + super.onFinishInflate(); + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { + doHapticKeyClick(); + } + return false; + } + + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + if (event.getAction() == KeyEvent.ACTION_DOWN) { + return onKeyDown(keyCode, event); + } + return false; + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityCallback.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityCallback.java new file mode 100644 index 000000000000..5b743c1a20c5 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityCallback.java @@ -0,0 +1,51 @@ +/* + * 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; + +public interface KeyguardSecurityCallback { + + /** + * Dismiss the given security screen. + * @param securityVerified true if the user correctly entered credentials for the given screen. + * @param targetUserId a user that needs to be the foreground user at the dismissal completion. + */ + void dismiss(boolean securityVerified, int targetUserId); + + /** + * Manually report user activity to keep the device awake. + */ + void userActivity(); + + /** + * Checks if keyguard is in "verify credentials" mode. + * @return true if user has been asked to verify security. + */ + boolean isVerifyUnlockOnly(); + + /** + * Call to report an unlock attempt. + * @param userId id of the user whose unlock attempt is recorded. + * @param success set to 'true' if user correctly entered security credentials. + * @param timeoutMs timeout in milliseconds to wait before reattempting an unlock. + * Only nonzero if 'success' is false + */ + void reportUnlockAttempt(int userId, boolean success, int timeoutMs); + + /** + * Resets the keyguard view. + */ + void reset(); +} diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java new file mode 100644 index 000000000000..8cdb906a0e56 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java @@ -0,0 +1,546 @@ +/* + * Copyright (C) 2014 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.app.Activity; +import android.app.AlertDialog; +import android.app.admin.DevicePolicyManager; +import android.content.Context; +import android.os.UserHandle; +import android.util.AttributeSet; +import android.util.Log; +import android.util.Slog; +import android.view.LayoutInflater; +import android.view.View; +import android.view.WindowManager; +import android.widget.FrameLayout; + +import com.android.internal.widget.LockPatternUtils; +import com.android.keyguard.KeyguardSecurityModel.SecurityMode; + +public class KeyguardSecurityContainer extends FrameLayout implements KeyguardSecurityView { + private static final boolean DEBUG = KeyguardConstants.DEBUG; + private static final String TAG = "KeyguardSecurityView"; + + private static final int USER_TYPE_PRIMARY = 1; + private static final int USER_TYPE_WORK_PROFILE = 2; + private static final int USER_TYPE_SECONDARY_USER = 3; + + private KeyguardSecurityModel mSecurityModel; + private LockPatternUtils mLockPatternUtils; + + private KeyguardSecurityViewFlipper mSecurityViewFlipper; + private boolean mIsVerifyUnlockOnly; + private SecurityMode mCurrentSecuritySelection = SecurityMode.Invalid; + private SecurityCallback mSecurityCallback; + + private final KeyguardUpdateMonitor mUpdateMonitor; + + // Used to notify the container when something interesting happens. + public interface SecurityCallback { + public boolean dismiss(boolean authenticated, int targetUserId); + public void userActivity(); + public void onSecurityModeChanged(SecurityMode securityMode, boolean needsInput); + + /** + * @param strongAuth wheher the user has authenticated with strong authentication like + * pattern, password or PIN but not by trust agents or fingerprint + * @param targetUserId a user that needs to be the foreground user at the finish completion. + */ + public void finish(boolean strongAuth, int targetUserId); + public void reset(); + } + + public KeyguardSecurityContainer(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public KeyguardSecurityContainer(Context context) { + this(context, null, 0); + } + + public KeyguardSecurityContainer(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + mSecurityModel = new KeyguardSecurityModel(context); + mLockPatternUtils = new LockPatternUtils(context); + mUpdateMonitor = KeyguardUpdateMonitor.getInstance(mContext); + } + + public void setSecurityCallback(SecurityCallback callback) { + mSecurityCallback = callback; + } + + @Override + public void onResume(int reason) { + if (mCurrentSecuritySelection != SecurityMode.None) { + getSecurityView(mCurrentSecuritySelection).onResume(reason); + } + } + + @Override + public void onPause() { + if (mCurrentSecuritySelection != SecurityMode.None) { + getSecurityView(mCurrentSecuritySelection).onPause(); + } + } + + public void startAppearAnimation() { + if (mCurrentSecuritySelection != SecurityMode.None) { + getSecurityView(mCurrentSecuritySelection).startAppearAnimation(); + } + } + + public boolean startDisappearAnimation(Runnable onFinishRunnable) { + if (mCurrentSecuritySelection != SecurityMode.None) { + return getSecurityView(mCurrentSecuritySelection).startDisappearAnimation( + onFinishRunnable); + } + return false; + } + + public void announceCurrentSecurityMethod() { + View v = (View) getSecurityView(mCurrentSecuritySelection); + if (v != null) { + v.announceForAccessibility(v.getContentDescription()); + } + } + + public CharSequence getCurrentSecurityModeContentDescription() { + View v = (View) getSecurityView(mCurrentSecuritySelection); + if (v != null) { + return v.getContentDescription(); + } + return ""; + } + + private KeyguardSecurityView getSecurityView(SecurityMode securityMode) { + final int securityViewIdForMode = getSecurityViewIdForMode(securityMode); + KeyguardSecurityView view = null; + final int children = mSecurityViewFlipper.getChildCount(); + for (int child = 0; child < children; child++) { + if (mSecurityViewFlipper.getChildAt(child).getId() == securityViewIdForMode) { + view = ((KeyguardSecurityView)mSecurityViewFlipper.getChildAt(child)); + break; + } + } + int layoutId = getLayoutIdFor(securityMode); + if (view == null && layoutId != 0) { + final LayoutInflater inflater = LayoutInflater.from(mContext); + if (DEBUG) Log.v(TAG, "inflating id = " + layoutId); + View v = inflater.inflate(layoutId, mSecurityViewFlipper, false); + mSecurityViewFlipper.addView(v); + updateSecurityView(v); + view = (KeyguardSecurityView)v; + } + + return view; + } + + private void updateSecurityView(View view) { + if (view instanceof KeyguardSecurityView) { + KeyguardSecurityView ksv = (KeyguardSecurityView) view; + ksv.setKeyguardCallback(mCallback); + ksv.setLockPatternUtils(mLockPatternUtils); + } else { + Log.w(TAG, "View " + view + " is not a KeyguardSecurityView"); + } + } + + protected void onFinishInflate() { + mSecurityViewFlipper = (KeyguardSecurityViewFlipper) findViewById(R.id.view_flipper); + mSecurityViewFlipper.setLockPatternUtils(mLockPatternUtils); + } + + public void setLockPatternUtils(LockPatternUtils utils) { + mLockPatternUtils = utils; + mSecurityModel.setLockPatternUtils(utils); + mSecurityViewFlipper.setLockPatternUtils(mLockPatternUtils); + } + + private void showDialog(String title, String message) { + final AlertDialog dialog = new AlertDialog.Builder(mContext) + .setTitle(title) + .setMessage(message) + .setCancelable(false) + .setNeutralButton(R.string.ok, null) + .create(); + if (!(mContext instanceof Activity)) { + dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); + } + dialog.show(); + } + + private void showTimeoutDialog(int userId, int timeoutMs) { + int timeoutInSeconds = (int) timeoutMs / 1000; + int messageId = 0; + + switch (mSecurityModel.getSecurityMode(userId)) { + case Pattern: + messageId = R.string.kg_too_many_failed_pattern_attempts_dialog_message; + break; + case PIN: + messageId = R.string.kg_too_many_failed_pin_attempts_dialog_message; + break; + case Password: + messageId = R.string.kg_too_many_failed_password_attempts_dialog_message; + break; + // These don't have timeout dialogs. + case Invalid: + case None: + case SimPin: + case SimPuk: + break; + } + + if (messageId != 0) { + final String message = mContext.getString(messageId, + KeyguardUpdateMonitor.getInstance(mContext).getFailedUnlockAttempts(userId), + timeoutInSeconds); + showDialog(null, message); + } + } + + private void showAlmostAtWipeDialog(int attempts, int remaining, int userType) { + String message = null; + switch (userType) { + case USER_TYPE_PRIMARY: + message = mContext.getString(R.string.kg_failed_attempts_almost_at_wipe, + attempts, remaining); + break; + case USER_TYPE_SECONDARY_USER: + message = mContext.getString(R.string.kg_failed_attempts_almost_at_erase_user, + attempts, remaining); + break; + case USER_TYPE_WORK_PROFILE: + message = mContext.getString(R.string.kg_failed_attempts_almost_at_erase_profile, + attempts, remaining); + break; + } + showDialog(null, message); + } + + private void showWipeDialog(int attempts, int userType) { + String message = null; + switch (userType) { + case USER_TYPE_PRIMARY: + message = mContext.getString(R.string.kg_failed_attempts_now_wiping, + attempts); + break; + case USER_TYPE_SECONDARY_USER: + message = mContext.getString(R.string.kg_failed_attempts_now_erasing_user, + attempts); + break; + case USER_TYPE_WORK_PROFILE: + message = mContext.getString(R.string.kg_failed_attempts_now_erasing_profile, + attempts); + break; + } + showDialog(null, message); + } + + private void reportFailedUnlockAttempt(int userId, int timeoutMs) { + final KeyguardUpdateMonitor monitor = KeyguardUpdateMonitor.getInstance(mContext); + final int failedAttempts = monitor.getFailedUnlockAttempts(userId) + 1; // +1 for this time + + if (DEBUG) Log.d(TAG, "reportFailedPatternAttempt: #" + failedAttempts); + + final DevicePolicyManager dpm = mLockPatternUtils.getDevicePolicyManager(); + final int failedAttemptsBeforeWipe = + dpm.getMaximumFailedPasswordsForWipe(null, userId); + + final int remainingBeforeWipe = failedAttemptsBeforeWipe > 0 ? + (failedAttemptsBeforeWipe - failedAttempts) + : Integer.MAX_VALUE; // because DPM returns 0 if no restriction + if (remainingBeforeWipe < LockPatternUtils.FAILED_ATTEMPTS_BEFORE_WIPE_GRACE) { + // The user has installed a DevicePolicyManager that requests a user/profile to be wiped + // N attempts. Once we get below the grace period, we post this dialog every time as a + // clear warning until the deletion fires. + // Check which profile has the strictest policy for failed password attempts + final int expiringUser = dpm.getProfileWithMinimumFailedPasswordsForWipe(userId); + int userType = USER_TYPE_PRIMARY; + if (expiringUser == userId) { + // TODO: http://b/23522538 + if (expiringUser != UserHandle.USER_SYSTEM) { + userType = USER_TYPE_SECONDARY_USER; + } + } else if (expiringUser != UserHandle.USER_NULL) { + userType = USER_TYPE_WORK_PROFILE; + } // If USER_NULL, which shouldn't happen, leave it as USER_TYPE_PRIMARY + if (remainingBeforeWipe > 0) { + showAlmostAtWipeDialog(failedAttempts, remainingBeforeWipe, userType); + } else { + // Too many attempts. The device will be wiped shortly. + Slog.i(TAG, "Too many unlock attempts; user " + expiringUser + " will be wiped!"); + showWipeDialog(failedAttempts, userType); + } + } + monitor.reportFailedStrongAuthUnlockAttempt(userId); + mLockPatternUtils.reportFailedPasswordAttempt(userId); + if (timeoutMs > 0) { + mLockPatternUtils.reportPasswordLockout(timeoutMs, userId); + showTimeoutDialog(userId, timeoutMs); + } + } + + /** + * Shows the primary security screen for the user. This will be either the multi-selector + * or the user's security method. + * @param turningOff true if the device is being turned off + */ + void showPrimarySecurityScreen(boolean turningOff) { + SecurityMode securityMode = mSecurityModel.getSecurityMode( + KeyguardUpdateMonitor.getCurrentUser()); + if (DEBUG) Log.v(TAG, "showPrimarySecurityScreen(turningOff=" + turningOff + ")"); + showSecurityScreen(securityMode); + } + + /** + * Shows the next security screen if there is one. + * @param authenticated true if the user entered the correct authentication + * @param targetUserId a user that needs to be the foreground user at the finish (if called) + * completion. + * @return true if keyguard is done + */ + boolean showNextSecurityScreenOrFinish(boolean authenticated, int targetUserId) { + if (DEBUG) Log.d(TAG, "showNextSecurityScreenOrFinish(" + authenticated + ")"); + boolean finish = false; + boolean strongAuth = false; + if (mUpdateMonitor.getUserCanSkipBouncer(targetUserId)) { + finish = true; + } else if (SecurityMode.None == mCurrentSecuritySelection) { + SecurityMode securityMode = mSecurityModel.getSecurityMode(targetUserId); + if (SecurityMode.None == securityMode) { + finish = true; // no security required + } else { + showSecurityScreen(securityMode); // switch to the alternate security view + } + } else if (authenticated) { + switch (mCurrentSecuritySelection) { + case Pattern: + case Password: + case PIN: + strongAuth = true; + finish = true; + break; + + case SimPin: + case SimPuk: + // Shortcut for SIM PIN/PUK to go to directly to user's security screen or home + SecurityMode securityMode = mSecurityModel.getSecurityMode(targetUserId); + if (securityMode != SecurityMode.None + || !mLockPatternUtils.isLockScreenDisabled( + KeyguardUpdateMonitor.getCurrentUser())) { + showSecurityScreen(securityMode); + } else { + finish = true; + } + break; + + default: + Log.v(TAG, "Bad security screen " + mCurrentSecuritySelection + ", fail safe"); + showPrimarySecurityScreen(false); + break; + } + } + if (finish) { + mSecurityCallback.finish(strongAuth, targetUserId); + } + return finish; + } + + /** + * Switches to the given security view unless it's already being shown, in which case + * this is a no-op. + * + * @param securityMode + */ + private void showSecurityScreen(SecurityMode securityMode) { + if (DEBUG) Log.d(TAG, "showSecurityScreen(" + securityMode + ")"); + + if (securityMode == mCurrentSecuritySelection) return; + + KeyguardSecurityView oldView = getSecurityView(mCurrentSecuritySelection); + KeyguardSecurityView newView = getSecurityView(securityMode); + + // Emulate Activity life cycle + if (oldView != null) { + oldView.onPause(); + oldView.setKeyguardCallback(mNullCallback); // ignore requests from old view + } + if (securityMode != SecurityMode.None) { + newView.onResume(KeyguardSecurityView.VIEW_REVEALED); + newView.setKeyguardCallback(mCallback); + } + + // Find and show this child. + final int childCount = mSecurityViewFlipper.getChildCount(); + + final int securityViewIdForMode = getSecurityViewIdForMode(securityMode); + for (int i = 0; i < childCount; i++) { + if (mSecurityViewFlipper.getChildAt(i).getId() == securityViewIdForMode) { + mSecurityViewFlipper.setDisplayedChild(i); + break; + } + } + + mCurrentSecuritySelection = securityMode; + mSecurityCallback.onSecurityModeChanged(securityMode, + securityMode != SecurityMode.None && newView.needsInput()); + } + + private KeyguardSecurityViewFlipper getFlipper() { + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if (child instanceof KeyguardSecurityViewFlipper) { + return (KeyguardSecurityViewFlipper) child; + } + } + return null; + } + + private KeyguardSecurityCallback mCallback = new KeyguardSecurityCallback() { + public void userActivity() { + if (mSecurityCallback != null) { + mSecurityCallback.userActivity(); + } + } + + public void dismiss(boolean authenticated, int targetId) { + mSecurityCallback.dismiss(authenticated, targetId); + } + + public boolean isVerifyUnlockOnly() { + return mIsVerifyUnlockOnly; + } + + public void reportUnlockAttempt(int userId, boolean success, int timeoutMs) { + KeyguardUpdateMonitor monitor = KeyguardUpdateMonitor.getInstance(mContext); + if (success) { + monitor.clearFailedUnlockAttempts(); + mLockPatternUtils.reportSuccessfulPasswordAttempt(userId); + } else { + KeyguardSecurityContainer.this.reportFailedUnlockAttempt(userId, timeoutMs); + } + } + + public void reset() { + mSecurityCallback.reset(); + } + }; + + // The following is used to ignore callbacks from SecurityViews that are no longer current + // (e.g. face unlock). This avoids unwanted asynchronous events from messing with the + // state for the current security method. + private KeyguardSecurityCallback mNullCallback = new KeyguardSecurityCallback() { + @Override + public void userActivity() { } + @Override + public void reportUnlockAttempt(int userId, boolean success, int timeoutMs) { } + @Override + public boolean isVerifyUnlockOnly() { return false; } + @Override + public void dismiss(boolean securityVerified, int targetUserId) { } + @Override + public void reset() {} + }; + + private int getSecurityViewIdForMode(SecurityMode securityMode) { + switch (securityMode) { + case Pattern: return R.id.keyguard_pattern_view; + case PIN: return R.id.keyguard_pin_view; + case Password: return R.id.keyguard_password_view; + case SimPin: return R.id.keyguard_sim_pin_view; + case SimPuk: return R.id.keyguard_sim_puk_view; + } + return 0; + } + + protected int getLayoutIdFor(SecurityMode securityMode) { + switch (securityMode) { + case Pattern: return R.layout.keyguard_pattern_view; + case PIN: return R.layout.keyguard_pin_view; + case Password: return R.layout.keyguard_password_view; + case SimPin: return R.layout.keyguard_sim_pin_view; + case SimPuk: return R.layout.keyguard_sim_puk_view; + default: + return 0; + } + } + + public SecurityMode getSecurityMode() { + return mSecurityModel.getSecurityMode(KeyguardUpdateMonitor.getCurrentUser()); + } + + public SecurityMode getCurrentSecurityMode() { + return mCurrentSecuritySelection; + } + + public void verifyUnlock() { + mIsVerifyUnlockOnly = true; + showSecurityScreen(getSecurityMode()); + } + + public SecurityMode getCurrentSecuritySelection() { + return mCurrentSecuritySelection; + } + + public void dismiss(boolean authenticated, int targetUserId) { + mCallback.dismiss(authenticated, targetUserId); + } + + public boolean needsInput() { + return mSecurityViewFlipper.needsInput(); + } + + @Override + public void setKeyguardCallback(KeyguardSecurityCallback callback) { + mSecurityViewFlipper.setKeyguardCallback(callback); + } + + @Override + public void reset() { + mSecurityViewFlipper.reset(); + } + + @Override + public KeyguardSecurityCallback getCallback() { + return mSecurityViewFlipper.getCallback(); + } + + @Override + public void showPromptReason(int reason) { + if (mCurrentSecuritySelection != SecurityMode.None) { + if (reason != PROMPT_REASON_NONE) { + Log.i(TAG, "Strong auth required, reason: " + reason); + } + getSecurityView(mCurrentSecuritySelection).showPromptReason(reason); + } + } + + + public void showMessage(String message, int color) { + if (mCurrentSecuritySelection != SecurityMode.None) { + getSecurityView(mCurrentSecuritySelection).showMessage(message, color); + } + } + + @Override + public void showUsabilityHint() { + mSecurityViewFlipper.showUsabilityHint(); + } + +} + diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityModel.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityModel.java new file mode 100644 index 000000000000..7baa57e7dae7 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityModel.java @@ -0,0 +1,91 @@ +/* + * 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.app.admin.DevicePolicyManager; +import android.content.Context; +import android.telephony.SubscriptionManager; + +import com.android.internal.telephony.IccCardConstants; +import com.android.internal.widget.LockPatternUtils; + +public class KeyguardSecurityModel { + + /** + * The different types of security available. + * @see KeyguardSecurityContainer#showSecurityScreen + */ + public enum SecurityMode { + Invalid, // NULL state + None, // No security enabled + Pattern, // Unlock by drawing a pattern. + Password, // Unlock by entering an alphanumeric password + PIN, // Strictly numeric password + SimPin, // Unlock by entering a sim pin. + SimPuk // Unlock by entering a sim puk + } + + private final Context mContext; + private final boolean mIsPukScreenAvailable; + + private LockPatternUtils mLockPatternUtils; + + KeyguardSecurityModel(Context context) { + mContext = context; + mLockPatternUtils = new LockPatternUtils(context); + mIsPukScreenAvailable = mContext.getResources().getBoolean( + com.android.internal.R.bool.config_enable_puk_unlock_screen); + } + + void setLockPatternUtils(LockPatternUtils utils) { + mLockPatternUtils = utils; + } + + SecurityMode getSecurityMode(int userId) { + KeyguardUpdateMonitor monitor = KeyguardUpdateMonitor.getInstance(mContext); + + if (SubscriptionManager.isValidSubscriptionId( + monitor.getNextSubIdForState(IccCardConstants.State.PIN_REQUIRED))) { + return SecurityMode.SimPin; + } + + if (mIsPukScreenAvailable && SubscriptionManager.isValidSubscriptionId( + monitor.getNextSubIdForState(IccCardConstants.State.PUK_REQUIRED))) { + return SecurityMode.SimPuk; + } + + final int security = mLockPatternUtils.getActivePasswordQuality(userId); + switch (security) { + case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC: + case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX: + return SecurityMode.PIN; + + case DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC: + case DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC: + case DevicePolicyManager.PASSWORD_QUALITY_COMPLEX: + case DevicePolicyManager.PASSWORD_QUALITY_MANAGED: + return SecurityMode.Password; + + case DevicePolicyManager.PASSWORD_QUALITY_SOMETHING: + return SecurityMode.Pattern; + case DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED: + return SecurityMode.None; + + default: + throw new IllegalStateException("Unknown security quality:" + security); + } + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java new file mode 100644 index 000000000000..829084202f5a --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java @@ -0,0 +1,130 @@ +/* + * 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 com.android.internal.widget.LockPatternUtils; + +public interface KeyguardSecurityView { + static public final int SCREEN_ON = 1; + static public final int VIEW_REVEALED = 2; + + int PROMPT_REASON_NONE = 0; + + /** + * Strong auth is required because the device has just booted. + */ + int PROMPT_REASON_RESTART = 1; + + /** + * Strong auth is required because the user hasn't used strong auth since a while. + */ + int PROMPT_REASON_TIMEOUT = 2; + + /** + * Strong auth is required because a device admin requested it. + */ + int PROMPT_REASON_DEVICE_ADMIN = 3; + + /** + * Some auth is required because the user force locked. + */ + int PROMPT_REASON_USER_REQUEST = 4; + + /** + * Some auth is required because too many wrong credentials led to a lockout. + */ + int PROMPT_REASON_AFTER_LOCKOUT = 5; + + /** + * Interface back to keyguard to tell it when security + * @param callback + */ + void setKeyguardCallback(KeyguardSecurityCallback callback); + + /** + * Set {@link LockPatternUtils} object. Useful for providing a mock interface. + * @param utils + */ + void setLockPatternUtils(LockPatternUtils utils); + + /** + * Reset the view and prepare to take input. This should do things like clearing the + * password or pattern and clear error messages. + */ + void reset(); + + /** + * Emulate activity life cycle within the view. When called, the view should clean up + * and prepare to be removed. + */ + void onPause(); + + /** + * Emulate activity life cycle within this view. When called, the view should prepare itself + * to be shown. + * @param reason the root cause of the event. + */ + void onResume(int reason); + + /** + * Inquire whether this view requires IME (keyboard) interaction. + * + * @return true if IME interaction is required. + */ + boolean needsInput(); + + /** + * Get {@link KeyguardSecurityCallback} for the given object + * @return KeyguardSecurityCallback + */ + KeyguardSecurityCallback getCallback(); + + /** + * Show a string explaining why the security view needs to be solved. + * + * @param reason a flag indicating which string should be shown, see {@link #PROMPT_REASON_NONE} + * and {@link #PROMPT_REASON_RESTART} + */ + void showPromptReason(int reason); + + /** + * Show a message on the security view with a specified color + * + * @param message the message to show + * @param color the color to use + */ + void showMessage(String message, int color); + + /** + * Instruct the view to show usability hints, if any. + * + */ + void showUsabilityHint(); + + /** + * Starts the animation which should run when the security view appears. + */ + void startAppearAnimation(); + + /** + * Starts the animation which should run when the security view disappears. + * + * @param finishRunnable the runnable to be run when the animation ended + * @return true if an animation started and {@code finishRunnable} will be run, false if no + * animation started and {@code finishRunnable} will not be run + */ + boolean startDisappearAnimation(Runnable finishRunnable); +} diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipper.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipper.java new file mode 100644 index 000000000000..6012c4501412 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipper.java @@ -0,0 +1,301 @@ +/* + * 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.annotation.NonNull; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewDebug; +import android.view.ViewGroup; +import android.view.ViewHierarchyEncoder; +import android.view.WindowManager; +import android.widget.FrameLayout; +import android.widget.ViewFlipper; + +import com.android.internal.widget.LockPatternUtils; + +import java.lang.Override; + +/** + * Subclass of the current view flipper that allows us to overload dispatchTouchEvent() so + * we can emulate {@link WindowManager.LayoutParams#FLAG_SLIPPERY} within a view hierarchy. + * + */ +public class KeyguardSecurityViewFlipper extends ViewFlipper implements KeyguardSecurityView { + private static final String TAG = "KeyguardSecurityViewFlipper"; + private static final boolean DEBUG = KeyguardConstants.DEBUG; + + private Rect mTempRect = new Rect(); + + public KeyguardSecurityViewFlipper(Context context) { + this(context, null); + } + + public KeyguardSecurityViewFlipper(Context context, AttributeSet attr) { + super(context, attr); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + boolean result = super.onTouchEvent(ev); + mTempRect.set(0, 0, 0, 0); + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if (child.getVisibility() == View.VISIBLE) { + offsetRectIntoDescendantCoords(child, mTempRect); + ev.offsetLocation(mTempRect.left, mTempRect.top); + result = child.dispatchTouchEvent(ev) || result; + ev.offsetLocation(-mTempRect.left, -mTempRect.top); + } + } + return result; + } + + KeyguardSecurityView getSecurityView() { + View child = getChildAt(getDisplayedChild()); + if (child instanceof KeyguardSecurityView) { + return (KeyguardSecurityView) child; + } + return null; + } + + @Override + public void setKeyguardCallback(KeyguardSecurityCallback callback) { + KeyguardSecurityView ksv = getSecurityView(); + if (ksv != null) { + ksv.setKeyguardCallback(callback); + } + } + + @Override + public void setLockPatternUtils(LockPatternUtils utils) { + KeyguardSecurityView ksv = getSecurityView(); + if (ksv != null) { + ksv.setLockPatternUtils(utils); + } + } + + @Override + public void reset() { + KeyguardSecurityView ksv = getSecurityView(); + if (ksv != null) { + ksv.reset(); + } + } + + @Override + public void onPause() { + KeyguardSecurityView ksv = getSecurityView(); + if (ksv != null) { + ksv.onPause(); + } + } + + @Override + public void onResume(int reason) { + KeyguardSecurityView ksv = getSecurityView(); + if (ksv != null) { + ksv.onResume(reason); + } + } + + @Override + public boolean needsInput() { + KeyguardSecurityView ksv = getSecurityView(); + return (ksv != null) ? ksv.needsInput() : false; + } + + @Override + public KeyguardSecurityCallback getCallback() { + KeyguardSecurityView ksv = getSecurityView(); + return (ksv != null) ? ksv.getCallback() : null; + } + + @Override + public void showPromptReason(int reason) { + KeyguardSecurityView ksv = getSecurityView(); + if (ksv != null) { + ksv.showPromptReason(reason); + } + } + + @Override + public void showMessage(String message, int color) { + KeyguardSecurityView ksv = getSecurityView(); + if (ksv != null) { + ksv.showMessage(message, color); + } + } + + @Override + public void showUsabilityHint() { + KeyguardSecurityView ksv = getSecurityView(); + if (ksv != null) { + ksv.showUsabilityHint(); + } + } + + @Override + public void startAppearAnimation() { + KeyguardSecurityView ksv = getSecurityView(); + if (ksv != null) { + ksv.startAppearAnimation(); + } + } + + @Override + public boolean startDisappearAnimation(Runnable finishRunnable) { + KeyguardSecurityView ksv = getSecurityView(); + if (ksv != null) { + return ksv.startDisappearAnimation(finishRunnable); + } else { + return false; + } + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams; + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams ? new LayoutParams((LayoutParams) p) : new LayoutParams(p); + } + + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return new LayoutParams(getContext(), attrs); + } + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + final int widthMode = MeasureSpec.getMode(widthSpec); + final int heightMode = MeasureSpec.getMode(heightSpec); + if (DEBUG && widthMode != MeasureSpec.AT_MOST) { + Log.w(TAG, "onMeasure: widthSpec " + MeasureSpec.toString(widthSpec) + + " should be AT_MOST"); + } + if (DEBUG && heightMode != MeasureSpec.AT_MOST) { + Log.w(TAG, "onMeasure: heightSpec " + MeasureSpec.toString(heightSpec) + + " should be AT_MOST"); + } + + final int widthSize = MeasureSpec.getSize(widthSpec); + final int heightSize = MeasureSpec.getSize(heightSpec); + int maxWidth = widthSize; + int maxHeight = heightSize; + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + if (lp.maxWidth > 0 && lp.maxWidth < maxWidth) { + maxWidth = lp.maxWidth; + } + if (lp.maxHeight > 0 && lp.maxHeight < maxHeight) { + maxHeight = lp.maxHeight; + } + } + + final int wPadding = getPaddingLeft() + getPaddingRight(); + final int hPadding = getPaddingTop() + getPaddingBottom(); + maxWidth = Math.max(0, maxWidth - wPadding); + maxHeight = Math.max(0, maxHeight - hPadding); + + int width = widthMode == MeasureSpec.EXACTLY ? widthSize : 0; + int height = heightMode == MeasureSpec.EXACTLY ? heightSize : 0; + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + final int childWidthSpec = makeChildMeasureSpec(maxWidth, lp.width); + final int childHeightSpec = makeChildMeasureSpec(maxHeight, lp.height); + + child.measure(childWidthSpec, childHeightSpec); + + width = Math.max(width, Math.min(child.getMeasuredWidth(), widthSize - wPadding)); + height = Math.max(height, Math.min(child.getMeasuredHeight(), heightSize - hPadding)); + } + setMeasuredDimension(width + wPadding, height + hPadding); + } + + private int makeChildMeasureSpec(int maxSize, int childDimen) { + final int mode; + final int size; + switch (childDimen) { + case LayoutParams.WRAP_CONTENT: + mode = MeasureSpec.AT_MOST; + size = maxSize; + break; + case LayoutParams.MATCH_PARENT: + mode = MeasureSpec.EXACTLY; + size = maxSize; + break; + default: + mode = MeasureSpec.EXACTLY; + size = Math.min(maxSize, childDimen); + break; + } + return MeasureSpec.makeMeasureSpec(size, mode); + } + + public static class LayoutParams extends FrameLayout.LayoutParams { + @ViewDebug.ExportedProperty(category = "layout") + public int maxWidth; + + @ViewDebug.ExportedProperty(category = "layout") + public int maxHeight; + + public LayoutParams(ViewGroup.LayoutParams other) { + super(other); + } + + public LayoutParams(LayoutParams other) { + super(other); + + maxWidth = other.maxWidth; + maxHeight = other.maxHeight; + } + + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + + final TypedArray a = c.obtainStyledAttributes(attrs, + R.styleable.KeyguardSecurityViewFlipper_Layout, 0, 0); + maxWidth = a.getDimensionPixelSize( + R.styleable.KeyguardSecurityViewFlipper_Layout_layout_maxWidth, 0); + maxHeight = a.getDimensionPixelSize( + R.styleable.KeyguardSecurityViewFlipper_Layout_layout_maxHeight, 0); + a.recycle(); + } + + /** @hide */ + @Override + protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) { + super.encodeProperties(encoder); + + encoder.addProperty("layout:maxWidth", maxWidth); + encoder.addProperty("layout:maxHeight", maxHeight); + } + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinView.java new file mode 100644 index 000000000000..839d3cec5e96 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinView.java @@ -0,0 +1,318 @@ +/* + * 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 com.android.internal.telephony.ITelephony; +import com.android.internal.telephony.IccCardConstants; +import com.android.internal.telephony.IccCardConstants.State; +import com.android.internal.telephony.PhoneConstants; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.app.AlertDialog; +import android.app.AlertDialog.Builder; +import android.app.Dialog; +import android.app.ProgressDialog; +import android.graphics.Color; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.util.AttributeSet; +import android.util.Log; +import android.view.WindowManager; +import android.widget.ImageView; + +/** + * Displays a PIN pad for unlocking. + */ +public class KeyguardSimPinView extends KeyguardPinBasedInputView { + private static final String LOG_TAG = "KeyguardSimPinView"; + private static final boolean DEBUG = KeyguardConstants.DEBUG_SIM_STATES; + public static final String TAG = "KeyguardSimPinView"; + + private ProgressDialog mSimUnlockProgressDialog = null; + private CheckSimPin mCheckSimPinThread; + + private AlertDialog mRemainingAttemptsDialog; + private int mSubId; + private ImageView mSimImageView; + + KeyguardUpdateMonitorCallback mUpdateMonitorCallback = new KeyguardUpdateMonitorCallback() { + @Override + public void onSimStateChanged(int subId, int slotId, State simState) { + if (DEBUG) Log.v(TAG, "onSimStateChanged(subId=" + subId + ",state=" + simState + ")"); + resetState(); + }; + }; + + public KeyguardSimPinView(Context context) { + this(context, null); + } + + public KeyguardSimPinView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void resetState() { + super.resetState(); + if (DEBUG) Log.v(TAG, "Resetting state"); + KeyguardUpdateMonitor monitor = KeyguardUpdateMonitor.getInstance(mContext); + mSubId = monitor.getNextSubIdForState(IccCardConstants.State.PIN_REQUIRED); + if (SubscriptionManager.isValidSubscriptionId(mSubId)) { + int count = TelephonyManager.getDefault().getSimCount(); + Resources rez = getResources(); + final String msg; + int color = Color.WHITE; + if (count < 2) { + msg = rez.getString(R.string.kg_sim_pin_instructions); + } else { + SubscriptionInfo info = monitor.getSubscriptionInfoForSubId(mSubId); + CharSequence displayName = info != null ? info.getDisplayName() : ""; // don't crash + msg = rez.getString(R.string.kg_sim_pin_instructions_multi, displayName); + if (info != null) { + color = info.getIconTint(); + } + } + mSecurityMessageDisplay.setMessage(msg); + mSimImageView.setImageTintList(ColorStateList.valueOf(color)); + } + } + + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + resetState(); + } + + @Override + protected int getPromtReasonStringRes(int reason) { + // No message on SIM Pin + return 0; + } + + private String getPinPasswordErrorMessage(int attemptsRemaining) { + String displayMessage; + + if (attemptsRemaining == 0) { + displayMessage = getContext().getString(R.string.kg_password_wrong_pin_code_pukked); + } else if (attemptsRemaining > 0) { + displayMessage = getContext().getResources() + .getQuantityString(R.plurals.kg_password_wrong_pin_code, attemptsRemaining, + attemptsRemaining); + } else { + displayMessage = getContext().getString(R.string.kg_password_pin_failed); + } + if (DEBUG) Log.d(LOG_TAG, "getPinPasswordErrorMessage:" + + " attemptsRemaining=" + attemptsRemaining + " displayMessage=" + displayMessage); + return displayMessage; + } + + @Override + protected boolean shouldLockout(long deadline) { + // SIM PIN doesn't have a timed lockout + return false; + } + + @Override + protected int getPasswordTextViewId() { + return R.id.simPinEntry; + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + if (mEcaView instanceof EmergencyCarrierArea) { + ((EmergencyCarrierArea) mEcaView).setCarrierTextVisible(true); + } + mSimImageView = (ImageView) findViewById(R.id.keyguard_sim); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + KeyguardUpdateMonitor.getInstance(mContext).registerCallback(mUpdateMonitorCallback); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + KeyguardUpdateMonitor.getInstance(mContext).removeCallback(mUpdateMonitorCallback); + } + + @Override + public void showUsabilityHint() { + } + + @Override + public void onPause() { + // dismiss the dialog. + if (mSimUnlockProgressDialog != null) { + mSimUnlockProgressDialog.dismiss(); + mSimUnlockProgressDialog = null; + } + } + + /** + * Since the IPC can block, we want to run the request in a separate thread + * with a callback. + */ + private abstract class CheckSimPin extends Thread { + private final String mPin; + private int mSubId; + + protected CheckSimPin(String pin, int subId) { + mPin = pin; + mSubId = subId; + } + + abstract void onSimCheckResponse(final int result, final int attemptsRemaining); + + @Override + public void run() { + try { + if (DEBUG) { + Log.v(TAG, "call supplyPinReportResultForSubscriber(subid=" + mSubId + ")"); + } + final int[] result = ITelephony.Stub.asInterface(ServiceManager + .checkService("phone")).supplyPinReportResultForSubscriber(mSubId, mPin); + if (DEBUG) { + Log.v(TAG, "supplyPinReportResult returned: " + result[0] + " " + result[1]); + } + post(new Runnable() { + @Override + public void run() { + onSimCheckResponse(result[0], result[1]); + } + }); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException for supplyPinReportResult:", e); + post(new Runnable() { + @Override + public void run() { + onSimCheckResponse(PhoneConstants.PIN_GENERAL_FAILURE, -1); + } + }); + } + } + } + + private Dialog getSimUnlockProgressDialog() { + if (mSimUnlockProgressDialog == null) { + mSimUnlockProgressDialog = new ProgressDialog(mContext); + mSimUnlockProgressDialog.setMessage( + mContext.getString(R.string.kg_sim_unlock_progress_dialog_message)); + mSimUnlockProgressDialog.setIndeterminate(true); + mSimUnlockProgressDialog.setCancelable(false); + mSimUnlockProgressDialog.getWindow().setType( + WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); + } + return mSimUnlockProgressDialog; + } + + private Dialog getSimRemainingAttemptsDialog(int remaining) { + String msg = getPinPasswordErrorMessage(remaining); + if (mRemainingAttemptsDialog == null) { + Builder builder = new AlertDialog.Builder(mContext); + builder.setMessage(msg); + builder.setCancelable(false); + builder.setNeutralButton(R.string.ok, null); + mRemainingAttemptsDialog = builder.create(); + mRemainingAttemptsDialog.getWindow().setType( + WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); + } else { + mRemainingAttemptsDialog.setMessage(msg); + } + return mRemainingAttemptsDialog; + } + + @Override + protected void verifyPasswordAndUnlock() { + String entry = mPasswordEntry.getText(); + + if (entry.length() < 4) { + // otherwise, display a message to the user, and don't submit. + mSecurityMessageDisplay.setMessage(R.string.kg_invalid_sim_pin_hint); + resetPasswordText(true /* animate */, true /* announce */); + mCallback.userActivity(); + return; + } + + getSimUnlockProgressDialog().show(); + + if (mCheckSimPinThread == null) { + mCheckSimPinThread = new CheckSimPin(mPasswordEntry.getText(), mSubId) { + @Override + void onSimCheckResponse(final int result, final int attemptsRemaining) { + post(new Runnable() { + @Override + public void run() { + if (mSimUnlockProgressDialog != null) { + mSimUnlockProgressDialog.hide(); + } + resetPasswordText(true /* animate */, + result != PhoneConstants.PIN_RESULT_SUCCESS /* announce */); + if (result == PhoneConstants.PIN_RESULT_SUCCESS) { + KeyguardUpdateMonitor.getInstance(getContext()) + .reportSimUnlocked(mSubId); + mCallback.dismiss(true, KeyguardUpdateMonitor.getCurrentUser()); + } else { + if (result == PhoneConstants.PIN_PASSWORD_INCORRECT) { + if (attemptsRemaining <= 2) { + // this is getting critical - show dialog + getSimRemainingAttemptsDialog(attemptsRemaining).show(); + } else { + // show message + mSecurityMessageDisplay.setMessage( + getPinPasswordErrorMessage(attemptsRemaining)); + } + } else { + // "PIN operation failed!" - no idea what this was and no way to + // find out. :/ + mSecurityMessageDisplay.setMessage(getContext().getString( + R.string.kg_password_pin_failed)); + } + if (DEBUG) Log.d(LOG_TAG, "verifyPasswordAndUnlock " + + " CheckSimPin.onSimCheckResponse: " + result + + " attemptsRemaining=" + attemptsRemaining); + } + mCallback.userActivity(); + mCheckSimPinThread = null; + } + }); + } + }; + mCheckSimPinThread.start(); + } + } + + @Override + public void startAppearAnimation() { + // noop. + } + + @Override + public boolean startDisappearAnimation(Runnable finishRunnable) { + return false; + } +} + diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukView.java new file mode 100644 index 000000000000..3871448cb617 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukView.java @@ -0,0 +1,379 @@ +/* + * 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.content.Context; +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.ProgressDialog; +import android.graphics.Color; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.util.AttributeSet; +import android.util.Log; +import android.view.WindowManager; +import android.widget.ImageView; + +import com.android.internal.telephony.ITelephony; +import com.android.internal.telephony.IccCardConstants; +import com.android.internal.telephony.PhoneConstants; +import com.android.internal.telephony.IccCardConstants.State; + + +/** + * Displays a PIN pad for entering a PUK (Pin Unlock Kode) provided by a carrier. + */ +public class KeyguardSimPukView extends KeyguardPinBasedInputView { + private static final String LOG_TAG = "KeyguardSimPukView"; + private static final boolean DEBUG = KeyguardConstants.DEBUG; + public static final String TAG = "KeyguardSimPukView"; + + private ProgressDialog mSimUnlockProgressDialog = null; + private CheckSimPuk mCheckSimPukThread; + private String mPukText; + private String mPinText; + private StateMachine mStateMachine = new StateMachine(); + private AlertDialog mRemainingAttemptsDialog; + private int mSubId; + private ImageView mSimImageView; + + KeyguardUpdateMonitorCallback mUpdateMonitorCallback = new KeyguardUpdateMonitorCallback() { + @Override + public void onSimStateChanged(int subId, int slotId, State simState) { + if (DEBUG) Log.v(TAG, "onSimStateChanged(subId=" + subId + ",state=" + simState + ")"); + resetState(); + }; + }; + + public KeyguardSimPukView(Context context) { + this(context, null); + } + + public KeyguardSimPukView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + private class StateMachine { + final int ENTER_PUK = 0; + final int ENTER_PIN = 1; + final int CONFIRM_PIN = 2; + final int DONE = 3; + private int state = ENTER_PUK; + + public void next() { + int msg = 0; + if (state == ENTER_PUK) { + if (checkPuk()) { + state = ENTER_PIN; + msg = R.string.kg_puk_enter_pin_hint; + } else { + msg = R.string.kg_invalid_sim_puk_hint; + } + } else if (state == ENTER_PIN) { + if (checkPin()) { + state = CONFIRM_PIN; + msg = R.string.kg_enter_confirm_pin_hint; + } else { + msg = R.string.kg_invalid_sim_pin_hint; + } + } else if (state == CONFIRM_PIN) { + if (confirmPin()) { + state = DONE; + msg = R.string.keyguard_sim_unlock_progress_dialog_message; + updateSim(); + } else { + state = ENTER_PIN; // try again? + msg = R.string.kg_invalid_confirm_pin_hint; + } + } + resetPasswordText(true /* animate */, true /* announce */); + if (msg != 0) { + mSecurityMessageDisplay.setMessage(msg); + } + } + + void reset() { + mPinText=""; + mPukText=""; + state = ENTER_PUK; + KeyguardUpdateMonitor monitor = KeyguardUpdateMonitor.getInstance(mContext); + mSubId = monitor.getNextSubIdForState(IccCardConstants.State.PUK_REQUIRED); + if (SubscriptionManager.isValidSubscriptionId(mSubId)) { + int count = TelephonyManager.getDefault().getSimCount(); + Resources rez = getResources(); + final String msg; + int color = Color.WHITE; + if (count < 2) { + msg = rez.getString(R.string.kg_puk_enter_puk_hint); + } else { + SubscriptionInfo info = monitor.getSubscriptionInfoForSubId(mSubId); + CharSequence displayName = info != null ? info.getDisplayName() : ""; + msg = rez.getString(R.string.kg_puk_enter_puk_hint_multi, displayName); + if (info != null) { + color = info.getIconTint(); + } + } + mSecurityMessageDisplay.setMessage(msg); + mSimImageView.setImageTintList(ColorStateList.valueOf(color)); + } + mPasswordEntry.requestFocus(); + } + } + + @Override + protected int getPromtReasonStringRes(int reason) { + // No message on SIM Puk + return 0; + } + + private String getPukPasswordErrorMessage(int attemptsRemaining) { + String displayMessage; + + if (attemptsRemaining == 0) { + displayMessage = getContext().getString(R.string.kg_password_wrong_puk_code_dead); + } else if (attemptsRemaining > 0) { + displayMessage = getContext().getResources() + .getQuantityString(R.plurals.kg_password_wrong_puk_code, attemptsRemaining, + attemptsRemaining); + } else { + displayMessage = getContext().getString(R.string.kg_password_puk_failed); + } + if (DEBUG) Log.d(LOG_TAG, "getPukPasswordErrorMessage:" + + " attemptsRemaining=" + attemptsRemaining + " displayMessage=" + displayMessage); + return displayMessage; + } + + @Override + public void resetState() { + super.resetState(); + mStateMachine.reset(); + } + + @Override + protected boolean shouldLockout(long deadline) { + // SIM PUK doesn't have a timed lockout + return false; + } + + @Override + protected int getPasswordTextViewId() { + return R.id.pukEntry; + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + if (mEcaView instanceof EmergencyCarrierArea) { + ((EmergencyCarrierArea) mEcaView).setCarrierTextVisible(true); + } + mSimImageView = (ImageView) findViewById(R.id.keyguard_sim); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + KeyguardUpdateMonitor.getInstance(mContext).registerCallback(mUpdateMonitorCallback); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + KeyguardUpdateMonitor.getInstance(mContext).removeCallback(mUpdateMonitorCallback); + } + + @Override + public void showUsabilityHint() { + } + + @Override + public void onPause() { + // dismiss the dialog. + if (mSimUnlockProgressDialog != null) { + mSimUnlockProgressDialog.dismiss(); + mSimUnlockProgressDialog = null; + } + } + + /** + * Since the IPC can block, we want to run the request in a separate thread + * with a callback. + */ + private abstract class CheckSimPuk extends Thread { + + private final String mPin, mPuk; + private final int mSubId; + + protected CheckSimPuk(String puk, String pin, int subId) { + mPuk = puk; + mPin = pin; + mSubId = subId; + } + + abstract void onSimLockChangedResponse(final int result, final int attemptsRemaining); + + @Override + public void run() { + try { + if (DEBUG) Log.v(TAG, "call supplyPukReportResult()"); + final int[] result = ITelephony.Stub.asInterface(ServiceManager + .checkService("phone")).supplyPukReportResultForSubscriber(mSubId, mPuk, mPin); + if (DEBUG) { + Log.v(TAG, "supplyPukReportResult returned: " + result[0] + " " + result[1]); + } + post(new Runnable() { + @Override + public void run() { + onSimLockChangedResponse(result[0], result[1]); + } + }); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException for supplyPukReportResult:", e); + post(new Runnable() { + @Override + public void run() { + onSimLockChangedResponse(PhoneConstants.PIN_GENERAL_FAILURE, -1); + } + }); + } + } + } + + private Dialog getSimUnlockProgressDialog() { + if (mSimUnlockProgressDialog == null) { + mSimUnlockProgressDialog = new ProgressDialog(mContext); + mSimUnlockProgressDialog.setMessage( + mContext.getString(R.string.kg_sim_unlock_progress_dialog_message)); + mSimUnlockProgressDialog.setIndeterminate(true); + mSimUnlockProgressDialog.setCancelable(false); + if (!(mContext instanceof Activity)) { + mSimUnlockProgressDialog.getWindow().setType( + WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); + } + } + return mSimUnlockProgressDialog; + } + + private Dialog getPukRemainingAttemptsDialog(int remaining) { + String msg = getPukPasswordErrorMessage(remaining); + if (mRemainingAttemptsDialog == null) { + AlertDialog.Builder builder = new AlertDialog.Builder(mContext); + builder.setMessage(msg); + builder.setCancelable(false); + builder.setNeutralButton(R.string.ok, null); + mRemainingAttemptsDialog = builder.create(); + mRemainingAttemptsDialog.getWindow().setType( + WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); + } else { + mRemainingAttemptsDialog.setMessage(msg); + } + return mRemainingAttemptsDialog; + } + + private boolean checkPuk() { + // make sure the puk is at least 8 digits long. + if (mPasswordEntry.getText().length() == 8) { + mPukText = mPasswordEntry.getText(); + return true; + } + return false; + } + + private boolean checkPin() { + // make sure the PIN is between 4 and 8 digits + int length = mPasswordEntry.getText().length(); + if (length >= 4 && length <= 8) { + mPinText = mPasswordEntry.getText(); + return true; + } + return false; + } + + public boolean confirmPin() { + return mPinText.equals(mPasswordEntry.getText()); + } + + private void updateSim() { + getSimUnlockProgressDialog().show(); + + if (mCheckSimPukThread == null) { + mCheckSimPukThread = new CheckSimPuk(mPukText, mPinText, mSubId) { + @Override + void onSimLockChangedResponse(final int result, final int attemptsRemaining) { + post(new Runnable() { + @Override + public void run() { + if (mSimUnlockProgressDialog != null) { + mSimUnlockProgressDialog.hide(); + } + resetPasswordText(true /* animate */, + result != PhoneConstants.PIN_RESULT_SUCCESS /* announce */); + if (result == PhoneConstants.PIN_RESULT_SUCCESS) { + KeyguardUpdateMonitor.getInstance(getContext()) + .reportSimUnlocked(mSubId); + mCallback.dismiss(true, KeyguardUpdateMonitor.getCurrentUser()); + } else { + if (result == PhoneConstants.PIN_PASSWORD_INCORRECT) { + if (attemptsRemaining <= 2) { + // this is getting critical - show dialog + getPukRemainingAttemptsDialog(attemptsRemaining).show(); + } else { + // show message + mSecurityMessageDisplay.setMessage( + getPukPasswordErrorMessage(attemptsRemaining)); + } + } else { + mSecurityMessageDisplay.setMessage(getContext().getString( + R.string.kg_password_puk_failed)); + } + if (DEBUG) Log.d(LOG_TAG, "verifyPasswordAndUnlock " + + " UpdateSim.onSimCheckResponse: " + + " attemptsRemaining=" + attemptsRemaining); + mStateMachine.reset(); + } + mCheckSimPukThread = null; + } + }); + } + }; + mCheckSimPukThread.start(); + } + } + + @Override + protected void verifyPasswordAndUnlock() { + mStateMachine.next(); + } + + @Override + public void startAppearAnimation() { + // noop. + } + + @Override + public boolean startDisappearAnimation(Runnable finishRunnable) { + return false; + } +} + + diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java new file mode 100644 index 000000000000..f8f4f2a8b8fc --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java @@ -0,0 +1,282 @@ +/* + * 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.app.ActivityManager; +import android.app.AlarmManager; +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.os.UserHandle; +import android.text.TextUtils; +import android.text.format.DateFormat; +import android.util.AttributeSet; +import android.util.Log; +import android.util.Slog; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; +import android.widget.GridLayout; +import android.widget.TextClock; +import android.widget.TextView; + +import com.android.internal.widget.LockPatternUtils; + +import java.util.Locale; + +public class KeyguardStatusView extends GridLayout { + private static final boolean DEBUG = KeyguardConstants.DEBUG; + private static final String TAG = "KeyguardStatusView"; + + private final LockPatternUtils mLockPatternUtils; + private final AlarmManager mAlarmManager; + + private TextView mAlarmStatusView; + private TextClock mDateView; + private TextClock mClockView; + private TextView mOwnerInfo; + private ViewGroup mClockContainer; + + private KeyguardUpdateMonitorCallback mInfoCallback = new KeyguardUpdateMonitorCallback() { + + @Override + public void onTimeChanged() { + refresh(); + } + + @Override + public void onKeyguardVisibilityChanged(boolean showing) { + if (showing) { + if (DEBUG) Slog.v(TAG, "refresh statusview showing:" + showing); + refresh(); + updateOwnerInfo(); + } + } + + @Override + public void onStartedWakingUp() { + setEnableMarquee(true); + } + + @Override + public void onFinishedGoingToSleep(int why) { + setEnableMarquee(false); + } + + @Override + public void onUserSwitchComplete(int userId) { + refresh(); + updateOwnerInfo(); + } + }; + + 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); + mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + mLockPatternUtils = new LockPatternUtils(getContext()); + } + + private void setEnableMarquee(boolean enabled) { + if (DEBUG) Log.v(TAG, (enabled ? "Enable" : "Disable") + " transport text marquee"); + if (mAlarmStatusView != null) mAlarmStatusView.setSelected(enabled); + if (mOwnerInfo != null) mOwnerInfo.setSelected(enabled); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mClockContainer = (ViewGroup) findViewById(R.id.keyguard_clock_container); + mAlarmStatusView = (TextView) findViewById(R.id.alarm_status); + mDateView = (TextClock) findViewById(R.id.date_view); + mClockView = (TextClock) findViewById(R.id.clock_view); + mDateView.setShowCurrentUserTime(true); + mClockView.setShowCurrentUserTime(true); + mOwnerInfo = (TextView) findViewById(R.id.owner_info); + + boolean shouldMarquee = KeyguardUpdateMonitor.getInstance(mContext).isDeviceInteractive(); + setEnableMarquee(shouldMarquee); + refresh(); + updateOwnerInfo(); + + // Disable elegant text height because our fancy colon makes the ymin value huge for no + // reason. + mClockView.setElegantTextHeight(false); + } + + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + mClockView.setTextSize(TypedValue.COMPLEX_UNIT_PX, + getResources().getDimensionPixelSize(R.dimen.widget_big_font_size)); + // Some layouts like burmese have a different margin for the clock + MarginLayoutParams layoutParams = (MarginLayoutParams) mClockView.getLayoutParams(); + layoutParams.bottomMargin = getResources().getDimensionPixelSize( + R.dimen.bottom_text_spacing_digital); + mClockView.setLayoutParams(layoutParams); + mDateView.setTextSize(TypedValue.COMPLEX_UNIT_PX, + getResources().getDimensionPixelSize(R.dimen.widget_label_font_size)); + mOwnerInfo.setTextSize(TypedValue.COMPLEX_UNIT_PX, + getResources().getDimensionPixelSize(R.dimen.widget_label_font_size)); + } + + public void refreshTime() { + mDateView.setFormat24Hour(Patterns.dateView); + mDateView.setFormat12Hour(Patterns.dateView); + + mClockView.setFormat12Hour(Patterns.clockView12); + mClockView.setFormat24Hour(Patterns.clockView24); + } + + private void refresh() { + AlarmManager.AlarmClockInfo nextAlarm = + mAlarmManager.getNextAlarmClock(UserHandle.USER_CURRENT); + Patterns.update(mContext, nextAlarm != null); + + refreshTime(); + refreshAlarmStatus(nextAlarm); + } + + void refreshAlarmStatus(AlarmManager.AlarmClockInfo nextAlarm) { + if (nextAlarm != null) { + String alarm = formatNextAlarm(mContext, nextAlarm); + mAlarmStatusView.setText(alarm); + mAlarmStatusView.setContentDescription( + getResources().getString(R.string.keyguard_accessibility_next_alarm, alarm)); + mAlarmStatusView.setVisibility(View.VISIBLE); + } else { + mAlarmStatusView.setVisibility(View.GONE); + } + } + + public int getClockBottom() { + return mClockView.getBottom() + + ((MarginLayoutParams) mClockView.getLayoutParams()).bottomMargin; + } + + public static String formatNextAlarm(Context context, AlarmManager.AlarmClockInfo info) { + if (info == null) { + return ""; + } + String skeleton = DateFormat.is24HourFormat(context, ActivityManager.getCurrentUser()) + ? "EHm" + : "Ehma"; + String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton); + return DateFormat.format(pattern, info.getTriggerTime()).toString(); + } + + private void updateOwnerInfo() { + if (mOwnerInfo == null) return; + String ownerInfo = getOwnerInfo(); + if (!TextUtils.isEmpty(ownerInfo)) { + mOwnerInfo.setVisibility(View.VISIBLE); + mOwnerInfo.setText(ownerInfo); + } else { + mOwnerInfo.setVisibility(View.GONE); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + KeyguardUpdateMonitor.getInstance(mContext).registerCallback(mInfoCallback); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + KeyguardUpdateMonitor.getInstance(mContext).removeCallback(mInfoCallback); + } + + private String getOwnerInfo() { + String info = null; + if (mLockPatternUtils.isDeviceOwnerInfoEnabled()) { + // Use the device owner information set by device policy client via + // device policy manager. + info = mLockPatternUtils.getDeviceOwnerInfo(); + } else { + // Use the current user owner information if enabled. + final boolean ownerInfoEnabled = mLockPatternUtils.isOwnerInfoEnabled( + KeyguardUpdateMonitor.getCurrentUser()); + if (ownerInfoEnabled) { + info = mLockPatternUtils.getOwnerInfo(KeyguardUpdateMonitor.getCurrentUser()); + } + } + return info; + } + + @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 dateView; + static String clockView12; + static String clockView24; + static String cacheKey; + + static void update(Context context, boolean hasAlarm) { + final Locale locale = Locale.getDefault(); + final Resources res = context.getResources(); + final String dateViewSkel = res.getString(hasAlarm + ? R.string.abbrev_wday_month_day_no_year_alarm + : R.string.abbrev_wday_month_day_no_year); + 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() + dateViewSkel + clockView12Skel + clockView24Skel; + if (key.equals(cacheKey)) return; + + dateView = DateFormat.getBestDateTimePattern(locale, dateViewSkel); + + 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 setDark(boolean dark) { + final int N = mClockContainer.getChildCount(); + for (int i = 0; i < N; i++) { + View child = mClockContainer.getChildAt(i); + if (child == mClockView) { + continue; + } + child.setAlpha(dark ? 0 : 1); + } + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java new file mode 100644 index 000000000000..80d4a264cac8 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -0,0 +1,1798 @@ +/* + * Copyright (C) 2008 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 static android.content.Intent.ACTION_USER_UNLOCKED; +import static android.os.BatteryManager.BATTERY_HEALTH_UNKNOWN; +import static android.os.BatteryManager.BATTERY_STATUS_FULL; +import static android.os.BatteryManager.BATTERY_STATUS_UNKNOWN; +import static android.os.BatteryManager.EXTRA_HEALTH; +import static android.os.BatteryManager.EXTRA_LEVEL; +import static android.os.BatteryManager.EXTRA_MAX_CHARGING_CURRENT; +import static android.os.BatteryManager.EXTRA_MAX_CHARGING_VOLTAGE; +import static android.os.BatteryManager.EXTRA_PLUGGED; +import static android.os.BatteryManager.EXTRA_STATUS; + +import android.app.ActivityManager; +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.app.UserSwitchObserver; +import android.app.admin.DevicePolicyManager; +import android.app.trust.TrustManager; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.database.ContentObserver; +import android.graphics.Bitmap; +import android.hardware.fingerprint.FingerprintManager; +import android.hardware.fingerprint.FingerprintManager.AuthenticationCallback; +import android.hardware.fingerprint.FingerprintManager.AuthenticationResult; +import android.media.AudioManager; +import android.os.BatteryManager; +import android.os.CancellationSignal; +import android.os.Handler; +import android.os.IRemoteCallback; +import android.os.Message; +import android.os.RemoteException; +import android.os.SystemClock; +import android.os.Trace; +import android.os.UserHandle; +import android.os.UserManager; +import android.provider.Settings; +import android.telephony.ServiceState; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener; +import android.telephony.TelephonyManager; +import android.util.ArraySet; +import android.util.Log; +import android.util.SparseBooleanArray; +import android.util.SparseIntArray; + +import com.google.android.collect.Lists; + +import com.android.internal.telephony.IccCardConstants; +import com.android.internal.telephony.IccCardConstants.State; +import com.android.internal.telephony.PhoneConstants; +import com.android.internal.telephony.TelephonyIntents; +import com.android.internal.widget.LockPatternUtils; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map.Entry; + +/** + * Watches for updates that may be interesting to the keyguard, and provides + * the up to date information as well as a registration for callbacks that care + * to be updated. + * + * Note: under time crunch, this has been extended to include some stuff that + * doesn't really belong here. see {@link #handleBatteryUpdate} where it shutdowns + * the device, and {@link #getFailedUnlockAttempts()}, {@link #reportFailedAttempt()} + * and {@link #clearFailedUnlockAttempts()}. Maybe we should rename this 'KeyguardContext'... + */ +public class KeyguardUpdateMonitor implements TrustManager.TrustListener { + + private static final String TAG = "KeyguardUpdateMonitor"; + private static final boolean DEBUG = KeyguardConstants.DEBUG; + private static final boolean DEBUG_SIM_STATES = KeyguardConstants.DEBUG_SIM_STATES; + private static final int LOW_BATTERY_THRESHOLD = 20; + + private static final String ACTION_FACE_UNLOCK_STARTED + = "com.android.facelock.FACE_UNLOCK_STARTED"; + private static final String ACTION_FACE_UNLOCK_STOPPED + = "com.android.facelock.FACE_UNLOCK_STOPPED"; + + // Callback messages + private static final int MSG_TIME_UPDATE = 301; + private static final int MSG_BATTERY_UPDATE = 302; + private static final int MSG_SIM_STATE_CHANGE = 304; + private static final int MSG_RINGER_MODE_CHANGED = 305; + private static final int MSG_PHONE_STATE_CHANGED = 306; + private static final int MSG_DEVICE_PROVISIONED = 308; + private static final int MSG_DPM_STATE_CHANGED = 309; + private static final int MSG_USER_SWITCHING = 310; + private static final int MSG_KEYGUARD_RESET = 312; + private static final int MSG_BOOT_COMPLETED = 313; + private static final int MSG_USER_SWITCH_COMPLETE = 314; + private static final int MSG_USER_INFO_CHANGED = 317; + private static final int MSG_REPORT_EMERGENCY_CALL_ACTION = 318; + private static final int MSG_STARTED_WAKING_UP = 319; + private static final int MSG_FINISHED_GOING_TO_SLEEP = 320; + private static final int MSG_STARTED_GOING_TO_SLEEP = 321; + private static final int MSG_KEYGUARD_BOUNCER_CHANGED = 322; + private static final int MSG_FACE_UNLOCK_STATE_CHANGED = 327; + private static final int MSG_SIM_SUBSCRIPTION_INFO_CHANGED = 328; + private static final int MSG_AIRPLANE_MODE_CHANGED = 329; + private static final int MSG_SERVICE_STATE_CHANGE = 330; + private static final int MSG_SCREEN_TURNED_ON = 331; + private static final int MSG_SCREEN_TURNED_OFF = 332; + private static final int MSG_DREAMING_STATE_CHANGED = 333; + private static final int MSG_USER_UNLOCKED = 334; + + /** Fingerprint state: Not listening to fingerprint. */ + private static final int FINGERPRINT_STATE_STOPPED = 0; + + /** Fingerprint state: Listening. */ + private static final int FINGERPRINT_STATE_RUNNING = 1; + + /** + * Fingerprint state: Cancelling and waiting for the confirmation from FingerprintService to + * send us the confirmation that cancellation has happened. + */ + private static final int FINGERPRINT_STATE_CANCELLING = 2; + + /** + * Fingerprint state: During cancelling we got another request to start listening, so when we + * receive the cancellation done signal, we should start listening again. + */ + private static final int FINGERPRINT_STATE_CANCELLING_RESTARTING = 3; + + private static final int DEFAULT_CHARGING_VOLTAGE_MICRO_VOLT = 5000000; + + private static final ComponentName FALLBACK_HOME_COMPONENT = new ComponentName( + "com.android.settings", "com.android.settings.FallbackHome"); + + private static KeyguardUpdateMonitor sInstance; + + private final Context mContext; + HashMap<Integer, SimData> mSimDatas = new HashMap<Integer, SimData>(); + HashMap<Integer, ServiceState> mServiceStates = new HashMap<Integer, ServiceState>(); + + private int mRingMode; + private int mPhoneState; + private boolean mKeyguardIsVisible; + + /** + * If true, fingerprint was already authenticated and we don't need to start listening again + * until the Keyguard has been dismissed. + */ + private boolean mFingerprintAlreadyAuthenticated; + private boolean mGoingToSleep; + private boolean mBouncer; + private boolean mBootCompleted; + private boolean mNeedsSlowUnlockTransition; + private boolean mHasLockscreenWallpaper; + + // Device provisioning state + private boolean mDeviceProvisioned; + + // Battery status + private BatteryStatus mBatteryStatus; + + // Password attempts + private SparseIntArray mFailedAttempts = new SparseIntArray(); + + private final StrongAuthTracker mStrongAuthTracker; + + private final ArrayList<WeakReference<KeyguardUpdateMonitorCallback>> + mCallbacks = Lists.newArrayList(); + private ContentObserver mDeviceProvisionedObserver; + + private boolean mSwitchingUser; + + private boolean mDeviceInteractive; + private boolean mScreenOn; + private SubscriptionManager mSubscriptionManager; + private List<SubscriptionInfo> mSubscriptionInfo; + private TrustManager mTrustManager; + private UserManager mUserManager; + private int mFingerprintRunningState = FINGERPRINT_STATE_STOPPED; + private LockPatternUtils mLockPatternUtils; + + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_TIME_UPDATE: + handleTimeUpdate(); + break; + case MSG_BATTERY_UPDATE: + handleBatteryUpdate((BatteryStatus) msg.obj); + break; + case MSG_SIM_STATE_CHANGE: + handleSimStateChange(msg.arg1, msg.arg2, (State) msg.obj); + break; + case MSG_RINGER_MODE_CHANGED: + handleRingerModeChange(msg.arg1); + break; + case MSG_PHONE_STATE_CHANGED: + handlePhoneStateChanged((String) msg.obj); + break; + case MSG_DEVICE_PROVISIONED: + handleDeviceProvisioned(); + break; + case MSG_DPM_STATE_CHANGED: + handleDevicePolicyManagerStateChanged(); + break; + case MSG_USER_SWITCHING: + handleUserSwitching(msg.arg1, (IRemoteCallback) msg.obj); + break; + case MSG_USER_SWITCH_COMPLETE: + handleUserSwitchComplete(msg.arg1); + break; + case MSG_KEYGUARD_RESET: + handleKeyguardReset(); + break; + case MSG_KEYGUARD_BOUNCER_CHANGED: + handleKeyguardBouncerChanged(msg.arg1); + break; + case MSG_BOOT_COMPLETED: + handleBootCompleted(); + break; + case MSG_USER_INFO_CHANGED: + handleUserInfoChanged(msg.arg1); + break; + case MSG_REPORT_EMERGENCY_CALL_ACTION: + handleReportEmergencyCallAction(); + break; + case MSG_STARTED_GOING_TO_SLEEP: + handleStartedGoingToSleep(msg.arg1); + break; + case MSG_FINISHED_GOING_TO_SLEEP: + handleFinishedGoingToSleep(msg.arg1); + break; + case MSG_STARTED_WAKING_UP: + Trace.beginSection("KeyguardUpdateMonitor#handler MSG_STARTED_WAKING_UP"); + handleStartedWakingUp(); + Trace.endSection(); + break; + case MSG_FACE_UNLOCK_STATE_CHANGED: + Trace.beginSection("KeyguardUpdateMonitor#handler MSG_FACE_UNLOCK_STATE_CHANGED"); + handleFaceUnlockStateChanged(msg.arg1 != 0, msg.arg2); + Trace.endSection(); + break; + case MSG_SIM_SUBSCRIPTION_INFO_CHANGED: + handleSimSubscriptionInfoChanged(); + break; + case MSG_AIRPLANE_MODE_CHANGED: + handleAirplaneModeChanged(); + break; + case MSG_SERVICE_STATE_CHANGE: + handleServiceStateChange(msg.arg1, (ServiceState) msg.obj); + break; + case MSG_SCREEN_TURNED_ON: + handleScreenTurnedOn(); + break; + case MSG_SCREEN_TURNED_OFF: + Trace.beginSection("KeyguardUpdateMonitor#handler MSG_SCREEN_TURNED_ON"); + handleScreenTurnedOff(); + Trace.endSection(); + break; + case MSG_DREAMING_STATE_CHANGED: + handleDreamingStateChanged(msg.arg1); + break; + case MSG_USER_UNLOCKED: + handleUserUnlocked(); + break; + } + } + }; + + private OnSubscriptionsChangedListener mSubscriptionListener = + new OnSubscriptionsChangedListener() { + @Override + public void onSubscriptionsChanged() { + mHandler.sendEmptyMessage(MSG_SIM_SUBSCRIPTION_INFO_CHANGED); + } + }; + + private SparseBooleanArray mUserHasTrust = new SparseBooleanArray(); + private SparseBooleanArray mUserTrustIsManaged = new SparseBooleanArray(); + private SparseBooleanArray mUserFingerprintAuthenticated = new SparseBooleanArray(); + private SparseBooleanArray mUserFaceUnlockRunning = new SparseBooleanArray(); + + private static int sCurrentUser; + + public synchronized static void setCurrentUser(int currentUser) { + sCurrentUser = currentUser; + } + + public synchronized static int getCurrentUser() { + return sCurrentUser; + } + + @Override + public void onTrustChanged(boolean enabled, int userId, int flags) { + mUserHasTrust.put(userId, enabled); + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onTrustChanged(userId); + if (enabled && flags != 0) { + cb.onTrustGrantedWithFlags(flags, userId); + } + } + } + } + + protected void handleSimSubscriptionInfoChanged() { + if (DEBUG_SIM_STATES) { + Log.v(TAG, "onSubscriptionInfoChanged()"); + List<SubscriptionInfo> sil = mSubscriptionManager.getActiveSubscriptionInfoList(); + if (sil != null) { + for (SubscriptionInfo subInfo : sil) { + Log.v(TAG, "SubInfo:" + subInfo); + } + } else { + Log.v(TAG, "onSubscriptionInfoChanged: list is null"); + } + } + List<SubscriptionInfo> subscriptionInfos = getSubscriptionInfo(true /* forceReload */); + + // Hack level over 9000: Because the subscription id is not yet valid when we see the + // first update in handleSimStateChange, we need to force refresh all all SIM states + // so the subscription id for them is consistent. + ArrayList<SubscriptionInfo> changedSubscriptions = new ArrayList<>(); + for (int i = 0; i < subscriptionInfos.size(); i++) { + SubscriptionInfo info = subscriptionInfos.get(i); + boolean changed = refreshSimState(info.getSubscriptionId(), info.getSimSlotIndex()); + if (changed) { + changedSubscriptions.add(info); + } + } + for (int i = 0; i < changedSubscriptions.size(); i++) { + SimData data = mSimDatas.get(changedSubscriptions.get(i).getSubscriptionId()); + for (int j = 0; j < mCallbacks.size(); j++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(j).get(); + if (cb != null) { + cb.onSimStateChanged(data.subId, data.slotId, data.simState); + } + } + } + for (int j = 0; j < mCallbacks.size(); j++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(j).get(); + if (cb != null) { + cb.onRefreshCarrierInfo(); + } + } + } + + private void handleAirplaneModeChanged() { + for (int j = 0; j < mCallbacks.size(); j++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(j).get(); + if (cb != null) { + cb.onRefreshCarrierInfo(); + } + } + } + + /** @return List of SubscriptionInfo records, maybe empty but never null */ + public List<SubscriptionInfo> getSubscriptionInfo(boolean forceReload) { + List<SubscriptionInfo> sil = mSubscriptionInfo; + if (sil == null || forceReload) { + sil = mSubscriptionManager.getActiveSubscriptionInfoList(); + } + if (sil == null) { + // getActiveSubscriptionInfoList was null callers expect an empty list. + mSubscriptionInfo = new ArrayList<SubscriptionInfo>(); + } else { + mSubscriptionInfo = sil; + } + return mSubscriptionInfo; + } + + @Override + public void onTrustManagedChanged(boolean managed, int userId) { + mUserTrustIsManaged.put(userId, managed); + + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onTrustManagedChanged(userId); + } + } + } + + private void onFingerprintAuthenticated(int userId) { + Trace.beginSection("KeyGuardUpdateMonitor#onFingerPrintAuthenticated"); + mUserFingerprintAuthenticated.put(userId, true); + + // If fingerprint unlocking is allowed, this event will lead to a Keyguard dismiss or to a + // wake-up (if Keyguard is not showing), so we don't need to listen until Keyguard is + // fully gone. + mFingerprintAlreadyAuthenticated = isUnlockingWithFingerprintAllowed(); + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onFingerprintAuthenticated(userId); + } + } + Trace.endSection(); + } + + private void handleFingerprintAuthFailed() { + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onFingerprintAuthFailed(); + } + } + handleFingerprintHelp(-1, mContext.getString(R.string.fingerprint_not_recognized)); + } + + private void handleFingerprintAcquired(int acquireInfo) { + if (acquireInfo != FingerprintManager.FINGERPRINT_ACQUIRED_GOOD) { + return; + } + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onFingerprintAcquired(); + } + } + } + + private void handleFingerprintAuthenticated(int authUserId) { + Trace.beginSection("KeyGuardUpdateMonitor#handlerFingerPrintAuthenticated"); + try { + final int userId; + try { + userId = ActivityManager.getService().getCurrentUser().id; + } catch (RemoteException e) { + Log.e(TAG, "Failed to get current user id: ", e); + return; + } + if (userId != authUserId) { + Log.d(TAG, "Fingerprint authenticated for wrong user: " + authUserId); + return; + } + if (isFingerprintDisabled(userId)) { + Log.d(TAG, "Fingerprint disabled by DPM for userId: " + userId); + return; + } + onFingerprintAuthenticated(userId); + } finally { + setFingerprintRunningState(FINGERPRINT_STATE_STOPPED); + } + Trace.endSection(); + } + + private void handleFingerprintHelp(int msgId, String helpString) { + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onFingerprintHelp(msgId, helpString); + } + } + } + + private void handleFingerprintError(int msgId, String errString) { + if (msgId == FingerprintManager.FINGERPRINT_ERROR_CANCELED + && mFingerprintRunningState == FINGERPRINT_STATE_CANCELLING_RESTARTING) { + setFingerprintRunningState(FINGERPRINT_STATE_STOPPED); + startListeningForFingerprint(); + } else { + setFingerprintRunningState(FINGERPRINT_STATE_STOPPED); + } + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onFingerprintError(msgId, errString); + } + } + } + + private void handleFingerprintLockoutReset() { + updateFingerprintListeningState(); + } + + private void setFingerprintRunningState(int fingerprintRunningState) { + boolean wasRunning = mFingerprintRunningState == FINGERPRINT_STATE_RUNNING; + boolean isRunning = fingerprintRunningState == FINGERPRINT_STATE_RUNNING; + mFingerprintRunningState = fingerprintRunningState; + + // Clients of KeyguardUpdateMonitor don't care about the internal state about the + // asynchronousness of the cancel cycle. So only notify them if the actualy running state + // has changed. + if (wasRunning != isRunning) { + notifyFingerprintRunningStateChanged(); + } + } + + private void notifyFingerprintRunningStateChanged() { + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onFingerprintRunningStateChanged(isFingerprintDetectionRunning()); + } + } + } + private void handleFaceUnlockStateChanged(boolean running, int userId) { + mUserFaceUnlockRunning.put(userId, running); + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onFaceUnlockStateChanged(running, userId); + } + } + } + + public boolean isFaceUnlockRunning(int userId) { + return mUserFaceUnlockRunning.get(userId); + } + + public boolean isFingerprintDetectionRunning() { + return mFingerprintRunningState == FINGERPRINT_STATE_RUNNING; + } + + private boolean isTrustDisabled(int userId) { + // Don't allow trust agent if device is secured with a SIM PIN. This is here + // mainly because there's no other way to prompt the user to enter their SIM PIN + // once they get past the keyguard screen. + final boolean disabledBySimPin = isSimPinSecure(); + return disabledBySimPin; + } + + private boolean isFingerprintDisabled(int userId) { + final DevicePolicyManager dpm = + (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE); + return dpm != null && (dpm.getKeyguardDisabledFeatures(null, userId) + & DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT) != 0 + || isSimPinSecure(); + } + + public boolean getUserCanSkipBouncer(int userId) { + return getUserHasTrust(userId) || (mUserFingerprintAuthenticated.get(userId) + && isUnlockingWithFingerprintAllowed()); + } + + public boolean getUserHasTrust(int userId) { + return !isTrustDisabled(userId) && mUserHasTrust.get(userId); + } + + public boolean getUserTrustIsManaged(int userId) { + return mUserTrustIsManaged.get(userId) && !isTrustDisabled(userId); + } + + public boolean isUnlockingWithFingerprintAllowed() { + return mStrongAuthTracker.isUnlockingWithFingerprintAllowed(); + } + + public boolean needsSlowUnlockTransition() { + return mNeedsSlowUnlockTransition; + } + + public StrongAuthTracker getStrongAuthTracker() { + return mStrongAuthTracker; + } + + public void reportSuccessfulStrongAuthUnlockAttempt() { + if (mFpm != null) { + byte[] token = null; /* TODO: pass real auth token once fp HAL supports it */ + mFpm.resetTimeout(token); + } + } + + private void notifyStrongAuthStateChanged(int userId) { + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onStrongAuthStateChanged(userId); + } + } + } + + static class DisplayClientState { + public int clientGeneration; + public boolean clearing; + public PendingIntent intent; + public int playbackState; + public long playbackEventTime; + } + + private DisplayClientState mDisplayClientState = new DisplayClientState(); + + private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (DEBUG) Log.d(TAG, "received broadcast " + action); + + if (Intent.ACTION_TIME_TICK.equals(action) + || Intent.ACTION_TIME_CHANGED.equals(action) + || Intent.ACTION_TIMEZONE_CHANGED.equals(action)) { + mHandler.sendEmptyMessage(MSG_TIME_UPDATE); + } else if (Intent.ACTION_BATTERY_CHANGED.equals(action)) { + final int status = intent.getIntExtra(EXTRA_STATUS, BATTERY_STATUS_UNKNOWN); + final int plugged = intent.getIntExtra(EXTRA_PLUGGED, 0); + final int level = intent.getIntExtra(EXTRA_LEVEL, 0); + final int health = intent.getIntExtra(EXTRA_HEALTH, BATTERY_HEALTH_UNKNOWN); + + final int maxChargingMicroAmp = intent.getIntExtra(EXTRA_MAX_CHARGING_CURRENT, -1); + int maxChargingMicroVolt = intent.getIntExtra(EXTRA_MAX_CHARGING_VOLTAGE, -1); + final int maxChargingMicroWatt; + + if (maxChargingMicroVolt <= 0) { + maxChargingMicroVolt = DEFAULT_CHARGING_VOLTAGE_MICRO_VOLT; + } + if (maxChargingMicroAmp > 0) { + // Calculating muW = muA * muV / (10^6 mu^2 / mu); splitting up the divisor + // to maintain precision equally on both factors. + maxChargingMicroWatt = (maxChargingMicroAmp / 1000) + * (maxChargingMicroVolt / 1000); + } else { + maxChargingMicroWatt = -1; + } + final Message msg = mHandler.obtainMessage( + MSG_BATTERY_UPDATE, new BatteryStatus(status, level, plugged, health, + maxChargingMicroWatt)); + mHandler.sendMessage(msg); + } else if (TelephonyIntents.ACTION_SIM_STATE_CHANGED.equals(action)) { + SimData args = SimData.fromIntent(intent); + if (DEBUG_SIM_STATES) { + Log.v(TAG, "action " + action + + " state: " + intent.getStringExtra(IccCardConstants.INTENT_KEY_ICC_STATE) + + " slotId: " + args.slotId + " subid: " + args.subId); + } + mHandler.obtainMessage(MSG_SIM_STATE_CHANGE, args.subId, args.slotId, args.simState) + .sendToTarget(); + } else if (AudioManager.RINGER_MODE_CHANGED_ACTION.equals(action)) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_RINGER_MODE_CHANGED, + intent.getIntExtra(AudioManager.EXTRA_RINGER_MODE, -1), 0)); + } else if (TelephonyManager.ACTION_PHONE_STATE_CHANGED.equals(action)) { + String state = intent.getStringExtra(TelephonyManager.EXTRA_STATE); + mHandler.sendMessage(mHandler.obtainMessage(MSG_PHONE_STATE_CHANGED, state)); + } else if (Intent.ACTION_AIRPLANE_MODE_CHANGED.equals(action)) { + mHandler.sendEmptyMessage(MSG_AIRPLANE_MODE_CHANGED); + } else if (Intent.ACTION_BOOT_COMPLETED.equals(action)) { + dispatchBootCompleted(); + } else if (TelephonyIntents.ACTION_SERVICE_STATE_CHANGED.equals(action)) { + ServiceState serviceState = ServiceState.newFromBundle(intent.getExtras()); + int subId = intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY, + SubscriptionManager.INVALID_SUBSCRIPTION_ID); + if (DEBUG) { + Log.v(TAG, "action " + action + " serviceState=" + serviceState + " subId=" + + subId); + } + mHandler.sendMessage( + mHandler.obtainMessage(MSG_SERVICE_STATE_CHANGE, subId, 0, serviceState)); + } + } + }; + + private final BroadcastReceiver mBroadcastAllReceiver = new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED.equals(action)) { + mHandler.sendEmptyMessage(MSG_TIME_UPDATE); + } else if (Intent.ACTION_USER_INFO_CHANGED.equals(action)) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_USER_INFO_CHANGED, + intent.getIntExtra(Intent.EXTRA_USER_HANDLE, getSendingUserId()), 0)); + } else if (ACTION_FACE_UNLOCK_STARTED.equals(action)) { + Trace.beginSection("KeyguardUpdateMonitor.mBroadcastAllReceiver#onReceive ACTION_FACE_UNLOCK_STARTED"); + mHandler.sendMessage(mHandler.obtainMessage(MSG_FACE_UNLOCK_STATE_CHANGED, 1, + getSendingUserId())); + Trace.endSection(); + } else if (ACTION_FACE_UNLOCK_STOPPED.equals(action)) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_FACE_UNLOCK_STATE_CHANGED, 0, + getSendingUserId())); + } else if (DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED + .equals(action)) { + mHandler.sendEmptyMessage(MSG_DPM_STATE_CHANGED); + } else if (ACTION_USER_UNLOCKED.equals(action)) { + mHandler.sendEmptyMessage(MSG_USER_UNLOCKED); + } + } + }; + + private final FingerprintManager.LockoutResetCallback mLockoutResetCallback + = new FingerprintManager.LockoutResetCallback() { + @Override + public void onLockoutReset() { + handleFingerprintLockoutReset(); + } + }; + + private FingerprintManager.AuthenticationCallback mAuthenticationCallback + = new AuthenticationCallback() { + + @Override + public void onAuthenticationFailed() { + handleFingerprintAuthFailed(); + }; + + @Override + public void onAuthenticationSucceeded(AuthenticationResult result) { + Trace.beginSection("KeyguardUpdateMonitor#onAuthenticationSucceeded"); + handleFingerprintAuthenticated(result.getUserId()); + Trace.endSection(); + } + + @Override + public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) { + handleFingerprintHelp(helpMsgId, helpString.toString()); + } + + @Override + public void onAuthenticationError(int errMsgId, CharSequence errString) { + handleFingerprintError(errMsgId, errString.toString()); + } + + @Override + public void onAuthenticationAcquired(int acquireInfo) { + handleFingerprintAcquired(acquireInfo); + } + }; + private CancellationSignal mFingerprintCancelSignal; + private FingerprintManager mFpm; + + /** + * When we receive a + * {@link com.android.internal.telephony.TelephonyIntents#ACTION_SIM_STATE_CHANGED} broadcast, + * and then pass a result via our handler to {@link KeyguardUpdateMonitor#handleSimStateChange}, + * we need a single object to pass to the handler. This class helps decode + * the intent and provide a {@link SimCard.State} result. + */ + private static class SimData { + public State simState; + public int slotId; + public int subId; + + SimData(State state, int slot, int id) { + simState = state; + slotId = slot; + subId = id; + } + + static SimData fromIntent(Intent intent) { + State state; + if (!TelephonyIntents.ACTION_SIM_STATE_CHANGED.equals(intent.getAction())) { + throw new IllegalArgumentException("only handles intent ACTION_SIM_STATE_CHANGED"); + } + String stateExtra = intent.getStringExtra(IccCardConstants.INTENT_KEY_ICC_STATE); + int slotId = intent.getIntExtra(PhoneConstants.SLOT_KEY, 0); + int subId = intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY, + SubscriptionManager.INVALID_SUBSCRIPTION_ID); + if (IccCardConstants.INTENT_VALUE_ICC_ABSENT.equals(stateExtra)) { + final String absentReason = intent + .getStringExtra(IccCardConstants.INTENT_KEY_LOCKED_REASON); + + if (IccCardConstants.INTENT_VALUE_ABSENT_ON_PERM_DISABLED.equals( + absentReason)) { + state = IccCardConstants.State.PERM_DISABLED; + } else { + state = IccCardConstants.State.ABSENT; + } + } else if (IccCardConstants.INTENT_VALUE_ICC_READY.equals(stateExtra)) { + state = IccCardConstants.State.READY; + } else if (IccCardConstants.INTENT_VALUE_ICC_LOCKED.equals(stateExtra)) { + final String lockedReason = intent + .getStringExtra(IccCardConstants.INTENT_KEY_LOCKED_REASON); + if (IccCardConstants.INTENT_VALUE_LOCKED_ON_PIN.equals(lockedReason)) { + state = IccCardConstants.State.PIN_REQUIRED; + } else if (IccCardConstants.INTENT_VALUE_LOCKED_ON_PUK.equals(lockedReason)) { + state = IccCardConstants.State.PUK_REQUIRED; + } else { + state = IccCardConstants.State.UNKNOWN; + } + } else if (IccCardConstants.INTENT_VALUE_LOCKED_NETWORK.equals(stateExtra)) { + state = IccCardConstants.State.NETWORK_LOCKED; + } else if (IccCardConstants.INTENT_VALUE_ICC_LOADED.equals(stateExtra) + || IccCardConstants.INTENT_VALUE_ICC_IMSI.equals(stateExtra)) { + // This is required because telephony doesn't return to "READY" after + // these state transitions. See bug 7197471. + state = IccCardConstants.State.READY; + } else { + state = IccCardConstants.State.UNKNOWN; + } + return new SimData(state, slotId, subId); + } + + @Override + public String toString() { + return "SimData{state=" + simState + ",slotId=" + slotId + ",subId=" + subId + "}"; + } + } + + public static class BatteryStatus { + public static final int CHARGING_UNKNOWN = -1; + public static final int CHARGING_SLOWLY = 0; + public static final int CHARGING_REGULAR = 1; + public static final int CHARGING_FAST = 2; + + public final int status; + public final int level; + public final int plugged; + public final int health; + public final int maxChargingWattage; + public BatteryStatus(int status, int level, int plugged, int health, + int maxChargingWattage) { + this.status = status; + this.level = level; + this.plugged = plugged; + this.health = health; + this.maxChargingWattage = maxChargingWattage; + } + + /** + * Determine whether the device is plugged in (USB, power, or wireless). + * @return true if the device is plugged in. + */ + public boolean isPluggedIn() { + return plugged == BatteryManager.BATTERY_PLUGGED_AC + || plugged == BatteryManager.BATTERY_PLUGGED_USB + || plugged == BatteryManager.BATTERY_PLUGGED_WIRELESS; + } + + /** + * Whether or not the device is charged. Note that some devices never return 100% for + * battery level, so this allows either battery level or status to determine if the + * battery is charged. + * @return true if the device is charged + */ + public boolean isCharged() { + return status == BATTERY_STATUS_FULL || level >= 100; + } + + /** + * Whether battery is low and needs to be charged. + * @return true if battery is low + */ + public boolean isBatteryLow() { + return level < LOW_BATTERY_THRESHOLD; + } + + public final int getChargingSpeed(int slowThreshold, int fastThreshold) { + return maxChargingWattage <= 0 ? CHARGING_UNKNOWN : + maxChargingWattage < slowThreshold ? CHARGING_SLOWLY : + maxChargingWattage > fastThreshold ? CHARGING_FAST : + CHARGING_REGULAR; + } + } + + public class StrongAuthTracker extends LockPatternUtils.StrongAuthTracker { + public StrongAuthTracker(Context context) { + super(context); + } + + public boolean isUnlockingWithFingerprintAllowed() { + int userId = getCurrentUser(); + return isFingerprintAllowedForUser(userId); + } + + public boolean hasUserAuthenticatedSinceBoot() { + int userId = getCurrentUser(); + return (getStrongAuthForUser(userId) + & STRONG_AUTH_REQUIRED_AFTER_BOOT) == 0; + } + + @Override + public void onStrongAuthRequiredChanged(int userId) { + notifyStrongAuthStateChanged(userId); + } + } + + public static KeyguardUpdateMonitor getInstance(Context context) { + if (sInstance == null) { + sInstance = new KeyguardUpdateMonitor(context); + } + return sInstance; + } + + protected void handleStartedWakingUp() { + Trace.beginSection("KeyguardUpdateMonitor#handleStartedWakingUp"); + updateFingerprintListeningState(); + final int count = mCallbacks.size(); + for (int i = 0; i < count; i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onStartedWakingUp(); + } + } + Trace.endSection(); + } + + protected void handleStartedGoingToSleep(int arg1) { + clearFingerprintRecognized(); + final int count = mCallbacks.size(); + for (int i = 0; i < count; i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onStartedGoingToSleep(arg1); + } + } + mGoingToSleep = true; + mFingerprintAlreadyAuthenticated = false; + updateFingerprintListeningState(); + } + + protected void handleFinishedGoingToSleep(int arg1) { + mGoingToSleep = false; + final int count = mCallbacks.size(); + for (int i = 0; i < count; i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onFinishedGoingToSleep(arg1); + } + } + updateFingerprintListeningState(); + } + + private void handleScreenTurnedOn() { + final int count = mCallbacks.size(); + for (int i = 0; i < count; i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onScreenTurnedOn(); + } + } + } + + private void handleScreenTurnedOff() { + final int count = mCallbacks.size(); + for (int i = 0; i < count; i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onScreenTurnedOff(); + } + } + } + + private void handleDreamingStateChanged(int dreamStart) { + final int count = mCallbacks.size(); + boolean showingDream = dreamStart == 1; + for (int i = 0; i < count; i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onDreamingStateChanged(showingDream); + } + } + } + + /** + * IMPORTANT: Must be called from UI thread. + */ + public void dispatchSetBackground(Bitmap bmp) { + if (DEBUG) Log.d(TAG, "dispatchSetBackground"); + final int count = mCallbacks.size(); + for (int i = 0; i < count; i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onSetBackground(bmp); + } + } + } + + private void handleUserInfoChanged(int userId) { + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onUserInfoChanged(userId); + } + } + } + + private void handleUserUnlocked() { + mNeedsSlowUnlockTransition = resolveNeedsSlowUnlockTransition(); + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onUserUnlocked(); + } + } + } + + private KeyguardUpdateMonitor(Context context) { + mContext = context; + mSubscriptionManager = SubscriptionManager.from(context); + mDeviceProvisioned = isDeviceProvisionedInSettingsDb(); + mStrongAuthTracker = new StrongAuthTracker(context); + + // Since device can't be un-provisioned, we only need to register a content observer + // to update mDeviceProvisioned when we are... + if (!mDeviceProvisioned) { + watchForDeviceProvisioning(); + } + + // Take a guess at initial SIM state, battery status and PLMN until we get an update + mBatteryStatus = new BatteryStatus(BATTERY_STATUS_UNKNOWN, 100, 0, 0, 0); + + // Watch for interesting updates + final IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_TIME_TICK); + filter.addAction(Intent.ACTION_TIME_CHANGED); + filter.addAction(Intent.ACTION_BATTERY_CHANGED); + filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); + filter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED); + filter.addAction(TelephonyIntents.ACTION_SIM_STATE_CHANGED); + filter.addAction(TelephonyIntents.ACTION_SERVICE_STATE_CHANGED); + filter.addAction(TelephonyManager.ACTION_PHONE_STATE_CHANGED); + filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION); + context.registerReceiver(mBroadcastReceiver, filter); + + final IntentFilter bootCompleteFilter = new IntentFilter(); + bootCompleteFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); + bootCompleteFilter.addAction(Intent.ACTION_BOOT_COMPLETED); + context.registerReceiver(mBroadcastReceiver, bootCompleteFilter); + + final IntentFilter allUserFilter = new IntentFilter(); + allUserFilter.addAction(Intent.ACTION_USER_INFO_CHANGED); + allUserFilter.addAction(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED); + allUserFilter.addAction(ACTION_FACE_UNLOCK_STARTED); + allUserFilter.addAction(ACTION_FACE_UNLOCK_STOPPED); + allUserFilter.addAction(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED); + allUserFilter.addAction(ACTION_USER_UNLOCKED); + context.registerReceiverAsUser(mBroadcastAllReceiver, UserHandle.ALL, allUserFilter, + null, null); + + mSubscriptionManager.addOnSubscriptionsChangedListener(mSubscriptionListener); + try { + ActivityManager.getService().registerUserSwitchObserver( + new UserSwitchObserver() { + @Override + public void onUserSwitching(int newUserId, IRemoteCallback reply) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_USER_SWITCHING, + newUserId, 0, reply)); + } + @Override + public void onUserSwitchComplete(int newUserId) throws RemoteException { + mHandler.sendMessage(mHandler.obtainMessage(MSG_USER_SWITCH_COMPLETE, + newUserId, 0)); + } + }, TAG); + } catch (RemoteException e) { + e.rethrowAsRuntimeException(); + } + + mTrustManager = (TrustManager) context.getSystemService(Context.TRUST_SERVICE); + mTrustManager.registerTrustListener(this); + mLockPatternUtils = new LockPatternUtils(context); + mLockPatternUtils.registerStrongAuthTracker(mStrongAuthTracker); + + if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) { + mFpm = (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE); + } + updateFingerprintListeningState(); + if (mFpm != null) { + mFpm.addLockoutResetCallback(mLockoutResetCallback); + } + + mUserManager = context.getSystemService(UserManager.class); + } + + private void updateFingerprintListeningState() { + boolean shouldListenForFingerprint = shouldListenForFingerprint(); + if (mFingerprintRunningState == FINGERPRINT_STATE_RUNNING && !shouldListenForFingerprint) { + stopListeningForFingerprint(); + } else if (mFingerprintRunningState != FINGERPRINT_STATE_RUNNING + && shouldListenForFingerprint) { + startListeningForFingerprint(); + } + } + + private boolean shouldListenForFingerprint() { + return (mKeyguardIsVisible || !mDeviceInteractive || mBouncer || mGoingToSleep) + && !mSwitchingUser && !mFingerprintAlreadyAuthenticated + && !isFingerprintDisabled(getCurrentUser()); + } + + private void startListeningForFingerprint() { + if (mFingerprintRunningState == FINGERPRINT_STATE_CANCELLING) { + setFingerprintRunningState(FINGERPRINT_STATE_CANCELLING_RESTARTING); + return; + } + if (DEBUG) Log.v(TAG, "startListeningForFingerprint()"); + int userId = ActivityManager.getCurrentUser(); + if (isUnlockWithFingerprintPossible(userId)) { + if (mFingerprintCancelSignal != null) { + mFingerprintCancelSignal.cancel(); + } + mFingerprintCancelSignal = new CancellationSignal(); + mFpm.authenticate(null, mFingerprintCancelSignal, 0, mAuthenticationCallback, null, userId); + setFingerprintRunningState(FINGERPRINT_STATE_RUNNING); + } + } + + public boolean isUnlockWithFingerprintPossible(int userId) { + return mFpm != null && mFpm.isHardwareDetected() && !isFingerprintDisabled(userId) + && mFpm.getEnrolledFingerprints(userId).size() > 0; + } + + private void stopListeningForFingerprint() { + if (DEBUG) Log.v(TAG, "stopListeningForFingerprint()"); + if (mFingerprintRunningState == FINGERPRINT_STATE_RUNNING) { + mFingerprintCancelSignal.cancel(); + mFingerprintCancelSignal = null; + setFingerprintRunningState(FINGERPRINT_STATE_CANCELLING); + } + if (mFingerprintRunningState == FINGERPRINT_STATE_CANCELLING_RESTARTING) { + setFingerprintRunningState(FINGERPRINT_STATE_CANCELLING); + } + } + + private boolean isDeviceProvisionedInSettingsDb() { + return Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.DEVICE_PROVISIONED, 0) != 0; + } + + private void watchForDeviceProvisioning() { + mDeviceProvisionedObserver = new ContentObserver(mHandler) { + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + mDeviceProvisioned = isDeviceProvisionedInSettingsDb(); + if (mDeviceProvisioned) { + mHandler.sendEmptyMessage(MSG_DEVICE_PROVISIONED); + } + if (DEBUG) Log.d(TAG, "DEVICE_PROVISIONED state = " + mDeviceProvisioned); + } + }; + + mContext.getContentResolver().registerContentObserver( + Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED), + false, mDeviceProvisionedObserver); + + // prevent a race condition between where we check the flag and where we register the + // observer by grabbing the value once again... + boolean provisioned = isDeviceProvisionedInSettingsDb(); + if (provisioned != mDeviceProvisioned) { + mDeviceProvisioned = provisioned; + if (mDeviceProvisioned) { + mHandler.sendEmptyMessage(MSG_DEVICE_PROVISIONED); + } + } + } + + /** + * Update the state whether Keyguard currently has a lockscreen wallpaper. + * + * @param hasLockscreenWallpaper Whether Keyguard has a lockscreen wallpaper. + */ + public void setHasLockscreenWallpaper(boolean hasLockscreenWallpaper) { + if (hasLockscreenWallpaper != mHasLockscreenWallpaper) { + mHasLockscreenWallpaper = hasLockscreenWallpaper; + for (int i = mCallbacks.size() - 1; i >= 0; i--) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onHasLockscreenWallpaperChanged(hasLockscreenWallpaper); + } + } + } + } + + /** + * @return Whether Keyguard has a lockscreen wallpaper. + */ + public boolean hasLockscreenWallpaper() { + return mHasLockscreenWallpaper; + } + + /** + * Handle {@link #MSG_DPM_STATE_CHANGED} + */ + protected void handleDevicePolicyManagerStateChanged() { + updateFingerprintListeningState(); + for (int i = mCallbacks.size() - 1; i >= 0; i--) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onDevicePolicyManagerStateChanged(); + } + } + } + + /** + * Handle {@link #MSG_USER_SWITCHING} + */ + protected void handleUserSwitching(int userId, IRemoteCallback reply) { + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onUserSwitching(userId); + } + } + try { + reply.sendResult(null); + } catch (RemoteException e) { + } + } + + /** + * Handle {@link #MSG_USER_SWITCH_COMPLETE} + */ + protected void handleUserSwitchComplete(int userId) { + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onUserSwitchComplete(userId); + } + } + } + + /** + * This is exposed since {@link Intent#ACTION_BOOT_COMPLETED} is not sticky. If + * keyguard crashes sometime after boot, then it will never receive this + * broadcast and hence not handle the event. This method is ultimately called by + * PhoneWindowManager in this case. + */ + public void dispatchBootCompleted() { + mHandler.sendEmptyMessage(MSG_BOOT_COMPLETED); + } + + /** + * Handle {@link #MSG_BOOT_COMPLETED} + */ + protected void handleBootCompleted() { + if (mBootCompleted) return; + mBootCompleted = true; + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onBootCompleted(); + } + } + } + + /** + * We need to store this state in the KeyguardUpdateMonitor since this class will not be + * destroyed. + */ + public boolean hasBootCompleted() { + return mBootCompleted; + } + + /** + * Handle {@link #MSG_DEVICE_PROVISIONED} + */ + protected void handleDeviceProvisioned() { + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onDeviceProvisioned(); + } + } + if (mDeviceProvisionedObserver != null) { + // We don't need the observer anymore... + mContext.getContentResolver().unregisterContentObserver(mDeviceProvisionedObserver); + mDeviceProvisionedObserver = null; + } + } + + /** + * Handle {@link #MSG_PHONE_STATE_CHANGED} + */ + protected void handlePhoneStateChanged(String newState) { + if (DEBUG) Log.d(TAG, "handlePhoneStateChanged(" + newState + ")"); + if (TelephonyManager.EXTRA_STATE_IDLE.equals(newState)) { + mPhoneState = TelephonyManager.CALL_STATE_IDLE; + } else if (TelephonyManager.EXTRA_STATE_OFFHOOK.equals(newState)) { + mPhoneState = TelephonyManager.CALL_STATE_OFFHOOK; + } else if (TelephonyManager.EXTRA_STATE_RINGING.equals(newState)) { + mPhoneState = TelephonyManager.CALL_STATE_RINGING; + } + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onPhoneStateChanged(mPhoneState); + } + } + } + + /** + * Handle {@link #MSG_RINGER_MODE_CHANGED} + */ + protected void handleRingerModeChange(int mode) { + if (DEBUG) Log.d(TAG, "handleRingerModeChange(" + mode + ")"); + mRingMode = mode; + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onRingerModeChanged(mode); + } + } + } + + /** + * Handle {@link #MSG_TIME_UPDATE} + */ + private void handleTimeUpdate() { + if (DEBUG) Log.d(TAG, "handleTimeUpdate"); + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onTimeChanged(); + } + } + } + + /** + * Handle {@link #MSG_BATTERY_UPDATE} + */ + private void handleBatteryUpdate(BatteryStatus status) { + if (DEBUG) Log.d(TAG, "handleBatteryUpdate"); + final boolean batteryUpdateInteresting = isBatteryUpdateInteresting(mBatteryStatus, status); + mBatteryStatus = status; + if (batteryUpdateInteresting) { + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onRefreshBatteryInfo(status); + } + } + } + } + + /** + * Handle {@link #MSG_SIM_STATE_CHANGE} + */ + private void handleSimStateChange(int subId, int slotId, State state) { + + if (DEBUG_SIM_STATES) { + Log.d(TAG, "handleSimStateChange(subId=" + subId + ", slotId=" + + slotId + ", state=" + state +")"); + } + + if (!SubscriptionManager.isValidSubscriptionId(subId)) { + Log.w(TAG, "invalid subId in handleSimStateChange()"); + return; + } + + SimData data = mSimDatas.get(subId); + final boolean changed; + if (data == null) { + data = new SimData(state, slotId, subId); + mSimDatas.put(subId, data); + changed = true; // no data yet; force update + } else { + changed = (data.simState != state || data.subId != subId || data.slotId != slotId); + data.simState = state; + data.subId = subId; + data.slotId = slotId; + } + if (changed && state != State.UNKNOWN) { + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onSimStateChanged(subId, slotId, state); + } + } + } + } + + /** + * Handle {@link #MSG_SERVICE_STATE_CHANGE} + */ + private void handleServiceStateChange(int subId, ServiceState serviceState) { + if (DEBUG) { + Log.d(TAG, + "handleServiceStateChange(subId=" + subId + ", serviceState=" + serviceState); + } + + if (!SubscriptionManager.isValidSubscriptionId(subId)) { + Log.w(TAG, "invalid subId in handleServiceStateChange()"); + return; + } + + mServiceStates.put(subId, serviceState); + + for (int j = 0; j < mCallbacks.size(); j++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(j).get(); + if (cb != null) { + cb.onRefreshCarrierInfo(); + } + } + } + + /** + * Notifies that the visibility state of Keyguard has changed. + * + * <p>Needs to be called from the main thread. + */ + public void onKeyguardVisibilityChanged(boolean showing) { + if (DEBUG) Log.d(TAG, "onKeyguardVisibilityChanged(" + showing + ")"); + mKeyguardIsVisible = showing; + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onKeyguardVisibilityChangedRaw(showing); + } + } + if (!showing) { + mFingerprintAlreadyAuthenticated = false; + } + updateFingerprintListeningState(); + } + + /** + * Handle {@link #MSG_KEYGUARD_RESET} + */ + private void handleKeyguardReset() { + if (DEBUG) Log.d(TAG, "handleKeyguardReset"); + updateFingerprintListeningState(); + mNeedsSlowUnlockTransition = resolveNeedsSlowUnlockTransition(); + } + + private boolean resolveNeedsSlowUnlockTransition() { + if (mUserManager.isUserUnlocked(getCurrentUser())) { + return false; + } + Intent homeIntent = new Intent(Intent.ACTION_MAIN) + .addCategory(Intent.CATEGORY_HOME); + ResolveInfo resolveInfo = mContext.getPackageManager().resolveActivity(homeIntent, + 0 /* flags */); + return FALLBACK_HOME_COMPONENT.equals(resolveInfo.getComponentInfo().getComponentName()); + } + + /** + * Handle {@link #MSG_KEYGUARD_BOUNCER_CHANGED} + * @see #sendKeyguardBouncerChanged(boolean) + */ + private void handleKeyguardBouncerChanged(int bouncer) { + if (DEBUG) Log.d(TAG, "handleKeyguardBouncerChanged(" + bouncer + ")"); + boolean isBouncer = (bouncer == 1); + mBouncer = isBouncer; + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onKeyguardBouncerChanged(isBouncer); + } + } + updateFingerprintListeningState(); + } + + /** + * Handle {@link #MSG_REPORT_EMERGENCY_CALL_ACTION} + */ + private void handleReportEmergencyCallAction() { + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onEmergencyCallAction(); + } + } + } + + private static boolean isBatteryUpdateInteresting(BatteryStatus old, BatteryStatus current) { + final boolean nowPluggedIn = current.isPluggedIn(); + final boolean wasPluggedIn = old.isPluggedIn(); + final boolean stateChangedWhilePluggedIn = + wasPluggedIn == true && nowPluggedIn == true + && (old.status != current.status); + + // change in plug state is always interesting + if (wasPluggedIn != nowPluggedIn || stateChangedWhilePluggedIn) { + return true; + } + + // change in battery level while plugged in + if (nowPluggedIn && old.level != current.level) { + return true; + } + + // change where battery needs charging + if (!nowPluggedIn && current.isBatteryLow() && current.level != old.level) { + return true; + } + + // change in charging current while plugged in + if (nowPluggedIn && current.maxChargingWattage != old.maxChargingWattage) { + return true; + } + + return false; + } + + /** + * Remove the given observer's callback. + * + * @param callback The callback to remove + */ + public void removeCallback(KeyguardUpdateMonitorCallback callback) { + if (DEBUG) Log.v(TAG, "*** unregister callback for " + callback); + for (int i = mCallbacks.size() - 1; i >= 0; i--) { + if (mCallbacks.get(i).get() == callback) { + mCallbacks.remove(i); + } + } + } + + /** + * Register to receive notifications about general keyguard information + * (see {@link InfoCallback}. + * @param callback The callback to register + */ + public void registerCallback(KeyguardUpdateMonitorCallback callback) { + if (DEBUG) Log.v(TAG, "*** register callback for " + callback); + // Prevent adding duplicate callbacks + for (int i = 0; i < mCallbacks.size(); i++) { + if (mCallbacks.get(i).get() == callback) { + if (DEBUG) Log.e(TAG, "Object tried to add another callback", + new Exception("Called by")); + return; + } + } + mCallbacks.add(new WeakReference<KeyguardUpdateMonitorCallback>(callback)); + removeCallback(null); // remove unused references + sendUpdates(callback); + } + + public boolean isSwitchingUser() { + return mSwitchingUser; + } + + public void setSwitchingUser(boolean switching) { + mSwitchingUser = switching; + updateFingerprintListeningState(); + } + + private void sendUpdates(KeyguardUpdateMonitorCallback callback) { + // Notify listener of the current state + callback.onRefreshBatteryInfo(mBatteryStatus); + callback.onTimeChanged(); + callback.onRingerModeChanged(mRingMode); + callback.onPhoneStateChanged(mPhoneState); + callback.onRefreshCarrierInfo(); + callback.onClockVisibilityChanged(); + for (Entry<Integer, SimData> data : mSimDatas.entrySet()) { + final SimData state = data.getValue(); + callback.onSimStateChanged(state.subId, state.slotId, state.simState); + } + } + + public void sendKeyguardReset() { + mHandler.obtainMessage(MSG_KEYGUARD_RESET).sendToTarget(); + } + + /** + * @see #handleKeyguardBouncerChanged(int) + */ + public void sendKeyguardBouncerChanged(boolean showingBouncer) { + if (DEBUG) Log.d(TAG, "sendKeyguardBouncerChanged(" + showingBouncer + ")"); + Message message = mHandler.obtainMessage(MSG_KEYGUARD_BOUNCER_CHANGED); + message.arg1 = showingBouncer ? 1 : 0; + message.sendToTarget(); + } + + /** + * Report that the user successfully entered the SIM PIN or PUK/SIM PIN so we + * have the information earlier than waiting for the intent + * broadcast from the telephony code. + * + * NOTE: Because handleSimStateChange() invokes callbacks immediately without going + * through mHandler, this *must* be called from the UI thread. + */ + public void reportSimUnlocked(int subId) { + if (DEBUG_SIM_STATES) Log.v(TAG, "reportSimUnlocked(subId=" + subId + ")"); + int slotId = SubscriptionManager.getSlotId(subId); + handleSimStateChange(subId, slotId, State.READY); + } + + /** + * Report that the emergency call button has been pressed and the emergency dialer is + * about to be displayed. + * + * @param bypassHandler runs immediately. + * + * NOTE: Must be called from UI thread if bypassHandler == true. + */ + public void reportEmergencyCallAction(boolean bypassHandler) { + if (!bypassHandler) { + mHandler.obtainMessage(MSG_REPORT_EMERGENCY_CALL_ACTION).sendToTarget(); + } else { + handleReportEmergencyCallAction(); + } + } + + /** + * @return Whether the device is provisioned (whether they have gone through + * the setup wizard) + */ + public boolean isDeviceProvisioned() { + return mDeviceProvisioned; + } + + public void clearFailedUnlockAttempts() { + mFailedAttempts.delete(sCurrentUser); + } + + public int getFailedUnlockAttempts(int userId) { + return mFailedAttempts.get(userId, 0); + } + + public void reportFailedStrongAuthUnlockAttempt(int userId) { + mFailedAttempts.put(userId, getFailedUnlockAttempts(userId) + 1); + } + + public void clearFingerprintRecognized() { + mUserFingerprintAuthenticated.clear(); + } + + public boolean isSimPinVoiceSecure() { + // TODO: only count SIMs that handle voice + return isSimPinSecure(); + } + + public boolean isSimPinSecure() { + // True if any SIM is pin secure + for (SubscriptionInfo info : getSubscriptionInfo(false /* forceReload */)) { + if (isSimPinSecure(getSimState(info.getSubscriptionId()))) return true; + } + return false; + } + + public State getSimState(int subId) { + if (mSimDatas.containsKey(subId)) { + return mSimDatas.get(subId).simState; + } else { + return State.UNKNOWN; + } + } + + /** + * @return true if and only if the state has changed for the specified {@code slotId} + */ + private boolean refreshSimState(int subId, int slotId) { + + // This is awful. It exists because there are two APIs for getting the SIM status + // that don't return the complete set of values and have different types. In Keyguard we + // need IccCardConstants, but TelephonyManager would only give us + // TelephonyManager.SIM_STATE*, so we retrieve it manually. + final TelephonyManager tele = TelephonyManager.from(mContext); + int simState = tele.getSimState(slotId); + State state; + try { + state = State.intToState(simState); + } catch(IllegalArgumentException ex) { + Log.w(TAG, "Unknown sim state: " + simState); + state = State.UNKNOWN; + } + SimData data = mSimDatas.get(subId); + final boolean changed; + if (data == null) { + data = new SimData(state, slotId, subId); + mSimDatas.put(subId, data); + changed = true; // no data yet; force update + } else { + changed = data.simState != state; + data.simState = state; + } + return changed; + } + + public static boolean isSimPinSecure(IccCardConstants.State state) { + final IccCardConstants.State simState = state; + return (simState == IccCardConstants.State.PIN_REQUIRED + || simState == IccCardConstants.State.PUK_REQUIRED + || simState == IccCardConstants.State.PERM_DISABLED); + } + + public DisplayClientState getCachedDisplayClientState() { + return mDisplayClientState; + } + + // TODO: use these callbacks elsewhere in place of the existing notifyScreen*() + // (KeyguardViewMediator, KeyguardHostView) + public void dispatchStartedWakingUp() { + synchronized (this) { + mDeviceInteractive = true; + } + mHandler.sendEmptyMessage(MSG_STARTED_WAKING_UP); + } + + public void dispatchStartedGoingToSleep(int why) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_STARTED_GOING_TO_SLEEP, why, 0)); + } + + public void dispatchFinishedGoingToSleep(int why) { + synchronized(this) { + mDeviceInteractive = false; + } + mHandler.sendMessage(mHandler.obtainMessage(MSG_FINISHED_GOING_TO_SLEEP, why, 0)); + } + + public void dispatchScreenTurnedOn() { + synchronized (this) { + mScreenOn = true; + } + mHandler.sendEmptyMessage(MSG_SCREEN_TURNED_ON); + } + + public void dispatchScreenTurnedOff() { + synchronized(this) { + mScreenOn = false; + } + mHandler.sendEmptyMessage(MSG_SCREEN_TURNED_OFF); + } + + public void dispatchDreamingStarted() { + mHandler.sendMessage(mHandler.obtainMessage(MSG_DREAMING_STATE_CHANGED, 1, 0)); + } + + public void dispatchDreamingStopped() { + mHandler.sendMessage(mHandler.obtainMessage(MSG_DREAMING_STATE_CHANGED, 0, 0)); + } + + public boolean isDeviceInteractive() { + return mDeviceInteractive; + } + + public boolean isGoingToSleep() { + return mGoingToSleep; + } + + /** + * Find the next SubscriptionId for a SIM in the given state, favoring lower slot numbers first. + * @param state + * @return subid or {@link SubscriptionManager#INVALID_SUBSCRIPTION_ID} if none found + */ + public int getNextSubIdForState(State state) { + List<SubscriptionInfo> list = getSubscriptionInfo(false /* forceReload */); + int resultId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; + int bestSlotId = Integer.MAX_VALUE; // Favor lowest slot first + for (int i = 0; i < list.size(); i++) { + final SubscriptionInfo info = list.get(i); + final int id = info.getSubscriptionId(); + int slotId = SubscriptionManager.getSlotId(id); + if (state == getSimState(id) && bestSlotId > slotId ) { + resultId = id; + bestSlotId = slotId; + } + } + return resultId; + } + + public SubscriptionInfo getSubscriptionInfoForSubId(int subId) { + List<SubscriptionInfo> list = getSubscriptionInfo(false /* forceReload */); + for (int i = 0; i < list.size(); i++) { + SubscriptionInfo info = list.get(i); + if (subId == info.getSubscriptionId()) return info; + } + return null; // not found + } + + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("KeyguardUpdateMonitor state:"); + pw.println(" SIM States:"); + for (SimData data : mSimDatas.values()) { + pw.println(" " + data.toString()); + } + pw.println(" Subs:"); + if (mSubscriptionInfo != null) { + for (int i = 0; i < mSubscriptionInfo.size(); i++) { + pw.println(" " + mSubscriptionInfo.get(i)); + } + } + pw.println(" Service states:"); + for (int subId : mServiceStates.keySet()) { + pw.println(" " + subId + "=" + mServiceStates.get(subId)); + } + if (mFpm != null && mFpm.isHardwareDetected()) { + final int userId = ActivityManager.getCurrentUser(); + final int strongAuthFlags = mStrongAuthTracker.getStrongAuthForUser(userId); + pw.println(" Fingerprint state (user=" + userId + ")"); + pw.println(" allowed=" + isUnlockingWithFingerprintAllowed()); + pw.println(" auth'd=" + mUserFingerprintAuthenticated.get(userId)); + pw.println(" authSinceBoot=" + + getStrongAuthTracker().hasUserAuthenticatedSinceBoot()); + pw.println(" disabled(DPM)=" + isFingerprintDisabled(userId)); + pw.println(" possible=" + isUnlockWithFingerprintPossible(userId)); + pw.println(" strongAuthFlags=" + Integer.toHexString(strongAuthFlags)); + pw.println(" trustManaged=" + getUserTrustIsManaged(userId)); + } + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java new file mode 100644 index 000000000000..14d6b599a9b5 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java @@ -0,0 +1,259 @@ +/* + * 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.app.admin.DevicePolicyManager; +import android.graphics.Bitmap; +import android.media.AudioManager; +import android.os.SystemClock; +import android.hardware.fingerprint.FingerprintManager; +import android.telephony.TelephonyManager; +import android.view.WindowManagerPolicy; + +import com.android.internal.telephony.IccCardConstants; + +/** + * Callback for general information relevant to lock screen. + */ +public class KeyguardUpdateMonitorCallback { + + private static final long VISIBILITY_CHANGED_COLLAPSE_MS = 1000; + private long mVisibilityChangedCalled; + private boolean mShowing; + + /** + * Called when the battery status changes, e.g. when plugged in or unplugged, charge + * level, etc. changes. + * + * @param status current battery status + */ + public void onRefreshBatteryInfo(KeyguardUpdateMonitor.BatteryStatus status) { } + + /** + * Called once per minute or when the time changes. + */ + public void onTimeChanged() { } + + /** + * Called when the carrier PLMN or SPN changes. + */ + public void onRefreshCarrierInfo() { } + + /** + * Called when the ringer mode changes. + * @param state the current ringer state, as defined in + * {@link AudioManager#RINGER_MODE_CHANGED_ACTION} + */ + public void onRingerModeChanged(int state) { } + + /** + * Called when the phone state changes. String will be one of: + * {@link TelephonyManager#EXTRA_STATE_IDLE} + * {@link TelephonyManager@EXTRA_STATE_RINGING} + * {@link TelephonyManager#EXTRA_STATE_OFFHOOK + */ + public void onPhoneStateChanged(int phoneState) { } + + /** + * Called when the visibility of the keyguard changes. + * @param showing Indicates if the keyguard is now visible. + */ + public void onKeyguardVisibilityChanged(boolean showing) { } + + public void onKeyguardVisibilityChangedRaw(boolean showing) { + final long now = SystemClock.elapsedRealtime(); + if (showing == mShowing + && (now - mVisibilityChangedCalled) < VISIBILITY_CHANGED_COLLAPSE_MS) return; + onKeyguardVisibilityChanged(showing); + mVisibilityChangedCalled = now; + mShowing = showing; + } + + /** + * Called when the keyguard enters or leaves bouncer mode. + * @param bouncer if true, keyguard is now in bouncer mode. + */ + public void onKeyguardBouncerChanged(boolean bouncer) { } + + /** + * Called when visibility of lockscreen clock changes, such as when + * obscured by a widget. + */ + public void onClockVisibilityChanged() { } + + /** + * Called when the device becomes provisioned + */ + public void onDeviceProvisioned() { } + + /** + * Called when the device policy changes. + * See {@link DevicePolicyManager#ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED} + */ + public void onDevicePolicyManagerStateChanged() { } + + /** + * Called when the user change begins. + */ + public void onUserSwitching(int userId) { } + + /** + * Called when the user change is complete. + */ + public void onUserSwitchComplete(int userId) { } + + /** + * Called when the SIM state changes. + * @param slotId + * @param simState + */ + public void onSimStateChanged(int subId, int slotId, IccCardConstants.State simState) { } + + /** + * Called when the user's info changed. + */ + public void onUserInfoChanged(int userId) { } + + /** + * Called when a user got unlocked. + */ + public void onUserUnlocked() { } + + /** + * Called when boot completed. + * + * Note, this callback will only be received if boot complete occurs after registering with + * KeyguardUpdateMonitor. + */ + public void onBootCompleted() { } + + /** + * Called when the emergency call button is pressed. + */ + public void onEmergencyCallAction() { } + + /** + * Called when the transport background changes. + * @param bitmap + */ + public void onSetBackground(Bitmap bitmap) { + } + + /** + * Called when the device has started waking up. + */ + public void onStartedWakingUp() { } + + /** + * Called when the device has started going to sleep. + * @param why see {@link #onFinishedGoingToSleep(int)} + */ + public void onStartedGoingToSleep(int why) { } + + /** + * Called when the device has finished going to sleep. + * @param why either {@link WindowManagerPolicy#OFF_BECAUSE_OF_ADMIN}, + * {@link WindowManagerPolicy#OFF_BECAUSE_OF_USER}, or + * {@link WindowManagerPolicy#OFF_BECAUSE_OF_TIMEOUT}. + */ + public void onFinishedGoingToSleep(int why) { } + + /** + * Called when the screen has been turned on. + */ + public void onScreenTurnedOn() { } + + /** + * Called when the screen has been turned off. + */ + public void onScreenTurnedOff() { } + + /** + * Called when trust changes for a user. + */ + public void onTrustChanged(int userId) { } + + /** + * Called when trust being managed changes for a user. + */ + public void onTrustManagedChanged(int userId) { } + + /** + * Called after trust was granted with non-zero flags. + */ + public void onTrustGrantedWithFlags(int flags, int userId) { } + + /** + * Called when a finger has been acquired. + * <p> + * It is guaranteed that either {@link #onFingerprintAuthenticated} or + * {@link #onFingerprintAuthFailed()} is called after this method eventually. + */ + public void onFingerprintAcquired() { } + + /** + * Called when a fingerprint couldn't be authenticated. + */ + public void onFingerprintAuthFailed() { } + + /** + * Called when a fingerprint is recognized. + * @param userId the user id for which the fingerprint was authenticated + */ + public void onFingerprintAuthenticated(int userId) { } + + /** + * Called when fingerprint provides help string (e.g. "Try again") + * @param msgId + * @param helpString + */ + public void onFingerprintHelp(int msgId, String helpString) { } + + /** + * Called when fingerprint provides an semi-permanent error message + * (e.g. "Hardware not available"). + * @param msgId one of the error messages listed in {@link FingerprintManager} + * @param errString + */ + public void onFingerprintError(int msgId, String errString) { } + + /** + * Called when the state of face unlock changed. + */ + public void onFaceUnlockStateChanged(boolean running, int userId) { } + + /** + * Called when the fingerprint running state changed. + */ + public void onFingerprintRunningStateChanged(boolean running) { } + + /** + * Called when the state that the user hasn't used strong authentication since quite some time + * has changed. + */ + public void onStrongAuthStateChanged(int userId) { } + + /** + * Called when the state whether we have a lockscreen wallpaper has changed. + */ + public void onHasLockscreenWallpaperChanged(boolean hasLockscreenWallpaper) { } + + /** + * Called when the dream's window state is changed. + * @param dreaming true if the dream's window has been created and is visible + */ + public void onDreamingStateChanged(boolean dreaming) { } +} diff --git a/packages/SystemUI/src/com/android/keyguard/LatencyTracker.java b/packages/SystemUI/src/com/android/keyguard/LatencyTracker.java new file mode 100644 index 000000000000..cee0afcd37d6 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/LatencyTracker.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2016 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.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Build; +import android.os.SystemClock; +import android.os.SystemProperties; +import android.os.Trace; +import android.util.EventLog; +import android.util.Log; +import android.util.SparseLongArray; + +import com.android.systemui.EventLogTags; + +/** + * Class to track various latencies in SystemUI. It then outputs the latency to logcat so these + * latencies can be captured by tests and then used for dashboards. + * <p> + * This is currently only in Keyguard so it can be shared between SystemUI and Keyguard, but + * eventually we'd want to merge these two packages together so Keyguard can use common classes + * that are shared with SystemUI. + */ +public class LatencyTracker { + + private static final String ACTION_RELOAD_PROPERTY = + "com.android.systemui.RELOAD_LATENCY_TRACKER_PROPERTY"; + + private static final String TAG = "LatencyTracker"; + + /** + * Time it takes until the first frame of the notification panel to be displayed while expanding + */ + public static final int ACTION_EXPAND_PANEL = 0; + + /** + * Time it takes until the first frame of recents is drawn after invoking it with the button. + */ + public static final int ACTION_TOGGLE_RECENTS = 1; + + /** + * Time between we get a fingerprint acquired signal until we start with the unlock animation + */ + public static final int ACTION_FINGERPRINT_WAKE_AND_UNLOCK = 2; + + /** + * Time it takes to check PIN/Pattern/Password. + */ + public static final int ACTION_CHECK_CREDENTIAL = 3; + + /** + * Time it takes to check fully PIN/Pattern/Password, i.e. that's the time spent including the + * actions to unlock a user. + */ + public static final int ACTION_CHECK_CREDENTIAL_UNLOCKED = 4; + + /** + * Time it takes to turn on the screen. + */ + public static final int ACTION_TURN_ON_SCREEN = 5; + + private static final String[] NAMES = new String[] { + "expand panel", + "toggle recents", + "fingerprint wake-and-unlock", + "check credential", + "check credential unlocked", + "turn on screen" }; + + private static LatencyTracker sLatencyTracker; + + private final SparseLongArray mStartRtc = new SparseLongArray(); + private boolean mEnabled; + + public static LatencyTracker getInstance(Context context) { + if (sLatencyTracker == null) { + sLatencyTracker = new LatencyTracker(context); + } + return sLatencyTracker; + } + + private LatencyTracker(Context context) { + context.registerReceiver(new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + reloadProperty(); + } + }, new IntentFilter(ACTION_RELOAD_PROPERTY)); + reloadProperty(); + } + + private void reloadProperty() { + mEnabled = SystemProperties.getBoolean("debug.systemui.latency_tracking", false); + } + + public static boolean isEnabled(Context ctx) { + return Build.IS_DEBUGGABLE && getInstance(ctx).mEnabled; + } + + /** + * Notifies that an action is starting. This needs to be called from the main thread. + * + * @param action The action to start. One of the ACTION_* values. + */ + public void onActionStart(int action) { + if (!mEnabled) { + return; + } + Trace.asyncTraceBegin(Trace.TRACE_TAG_APP, NAMES[action], 0); + mStartRtc.put(action, SystemClock.elapsedRealtime()); + } + + /** + * Notifies that an action has ended. This needs to be called from the main thread. + * + * @param action The action to end. One of the ACTION_* values. + */ + public void onActionEnd(int action) { + if (!mEnabled) { + return; + } + long endRtc = SystemClock.elapsedRealtime(); + long startRtc = mStartRtc.get(action, -1); + if (startRtc == -1) { + return; + } + mStartRtc.delete(action); + Trace.asyncTraceEnd(Trace.TRACE_TAG_APP, NAMES[action], 0); + long duration = endRtc - startRtc; + Log.i(TAG, "action=" + action + " latency=" + duration); + EventLog.writeEvent(EventLogTags.SYSUI_LATENCY, action, (int) duration); + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/LiftToActivateListener.java b/packages/SystemUI/src/com/android/keyguard/LiftToActivateListener.java new file mode 100644 index 000000000000..e59602b1cfff --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/LiftToActivateListener.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2013 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.content.Context; +import android.view.MotionEvent; +import android.view.View; +import android.view.accessibility.AccessibilityManager; + +/** + * Hover listener that implements lift-to-activate interaction for + * accessibility. May be added to multiple views. + */ +class LiftToActivateListener implements View.OnHoverListener { + /** Manager used to query accessibility enabled state. */ + private final AccessibilityManager mAccessibilityManager; + + private boolean mCachedClickableState; + + public LiftToActivateListener(Context context) { + mAccessibilityManager = (AccessibilityManager) context.getSystemService( + Context.ACCESSIBILITY_SERVICE); + } + + @Override + public boolean onHover(View v, MotionEvent event) { + // When touch exploration is turned on, lifting a finger while + // inside the view bounds should perform a click action. + if (mAccessibilityManager.isEnabled() + && mAccessibilityManager.isTouchExplorationEnabled()) { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_HOVER_ENTER: + // Lift-to-type temporarily disables double-tap + // activation by setting the view as not clickable. + mCachedClickableState = v.isClickable(); + v.setClickable(false); + break; + case MotionEvent.ACTION_HOVER_EXIT: + final int x = (int) event.getX(); + final int y = (int) event.getY(); + if ((x > v.getPaddingLeft()) && (y > v.getPaddingTop()) + && (x < v.getWidth() - v.getPaddingRight()) + && (y < v.getHeight() - v.getPaddingBottom())) { + v.performClick(); + } + v.setClickable(mCachedClickableState); + break; + } + } + + // Pass the event to View.onHoverEvent() to handle accessibility. + v.onHoverEvent(event); + + // Consume the event so it doesn't fall through to other views. + return true; + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/keyguard/NumPadKey.java b/packages/SystemUI/src/com/android/keyguard/NumPadKey.java new file mode 100644 index 000000000000..1518bdced99a --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/NumPadKey.java @@ -0,0 +1,180 @@ +/* + * 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.content.Context; +import android.content.res.TypedArray; +import android.os.PowerManager; +import android.os.SystemClock; +import android.util.AttributeSet; +import android.view.HapticFeedbackConstants; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.android.internal.widget.LockPatternUtils; + +public class NumPadKey extends ViewGroup { + // list of "ABC", etc per digit, starting with '0' + static String sKlondike[]; + + private int mDigit = -1; + private int mTextViewResId; + private PasswordTextView mTextView; + private TextView mDigitText; + private TextView mKlondikeText; + private boolean mEnableHaptics; + private PowerManager mPM; + + private View.OnClickListener mListener = new View.OnClickListener() { + @Override + public void onClick(View thisView) { + if (mTextView == null && mTextViewResId > 0) { + final View v = NumPadKey.this.getRootView().findViewById(mTextViewResId); + if (v != null && v instanceof PasswordTextView) { + mTextView = (PasswordTextView) v; + } + } + if (mTextView != null && mTextView.isEnabled()) { + mTextView.append(Character.forDigit(mDigit, 10)); + } + userActivity();; + } + }; + + public void userActivity() { + mPM.userActivity(SystemClock.uptimeMillis(), false); + } + + public NumPadKey(Context context) { + this(context, null); + } + + public NumPadKey(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public NumPadKey(Context context, AttributeSet attrs, int defStyle) { + this(context, attrs, defStyle, R.layout.keyguard_num_pad_key); + } + + protected NumPadKey(Context context, AttributeSet attrs, int defStyle, int contentResource) { + super(context, attrs, defStyle); + setFocusable(true); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NumPadKey); + + try { + mDigit = a.getInt(R.styleable.NumPadKey_digit, mDigit); + mTextViewResId = a.getResourceId(R.styleable.NumPadKey_textView, 0); + } finally { + a.recycle(); + } + + setOnClickListener(mListener); + setOnHoverListener(new LiftToActivateListener(context)); + setAccessibilityDelegate(new ObscureSpeechDelegate(context)); + + mEnableHaptics = new LockPatternUtils(context).isTactileFeedbackEnabled(); + + mPM = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); + LayoutInflater inflater = (LayoutInflater) getContext().getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(contentResource, this, true); + + mDigitText = (TextView) findViewById(R.id.digit_text); + mDigitText.setText(Integer.toString(mDigit)); + mKlondikeText = (TextView) findViewById(R.id.klondike_text); + + if (mDigit >= 0) { + if (sKlondike == null) { + sKlondike = getResources().getStringArray(R.array.lockscreen_num_pad_klondike); + } + if (sKlondike != null && sKlondike.length > mDigit) { + String klondike = sKlondike[mDigit]; + final int len = klondike.length(); + if (len > 0) { + mKlondikeText.setText(klondike); + } else { + mKlondikeText.setVisibility(View.INVISIBLE); + } + } + } + + a = context.obtainStyledAttributes(attrs, android.R.styleable.View); + if (!a.hasValueOrEmpty(android.R.styleable.View_background)) { + setBackground(mContext.getDrawable(R.drawable.ripple_drawable)); + } + a.recycle(); + setContentDescription(mDigitText.getText().toString()); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { + doHapticKeyClick(); + } + return super.onTouchEvent(event); + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + // Reset the "announced headset" flag when detached. + ObscureSpeechDelegate.sAnnouncedHeadset = false; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + measureChildren(widthMeasureSpec, heightMeasureSpec); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + int digitHeight = mDigitText.getMeasuredHeight(); + int klondikeHeight = mKlondikeText.getMeasuredHeight(); + int totalHeight = digitHeight + klondikeHeight; + int top = getHeight() / 2 - totalHeight / 2; + int centerX = getWidth() / 2; + int left = centerX - mDigitText.getMeasuredWidth() / 2; + int bottom = top + digitHeight; + mDigitText.layout(left, top, left + mDigitText.getMeasuredWidth(), bottom); + top = (int) (bottom - klondikeHeight * 0.35f); + bottom = top + klondikeHeight; + + left = centerX - mKlondikeText.getMeasuredWidth() / 2; + mKlondikeText.layout(left, top, left + mKlondikeText.getMeasuredWidth(), bottom); + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } + + // Cause a VIRTUAL_KEY vibration + public void doHapticKeyClick() { + if (mEnableHaptics) { + performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, + HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING + | HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING); + } + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/ObscureSpeechDelegate.java b/packages/SystemUI/src/com/android/keyguard/ObscureSpeechDelegate.java new file mode 100644 index 000000000000..410a43a6789a --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/ObscureSpeechDelegate.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2013 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.content.ContentResolver; +import android.content.Context; +import android.media.AudioManager; +import android.os.UserHandle; +import android.provider.Settings; +import android.view.View; +import android.view.View.AccessibilityDelegate; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; + +import com.android.internal.R; + +/** + * Accessibility delegate that obscures speech for a view when the user has + * not turned on the "speak passwords" preference and is not listening + * through headphones. + */ +class ObscureSpeechDelegate extends AccessibilityDelegate { + /** Whether any client has announced the "headset" notification. */ + static boolean sAnnouncedHeadset = false; + + private final ContentResolver mContentResolver; + private final AudioManager mAudioManager; + + public ObscureSpeechDelegate(Context context) { + mContentResolver = context.getContentResolver(); + mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + } + + @Override + public void sendAccessibilityEvent(View host, int eventType) { + super.sendAccessibilityEvent(host, eventType); + + // Play the "headset required" announcement the first time the user + // places accessibility focus on a key. + if ((eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) + && !sAnnouncedHeadset && shouldObscureSpeech()) { + sAnnouncedHeadset = true; + host.announceForAccessibility(host.getContext().getString( + R.string.keyboard_headset_required_to_hear_password)); + } + } + + @Override + public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) { + super.onPopulateAccessibilityEvent(host, event); + + if ((event.getEventType() != AccessibilityEvent.TYPE_ANNOUNCEMENT) + && shouldObscureSpeech()) { + event.getText().clear(); + event.setContentDescription(host.getContext().getString( + R.string.keyboard_password_character_no_headset)); + } + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + + if (shouldObscureSpeech()) { + final Context ctx = host.getContext(); + info.setText(null); + info.setContentDescription( + ctx.getString(R.string.keyboard_password_character_no_headset)); + } + } + + @SuppressWarnings("deprecation") + private boolean shouldObscureSpeech() { + // The user can optionally force speaking passwords. + if (Settings.Secure.getIntForUser(mContentResolver, + Settings.Secure.ACCESSIBILITY_SPEAK_PASSWORD, 0, UserHandle.USER_CURRENT) != 0) { + return false; + } + + // Always speak if the user is listening through headphones. + if (mAudioManager.isWiredHeadsetOn() || mAudioManager.isBluetoothA2dpOn()) { + return false; + } + + // Don't speak since this key is used to type a password. + return true; + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/keyguard/PasswordTextView.java b/packages/SystemUI/src/com/android/keyguard/PasswordTextView.java new file mode 100644 index 000000000000..48737f9a9bfb --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/PasswordTextView.java @@ -0,0 +1,703 @@ +/* + * Copyright (C) 2014 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.animation.AnimatorSet; +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.os.PowerManager; +import android.os.SystemClock; +import android.os.UserHandle; +import android.provider.Settings; +import android.text.InputType; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; + +import java.util.ArrayList; +import java.util.Stack; + +/** + * A View similar to a textView which contains password text and can animate when the text is + * changed + */ +public class PasswordTextView extends View { + + private static final float DOT_OVERSHOOT_FACTOR = 1.5f; + private static final long DOT_APPEAR_DURATION_OVERSHOOT = 320; + private static final long APPEAR_DURATION = 160; + private static final long DISAPPEAR_DURATION = 160; + private static final long RESET_DELAY_PER_ELEMENT = 40; + private static final long RESET_MAX_DELAY = 200; + + /** + * The overlap between the text disappearing and the dot appearing animation + */ + private static final long DOT_APPEAR_TEXT_DISAPPEAR_OVERLAP_DURATION = 130; + + /** + * The duration the text needs to stay there at least before it can morph into a dot + */ + private static final long TEXT_REST_DURATION_AFTER_APPEAR = 100; + + /** + * The duration the text should be visible, starting with the appear animation + */ + private static final long TEXT_VISIBILITY_DURATION = 1300; + + /** + * The position in time from [0,1] where the overshoot should be finished and the settle back + * animation of the dot should start + */ + private static final float OVERSHOOT_TIME_POSITION = 0.5f; + + /** + * The raw text size, will be multiplied by the scaled density when drawn + */ + private final int mTextHeightRaw; + private final int mGravity; + private ArrayList<CharState> mTextChars = new ArrayList<>(); + private String mText = ""; + private Stack<CharState> mCharPool = new Stack<>(); + private int mDotSize; + private PowerManager mPM; + private int mCharPadding; + private final Paint mDrawPaint = new Paint(); + private Interpolator mAppearInterpolator; + private Interpolator mDisappearInterpolator; + private Interpolator mFastOutSlowInInterpolator; + private boolean mShowPassword; + private UserActivityListener mUserActivityListener; + + public interface UserActivityListener { + void onUserActivity(); + } + + public PasswordTextView(Context context) { + this(context, null); + } + + public PasswordTextView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public PasswordTextView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public PasswordTextView(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + setFocusableInTouchMode(true); + setFocusable(true); + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PasswordTextView); + try { + mTextHeightRaw = a.getInt(R.styleable.PasswordTextView_scaledTextSize, 0); + mGravity = a.getInt(R.styleable.PasswordTextView_android_gravity, Gravity.CENTER); + mDotSize = a.getDimensionPixelSize(R.styleable.PasswordTextView_dotSize, + getContext().getResources().getDimensionPixelSize(R.dimen.password_dot_size)); + mCharPadding = a.getDimensionPixelSize(R.styleable.PasswordTextView_charPadding, + getContext().getResources().getDimensionPixelSize( + R.dimen.password_char_padding)); + } finally { + a.recycle(); + } + mDrawPaint.setFlags(Paint.SUBPIXEL_TEXT_FLAG | Paint.ANTI_ALIAS_FLAG); + mDrawPaint.setTextAlign(Paint.Align.CENTER); + mDrawPaint.setColor(0xffffffff); + mDrawPaint.setTypeface(Typeface.create("sans-serif-light", 0)); + mShowPassword = Settings.System.getInt(mContext.getContentResolver(), + Settings.System.TEXT_SHOW_PASSWORD, 1) == 1; + mAppearInterpolator = AnimationUtils.loadInterpolator(mContext, + android.R.interpolator.linear_out_slow_in); + mDisappearInterpolator = AnimationUtils.loadInterpolator(mContext, + android.R.interpolator.fast_out_linear_in); + mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(mContext, + android.R.interpolator.fast_out_slow_in); + mPM = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); + } + + @Override + protected void onDraw(Canvas canvas) { + float totalDrawingWidth = getDrawingWidth(); + float currentDrawPosition; + if ((mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.LEFT) { + if ((mGravity & Gravity.RELATIVE_LAYOUT_DIRECTION) != 0 + && getLayoutDirection() == LAYOUT_DIRECTION_RTL) { + currentDrawPosition = getWidth() - getPaddingRight() - totalDrawingWidth; + } else { + currentDrawPosition = getPaddingLeft(); + } + } else { + currentDrawPosition = getWidth() / 2 - totalDrawingWidth / 2; + } + int length = mTextChars.size(); + Rect bounds = getCharBounds(); + int charHeight = (bounds.bottom - bounds.top); + float yPosition = + (getHeight() - getPaddingBottom() - getPaddingTop()) / 2 + getPaddingTop(); + canvas.clipRect(getPaddingLeft(), getPaddingTop(), + getWidth() - getPaddingRight(), getHeight() - getPaddingBottom()); + float charLength = bounds.right - bounds.left; + for (int i = 0; i < length; i++) { + CharState charState = mTextChars.get(i); + float charWidth = charState.draw(canvas, currentDrawPosition, charHeight, yPosition, + charLength); + currentDrawPosition += charWidth; + } + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } + + private Rect getCharBounds() { + float textHeight = mTextHeightRaw * getResources().getDisplayMetrics().scaledDensity; + mDrawPaint.setTextSize(textHeight); + Rect bounds = new Rect(); + mDrawPaint.getTextBounds("0", 0, 1, bounds); + return bounds; + } + + private float getDrawingWidth() { + int width = 0; + int length = mTextChars.size(); + Rect bounds = getCharBounds(); + int charLength = bounds.right - bounds.left; + for (int i = 0; i < length; i++) { + CharState charState = mTextChars.get(i); + if (i != 0) { + width += mCharPadding * charState.currentWidthFactor; + } + width += charLength * charState.currentWidthFactor; + } + return width; + } + + + public void append(char c) { + int visibleChars = mTextChars.size(); + String textbefore = mText; + mText = mText + c; + int newLength = mText.length(); + CharState charState; + if (newLength > visibleChars) { + charState = obtainCharState(c); + mTextChars.add(charState); + } else { + charState = mTextChars.get(newLength - 1); + charState.whichChar = c; + } + charState.startAppearAnimation(); + + // ensure that the previous element is being swapped + if (newLength > 1) { + CharState previousState = mTextChars.get(newLength - 2); + if (previousState.isDotSwapPending) { + previousState.swapToDotWhenAppearFinished(); + } + } + userActivity(); + sendAccessibilityEventTypeViewTextChanged(textbefore, textbefore.length(), 0, 1); + } + + public void setUserActivityListener(UserActivityListener userActivitiListener) { + mUserActivityListener = userActivitiListener; + } + + private void userActivity() { + mPM.userActivity(SystemClock.uptimeMillis(), false); + if (mUserActivityListener != null) { + mUserActivityListener.onUserActivity(); + } + } + + public void deleteLastChar() { + int length = mText.length(); + String textbefore = mText; + if (length > 0) { + mText = mText.substring(0, length - 1); + CharState charState = mTextChars.get(length - 1); + charState.startRemoveAnimation(0, 0); + } + userActivity(); + sendAccessibilityEventTypeViewTextChanged(textbefore, textbefore.length() - 1, 1, 0); + } + + public String getText() { + return mText; + } + + private CharState obtainCharState(char c) { + CharState charState; + if(mCharPool.isEmpty()) { + charState = new CharState(); + } else { + charState = mCharPool.pop(); + charState.reset(); + } + charState.whichChar = c; + return charState; + } + + public void reset(boolean animated, boolean announce) { + String textbefore = mText; + mText = ""; + int length = mTextChars.size(); + int middleIndex = (length - 1) / 2; + long delayPerElement = RESET_DELAY_PER_ELEMENT; + for (int i = 0; i < length; i++) { + CharState charState = mTextChars.get(i); + if (animated) { + int delayIndex; + if (i <= middleIndex) { + delayIndex = i * 2; + } else { + int distToMiddle = i - middleIndex; + delayIndex = (length - 1) - (distToMiddle - 1) * 2; + } + long startDelay = delayIndex * delayPerElement; + startDelay = Math.min(startDelay, RESET_MAX_DELAY); + long maxDelay = delayPerElement * (length - 1); + maxDelay = Math.min(maxDelay, RESET_MAX_DELAY) + DISAPPEAR_DURATION; + charState.startRemoveAnimation(startDelay, maxDelay); + charState.removeDotSwapCallbacks(); + } else { + mCharPool.push(charState); + } + } + if (!animated) { + mTextChars.clear(); + } + if (announce) { + sendAccessibilityEventTypeViewTextChanged(textbefore, 0, textbefore.length(), 0); + } + } + + void sendAccessibilityEventTypeViewTextChanged(String beforeText, int fromIndex, + int removedCount, int addedCount) { + if (AccessibilityManager.getInstance(mContext).isEnabled() && + (isFocused() || isSelected() && isShown())) { + if (!shouldSpeakPasswordsForAccessibility()) { + beforeText = null; + } + AccessibilityEvent event = + AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED); + event.setFromIndex(fromIndex); + event.setRemovedCount(removedCount); + event.setAddedCount(addedCount); + event.setBeforeText(beforeText); + event.setPassword(true); + sendAccessibilityEventUnchecked(event); + } + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + + event.setClassName(PasswordTextView.class.getName()); + event.setPassword(true); + } + + @Override + public void onPopulateAccessibilityEvent(AccessibilityEvent event) { + super.onPopulateAccessibilityEvent(event); + + if (shouldSpeakPasswordsForAccessibility()) { + final CharSequence text = mText; + if (!TextUtils.isEmpty(text)) { + event.getText().add(text); + } + } + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + + info.setClassName(PasswordTextView.class.getName()); + info.setPassword(true); + + if (shouldSpeakPasswordsForAccessibility()) { + info.setText(mText); + } + + info.setEditable(true); + + info.setInputType(InputType.TYPE_NUMBER_VARIATION_PASSWORD); + } + + /** + * @return true if the user has explicitly allowed accessibility services + * to speak passwords. + */ + private boolean shouldSpeakPasswordsForAccessibility() { + return (Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_SPEAK_PASSWORD, 0, + UserHandle.USER_CURRENT_OR_SELF) == 1); + } + + private class CharState { + char whichChar; + ValueAnimator textAnimator; + boolean textAnimationIsGrowing; + Animator dotAnimator; + boolean dotAnimationIsGrowing; + ValueAnimator widthAnimator; + boolean widthAnimationIsGrowing; + float currentTextSizeFactor; + float currentDotSizeFactor; + float currentWidthFactor; + boolean isDotSwapPending; + float currentTextTranslationY = 1.0f; + ValueAnimator textTranslateAnimator; + + Animator.AnimatorListener removeEndListener = new AnimatorListenerAdapter() { + private boolean mCancelled; + @Override + public void onAnimationCancel(Animator animation) { + mCancelled = true; + } + + @Override + public void onAnimationEnd(Animator animation) { + if (!mCancelled) { + mTextChars.remove(CharState.this); + mCharPool.push(CharState.this); + reset(); + cancelAnimator(textTranslateAnimator); + textTranslateAnimator = null; + } + } + + @Override + public void onAnimationStart(Animator animation) { + mCancelled = false; + } + }; + + Animator.AnimatorListener dotFinishListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + dotAnimator = null; + } + }; + + Animator.AnimatorListener textFinishListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + textAnimator = null; + } + }; + + Animator.AnimatorListener textTranslateFinishListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + textTranslateAnimator = null; + } + }; + + Animator.AnimatorListener widthFinishListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + widthAnimator = null; + } + }; + + private ValueAnimator.AnimatorUpdateListener dotSizeUpdater + = new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + currentDotSizeFactor = (float) animation.getAnimatedValue(); + invalidate(); + } + }; + + private ValueAnimator.AnimatorUpdateListener textSizeUpdater + = new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + currentTextSizeFactor = (float) animation.getAnimatedValue(); + invalidate(); + } + }; + + private ValueAnimator.AnimatorUpdateListener textTranslationUpdater + = new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + currentTextTranslationY = (float) animation.getAnimatedValue(); + invalidate(); + } + }; + + private ValueAnimator.AnimatorUpdateListener widthUpdater + = new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + currentWidthFactor = (float) animation.getAnimatedValue(); + invalidate(); + } + }; + + private Runnable dotSwapperRunnable = new Runnable() { + @Override + public void run() { + performSwap(); + isDotSwapPending = false; + } + }; + + void reset() { + whichChar = 0; + currentTextSizeFactor = 0.0f; + currentDotSizeFactor = 0.0f; + currentWidthFactor = 0.0f; + cancelAnimator(textAnimator); + textAnimator = null; + cancelAnimator(dotAnimator); + dotAnimator = null; + cancelAnimator(widthAnimator); + widthAnimator = null; + currentTextTranslationY = 1.0f; + removeDotSwapCallbacks(); + } + + void startRemoveAnimation(long startDelay, long widthDelay) { + boolean dotNeedsAnimation = (currentDotSizeFactor > 0.0f && dotAnimator == null) + || (dotAnimator != null && dotAnimationIsGrowing); + boolean textNeedsAnimation = (currentTextSizeFactor > 0.0f && textAnimator == null) + || (textAnimator != null && textAnimationIsGrowing); + boolean widthNeedsAnimation = (currentWidthFactor > 0.0f && widthAnimator == null) + || (widthAnimator != null && widthAnimationIsGrowing); + if (dotNeedsAnimation) { + startDotDisappearAnimation(startDelay); + } + if (textNeedsAnimation) { + startTextDisappearAnimation(startDelay); + } + if (widthNeedsAnimation) { + startWidthDisappearAnimation(widthDelay); + } + } + + void startAppearAnimation() { + boolean dotNeedsAnimation = !mShowPassword + && (dotAnimator == null || !dotAnimationIsGrowing); + boolean textNeedsAnimation = mShowPassword + && (textAnimator == null || !textAnimationIsGrowing); + boolean widthNeedsAnimation = (widthAnimator == null || !widthAnimationIsGrowing); + if (dotNeedsAnimation) { + startDotAppearAnimation(0); + } + if (textNeedsAnimation) { + startTextAppearAnimation(); + } + if (widthNeedsAnimation) { + startWidthAppearAnimation(); + } + if (mShowPassword) { + postDotSwap(TEXT_VISIBILITY_DURATION); + } + } + + /** + * Posts a runnable which ensures that the text will be replaced by a dot after {@link + * com.android.keyguard.PasswordTextView#TEXT_VISIBILITY_DURATION}. + */ + private void postDotSwap(long delay) { + removeDotSwapCallbacks(); + postDelayed(dotSwapperRunnable, delay); + isDotSwapPending = true; + } + + private void removeDotSwapCallbacks() { + removeCallbacks(dotSwapperRunnable); + isDotSwapPending = false; + } + + void swapToDotWhenAppearFinished() { + removeDotSwapCallbacks(); + if (textAnimator != null) { + long remainingDuration = textAnimator.getDuration() + - textAnimator.getCurrentPlayTime(); + postDotSwap(remainingDuration + TEXT_REST_DURATION_AFTER_APPEAR); + } else { + performSwap(); + } + } + + private void performSwap() { + startTextDisappearAnimation(0); + startDotAppearAnimation(DISAPPEAR_DURATION + - DOT_APPEAR_TEXT_DISAPPEAR_OVERLAP_DURATION); + } + + private void startWidthDisappearAnimation(long widthDelay) { + cancelAnimator(widthAnimator); + widthAnimator = ValueAnimator.ofFloat(currentWidthFactor, 0.0f); + widthAnimator.addUpdateListener(widthUpdater); + widthAnimator.addListener(widthFinishListener); + widthAnimator.addListener(removeEndListener); + widthAnimator.setDuration((long) (DISAPPEAR_DURATION * currentWidthFactor)); + widthAnimator.setStartDelay(widthDelay); + widthAnimator.start(); + widthAnimationIsGrowing = false; + } + + private void startTextDisappearAnimation(long startDelay) { + cancelAnimator(textAnimator); + textAnimator = ValueAnimator.ofFloat(currentTextSizeFactor, 0.0f); + textAnimator.addUpdateListener(textSizeUpdater); + textAnimator.addListener(textFinishListener); + textAnimator.setInterpolator(mDisappearInterpolator); + textAnimator.setDuration((long) (DISAPPEAR_DURATION * currentTextSizeFactor)); + textAnimator.setStartDelay(startDelay); + textAnimator.start(); + textAnimationIsGrowing = false; + } + + private void startDotDisappearAnimation(long startDelay) { + cancelAnimator(dotAnimator); + ValueAnimator animator = ValueAnimator.ofFloat(currentDotSizeFactor, 0.0f); + animator.addUpdateListener(dotSizeUpdater); + animator.addListener(dotFinishListener); + animator.setInterpolator(mDisappearInterpolator); + long duration = (long) (DISAPPEAR_DURATION * Math.min(currentDotSizeFactor, 1.0f)); + animator.setDuration(duration); + animator.setStartDelay(startDelay); + animator.start(); + dotAnimator = animator; + dotAnimationIsGrowing = false; + } + + private void startWidthAppearAnimation() { + cancelAnimator(widthAnimator); + widthAnimator = ValueAnimator.ofFloat(currentWidthFactor, 1.0f); + widthAnimator.addUpdateListener(widthUpdater); + widthAnimator.addListener(widthFinishListener); + widthAnimator.setDuration((long) (APPEAR_DURATION * (1f - currentWidthFactor))); + widthAnimator.start(); + widthAnimationIsGrowing = true; + } + + private void startTextAppearAnimation() { + cancelAnimator(textAnimator); + textAnimator = ValueAnimator.ofFloat(currentTextSizeFactor, 1.0f); + textAnimator.addUpdateListener(textSizeUpdater); + textAnimator.addListener(textFinishListener); + textAnimator.setInterpolator(mAppearInterpolator); + textAnimator.setDuration((long) (APPEAR_DURATION * (1f - currentTextSizeFactor))); + textAnimator.start(); + textAnimationIsGrowing = true; + + // handle translation + if (textTranslateAnimator == null) { + textTranslateAnimator = ValueAnimator.ofFloat(1.0f, 0.0f); + textTranslateAnimator.addUpdateListener(textTranslationUpdater); + textTranslateAnimator.addListener(textTranslateFinishListener); + textTranslateAnimator.setInterpolator(mAppearInterpolator); + textTranslateAnimator.setDuration(APPEAR_DURATION); + textTranslateAnimator.start(); + } + } + + private void startDotAppearAnimation(long delay) { + cancelAnimator(dotAnimator); + if (!mShowPassword) { + // We perform an overshoot animation + ValueAnimator overShootAnimator = ValueAnimator.ofFloat(currentDotSizeFactor, + DOT_OVERSHOOT_FACTOR); + overShootAnimator.addUpdateListener(dotSizeUpdater); + overShootAnimator.setInterpolator(mAppearInterpolator); + long overShootDuration = (long) (DOT_APPEAR_DURATION_OVERSHOOT + * OVERSHOOT_TIME_POSITION); + overShootAnimator.setDuration(overShootDuration); + ValueAnimator settleBackAnimator = ValueAnimator.ofFloat(DOT_OVERSHOOT_FACTOR, + 1.0f); + settleBackAnimator.addUpdateListener(dotSizeUpdater); + settleBackAnimator.setDuration(DOT_APPEAR_DURATION_OVERSHOOT - overShootDuration); + settleBackAnimator.addListener(dotFinishListener); + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playSequentially(overShootAnimator, settleBackAnimator); + animatorSet.setStartDelay(delay); + animatorSet.start(); + dotAnimator = animatorSet; + } else { + ValueAnimator growAnimator = ValueAnimator.ofFloat(currentDotSizeFactor, 1.0f); + growAnimator.addUpdateListener(dotSizeUpdater); + growAnimator.setDuration((long) (APPEAR_DURATION * (1.0f - currentDotSizeFactor))); + growAnimator.addListener(dotFinishListener); + growAnimator.setStartDelay(delay); + growAnimator.start(); + dotAnimator = growAnimator; + } + dotAnimationIsGrowing = true; + } + + private void cancelAnimator(Animator animator) { + if (animator != null) { + animator.cancel(); + } + } + + /** + * Draw this char to the canvas. + * + * @return The width this character contributes, including padding. + */ + public float draw(Canvas canvas, float currentDrawPosition, int charHeight, float yPosition, + float charLength) { + boolean textVisible = currentTextSizeFactor > 0; + boolean dotVisible = currentDotSizeFactor > 0; + float charWidth = charLength * currentWidthFactor; + if (textVisible) { + float currYPosition = yPosition + charHeight / 2.0f * currentTextSizeFactor + + charHeight * currentTextTranslationY * 0.8f; + canvas.save(); + float centerX = currentDrawPosition + charWidth / 2; + canvas.translate(centerX, currYPosition); + canvas.scale(currentTextSizeFactor, currentTextSizeFactor); + canvas.drawText(Character.toString(whichChar), 0, 0, mDrawPaint); + canvas.restore(); + } + if (dotVisible) { + canvas.save(); + float centerX = currentDrawPosition + charWidth / 2; + canvas.translate(centerX, yPosition); + canvas.drawCircle(0, 0, mDotSize / 2 * currentDotSizeFactor, mDrawPaint); + canvas.restore(); + } + return charWidth + mCharPadding * currentWidthFactor; + } + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/SecurityMessageDisplay.java b/packages/SystemUI/src/com/android/keyguard/SecurityMessageDisplay.java new file mode 100644 index 000000000000..6977b51e8712 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/SecurityMessageDisplay.java @@ -0,0 +1,28 @@ +/* + * 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; + +public interface SecurityMessageDisplay { + + void setNextMessageColor(int color); + + void setMessage(CharSequence msg); + + void setMessage(int resId); + + void formatMessage(int resId, Object... formatArgs); +} diff --git a/packages/SystemUI/src/com/android/keyguard/ViewMediatorCallback.java b/packages/SystemUI/src/com/android/keyguard/ViewMediatorCallback.java new file mode 100644 index 000000000000..327d218913d2 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/ViewMediatorCallback.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2014 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; + +/** + * The callback used by the keyguard view to tell the {@link KeyguardViewMediator} + * various things. + */ +public interface ViewMediatorCallback { + /** + * Reports user activity and requests that the screen stay on. + */ + void userActivity(); + + /** + * Report that the keyguard is done. + * + * @param strongAuth whether the user has authenticated with strong authentication like + * pattern, password or PIN but not by trust agents or fingerprint + * @param targetUserId a user that needs to be the foreground user at the completion. + */ + void keyguardDone(boolean strongAuth, int targetUserId); + + /** + * Report that the keyguard is done drawing. + */ + void keyguardDoneDrawing(); + + /** + * Tell ViewMediator that the current view needs IME input + * @param needsInput + */ + void setNeedsInput(boolean needsInput); + + /** + * Report that the keyguard is dismissable, pending the next keyguardDone call. + * + * @param strongAuth whether the user has authenticated with strong authentication like + * pattern, password or PIN but not by trust agents or fingerprint + * @param targetUserId a user that needs to be the foreground user at the completion. + */ + void keyguardDonePending(boolean strongAuth, int targetUserId); + + /** + * Report when keyguard is actually gone + */ + void keyguardGone(); + + /** + * Report when the UI is ready for dismissing the whole Keyguard. + */ + void readyForKeyguardDone(); + + /** + * Reset the keyguard and bouncer. + */ + void resetKeyguard(); + + /** + * Play the "device trusted" sound. + */ + void playTrustedSound(); + + /** + * @return true if the screen is on + */ + boolean isScreenOn(); + + /** + * @return one of the reasons why the bouncer needs to be shown right now and the user can't use + * his normal unlock method like fingerprint or trust agents. See + * {@link KeyguardSecurityView#PROMPT_REASON_NONE}, + * {@link KeyguardSecurityView#PROMPT_REASON_RESTART} and + * {@link KeyguardSecurityView#PROMPT_REASON_TIMEOUT}. + */ + int getBouncerPromptReason(); +} diff --git a/packages/SystemUI/src/com/android/systemui/EventLogTags.logtags b/packages/SystemUI/src/com/android/systemui/EventLogTags.logtags new file mode 100644 index 000000000000..d4149ea6d68f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/EventLogTags.logtags @@ -0,0 +1,68 @@ +# See system/core/logcat/event.logtags for a description of the format of this file. + +option java_package com.android.systemui; + +# --------------------------- +# PhoneStatusBar.java +# --------------------------- +36000 sysui_statusbar_touch (type|1),(x|1),(y|1),(disable1|1),(disable2|1) +36001 sysui_heads_up_status (key|3),(visible|1) +36002 sysui_fullscreen_notification (key|3) +36003 sysui_heads_up_escalation (key|3) +# sysui_status_bar_state: Logged whenever the status bar / keyguard state changes +## state: 0: SHADE, 1: KEYGUARD, 2: SHADE_LOCKED +## keyguardShowing: 1: Keyguard shown to the user (or keyguardOccluded) +## keyguardOccluded: 1: Keyguard active, but another activity is occluding it +## bouncerShowing: 1: Bouncer currently shown to the user +## secure: 1: The user has set up a secure unlock method (PIN, password, etc.) +## currentlyInsecure: 1: No secure unlock method set up (!secure), or trusted environment (TrustManager) +36004 sysui_status_bar_state (state|1),(keyguardShowing|1),(keyguardOccluded|1),(bouncerShowing|1),(secure|1),(currentlyInsecure|1) + +# --------------------------- +# PhoneStatusBarView.java +# --------------------------- +36010 sysui_panelbar_touch (type|1),(x|1),(y|1),(enabled|1) + +# --------------------------- +# NotificationPanelView.java +# --------------------------- +36020 sysui_notificationpanel_touch (type|1),(x|1),(y|1) +## type: 1: SWIPE_UP_UNLOCK Swiped up to dismiss the lockscreen. +## 2: SWIPE_DOWN_FULL_SHADE Swiped down to enter full shade. +## 3: TAP_UNLOCK_HINT Tapped in empty area, causes unlock hint. +## 4: SWIPE_CAMERA Swiped the camera icon, launches. +## 5: SWIPE_DIALER Swiped the dialer icon, launches. +## 6: TAP_LOCK Tapped the (unlocked) lock icon, locks the device. +## 7: TAP_NOTIFICATION_ACTIVATE Tapped a lockscreen notification, causes "tap again" hint. +## Note: Second tap logged as notification_clicked. +36021 sysui_lockscreen_gesture (type|1),(lengthDp|1),(velocityDp|1) + +# --------------------------- +# SettingsPanelView.java +# --------------------------- +36030 sysui_quickpanel_touch (type|1),(x|1),(y|1) + +# --------------------------- +# PanelHolder.java +# --------------------------- +36040 sysui_panelholder_touch (type|1),(x|1),(y|1) + +# --------------------------- +# SearchPanelView.java +# --------------------------- +36050 sysui_searchpanel_touch (type|1),(x|1),(y|1) + +# --------------------------- +# Recents.java, RecentsSystemUser.java +# --------------------------- +## type: 1: USER_BIND_SERVICE Secondary user tries binding to the system sysui service +## 2: USER_SYSTEM_BOUND Secondary user is bound to the system sysui service +## 3: USER_SYSTEM_UNBOUND Secondary user loses connection after system sysui has died +## 4: SYSTEM_REGISTER_USER System sysui registers user's callbacks +## 5: SYSTEM_UNREGISTER_USER System sysui unregisters user's callbacks (after death) +36060 sysui_recents_connection (type|1),(user|1) + +# --------------------------- +# LatencyTracker.java +# --------------------------- +36070 sysui_latency (action|1|5),(latency|1|3) |