diff options
author | Jim Miller <jaggies@google.com> | 2013-01-09 18:50:26 -0800 |
---|---|---|
committer | Jim Miller <jaggies@google.com> | 2013-02-27 17:26:43 -0800 |
commit | 25a272a9f6323f6a3513bb522d45e839449878ce (patch) | |
tree | 383249596b1d41e7643c38071789eca108c4db5e /packages/Keyguard/src/com/android | |
parent | 2973ccdba848b03cabba95f2b8eeae1b4204713e (diff) | |
download | frameworks_base-25a272a9f6323f6a3513bb522d45e839449878ce.zip frameworks_base-25a272a9f6323f6a3513bb522d45e839449878ce.tar.gz frameworks_base-25a272a9f6323f6a3513bb522d45e839449878ce.tar.bz2 |
Move keyguard source and resources into new package
This is part 1 of two commits. This commit moves all keyguard
source and resources to a new com.android.keyguard package.
The second part of this change applies an overlay that makes
it work.
Change-Id: I360e9ac7783c6cb289c992733818b9535df185b9
Diffstat (limited to 'packages/Keyguard/src/com/android')
49 files changed, 18362 insertions, 0 deletions
diff --git a/packages/Keyguard/src/com/android/keyguard/BiometricSensorUnlock.java b/packages/Keyguard/src/com/android/keyguard/BiometricSensorUnlock.java new file mode 100644 index 0000000..e65a716 --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/BiometricSensorUnlock.java @@ -0,0 +1,68 @@ +/* + * 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.internal.policy.impl.keyguard; + +import android.view.View; + +interface BiometricSensorUnlock { + /** + * Initializes the view provided for the biometric unlock UI to work within. The provided area + * completely covers the backup unlock mechanism. + * @param biometricUnlockView View provided for the biometric unlock UI. + */ + public void initializeView(View biometricUnlockView); + + /** + * Indicates whether the biometric unlock is running. Before + * {@link BiometricSensorUnlock#start} is called, isRunning() returns false. After a successful + * call to {@link BiometricSensorUnlock#start}, isRunning() returns true until the biometric + * unlock completes, {@link BiometricSensorUnlock#stop} has been called, or an error has + * forced the biometric unlock to stop. + * @return whether the biometric unlock is currently running. + */ + public boolean isRunning(); + + /** + * Stops and removes the biometric unlock and shows the backup unlock + */ + public void stopAndShowBackup(); + + /** + * Binds to the biometric unlock service and starts the unlock procedure. Called on the UI + * thread. + * @return false if it can't be started or the backup should be used. + */ + public boolean start(); + + /** + * Stops the biometric unlock procedure and unbinds from the service. Called on the UI thread. + * @return whether the biometric unlock was running when called. + */ + public boolean stop(); + + /** + * Cleans up any resources used by the biometric unlock. + */ + public void cleanUp(); + + /** + * Gets the Device Policy Manager quality of the biometric unlock sensor + * (e.g., PASSWORD_QUALITY_BIOMETRIC_WEAK). + * @return biometric unlock sensor quality, as defined by Device Policy Manager. + */ + public int getQuality(); +} diff --git a/packages/Keyguard/src/com/android/keyguard/CameraWidgetFrame.java b/packages/Keyguard/src/com/android/keyguard/CameraWidgetFrame.java new file mode 100644 index 0000000..762711d --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/CameraWidgetFrame.java @@ -0,0 +1,458 @@ +/* + * 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.internal.policy.impl.keyguard; + +import android.content.Context; +import android.content.pm.PackageManager.NameNotFoundException; +import android.graphics.Color; +import android.graphics.Point; +import android.graphics.Rect; +import android.os.Handler; +import android.os.SystemClock; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.ImageView.ScaleType; + +import com.android.internal.R; +import com.android.internal.policy.impl.keyguard.KeyguardActivityLauncher.CameraWidgetInfo; + +public class CameraWidgetFrame extends KeyguardWidgetFrame implements View.OnClickListener { + private static final String TAG = CameraWidgetFrame.class.getSimpleName(); + private static final boolean DEBUG = KeyguardHostView.DEBUG; + private static final int WIDGET_ANIMATION_DURATION = 250; // ms + private static final int WIDGET_WAIT_DURATION = 650; // ms + private static final int RECOVERY_DELAY = 1000; // ms + + interface Callbacks { + void onLaunchingCamera(); + void onCameraLaunchedSuccessfully(); + void onCameraLaunchedUnsuccessfully(); + } + + private final Handler mHandler = new Handler(); + private final KeyguardActivityLauncher mActivityLauncher; + private final Callbacks mCallbacks; + private final CameraWidgetInfo mWidgetInfo; + private final WindowManager mWindowManager; + private final Point mRenderedSize = new Point(); + private final int[] mTmpLoc = new int[2]; + private final Rect mTmpRect = new Rect(); + + private long mLaunchCameraStart; + private boolean mActive; + private boolean mTransitioning; + private boolean mDown; + + private FixedSizeFrameLayout mPreview; + private View mFullscreenPreview; + + private final Runnable mTransitionToCameraRunnable = new Runnable() { + @Override + public void run() { + transitionToCamera(); + }}; + + private final Runnable mTransitionToCameraEndAction = new Runnable() { + @Override + public void run() { + if (!mTransitioning) + return; + Handler worker = getWorkerHandler() != null ? getWorkerHandler() : mHandler; + mLaunchCameraStart = SystemClock.uptimeMillis(); + if (DEBUG) Log.d(TAG, "Launching camera at " + mLaunchCameraStart); + mActivityLauncher.launchCamera(worker, mSecureCameraActivityStartedRunnable); + }}; + + private final Runnable mPostTransitionToCameraEndAction = new Runnable() { + @Override + public void run() { + mHandler.post(mTransitionToCameraEndAction); + }}; + + private final Runnable mRecoverRunnable = new Runnable() { + @Override + public void run() { + recover(); + }}; + + private final Runnable mRenderRunnable = new Runnable() { + @Override + public void run() { + render(); + }}; + + private final Runnable mSecureCameraActivityStartedRunnable = new Runnable() { + @Override + public void run() { + onSecureCameraActivityStarted(); + } + }; + + private final KeyguardUpdateMonitorCallback mCallback = new KeyguardUpdateMonitorCallback() { + private boolean mShowing; + void onKeyguardVisibilityChanged(boolean showing) { + if (mShowing == showing) + return; + mShowing = showing; + CameraWidgetFrame.this.onKeyguardVisibilityChanged(mShowing); + }; + }; + + private static final class FixedSizeFrameLayout extends FrameLayout { + int width; + int height; + + FixedSizeFrameLayout(Context context) { + super(context); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + measureChildren( + MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); + setMeasuredDimension(width, height); + } + } + + private CameraWidgetFrame(Context context, Callbacks callbacks, + KeyguardActivityLauncher activityLauncher, + CameraWidgetInfo widgetInfo, View previewWidget) { + super(context); + mCallbacks = callbacks; + mActivityLauncher = activityLauncher; + mWidgetInfo = widgetInfo; + mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + KeyguardUpdateMonitor.getInstance(context).registerCallback(mCallback); + + mPreview = new FixedSizeFrameLayout(context); + mPreview.addView(previewWidget); + addView(mPreview); + + View clickBlocker = new View(context); + clickBlocker.setBackgroundColor(Color.TRANSPARENT); + clickBlocker.setOnClickListener(this); + addView(clickBlocker); + + setContentDescription(context.getString(R.string.keyguard_accessibility_camera)); + if (DEBUG) Log.d(TAG, "new CameraWidgetFrame instance " + instanceId()); + } + + public static CameraWidgetFrame create(Context context, Callbacks callbacks, + KeyguardActivityLauncher launcher) { + if (context == null || callbacks == null || launcher == null) + return null; + + CameraWidgetInfo widgetInfo = launcher.getCameraWidgetInfo(); + if (widgetInfo == null) + return null; + View previewWidget = getPreviewWidget(context, widgetInfo); + if (previewWidget == null) + return null; + + return new CameraWidgetFrame(context, callbacks, launcher, widgetInfo, previewWidget); + } + + private static View getPreviewWidget(Context context, CameraWidgetInfo widgetInfo) { + return widgetInfo.layoutId > 0 ? + inflateWidgetView(context, widgetInfo) : + inflateGenericWidgetView(context); + } + + private static View inflateWidgetView(Context context, CameraWidgetInfo widgetInfo) { + if (DEBUG) Log.d(TAG, "inflateWidgetView: " + widgetInfo.contextPackage); + View widgetView = null; + Exception exception = null; + try { + Context cameraContext = context.createPackageContext( + widgetInfo.contextPackage, Context.CONTEXT_RESTRICTED); + LayoutInflater cameraInflater = (LayoutInflater) + cameraContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + cameraInflater = cameraInflater.cloneInContext(cameraContext); + widgetView = cameraInflater.inflate(widgetInfo.layoutId, null, false); + } catch (NameNotFoundException e) { + exception = e; + } catch (RuntimeException e) { + exception = e; + } + if (exception != null) { + Log.w(TAG, "Error creating camera widget view", exception); + } + return widgetView; + } + + private static View inflateGenericWidgetView(Context context) { + if (DEBUG) Log.d(TAG, "inflateGenericWidgetView"); + ImageView iv = new ImageView(context); + iv.setImageResource(com.android.internal.R.drawable.ic_lockscreen_camera); + iv.setScaleType(ScaleType.CENTER); + iv.setBackgroundColor(Color.argb(127, 0, 0, 0)); + return iv; + } + + private void render() { + final View root = getRootView(); + final int width = root.getWidth(); + final int height = root.getHeight(); + if (mRenderedSize.x == width && mRenderedSize.y == height) { + if (DEBUG) Log.d(TAG, String.format("Already rendered at size=%sx%s", width, height)); + return; + } + if (width == 0 || height == 0) { + return; + } + + mPreview.width = width; + mPreview.height = height; + mPreview.requestLayout(); + + final int thisWidth = getWidth() - getPaddingLeft() - getPaddingRight(); + final int thisHeight = getHeight() - getPaddingTop() - getPaddingBottom(); + + final float pvScaleX = (float) thisWidth / width; + final float pvScaleY = (float) thisHeight / height; + final float pvScale = Math.min(pvScaleX, pvScaleY); + + final int pvWidth = (int) (pvScale * width); + final int pvHeight = (int) (pvScale * height); + + final float pvTransX = pvWidth < thisWidth ? (thisWidth - pvWidth) / 2 : 0; + final float pvTransY = pvHeight < thisHeight ? (thisHeight - pvHeight) / 2 : 0; + + mPreview.setPivotX(0); + mPreview.setPivotY(0); + mPreview.setScaleX(pvScale); + mPreview.setScaleY(pvScale); + mPreview.setTranslationX(pvTransX); + mPreview.setTranslationY(pvTransY); + + mRenderedSize.set(width, height); + if (DEBUG) Log.d(TAG, String.format("Rendered camera widget size=%sx%s instance=%s", + width, height, instanceId())); + } + + private void transitionToCamera() { + if (mTransitioning || mDown) return; + + mTransitioning = true; + + enableWindowExitAnimation(false); + + mPreview.getLocationInWindow(mTmpLoc); + final float pvHeight = mPreview.getHeight() * mPreview.getScaleY(); + final float pvCenter = mTmpLoc[1] + pvHeight / 2f; + + final ViewGroup root = (ViewGroup) getRootView(); + if (mFullscreenPreview == null) { + mFullscreenPreview = getPreviewWidget(mContext, mWidgetInfo); + mFullscreenPreview.setClickable(false); + root.addView(mFullscreenPreview); + } + + root.getWindowVisibleDisplayFrame(mTmpRect); + final float fsHeight = mTmpRect.height(); + final float fsCenter = mTmpRect.top + fsHeight / 2; + + final float fsScaleY = pvHeight / fsHeight; + final float fsTransY = pvCenter - fsCenter; + final float fsScaleX = mPreview.getScaleX(); + + mPreview.setVisibility(View.GONE); + mFullscreenPreview.setVisibility(View.VISIBLE); + mFullscreenPreview.setTranslationY(fsTransY); + mFullscreenPreview.setScaleX(fsScaleX); + mFullscreenPreview.setScaleY(fsScaleY); + mFullscreenPreview + .animate() + .scaleX(1) + .scaleY(1) + .translationX(0) + .translationY(0) + .setDuration(WIDGET_ANIMATION_DURATION) + .withEndAction(mPostTransitionToCameraEndAction) + .start(); + mCallbacks.onLaunchingCamera(); + } + + private void recover() { + if (DEBUG) Log.d(TAG, "recovering at " + SystemClock.uptimeMillis()); + mCallbacks.onCameraLaunchedUnsuccessfully(); + reset(); + } + + @Override + public void setOnLongClickListener(OnLongClickListener l) { + // ignore + } + + @Override + public void onClick(View v) { + if (DEBUG) Log.d(TAG, "clicked"); + if (mTransitioning) return; + if (mActive) { + cancelTransitionToCamera(); + transitionToCamera(); + } + } + + @Override + protected void onDetachedFromWindow() { + if (DEBUG) Log.d(TAG, "onDetachedFromWindow: instance " + instanceId() + + " at " + SystemClock.uptimeMillis()); + super.onDetachedFromWindow(); + KeyguardUpdateMonitor.getInstance(mContext).removeCallback(mCallback); + cancelTransitionToCamera(); + mHandler.removeCallbacks(mRecoverRunnable); + } + + @Override + public void onActive(boolean isActive) { + mActive = isActive; + if (mActive) { + rescheduleTransitionToCamera(); + } else { + reset(); + } + } + + @Override + public boolean onUserInteraction(MotionEvent event) { + if (mTransitioning) { + if (DEBUG) Log.d(TAG, "onUserInteraction eaten: mTransitioning"); + return true; + } + + getLocationOnScreen(mTmpLoc); + int rawBottom = mTmpLoc[1] + getHeight(); + if (event.getRawY() > rawBottom) { + if (DEBUG) Log.d(TAG, "onUserInteraction eaten: below widget"); + return true; + } + + int action = event.getAction(); + mDown = action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_MOVE; + if (mActive) { + rescheduleTransitionToCamera(); + } + if (DEBUG) Log.d(TAG, "onUserInteraction observed, not eaten"); + return false; + } + + @Override + protected void onFocusLost() { + if (DEBUG) Log.d(TAG, "onFocusLost at " + SystemClock.uptimeMillis()); + cancelTransitionToCamera(); + super.onFocusLost(); + } + + public void onScreenTurnedOff() { + if (DEBUG) Log.d(TAG, "onScreenTurnedOff"); + reset(); + } + + private void rescheduleTransitionToCamera() { + if (DEBUG) Log.d(TAG, "rescheduleTransitionToCamera at " + SystemClock.uptimeMillis()); + mHandler.removeCallbacks(mTransitionToCameraRunnable); + mHandler.postDelayed(mTransitionToCameraRunnable, WIDGET_WAIT_DURATION); + } + + private void cancelTransitionToCamera() { + if (DEBUG) Log.d(TAG, "cancelTransitionToCamera at " + SystemClock.uptimeMillis()); + mHandler.removeCallbacks(mTransitionToCameraRunnable); + } + + private void onCameraLaunched() { + mCallbacks.onCameraLaunchedSuccessfully(); + reset(); + } + + private void reset() { + if (DEBUG) Log.d(TAG, "reset at " + SystemClock.uptimeMillis()); + mLaunchCameraStart = 0; + mTransitioning = false; + mDown = false; + cancelTransitionToCamera(); + mHandler.removeCallbacks(mRecoverRunnable); + mPreview.setVisibility(View.VISIBLE); + if (mFullscreenPreview != null) { + mFullscreenPreview.animate().cancel(); + mFullscreenPreview.setVisibility(View.GONE); + } + enableWindowExitAnimation(true); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + if (DEBUG) Log.d(TAG, String.format("onSizeChanged new=%sx%s old=%sx%s at %s", + w, h, oldw, oldh, SystemClock.uptimeMillis())); + mHandler.post(mRenderRunnable); + super.onSizeChanged(w, h, oldw, oldh); + } + + @Override + public void onBouncerShowing(boolean showing) { + if (showing) { + mTransitioning = false; + mHandler.post(mRecoverRunnable); + } + } + + private void enableWindowExitAnimation(boolean isEnabled) { + View root = getRootView(); + ViewGroup.LayoutParams lp = root.getLayoutParams(); + if (!(lp instanceof WindowManager.LayoutParams)) + return; + WindowManager.LayoutParams wlp = (WindowManager.LayoutParams) lp; + int newWindowAnimations = isEnabled ? com.android.internal.R.style.Animation_LockScreen : 0; + if (newWindowAnimations != wlp.windowAnimations) { + if (DEBUG) Log.d(TAG, "setting windowAnimations to: " + newWindowAnimations + + " at " + SystemClock.uptimeMillis()); + wlp.windowAnimations = newWindowAnimations; + mWindowManager.updateViewLayout(root, wlp); + } + } + + private void onKeyguardVisibilityChanged(boolean showing) { + if (DEBUG) Log.d(TAG, "onKeyguardVisibilityChanged " + showing + + " at " + SystemClock.uptimeMillis()); + if (mTransitioning && !showing) { + mTransitioning = false; + mHandler.removeCallbacks(mRecoverRunnable); + if (mLaunchCameraStart > 0) { + long launchTime = SystemClock.uptimeMillis() - mLaunchCameraStart; + if (DEBUG) Log.d(TAG, String.format("Camera took %sms to launch", launchTime)); + mLaunchCameraStart = 0; + onCameraLaunched(); + } + } + } + + private void onSecureCameraActivityStarted() { + if (DEBUG) Log.d(TAG, "onSecureCameraActivityStarted at " + SystemClock.uptimeMillis()); + mHandler.postDelayed(mRecoverRunnable, RECOVERY_DELAY); + } + + private String instanceId() { + return Integer.toHexString(hashCode()); + } +} diff --git a/packages/Keyguard/src/com/android/keyguard/CarrierText.java b/packages/Keyguard/src/com/android/keyguard/CarrierText.java new file mode 100644 index 0000000..a38e86d --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/CarrierText.java @@ -0,0 +1,258 @@ +/* + * 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.internal.policy.impl.keyguard; + +import android.content.Context; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.widget.TextView; + +import com.android.internal.R; +import com.android.internal.telephony.IccCardConstants; +import com.android.internal.telephony.IccCardConstants.State; +import com.android.internal.widget.LockPatternUtils; + +public class CarrierText extends TextView { + private static CharSequence mSeparator; + + private LockPatternUtils mLockPatternUtils; + + private KeyguardUpdateMonitorCallback mCallback = new KeyguardUpdateMonitorCallback() { + private CharSequence mPlmn; + private CharSequence mSpn; + private State mSimState; + + @Override + public void onRefreshCarrierInfo(CharSequence plmn, CharSequence spn) { + mPlmn = plmn; + mSpn = spn; + updateCarrierText(mSimState, mPlmn, mSpn); + } + + @Override + public void onSimStateChanged(IccCardConstants.State simState) { + mSimState = simState; + updateCarrierText(mSimState, mPlmn, mSpn); + } + }; + /** + * 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); + mLockPatternUtils = new LockPatternUtils(mContext); + } + + protected void updateCarrierText(State simState, CharSequence plmn, CharSequence spn) { + CharSequence text = getCarrierTextForSimState(simState, plmn, spn); + if (KeyguardViewManager.USE_UPPER_CASE) { + setText(text != null ? text.toString().toUpperCase() : null); + } else { + setText(text); + } + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mSeparator = getResources().getString(R.string.kg_text_message_separator); + setSelected(true); // Allow marquee to work. + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + KeyguardUpdateMonitor.getInstance(mContext).registerCallback(mCallback); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + KeyguardUpdateMonitor.getInstance(mContext).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 plmn + * @param spn + * @return + */ + private CharSequence getCarrierTextForSimState(IccCardConstants.State simState, + CharSequence plmn, CharSequence spn) { + CharSequence carrierText = null; + StatusMode status = getStatusForIccState(simState); + switch (status) { + case Normal: + carrierText = concatenate(plmn, spn); + break; + + case SimNotReady: + carrierText = null; // nothing to display yet. + break; + + case NetworkLocked: + carrierText = makeCarrierStringOnEmergencyCapable( + mContext.getText(R.string.lockscreen_network_locked_message), plmn); + break; + + case SimMissing: + // 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" + carrierText = makeCarrierStringOnEmergencyCapable( + getContext().getText(R.string.lockscreen_missing_sim_message_short), + plmn); + break; + + case SimPermDisabled: + carrierText = getContext().getText( + R.string.lockscreen_permanent_disabled_sim_message_short); + break; + + case SimMissingLocked: + carrierText = makeCarrierStringOnEmergencyCapable( + getContext().getText(R.string.lockscreen_missing_sim_message_short), + plmn); + break; + + case SimLocked: + carrierText = makeCarrierStringOnEmergencyCapable( + getContext().getText(R.string.lockscreen_sim_locked_message), + plmn); + break; + + case SimPukLocked: + carrierText = makeCarrierStringOnEmergencyCapable( + getContext().getText(R.string.lockscreen_sim_puk_locked_message), + plmn); + break; + } + + return carrierText; + } + + /* + * Add emergencyCallMessage to carrier string only if phone supports emergency calls. + */ + private CharSequence makeCarrierStringOnEmergencyCapable( + CharSequence simMessage, CharSequence emergencyCallMessage) { + if (mLockPatternUtils.isEmergencyCallCapable()) { + 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.lockscreen_instructions_when_pattern_disabled; + break; + + case SimMissing: + carrierHelpTextId = R.string.lockscreen_missing_sim_instructions_long; + break; + + case SimPermDisabled: + carrierHelpTextId = R.string.lockscreen_permanent_disabled_sim_instructions; + break; + + case SimMissingLocked: + carrierHelpTextId = R.string.lockscreen_missing_sim_instructions; + break; + + case Normal: + case SimLocked: + case SimPukLocked: + break; + } + + return mContext.getText(carrierHelpTextId); + } +} diff --git a/packages/Keyguard/src/com/android/keyguard/ChallengeLayout.java b/packages/Keyguard/src/com/android/keyguard/ChallengeLayout.java new file mode 100644 index 0000000..8ece559 --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/ChallengeLayout.java @@ -0,0 +1,97 @@ +/* + * 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.internal.policy.impl.keyguard; + +/** + * Interface implemented by ViewGroup-derived layouts that implement + * special logic for presenting security challenges to the user. + */ +public interface ChallengeLayout { + /** + * @return true if the security challenge area of this layout is currently visible + */ + boolean isChallengeShowing(); + + /** + * @return true if the challenge area significantly overlaps other content + */ + boolean isChallengeOverlapping(); + + /** + * Show or hide the challenge layout. + * + * If you want to show the challenge layout in bouncer mode where applicable, + * use {@link #showBouncer()} instead. + * + * @param b true to show, false to hide + */ + void showChallenge(boolean b); + + /** + * Show the bouncer challenge. This may block access to other child views. + */ + void showBouncer(); + + /** + * Hide the bouncer challenge if it is currently showing. + * This may restore previously blocked access to other child views. + */ + void hideBouncer(); + + /** + * Returns true if the challenge is currently in bouncer mode, + * potentially blocking access to other child views. + */ + boolean isBouncing(); + + /** + * Returns the duration of the bounce animation. + */ + int getBouncerAnimationDuration(); + + /** + * Set a listener that will respond to changes in bouncer state. + * + * @param listener listener to register + */ + void setOnBouncerStateChangedListener(OnBouncerStateChangedListener listener); + + /** + * Listener interface that reports changes in bouncer state. + * The bouncer is + */ + public interface OnBouncerStateChangedListener { + /** + * Called when the bouncer state changes. + * The bouncer is activated when the user must pass a security challenge + * to proceed with the requested action. + * + * <p>This differs from simply showing or hiding the security challenge + * as the bouncer will prevent interaction with other elements of the UI. + * If the user attempts to escape from the bouncer, it will be dismissed, + * this method will be called with false as the parameter, and the action + * should be canceled. If the security component reports a successful + * authentication and the containing code calls hideBouncer() as a result, + * this method will also be called with a false parameter. It is up to the + * caller of hideBouncer to be ready for this.</p> + * + * @param bouncerActive true if the bouncer is now active, + * false if the bouncer was dismissed. + */ + public void onBouncerStateChanged(boolean bouncerActive); + } +} diff --git a/packages/Keyguard/src/com/android/keyguard/CheckLongPressHelper.java b/packages/Keyguard/src/com/android/keyguard/CheckLongPressHelper.java new file mode 100644 index 0000000..4825e23 --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/CheckLongPressHelper.java @@ -0,0 +1,82 @@ +/* + * 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.internal.policy.impl.keyguard; + +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; + +public class CheckLongPressHelper { + private View mView; + private boolean mHasPerformedLongPress; + private CheckForLongPress mPendingCheckForLongPress; + private float mDownX, mDownY; + private int mLongPressTimeout; + private int mScaledTouchSlop; + + class CheckForLongPress implements Runnable { + public void run() { + if ((mView.getParent() != null) && mView.hasWindowFocus() + && !mHasPerformedLongPress) { + if (mView.performLongClick()) { + mView.setPressed(false); + mHasPerformedLongPress = true; + } + } + } + } + + public CheckLongPressHelper(View v) { + mScaledTouchSlop = ViewConfiguration.get(v.getContext()).getScaledTouchSlop(); + mLongPressTimeout = ViewConfiguration.getLongPressTimeout(); + mView = v; + } + + public void postCheckForLongPress(MotionEvent ev) { + mDownX = ev.getX(); + mDownY = ev.getY(); + mHasPerformedLongPress = false; + + if (mPendingCheckForLongPress == null) { + mPendingCheckForLongPress = new CheckForLongPress(); + } + mView.postDelayed(mPendingCheckForLongPress, mLongPressTimeout); + } + + public void onMove(MotionEvent ev) { + float x = ev.getX(); + float y = ev.getY(); + boolean xMoved = Math.abs(mDownX - x) > mScaledTouchSlop; + boolean yMoved = Math.abs(mDownY - y) > mScaledTouchSlop; + + if (xMoved || yMoved) { + cancelLongPress(); + } + } + + public void cancelLongPress() { + mHasPerformedLongPress = false; + if (mPendingCheckForLongPress != null) { + mView.removeCallbacks(mPendingCheckForLongPress); + mPendingCheckForLongPress = null; + } + } + + public boolean hasPerformedLongPress() { + return mHasPerformedLongPress; + } +} diff --git a/packages/Keyguard/src/com/android/keyguard/ClockView.java b/packages/Keyguard/src/com/android/keyguard/ClockView.java new file mode 100644 index 0000000..6c701c7 --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/ClockView.java @@ -0,0 +1,224 @@ +/* + * 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.internal.policy.impl.keyguard; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.database.ContentObserver; +import android.graphics.Typeface; +import android.os.Handler; +import android.os.UserHandle; +import android.provider.Settings; +import android.text.format.DateFormat; +import android.util.AttributeSet; +import android.view.View; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import java.lang.ref.WeakReference; +import java.text.DateFormatSymbols; +import java.util.Calendar; +import com.android.internal.R; + +/** + * Displays the time + */ +public class ClockView extends RelativeLayout { + private static final String ANDROID_CLOCK_FONT_FILE = "/system/fonts/AndroidClock.ttf"; + private final static String M12 = "h:mm"; + private final static String M24 = "kk:mm"; + + private Calendar mCalendar; + private String mFormat; + private TextView mTimeView; + private AmPm mAmPm; + private ContentObserver mFormatChangeObserver; + private int mAttached = 0; // for debugging - tells us whether attach/detach is unbalanced + + /* called by system on minute ticks */ + private final Handler mHandler = new Handler(); + private BroadcastReceiver mIntentReceiver; + + private static class TimeChangedReceiver extends BroadcastReceiver { + private WeakReference<ClockView> mClock; + private Context mContext; + + public TimeChangedReceiver(ClockView clock) { + mClock = new WeakReference<ClockView>(clock); + mContext = clock.getContext(); + } + + @Override + public void onReceive(Context context, Intent intent) { + // Post a runnable to avoid blocking the broadcast. + final boolean timezoneChanged = + intent.getAction().equals(Intent.ACTION_TIMEZONE_CHANGED); + final ClockView clock = mClock.get(); + if (clock != null) { + clock.mHandler.post(new Runnable() { + public void run() { + if (timezoneChanged) { + clock.mCalendar = Calendar.getInstance(); + } + clock.updateTime(); + } + }); + } else { + try { + mContext.unregisterReceiver(this); + } catch (RuntimeException e) { + // Shouldn't happen + } + } + } + }; + + static class AmPm { + private TextView mAmPmTextView; + private String mAmString, mPmString; + + AmPm(View parent, Typeface tf) { + // No longer used, uncomment if we decide to use AM/PM indicator again + // mAmPmTextView = (TextView) parent.findViewById(R.id.am_pm); + if (mAmPmTextView != null && tf != null) { + mAmPmTextView.setTypeface(tf); + } + + String[] ampm = new DateFormatSymbols().getAmPmStrings(); + mAmString = ampm[0]; + mPmString = ampm[1]; + } + + void setShowAmPm(boolean show) { + if (mAmPmTextView != null) { + mAmPmTextView.setVisibility(show ? View.VISIBLE : View.GONE); + } + } + + void setIsMorning(boolean isMorning) { + if (mAmPmTextView != null) { + mAmPmTextView.setText(isMorning ? mAmString : mPmString); + } + } + } + + private static class FormatChangeObserver extends ContentObserver { + private WeakReference<ClockView> mClock; + private Context mContext; + public FormatChangeObserver(ClockView clock) { + super(new Handler()); + mClock = new WeakReference<ClockView>(clock); + mContext = clock.getContext(); + } + @Override + public void onChange(boolean selfChange) { + ClockView digitalClock = mClock.get(); + if (digitalClock != null) { + digitalClock.setDateFormat(); + digitalClock.updateTime(); + } else { + try { + mContext.getContentResolver().unregisterContentObserver(this); + } catch (RuntimeException e) { + // Shouldn't happen + } + } + } + } + + public ClockView(Context context) { + this(context, null); + } + + public ClockView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mTimeView = (TextView) findViewById(R.id.clock_text); + mTimeView.setTypeface(Typeface.createFromFile(ANDROID_CLOCK_FONT_FILE)); + mAmPm = new AmPm(this, null); + mCalendar = Calendar.getInstance(); + setDateFormat(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + mAttached++; + + /* monitor time ticks, time changed, timezone */ + if (mIntentReceiver == null) { + mIntentReceiver = new TimeChangedReceiver(this); + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_TIME_TICK); + filter.addAction(Intent.ACTION_TIME_CHANGED); + filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); + mContext.registerReceiverAsUser(mIntentReceiver, UserHandle.OWNER, filter, null, null ); + } + + /* monitor 12/24-hour display preference */ + if (mFormatChangeObserver == null) { + mFormatChangeObserver = new FormatChangeObserver(this); + mContext.getContentResolver().registerContentObserver( + Settings.System.CONTENT_URI, true, mFormatChangeObserver); + } + + updateTime(); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + mAttached--; + + if (mIntentReceiver != null) { + mContext.unregisterReceiver(mIntentReceiver); + } + if (mFormatChangeObserver != null) { + mContext.getContentResolver().unregisterContentObserver( + mFormatChangeObserver); + } + + mFormatChangeObserver = null; + mIntentReceiver = null; + } + + void updateTime(Calendar c) { + mCalendar = c; + updateTime(); + } + + public void updateTime() { + mCalendar.setTimeInMillis(System.currentTimeMillis()); + + CharSequence newTime = DateFormat.format(mFormat, mCalendar); + mTimeView.setText(newTime); + mAmPm.setIsMorning(mCalendar.get(Calendar.AM_PM) == 0); + } + + private void setDateFormat() { + mFormat = android.text.format.DateFormat.is24HourFormat(getContext()) ? M24 : M12; + mAmPm.setShowAmPm(mFormat.equals(M12)); + } +} diff --git a/packages/Keyguard/src/com/android/keyguard/EmergencyButton.java b/packages/Keyguard/src/com/android/keyguard/EmergencyButton.java new file mode 100644 index 0000000..cd7324c --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/EmergencyButton.java @@ -0,0 +1,130 @@ +/* + * 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.internal.policy.impl.keyguard; + +import android.content.Context; +import android.content.Intent; +import android.os.PowerManager; +import android.os.SystemClock; +import android.telephony.TelephonyManager; +import android.util.AttributeSet; +import android.view.View; +import android.widget.Button; + +import com.android.internal.telephony.IccCardConstants.State; +import com.android.internal.widget.LockPatternUtils; + +/** + * 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 int EMERGENCY_CALL_TIMEOUT = 10000; // screen timeout after starting e.d. + private static final String ACTION_EMERGENCY_DIAL = "com.android.phone.EmergencyDialer.DIAL"; + + KeyguardUpdateMonitorCallback mInfoCallback = new KeyguardUpdateMonitorCallback() { + + @Override + public void onSimStateChanged(State simState) { + int phoneState = KeyguardUpdateMonitor.getInstance(mContext).getPhoneState(); + updateEmergencyCallButton(simState, phoneState); + } + + void onPhoneStateChanged(int phoneState) { + State simState = KeyguardUpdateMonitor.getInstance(mContext).getSimState(); + updateEmergencyCallButton(simState, phoneState); + }; + }; + private LockPatternUtils mLockPatternUtils; + private PowerManager mPowerManager; + + public EmergencyButton(Context context) { + this(context, null); + } + + public EmergencyButton(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @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(); + } + }); + int phoneState = KeyguardUpdateMonitor.getInstance(mContext).getPhoneState(); + State simState = KeyguardUpdateMonitor.getInstance(mContext).getSimState(); + updateEmergencyCallButton(simState, phoneState); + } + + /** + * Shows the emergency dialer or returns the user to the existing call. + */ + public void takeEmergencyCallAction() { + // 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); + if (TelephonyManager.getDefault().getCallState() + == TelephonyManager.CALL_STATE_OFFHOOK) { + mLockPatternUtils.resumeCall(); + } else { + Intent intent = new Intent(ACTION_EMERGENCY_DIAL); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + getContext().startActivity(intent); + } + } + + private void updateEmergencyCallButton(State simState, int phoneState) { + boolean enabled = false; + if (phoneState == TelephonyManager.CALL_STATE_OFFHOOK) { + enabled = true; // always show "return to call" if phone is off-hook + } else if (mLockPatternUtils.isEmergencyCallCapable()) { + boolean simLocked = KeyguardUpdateMonitor.getInstance(mContext).isSimLocked(); + if (simLocked) { + // Some countries can't handle emergency calls while SIM is locked. + enabled = mLockPatternUtils.isEmergencyCallEnabledWhileSimLocked(); + } else { + // True if we need to show a secure screen (pin/pattern/SIM pin/SIM puk); + // hides emergency button on "Slide" screen if device is not secure. + enabled = mLockPatternUtils.isSecure(); + } + } + mLockPatternUtils.updateEmergencyCallButtonState(this, phoneState, enabled, + KeyguardViewManager.USE_UPPER_CASE, false); + } + +} diff --git a/packages/Keyguard/src/com/android/keyguard/FaceUnlock.java b/packages/Keyguard/src/com/android/keyguard/FaceUnlock.java new file mode 100644 index 0000000..e58eb5b --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/FaceUnlock.java @@ -0,0 +1,462 @@ +/* + * 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.internal.policy.impl.keyguard; + +import com.android.internal.policy.IFaceLockCallback; +import com.android.internal.policy.IFaceLockInterface; +import com.android.internal.widget.LockPatternUtils; + +import android.app.admin.DevicePolicyManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.PowerManager; +import android.os.RemoteException; +import android.os.UserHandle; +import android.util.Log; +import android.view.View; + +public class FaceUnlock implements BiometricSensorUnlock, Handler.Callback { + + private static final boolean DEBUG = false; + private static final String TAG = "FULLockscreen"; + + private final Context mContext; + private final LockPatternUtils mLockPatternUtils; + + // TODO: is mServiceRunning needed or can we just use mIsRunning or check if mService is null? + private boolean mServiceRunning = false; + // TODO: now that the code has been restructure to do almost all operations from a handler, this + // lock may no longer be necessary. + private final Object mServiceRunningLock = new Object(); + private IFaceLockInterface mService; + private boolean mBoundToService = false; + private View mFaceUnlockView; + + private Handler mHandler; + private final int MSG_SERVICE_CONNECTED = 0; + private final int MSG_SERVICE_DISCONNECTED = 1; + private final int MSG_UNLOCK = 2; + private final int MSG_CANCEL = 3; + private final int MSG_REPORT_FAILED_ATTEMPT = 4; + private final int MSG_POKE_WAKELOCK = 5; + + // TODO: This was added for the purpose of adhering to what the biometric interface expects + // the isRunning() function to return. However, it is probably not necessary to have both + // mRunning and mServiceRunning. I'd just rather wait to change that logic. + private volatile boolean mIsRunning = false; + + // So the user has a consistent amount of time when brought to the backup method from Face + // Unlock + private final int BACKUP_LOCK_TIMEOUT = 5000; + + KeyguardSecurityCallback mKeyguardScreenCallback; + + /** + * Stores some of the structures that Face Unlock will need to access and creates the handler + * will be used to execute messages on the UI thread. + */ + public FaceUnlock(Context context) { + mContext = context; + mLockPatternUtils = new LockPatternUtils(context); + mHandler = new Handler(this); + } + + public void setKeyguardCallback(KeyguardSecurityCallback keyguardScreenCallback) { + mKeyguardScreenCallback = keyguardScreenCallback; + } + + /** + * Stores and displays the view that Face Unlock is allowed to draw within. + * TODO: since the layout object will eventually be shared by multiple biometric unlock + * methods, we will have to add our other views (background, cancel button) here. + */ + public void initializeView(View biometricUnlockView) { + Log.d(TAG, "initializeView()"); + mFaceUnlockView = biometricUnlockView; + } + + /** + * Indicates whether Face Unlock is currently running. + */ + public boolean isRunning() { + return mIsRunning; + } + + /** + * Dismisses face unlock and goes to the backup lock + */ + public void stopAndShowBackup() { + if (DEBUG) Log.d(TAG, "stopAndShowBackup()"); + mHandler.sendEmptyMessage(MSG_CANCEL); + } + + /** + * Binds to the Face Unlock service. Face Unlock will be started when the bind completes. The + * Face Unlock view is displayed to hide the backup lock while the service is starting up. + * Called on the UI thread. + */ + public boolean start() { + if (DEBUG) Log.d(TAG, "start()"); + if (mHandler.getLooper() != Looper.myLooper()) { + Log.e(TAG, "start() called off of the UI thread"); + } + + if (mIsRunning) { + Log.w(TAG, "start() called when already running"); + } + + if (!mBoundToService) { + Log.d(TAG, "Binding to Face Unlock service for user=" + + mLockPatternUtils.getCurrentUser()); + mContext.bindServiceAsUser(new Intent(IFaceLockInterface.class.getName()), + mConnection, + Context.BIND_AUTO_CREATE, + new UserHandle(mLockPatternUtils.getCurrentUser())); + mBoundToService = true; + } else { + Log.w(TAG, "Attempt to bind to Face Unlock when already bound"); + } + + mIsRunning = true; + return true; + } + + /** + * Stops Face Unlock and unbinds from the service. Called on the UI thread. + */ + public boolean stop() { + if (DEBUG) Log.d(TAG, "stop()"); + if (mHandler.getLooper() != Looper.myLooper()) { + Log.e(TAG, "stop() called from non-UI thread"); + } + + // Clearing any old service connected messages. + mHandler.removeMessages(MSG_SERVICE_CONNECTED); + + boolean mWasRunning = mIsRunning; + + stopUi(); + + if (mBoundToService) { + if (mService != null) { + try { + mService.unregisterCallback(mFaceUnlockCallback); + } catch (RemoteException e) { + // Not much we can do + } + } + Log.d(TAG, "Unbinding from Face Unlock service"); + mContext.unbindService(mConnection); + mBoundToService = false; + } else { + // This is usually not an error when this happens. Sometimes we will tell it to + // unbind multiple times because it's called from both onWindowFocusChanged and + // onDetachedFromWindow. + if (DEBUG) Log.d(TAG, "Attempt to unbind from Face Unlock when not bound"); + } + mIsRunning = false; + return mWasRunning; + } + + /** + * Frees up resources used by Face Unlock and stops it if it is still running. + */ + public void cleanUp() { + if (DEBUG) Log.d(TAG, "cleanUp()"); + if (mService != null) { + try { + mService.unregisterCallback(mFaceUnlockCallback); + } catch (RemoteException e) { + // Not much we can do + } + stopUi(); + mService = null; + } + } + + /** + * Returns the Device Policy Manager quality for Face Unlock, which is BIOMETRIC_WEAK. + */ + public int getQuality() { + return DevicePolicyManager.PASSWORD_QUALITY_BIOMETRIC_WEAK; + } + + /** + * Handles messages such that everything happens on the UI thread in a deterministic order. + * Calls from the Face Unlock service come from binder threads. Calls from lockscreen typically + * come from the UI thread. This makes sure there are no race conditions between those calls. + */ + public boolean handleMessage(Message msg) { + switch (msg.what) { + case MSG_SERVICE_CONNECTED: + handleServiceConnected(); + break; + case MSG_SERVICE_DISCONNECTED: + handleServiceDisconnected(); + break; + case MSG_UNLOCK: + handleUnlock(msg.arg1); + break; + case MSG_CANCEL: + handleCancel(); + break; + case MSG_REPORT_FAILED_ATTEMPT: + handleReportFailedAttempt(); + break; + case MSG_POKE_WAKELOCK: + handlePokeWakelock(msg.arg1); + break; + default: + Log.e(TAG, "Unhandled message"); + return false; + } + return true; + } + + /** + * Tells the service to start its UI via an AIDL interface. Called when the + * onServiceConnected() callback is received. + */ + void handleServiceConnected() { + Log.d(TAG, "handleServiceConnected()"); + + // It is possible that an unbind has occurred in the time between the bind and when this + // function is reached. If an unbind has already occurred, proceeding on to call startUi() + // can result in a fatal error. Note that the onServiceConnected() callback is + // asynchronous, so this possibility would still exist if we executed this directly in + // onServiceConnected() rather than using a handler. + if (!mBoundToService) { + Log.d(TAG, "Dropping startUi() in handleServiceConnected() because no longer bound"); + return; + } + + try { + mService.registerCallback(mFaceUnlockCallback); + } catch (RemoteException e) { + Log.e(TAG, "Caught exception connecting to Face Unlock: " + e.toString()); + mService = null; + mBoundToService = false; + mIsRunning = false; + return; + } + + if (mFaceUnlockView != null) { + IBinder windowToken = mFaceUnlockView.getWindowToken(); + if (windowToken != null) { + // When switching between portrait and landscape view while Face Unlock is running, + // the screen will eventually go dark unless we poke the wakelock when Face Unlock + // is restarted. + mKeyguardScreenCallback.userActivity(0); + + int[] position; + position = new int[2]; + mFaceUnlockView.getLocationInWindow(position); + startUi(windowToken, position[0], position[1], mFaceUnlockView.getWidth(), + mFaceUnlockView.getHeight()); + } else { + Log.e(TAG, "windowToken is null in handleServiceConnected()"); + } + } + } + + /** + * Called when the onServiceDisconnected() callback is received. This should not happen during + * normal operation. It indicates an error has occurred. + */ + void handleServiceDisconnected() { + Log.e(TAG, "handleServiceDisconnected()"); + // TODO: this lock may no longer be needed now that everything is being called from a + // handler + synchronized (mServiceRunningLock) { + mService = null; + mServiceRunning = false; + } + mBoundToService = false; + mIsRunning = false; + } + + /** + * Stops the Face Unlock service and tells the device to grant access to the user. + */ + void handleUnlock(int authenticatedUserId) { + if (DEBUG) Log.d(TAG, "handleUnlock()"); + stop(); + int currentUserId = mLockPatternUtils.getCurrentUser(); + if (authenticatedUserId == currentUserId) { + if (DEBUG) Log.d(TAG, "Unlocking for user " + authenticatedUserId); + mKeyguardScreenCallback.reportSuccessfulUnlockAttempt(); + mKeyguardScreenCallback.dismiss(true); + } else { + Log.d(TAG, "Ignoring unlock for authenticated user (" + authenticatedUserId + + ") because the current user is " + currentUserId); + } + } + + /** + * Stops the Face Unlock service and goes to the backup lock. + */ + void handleCancel() { + if (DEBUG) Log.d(TAG, "handleCancel()"); + // We are going to the backup method, so we don't want to see Face Unlock again until the + // next time the user visits keyguard. + KeyguardUpdateMonitor.getInstance(mContext).setAlternateUnlockEnabled(false); + + mKeyguardScreenCallback.showBackupSecurity(); + stop(); + mKeyguardScreenCallback.userActivity(BACKUP_LOCK_TIMEOUT); + } + + /** + * Increments the number of failed Face Unlock attempts. + */ + void handleReportFailedAttempt() { + if (DEBUG) Log.d(TAG, "handleReportFailedAttempt()"); + // We are going to the backup method, so we don't want to see Face Unlock again until the + // next time the user visits keyguard. + KeyguardUpdateMonitor.getInstance(mContext).setAlternateUnlockEnabled(false); + + mKeyguardScreenCallback.reportFailedUnlockAttempt(); + } + + /** + * If the screen is on, pokes the wakelock to keep the screen alive and active for a specific + * amount of time. + */ + void handlePokeWakelock(int millis) { + PowerManager powerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); + if (powerManager.isScreenOn()) { + mKeyguardScreenCallback.userActivity(millis); + } + } + + /** + * Implements service connection methods. + */ + private ServiceConnection mConnection = new ServiceConnection() { + /** + * Called when the Face Unlock service connects after calling bind(). + */ + public void onServiceConnected(ComponentName className, IBinder iservice) { + Log.d(TAG, "Connected to Face Unlock service"); + mService = IFaceLockInterface.Stub.asInterface(iservice); + mHandler.sendEmptyMessage(MSG_SERVICE_CONNECTED); + } + + /** + * Called if the Face Unlock service unexpectedly disconnects. This indicates an error. + */ + public void onServiceDisconnected(ComponentName className) { + Log.e(TAG, "Unexpected disconnect from Face Unlock service"); + mHandler.sendEmptyMessage(MSG_SERVICE_DISCONNECTED); + } + }; + + /** + * Tells the Face Unlock service to start displaying its UI and start processing. + */ + private void startUi(IBinder windowToken, int x, int y, int w, int h) { + if (DEBUG) Log.d(TAG, "startUi()"); + synchronized (mServiceRunningLock) { + if (!mServiceRunning) { + Log.d(TAG, "Starting Face Unlock"); + try { + mService.startUi(windowToken, x, y, w, h, + mLockPatternUtils.isBiometricWeakLivelinessEnabled()); + } catch (RemoteException e) { + Log.e(TAG, "Caught exception starting Face Unlock: " + e.toString()); + return; + } + mServiceRunning = true; + } else { + Log.w(TAG, "startUi() attempted while running"); + } + } + } + + /** + * Tells the Face Unlock service to stop displaying its UI and stop processing. + */ + private void stopUi() { + if (DEBUG) Log.d(TAG, "stopUi()"); + // Note that attempting to stop Face Unlock when it's not running is not an issue. + // Face Unlock can return, which stops it and then we try to stop it when the + // screen is turned off. That's why we check. + synchronized (mServiceRunningLock) { + if (mServiceRunning) { + Log.d(TAG, "Stopping Face Unlock"); + try { + mService.stopUi(); + } catch (RemoteException e) { + Log.e(TAG, "Caught exception stopping Face Unlock: " + e.toString()); + } + mServiceRunning = false; + } else { + // This is usually not an error when this happens. Sometimes we will tell it to + // stop multiple times because it's called from both onWindowFocusChanged and + // onDetachedFromWindow. + if (DEBUG) Log.d(TAG, "stopUi() attempted while not running"); + } + } + } + + /** + * Implements the AIDL biometric unlock service callback interface. + */ + private final IFaceLockCallback mFaceUnlockCallback = new IFaceLockCallback.Stub() { + /** + * Called when Face Unlock wants to grant access to the user. + */ + public void unlock() { + if (DEBUG) Log.d(TAG, "unlock()"); + Message message = mHandler.obtainMessage(MSG_UNLOCK, UserHandle.getCallingUserId(), -1); + mHandler.sendMessage(message); + } + + /** + * Called when Face Unlock wants to go to the backup. + */ + public void cancel() { + if (DEBUG) Log.d(TAG, "cancel()"); + mHandler.sendEmptyMessage(MSG_CANCEL); + } + + /** + * Called when Face Unlock wants to increment the number of failed attempts. + */ + public void reportFailedAttempt() { + if (DEBUG) Log.d(TAG, "reportFailedAttempt()"); + mHandler.sendEmptyMessage(MSG_REPORT_FAILED_ATTEMPT); + } + + /** + * Called when Face Unlock wants to keep the screen alive and active for a specific amount + * of time. + */ + public void pokeWakelock(int millis) { + if (DEBUG) Log.d(TAG, "pokeWakelock() for " + millis + "ms"); + Message message = mHandler.obtainMessage(MSG_POKE_WAKELOCK, millis, -1); + mHandler.sendMessage(message); + } + + }; +} diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardAbsKeyInputView.java b/packages/Keyguard/src/com/android/keyguard/KeyguardAbsKeyInputView.java new file mode 100644 index 0000000..cc520dc --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardAbsKeyInputView.java @@ -0,0 +1,263 @@ +/* + * 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.internal.policy.impl.keyguard; + +import android.content.Context; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.CountDownTimer; +import android.os.SystemClock; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.view.HapticFeedbackConstants; +import android.view.KeyEvent; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.TextView.OnEditorActionListener; + +import com.android.internal.R; +import com.android.internal.widget.LockPatternUtils; + +/** + * Base class for PIN and password unlock screens. + */ +public abstract class KeyguardAbsKeyInputView extends LinearLayout + implements KeyguardSecurityView, OnEditorActionListener, TextWatcher { + protected KeyguardSecurityCallback mCallback; + protected TextView mPasswordEntry; + protected LockPatternUtils mLockPatternUtils; + protected SecurityMessageDisplay mSecurityMessageDisplay; + protected View mEcaView; + private Drawable mBouncerFrame; + protected boolean mEnableHaptics; + + // 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); + } + + public void setKeyguardCallback(KeyguardSecurityCallback callback) { + mCallback = callback; + } + + public void setLockPatternUtils(LockPatternUtils utils) { + mLockPatternUtils = utils; + mEnableHaptics = mLockPatternUtils.isTactileFeedbackEnabled(); + } + + @Override + public void onWindowFocusChanged(boolean hasWindowFocus) { + if (hasWindowFocus) { + reset(); + } + } + + public void reset() { + // start fresh + mPasswordEntry.setText(""); + mPasswordEntry.requestFocus(); + + // if the user is currently locked out, enforce it. + long deadline = mLockPatternUtils.getLockoutAttemptDeadline(); + if (deadline != 0) { + handleAttemptLockout(deadline); + } else { + resetState(); + } + } + + protected abstract int getPasswordTextViewId(); + protected abstract void resetState(); + + @Override + protected void onFinishInflate() { + mLockPatternUtils = new LockPatternUtils(mContext); + + mPasswordEntry = (TextView) findViewById(getPasswordTextViewId()); + mPasswordEntry.setOnEditorActionListener(this); + mPasswordEntry.addTextChangedListener(this); + + // Set selected property on so the view can send accessibility events. + mPasswordEntry.setSelected(true); + + // Poke the wakelock any time the text is selected or modified + mPasswordEntry.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + mCallback.userActivity(0); // TODO: customize timeout for text? + } + }); + + mPasswordEntry.addTextChangedListener(new TextWatcher() { + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + public void afterTextChanged(Editable s) { + if (mCallback != null) { + mCallback.userActivity(0); + } + } + }); + mSecurityMessageDisplay = new KeyguardMessageArea.Helper(this); + mEcaView = findViewById(R.id.keyguard_selector_fade_container); + View bouncerFrameView = findViewById(R.id.keyguard_bouncer_frame); + if (bouncerFrameView != null) { + mBouncerFrame = bouncerFrameView.getBackground(); + } + } + + @Override + protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { + // send focus to the password field + return mPasswordEntry.requestFocus(direction, previouslyFocusedRect); + } + + /* + * 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() { + String entry = mPasswordEntry.getText().toString(); + if (mLockPatternUtils.checkPassword(entry)) { + mCallback.reportSuccessfulUnlockAttempt(); + mCallback.dismiss(true); + } else 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. + mCallback.reportFailedUnlockAttempt(); + if (0 == (mCallback.getFailedAttempts() + % LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT)) { + long deadline = mLockPatternUtils.setLockoutAttemptDeadline(); + handleAttemptLockout(deadline); + } + mSecurityMessageDisplay.setMessage(getWrongPasswordStringId(), true); + } + mPasswordEntry.setText(""); + } + + // Prevent user from using the PIN/Password entry until scheduled deadline. + protected void handleAttemptLockout(long elapsedRealtimeDeadline) { + mPasswordEntry.setEnabled(false); + long elapsedRealtime = SystemClock.elapsedRealtime(); + new CountDownTimer(elapsedRealtimeDeadline - elapsedRealtime, 1000) { + + @Override + public void onTick(long millisUntilFinished) { + int secondsRemaining = (int) (millisUntilFinished / 1000); + mSecurityMessageDisplay.setMessage( + R.string.kg_too_many_failed_attempts_countdown, true, secondsRemaining); + } + + @Override + public void onFinish() { + mSecurityMessageDisplay.setMessage("", false); + resetState(); + } + }.start(); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + mCallback.userActivity(0); + return false; + } + + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + // Check if this was the result of hitting the enter key + if (actionId == EditorInfo.IME_NULL || actionId == EditorInfo.IME_ACTION_DONE + || actionId == EditorInfo.IME_ACTION_NEXT) { + verifyPasswordAndUnlock(); + return true; + } + return false; + } + + @Override + public boolean needsInput() { + return false; + } + + @Override + public void onPause() { + + } + + @Override + public void onResume(int reason) { + reset(); + } + + @Override + public KeyguardSecurityCallback getCallback() { + return mCallback; + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + if (mCallback != null) { + mCallback.userActivity(KeyguardViewManager.DIGIT_PRESS_WAKE_MILLIS); + } + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(Editable s) { + } + + // 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 void showBouncer(int duration) { + KeyguardSecurityViewHelper. + showBouncer(mSecurityMessageDisplay, mEcaView, mBouncerFrame, duration); + } + + @Override + public void hideBouncer(int duration) { + KeyguardSecurityViewHelper. + hideBouncer(mSecurityMessageDisplay, mEcaView, mBouncerFrame, duration); + } +} + diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardAccountView.java b/packages/Keyguard/src/com/android/keyguard/KeyguardAccountView.java new file mode 100644 index 0000000..e0e7128 --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardAccountView.java @@ -0,0 +1,333 @@ +/* + * 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.internal.policy.impl.keyguard; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AccountManagerCallback; +import android.accounts.AccountManagerFuture; +import android.accounts.AuthenticatorException; +import android.accounts.OperationCanceledException; +import android.app.Dialog; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.Intent; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.UserHandle; +import android.text.Editable; +import android.text.InputFilter; +import android.text.LoginFilter; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.View; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; + +import com.android.internal.widget.LockPatternUtils; +import com.android.internal.R; + +import java.io.IOException; + +/** + * When the user forgets their password a bunch of times, we fall back on their + * account's login/password to unlock the phone (and reset their lock pattern). + */ +public class KeyguardAccountView extends LinearLayout implements KeyguardSecurityView, + View.OnClickListener, TextWatcher { + private static final int AWAKE_POKE_MILLIS = 30000; + private static final String LOCK_PATTERN_PACKAGE = "com.android.settings"; + private static final String LOCK_PATTERN_CLASS = LOCK_PATTERN_PACKAGE + ".ChooseLockGeneric"; + + private KeyguardSecurityCallback mCallback; + private LockPatternUtils mLockPatternUtils; + private EditText mLogin; + private EditText mPassword; + private Button mOk; + public boolean mEnableFallback; + private SecurityMessageDisplay mSecurityMessageDisplay; + + /** + * Shown while making asynchronous check of password. + */ + private ProgressDialog mCheckingDialog; + + public KeyguardAccountView(Context context) { + this(context, null, 0); + } + + public KeyguardAccountView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public KeyguardAccountView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + mLockPatternUtils = new LockPatternUtils(getContext()); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + mLogin = (EditText) findViewById(R.id.login); + mLogin.setFilters(new InputFilter[] { new LoginFilter.UsernameFilterGeneric() } ); + mLogin.addTextChangedListener(this); + + mPassword = (EditText) findViewById(R.id.password); + mPassword.addTextChangedListener(this); + + mOk = (Button) findViewById(R.id.ok); + mOk.setOnClickListener(this); + + mSecurityMessageDisplay = new KeyguardMessageArea.Helper(this); + reset(); + } + + public void setKeyguardCallback(KeyguardSecurityCallback callback) { + mCallback = callback; + } + + public void setLockPatternUtils(LockPatternUtils utils) { + mLockPatternUtils = utils; + } + + public KeyguardSecurityCallback getCallback() { + return mCallback; + } + + + public void afterTextChanged(Editable s) { + } + + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + public void onTextChanged(CharSequence s, int start, int before, int count) { + if (mCallback != null) { + mCallback.userActivity(AWAKE_POKE_MILLIS); + } + } + + @Override + protected boolean onRequestFocusInDescendants(int direction, + Rect previouslyFocusedRect) { + // send focus to the login field + return mLogin.requestFocus(direction, previouslyFocusedRect); + } + + public boolean needsInput() { + return true; + } + + public void reset() { + // start fresh + mLogin.setText(""); + mPassword.setText(""); + mLogin.requestFocus(); + boolean permLocked = mLockPatternUtils.isPermanentlyLocked(); + mSecurityMessageDisplay.setMessage(permLocked ? R.string.kg_login_too_many_attempts : + R.string.kg_login_instructions, permLocked ? true : false); + } + + /** {@inheritDoc} */ + public void cleanUp() { + if (mCheckingDialog != null) { + mCheckingDialog.hide(); + } + mCallback = null; + mLockPatternUtils = null; + } + + public void onClick(View v) { + mCallback.userActivity(0); + if (v == mOk) { + asyncCheckPassword(); + } + } + + private void postOnCheckPasswordResult(final boolean success) { + // ensure this runs on UI thread + mLogin.post(new Runnable() { + public void run() { + if (success) { + // clear out forgotten password + mLockPatternUtils.setPermanentlyLocked(false); + mLockPatternUtils.setLockPatternEnabled(false); + mLockPatternUtils.saveLockPattern(null); + + // launch the 'choose lock pattern' activity so + // the user can pick a new one if they want to + Intent intent = new Intent(); + intent.setClassName(LOCK_PATTERN_PACKAGE, LOCK_PATTERN_CLASS); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mContext.startActivityAsUser(intent, + new UserHandle(mLockPatternUtils.getCurrentUser())); + mCallback.reportSuccessfulUnlockAttempt(); + + // dismiss keyguard + mCallback.dismiss(true); + } else { + mSecurityMessageDisplay.setMessage(R.string.kg_login_invalid_input, true); + mPassword.setText(""); + mCallback.reportFailedUnlockAttempt(); + } + } + }); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (event.getAction() == KeyEvent.ACTION_DOWN + && event.getKeyCode() == KeyEvent.KEYCODE_BACK) { + if (mLockPatternUtils.isPermanentlyLocked()) { + mCallback.dismiss(false); + } else { + // TODO: mCallback.forgotPattern(false); + } + return true; + } + return super.dispatchKeyEvent(event); + } + + /** + * Given the string the user entered in the 'username' field, find + * the stored account that they probably intended. Prefer, in order: + * + * - an exact match for what was typed, or + * - a case-insensitive match for what was typed, or + * - if they didn't include a domain, an exact match of the username, or + * - if they didn't include a domain, a case-insensitive + * match of the username. + * + * If there is a tie for the best match, choose neither -- + * the user needs to be more specific. + * + * @return an account name from the database, or null if we can't + * find a single best match. + */ + private Account findIntendedAccount(String username) { + Account[] accounts = AccountManager.get(mContext).getAccountsByTypeAsUser("com.google", + new UserHandle(mLockPatternUtils.getCurrentUser())); + + // Try to figure out which account they meant if they + // typed only the username (and not the domain), or got + // the case wrong. + + Account bestAccount = null; + int bestScore = 0; + for (Account a: accounts) { + int score = 0; + if (username.equals(a.name)) { + score = 4; + } else if (username.equalsIgnoreCase(a.name)) { + score = 3; + } else if (username.indexOf('@') < 0) { + int i = a.name.indexOf('@'); + if (i >= 0) { + String aUsername = a.name.substring(0, i); + if (username.equals(aUsername)) { + score = 2; + } else if (username.equalsIgnoreCase(aUsername)) { + score = 1; + } + } + } + if (score > bestScore) { + bestAccount = a; + bestScore = score; + } else if (score == bestScore) { + bestAccount = null; + } + } + return bestAccount; + } + + private void asyncCheckPassword() { + mCallback.userActivity(AWAKE_POKE_MILLIS); + final String login = mLogin.getText().toString(); + final String password = mPassword.getText().toString(); + Account account = findIntendedAccount(login); + if (account == null) { + postOnCheckPasswordResult(false); + return; + } + getProgressDialog().show(); + Bundle options = new Bundle(); + options.putString(AccountManager.KEY_PASSWORD, password); + AccountManager.get(mContext).confirmCredentialsAsUser(account, options, null /* activity */, + new AccountManagerCallback<Bundle>() { + public void run(AccountManagerFuture<Bundle> future) { + try { + mCallback.userActivity(AWAKE_POKE_MILLIS); + final Bundle result = future.getResult(); + final boolean verified = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT); + postOnCheckPasswordResult(verified); + } catch (OperationCanceledException e) { + postOnCheckPasswordResult(false); + } catch (IOException e) { + postOnCheckPasswordResult(false); + } catch (AuthenticatorException e) { + postOnCheckPasswordResult(false); + } finally { + mLogin.post(new Runnable() { + public void run() { + getProgressDialog().hide(); + } + }); + } + } + }, null /* handler */, new UserHandle(mLockPatternUtils.getCurrentUser())); + } + + private Dialog getProgressDialog() { + if (mCheckingDialog == null) { + mCheckingDialog = new ProgressDialog(mContext); + mCheckingDialog.setMessage( + mContext.getString(R.string.kg_login_checking_password)); + mCheckingDialog.setIndeterminate(true); + mCheckingDialog.setCancelable(false); + mCheckingDialog.getWindow().setType( + WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); + } + return mCheckingDialog; + } + + @Override + public void onPause() { + + } + + @Override + public void onResume(int reason) { + reset(); + } + + @Override + public void showUsabilityHint() { + } + + @Override + public void showBouncer(int duration) { + } + + @Override + public void hideBouncer(int duration) { + } +} + diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardActivityLauncher.java b/packages/Keyguard/src/com/android/keyguard/KeyguardActivityLauncher.java new file mode 100644 index 0000000..6539db3 --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardActivityLauncher.java @@ -0,0 +1,273 @@ +/* + * 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.internal.policy.impl.keyguard; + +import android.app.ActivityManagerNative; +import android.app.ActivityOptions; +import android.app.IActivityManager.WaitResult; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProviderInfo; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.Bundle; +import android.os.Handler; +import android.os.RemoteException; +import android.os.SystemClock; +import android.os.UserHandle; +import android.provider.MediaStore; +import android.util.Log; +import android.view.WindowManager; + +import com.android.internal.policy.impl.keyguard.KeyguardHostView.OnDismissAction; +import com.android.internal.widget.LockPatternUtils; + +import java.util.List; + +public abstract class KeyguardActivityLauncher { + private static final String TAG = KeyguardActivityLauncher.class.getSimpleName(); + private static final boolean DEBUG = KeyguardHostView.DEBUG; + private static final String META_DATA_KEYGUARD_LAYOUT = "com.android.keyguard.layout"; + private static final Intent SECURE_CAMERA_INTENT = + new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE) + .addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + private static final Intent INSECURE_CAMERA_INTENT = + new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA); + + abstract Context getContext(); + + abstract KeyguardSecurityCallback getCallback(); + + abstract LockPatternUtils getLockPatternUtils(); + + public static class CameraWidgetInfo { + public String contextPackage; + public int layoutId; + } + + public CameraWidgetInfo getCameraWidgetInfo() { + CameraWidgetInfo info = new CameraWidgetInfo(); + Intent intent = getCameraIntent(); + PackageManager packageManager = getContext().getPackageManager(); + final List<ResolveInfo> appList = packageManager.queryIntentActivitiesAsUser( + intent, PackageManager.MATCH_DEFAULT_ONLY, getLockPatternUtils().getCurrentUser()); + if (appList.size() == 0) { + if (DEBUG) Log.d(TAG, "getCameraWidgetInfo(): Nothing found"); + return null; + } + ResolveInfo resolved = packageManager.resolveActivityAsUser(intent, + PackageManager.MATCH_DEFAULT_ONLY | PackageManager.GET_META_DATA, + getLockPatternUtils().getCurrentUser()); + if (DEBUG) Log.d(TAG, "getCameraWidgetInfo(): resolved: " + resolved); + if (wouldLaunchResolverActivity(resolved, appList)) { + if (DEBUG) Log.d(TAG, "getCameraWidgetInfo(): Would launch resolver"); + return info; + } + if (resolved == null || resolved.activityInfo == null) { + return null; + } + if (resolved.activityInfo.metaData == null || resolved.activityInfo.metaData.isEmpty()) { + if (DEBUG) Log.d(TAG, "getCameraWidgetInfo(): no metadata found"); + return info; + } + int layoutId = resolved.activityInfo.metaData.getInt(META_DATA_KEYGUARD_LAYOUT); + if (layoutId == 0) { + if (DEBUG) Log.d(TAG, "getCameraWidgetInfo(): no layout specified"); + return info; + } + info.contextPackage = resolved.activityInfo.packageName; + info.layoutId = layoutId; + return info; + } + + public void launchCamera(Handler worker, Runnable onSecureCameraStarted) { + LockPatternUtils lockPatternUtils = getLockPatternUtils(); + if (lockPatternUtils.isSecure()) { + // Launch the secure version of the camera + if (wouldLaunchResolverActivity(SECURE_CAMERA_INTENT)) { + // TODO: Show disambiguation dialog instead. + // For now, we'll treat this like launching any other app from secure keyguard. + // When they do, user sees the system's ResolverActivity which lets them choose + // which secure camera to use. + launchActivity(SECURE_CAMERA_INTENT, false, false, null, null); + } else { + launchActivity(SECURE_CAMERA_INTENT, true, false, worker, onSecureCameraStarted); + } + } else { + // Launch the normal camera + launchActivity(INSECURE_CAMERA_INTENT, false, false, null, null); + } + } + + public void launchWidgetPicker(int appWidgetId) { + Intent pickIntent = new Intent(AppWidgetManager.ACTION_KEYGUARD_APPWIDGET_PICK); + + pickIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); + pickIntent.putExtra(AppWidgetManager.EXTRA_CUSTOM_SORT, false); + pickIntent.putExtra(AppWidgetManager.EXTRA_CATEGORY_FILTER, + AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD); + + Bundle options = new Bundle(); + options.putInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY, + AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD); + pickIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS, options); + pickIntent.addFlags( + Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_SINGLE_TOP + | Intent.FLAG_ACTIVITY_CLEAR_TOP + | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + + launchActivity(pickIntent, false, false, null, null); + } + + /** + * Launches the said intent for the current foreground user. + * + * @param intent + * @param showsWhileLocked true if the activity can be run on top of keyguard. + * See {@link WindowManager#FLAG_SHOW_WHEN_LOCKED} + * @param useDefaultAnimations true if default transitions should be used, else suppressed. + * @param worker if supplied along with onStarted, used to launch the blocking activity call. + * @param onStarted if supplied along with worker, called after activity is started. + */ + public void launchActivity(final Intent intent, + boolean showsWhileLocked, + boolean useDefaultAnimations, + final Handler worker, + final Runnable onStarted) { + + final Context context = getContext(); + final Bundle animation = useDefaultAnimations ? null + : ActivityOptions.makeCustomAnimation(context, 0, 0).toBundle(); + launchActivityWithAnimation(intent, showsWhileLocked, animation, worker, onStarted); + } + + public void launchActivityWithAnimation(final Intent intent, + boolean showsWhileLocked, + final Bundle animation, + final Handler worker, + final Runnable onStarted) { + + LockPatternUtils lockPatternUtils = getLockPatternUtils(); + intent.addFlags( + Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_SINGLE_TOP + | Intent.FLAG_ACTIVITY_CLEAR_TOP); + boolean isSecure = lockPatternUtils.isSecure(); + if (!isSecure || showsWhileLocked) { + if (!isSecure) { + dismissKeyguardOnNextActivity(); + } + try { + if (DEBUG) Log.d(TAG, String.format("Starting activity for intent %s at %s", + intent, SystemClock.uptimeMillis())); + startActivityForCurrentUser(intent, animation, worker, onStarted); + } catch (ActivityNotFoundException e) { + Log.w(TAG, "Activity not found for intent + " + intent.getAction()); + } + } else { + // Create a runnable to start the activity and ask the user to enter their + // credentials. + KeyguardSecurityCallback callback = getCallback(); + callback.setOnDismissAction(new OnDismissAction() { + @Override + public boolean onDismiss() { + dismissKeyguardOnNextActivity(); + startActivityForCurrentUser(intent, animation, worker, onStarted); + return true; + } + }); + callback.dismiss(false); + } + } + + private void dismissKeyguardOnNextActivity() { + try { + ActivityManagerNative.getDefault().dismissKeyguardOnNextActivity(); + } catch (RemoteException e) { + Log.w(TAG, "can't dismiss keyguard on launch"); + } + } + + private void startActivityForCurrentUser(final Intent intent, final Bundle options, + Handler worker, final Runnable onStarted) { + final UserHandle user = new UserHandle(UserHandle.USER_CURRENT); + if (worker == null || onStarted == null) { + getContext().startActivityAsUser(intent, options, user); + return; + } + // if worker + onStarted are supplied, run blocking activity launch call in the background + worker.post(new Runnable(){ + @Override + public void run() { + try { + WaitResult result = ActivityManagerNative.getDefault().startActivityAndWait( + null /*caller*/, + null /*caller pkg*/, + intent, + intent.resolveTypeIfNeeded(getContext().getContentResolver()), + null /*resultTo*/, + null /*resultWho*/, + 0 /*requestCode*/, + Intent.FLAG_ACTIVITY_NEW_TASK, + null /*profileFile*/, + null /*profileFd*/, + options, + user.getIdentifier()); + if (DEBUG) Log.d(TAG, String.format("waitResult[%s,%s,%s,%s] at %s", + result.result, result.thisTime, result.totalTime, result.who, + SystemClock.uptimeMillis())); + } catch (RemoteException e) { + Log.w(TAG, "Error starting activity", e); + return; + } + try { + onStarted.run(); + } catch (Throwable t) { + Log.w(TAG, "Error running onStarted callback", t); + } + }}); + } + + private Intent getCameraIntent() { + return getLockPatternUtils().isSecure() ? SECURE_CAMERA_INTENT : INSECURE_CAMERA_INTENT; + } + + private boolean wouldLaunchResolverActivity(Intent intent) { + PackageManager packageManager = getContext().getPackageManager(); + ResolveInfo resolved = packageManager.resolveActivityAsUser(intent, + PackageManager.MATCH_DEFAULT_ONLY, getLockPatternUtils().getCurrentUser()); + List<ResolveInfo> appList = packageManager.queryIntentActivitiesAsUser( + intent, PackageManager.MATCH_DEFAULT_ONLY, getLockPatternUtils().getCurrentUser()); + return wouldLaunchResolverActivity(resolved, appList); + } + + private boolean wouldLaunchResolverActivity(ResolveInfo resolved, List<ResolveInfo> appList) { + // If the list contains the above resolved activity, then it can't be + // ResolverActivity itself. + for (int i = 0; i < appList.size(); i++) { + ResolveInfo tmp = appList.get(i); + if (tmp.activityInfo.name.equals(resolved.activityInfo.name) + && tmp.activityInfo.packageName.equals(resolved.activityInfo.packageName)) { + return false; + } + } + return true; + } +} diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardCircleFramedDrawable.java b/packages/Keyguard/src/com/android/keyguard/KeyguardCircleFramedDrawable.java new file mode 100644 index 0000000..79b66f4 --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardCircleFramedDrawable.java @@ -0,0 +1,160 @@ +/* + * 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.internal.policy.impl.keyguard; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PixelFormat; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; + +import android.util.Log; + +class KeyguardCircleFramedDrawable extends Drawable { + + private final Bitmap mBitmap; + private final int mSize; + private final Paint mPaint; + private final float mShadowRadius; + private final float mStrokeWidth; + private final int mFrameColor; + private final int mHighlightColor; + private final int mFrameShadowColor; + + private float mScale; + private Path mFramePath; + private Rect mSrcRect; + private RectF mDstRect; + private RectF mFrameRect; + private boolean mPressed; + + public KeyguardCircleFramedDrawable(Bitmap bitmap, int size, + int frameColor, float strokeWidth, + int frameShadowColor, float shadowRadius, + int highlightColor) { + super(); + mSize = size; + mShadowRadius = shadowRadius; + mFrameColor = frameColor; + mFrameShadowColor = frameShadowColor; + mStrokeWidth = strokeWidth; + mHighlightColor = highlightColor; + + mBitmap = Bitmap.createBitmap(mSize, mSize, Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(mBitmap); + + final int width = bitmap.getWidth(); + final int height = bitmap.getHeight(); + final int square = Math.min(width, height); + + final Rect cropRect = new Rect((width - square) / 2, (height - square) / 2, square, square); + final RectF circleRect = new RectF(0f, 0f, mSize, mSize); + circleRect.inset(mStrokeWidth / 2f, mStrokeWidth / 2f); + circleRect.inset(mShadowRadius, mShadowRadius); + + final Path fillPath = new Path(); + fillPath.addArc(circleRect, 0f, 360f); + + canvas.drawColor(0, PorterDuff.Mode.CLEAR); + + // opaque circle matte + mPaint = new Paint(); + mPaint.setAntiAlias(true); + mPaint.setColor(Color.BLACK); + mPaint.setStyle(Paint.Style.FILL); + canvas.drawPath(fillPath, mPaint); + + // mask in the icon where the bitmap is opaque + mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP)); + canvas.drawBitmap(bitmap, cropRect, circleRect, mPaint); + + // prepare paint for frame drawing + mPaint.setXfermode(null); + + mScale = 1f; + + mSrcRect = new Rect(0, 0, mSize, mSize); + mDstRect = new RectF(0, 0, mSize, mSize); + mFrameRect = new RectF(mDstRect); + mFramePath = new Path(); + } + + @Override + public void draw(Canvas canvas) { + // clear background + final float outside = Math.min(canvas.getWidth(), canvas.getHeight()); + final float inside = mScale * outside; + final float pad = (outside - inside) / 2f; + + mDstRect.set(pad, pad, outside - pad, outside - pad); + canvas.drawBitmap(mBitmap, mSrcRect, mDstRect, null); + + mFrameRect.set(mDstRect); + mFrameRect.inset(mStrokeWidth / 2f, mStrokeWidth / 2f); + mFrameRect.inset(mShadowRadius, mShadowRadius); + + mFramePath.reset(); + mFramePath.addArc(mFrameRect, 0f, 360f); + + // white frame + if (mPressed) { + mPaint.setStyle(Paint.Style.FILL); + mPaint.setColor(Color.argb((int) (0.33f * 255), + Color.red(mHighlightColor), + Color.green(mHighlightColor), + Color.blue(mHighlightColor))); + canvas.drawPath(mFramePath, mPaint); + } + mPaint.setStrokeWidth(mStrokeWidth); + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setColor(mPressed ? mHighlightColor : mFrameColor); + mPaint.setShadowLayer(mShadowRadius, 0f, 0f, mFrameShadowColor); + canvas.drawPath(mFramePath, mPaint); + } + + public void setScale(float scale) { + mScale = scale; + } + + public float getScale() { + return mScale; + } + + public void setPressed(boolean pressed) { + mPressed = pressed; + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + @Override + public void setAlpha(int alpha) { + } + + @Override + public void setColorFilter(ColorFilter cf) { + } +} diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardFaceUnlockView.java b/packages/Keyguard/src/com/android/keyguard/KeyguardFaceUnlockView.java new file mode 100644 index 0000000..4df434c --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardFaceUnlockView.java @@ -0,0 +1,245 @@ +/* + * 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.internal.policy.impl.keyguard; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.PowerManager; +import android.telephony.TelephonyManager; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.ImageButton; +import android.widget.LinearLayout; + +import com.android.internal.R; + +import com.android.internal.widget.LockPatternUtils; + +public class KeyguardFaceUnlockView extends LinearLayout implements KeyguardSecurityView { + + private static final String TAG = "FULKeyguardFaceUnlockView"; + private static final boolean DEBUG = false; + private KeyguardSecurityCallback mKeyguardSecurityCallback; + private LockPatternUtils mLockPatternUtils; + private BiometricSensorUnlock mBiometricUnlock; + private View mFaceUnlockAreaView; + private ImageButton mCancelButton; + private SecurityMessageDisplay mSecurityMessageDisplay; + private View mEcaView; + private Drawable mBouncerFrame; + + private boolean mIsShowing = false; + private final Object mIsShowingLock = new Object(); + + public KeyguardFaceUnlockView(Context context) { + this(context, null); + } + + public KeyguardFaceUnlockView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + initializeBiometricUnlockView(); + + mSecurityMessageDisplay = new KeyguardMessageArea.Helper(this); + mEcaView = findViewById(R.id.keyguard_selector_fade_container); + View bouncerFrameView = findViewById(R.id.keyguard_bouncer_frame); + if (bouncerFrameView != null) { + mBouncerFrame = bouncerFrameView.getBackground(); + } + } + + @Override + public void setKeyguardCallback(KeyguardSecurityCallback callback) { + mKeyguardSecurityCallback = callback; + // TODO: formalize this in the interface or factor it out + ((FaceUnlock)mBiometricUnlock).setKeyguardCallback(callback); + } + + @Override + public void setLockPatternUtils(LockPatternUtils utils) { + mLockPatternUtils = utils; + } + + @Override + public void reset() { + + } + + @Override + public void onDetachedFromWindow() { + if (DEBUG) Log.d(TAG, "onDetachedFromWindow()"); + if (mBiometricUnlock != null) { + mBiometricUnlock.stop(); + } + KeyguardUpdateMonitor.getInstance(mContext).removeCallback(mUpdateCallback); + } + + @Override + public void onPause() { + if (DEBUG) Log.d(TAG, "onPause()"); + if (mBiometricUnlock != null) { + mBiometricUnlock.stop(); + } + KeyguardUpdateMonitor.getInstance(mContext).removeCallback(mUpdateCallback); + } + + @Override + public void onResume(int reason) { + if (DEBUG) Log.d(TAG, "onResume()"); + mIsShowing = KeyguardUpdateMonitor.getInstance(mContext).isKeyguardVisible(); + maybeStartBiometricUnlock(); + KeyguardUpdateMonitor.getInstance(mContext).registerCallback(mUpdateCallback); + } + + @Override + public boolean needsInput() { + return false; + } + + @Override + public KeyguardSecurityCallback getCallback() { + return mKeyguardSecurityCallback; + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + mBiometricUnlock.initializeView(mFaceUnlockAreaView); + } + + private void initializeBiometricUnlockView() { + if (DEBUG) Log.d(TAG, "initializeBiometricUnlockView()"); + mFaceUnlockAreaView = findViewById(R.id.face_unlock_area_view); + if (mFaceUnlockAreaView != null) { + mBiometricUnlock = new FaceUnlock(mContext); + + mCancelButton = (ImageButton) findViewById(R.id.face_unlock_cancel_button); + mCancelButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mBiometricUnlock.stopAndShowBackup(); + } + }); + } else { + Log.w(TAG, "Couldn't find biometric unlock view"); + } + } + + /** + * Starts the biometric unlock if it should be started based on a number of factors. If it + * should not be started, it either goes to the back up, or remains showing to prepare for + * it being started later. + */ + private void maybeStartBiometricUnlock() { + if (DEBUG) Log.d(TAG, "maybeStartBiometricUnlock()"); + if (mBiometricUnlock != null) { + KeyguardUpdateMonitor monitor = KeyguardUpdateMonitor.getInstance(mContext); + final boolean backupIsTimedOut = ( + monitor.getFailedUnlockAttempts() >= + LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT); + PowerManager powerManager = (PowerManager) mContext.getSystemService( + Context.POWER_SERVICE); + + boolean isShowing; + synchronized(mIsShowingLock) { + isShowing = mIsShowing; + } + + // Don't start it if the screen is off or if it's not showing, but keep this view up + // because we want it here and ready for when the screen turns on or when it does start + // showing. + if (!powerManager.isScreenOn() || !isShowing) { + mBiometricUnlock.stop(); // It shouldn't be running but calling this can't hurt. + return; + } + + // TODO: Some of these conditions are handled in KeyguardSecurityModel and may not be + // necessary here. + if (monitor.getPhoneState() == TelephonyManager.CALL_STATE_IDLE + && !monitor.getMaxBiometricUnlockAttemptsReached() + && !backupIsTimedOut) { + mBiometricUnlock.start(); + } else { + mBiometricUnlock.stopAndShowBackup(); + } + } + } + + KeyguardUpdateMonitorCallback mUpdateCallback = new KeyguardUpdateMonitorCallback() { + // We need to stop the biometric unlock when a phone call comes in + @Override + public void onPhoneStateChanged(int phoneState) { + if (DEBUG) Log.d(TAG, "onPhoneStateChanged(" + phoneState + ")"); + if (phoneState == TelephonyManager.CALL_STATE_RINGING) { + if (mBiometricUnlock != null) { + mBiometricUnlock.stopAndShowBackup(); + } + } + } + + @Override + public void onUserSwitching(int userId) { + if (DEBUG) Log.d(TAG, "onUserSwitched(" + userId + ")"); + if (mBiometricUnlock != null) { + mBiometricUnlock.stop(); + } + // No longer required; static value set by KeyguardViewMediator + // mLockPatternUtils.setCurrentUser(userId); + } + + @Override + public void onKeyguardVisibilityChanged(boolean showing) { + if (DEBUG) Log.d(TAG, "onKeyguardVisibilityChanged(" + showing + ")"); + boolean wasShowing = false; + synchronized(mIsShowingLock) { + wasShowing = mIsShowing; + mIsShowing = showing; + } + PowerManager powerManager = (PowerManager) mContext.getSystemService( + Context.POWER_SERVICE); + if (mBiometricUnlock != null) { + if (!showing && wasShowing) { + mBiometricUnlock.stop(); + } else if (showing && powerManager.isScreenOn() && !wasShowing) { + maybeStartBiometricUnlock(); + } + } + } + }; + + @Override + public void showUsabilityHint() { + } + + @Override + public void showBouncer(int duration) { + KeyguardSecurityViewHelper. + showBouncer(mSecurityMessageDisplay, mEcaView, mBouncerFrame, duration); + } + + @Override + public void hideBouncer(int duration) { + KeyguardSecurityViewHelper. + hideBouncer(mSecurityMessageDisplay, mEcaView, mBouncerFrame, duration); + } + +} diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardGlowStripView.java b/packages/Keyguard/src/com/android/keyguard/KeyguardGlowStripView.java new file mode 100644 index 0000000..e1c95f0 --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardGlowStripView.java @@ -0,0 +1,139 @@ +/* + * 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.internal.policy.impl.keyguard; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; +import android.view.animation.LinearInterpolator; +import android.widget.LinearLayout; + +import com.android.internal.R; + +/** + * A layout which animates a strip of horizontal, pulsing dots on request. This is used + * to indicate the presence of pages to the left / right. + */ +public class KeyguardGlowStripView extends LinearLayout { + private static final int DURATION = 500; + + private static final float SLIDING_WINDOW_SIZE = 0.4f; + private int mDotStripTop; + private int mHorizontalDotGap; + + private int mDotSize; + private int mNumDots; + private Drawable mDotDrawable; + private boolean mLeftToRight = true; + + private float mAnimationProgress = 0f; + private boolean mDrawDots = false; + private ValueAnimator mAnimator; + private Interpolator mDotAlphaInterpolator = new DecelerateInterpolator(0.5f); + + public KeyguardGlowStripView(Context context) { + this(context, null, 0); + } + + public KeyguardGlowStripView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public KeyguardGlowStripView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.KeyguardGlowStripView); + mDotSize = a.getDimensionPixelSize(R.styleable.KeyguardGlowStripView_dotSize, mDotSize); + mNumDots = a.getInt(R.styleable.KeyguardGlowStripView_numDots, mNumDots); + mDotDrawable = a.getDrawable(R.styleable.KeyguardGlowStripView_glowDot); + mLeftToRight = a.getBoolean(R.styleable.KeyguardGlowStripView_leftToRight, mLeftToRight); + } + + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + int availableWidth = w - getPaddingLeft() - getPaddingRight(); + mHorizontalDotGap = (availableWidth - mDotSize * mNumDots) / (mNumDots - 1); + mDotStripTop = getPaddingTop(); + invalidate(); + } + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + + if (!mDrawDots) return; + + int xOffset = getPaddingLeft(); + mDotDrawable.setBounds(0, 0, mDotSize, mDotSize); + + for (int i = 0; i < mNumDots; i++) { + // We fudge the relative position to provide a fade in of the first dot and a fade + // out of the final dot. + float relativeDotPosition = SLIDING_WINDOW_SIZE / 2 + ((1.0f * i) / (mNumDots - 1)) * + (1 - SLIDING_WINDOW_SIZE); + float distance = Math.abs(relativeDotPosition - mAnimationProgress); + float alpha = Math.max(0, 1 - distance / (SLIDING_WINDOW_SIZE / 2)); + + alpha = mDotAlphaInterpolator.getInterpolation(alpha); + + canvas.save(); + canvas.translate(xOffset, mDotStripTop); + mDotDrawable.setAlpha((int) (alpha * 255)); + mDotDrawable.draw(canvas); + canvas.restore(); + xOffset += mDotSize + mHorizontalDotGap; + } + } + + public void makeEmGo() { + if (mAnimator != null) { + mAnimator.cancel(); + } + float from = mLeftToRight ? 0f : 1f; + float to = mLeftToRight ? 1f : 0f; + mAnimator = ValueAnimator.ofFloat(from, to); + mAnimator.setDuration(DURATION); + mAnimator.setInterpolator(new LinearInterpolator()); + mAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mDrawDots = false; + // make sure we draw one frame at the end with everything gone. + invalidate(); + } + + @Override + public void onAnimationStart(Animator animation) { + mDrawDots = true; + } + }); + mAnimator.addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + mAnimationProgress = (Float) animation.getAnimatedValue(); + invalidate(); + } + }); + mAnimator.start(); + } +} diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardHostView.java b/packages/Keyguard/src/com/android/keyguard/KeyguardHostView.java new file mode 100644 index 0000000..06f06b5 --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardHostView.java @@ -0,0 +1,1611 @@ +/* + * 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.internal.policy.impl.keyguard; + +import android.app.Activity; +import android.app.ActivityManager; +import android.app.ActivityOptions; +import android.app.AlertDialog; +import android.app.SearchManager; +import android.app.admin.DevicePolicyManager; +import android.appwidget.AppWidgetHost; +import android.appwidget.AppWidgetHostView; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProviderInfo; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentSender; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.UserInfo; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.os.Looper; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; +import android.os.UserHandle; +import android.os.UserManager; +import android.provider.Settings; +import android.util.AttributeSet; +import android.util.Log; +import android.util.Slog; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.WindowManager; +import android.view.animation.AnimationUtils; +import android.widget.RemoteViews.OnClickHandler; + +import com.android.internal.R; +import com.android.internal.policy.impl.keyguard.KeyguardSecurityModel.SecurityMode; +import com.android.internal.widget.LockPatternUtils; + +import java.io.File; +import java.util.List; + +public class KeyguardHostView extends KeyguardViewBase { + private static final String TAG = "KeyguardHostView"; + + // Use this to debug all of keyguard + public static boolean DEBUG = KeyguardViewMediator.DEBUG; + + // Found in KeyguardAppWidgetPickActivity.java + static final int APPWIDGET_HOST_ID = 0x4B455947; + + private final int MAX_WIDGETS = 5; + + private AppWidgetHost mAppWidgetHost; + private AppWidgetManager mAppWidgetManager; + private KeyguardWidgetPager mAppWidgetContainer; + private KeyguardSecurityViewFlipper mSecurityViewContainer; + private KeyguardSelectorView mKeyguardSelectorView; + private KeyguardTransportControlView mTransportControl; + private boolean mIsVerifyUnlockOnly; + private boolean mEnableFallback; // TODO: This should get the value from KeyguardPatternView + private SecurityMode mCurrentSecuritySelection = SecurityMode.Invalid; + private int mAppWidgetToShow; + + private boolean mCheckAppWidgetConsistencyOnBootCompleted = false; + private boolean mCleanupAppWidgetsOnBootCompleted = false; + + protected OnDismissAction mDismissAction; + + protected int mFailedAttempts; + private LockPatternUtils mLockPatternUtils; + + private KeyguardSecurityModel mSecurityModel; + private KeyguardViewStateManager mViewStateManager; + + private Rect mTempRect = new Rect(); + + private int mDisabledFeatures; + + private boolean mCameraDisabled; + + private boolean mSafeModeEnabled; + + private boolean mUserSetupCompleted; + + // User for whom this host view was created. Final because we should never change the + // id without reconstructing an instance of KeyguardHostView. See note below... + private final int mUserId; + + private KeyguardMultiUserSelectorView mKeyguardMultiUserSelectorView; + + /*package*/ interface TransportCallback { + void onListenerDetached(); + void onListenerAttached(); + void onPlayStateChanged(); + } + + /*package*/ interface UserSwitcherCallback { + void hideSecurityView(int duration); + void showSecurityView(); + void showUnlockHint(); + void userActivity(); + } + + /*package*/ interface OnDismissAction { + /* returns true if the dismiss should be deferred */ + boolean onDismiss(); + } + + public KeyguardHostView(Context context) { + this(context, null); + } + + public KeyguardHostView(Context context, AttributeSet attrs) { + super(context, attrs); + mLockPatternUtils = new LockPatternUtils(context); + + // Note: This depends on KeyguardHostView getting reconstructed every time the + // user switches, since mUserId will be used for the entire session. + // Once created, keyguard should *never* re-use this instance with another user. + // In other words, mUserId should never change - hence it's marked final. + mUserId = mLockPatternUtils.getCurrentUser(); + + Context userContext = null; + try { + final String packageName = "system"; + userContext = mContext.createPackageContextAsUser(packageName, 0, + new UserHandle(mUserId)); + + } catch (NameNotFoundException e) { + e.printStackTrace(); + // This should never happen, but it's better to have no widgets than to crash. + userContext = context; + } + + // These need to be created with the user context... + mAppWidgetHost = new AppWidgetHost(userContext, APPWIDGET_HOST_ID, mOnClickHandler, + Looper.myLooper()); + mAppWidgetManager = AppWidgetManager.getInstance(userContext); + + cleanupAppWidgetIds(); + + mSecurityModel = new KeyguardSecurityModel(context); + + mViewStateManager = new KeyguardViewStateManager(this); + + DevicePolicyManager dpm = + (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE); + if (dpm != null) { + mDisabledFeatures = getDisabledFeatures(dpm); + mCameraDisabled = dpm.getCameraDisabled(null); + } + + mSafeModeEnabled = LockPatternUtils.isSafeModeEnabled(); + + cleanupAppWidgetIds(); + + mAppWidgetManager = AppWidgetManager.getInstance(mContext); + mSecurityModel = new KeyguardSecurityModel(context); + + mViewStateManager = new KeyguardViewStateManager(this); + + mUserSetupCompleted = Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.USER_SETUP_COMPLETE, 0, UserHandle.USER_CURRENT) != 0; + + if (mSafeModeEnabled) { + Log.v(TAG, "Keyguard widgets disabled by safe mode"); + } + if ((mDisabledFeatures & DevicePolicyManager.KEYGUARD_DISABLE_WIDGETS_ALL) != 0) { + Log.v(TAG, "Keyguard widgets disabled by DPM"); + } + if ((mDisabledFeatures & DevicePolicyManager.KEYGUARD_DISABLE_SECURE_CAMERA) != 0) { + Log.v(TAG, "Keyguard secure camera disabled by DPM"); + } + } + + private void cleanupAppWidgetIds() { + // Since this method may delete a widget (which we can't do until boot completed) we + // may have to defer it until after boot complete. + if (!KeyguardUpdateMonitor.getInstance(mContext).hasBootCompleted()) { + mCleanupAppWidgetsOnBootCompleted = true; + return; + } + if (!mSafeModeEnabled && !widgetsDisabledByDpm()) { + // Clean up appWidgetIds that are bound to lockscreen, but not actually used + // This is only to clean up after another bug: we used to not call + // deleteAppWidgetId when a user manually deleted a widget in keyguard. This code + // shouldn't have to run more than once per user. AppWidgetProviders rely on callbacks + // that are triggered by deleteAppWidgetId, which is why we're doing this + int[] appWidgetIdsInKeyguardSettings = mLockPatternUtils.getAppWidgets(); + int[] appWidgetIdsBoundToHost = mAppWidgetHost.getAppWidgetIds(); + for (int i = 0; i < appWidgetIdsBoundToHost.length; i++) { + int appWidgetId = appWidgetIdsBoundToHost[i]; + if (!contains(appWidgetIdsInKeyguardSettings, appWidgetId)) { + Log.d(TAG, "Found a appWidgetId that's not being used by keyguard, deleting id " + + appWidgetId); + mAppWidgetHost.deleteAppWidgetId(appWidgetId); + } + } + } + } + + private static boolean contains(int[] array, int target) { + for (int value : array) { + if (value == target) { + return true; + } + } + return false; + } + + private KeyguardUpdateMonitorCallback mUpdateMonitorCallbacks = + new KeyguardUpdateMonitorCallback() { + @Override + public void onBootCompleted() { + if (mCheckAppWidgetConsistencyOnBootCompleted) { + checkAppWidgetConsistency(); + mSwitchPageRunnable.run(); + mCheckAppWidgetConsistencyOnBootCompleted = false; + } + if (mCleanupAppWidgetsOnBootCompleted) { + cleanupAppWidgetIds(); + mCleanupAppWidgetsOnBootCompleted = false; + } + } + @Override + public void onUserSwitchComplete(int userId) { + if (mKeyguardMultiUserSelectorView != null) { + mKeyguardMultiUserSelectorView.finalizeActiveUserView(true); + } + } + }; + + private SlidingChallengeLayout mSlidingChallengeLayout; + + @Override + public boolean onTouchEvent(MotionEvent ev) { + boolean result = super.onTouchEvent(ev); + mTempRect.set(0, 0, 0, 0); + offsetRectIntoDescendantCoords(mSecurityViewContainer, mTempRect); + ev.offsetLocation(mTempRect.left, mTempRect.top); + result = mSecurityViewContainer.dispatchTouchEvent(ev) || result; + ev.offsetLocation(-mTempRect.left, -mTempRect.top); + return result; + } + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + if (mViewMediatorCallback != null) { + mViewMediatorCallback.keyguardDoneDrawing(); + } + } + + private int getWidgetPosition(int id) { + final int children = mAppWidgetContainer.getChildCount(); + for (int i = 0; i < children; i++) { + if (mAppWidgetContainer.getWidgetPageAt(i).getContent().getId() == id) { + return i; + } + } + return -1; + } + + @Override + protected void onFinishInflate() { + // Grab instances of and make any necessary changes to the main layouts. Create + // view state manager and wire up necessary listeners / callbacks. + View deleteDropTarget = findViewById(R.id.keyguard_widget_pager_delete_target); + mAppWidgetContainer = (KeyguardWidgetPager) findViewById(R.id.app_widget_container); + mAppWidgetContainer.setVisibility(VISIBLE); + mAppWidgetContainer.setCallbacks(mWidgetCallbacks); + mAppWidgetContainer.setDeleteDropTarget(deleteDropTarget); + mAppWidgetContainer.setMinScale(0.5f); + + mSlidingChallengeLayout = (SlidingChallengeLayout) findViewById(R.id.sliding_layout); + if (mSlidingChallengeLayout != null) { + mSlidingChallengeLayout.setOnChallengeScrolledListener(mViewStateManager); + } + mAppWidgetContainer.setViewStateManager(mViewStateManager); + mAppWidgetContainer.setLockPatternUtils(mLockPatternUtils); + + ChallengeLayout challenge = mSlidingChallengeLayout != null ? mSlidingChallengeLayout : + (ChallengeLayout) findViewById(R.id.multi_pane_challenge); + challenge.setOnBouncerStateChangedListener(mViewStateManager); + mAppWidgetContainer.setBouncerAnimationDuration(challenge.getBouncerAnimationDuration()); + mViewStateManager.setPagedView(mAppWidgetContainer); + mViewStateManager.setChallengeLayout(challenge); + mSecurityViewContainer = (KeyguardSecurityViewFlipper) findViewById(R.id.view_flipper); + mKeyguardSelectorView = (KeyguardSelectorView) findViewById(R.id.keyguard_selector_view); + mViewStateManager.setSecurityViewContainer(mSecurityViewContainer); + + if (!(mContext instanceof Activity)) { + setSystemUiVisibility(getSystemUiVisibility() | View.STATUS_BAR_DISABLE_BACK); + } + + addDefaultWidgets(); + + addWidgetsFromSettings(); + if (!shouldEnableAddWidget()) { + mAppWidgetContainer.setAddWidgetEnabled(false); + } + checkAppWidgetConsistency(); + mSwitchPageRunnable.run(); + // This needs to be called after the pages are all added. + mViewStateManager.showUsabilityHints(); + + showPrimarySecurityScreen(false); + updateSecurityViews(); + } + + private boolean shouldEnableAddWidget() { + return numWidgets() < MAX_WIDGETS && mUserSetupCompleted; + } + + private int getDisabledFeatures(DevicePolicyManager dpm) { + int disabledFeatures = DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_NONE; + if (dpm != null) { + final int currentUser = mLockPatternUtils.getCurrentUser(); + disabledFeatures = dpm.getKeyguardDisabledFeatures(null, currentUser); + } + return disabledFeatures; + } + + private boolean widgetsDisabledByDpm() { + return (mDisabledFeatures & DevicePolicyManager.KEYGUARD_DISABLE_WIDGETS_ALL) != 0; + } + + private boolean cameraDisabledByDpm() { + return mCameraDisabled + || (mDisabledFeatures & DevicePolicyManager.KEYGUARD_DISABLE_SECURE_CAMERA) != 0; + } + + private void updateSecurityViews() { + int children = mSecurityViewContainer.getChildCount(); + for (int i = 0; i < children; i++) { + updateSecurityView(mSecurityViewContainer.getChildAt(i)); + } + } + + private void updateSecurityView(View view) { + if (view instanceof KeyguardSecurityView) { + KeyguardSecurityView ksv = (KeyguardSecurityView) view; + ksv.setKeyguardCallback(mCallback); + ksv.setLockPatternUtils(mLockPatternUtils); + if (mViewStateManager.isBouncing()) { + ksv.showBouncer(0); + } else { + ksv.hideBouncer(0); + } + } else { + Log.w(TAG, "View " + view + " is not a KeyguardSecurityView"); + } + } + + void setLockPatternUtils(LockPatternUtils utils) { + mSecurityModel.setLockPatternUtils(utils); + mLockPatternUtils = utils; + updateSecurityViews(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mAppWidgetHost.startListening(); + KeyguardUpdateMonitor.getInstance(mContext).registerCallback(mUpdateMonitorCallbacks); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mAppWidgetHost.stopListening(); + KeyguardUpdateMonitor.getInstance(mContext).removeCallback(mUpdateMonitorCallbacks); + } + + void addWidget(AppWidgetHostView view, int pageIndex) { + mAppWidgetContainer.addWidget(view, pageIndex); + } + + private KeyguardWidgetPager.Callbacks mWidgetCallbacks + = new KeyguardWidgetPager.Callbacks() { + @Override + public void userActivity() { + KeyguardHostView.this.userActivity(); + } + + @Override + public void onUserActivityTimeoutChanged() { + KeyguardHostView.this.onUserActivityTimeoutChanged(); + } + + @Override + public void onAddView(View v) { + if (!shouldEnableAddWidget()) { + mAppWidgetContainer.setAddWidgetEnabled(false); + } + } + + @Override + public void onRemoveView(View v, boolean deletePermanently) { + if (deletePermanently) { + final int appWidgetId = ((KeyguardWidgetFrame) v).getContentAppWidgetId(); + if (appWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID && + appWidgetId != LockPatternUtils.ID_DEFAULT_STATUS_WIDGET) { + mAppWidgetHost.deleteAppWidgetId(appWidgetId); + } + } + } + + @Override + public void onRemoveViewAnimationCompleted() { + if (shouldEnableAddWidget()) { + mAppWidgetContainer.setAddWidgetEnabled(true); + } + } + }; + + public void initializeSwitchingUserState(boolean switching) { + if (!switching && mKeyguardMultiUserSelectorView != null) { + mKeyguardMultiUserSelectorView.finalizeActiveUserView(false); + } + } + + public void userActivity() { + if (mViewMediatorCallback != null) { + mViewMediatorCallback.userActivity(); + } + } + + public void onUserActivityTimeoutChanged() { + if (mViewMediatorCallback != null) { + mViewMediatorCallback.onUserActivityTimeoutChanged(); + } + } + + @Override + public long getUserActivityTimeout() { + // Currently only considering user activity timeouts needed by widgets. + // Could also take into account longer timeouts for certain security views. + if (mAppWidgetContainer != null) { + return mAppWidgetContainer.getUserActivityTimeout(); + } + return -1; + } + + private KeyguardSecurityCallback mCallback = new KeyguardSecurityCallback() { + + public void userActivity(long timeout) { + if (mViewMediatorCallback != null) { + mViewMediatorCallback.userActivity(timeout); + } + } + + public void dismiss(boolean authenticated) { + showNextSecurityScreenOrFinish(authenticated); + } + + public boolean isVerifyUnlockOnly() { + return mIsVerifyUnlockOnly; + } + + public void reportSuccessfulUnlockAttempt() { + KeyguardUpdateMonitor.getInstance(mContext).clearFailedUnlockAttempts(); + mLockPatternUtils.reportSuccessfulPasswordAttempt(); + } + + public void reportFailedUnlockAttempt() { + if (mCurrentSecuritySelection == SecurityMode.Biometric) { + KeyguardUpdateMonitor.getInstance(mContext).reportFailedBiometricUnlockAttempt(); + } else { + KeyguardHostView.this.reportFailedUnlockAttempt(); + } + } + + public int getFailedAttempts() { + return KeyguardUpdateMonitor.getInstance(mContext).getFailedUnlockAttempts(); + } + + @Override + public void showBackupSecurity() { + KeyguardHostView.this.showBackupSecurityScreen(); + } + + @Override + public void setOnDismissAction(OnDismissAction action) { + KeyguardHostView.this.setOnDismissAction(action); + } + + }; + + private void showDialog(String title, String message) { + final AlertDialog dialog = new AlertDialog.Builder(mContext) + .setTitle(title) + .setMessage(message) + .setNeutralButton(com.android.internal.R.string.ok, null) + .create(); + if (!(mContext instanceof Activity)) { + dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); + } + dialog.show(); + } + + private void showTimeoutDialog() { + int timeoutInSeconds = (int) LockPatternUtils.FAILED_ATTEMPT_TIMEOUT_MS / 1000; + int messageId = 0; + + switch (mSecurityModel.getSecurityMode()) { + 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; + } + + if (messageId != 0) { + final String message = mContext.getString(messageId, + KeyguardUpdateMonitor.getInstance(mContext).getFailedUnlockAttempts(), + timeoutInSeconds); + showDialog(null, message); + } + } + + private void showAlmostAtWipeDialog(int attempts, int remaining) { + int timeoutInSeconds = (int) LockPatternUtils.FAILED_ATTEMPT_TIMEOUT_MS / 1000; + String message = mContext.getString(R.string.kg_failed_attempts_almost_at_wipe, + attempts, remaining); + showDialog(null, message); + } + + private void showWipeDialog(int attempts) { + String message = mContext.getString(R.string.kg_failed_attempts_now_wiping, attempts); + showDialog(null, message); + } + + private void showAlmostAtAccountLoginDialog() { + final int timeoutInSeconds = (int) LockPatternUtils.FAILED_ATTEMPT_TIMEOUT_MS / 1000; + final int count = LockPatternUtils.FAILED_ATTEMPTS_BEFORE_RESET + - LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT; + String message = mContext.getString(R.string.kg_failed_attempts_almost_at_login, + count, LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT, timeoutInSeconds); + showDialog(null, message); + } + + private void reportFailedUnlockAttempt() { + final KeyguardUpdateMonitor monitor = KeyguardUpdateMonitor.getInstance(mContext); + final int failedAttempts = monitor.getFailedUnlockAttempts() + 1; // +1 for this time + + if (DEBUG) Log.d(TAG, "reportFailedPatternAttempt: #" + failedAttempts); + + SecurityMode mode = mSecurityModel.getSecurityMode(); + final boolean usingPattern = mode == KeyguardSecurityModel.SecurityMode.Pattern; + + final int failedAttemptsBeforeWipe = mLockPatternUtils.getDevicePolicyManager() + .getMaximumFailedPasswordsForWipe(null, mLockPatternUtils.getCurrentUser()); + + final int failedAttemptWarning = LockPatternUtils.FAILED_ATTEMPTS_BEFORE_RESET + - LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT; + + final int remainingBeforeWipe = failedAttemptsBeforeWipe > 0 ? + (failedAttemptsBeforeWipe - failedAttempts) + : Integer.MAX_VALUE; // because DPM returns 0 if no restriction + + boolean showTimeout = false; + if (remainingBeforeWipe < LockPatternUtils.FAILED_ATTEMPTS_BEFORE_WIPE_GRACE) { + // If we reach this code, it means the user has installed a DevicePolicyManager + // that requests device wipe after N attempts. Once we get below the grace + // period, we'll post this dialog every time as a clear warning until the + // bombshell hits and the device is wiped. + if (remainingBeforeWipe > 0) { + showAlmostAtWipeDialog(failedAttempts, remainingBeforeWipe); + } else { + // Too many attempts. The device will be wiped shortly. + Slog.i(TAG, "Too many unlock attempts; device will be wiped!"); + showWipeDialog(failedAttempts); + } + } else { + showTimeout = + (failedAttempts % LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT) == 0; + if (usingPattern && mEnableFallback) { + if (failedAttempts == failedAttemptWarning) { + showAlmostAtAccountLoginDialog(); + showTimeout = false; // don't show both dialogs + } else if (failedAttempts >= LockPatternUtils.FAILED_ATTEMPTS_BEFORE_RESET) { + mLockPatternUtils.setPermanentlyLocked(true); + showSecurityScreen(SecurityMode.Account); + // don't show timeout dialog because we show account unlock screen next + showTimeout = false; + } + } + } + monitor.reportFailedUnlockAttempt(); + mLockPatternUtils.reportFailedPasswordAttempt(); + if (showTimeout) { + showTimeoutDialog(); + } + } + + /** + * 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(); + if (DEBUG) Log.v(TAG, "showPrimarySecurityScreen(turningOff=" + turningOff + ")"); + if (!turningOff && + KeyguardUpdateMonitor.getInstance(mContext).isAlternateUnlockEnabled()) { + // If we're not turning off, then allow biometric alternate. + // We'll reload it when the device comes back on. + securityMode = mSecurityModel.getAlternateFor(securityMode); + } + showSecurityScreen(securityMode); + } + + /** + * Shows the backup security screen for the current security mode. This could be used for + * password recovery screens but is currently only used for pattern unlock to show the + * account unlock screen and biometric unlock to show the user's normal unlock. + */ + private void showBackupSecurityScreen() { + if (DEBUG) Log.d(TAG, "showBackupSecurity()"); + SecurityMode backup = mSecurityModel.getBackupSecurityMode(mCurrentSecuritySelection); + showSecurityScreen(backup); + } + + public boolean showNextSecurityScreenIfPresent() { + SecurityMode securityMode = mSecurityModel.getSecurityMode(); + // Allow an alternate, such as biometric unlock + securityMode = mSecurityModel.getAlternateFor(securityMode); + if (SecurityMode.None == securityMode) { + return false; + } else { + showSecurityScreen(securityMode); // switch to the alternate security view + return true; + } + } + + private void showNextSecurityScreenOrFinish(boolean authenticated) { + if (DEBUG) Log.d(TAG, "showNextSecurityScreenOrFinish(" + authenticated + ")"); + boolean finish = false; + if (SecurityMode.None == mCurrentSecuritySelection) { + SecurityMode securityMode = mSecurityModel.getSecurityMode(); + // Allow an alternate, such as biometric unlock + securityMode = mSecurityModel.getAlternateFor(securityMode); + 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: + case Account: + case Biometric: + 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(); + if (securityMode != SecurityMode.None) { + showSecurityScreen(securityMode); + } else { + finish = true; + } + break; + + default: + Log.v(TAG, "Bad security screen " + mCurrentSecuritySelection + ", fail safe"); + showPrimarySecurityScreen(false); + break; + } + } else { + showPrimarySecurityScreen(false); + } + if (finish) { + // If the alternate unlock was suppressed, it can now be safely + // enabled because the user has left keyguard. + KeyguardUpdateMonitor.getInstance(mContext).setAlternateUnlockEnabled(true); + + // 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; + } + if (mViewMediatorCallback != null) { + if (deferKeyguardDone) { + mViewMediatorCallback.keyguardDonePending(); + } else { + mViewMediatorCallback.keyguardDone(true); + } + } + } else { + mViewStateManager.showBouncer(true); + } + } + + private OnClickHandler mOnClickHandler = new OnClickHandler() { + @Override + public boolean onClickHandler(final View view, + final android.app.PendingIntent pendingIntent, + final Intent fillInIntent) { + if (pendingIntent.isActivity()) { + setOnDismissAction(new OnDismissAction() { + public boolean onDismiss() { + try { + // TODO: Unregister this handler if PendingIntent.FLAG_ONE_SHOT? + Context context = view.getContext(); + ActivityOptions opts = ActivityOptions.makeScaleUpAnimation(view, + 0, 0, + view.getMeasuredWidth(), view.getMeasuredHeight()); + context.startIntentSender( + pendingIntent.getIntentSender(), fillInIntent, + Intent.FLAG_ACTIVITY_NEW_TASK, + Intent.FLAG_ACTIVITY_NEW_TASK, 0, opts.toBundle()); + } catch (IntentSender.SendIntentException e) { + android.util.Log.e(TAG, "Cannot send pending intent: ", e); + } catch (Exception e) { + android.util.Log.e(TAG, "Cannot send pending intent due to " + + "unknown exception: ", e); + } + return false; + } + }); + + if (mViewStateManager.isChallengeShowing()) { + mViewStateManager.showBouncer(true); + } else { + mCallback.dismiss(false); + } + return true; + } else { + return super.onClickHandler(view, pendingIntent, fillInIntent); + } + }; + }; + + // Used to ignore callbacks from methods that are no longer current (e.g. face unlock). + // This avoids unwanted asynchronous events from messing with the state. + private KeyguardSecurityCallback mNullCallback = new KeyguardSecurityCallback() { + + @Override + public void userActivity(long timeout) { + } + + @Override + public void showBackupSecurity() { + } + + @Override + public void setOnDismissAction(OnDismissAction action) { + } + + @Override + public void reportSuccessfulUnlockAttempt() { + } + + @Override + public void reportFailedUnlockAttempt() { + } + + @Override + public boolean isVerifyUnlockOnly() { + return false; + } + + @Override + public int getFailedAttempts() { + return 0; + } + + @Override + public void dismiss(boolean securityVerified) { + } + }; + + protected boolean mShowSecurityWhenReturn; + + @Override + public void reset() { + mIsVerifyUnlockOnly = false; + mAppWidgetContainer.setCurrentPage(getWidgetPosition(R.id.keyguard_status_view)); + } + + /** + * Sets an action to perform when keyguard is dismissed. + * @param action + */ + protected void setOnDismissAction(OnDismissAction action) { + mDismissAction = action; + } + + private KeyguardSecurityView getSecurityView(SecurityMode securityMode) { + final int securityViewIdForMode = getSecurityViewIdForMode(securityMode); + KeyguardSecurityView view = null; + final int children = mSecurityViewContainer.getChildCount(); + for (int child = 0; child < children; child++) { + if (mSecurityViewContainer.getChildAt(child).getId() == securityViewIdForMode) { + view = ((KeyguardSecurityView)mSecurityViewContainer.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, mSecurityViewContainer, false); + mSecurityViewContainer.addView(v); + updateSecurityView(v); + view = (KeyguardSecurityView)v; + } + + if (view instanceof KeyguardSelectorView) { + KeyguardSelectorView selectorView = (KeyguardSelectorView) view; + View carrierText = selectorView.findViewById(R.id.keyguard_selector_fade_container); + selectorView.setCarrierArea(carrierText); + } + + return view; + } + + /** + * 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); + + // Enter full screen mode if we're in SIM or Account screen + boolean fullScreenEnabled = getResources().getBoolean( + com.android.internal.R.bool.kg_sim_puk_account_full_screen); + boolean isSimOrAccount = securityMode == SecurityMode.SimPin + || securityMode == SecurityMode.SimPuk + || securityMode == SecurityMode.Account; + mAppWidgetContainer.setVisibility( + isSimOrAccount && fullScreenEnabled ? View.GONE : View.VISIBLE); + + if (mSlidingChallengeLayout != null) { + mSlidingChallengeLayout.setChallengeInteractive(!fullScreenEnabled); + } + + // Emulate Activity life cycle + if (oldView != null) { + oldView.onPause(); + oldView.setKeyguardCallback(mNullCallback); // ignore requests from old view + } + newView.onResume(KeyguardSecurityView.VIEW_REVEALED); + newView.setKeyguardCallback(mCallback); + + final boolean needsInput = newView.needsInput(); + if (mViewMediatorCallback != null) { + mViewMediatorCallback.setNeedsInput(needsInput); + } + + // Find and show this child. + final int childCount = mSecurityViewContainer.getChildCount(); + + mSecurityViewContainer.setInAnimation( + AnimationUtils.loadAnimation(mContext, R.anim.keyguard_security_fade_in)); + mSecurityViewContainer.setOutAnimation( + AnimationUtils.loadAnimation(mContext, R.anim.keyguard_security_fade_out)); + final int securityViewIdForMode = getSecurityViewIdForMode(securityMode); + for (int i = 0; i < childCount; i++) { + if (mSecurityViewContainer.getChildAt(i).getId() == securityViewIdForMode) { + mSecurityViewContainer.setDisplayedChild(i); + break; + } + } + + if (securityMode == SecurityMode.None) { + // Discard current runnable if we're switching back to the selector view + setOnDismissAction(null); + } + mCurrentSecuritySelection = securityMode; + } + + @Override + public void onScreenTurnedOn() { + if (DEBUG) Log.d(TAG, "screen on, instance " + Integer.toHexString(hashCode())); + showPrimarySecurityScreen(false); + getSecurityView(mCurrentSecuritySelection).onResume(KeyguardSecurityView.SCREEN_ON); + + // This is a an attempt to fix bug 7137389 where the device comes back on but the entire + // layout is blank but forcing a layout causes it to reappear (e.g. with with + // hierarchyviewer). + requestLayout(); + + if (mViewStateManager != null) { + mViewStateManager.showUsabilityHints(); + } + requestFocus(); + } + + @Override + public void onScreenTurnedOff() { + if (DEBUG) Log.d(TAG, String.format("screen off, instance %s at %s", + Integer.toHexString(hashCode()), SystemClock.uptimeMillis())); + // Once the screen turns off, we no longer consider this to be first boot and we want the + // biometric unlock to start next time keyguard is shown. + KeyguardUpdateMonitor.getInstance(mContext).setAlternateUnlockEnabled(true); + // We use mAppWidgetToShow to show a particular widget after you add it-- once the screen + // turns off we reset that behavior + clearAppWidgetToShow(); + checkAppWidgetConsistency(); + showPrimarySecurityScreen(true); + getSecurityView(mCurrentSecuritySelection).onPause(); + CameraWidgetFrame cameraPage = findCameraPage(); + if (cameraPage != null) { + cameraPage.onScreenTurnedOff(); + } + clearFocus(); + } + + public void clearAppWidgetToShow() { + mAppWidgetToShow = AppWidgetManager.INVALID_APPWIDGET_ID; + } + + @Override + public void show() { + if (DEBUG) Log.d(TAG, "show()"); + showPrimarySecurityScreen(false); + } + + private boolean isSecure() { + SecurityMode mode = mSecurityModel.getSecurityMode(); + switch (mode) { + case Pattern: + return mLockPatternUtils.isLockPatternEnabled(); + case Password: + case PIN: + return mLockPatternUtils.isLockPasswordEnabled(); + case SimPin: + case SimPuk: + case Account: + return true; + case None: + return false; + default: + throw new IllegalStateException("Unknown security mode " + mode); + } + } + + @Override + public void wakeWhenReadyTq(int keyCode) { + if (DEBUG) Log.d(TAG, "onWakeKey"); + if (keyCode == KeyEvent.KEYCODE_MENU && isSecure()) { + if (DEBUG) Log.d(TAG, "switching screens to unlock screen because wake key was MENU"); + showSecurityScreen(SecurityMode.None); + } else { + if (DEBUG) Log.d(TAG, "poking wake lock immediately"); + } + if (mViewMediatorCallback != null) { + mViewMediatorCallback.wakeUp(); + } + } + + @Override + public void verifyUnlock() { + SecurityMode securityMode = mSecurityModel.getSecurityMode(); + if (securityMode == KeyguardSecurityModel.SecurityMode.None) { + if (mViewMediatorCallback != null) { + mViewMediatorCallback.keyguardDone(true); + } + } else if (securityMode != KeyguardSecurityModel.SecurityMode.Pattern + && securityMode != KeyguardSecurityModel.SecurityMode.PIN + && securityMode != KeyguardSecurityModel.SecurityMode.Password) { + // can only verify unlock when in pattern/password mode + if (mViewMediatorCallback != null) { + mViewMediatorCallback.keyguardDone(false); + } + } else { + // otherwise, go to the unlock screen, see if they can verify it + mIsVerifyUnlockOnly = true; + showSecurityScreen(securityMode); + } + } + + private int getSecurityViewIdForMode(SecurityMode securityMode) { + switch (securityMode) { + case None: return R.id.keyguard_selector_view; + 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 Biometric: return R.id.keyguard_face_unlock_view; + case Account: return R.id.keyguard_account_view; + case SimPin: return R.id.keyguard_sim_pin_view; + case SimPuk: return R.id.keyguard_sim_puk_view; + } + return 0; + } + + private int getLayoutIdFor(SecurityMode securityMode) { + switch (securityMode) { + case None: return R.layout.keyguard_selector_view; + 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 Biometric: return R.layout.keyguard_face_unlock_view; + case Account: return R.layout.keyguard_account_view; + case SimPin: return R.layout.keyguard_sim_pin_view; + case SimPuk: return R.layout.keyguard_sim_puk_view; + default: + return 0; + } + } + + private boolean addWidget(int appId, int pageIndex, boolean updateDbIfFailed) { + AppWidgetProviderInfo appWidgetInfo = mAppWidgetManager.getAppWidgetInfo(appId); + if (appWidgetInfo != null) { + AppWidgetHostView view = mAppWidgetHost.createView(mContext, appId, appWidgetInfo); + addWidget(view, pageIndex); + return true; + } else { + if (updateDbIfFailed) { + Log.w(TAG, "*** AppWidgetInfo for app widget id " + appId + " was null for user" + + mUserId + ", deleting"); + mAppWidgetHost.deleteAppWidgetId(appId); + mLockPatternUtils.removeAppWidget(appId); + } + return false; + } + } + + private final CameraWidgetFrame.Callbacks mCameraWidgetCallbacks = + new CameraWidgetFrame.Callbacks() { + @Override + public void onLaunchingCamera() { + setSliderHandleAlpha(0); + } + + @Override + public void onCameraLaunchedSuccessfully() { + if (mAppWidgetContainer.isCameraPage(mAppWidgetContainer.getCurrentPage())) { + mAppWidgetContainer.scrollLeft(); + } + setSliderHandleAlpha(1); + mShowSecurityWhenReturn = true; + } + + @Override + public void onCameraLaunchedUnsuccessfully() { + setSliderHandleAlpha(1); + } + + private void setSliderHandleAlpha(float alpha) { + SlidingChallengeLayout slider = + (SlidingChallengeLayout) findViewById(R.id.sliding_layout); + if (slider != null) { + slider.setHandleAlpha(alpha); + } + } + }; + + private final KeyguardActivityLauncher mActivityLauncher = new KeyguardActivityLauncher() { + @Override + Context getContext() { + return mContext; + } + + @Override + KeyguardSecurityCallback getCallback() { + return mCallback; + } + + @Override + LockPatternUtils getLockPatternUtils() { + return mLockPatternUtils; + } + }; + + private int numWidgets() { + final int childCount = mAppWidgetContainer.getChildCount(); + int widgetCount = 0; + for (int i = 0; i < childCount; i++) { + if (mAppWidgetContainer.isWidgetPage(i)) { + widgetCount++; + } + } + return widgetCount; + } + + private void addDefaultWidgets() { + LayoutInflater inflater = LayoutInflater.from(mContext); + inflater.inflate(R.layout.keyguard_transport_control_view, this, true); + + if (!mSafeModeEnabled && !widgetsDisabledByDpm()) { + View addWidget = inflater.inflate(R.layout.keyguard_add_widget, this, false); + mAppWidgetContainer.addWidget(addWidget, 0); + View addWidgetButton = addWidget.findViewById(R.id.keyguard_add_widget_view); + addWidgetButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + // Pass in an invalid widget id... the picker will allocate an ID for us + mActivityLauncher.launchWidgetPicker(AppWidgetManager.INVALID_APPWIDGET_ID); + } + }); + } + + // We currently disable cameras in safe mode because we support loading 3rd party + // cameras we can't trust. TODO: plumb safe mode into camera creation code and only + // inflate system-provided camera? + if (!mSafeModeEnabled && !cameraDisabledByDpm() && mUserSetupCompleted + && mContext.getResources().getBoolean(R.bool.kg_enable_camera_default_widget)) { + View cameraWidget = + CameraWidgetFrame.create(mContext, mCameraWidgetCallbacks, mActivityLauncher); + if (cameraWidget != null) { + mAppWidgetContainer.addWidget(cameraWidget); + } + } + + enableUserSelectorIfNecessary(); + initializeTransportControl(); + } + + private boolean removeTransportFromWidgetPager() { + int page = getWidgetPosition(R.id.keyguard_transport_control); + if (page != -1) { + mAppWidgetContainer.removeWidget(mTransportControl); + + // XXX keep view attached so we still get show/hide events from AudioManager + KeyguardHostView.this.addView(mTransportControl); + mTransportControl.setVisibility(View.GONE); + mViewStateManager.setTransportState(KeyguardViewStateManager.TRANSPORT_GONE); + return true; + } + return false; + } + + private void addTransportToWidgetPager() { + if (getWidgetPosition(R.id.keyguard_transport_control) == -1) { + KeyguardHostView.this.removeView(mTransportControl); + // insert to left of camera if it exists, otherwise after right-most widget + int lastWidget = mAppWidgetContainer.getChildCount() - 1; + int position = 0; // handle no widget case + if (lastWidget >= 0) { + position = mAppWidgetContainer.isCameraPage(lastWidget) ? + lastWidget : lastWidget + 1; + } + mAppWidgetContainer.addWidget(mTransportControl, position); + mTransportControl.setVisibility(View.VISIBLE); + } + } + + private void initializeTransportControl() { + mTransportControl = + (KeyguardTransportControlView) findViewById(R.id.keyguard_transport_control); + mTransportControl.setVisibility(View.GONE); + + // This code manages showing/hiding the transport control. We keep it around and only + // add it to the hierarchy if it needs to be present. + if (mTransportControl != null) { + mTransportControl.setKeyguardCallback(new TransportCallback() { + @Override + public void onListenerDetached() { + if (removeTransportFromWidgetPager()) { + mTransportControl.post(mSwitchPageRunnable); + } + } + + @Override + public void onListenerAttached() { + // Transport will be added when playstate changes... + mTransportControl.post(mSwitchPageRunnable); + } + + @Override + public void onPlayStateChanged() { + mTransportControl.post(mSwitchPageRunnable); + } + }); + } + } + + private int getInsertPageIndex() { + View addWidget = mAppWidgetContainer.findViewById(R.id.keyguard_add_widget); + int insertionIndex = mAppWidgetContainer.indexOfChild(addWidget); + if (insertionIndex < 0) { + insertionIndex = 0; // no add widget page found + } else { + insertionIndex++; // place after add widget + } + return insertionIndex; + } + + private void addDefaultStatusWidget(int index) { + LayoutInflater inflater = LayoutInflater.from(mContext); + View statusWidget = inflater.inflate(R.layout.keyguard_status_view, null, true); + mAppWidgetContainer.addWidget(statusWidget, index); + } + + private void addWidgetsFromSettings() { + if (mSafeModeEnabled || widgetsDisabledByDpm()) { + return; + } + + int insertionIndex = getInsertPageIndex(); + + // Add user-selected widget + final int[] widgets = mLockPatternUtils.getAppWidgets(); + + if (widgets == null) { + Log.d(TAG, "Problem reading widgets"); + } else { + for (int i = widgets.length -1; i >= 0; i--) { + if (widgets[i] == LockPatternUtils.ID_DEFAULT_STATUS_WIDGET) { + addDefaultStatusWidget(insertionIndex); + } else { + // We add the widgets from left to right, starting after the first page after + // the add page. We count down, since the order will be persisted from right + // to left, starting after camera. + addWidget(widgets[i], insertionIndex, true); + } + } + } + } + + private int allocateIdForDefaultAppWidget() { + int appWidgetId; + Resources res = getContext().getResources(); + ComponentName defaultAppWidget = new ComponentName( + res.getString(R.string.widget_default_package_name), + res.getString(R.string.widget_default_class_name)); + + // Note: we don't support configuring the widget + appWidgetId = mAppWidgetHost.allocateAppWidgetId(); + + try { + mAppWidgetManager.bindAppWidgetId(appWidgetId, defaultAppWidget); + + } catch (IllegalArgumentException e) { + Log.e(TAG, "Error when trying to bind default AppWidget: " + e); + mAppWidgetHost.deleteAppWidgetId(appWidgetId); + appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID; + } + return appWidgetId; + } + public void checkAppWidgetConsistency() { + // Since this method may bind a widget (which we can't do until boot completed) we + // may have to defer it until after boot complete. + if (!KeyguardUpdateMonitor.getInstance(mContext).hasBootCompleted()) { + mCheckAppWidgetConsistencyOnBootCompleted = true; + return; + } + final int childCount = mAppWidgetContainer.getChildCount(); + boolean widgetPageExists = false; + for (int i = 0; i < childCount; i++) { + if (mAppWidgetContainer.isWidgetPage(i)) { + widgetPageExists = true; + break; + } + } + if (!widgetPageExists) { + final int insertPageIndex = getInsertPageIndex(); + + final boolean userAddedWidgetsEnabled = !widgetsDisabledByDpm(); + boolean addedDefaultAppWidget = false; + + if (!mSafeModeEnabled) { + if (userAddedWidgetsEnabled) { + int appWidgetId = allocateIdForDefaultAppWidget(); + if (appWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID) { + addedDefaultAppWidget = addWidget(appWidgetId, insertPageIndex, true); + } + } else { + // note: even if widgetsDisabledByDpm() returns true, we still bind/create + // the default appwidget if possible + int appWidgetId = mLockPatternUtils.getFallbackAppWidgetId(); + if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) { + appWidgetId = allocateIdForDefaultAppWidget(); + if (appWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID) { + mLockPatternUtils.writeFallbackAppWidgetId(appWidgetId); + } + } + if (appWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID) { + addedDefaultAppWidget = addWidget(appWidgetId, insertPageIndex, false); + if (!addedDefaultAppWidget) { + mAppWidgetHost.deleteAppWidgetId(appWidgetId); + mLockPatternUtils.writeFallbackAppWidgetId( + AppWidgetManager.INVALID_APPWIDGET_ID); + } + } + } + } + + // Use the built-in status/clock view if we can't inflate the default widget + if (!addedDefaultAppWidget) { + addDefaultStatusWidget(insertPageIndex); + } + + // trigger DB updates only if user-added widgets are enabled + if (!mSafeModeEnabled && userAddedWidgetsEnabled) { + mAppWidgetContainer.onAddView( + mAppWidgetContainer.getChildAt(insertPageIndex), insertPageIndex); + } + } + } + + Runnable mSwitchPageRunnable = new Runnable() { + @Override + public void run() { + showAppropriateWidgetPage(); + } + }; + + static class SavedState extends BaseSavedState { + int transportState; + int appWidgetToShow = AppWidgetManager.INVALID_APPWIDGET_ID; + + SavedState(Parcelable superState) { + super(superState); + } + + private SavedState(Parcel in) { + super(in); + this.transportState = in.readInt(); + this.appWidgetToShow = in.readInt(); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeInt(this.transportState); + out.writeInt(this.appWidgetToShow); + } + + public static final Parcelable.Creator<SavedState> CREATOR + = new Parcelable.Creator<SavedState>() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + @Override + public Parcelable onSaveInstanceState() { + if (DEBUG) Log.d(TAG, "onSaveInstanceState"); + Parcelable superState = super.onSaveInstanceState(); + SavedState ss = new SavedState(superState); + ss.transportState = mViewStateManager.getTransportState(); + ss.appWidgetToShow = mAppWidgetToShow; + return ss; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + if (DEBUG) Log.d(TAG, "onRestoreInstanceState"); + if (!(state instanceof SavedState)) { + super.onRestoreInstanceState(state); + return; + } + SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + mViewStateManager.setTransportState(ss.transportState); + mAppWidgetToShow = ss.appWidgetToShow; + post(mSwitchPageRunnable); + } + + @Override + public void onWindowFocusChanged(boolean hasWindowFocus) { + super.onWindowFocusChanged(hasWindowFocus); + if (DEBUG) Log.d(TAG, "Window is " + (hasWindowFocus ? "focused" : "unfocused")); + if (hasWindowFocus && mShowSecurityWhenReturn) { + SlidingChallengeLayout slider = + (SlidingChallengeLayout) findViewById(R.id.sliding_layout); + if (slider != null) { + slider.setHandleAlpha(1); + slider.showChallenge(true); + } + mShowSecurityWhenReturn = false; + } + } + + private void showAppropriateWidgetPage() { + int state = mViewStateManager.getTransportState(); + boolean isMusicPlaying = mTransportControl.isMusicPlaying() + || state == KeyguardViewStateManager.TRANSPORT_VISIBLE; + if (isMusicPlaying) { + mViewStateManager.setTransportState(KeyguardViewStateManager.TRANSPORT_VISIBLE); + addTransportToWidgetPager(); + } else if (state == KeyguardViewStateManager.TRANSPORT_VISIBLE) { + mViewStateManager.setTransportState(KeyguardViewStateManager.TRANSPORT_INVISIBLE); + } + int pageToShow = getAppropriateWidgetPage(isMusicPlaying); + mAppWidgetContainer.setCurrentPage(pageToShow); + } + + private CameraWidgetFrame findCameraPage() { + for (int i = mAppWidgetContainer.getChildCount() - 1; i >= 0; i--) { + if (mAppWidgetContainer.isCameraPage(i)) { + return (CameraWidgetFrame) mAppWidgetContainer.getChildAt(i); + } + } + return null; + } + + boolean isMusicPage(int pageIndex) { + return pageIndex >= 0 && pageIndex == getWidgetPosition(R.id.keyguard_transport_control); + } + + private int getAppropriateWidgetPage(boolean isMusicPlaying) { + // assumes at least one widget (besides camera + add) + if (mAppWidgetToShow != AppWidgetManager.INVALID_APPWIDGET_ID) { + final int childCount = mAppWidgetContainer.getChildCount(); + for (int i = 0; i < childCount; i++) { + if (mAppWidgetContainer.getWidgetPageAt(i).getContentAppWidgetId() + == mAppWidgetToShow) { + return i; + } + } + mAppWidgetToShow = AppWidgetManager.INVALID_APPWIDGET_ID; + } + // if music playing, show transport + if (isMusicPlaying) { + if (DEBUG) Log.d(TAG, "Music playing, show transport"); + return mAppWidgetContainer.getWidgetPageIndex(mTransportControl); + } + + // else show the right-most widget (except for camera) + int rightMost = mAppWidgetContainer.getChildCount() - 1; + if (mAppWidgetContainer.isCameraPage(rightMost)) { + rightMost--; + } + if (DEBUG) Log.d(TAG, "Show right-most page " + rightMost); + return rightMost; + } + + private void enableUserSelectorIfNecessary() { + if (!UserManager.supportsMultipleUsers()) { + return; // device doesn't support multi-user mode + } + final UserManager um = (UserManager) mContext.getSystemService(Context.USER_SERVICE); + if (um == null) { + Throwable t = new Throwable(); + t.fillInStackTrace(); + Log.e(TAG, "user service is null.", t); + return; + } + + // if there are multiple users, we need to enable to multi-user switcher + final List<UserInfo> users = um.getUsers(true); + if (users == null) { + Throwable t = new Throwable(); + t.fillInStackTrace(); + Log.e(TAG, "list of users is null.", t); + return; + } + + final View multiUserView = findViewById(R.id.keyguard_user_selector); + if (multiUserView == null) { + Throwable t = new Throwable(); + t.fillInStackTrace(); + Log.e(TAG, "can't find user_selector in layout.", t); + return; + } + + if (users.size() > 1) { + if (multiUserView instanceof KeyguardMultiUserSelectorView) { + mKeyguardMultiUserSelectorView = (KeyguardMultiUserSelectorView) multiUserView; + mKeyguardMultiUserSelectorView.setVisibility(View.VISIBLE); + mKeyguardMultiUserSelectorView.addUsers(users); + UserSwitcherCallback callback = new UserSwitcherCallback() { + @Override + public void hideSecurityView(int duration) { + mSecurityViewContainer.animate().alpha(0).setDuration(duration); + } + + @Override + public void showSecurityView() { + mSecurityViewContainer.setAlpha(1.0f); + } + + @Override + public void showUnlockHint() { + if (mKeyguardSelectorView != null) { + mKeyguardSelectorView.showUsabilityHint(); + } + } + + @Override + public void userActivity() { + if (mViewMediatorCallback != null) { + mViewMediatorCallback.userActivity(); + } + } + }; + mKeyguardMultiUserSelectorView.setCallback(callback); + } else { + Throwable t = new Throwable(); + t.fillInStackTrace(); + if (multiUserView == null) { + Log.e(TAG, "could not find the user_selector.", t); + } else { + Log.e(TAG, "user_selector is the wrong type.", t); + } + } + } + } + + @Override + public void cleanUp() { + + } + + /** + * 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"; + private boolean shouldEnableMenuKey() { + final Resources res = getResources(); + final boolean configDisabled = res.getBoolean( + com.android.internal.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 goToUserSwitcher() { + mAppWidgetContainer.setCurrentPage(getWidgetPosition(R.id.keyguard_multi_user_selector)); + } + + public void goToWidget(int appWidgetId) { + mAppWidgetToShow = appWidgetId; + mSwitchPageRunnable.run(); + } + + public boolean handleMenuKey() { + // The following enables the MENU key to work for testing automation + if (shouldEnableMenuKey()) { + showNextSecurityScreenOrFinish(false); + return true; + } + return false; + } + + public boolean handleBackKey() { + if (mCurrentSecuritySelection != SecurityMode.None) { + mCallback.dismiss(false); + return true; + } + return false; + } + + /** + * Dismisses the keyguard by going to the next screen or making it gone. + */ + public void dismiss() { + showNextSecurityScreenOrFinish(false); + } + + public void showAssistant() { + final Intent intent = ((SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE)) + .getAssistIntent(mContext, true, UserHandle.USER_CURRENT); + + if (intent == null) return; + + final ActivityOptions opts = ActivityOptions.makeCustomAnimation(mContext, + R.anim.keyguard_action_assist_enter, R.anim.keyguard_action_assist_exit, + getHandler(), null); + + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + mActivityLauncher.launchActivityWithAnimation( + intent, false, opts.toBundle(), null, null); + } +} diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardLinearLayout.java b/packages/Keyguard/src/com/android/keyguard/KeyguardLinearLayout.java new file mode 100644 index 0000000..0fc54cd --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardLinearLayout.java @@ -0,0 +1,46 @@ +/* + * 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.internal.policy.impl.keyguard; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.LinearLayout; + +/** + * A layout that arranges its children into a special type of grid. + */ +public class KeyguardLinearLayout extends LinearLayout { + int mTopChild = 0; + + public KeyguardLinearLayout(Context context) { + this(context, null, 0); + } + + public KeyguardLinearLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public KeyguardLinearLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public void setTopChild(View child) { + int top = indexOfChild(child); + mTopChild = top; + invalidate(); + } +} diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardMessageArea.java b/packages/Keyguard/src/com/android/keyguard/KeyguardMessageArea.java new file mode 100644 index 0000000..77359ff --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardMessageArea.java @@ -0,0 +1,309 @@ +/* + * 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.internal.policy.impl.keyguard; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.content.ContentResolver; +import android.content.Context; +import android.os.BatteryManager; +import android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; +import android.os.UserHandle; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.View; +import android.widget.TextView; + +import libcore.util.MutableInt; + +import java.lang.ref.WeakReference; + +import com.android.internal.R; + +/*** + * Manages a number of views inside of the given layout. See below for a list of widgets. + */ +class KeyguardMessageArea extends TextView { + /** 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; + + static final int CHARGING_ICON = 0; //R.drawable.ic_lock_idle_charging; + static final int BATTERY_LOW_ICON = 0; //R.drawable.ic_lock_idle_low_battery; + + static final int SECURITY_MESSAGE_DURATION = 5000; + protected static final int FADE_DURATION = 750; + + // are we showing battery information? + boolean mShowingBatteryInfo = false; + + // is the bouncer up? + boolean mShowingBouncer = false; + + // last known plugged in state + boolean mCharging = false; + + // last known battery level + int mBatteryLevel = 100; + + KeyguardUpdateMonitor mUpdateMonitor; + + // Timeout before we reset the message to show charging/owner info + long mTimeout = SECURITY_MESSAGE_DURATION; + + // Shadowed text values + protected boolean mBatteryCharged; + protected boolean mBatteryIsLow; + + private Handler mHandler; + + CharSequence mMessage; + boolean mShowingMessage; + Runnable mClearMessageRunnable = new Runnable() { + @Override + public void run() { + mMessage = null; + mShowingMessage = false; + if (mShowingBouncer) { + hideMessage(FADE_DURATION, true); + } else { + update(); + } + } + }; + + public static class Helper implements SecurityMessageDisplay { + KeyguardMessageArea mMessageArea; + Helper(View v) { + mMessageArea = (KeyguardMessageArea) v.findViewById(R.id.keyguard_message_area); + if (mMessageArea == null) { + throw new RuntimeException("Can't find keyguard_message_area in " + v.getClass()); + } + } + + public void setMessage(CharSequence msg, boolean important) { + if (!TextUtils.isEmpty(msg) && important) { + mMessageArea.mMessage = msg; + mMessageArea.securityMessageChanged(); + } + } + + public void setMessage(int resId, boolean important) { + if (resId != 0 && important) { + mMessageArea.mMessage = mMessageArea.getContext().getResources().getText(resId); + mMessageArea.securityMessageChanged(); + } + } + + public void setMessage(int resId, boolean important, Object... formatArgs) { + if (resId != 0 && important) { + mMessageArea.mMessage = mMessageArea.getContext().getString(resId, formatArgs); + mMessageArea.securityMessageChanged(); + } + } + + @Override + public void showBouncer(int duration) { + mMessageArea.hideMessage(duration, false); + mMessageArea.mShowingBouncer = true; + } + + @Override + public void hideBouncer(int duration) { + mMessageArea.showMessage(duration); + mMessageArea.mShowingBouncer = false; + } + + @Override + public void setTimeout(int timeoutMs) { + mMessageArea.mTimeout = timeoutMs; + } + } + + private KeyguardUpdateMonitorCallback mInfoCallback = new KeyguardUpdateMonitorCallback() { + @Override + public void onRefreshBatteryInfo(KeyguardUpdateMonitor.BatteryStatus status) { + mShowingBatteryInfo = status.isPluggedIn() || status.isBatteryLow(); + mCharging = status.status == BatteryManager.BATTERY_STATUS_CHARGING + || status.status == BatteryManager.BATTERY_STATUS_FULL; + mBatteryLevel = status.level; + mBatteryCharged = status.isCharged(); + mBatteryIsLow = status.isBatteryLow(); + update(); + } + }; + + private CharSequence mSeparator; + + public KeyguardMessageArea(Context context) { + this(context, null); + } + + public KeyguardMessageArea(Context context, AttributeSet attrs) { + super(context, attrs); + + // This is required to ensure marquee works + setSelected(true); + + // Registering this callback immediately updates the battery state, among other things. + mUpdateMonitor = KeyguardUpdateMonitor.getInstance(getContext()); + mUpdateMonitor.registerCallback(mInfoCallback); + mHandler = new Handler(Looper.myLooper()); + + mSeparator = getResources().getString(R.string.kg_text_message_separator); + + update(); + } + + public void securityMessageChanged() { + setAlpha(1f); + mShowingMessage = true; + update(); + mHandler.removeCallbacks(mClearMessageRunnable); + if (mTimeout > 0) { + mHandler.postDelayed(mClearMessageRunnable, mTimeout); + } + mHandler.removeCallbacksAndMessages(ANNOUNCE_TOKEN); + mHandler.postAtTime(new AnnounceRunnable(this, getText()), ANNOUNCE_TOKEN, + (SystemClock.uptimeMillis() + ANNOUNCEMENT_DELAY)); + } + + /** + * Update the status lines based on these rules: + * AlarmStatus: Alarm state always gets it's own line. + * Status1 is shared between help, battery status and generic unlock instructions, + * prioritized in that order. + * @param showStatusLines status lines are shown if true + */ + void update() { + MutableInt icon = new MutableInt(0); + CharSequence status = concat(getChargeInfo(icon), getOwnerInfo(), getCurrentMessage()); + setCompoundDrawablesWithIntrinsicBounds(icon.value, 0, 0, 0); + setText(status); + } + + private CharSequence concat(CharSequence... args) { + StringBuilder b = new StringBuilder(); + if (!TextUtils.isEmpty(args[0])) { + b.append(args[0]); + } + for (int i = 1; i < args.length; i++) { + CharSequence text = args[i]; + if (!TextUtils.isEmpty(text)) { + if (b.length() > 0) { + b.append(mSeparator); + } + b.append(text); + } + } + return b.toString(); + } + + CharSequence getCurrentMessage() { + return mShowingMessage ? mMessage : null; + } + + String getOwnerInfo() { + ContentResolver res = getContext().getContentResolver(); + final boolean ownerInfoEnabled = Settings.Secure.getIntForUser(res, + Settings.Secure.LOCK_SCREEN_OWNER_INFO_ENABLED, 1, UserHandle.USER_CURRENT) != 0; + return ownerInfoEnabled && !mShowingMessage ? + Settings.Secure.getStringForUser(res, Settings.Secure.LOCK_SCREEN_OWNER_INFO, + UserHandle.USER_CURRENT) : null; + } + + private CharSequence getChargeInfo(MutableInt icon) { + CharSequence string = null; + if (mShowingBatteryInfo && !mShowingMessage) { + // Battery status + if (mCharging) { + // Charging, charged or waiting to charge. + string = getContext().getString(mBatteryCharged + ? com.android.internal.R.string.lockscreen_charged + : com.android.internal.R.string.lockscreen_plugged_in, mBatteryLevel); + icon.value = CHARGING_ICON; + } else if (mBatteryIsLow) { + // Battery is low + string = getContext().getString( + com.android.internal.R.string.lockscreen_low_battery); + icon.value = BATTERY_LOW_ICON; + } + } + return string; + } + + private void hideMessage(int duration, boolean thenUpdate) { + if (duration > 0) { + Animator anim = ObjectAnimator.ofFloat(this, "alpha", 0f); + anim.setDuration(duration); + if (thenUpdate) { + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + update(); + } + }); + } + anim.start(); + } else { + setAlpha(0f); + if (thenUpdate) { + update(); + } + } + } + + private void showMessage(int duration) { + if (duration > 0) { + Animator anim = ObjectAnimator.ofFloat(this, "alpha", 1f); + anim.setDuration(duration); + anim.start(); + } else { + setAlpha(1f); + } + } + + /** + * Runnable used to delay accessibility announcements. + */ + private static class AnnounceRunnable implements Runnable { + private final WeakReference<View> mHost; + private final CharSequence mTextToAnnounce; + + public 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/Keyguard/src/com/android/keyguard/KeyguardMultiUserAvatar.java b/packages/Keyguard/src/com/android/keyguard/KeyguardMultiUserAvatar.java new file mode 100644 index 0000000..9d1f041 --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardMultiUserAvatar.java @@ -0,0 +1,235 @@ +/* + * 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.internal.policy.impl.keyguard; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.pm.UserInfo; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.internal.R; + +class KeyguardMultiUserAvatar extends FrameLayout { + private static final String TAG = KeyguardMultiUserAvatar.class.getSimpleName(); + private static final boolean DEBUG = KeyguardHostView.DEBUG; + + private ImageView mUserImage; + private TextView mUserName; + private UserInfo mUserInfo; + private static final float ACTIVE_ALPHA = 1.0f; + private static final float INACTIVE_ALPHA = 1.0f; + private static final float ACTIVE_SCALE = 1.5f; + private static final float ACTIVE_TEXT_ALPHA = 0f; + private static final float INACTIVE_TEXT_ALPHA = 0.5f; + private static final int SWITCH_ANIMATION_DURATION = 150; + + private final float mActiveAlpha; + private final float mActiveScale; + private final float mActiveTextAlpha; + private final float mInactiveAlpha; + private final float mInactiveTextAlpha; + private final float mShadowRadius; + private final float mStroke; + private final float mIconSize; + private final int mFrameColor; + private final int mFrameShadowColor; + private final int mTextColor; + private final int mHighlightColor; + + private boolean mTouched; + + private boolean mActive; + private boolean mInit = true; + private KeyguardMultiUserSelectorView mUserSelector; + private KeyguardCircleFramedDrawable mFramed; + private boolean mPressLock; + + public static KeyguardMultiUserAvatar fromXml(int resId, Context context, + KeyguardMultiUserSelectorView userSelector, UserInfo info) { + KeyguardMultiUserAvatar icon = (KeyguardMultiUserAvatar) + LayoutInflater.from(context).inflate(resId, userSelector, false); + + icon.init(info, userSelector); + return icon; + } + + public KeyguardMultiUserAvatar(Context context) { + this(context, null, 0); + } + + public KeyguardMultiUserAvatar(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public KeyguardMultiUserAvatar(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + Resources res = mContext.getResources(); + mTextColor = res.getColor(R.color.keyguard_avatar_nick_color); + mIconSize = res.getDimension(R.dimen.keyguard_avatar_size); + mStroke = res.getDimension(R.dimen.keyguard_avatar_frame_stroke_width); + mShadowRadius = res.getDimension(R.dimen.keyguard_avatar_frame_shadow_radius); + mFrameColor = res.getColor(R.color.keyguard_avatar_frame_color); + mFrameShadowColor = res.getColor(R.color.keyguard_avatar_frame_shadow_color); + mHighlightColor = res.getColor(R.color.keyguard_avatar_frame_pressed_color); + mActiveTextAlpha = ACTIVE_TEXT_ALPHA; + mInactiveTextAlpha = INACTIVE_TEXT_ALPHA; + mActiveScale = ACTIVE_SCALE; + mActiveAlpha = ACTIVE_ALPHA; + mInactiveAlpha = INACTIVE_ALPHA; + + mTouched = false; + + setLayerType(View.LAYER_TYPE_SOFTWARE, null); + } + + protected String rewriteIconPath(String path) { + if (!this.getClass().getName().contains("internal")) { + return path.replace("system", "data"); + } + return path; + } + + public void init(UserInfo user, KeyguardMultiUserSelectorView userSelector) { + mUserInfo = user; + mUserSelector = userSelector; + + mUserImage = (ImageView) findViewById(R.id.keyguard_user_avatar); + mUserName = (TextView) findViewById(R.id.keyguard_user_name); + + Bitmap icon = null; + try { + icon = BitmapFactory.decodeFile(rewriteIconPath(user.iconPath)); + } catch (Exception e) { + if (DEBUG) Log.d(TAG, "failed to open profile icon " + user.iconPath, e); + } + + if (icon == null) { + icon = BitmapFactory.decodeResource(mContext.getResources(), + com.android.internal.R.drawable.ic_contact_picture); + } + + mFramed = new KeyguardCircleFramedDrawable(icon, (int) mIconSize, mFrameColor, mStroke, + mFrameShadowColor, mShadowRadius, mHighlightColor); + mUserImage.setImageDrawable(mFramed); + mUserName.setText(mUserInfo.name); + setOnClickListener(mUserSelector); + mInit = false; + } + + public void setActive(boolean active, boolean animate, final Runnable onComplete) { + if (mActive != active || mInit) { + mActive = active; + + if (active) { + KeyguardLinearLayout parent = (KeyguardLinearLayout) getParent(); + parent.setTopChild(this); + // TODO: Create an appropriate asset when string changes are possible. + setContentDescription(mUserName.getText() + + ". " + mContext.getString(R.string.user_switched, "")); + } else { + setContentDescription(mUserName.getText()); + } + } + updateVisualsForActive(mActive, animate, SWITCH_ANIMATION_DURATION, onComplete); + } + + void updateVisualsForActive(boolean active, boolean animate, int duration, + final Runnable onComplete) { + final float finalAlpha = active ? mActiveAlpha : mInactiveAlpha; + final float initAlpha = active ? mInactiveAlpha : mActiveAlpha; + final float finalScale = active ? 1f : 1f / mActiveScale; + final float initScale = mFramed.getScale(); + final int finalTextAlpha = active ? (int) (mActiveTextAlpha * 255) : + (int) (mInactiveTextAlpha * 255); + final int initTextAlpha = active ? (int) (mInactiveTextAlpha * 255) : + (int) (mActiveTextAlpha * 255); + int textColor = mTextColor; + mUserName.setTextColor(textColor); + + if (animate && mTouched) { + ValueAnimator va = ValueAnimator.ofFloat(0f, 1f); + va.addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + float r = animation.getAnimatedFraction(); + float scale = (1 - r) * initScale + r * finalScale; + float alpha = (1 - r) * initAlpha + r * finalAlpha; + int textAlpha = (int) ((1 - r) * initTextAlpha + r * finalTextAlpha); + mFramed.setScale(scale); + mUserImage.setAlpha(alpha); + mUserName.setTextColor(Color.argb(textAlpha, 255, 255, 255)); + mUserImage.invalidate(); + } + }); + va.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (onComplete != null) { + onComplete.run(); + } + } + }); + va.setDuration(duration); + va.start(); + } else { + mFramed.setScale(finalScale); + mUserImage.setAlpha(finalAlpha); + mUserName.setTextColor(Color.argb(finalTextAlpha, 255, 255, 255)); + if (onComplete != null) { + post(onComplete); + } + } + + mTouched = true; + } + + @Override + public void setPressed(boolean pressed) { + if (mPressLock && !pressed) { + return; + } + + if (mPressLock || !pressed || isClickable()) { + super.setPressed(pressed); + mFramed.setPressed(pressed); + mUserImage.invalidate(); + } + } + + public void lockPressed(boolean pressed) { + mPressLock = pressed; + setPressed(pressed); + } + + public UserInfo getUserInfo() { + return mUserInfo; + } +} diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardMultiUserSelectorView.java b/packages/Keyguard/src/com/android/keyguard/KeyguardMultiUserSelectorView.java new file mode 100644 index 0000000..f9ea5bb --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardMultiUserSelectorView.java @@ -0,0 +1,172 @@ +/* + * 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.internal.policy.impl.keyguard; + +import android.app.ActivityManagerNative; +import android.content.Context; +import android.content.pm.UserInfo; +import android.os.RemoteException; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import com.android.internal.R; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; + +public class KeyguardMultiUserSelectorView extends FrameLayout implements View.OnClickListener { + private static final String TAG = "KeyguardMultiUserSelectorView"; + + private ViewGroup mUsersGrid; + private KeyguardMultiUserAvatar mActiveUserAvatar; + private KeyguardHostView.UserSwitcherCallback mCallback; + private static final int FADE_OUT_ANIMATION_DURATION = 100; + + public KeyguardMultiUserSelectorView(Context context) { + this(context, null, 0); + } + + public KeyguardMultiUserSelectorView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public KeyguardMultiUserSelectorView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + protected void onFinishInflate () { + mUsersGrid = (ViewGroup) findViewById(R.id.keyguard_users_grid); + mUsersGrid.removeAllViews(); + setClipChildren(false); + setClipToPadding(false); + + } + + public void setCallback(KeyguardHostView.UserSwitcherCallback callback) { + mCallback = callback; + } + + public void addUsers(Collection<UserInfo> userList) { + UserInfo activeUser; + try { + activeUser = ActivityManagerNative.getDefault().getCurrentUser(); + } catch (RemoteException re) { + activeUser = null; + } + + ArrayList<UserInfo> users = new ArrayList<UserInfo>(userList); + Collections.sort(users, mOrderAddedComparator); + + for (UserInfo user: users) { + KeyguardMultiUserAvatar uv = createAndAddUser(user); + if (user.id == activeUser.id) { + mActiveUserAvatar = uv; + } + uv.setActive(false, false, null); + } + mActiveUserAvatar.lockPressed(true); + } + + public void finalizeActiveUserView(boolean animate) { + if (animate) { + getHandler().postDelayed(new Runnable() { + @Override + public void run() { + finalizeActiveUserNow(true); + } + }, 500); + } else { + finalizeActiveUserNow(animate); + } + } + + void finalizeActiveUserNow(boolean animate) { + mActiveUserAvatar.lockPressed(false); + mActiveUserAvatar.setActive(true, animate, null); + } + + Comparator<UserInfo> mOrderAddedComparator = new Comparator<UserInfo>() { + @Override + public int compare(UserInfo lhs, UserInfo rhs) { + return (lhs.serialNumber - rhs.serialNumber); + } + }; + + private KeyguardMultiUserAvatar createAndAddUser(UserInfo user) { + KeyguardMultiUserAvatar uv = KeyguardMultiUserAvatar.fromXml( + R.layout.keyguard_multi_user_avatar, mContext, this, user); + mUsersGrid.addView(uv); + return uv; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + if(event.getActionMasked() != MotionEvent.ACTION_CANCEL && mCallback != null) { + mCallback.userActivity(); + } + return false; + } + + private void setAllClickable(boolean clickable) + { + for(int i = 0; i < mUsersGrid.getChildCount(); i++) { + View v = mUsersGrid.getChildAt(i); + v.setClickable(clickable); + v.setPressed(false); + } + } + + @Override + public void onClick(View v) { + if (!(v instanceof KeyguardMultiUserAvatar)) return; + final KeyguardMultiUserAvatar avatar = (KeyguardMultiUserAvatar) v; + if (avatar.isClickable()) { // catch race conditions + if (mActiveUserAvatar == avatar) { + // If they click the currently active user, show the unlock hint + mCallback.showUnlockHint(); + return; + } else { + // Reset the previously active user to appear inactive + mCallback.hideSecurityView(FADE_OUT_ANIMATION_DURATION); + setAllClickable(false); + avatar.lockPressed(true); + mActiveUserAvatar.setActive(false, true, new Runnable() { + @Override + public void run() { + mActiveUserAvatar = avatar; + if (this.getClass().getName().contains("internal")) { + try { + ActivityManagerNative.getDefault() + .switchUser(avatar.getUserInfo().id); + } catch (RemoteException re) { + Log.e(TAG, "Couldn't switch user " + re); + } + } else { + setAllClickable(true); + } + } + }); + } + } + } +} diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardPINView.java b/packages/Keyguard/src/com/android/keyguard/KeyguardPINView.java new file mode 100644 index 0000000..fa80352 --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardPINView.java @@ -0,0 +1,120 @@ +/* + * 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.internal.policy.impl.keyguard; + +import android.content.Context; +import android.text.Editable; +import android.text.InputType; +import android.text.TextWatcher; +import android.text.method.DigitsKeyListener; +import android.util.AttributeSet; +import android.view.View; +import android.widget.TextView.OnEditorActionListener; + +import com.android.internal.R; + +/** + * Displays a PIN pad for unlocking. + */ +public class KeyguardPINView extends KeyguardAbsKeyInputView + implements KeyguardSecurityView, OnEditorActionListener, TextWatcher { + + public KeyguardPINView(Context context) { + this(context, null); + } + + public KeyguardPINView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + protected void resetState() { + if (KeyguardUpdateMonitor.getInstance(mContext).getMaxBiometricUnlockAttemptsReached()) { + mSecurityMessageDisplay.setMessage(R.string.faceunlock_multiple_failures, true); + } else { + mSecurityMessageDisplay.setMessage(R.string.kg_pin_instructions, false); + } + mPasswordEntry.setEnabled(true); + } + + @Override + protected int getPasswordTextViewId() { + return R.id.pinEntry; + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + final View ok = findViewById(R.id.key_enter); + if (ok != null) { + ok.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + doHapticKeyClick(); + if (mPasswordEntry.isEnabled()) { + verifyPasswordAndUnlock(); + } + } + }); + ok.setOnHoverListener(new LiftToActivateListener(getContext())); + } + + // The delete button is of the PIN keyboard itself in some (e.g. tablet) layouts, + // not a separate view + View pinDelete = findViewById(R.id.delete_button); + if (pinDelete != null) { + pinDelete.setVisibility(View.VISIBLE); + pinDelete.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + // check for time-based lockouts + if (mPasswordEntry.isEnabled()) { + CharSequence str = mPasswordEntry.getText(); + if (str.length() > 0) { + mPasswordEntry.setText(str.subSequence(0, str.length()-1)); + } + } + doHapticKeyClick(); + } + }); + pinDelete.setOnLongClickListener(new View.OnLongClickListener() { + public boolean onLongClick(View v) { + // check for time-based lockouts + if (mPasswordEntry.isEnabled()) { + mPasswordEntry.setText(""); + } + doHapticKeyClick(); + return true; + } + }); + } + + mPasswordEntry.setKeyListener(DigitsKeyListener.getInstance()); + mPasswordEntry.setInputType(InputType.TYPE_CLASS_NUMBER + | InputType.TYPE_NUMBER_VARIATION_PASSWORD); + + mPasswordEntry.requestFocus(); + } + + @Override + public void showUsabilityHint() { + } + + @Override + public int getWrongPasswordStringId() { + return R.string.kg_wrong_pin; + } +} diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardPasswordView.java b/packages/Keyguard/src/com/android/keyguard/KeyguardPasswordView.java new file mode 100644 index 0000000..d52c993 --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardPasswordView.java @@ -0,0 +1,208 @@ +/* + * 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.internal.policy.impl.keyguard; + +import android.app.admin.DevicePolicyManager; +import android.content.Context; +import android.content.res.Configuration; +import android.text.Editable; +import android.text.InputType; +import android.text.TextWatcher; +import android.text.method.DigitsKeyListener; +import android.text.method.TextKeyListener; +import android.util.AttributeSet; +import android.view.View; +import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.InputMethodSubtype; +import android.widget.TextView.OnEditorActionListener; + +import com.android.internal.R; +import com.android.internal.widget.PasswordEntryKeyboardHelper; +import com.android.internal.widget.PasswordEntryKeyboardView; + +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; + + InputMethodManager mImm; + + 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); + } + + protected void resetState() { + mSecurityMessageDisplay.setMessage(R.string.kg_password_instructions, false); + mPasswordEntry.setEnabled(true); + } + + @Override + protected int getPasswordTextViewId() { + return R.id.passwordEntry; + } + + @Override + public boolean needsInput() { + return true; + } + + @Override + public void onResume(int reason) { + super.onResume(reason); + mPasswordEntry.requestFocus(); + if (reason != KeyguardSecurityView.SCREEN_ON || mShowImeAtScreenOn) { + mImm.showSoftInput(mPasswordEntry, InputMethodManager.SHOW_IMPLICIT); + } + } + + @Override + public void onPause() { + super.onPause(); + mImm.hideSoftInputFromWindow(getWindowToken(), 0); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + boolean imeOrDeleteButtonVisible = false; + + mImm = (InputMethodManager) getContext().getSystemService( + Context.INPUT_METHOD_SERVICE); + + mPasswordEntry.setKeyListener(TextKeyListener.getInstance()); + mPasswordEntry.setInputType(InputType.TYPE_CLASS_TEXT + | InputType.TYPE_TEXT_VARIATION_PASSWORD); + + // Poke the wakelock any time the text is selected or modified + mPasswordEntry.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + mCallback.userActivity(0); // TODO: customize timeout for text? + } + }); + + mPasswordEntry.addTextChangedListener(new TextWatcher() { + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + public void afterTextChanged(Editable s) { + if (mCallback != null) { + mCallback.userActivity(0); + } + } + }); + + mPasswordEntry.requestFocus(); + + // If there's more than one IME, enable the IME switcher button + View switchImeButton = findViewById(R.id.switch_ime_button); + if (switchImeButton != null && hasMultipleEnabledIMEsOrSubtypes(mImm, false)) { + switchImeButton.setVisibility(View.VISIBLE); + imeOrDeleteButtonVisible = true; + switchImeButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + mCallback.userActivity(0); // Leave the screen on a bit longer + mImm.showInputMethodPicker(); + } + }); + } + + // If no icon is visible, reset the start margin on the password field so the text is + // still centered. + if (!imeOrDeleteButtonVisible) { + android.view.ViewGroup.LayoutParams params = mPasswordEntry.getLayoutParams(); + if (params instanceof MarginLayoutParams) { + final MarginLayoutParams mlp = (MarginLayoutParams) params; + mlp.setMarginStart(0); + mPasswordEntry.setLayoutParams(params); + } + } + } + + /** + * 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; + } +} diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardPatternView.java b/packages/Keyguard/src/com/android/keyguard/KeyguardPatternView.java new file mode 100644 index 0000000..e114b78 --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardPatternView.java @@ -0,0 +1,412 @@ +/* + * 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.internal.policy.impl.keyguard; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AccountManagerCallback; +import android.accounts.AccountManagerFuture; +import android.accounts.AuthenticatorException; +import android.accounts.OperationCanceledException; +import android.content.Context; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.CountDownTimer; +import android.os.SystemClock; +import android.os.UserHandle; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.widget.Button; +import android.widget.LinearLayout; + +import com.android.internal.widget.LockPatternUtils; +import com.android.internal.widget.LockPatternView; +import com.android.internal.R; + +import java.io.IOException; +import java.util.List; + +public class KeyguardPatternView extends LinearLayout implements KeyguardSecurityView { + + private static final String TAG = "SecurityPatternView"; + private static final boolean DEBUG = false; + + // 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 long we stay awake after the user hits the first dot. + private static final int UNLOCK_PATTERN_WAKE_INTERVAL_FIRST_DOTS_MS = 2000; + + // how many cells the user has to cross before we poke the wakelock + private static final int MIN_PATTERN_BEFORE_POKE_WAKELOCK = 2; + + private int mFailedPatternAttemptsSinceLastTimeout = 0; + private int mTotalFailedPatternAttempts = 0; + private CountDownTimer mCountdownTimer = null; + private LockPatternUtils mLockPatternUtils; + private LockPatternView mLockPatternView; + private Button mForgotPatternButton; + private KeyguardSecurityCallback mCallback; + private boolean mEnableFallback; + + /** + * 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() { + public void run() { + mLockPatternView.clearPattern(); + } + }; + private Rect mTempRect = new Rect(); + private SecurityMessageDisplay mSecurityMessageDisplay; + private View mEcaView; + private Drawable mBouncerFrame; + + enum FooterMode { + Normal, + ForgotLockPattern, + VerifyUnlocked + } + + public KeyguardPatternView(Context context) { + this(context, null); + } + + public KeyguardPatternView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void setKeyguardCallback(KeyguardSecurityCallback callback) { + mCallback = callback; + } + + 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.setFocusable(false); + mLockPatternView.setOnPatternListener(new UnlockPatternListener()); + + // stealth mode will be the same for the life of this screen + mLockPatternView.setInStealthMode(!mLockPatternUtils.isVisiblePatternEnabled()); + + // vibrate mode will be the same for the life of this screen + mLockPatternView.setTactileFeedbackEnabled(mLockPatternUtils.isTactileFeedbackEnabled()); + + mForgotPatternButton = (Button) findViewById(R.id.forgot_password_button); + // note: some configurations don't have an emergency call area + if (mForgotPatternButton != null) { + mForgotPatternButton.setText(R.string.kg_forgot_pattern_button_text); + mForgotPatternButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + mCallback.showBackupSecurity(); + } + }); + } + + setFocusableInTouchMode(true); + + maybeEnableFallback(mContext); + mSecurityMessageDisplay = new KeyguardMessageArea.Helper(this); + mEcaView = findViewById(R.id.keyguard_selector_fade_container); + View bouncerFrameView = findViewById(R.id.keyguard_bouncer_frame); + if (bouncerFrameView != null) { + mBouncerFrame = bouncerFrameView.getBackground(); + } + } + + private void updateFooter(FooterMode mode) { + if (mForgotPatternButton == null) return; // no ECA? no footer + + switch (mode) { + case Normal: + if (DEBUG) Log.d(TAG, "mode normal"); + mForgotPatternButton.setVisibility(View.GONE); + break; + case ForgotLockPattern: + if (DEBUG) Log.d(TAG, "mode ForgotLockPattern"); + mForgotPatternButton.setVisibility(View.VISIBLE); + break; + case VerifyUnlocked: + if (DEBUG) Log.d(TAG, "mode VerifyUnlocked"); + mForgotPatternButton.setVisibility(View.GONE); + } + } + + @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; + } + + public void reset() { + // reset lock pattern + mLockPatternView.enableInput(); + mLockPatternView.setEnabled(true); + mLockPatternView.clearPattern(); + + // if the user is currently locked out, enforce it. + long deadline = mLockPatternUtils.getLockoutAttemptDeadline(); + if (deadline != 0) { + handleAttemptLockout(deadline); + } else { + displayDefaultSecurityMessage(); + } + + // the footer depends on how many total attempts the user has failed + if (mCallback.isVerifyUnlockOnly()) { + updateFooter(FooterMode.VerifyUnlocked); + } else if (mEnableFallback && + (mTotalFailedPatternAttempts >= LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT)) { + updateFooter(FooterMode.ForgotLockPattern); + } else { + updateFooter(FooterMode.Normal); + } + + } + + private void displayDefaultSecurityMessage() { + if (KeyguardUpdateMonitor.getInstance(mContext).getMaxBiometricUnlockAttemptsReached()) { + mSecurityMessageDisplay.setMessage(R.string.faceunlock_multiple_failures, true); + } else { + mSecurityMessageDisplay.setMessage(R.string.kg_pattern_instructions, false); + } + } + + @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); + } + + @Override + public void onWindowFocusChanged(boolean hasWindowFocus) { + super.onWindowFocusChanged(hasWindowFocus); + if (hasWindowFocus) { + // when timeout dialog closes we want to update our state + reset(); + } + } + + private class UnlockPatternListener implements LockPatternView.OnPatternListener { + + public void onPatternStart() { + mLockPatternView.removeCallbacks(mCancelPatternRunnable); + } + + public void onPatternCleared() { + } + + public void onPatternCellAdded(List<LockPatternView.Cell> pattern) { + // To guard against accidental poking of the wakelock, look for + // the user actually trying to draw a pattern of some minimal length. + if (pattern.size() > MIN_PATTERN_BEFORE_POKE_WAKELOCK) { + mCallback.userActivity(UNLOCK_PATTERN_WAKE_INTERVAL_MS); + } else { + // Give just a little extra time if they hit one of the first few dots + mCallback.userActivity(UNLOCK_PATTERN_WAKE_INTERVAL_FIRST_DOTS_MS); + } + } + + public void onPatternDetected(List<LockPatternView.Cell> pattern) { + if (mLockPatternUtils.checkPattern(pattern)) { + mCallback.reportSuccessfulUnlockAttempt(); + mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Correct); + mTotalFailedPatternAttempts = 0; + mCallback.dismiss(true); + } else { + if (pattern.size() > MIN_PATTERN_BEFORE_POKE_WAKELOCK) { + mCallback.userActivity(UNLOCK_PATTERN_WAKE_INTERVAL_MS); + } + mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong); + if (pattern.size() >= LockPatternUtils.MIN_PATTERN_REGISTER_FAIL) { + mTotalFailedPatternAttempts++; + mFailedPatternAttemptsSinceLastTimeout++; + mCallback.reportFailedUnlockAttempt(); + } + if (mFailedPatternAttemptsSinceLastTimeout + >= LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT) { + long deadline = mLockPatternUtils.setLockoutAttemptDeadline(); + handleAttemptLockout(deadline); + } else { + mSecurityMessageDisplay.setMessage(R.string.kg_wrong_pattern, true); + mLockPatternView.postDelayed(mCancelPatternRunnable, PATTERN_CLEAR_TIMEOUT_MS); + } + } + } + } + + private void maybeEnableFallback(Context context) { + // Ask the account manager if we have an account that can be used as a + // fallback in case the user forgets his pattern. + AccountAnalyzer accountAnalyzer = new AccountAnalyzer(AccountManager.get(context)); + accountAnalyzer.start(); + } + + private class AccountAnalyzer implements AccountManagerCallback<Bundle> { + private final AccountManager mAccountManager; + private final Account[] mAccounts; + private int mAccountIndex; + + private AccountAnalyzer(AccountManager accountManager) { + mAccountManager = accountManager; + mAccounts = accountManager.getAccountsByTypeAsUser("com.google", + new UserHandle(mLockPatternUtils.getCurrentUser())); + } + + private void next() { + // if we are ready to enable the fallback or if we depleted the list of accounts + // then finish and get out + if (mEnableFallback || mAccountIndex >= mAccounts.length) { + return; + } + + // lookup the confirmCredentials intent for the current account + mAccountManager.confirmCredentialsAsUser(mAccounts[mAccountIndex], null, null, this, + null, new UserHandle(mLockPatternUtils.getCurrentUser())); + } + + public void start() { + mEnableFallback = false; + mAccountIndex = 0; + next(); + } + + public void run(AccountManagerFuture<Bundle> future) { + try { + Bundle result = future.getResult(); + if (result.getParcelable(AccountManager.KEY_INTENT) != null) { + mEnableFallback = true; + } + } catch (OperationCanceledException e) { + // just skip the account if we are unable to query it + } catch (IOException e) { + // just skip the account if we are unable to query it + } catch (AuthenticatorException e) { + // just skip the account if we are unable to query it + } finally { + mAccountIndex++; + next(); + } + } + } + + private void handleAttemptLockout(long elapsedRealtimeDeadline) { + mLockPatternView.clearPattern(); + mLockPatternView.setEnabled(false); + final long elapsedRealtime = SystemClock.elapsedRealtime(); + if (mEnableFallback) { + updateFooter(FooterMode.ForgotLockPattern); + } + + mCountdownTimer = new CountDownTimer(elapsedRealtimeDeadline - elapsedRealtime, 1000) { + + @Override + public void onTick(long millisUntilFinished) { + final int secondsRemaining = (int) (millisUntilFinished / 1000); + mSecurityMessageDisplay.setMessage( + R.string.kg_too_many_failed_attempts_countdown, true, secondsRemaining); + } + + @Override + public void onFinish() { + mLockPatternView.setEnabled(true); + displayDefaultSecurityMessage(); + // TODO mUnlockIcon.setVisibility(View.VISIBLE); + mFailedPatternAttemptsSinceLastTimeout = 0; + if (mEnableFallback) { + updateFooter(FooterMode.ForgotLockPattern); + } else { + updateFooter(FooterMode.Normal); + } + } + + }.start(); + } + + @Override + public boolean needsInput() { + return false; + } + + @Override + public void onPause() { + if (mCountdownTimer != null) { + mCountdownTimer.cancel(); + mCountdownTimer = null; + } + } + + @Override + public void onResume(int reason) { + reset(); + } + + @Override + public KeyguardSecurityCallback getCallback() { + return mCallback; + } + + @Override + public void showBouncer(int duration) { + KeyguardSecurityViewHelper. + showBouncer(mSecurityMessageDisplay, mEcaView, mBouncerFrame, duration); + } + + @Override + public void hideBouncer(int duration) { + KeyguardSecurityViewHelper. + hideBouncer(mSecurityMessageDisplay, mEcaView, mBouncerFrame, duration); + } +} diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardSecurityCallback.java b/packages/Keyguard/src/com/android/keyguard/KeyguardSecurityCallback.java new file mode 100644 index 0000000..7e6c108 --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardSecurityCallback.java @@ -0,0 +1,68 @@ +/* + * 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.internal.policy.impl.keyguard; + +import com.android.internal.policy.impl.keyguard.KeyguardHostView.OnDismissAction; + +public interface KeyguardSecurityCallback { + + /** + * Dismiss the given security screen. + * @param securityVerified true if the user correctly entered credentials for the given screen. + */ + void dismiss(boolean securityVerified); + + /** + * Manually report user activity to keep the device awake. If timeout is 0, + * uses user-defined timeout. + * @param timeout + */ + void userActivity(long timeout); + + /** + * Checks if keyguard is in "verify credentials" mode. + * @return true if user has been asked to verify security. + */ + boolean isVerifyUnlockOnly(); + + /** + * Call when user correctly enters their credentials + */ + void reportSuccessfulUnlockAttempt(); + + /** + * Call when the user incorrectly enters their credentials + */ + void reportFailedUnlockAttempt(); + + /** + * Gets the number of attempts thus far as reported by {@link #reportFailedUnlockAttempt()} + * @return number of failed attempts + */ + int getFailedAttempts(); + + /** + * Shows the backup security for the current method. If none available, this call is a no-op. + */ + void showBackupSecurity(); + + /** + * Sets an action to perform after the user successfully enters their credentials. + * @param action + */ + void setOnDismissAction(OnDismissAction action); + +} diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/Keyguard/src/com/android/keyguard/KeyguardSecurityContainer.java new file mode 100644 index 0000000..375a96a --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardSecurityContainer.java @@ -0,0 +1,47 @@ +package com.android.internal.policy.impl.keyguard; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.FrameLayout; + +import com.android.internal.R; + +public class KeyguardSecurityContainer extends FrameLayout { + public KeyguardSecurityContainer(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public KeyguardSecurityContainer(Context context) { + this(null, null, 0); + } + + public KeyguardSecurityContainer(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + KeyguardSecurityViewFlipper getFlipper() { + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if (child instanceof KeyguardSecurityViewFlipper) { + return (KeyguardSecurityViewFlipper) child; + } + } + return null; + } + + public void showBouncer(int duration) { + KeyguardSecurityViewFlipper flipper = getFlipper(); + if (flipper != null) { + flipper.showBouncer(duration); + } + } + + public void hideBouncer(int duration) { + KeyguardSecurityViewFlipper flipper = getFlipper(); + if (flipper != null) { + flipper.hideBouncer(duration); + } + } +} + diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardSecurityModel.java b/packages/Keyguard/src/com/android/keyguard/KeyguardSecurityModel.java new file mode 100644 index 0000000..7a69586 --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardSecurityModel.java @@ -0,0 +1,147 @@ +/* + * 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.internal.policy.impl.keyguard; + +import android.app.admin.DevicePolicyManager; +import android.content.Context; +import android.telephony.TelephonyManager; + +import com.android.internal.telephony.IccCardConstants; +import com.android.internal.widget.LockPatternUtils; + +public class KeyguardSecurityModel { + /** + * The different types of security available for {@link Mode#UnlockScreen}. + * @see com.android.internal.policy.impl.LockPatternKeyguardView#getUnlockMode() + */ + 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 + Biometric, // Unlock with a biometric key (e.g. finger print or face unlock) + Account, // Unlock by entering an account's login and password. + SimPin, // Unlock by entering a sim pin. + SimPuk // Unlock by entering a sim puk + } + + private Context mContext; + private LockPatternUtils mLockPatternUtils; + + KeyguardSecurityModel(Context context) { + mContext = context; + mLockPatternUtils = new LockPatternUtils(context); + } + + void setLockPatternUtils(LockPatternUtils utils) { + mLockPatternUtils = utils; + } + + /** + * Returns true if biometric unlock is installed and selected. If this returns false there is + * no need to even construct the biometric unlock. + */ + boolean isBiometricUnlockEnabled() { + return mLockPatternUtils.usingBiometricWeak() + && mLockPatternUtils.isBiometricWeakInstalled(); + } + + /** + * Returns true if a condition is currently suppressing the biometric unlock. If this returns + * true there is no need to even construct the biometric unlock. + */ + private boolean isBiometricUnlockSuppressed() { + KeyguardUpdateMonitor monitor = KeyguardUpdateMonitor.getInstance(mContext); + final boolean backupIsTimedOut = monitor.getFailedUnlockAttempts() >= + LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT; + return monitor.getMaxBiometricUnlockAttemptsReached() || backupIsTimedOut + || !monitor.isAlternateUnlockEnabled() + || monitor.getPhoneState() != TelephonyManager.CALL_STATE_IDLE; + } + + SecurityMode getSecurityMode() { + KeyguardUpdateMonitor updateMonitor = KeyguardUpdateMonitor.getInstance(mContext); + final IccCardConstants.State simState = updateMonitor.getSimState(); + SecurityMode mode = SecurityMode.None; + if (simState == IccCardConstants.State.PIN_REQUIRED) { + mode = SecurityMode.SimPin; + } else if (simState == IccCardConstants.State.PUK_REQUIRED + && mLockPatternUtils.isPukUnlockScreenEnable()) { + mode = SecurityMode.SimPuk; + } else { + final int security = mLockPatternUtils.getKeyguardStoredPasswordQuality(); + switch (security) { + case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC: + mode = mLockPatternUtils.isLockPasswordEnabled() ? + SecurityMode.PIN : SecurityMode.None; + break; + case DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC: + case DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC: + case DevicePolicyManager.PASSWORD_QUALITY_COMPLEX: + mode = mLockPatternUtils.isLockPasswordEnabled() ? + SecurityMode.Password : SecurityMode.None; + break; + + case DevicePolicyManager.PASSWORD_QUALITY_SOMETHING: + case DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED: + if (mLockPatternUtils.isLockPatternEnabled()) { + mode = mLockPatternUtils.isPermanentlyLocked() ? + SecurityMode.Account : SecurityMode.Pattern; + } + break; + + default: + throw new IllegalStateException("Unknown unlock mode:" + mode); + } + } + return mode; + } + + /** + * Some unlock methods can have an alternate, such as biometric unlocks (e.g. face unlock). + * This function decides if an alternate unlock is available and returns it. Otherwise, + * returns @param mode. + * + * @param mode the mode we want the alternate for + * @return alternate or the given mode + */ + SecurityMode getAlternateFor(SecurityMode mode) { + if (isBiometricUnlockEnabled() && !isBiometricUnlockSuppressed() + && (mode == SecurityMode.Password + || mode == SecurityMode.PIN + || mode == SecurityMode.Pattern)) { + return SecurityMode.Biometric; + } + return mode; // no alternate, return what was given + } + + /** + * Some unlock methods can have a backup which gives the user another way to get into + * the device. This is currently only supported for Biometric and Pattern unlock. + * + * @return backup method or current security mode + */ + SecurityMode getBackupSecurityMode(SecurityMode mode) { + switch(mode) { + case Biometric: + return getSecurityMode(); + case Pattern: + return SecurityMode.Account; + } + return mode; // no backup, return current security mode + } +} diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardSecurityView.java b/packages/Keyguard/src/com/android/keyguard/KeyguardSecurityView.java new file mode 100644 index 0000000..a3ac39c --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardSecurityView.java @@ -0,0 +1,87 @@ +/* + * 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.internal.policy.impl.keyguard; + +import com.android.internal.widget.LockPatternUtils; + +public interface KeyguardSecurityView { + static public final int SCREEN_ON = 1; + static public final int VIEW_REVEALED = 2; + + /** + * 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(); + + /** + * Instruct the view to show usability hints, if any. + * + */ + void showUsabilityHint(); + + /** + * Place the security view into bouncer mode. + * Animate transisiton if duration is non-zero. + * @param duration millisends for the transisiton animation. + */ + void showBouncer(int duration); + + /** + * Place the security view into non-bouncer mode. + * Animate transisiton if duration is non-zero. + * @param duration millisends for the transisiton animation. + */ + void hideBouncer(int duration); +} diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardSecurityViewFlipper.java b/packages/Keyguard/src/com/android/keyguard/KeyguardSecurityViewFlipper.java new file mode 100644 index 0000000..aa31b00 --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardSecurityViewFlipper.java @@ -0,0 +1,279 @@ +/* + * 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.internal.policy.impl.keyguard; + +import com.android.internal.R; +import com.android.internal.widget.LockPatternUtils; + +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.WindowManager; +import android.widget.FrameLayout; +import android.widget.ViewFlipper; + +/** + * 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 = false; + + 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 showUsabilityHint() { + KeyguardSecurityView ksv = getSecurityView(); + if (ksv != null) { + ksv.showUsabilityHint(); + } + } + + @Override + public void showBouncer(int duration) { + KeyguardSecurityView active = getSecurityView(); + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if (child instanceof KeyguardSecurityView) { + KeyguardSecurityView ksv = (KeyguardSecurityView) child; + ksv.showBouncer(ksv == active ? duration : 0); + } + } + } + + @Override + public void hideBouncer(int duration) { + KeyguardSecurityView active = getSecurityView(); + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if (child instanceof KeyguardSecurityView) { + KeyguardSecurityView ksv = (KeyguardSecurityView) child; + ksv.hideBouncer(ksv == active ? duration : 0); + } + } + } + + @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 -= wPadding; + 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(); + } + } +} diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardSecurityViewHelper.java b/packages/Keyguard/src/com/android/keyguard/KeyguardSecurityViewHelper.java new file mode 100644 index 0000000..3d59f8d --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardSecurityViewHelper.java @@ -0,0 +1,94 @@ +/* + * 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.internal.policy.impl.keyguard; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.graphics.drawable.Drawable; +import android.view.View; + +/** + * Some common functions that are useful for KeyguardSecurityViews. + */ +public class KeyguardSecurityViewHelper { + + public static void showBouncer(SecurityMessageDisplay securityMessageDisplay, + final View ecaView, Drawable bouncerFrame, int duration) { + if (securityMessageDisplay != null) { + securityMessageDisplay.showBouncer(duration); + } + if (ecaView != null) { + if (duration > 0) { + Animator anim = ObjectAnimator.ofFloat(ecaView, "alpha", 0f); + anim.setDuration(duration); + anim.addListener(new AnimatorListenerAdapter() { + private boolean mCanceled; + @Override + public void onAnimationCancel(Animator animation) { + // Fail safe and show the emergency button in onAnimationEnd() + mCanceled = true; + ecaView.setAlpha(1f); + } + @Override + public void onAnimationEnd(Animator animation) { + ecaView.setVisibility(mCanceled ? View.VISIBLE : View.INVISIBLE); + } + }); + anim.start(); + } else { + ecaView.setAlpha(0f); + ecaView.setVisibility(View.INVISIBLE); + } + } + if (bouncerFrame != null) { + if (duration > 0) { + Animator anim = ObjectAnimator.ofInt(bouncerFrame, "alpha", 0, 255); + anim.setDuration(duration); + anim.start(); + } else { + bouncerFrame.setAlpha(255); + } + } + } + + public static void hideBouncer(SecurityMessageDisplay securityMessageDisplay, + View ecaView, Drawable bouncerFrame, int duration) { + if (securityMessageDisplay != null) { + securityMessageDisplay.hideBouncer(duration); + } + if (ecaView != null) { + ecaView.setVisibility(View.VISIBLE); + if (duration > 0) { + Animator anim = ObjectAnimator.ofFloat(ecaView, "alpha", 1f); + anim.setDuration(duration); + anim.start(); + } else { + ecaView.setAlpha(1f); + } + } + if (bouncerFrame != null) { + if (duration > 0) { + Animator anim = ObjectAnimator.ofInt(bouncerFrame, "alpha", 255, 0); + anim.setDuration(duration); + anim.start(); + } else { + bouncerFrame.setAlpha(0); + } + } + } +} diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardSelectorView.java b/packages/Keyguard/src/com/android/keyguard/KeyguardSelectorView.java new file mode 100644 index 0000000..6859042 --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardSelectorView.java @@ -0,0 +1,288 @@ +/* + * 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.internal.policy.impl.keyguard; + +import android.animation.ObjectAnimator; +import android.app.SearchManager; +import android.app.admin.DevicePolicyManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.AttributeSet; +import android.util.Log; +import android.util.Slog; +import android.view.View; +import android.widget.LinearLayout; + +import com.android.internal.telephony.IccCardConstants.State; +import com.android.internal.widget.LockPatternUtils; +import com.android.internal.widget.multiwaveview.GlowPadView; +import com.android.internal.widget.multiwaveview.GlowPadView.OnTriggerListener; +import com.android.internal.R; + +public class KeyguardSelectorView extends LinearLayout implements KeyguardSecurityView { + private static final boolean DEBUG = KeyguardHostView.DEBUG; + private static final String TAG = "SecuritySelectorView"; + private static final String ASSIST_ICON_METADATA_NAME = + "com.android.systemui.action_assist_icon"; + + private KeyguardSecurityCallback mCallback; + private GlowPadView mGlowPadView; + private ObjectAnimator mAnim; + private View mFadeView; + private boolean mIsBouncing; + private boolean mCameraDisabled; + private boolean mSearchDisabled; + private LockPatternUtils mLockPatternUtils; + private SecurityMessageDisplay mSecurityMessageDisplay; + private Drawable mBouncerFrame; + + OnTriggerListener mOnTriggerListener = new OnTriggerListener() { + + public void onTrigger(View v, int target) { + final int resId = mGlowPadView.getResourceIdForTarget(target); + switch (resId) { + case com.android.internal.R.drawable.ic_action_assist_generic: + Intent assistIntent = + ((SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE)) + .getAssistIntent(mContext, true, UserHandle.USER_CURRENT); + if (assistIntent != null) { + mActivityLauncher.launchActivity(assistIntent, false, true, null, null); + } else { + Log.w(TAG, "Failed to get intent for assist activity"); + } + mCallback.userActivity(0); + break; + + case com.android.internal.R.drawable.ic_lockscreen_camera: + mActivityLauncher.launchCamera(null, null); + mCallback.userActivity(0); + break; + + case com.android.internal.R.drawable.ic_lockscreen_unlock_phantom: + case com.android.internal.R.drawable.ic_lockscreen_unlock: + mCallback.userActivity(0); + mCallback.dismiss(false); + break; + } + } + + public void onReleased(View v, int handle) { + if (!mIsBouncing) { + doTransition(mFadeView, 1.0f); + } + } + + public void onGrabbed(View v, int handle) { + mCallback.userActivity(0); + doTransition(mFadeView, 0.0f); + } + + public void onGrabbedStateChange(View v, int handle) { + + } + + public void onFinishFinalAnimation() { + + } + + }; + + KeyguardUpdateMonitorCallback mInfoCallback = new KeyguardUpdateMonitorCallback() { + + @Override + public void onDevicePolicyManagerStateChanged() { + updateTargets(); + } + + @Override + public void onSimStateChanged(State simState) { + updateTargets(); + } + }; + + private final KeyguardActivityLauncher mActivityLauncher = new KeyguardActivityLauncher() { + + @Override + KeyguardSecurityCallback getCallback() { + return mCallback; + } + + @Override + LockPatternUtils getLockPatternUtils() { + return mLockPatternUtils; + } + + @Override + Context getContext() { + return mContext; + }}; + + public KeyguardSelectorView(Context context) { + this(context, null); + } + + public KeyguardSelectorView(Context context, AttributeSet attrs) { + super(context, attrs); + mLockPatternUtils = new LockPatternUtils(getContext()); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mGlowPadView = (GlowPadView) findViewById(R.id.glow_pad_view); + mGlowPadView.setOnTriggerListener(mOnTriggerListener); + updateTargets(); + + mSecurityMessageDisplay = new KeyguardMessageArea.Helper(this); + View bouncerFrameView = findViewById(R.id.keyguard_selector_view_frame); + mBouncerFrame = bouncerFrameView.getBackground(); + } + + public void setCarrierArea(View carrierArea) { + mFadeView = carrierArea; + } + + public boolean isTargetPresent(int resId) { + return mGlowPadView.getTargetPosition(resId) != -1; + } + + @Override + public void showUsabilityHint() { + mGlowPadView.ping(); + } + + private void updateTargets() { + int currentUserHandle = mLockPatternUtils.getCurrentUser(); + DevicePolicyManager dpm = mLockPatternUtils.getDevicePolicyManager(); + int disabledFeatures = dpm.getKeyguardDisabledFeatures(null, currentUserHandle); + boolean secureCameraDisabled = mLockPatternUtils.isSecure() + && (disabledFeatures & DevicePolicyManager.KEYGUARD_DISABLE_SECURE_CAMERA) != 0; + boolean cameraDisabledByAdmin = dpm.getCameraDisabled(null, currentUserHandle) + || secureCameraDisabled; + final KeyguardUpdateMonitor monitor = KeyguardUpdateMonitor.getInstance(getContext()); + boolean disabledBySimState = monitor.isSimLocked(); + boolean cameraTargetPresent = + isTargetPresent(com.android.internal.R.drawable.ic_lockscreen_camera); + boolean searchTargetPresent = + isTargetPresent(com.android.internal.R.drawable.ic_action_assist_generic); + + if (cameraDisabledByAdmin) { + Log.v(TAG, "Camera disabled by Device Policy"); + } else if (disabledBySimState) { + Log.v(TAG, "Camera disabled by Sim State"); + } + boolean currentUserSetup = 0 != Settings.Secure.getIntForUser( + mContext.getContentResolver(), + Settings.Secure.USER_SETUP_COMPLETE, + 0 /*default */, + currentUserHandle); + boolean searchActionAvailable = + ((SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE)) + .getAssistIntent(mContext, false, UserHandle.USER_CURRENT) != null; + mCameraDisabled = cameraDisabledByAdmin || disabledBySimState || !cameraTargetPresent + || !currentUserSetup; + mSearchDisabled = disabledBySimState || !searchActionAvailable || !searchTargetPresent + || !currentUserSetup; + updateResources(); + } + + public void updateResources() { + // Update the search icon with drawable from the search .apk + if (!mSearchDisabled) { + Intent intent = ((SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE)) + .getAssistIntent(mContext, false, UserHandle.USER_CURRENT); + if (intent != null) { + // XXX Hack. We need to substitute the icon here but haven't formalized + // the public API. The "_google" metadata will be going away, so + // DON'T USE IT! + ComponentName component = intent.getComponent(); + boolean replaced = mGlowPadView.replaceTargetDrawablesIfPresent(component, + ASSIST_ICON_METADATA_NAME + "_google", + com.android.internal.R.drawable.ic_action_assist_generic); + + if (!replaced && !mGlowPadView.replaceTargetDrawablesIfPresent(component, + ASSIST_ICON_METADATA_NAME, + com.android.internal.R.drawable.ic_action_assist_generic)) { + Slog.w(TAG, "Couldn't grab icon from package " + component); + } + } + } + + mGlowPadView.setEnableTarget(com.android.internal.R.drawable + .ic_lockscreen_camera, !mCameraDisabled); + mGlowPadView.setEnableTarget(com.android.internal.R.drawable + .ic_action_assist_generic, !mSearchDisabled); + } + + void doTransition(View view, float to) { + if (mAnim != null) { + mAnim.cancel(); + } + mAnim = ObjectAnimator.ofFloat(view, "alpha", to); + mAnim.start(); + } + + public void setKeyguardCallback(KeyguardSecurityCallback callback) { + mCallback = callback; + } + + public void setLockPatternUtils(LockPatternUtils utils) { + mLockPatternUtils = utils; + } + + @Override + public void reset() { + mGlowPadView.reset(false); + } + + @Override + public boolean needsInput() { + return false; + } + + @Override + public void onPause() { + KeyguardUpdateMonitor.getInstance(getContext()).removeCallback(mInfoCallback); + } + + @Override + public void onResume(int reason) { + KeyguardUpdateMonitor.getInstance(getContext()).registerCallback(mInfoCallback); + } + + @Override + public KeyguardSecurityCallback getCallback() { + return mCallback; + } + + @Override + public void showBouncer(int duration) { + mIsBouncing = true; + KeyguardSecurityViewHelper. + showBouncer(mSecurityMessageDisplay, mFadeView, mBouncerFrame, duration); + } + + @Override + public void hideBouncer(int duration) { + mIsBouncing = false; + KeyguardSecurityViewHelper. + hideBouncer(mSecurityMessageDisplay, mFadeView, mBouncerFrame, duration); + } +} diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardSimPinView.java b/packages/Keyguard/src/com/android/keyguard/KeyguardSimPinView.java new file mode 100644 index 0000000..ab364ee --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardSimPinView.java @@ -0,0 +1,213 @@ +/* + * 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.internal.policy.impl.keyguard; + +import com.android.internal.telephony.ITelephony; + +import android.content.Context; +import android.app.Activity; +import android.app.Dialog; +import android.app.ProgressDialog; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.text.Editable; +import android.text.InputType; +import android.text.TextWatcher; +import android.text.method.DigitsKeyListener; +import android.util.AttributeSet; +import android.view.View; +import android.view.WindowManager; +import android.widget.TextView.OnEditorActionListener; + +import com.android.internal.R; + +/** + * Displays a PIN pad for unlocking. + */ +public class KeyguardSimPinView extends KeyguardAbsKeyInputView + implements KeyguardSecurityView, OnEditorActionListener, TextWatcher { + + private ProgressDialog mSimUnlockProgressDialog = null; + private volatile boolean mSimCheckInProgress; + + public KeyguardSimPinView(Context context) { + this(context, null); + } + + public KeyguardSimPinView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void resetState() { + mSecurityMessageDisplay.setMessage(R.string.kg_sim_pin_instructions, true); + mPasswordEntry.setEnabled(true); + } + + @Override + protected int getPasswordTextViewId() { + return R.id.pinEntry; + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + final View ok = findViewById(R.id.key_enter); + if (ok != null) { + ok.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + doHapticKeyClick(); + verifyPasswordAndUnlock(); + } + }); + } + + // The delete button is of the PIN keyboard itself in some (e.g. tablet) layouts, + // not a separate view + View pinDelete = findViewById(R.id.delete_button); + if (pinDelete != null) { + pinDelete.setVisibility(View.VISIBLE); + pinDelete.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + CharSequence str = mPasswordEntry.getText(); + if (str.length() > 0) { + mPasswordEntry.setText(str.subSequence(0, str.length()-1)); + } + doHapticKeyClick(); + } + }); + pinDelete.setOnLongClickListener(new View.OnLongClickListener() { + public boolean onLongClick(View v) { + mPasswordEntry.setText(""); + doHapticKeyClick(); + return true; + } + }); + } + + mPasswordEntry.setKeyListener(DigitsKeyListener.getInstance()); + mPasswordEntry.setInputType(InputType.TYPE_CLASS_NUMBER + | InputType.TYPE_NUMBER_VARIATION_PASSWORD); + + mPasswordEntry.requestFocus(); + } + + @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; + + protected CheckSimPin(String pin) { + mPin = pin; + } + + abstract void onSimCheckResponse(boolean success); + + @Override + public void run() { + try { + final boolean result = ITelephony.Stub.asInterface(ServiceManager + .checkService("phone")).supplyPin(mPin); + post(new Runnable() { + public void run() { + onSimCheckResponse(result); + } + }); + } catch (RemoteException e) { + post(new Runnable() { + public void run() { + onSimCheckResponse(false); + } + }); + } + } + } + + 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; + } + + @Override + protected void verifyPasswordAndUnlock() { + String entry = mPasswordEntry.getText().toString(); + + if (entry.length() < 4) { + // otherwise, display a message to the user, and don't submit. + mSecurityMessageDisplay.setMessage(R.string.kg_invalid_sim_pin_hint, true); + mPasswordEntry.setText(""); + mCallback.userActivity(0); + return; + } + + getSimUnlockProgressDialog().show(); + + if (!mSimCheckInProgress) { + mSimCheckInProgress = true; // there should be only one + new CheckSimPin(mPasswordEntry.getText().toString()) { + void onSimCheckResponse(final boolean success) { + post(new Runnable() { + public void run() { + if (mSimUnlockProgressDialog != null) { + mSimUnlockProgressDialog.hide(); + } + if (success) { + // before closing the keyguard, report back that the sim is unlocked + // so it knows right away. + KeyguardUpdateMonitor.getInstance(getContext()).reportSimUnlocked(); + mCallback.dismiss(true); + } else { + mSecurityMessageDisplay.setMessage + (R.string.kg_password_wrong_pin_code, true); + mPasswordEntry.setText(""); + } + mCallback.userActivity(0); + mSimCheckInProgress = false; + } + }); + } + }.start(); + } + } +} + diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardSimPukView.java b/packages/Keyguard/src/com/android/keyguard/KeyguardSimPukView.java new file mode 100644 index 0000000..e5b4b73 --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardSimPukView.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.internal.policy.impl.keyguard; + +import android.app.Activity; +import android.app.Dialog; +import android.app.ProgressDialog; +import android.content.Context; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.text.Editable; +import android.text.InputType; +import android.text.TextWatcher; +import android.text.method.DigitsKeyListener; +import android.util.AttributeSet; +import android.view.View; +import android.view.WindowManager; +import android.widget.TextView.OnEditorActionListener; + +import com.android.internal.telephony.ITelephony; + +import com.android.internal.R; + +/** + * Displays a PIN pad for entering a PUK (Pin Unlock Kode) provided by a carrier. + */ +public class KeyguardSimPukView extends KeyguardAbsKeyInputView + implements KeyguardSecurityView, OnEditorActionListener, TextWatcher { + + private ProgressDialog mSimUnlockProgressDialog = null; + private volatile boolean mCheckInProgress; + private String mPukText; + private String mPinText; + private StateMachine mStateMachine = new StateMachine(); + + 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 = + com.android.internal.R.string.lockscreen_sim_unlock_progress_dialog_message; + updateSim(); + } else { + state = ENTER_PIN; // try again? + msg = R.string.kg_invalid_confirm_pin_hint; + } + } + mPasswordEntry.setText(null); + if (msg != 0) { + mSecurityMessageDisplay.setMessage(msg, true); + } + } + + void reset() { + mPinText=""; + mPukText=""; + state = ENTER_PUK; + mSecurityMessageDisplay.setMessage(R.string.kg_puk_enter_puk_hint, true); + mPasswordEntry.requestFocus(); + } + } + + public KeyguardSimPukView(Context context) { + this(context, null); + } + + public KeyguardSimPukView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void resetState() { + mStateMachine.reset(); + mPasswordEntry.setEnabled(true); + } + + @Override + protected int getPasswordTextViewId() { + return R.id.pinEntry; + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + final View ok = findViewById(R.id.key_enter); + if (ok != null) { + ok.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + doHapticKeyClick(); + verifyPasswordAndUnlock(); + } + }); + } + + // The delete button is of the PIN keyboard itself in some (e.g. tablet) layouts, + // not a separate view + View pinDelete = findViewById(R.id.delete_button); + if (pinDelete != null) { + pinDelete.setVisibility(View.VISIBLE); + pinDelete.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + CharSequence str = mPasswordEntry.getText(); + if (str.length() > 0) { + mPasswordEntry.setText(str.subSequence(0, str.length()-1)); + } + doHapticKeyClick(); + } + }); + pinDelete.setOnLongClickListener(new View.OnLongClickListener() { + public boolean onLongClick(View v) { + mPasswordEntry.setText(""); + doHapticKeyClick(); + return true; + } + }); + } + + mPasswordEntry.setKeyListener(DigitsKeyListener.getInstance()); + mPasswordEntry.setInputType(InputType.TYPE_CLASS_NUMBER + | InputType.TYPE_NUMBER_VARIATION_PASSWORD); + + mPasswordEntry.requestFocus(); + + mSecurityMessageDisplay.setTimeout(0); // don't show ownerinfo/charging status by default + } + + @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; + + protected CheckSimPuk(String puk, String pin) { + mPuk = puk; + mPin = pin; + } + + abstract void onSimLockChangedResponse(boolean success); + + @Override + public void run() { + try { + final boolean result = ITelephony.Stub.asInterface(ServiceManager + .checkService("phone")).supplyPuk(mPuk, mPin); + + post(new Runnable() { + public void run() { + onSimLockChangedResponse(result); + } + }); + } catch (RemoteException e) { + post(new Runnable() { + public void run() { + onSimLockChangedResponse(false); + } + }); + } + } + } + + 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 boolean checkPuk() { + // make sure the puk is at least 8 digits long. + if (mPasswordEntry.getText().length() >= 8) { + mPukText = mPasswordEntry.getText().toString(); + 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().toString(); + return true; + } + return false; + } + + public boolean confirmPin() { + return mPinText.equals(mPasswordEntry.getText().toString()); + } + + private void updateSim() { + getSimUnlockProgressDialog().show(); + + if (!mCheckInProgress) { + mCheckInProgress = true; + new CheckSimPuk(mPukText, mPinText) { + void onSimLockChangedResponse(final boolean success) { + post(new Runnable() { + public void run() { + if (mSimUnlockProgressDialog != null) { + mSimUnlockProgressDialog.hide(); + } + if (success) { + mCallback.dismiss(true); + } else { + mStateMachine.reset(); + mSecurityMessageDisplay.setMessage(R.string.kg_invalid_puk, true); + } + mCheckInProgress = false; + } + }); + } + }.start(); + } + } + + @Override + protected void verifyPasswordAndUnlock() { + mStateMachine.next(); + } +} + + diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardStatusView.java b/packages/Keyguard/src/com/android/keyguard/KeyguardStatusView.java new file mode 100644 index 0000000..d938cec --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardStatusView.java @@ -0,0 +1,156 @@ +/* + * 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.internal.policy.impl.keyguard; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Typeface; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Slog; +import android.view.View; +import android.widget.GridLayout; +import android.widget.TextView; + +import com.android.internal.R; +import com.android.internal.widget.LockPatternUtils; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +import libcore.icu.ICU; + +public class KeyguardStatusView extends GridLayout { + private static final boolean DEBUG = KeyguardViewMediator.DEBUG; + private static final String TAG = "KeyguardStatusView"; + + public static final int LOCK_ICON = 0; // R.drawable.ic_lock_idle_lock; + public static final int ALARM_ICON = com.android.internal.R.drawable.ic_lock_idle_alarm; + public static final int CHARGING_ICON = 0; //R.drawable.ic_lock_idle_charging; + public static final int BATTERY_LOW_ICON = 0; //R.drawable.ic_lock_idle_low_battery; + + private SimpleDateFormat mDateFormat; + private LockPatternUtils mLockPatternUtils; + + private TextView mDateView; + private TextView mAlarmStatusView; + private ClockView mClockView; + + private KeyguardUpdateMonitorCallback mInfoCallback = new KeyguardUpdateMonitorCallback() { + + @Override + public void onTimeChanged() { + refresh(); + } + + @Override + void onKeyguardVisibilityChanged(boolean showing) { + if (showing) { + if (DEBUG) Slog.v(TAG, "refresh statusview showing:" + showing); + refresh(); + } + }; + }; + + 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); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + Resources res = getContext().getResources(); + final Locale locale = Locale.getDefault(); + final String datePattern = + res.getString(com.android.internal.R.string.system_ui_date_pattern); + final String bestFormat = ICU.getBestDateTimePattern(datePattern, locale.toString()); + mDateFormat = new SimpleDateFormat(bestFormat, locale); + mDateView = (TextView) findViewById(R.id.date); + mAlarmStatusView = (TextView) findViewById(R.id.alarm_status); + mClockView = (ClockView) findViewById(R.id.clock_view); + mLockPatternUtils = new LockPatternUtils(getContext()); + + // Use custom font in mDateView + mDateView.setTypeface(Typeface.SANS_SERIF, Typeface.BOLD); + + // Required to get Marquee to work. + final View marqueeViews[] = { mDateView, mAlarmStatusView }; + for (int i = 0; i < marqueeViews.length; i++) { + View v = marqueeViews[i]; + if (v == null) { + throw new RuntimeException("Can't find widget at index " + i); + } + v.setSelected(true); + } + refresh(); + } + + protected void refresh() { + mClockView.updateTime(); + refreshDate(); + refreshAlarmStatus(); // might as well + } + + void refreshAlarmStatus() { + // Update Alarm status + String nextAlarm = mLockPatternUtils.getNextAlarm(); + if (!TextUtils.isEmpty(nextAlarm)) { + maybeSetUpperCaseText(mAlarmStatusView, nextAlarm); + mAlarmStatusView.setCompoundDrawablesWithIntrinsicBounds(ALARM_ICON, 0, 0, 0); + mAlarmStatusView.setVisibility(View.VISIBLE); + } else { + mAlarmStatusView.setVisibility(View.GONE); + } + } + + void refreshDate() { + maybeSetUpperCaseText(mDateView, mDateFormat.format(new Date())); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + KeyguardUpdateMonitor.getInstance(mContext).registerCallback(mInfoCallback); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + KeyguardUpdateMonitor.getInstance(mContext).removeCallback(mInfoCallback); + } + + public int getAppWidgetId() { + return LockPatternUtils.ID_DEFAULT_STATUS_WIDGET; + } + + private void maybeSetUpperCaseText(TextView textView, CharSequence text) { + if (KeyguardViewManager.USE_UPPER_CASE) { + textView.setText(text != null ? text.toString().toUpperCase() : null); + } else { + textView.setText(text); + } + } +} diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardTransportControlView.java b/packages/Keyguard/src/com/android/keyguard/KeyguardTransportControlView.java new file mode 100644 index 0000000..ffa88d5 --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardTransportControlView.java @@ -0,0 +1,520 @@ +/* + * 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.internal.policy.impl.keyguard; + +import android.app.PendingIntent; +import android.app.PendingIntent.CanceledException; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.media.AudioManager; +import android.media.IRemoteControlDisplay; +import android.media.MediaMetadataRetriever; +import android.media.RemoteControlClient; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.RemoteException; +import android.os.SystemClock; +import android.text.Spannable; +import android.text.TextUtils; +import android.text.style.ForegroundColorSpan; +import android.util.AttributeSet; +import android.util.Log; +import android.view.KeyEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.internal.R; + +import java.lang.ref.WeakReference; +/** + * This is the widget responsible for showing music controls in keyguard. + */ +public class KeyguardTransportControlView extends FrameLayout implements OnClickListener { + + private static final int MSG_UPDATE_STATE = 100; + private static final int MSG_SET_METADATA = 101; + private static final int MSG_SET_TRANSPORT_CONTROLS = 102; + private static final int MSG_SET_ARTWORK = 103; + private static final int MSG_SET_GENERATION_ID = 104; + private static final int DISPLAY_TIMEOUT_MS = 5000; // 5s + protected static final boolean DEBUG = false; + protected static final String TAG = "TransportControlView"; + + private ImageView mAlbumArt; + private TextView mTrackTitle; + private ImageView mBtnPrev; + private ImageView mBtnPlay; + private ImageView mBtnNext; + private int mClientGeneration; + private Metadata mMetadata = new Metadata(); + private boolean mAttached; + private PendingIntent mClientIntent; + private int mTransportControlFlags; + private int mCurrentPlayState; + private AudioManager mAudioManager; + private IRemoteControlDisplayWeak mIRCD; + private boolean mMusicClientPresent = true; + + /** + * The metadata which should be populated into the view once we've been attached + */ + private Bundle mPopulateMetadataWhenAttached = null; + + // This handler is required to ensure messages from IRCD are handled in sequence and on + // the UI thread. + private Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_UPDATE_STATE: + if (mClientGeneration == msg.arg1) updatePlayPauseState(msg.arg2); + break; + + case MSG_SET_METADATA: + if (mClientGeneration == msg.arg1) updateMetadata((Bundle) msg.obj); + break; + + case MSG_SET_TRANSPORT_CONTROLS: + if (mClientGeneration == msg.arg1) updateTransportControls(msg.arg2); + break; + + case MSG_SET_ARTWORK: + if (mClientGeneration == msg.arg1) { + if (mMetadata.bitmap != null) { + mMetadata.bitmap.recycle(); + } + mMetadata.bitmap = (Bitmap) msg.obj; + mAlbumArt.setImageBitmap(mMetadata.bitmap); + } + break; + + case MSG_SET_GENERATION_ID: + if (msg.arg2 != 0) { + // This means nobody is currently registered. Hide the view. + onListenerDetached(); + } else { + onListenerAttached(); + } + if (DEBUG) Log.v(TAG, "New genId = " + msg.arg1 + ", clearing = " + msg.arg2); + mClientGeneration = msg.arg1; + mClientIntent = (PendingIntent) msg.obj; + break; + + } + } + }; + private KeyguardHostView.TransportCallback mTransportCallback; + + /** + * This class is required to have weak linkage to the current TransportControlView + * because the remote process can hold a strong reference to this binder object and + * we can't predict when it will be GC'd in the remote process. Without this code, it + * would allow a heavyweight object to be held on this side of the binder when there's + * no requirement to run a GC on the other side. + */ + private static class IRemoteControlDisplayWeak extends IRemoteControlDisplay.Stub { + private WeakReference<Handler> mLocalHandler; + + IRemoteControlDisplayWeak(Handler handler) { + mLocalHandler = new WeakReference<Handler>(handler); + } + + public void setPlaybackState(int generationId, int state, long stateChangeTimeMs) { + Handler handler = mLocalHandler.get(); + if (handler != null) { + handler.obtainMessage(MSG_UPDATE_STATE, generationId, state).sendToTarget(); + } + } + + public void setMetadata(int generationId, Bundle metadata) { + Handler handler = mLocalHandler.get(); + if (handler != null) { + handler.obtainMessage(MSG_SET_METADATA, generationId, 0, metadata).sendToTarget(); + } + } + + public void setTransportControlFlags(int generationId, int flags) { + Handler handler = mLocalHandler.get(); + if (handler != null) { + handler.obtainMessage(MSG_SET_TRANSPORT_CONTROLS, generationId, flags) + .sendToTarget(); + } + } + + public void setArtwork(int generationId, Bitmap bitmap) { + Handler handler = mLocalHandler.get(); + if (handler != null) { + handler.obtainMessage(MSG_SET_ARTWORK, generationId, 0, bitmap).sendToTarget(); + } + } + + public void setAllMetadata(int generationId, Bundle metadata, Bitmap bitmap) { + Handler handler = mLocalHandler.get(); + if (handler != null) { + handler.obtainMessage(MSG_SET_METADATA, generationId, 0, metadata).sendToTarget(); + handler.obtainMessage(MSG_SET_ARTWORK, generationId, 0, bitmap).sendToTarget(); + } + } + + public void setCurrentClientId(int clientGeneration, PendingIntent mediaIntent, + boolean clearing) throws RemoteException { + Handler handler = mLocalHandler.get(); + if (handler != null) { + handler.obtainMessage(MSG_SET_GENERATION_ID, + clientGeneration, (clearing ? 1 : 0), mediaIntent).sendToTarget(); + } + } + }; + + public KeyguardTransportControlView(Context context, AttributeSet attrs) { + super(context, attrs); + if (DEBUG) Log.v(TAG, "Create TCV " + this); + mAudioManager = new AudioManager(mContext); + mCurrentPlayState = RemoteControlClient.PLAYSTATE_NONE; // until we get a callback + mIRCD = new IRemoteControlDisplayWeak(mHandler); + } + + protected void onListenerDetached() { + mMusicClientPresent = false; + if (DEBUG) Log.v(TAG, "onListenerDetached()"); + if (mTransportCallback != null) { + mTransportCallback.onListenerDetached(); + } else { + Log.w(TAG, "onListenerDetached: no callback"); + } + } + + private void onListenerAttached() { + mMusicClientPresent = true; + if (DEBUG) Log.v(TAG, "onListenerAttached()"); + if (mTransportCallback != null) { + mTransportCallback.onListenerAttached(); + } else { + Log.w(TAG, "onListenerAttached(): no callback"); + } + } + + private void updateTransportControls(int transportControlFlags) { + mTransportControlFlags = transportControlFlags; + } + + @Override + public void onFinishInflate() { + super.onFinishInflate(); + mTrackTitle = (TextView) findViewById(R.id.title); + mTrackTitle.setSelected(true); // enable marquee + mAlbumArt = (ImageView) findViewById(R.id.albumart); + mBtnPrev = (ImageView) findViewById(R.id.btn_prev); + mBtnPlay = (ImageView) findViewById(R.id.btn_play); + mBtnNext = (ImageView) findViewById(R.id.btn_next); + final View buttons[] = { mBtnPrev, mBtnPlay, mBtnNext }; + for (View view : buttons) { + view.setOnClickListener(this); + } + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + if (DEBUG) Log.v(TAG, "onAttachToWindow()"); + if (mPopulateMetadataWhenAttached != null) { + updateMetadata(mPopulateMetadataWhenAttached); + mPopulateMetadataWhenAttached = null; + } + if (!mAttached) { + if (DEBUG) Log.v(TAG, "Registering TCV " + this); + mAudioManager.registerRemoteControlDisplay(mIRCD); + } + mAttached = true; + } + + @Override + public void onDetachedFromWindow() { + if (DEBUG) Log.v(TAG, "onDetachFromWindow()"); + super.onDetachedFromWindow(); + if (mAttached) { + if (DEBUG) Log.v(TAG, "Unregistering TCV " + this); + mAudioManager.unregisterRemoteControlDisplay(mIRCD); + } + mAttached = false; + } + + class Metadata { + private String artist; + private String trackTitle; + private String albumTitle; + private Bitmap bitmap; + + public String toString() { + return "Metadata[artist=" + artist + " trackTitle=" + trackTitle + " albumTitle=" + albumTitle + "]"; + } + } + + private String getMdString(Bundle data, int id) { + return data.getString(Integer.toString(id)); + } + + private void updateMetadata(Bundle data) { + if (mAttached) { + mMetadata.artist = getMdString(data, MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST); + mMetadata.trackTitle = getMdString(data, MediaMetadataRetriever.METADATA_KEY_TITLE); + mMetadata.albumTitle = getMdString(data, MediaMetadataRetriever.METADATA_KEY_ALBUM); + populateMetadata(); + } else { + mPopulateMetadataWhenAttached = data; + } + } + + /** + * Populates the given metadata into the view + */ + private void populateMetadata() { + StringBuilder sb = new StringBuilder(); + int trackTitleLength = 0; + if (!TextUtils.isEmpty(mMetadata.trackTitle)) { + sb.append(mMetadata.trackTitle); + trackTitleLength = mMetadata.trackTitle.length(); + } + if (!TextUtils.isEmpty(mMetadata.artist)) { + if (sb.length() != 0) { + sb.append(" - "); + } + sb.append(mMetadata.artist); + } + if (!TextUtils.isEmpty(mMetadata.albumTitle)) { + if (sb.length() != 0) { + sb.append(" - "); + } + sb.append(mMetadata.albumTitle); + } + mTrackTitle.setText(sb.toString(), TextView.BufferType.SPANNABLE); + Spannable str = (Spannable) mTrackTitle.getText(); + if (trackTitleLength != 0) { + str.setSpan(new ForegroundColorSpan(0xffffffff), 0, trackTitleLength, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + trackTitleLength++; + } + if (sb.length() > trackTitleLength) { + str.setSpan(new ForegroundColorSpan(0x7fffffff), trackTitleLength, sb.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + mAlbumArt.setImageBitmap(mMetadata.bitmap); + final int flags = mTransportControlFlags; + setVisibilityBasedOnFlag(mBtnPrev, flags, RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS); + setVisibilityBasedOnFlag(mBtnNext, flags, RemoteControlClient.FLAG_KEY_MEDIA_NEXT); + setVisibilityBasedOnFlag(mBtnPlay, flags, + RemoteControlClient.FLAG_KEY_MEDIA_PLAY + | RemoteControlClient.FLAG_KEY_MEDIA_PAUSE + | RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE + | RemoteControlClient.FLAG_KEY_MEDIA_STOP); + + updatePlayPauseState(mCurrentPlayState); + } + + public boolean isMusicPlaying() { + return mCurrentPlayState == RemoteControlClient.PLAYSTATE_PLAYING + || mCurrentPlayState == RemoteControlClient.PLAYSTATE_BUFFERING; + } + + private static void setVisibilityBasedOnFlag(View view, int flags, int flag) { + if ((flags & flag) != 0) { + view.setVisibility(View.VISIBLE); + } else { + view.setVisibility(View.GONE); + } + } + + private void updatePlayPauseState(int state) { + if (DEBUG) Log.v(TAG, + "updatePlayPauseState(), old=" + mCurrentPlayState + ", state=" + state); + if (state == mCurrentPlayState) { + return; + } + final int imageResId; + final int imageDescId; + switch (state) { + case RemoteControlClient.PLAYSTATE_ERROR: + imageResId = com.android.internal.R.drawable.stat_sys_warning; + // TODO use more specific image description string for warning, but here the "play" + // message is still valid because this button triggers a play command. + imageDescId = com.android.internal.R.string.lockscreen_transport_play_description; + break; + + case RemoteControlClient.PLAYSTATE_PLAYING: + imageResId = com.android.internal.R.drawable.ic_media_pause; + imageDescId = com.android.internal.R.string.lockscreen_transport_pause_description; + break; + + case RemoteControlClient.PLAYSTATE_BUFFERING: + imageResId = com.android.internal.R.drawable.ic_media_stop; + imageDescId = com.android.internal.R.string.lockscreen_transport_stop_description; + break; + + case RemoteControlClient.PLAYSTATE_PAUSED: + default: + imageResId = com.android.internal.R.drawable.ic_media_play; + imageDescId = com.android.internal.R.string.lockscreen_transport_play_description; + break; + } + mBtnPlay.setImageResource(imageResId); + mBtnPlay.setContentDescription(getResources().getString(imageDescId)); + mCurrentPlayState = state; + mTransportCallback.onPlayStateChanged(); + } + + static class SavedState extends BaseSavedState { + boolean clientPresent; + + SavedState(Parcelable superState) { + super(superState); + } + + private SavedState(Parcel in) { + super(in); + this.clientPresent = in.readInt() != 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeInt(this.clientPresent ? 1 : 0); + } + + public static final Parcelable.Creator<SavedState> CREATOR + = new Parcelable.Creator<SavedState>() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + @Override + public Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState ss = new SavedState(superState); + ss.clientPresent = mMusicClientPresent; + return ss; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + if (!(state instanceof SavedState)) { + super.onRestoreInstanceState(state); + return; + } + SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + if (ss.clientPresent) { + if (DEBUG) Log.v(TAG, "Reattaching client because it was attached"); + onListenerAttached(); + } + } + + public void onClick(View v) { + int keyCode = -1; + if (v == mBtnPrev) { + keyCode = KeyEvent.KEYCODE_MEDIA_PREVIOUS; + } else if (v == mBtnNext) { + keyCode = KeyEvent.KEYCODE_MEDIA_NEXT; + } else if (v == mBtnPlay) { + keyCode = KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE; + + } + if (keyCode != -1) { + sendMediaButtonClick(keyCode); + } + } + + private void sendMediaButtonClick(int keyCode) { + if (mClientIntent == null) { + // Shouldn't be possible because this view should be hidden in this case. + Log.e(TAG, "sendMediaButtonClick(): No client is currently registered"); + return; + } + // use the registered PendingIntent that will be processed by the registered + // media button event receiver, which is the component of mClientIntent + KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode); + Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); + intent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent); + try { + mClientIntent.send(getContext(), 0, intent); + } catch (CanceledException e) { + Log.e(TAG, "Error sending intent for media button down: "+e); + e.printStackTrace(); + } + + keyEvent = new KeyEvent(KeyEvent.ACTION_UP, keyCode); + intent = new Intent(Intent.ACTION_MEDIA_BUTTON); + intent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent); + try { + mClientIntent.send(getContext(), 0, intent); + } catch (CanceledException e) { + Log.e(TAG, "Error sending intent for media button up: "+e); + e.printStackTrace(); + } + } + + public boolean providesClock() { + return false; + } + + private boolean wasPlayingRecently(int state, long stateChangeTimeMs) { + switch (state) { + case RemoteControlClient.PLAYSTATE_PLAYING: + case RemoteControlClient.PLAYSTATE_FAST_FORWARDING: + case RemoteControlClient.PLAYSTATE_REWINDING: + case RemoteControlClient.PLAYSTATE_SKIPPING_FORWARDS: + case RemoteControlClient.PLAYSTATE_SKIPPING_BACKWARDS: + case RemoteControlClient.PLAYSTATE_BUFFERING: + // actively playing or about to play + return true; + case RemoteControlClient.PLAYSTATE_NONE: + return false; + case RemoteControlClient.PLAYSTATE_STOPPED: + case RemoteControlClient.PLAYSTATE_PAUSED: + case RemoteControlClient.PLAYSTATE_ERROR: + // we have stopped playing, check how long ago + if (DEBUG) { + if ((SystemClock.elapsedRealtime() - stateChangeTimeMs) < DISPLAY_TIMEOUT_MS) { + Log.v(TAG, "wasPlayingRecently: time < TIMEOUT was playing recently"); + } else { + Log.v(TAG, "wasPlayingRecently: time > TIMEOUT"); + } + } + return ((SystemClock.elapsedRealtime() - stateChangeTimeMs) < DISPLAY_TIMEOUT_MS); + default: + Log.e(TAG, "Unknown playback state " + state + " in wasPlayingRecently()"); + return false; + } + } + + public void setKeyguardCallback(KeyguardHostView.TransportCallback transportCallback) { + mTransportCallback = transportCallback; + } +} diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/Keyguard/src/com/android/keyguard/KeyguardUpdateMonitor.java new file mode 100644 index 0000000..c9bffbe --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -0,0 +1,841 @@ +/* + * 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.internal.policy.impl.keyguard; + +import android.app.ActivityManagerNative; +import android.app.IUserSwitchObserver; +import android.app.admin.DevicePolicyManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.database.ContentObserver; +import static android.os.BatteryManager.BATTERY_STATUS_FULL; +import static android.os.BatteryManager.BATTERY_STATUS_UNKNOWN; +import static android.os.BatteryManager.BATTERY_HEALTH_UNKNOWN; +import static android.os.BatteryManager.EXTRA_STATUS; +import static android.os.BatteryManager.EXTRA_PLUGGED; +import static android.os.BatteryManager.EXTRA_LEVEL; +import static android.os.BatteryManager.EXTRA_HEALTH; +import android.media.AudioManager; +import android.os.BatteryManager; +import android.os.Handler; +import android.os.IRemoteCallback; +import android.os.Message; +import android.os.RemoteException; +import android.os.UserHandle; +import android.provider.Settings; + +import com.android.internal.telephony.IccCardConstants; +import com.android.internal.telephony.TelephonyIntents; + +import android.telephony.TelephonyManager; +import android.util.Log; +import com.android.internal.R; +import com.google.android.collect.Lists; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; + +/** + * 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 { + + private static final String TAG = "KeyguardUpdateMonitor"; + private static final boolean DEBUG = false; + private static final boolean DEBUG_SIM_STATES = DEBUG || false; + private static final int FAILED_BIOMETRIC_UNLOCK_ATTEMPTS_BEFORE_BACKUP = 3; + private static final int LOW_BATTERY_THRESHOLD = 20; + + // Callback messages + private static final int MSG_TIME_UPDATE = 301; + private static final int MSG_BATTERY_UPDATE = 302; + private static final int MSG_CARRIER_INFO_UPDATE = 303; + 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_CLOCK_VISIBILITY_CHANGED = 307; + 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_USER_REMOVED = 311; + private static final int MSG_KEYGUARD_VISIBILITY_CHANGED = 312; + protected static final int MSG_BOOT_COMPLETED = 313; + private static final int MSG_USER_SWITCH_COMPLETE = 314; + + + private static KeyguardUpdateMonitor sInstance; + + private final Context mContext; + + // Telephony state + private IccCardConstants.State mSimState = IccCardConstants.State.READY; + private CharSequence mTelephonyPlmn; + private CharSequence mTelephonySpn; + private int mRingMode; + private int mPhoneState; + private boolean mKeyguardIsVisible; + private boolean mBootCompleted; + + // Device provisioning state + private boolean mDeviceProvisioned; + + // Battery status + private BatteryStatus mBatteryStatus; + + // Password attempts + private int mFailedAttempts = 0; + private int mFailedBiometricUnlockAttempts = 0; + + private boolean mAlternateUnlockEnabled; + + private boolean mClockVisible; + + private final ArrayList<WeakReference<KeyguardUpdateMonitorCallback>> + mCallbacks = Lists.newArrayList(); + private ContentObserver mDeviceProvisionedObserver; + + 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_CARRIER_INFO_UPDATE: + handleCarrierInfoUpdate(); + break; + case MSG_SIM_STATE_CHANGE: + handleSimStateChange((SimArgs) msg.obj); + break; + case MSG_RINGER_MODE_CHANGED: + handleRingerModeChange(msg.arg1); + break; + case MSG_PHONE_STATE_CHANGED: + handlePhoneStateChanged((String)msg.obj); + break; + case MSG_CLOCK_VISIBILITY_CHANGED: + handleClockVisibilityChanged(); + 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_USER_REMOVED: + handleUserRemoved(msg.arg1); + break; + case MSG_KEYGUARD_VISIBILITY_CHANGED: + handleKeyguardVisibilityChanged(msg.arg1); + break; + case MSG_BOOT_COMPLETED: + handleBootCompleted(); + break; + + } + } + }; + + private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + + 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.sendMessage(mHandler.obtainMessage(MSG_TIME_UPDATE)); + } else if (TelephonyIntents.SPN_STRINGS_UPDATED_ACTION.equals(action)) { + mTelephonyPlmn = getTelephonyPlmnFrom(intent); + mTelephonySpn = getTelephonySpnFrom(intent); + mHandler.sendMessage(mHandler.obtainMessage(MSG_CARRIER_INFO_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 Message msg = mHandler.obtainMessage( + MSG_BATTERY_UPDATE, new BatteryStatus(status, level, plugged, health)); + mHandler.sendMessage(msg); + } else if (TelephonyIntents.ACTION_SIM_STATE_CHANGED.equals(action)) { + if (DEBUG_SIM_STATES) { + Log.v(TAG, "action " + action + " state" + + intent.getStringExtra(IccCardConstants.INTENT_KEY_ICC_STATE)); + } + mHandler.sendMessage(mHandler.obtainMessage( + MSG_SIM_STATE_CHANGE, SimArgs.fromIntent(intent))); + } 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 (DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED + .equals(action)) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_DPM_STATE_CHANGED)); + } else if (Intent.ACTION_USER_REMOVED.equals(action)) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_USER_REMOVED, + intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0), 0)); + } else if (Intent.ACTION_BOOT_COMPLETED.equals(action)) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_BOOT_COMPLETED)); + } + } + }; + + /** + * 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 SimArgs { + public final IccCardConstants.State simState; + + SimArgs(IccCardConstants.State state) { + simState = state; + } + + static SimArgs fromIntent(Intent intent) { + IccCardConstants.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); + 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 SimArgs(state); + } + + public String toString() { + return simState.toString(); + } + } + + /* package */ static class BatteryStatus { + public final int status; + public final int level; + public final int plugged; + public final int health; + public BatteryStatus(int status, int level, int plugged, int health) { + this.status = status; + this.level = level; + this.plugged = plugged; + this.health = health; + } + + /** + * Determine whether the device is plugged in (USB, power, or wireless). + * @return true if the device is plugged in. + */ + 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 static KeyguardUpdateMonitor getInstance(Context context) { + if (sInstance == null) { + sInstance = new KeyguardUpdateMonitor(context); + } + return sInstance; + } + + private KeyguardUpdateMonitor(Context context) { + mContext = context; + + mDeviceProvisioned = isDeviceProvisionedInSettingsDb(); + // 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 + mSimState = IccCardConstants.State.NOT_READY; + mBatteryStatus = new BatteryStatus(BATTERY_STATUS_UNKNOWN, 100, 0, 0); + mTelephonyPlmn = getDefaultPlmn(); + + // 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(TelephonyIntents.ACTION_SIM_STATE_CHANGED); + filter.addAction(TelephonyManager.ACTION_PHONE_STATE_CHANGED); + filter.addAction(TelephonyIntents.SPN_STRINGS_UPDATED_ACTION); + filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION); + filter.addAction(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED); + filter.addAction(Intent.ACTION_USER_REMOVED); + 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); + + try { + ActivityManagerNative.getDefault().registerUserSwitchObserver( + new IUserSwitchObserver.Stub() { + @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)); + } + }); + } catch (RemoteException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + 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.sendMessage(mHandler.obtainMessage(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.sendMessage(mHandler.obtainMessage(MSG_DEVICE_PROVISIONED)); + } + } + } + + /** + * Handle {@link #MSG_DPM_STATE_CHANGED} + */ + protected void handleDevicePolicyManagerStateChanged() { + 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); + } + } + setAlternateUnlockEnabled(false); + 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); + } + } + } + + /** + * Handle {@link #MSG_BOOT_COMPLETED} + */ + protected void handleBootCompleted() { + 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_USER_REMOVED} + */ + protected void handleUserRemoved(int userId) { + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onUserRemoved(userId); + } + } + } + + /** + * 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_CARRIER_INFO_UPDATE} + */ + private void handleCarrierInfoUpdate() { + if (DEBUG) Log.d(TAG, "handleCarrierInfoUpdate: plmn = " + mTelephonyPlmn + + ", spn = " + mTelephonySpn); + + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onRefreshCarrierInfo(mTelephonyPlmn, mTelephonySpn); + } + } + } + + /** + * Handle {@link #MSG_SIM_STATE_CHANGE} + */ + private void handleSimStateChange(SimArgs simArgs) { + final IccCardConstants.State state = simArgs.simState; + + if (DEBUG) { + Log.d(TAG, "handleSimStateChange: intentValue = " + simArgs + " " + + "state resolved to " + state.toString()); + } + + if (state != IccCardConstants.State.UNKNOWN && state != mSimState) { + mSimState = state; + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onSimStateChanged(state); + } + } + } + } + + /** + * Handle {@link #MSG_CLOCK_VISIBILITY_CHANGED} + */ + private void handleClockVisibilityChanged() { + if (DEBUG) Log.d(TAG, "handleClockVisibilityChanged()"); + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onClockVisibilityChanged(); + } + } + } + + /** + * Handle {@link #MSG_KEYGUARD_VISIBILITY_CHANGED} + */ + private void handleKeyguardVisibilityChanged(int showing) { + if (DEBUG) Log.d(TAG, "handleKeyguardVisibilityChanged(" + showing + ")"); + boolean isShowing = (showing == 1); + mKeyguardIsVisible = isShowing; + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onKeyguardVisibilityChanged(isShowing); + } + } + } + + public boolean isKeyguardVisible() { + return mKeyguardIsVisible; + } + + 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; + } + return false; + } + + /** + * @param intent The intent with action {@link TelephonyIntents#SPN_STRINGS_UPDATED_ACTION} + * @return The string to use for the plmn, or null if it should not be shown. + */ + private CharSequence getTelephonyPlmnFrom(Intent intent) { + if (intent.getBooleanExtra(TelephonyIntents.EXTRA_SHOW_PLMN, false)) { + final String plmn = intent.getStringExtra(TelephonyIntents.EXTRA_PLMN); + return (plmn != null) ? plmn : getDefaultPlmn(); + } + return null; + } + + /** + * @return The default plmn (no service) + */ + private CharSequence getDefaultPlmn() { + return mContext.getResources().getText(R.string.lockscreen_carrier_default); + } + + /** + * @param intent The intent with action {@link Telephony.Intents#SPN_STRINGS_UPDATED_ACTION} + * @return The string to use for the plmn, or null if it should not be shown. + */ + private CharSequence getTelephonySpnFrom(Intent intent) { + if (intent.getBooleanExtra(TelephonyIntents.EXTRA_SHOW_SPN, false)) { + final String spn = intent.getStringExtra(TelephonyIntents.EXTRA_SPN); + if (spn != null) { + return spn; + } + } + return null; + } + + /** + * 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); + } + + private void sendUpdates(KeyguardUpdateMonitorCallback callback) { + // Notify listener of the current state + callback.onRefreshBatteryInfo(mBatteryStatus); + callback.onTimeChanged(); + callback.onRingerModeChanged(mRingMode); + callback.onPhoneStateChanged(mPhoneState); + callback.onRefreshCarrierInfo(mTelephonyPlmn, mTelephonySpn); + callback.onClockVisibilityChanged(); + callback.onSimStateChanged(mSimState); + } + + public void sendKeyguardVisibilityChanged(boolean showing) { + if (DEBUG) Log.d(TAG, "sendKeyguardVisibilityChanged(" + showing + ")"); + Message message = mHandler.obtainMessage(MSG_KEYGUARD_VISIBILITY_CHANGED); + message.arg1 = showing ? 1 : 0; + message.sendToTarget(); + } + + public void reportClockVisible(boolean visible) { + mClockVisible = visible; + mHandler.obtainMessage(MSG_CLOCK_VISIBILITY_CHANGED).sendToTarget(); + } + + public IccCardConstants.State getSimState() { + return mSimState; + } + + /** + * 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() { + handleSimStateChange(new SimArgs(IccCardConstants.State.READY)); + } + + public CharSequence getTelephonyPlmn() { + return mTelephonyPlmn; + } + + public CharSequence getTelephonySpn() { + return mTelephonySpn; + } + + /** + * @return Whether the device is provisioned (whether they have gone through + * the setup wizard) + */ + public boolean isDeviceProvisioned() { + return mDeviceProvisioned; + } + + public int getFailedUnlockAttempts() { + return mFailedAttempts; + } + + public void clearFailedUnlockAttempts() { + mFailedAttempts = 0; + mFailedBiometricUnlockAttempts = 0; + } + + public void reportFailedUnlockAttempt() { + mFailedAttempts++; + } + + public boolean isClockVisible() { + return mClockVisible; + } + + public int getPhoneState() { + return mPhoneState; + } + + public void reportFailedBiometricUnlockAttempt() { + mFailedBiometricUnlockAttempts++; + } + + public boolean getMaxBiometricUnlockAttemptsReached() { + return mFailedBiometricUnlockAttempts >= FAILED_BIOMETRIC_UNLOCK_ATTEMPTS_BEFORE_BACKUP; + } + + public boolean isAlternateUnlockEnabled() { + return mAlternateUnlockEnabled; + } + + public void setAlternateUnlockEnabled(boolean enabled) { + mAlternateUnlockEnabled = enabled; + } + + public boolean isSimLocked() { + return isSimLocked(mSimState); + } + + public static boolean isSimLocked(IccCardConstants.State state) { + return state == IccCardConstants.State.PIN_REQUIRED + || state == IccCardConstants.State.PUK_REQUIRED + || state == IccCardConstants.State.PERM_DISABLED; + } + + public boolean isSimPinSecure() { + return isSimPinSecure(mSimState); + } + + 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); + } +} diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java b/packages/Keyguard/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java new file mode 100644 index 0000000..2126f06 --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java @@ -0,0 +1,115 @@ +/* + * 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.internal.policy.impl.keyguard; + +import android.app.admin.DevicePolicyManager; +import android.media.AudioManager; + +import com.android.internal.telephony.IccCardConstants; + +/** + * Callback for general information relevant to lock screen. + */ +class KeyguardUpdateMonitorCallback { + /** + * Called when the battery status changes, e.g. when plugged in or unplugged, charge + * level, etc. changes. + * + * @param status current battery status + */ + void onRefreshBatteryInfo(KeyguardUpdateMonitor.BatteryStatus status) { } + + /** + * Called once per minute or when the time changes. + */ + void onTimeChanged() { } + + /** + * Called when the carrier PLMN or SPN changes. + * + * @param plmn The operator name of the registered network. May be null if it shouldn't + * be displayed. + * @param spn The service provider name. May be null if it shouldn't be displayed. + */ + void onRefreshCarrierInfo(CharSequence plmn, CharSequence spn) { } + + /** + * Called when the ringer mode changes. + * @param state the current ringer state, as defined in + * {@link AudioManager#RINGER_MODE_CHANGED_ACTION} + */ + 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 + */ + void onPhoneStateChanged(int phoneState) { } + + /** + * Called when the visibility of the keyguard changes. + * @param showing Indicates if the keyguard is now visible. + */ + void onKeyguardVisibilityChanged(boolean showing) { } + + /** + * Called when visibility of lockscreen clock changes, such as when + * obscured by a widget. + */ + void onClockVisibilityChanged() { } + + /** + * Called when the device becomes provisioned + */ + void onDeviceProvisioned() { } + + /** + * Called when the device policy changes. + * See {@link DevicePolicyManager#ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED} + */ + void onDevicePolicyManagerStateChanged() { } + + /** + * Called when the user change begins. + */ + void onUserSwitching(int userId) { } + + /** + * Called when the user change is complete. + */ + void onUserSwitchComplete(int userId) { } + + /** + * Called when the SIM state changes. + * @param simState + */ + void onSimStateChanged(IccCardConstants.State simState) { } + + /** + * Called when a user is removed. + */ + void onUserRemoved(int userId) { } + + /** + * Called when boot completed. + * + * Note, this callback will only be received if boot complete occurs after registering with + * KeyguardUpdateMonitor. + */ + void onBootCompleted() { } +} diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardViewBase.java b/packages/Keyguard/src/com/android/keyguard/KeyguardViewBase.java new file mode 100644 index 0000000..6fcacd3 --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardViewBase.java @@ -0,0 +1,264 @@ +/* + * 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.internal.policy.impl.keyguard; + +import android.app.Activity; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.PixelFormat; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.media.AudioManager; +import android.media.IAudioService; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.telephony.TelephonyManager; +import android.util.AttributeSet; +import android.util.Log; +import android.util.Slog; +import android.view.KeyEvent; +import android.widget.FrameLayout; + +/** + * 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 abstract class KeyguardViewBase extends FrameLayout { + + private static final int BACKGROUND_COLOR = 0x70000000; + private AudioManager mAudioManager; + private TelephonyManager mTelephonyManager = null; + protected KeyguardViewMediator.ViewMediatorCallback mViewMediatorCallback; + + // 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 = true; + + // This is a faster way to draw the background on devices without hardware acceleration + private static final Drawable mBackgroundDrawable = new Drawable() { + @Override + public void draw(Canvas canvas) { + canvas.drawColor(BACKGROUND_COLOR, PorterDuff.Mode.SRC); + } + + @Override + public void setAlpha(int alpha) { + } + + @Override + public void setColorFilter(ColorFilter cf) { + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + }; + + public KeyguardViewBase(Context context) { + this(context, null); + } + + public KeyguardViewBase(Context context, AttributeSet attrs) { + super(context, attrs); + resetBackground(); + } + + public void resetBackground() { + setBackground(mBackgroundDrawable); + } + + /** + * Called when you need to reset the state of your view. + */ + abstract public void reset(); + + /** + * Called when the screen turned off. + */ + abstract public void onScreenTurnedOff(); + + /** + * Called when the screen turned on. + */ + abstract public void onScreenTurnedOn(); + + /** + * Called when the view needs to be shown. + */ + abstract public void show(); + + /** + * Called when a key has woken the device to give us a chance to adjust our + * state according the the key. We are responsible for waking the device + * (by poking the wake lock) once we are ready. + * + * The 'Tq' suffix is per the documentation in {@link android.view.WindowManagerPolicy}. + * Be sure not to take any action that takes a long time; any significant + * action should be posted to a handler. + * + * @param keyCode The wake key, which may be relevant for configuring the + * keyguard. May be {@link KeyEvent#KEYCODE_UNKNOWN} if waking for a reason + * other than a key press. + */ + abstract public void wakeWhenReadyTq(int keyCode); + + /** + * Verify that the user can get past the keyguard securely. This is called, + * for example, when the phone disables the keyguard but then wants to launch + * something else that requires secure access. + * + * The result will be propogated back via {@link KeyguardViewCallback#keyguardDone(boolean)} + */ + abstract public void verifyUnlock(); + + /** + * Called before this view is being removed. + */ + abstract public void cleanUp(); + + /** + * Gets the desired user activity timeout in milliseconds, or -1 if the + * default should be used. + */ + abstract public long getUserActivityTimeout(); + + @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. + */ + private 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: { + 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.adjustLocalOrRemoteStreamVolume( + AudioManager.STREAM_MUSIC, + keyCode == KeyEvent.KEYCODE_VOLUME_UP + ? AudioManager.ADJUST_RAISE + : AudioManager.ADJUST_LOWER); + // 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: { + handleMediaKeyEvent(event); + return true; + } + } + } + return false; + } + + void handleMediaKeyEvent(KeyEvent keyEvent) { + IAudioService audioService = IAudioService.Stub.asInterface( + ServiceManager.checkService(Context.AUDIO_SERVICE)); + if (audioService != null) { + try { + audioService.dispatchMediaKeyEvent(keyEvent); + } catch (RemoteException e) { + Log.e("KeyguardViewBase", "dispatchMediaKeyEvent threw exception " + e); + } + } else { + Slog.w("KeyguardViewBase", "Unable to find IAudioService for media key event"); + } + } + + @Override + public void dispatchSystemUiVisibilityChanged(int visibility) { + super.dispatchSystemUiVisibilityChanged(visibility); + + if (!(mContext instanceof Activity)) { + setSystemUiVisibility(STATUS_BAR_DISABLE_BACK); + } + } + + public void setViewMediatorCallback( + KeyguardViewMediator.ViewMediatorCallback viewMediatorCallback) { + mViewMediatorCallback = viewMediatorCallback; + } + +} diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardViewManager.java b/packages/Keyguard/src/com/android/keyguard/KeyguardViewManager.java new file mode 100644 index 0000000..8562f0c --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardViewManager.java @@ -0,0 +1,446 @@ +/* + * 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.internal.policy.impl.keyguard; + +import android.app.Activity; +import android.app.ActivityManager; +import android.appwidget.AppWidgetManager; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.IBinder; +import android.os.Parcelable; +import android.os.SystemProperties; +import android.util.Log; +import android.util.Slog; +import android.util.SparseArray; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewManager; +import android.view.WindowManager; +import android.widget.FrameLayout; + +import com.android.internal.R; +import com.android.internal.widget.LockPatternUtils; + +/** + * Manages creating, showing, hiding and resetting the keyguard. Calls back + * via {@link KeyguardViewMediator.ViewMediatorCallback} to poke + * the wake lock and report that the keyguard is done, which is in turn, + * reported to this class by the current {@link KeyguardViewBase}. + */ +public class KeyguardViewManager { + private final static boolean DEBUG = KeyguardViewMediator.DEBUG; + private static String TAG = "KeyguardViewManager"; + public static boolean USE_UPPER_CASE = true; + public final static String IS_SWITCHING_USER = "is_switching_user"; + + // Timeout used for keypresses + static final int DIGIT_PRESS_WAKE_MILLIS = 5000; + + private final Context mContext; + private final ViewManager mViewManager; + private final KeyguardViewMediator.ViewMediatorCallback mViewMediatorCallback; + + private WindowManager.LayoutParams mWindowLayoutParams; + private boolean mNeedsInput = false; + + private FrameLayout mKeyguardHost; + private KeyguardHostView mKeyguardView; + + private boolean mScreenOn = false; + private LockPatternUtils mLockPatternUtils; + + public interface ShowListener { + void onShown(IBinder windowToken); + }; + + /** + * @param context Used to create views. + * @param viewManager Keyguard will be attached to this. + * @param callback Used to notify of changes. + * @param lockPatternUtils + */ + public KeyguardViewManager(Context context, ViewManager viewManager, + KeyguardViewMediator.ViewMediatorCallback callback, + LockPatternUtils lockPatternUtils) { + mContext = context; + mViewManager = viewManager; + mViewMediatorCallback = callback; + mLockPatternUtils = lockPatternUtils; + } + + /** + * Show the keyguard. Will handle creating and attaching to the view manager + * lazily. + */ + public synchronized void show(Bundle options) { + if (DEBUG) Log.d(TAG, "show(); mKeyguardView==" + mKeyguardView); + + boolean enableScreenRotation = shouldEnableScreenRotation(); + + maybeCreateKeyguardLocked(enableScreenRotation, false, options); + maybeEnableScreenRotation(enableScreenRotation); + + // Disable common aspects of the system/status/navigation bars that are not appropriate or + // useful on any keyguard screen but can be re-shown by dialogs or SHOW_WHEN_LOCKED + // activities. Other disabled bits are handled by the KeyguardViewMediator talking + // directly to the status bar service. + final int visFlags = View.STATUS_BAR_DISABLE_HOME; + if (DEBUG) Log.v(TAG, "show:setSystemUiVisibility(" + Integer.toHexString(visFlags)+")"); + mKeyguardHost.setSystemUiVisibility(visFlags); + + mViewManager.updateViewLayout(mKeyguardHost, mWindowLayoutParams); + mKeyguardHost.setVisibility(View.VISIBLE); + mKeyguardView.show(); + mKeyguardView.requestFocus(); + } + + private boolean shouldEnableScreenRotation() { + Resources res = mContext.getResources(); + return SystemProperties.getBoolean("lockscreen.rot_override",false) + || res.getBoolean(com.android.internal.R.bool.config_enableLockScreenRotation); + } + + class ViewManagerHost extends FrameLayout { + public ViewManagerHost(Context context) { + super(context); + setFitsSystemWindows(true); + } + + @Override + protected boolean fitSystemWindows(Rect insets) { + Log.v("TAG", "bug 7643792: fitSystemWindows(" + insets.toShortString() + ")"); + return super.fitSystemWindows(insets); + } + + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + if (mKeyguardHost.getVisibility() == View.VISIBLE) { + // only propagate configuration messages if we're currently showing + maybeCreateKeyguardLocked(shouldEnableScreenRotation(), true, null); + } else { + if (DEBUG) Log.v(TAG, "onConfigurationChanged: view not visible"); + } + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (mKeyguardView != null) { + // Always process back and menu keys, regardless of focus + if (event.getAction() == KeyEvent.ACTION_DOWN) { + int keyCode = event.getKeyCode(); + if (keyCode == KeyEvent.KEYCODE_BACK && mKeyguardView.handleBackKey()) { + return true; + } else if (keyCode == KeyEvent.KEYCODE_MENU && mKeyguardView.handleMenuKey()) { + return true; + } + } + // Always process media keys, regardless of focus + if (mKeyguardView.dispatchKeyEvent(event)) { + return true; + } + } + return super.dispatchKeyEvent(event); + } + } + + SparseArray<Parcelable> mStateContainer = new SparseArray<Parcelable>(); + + private void maybeCreateKeyguardLocked(boolean enableScreenRotation, boolean force, + Bundle options) { + final boolean isActivity = (mContext instanceof Activity); // for test activity + + if (mKeyguardHost != null) { + mKeyguardHost.saveHierarchyState(mStateContainer); + } + + if (mKeyguardHost == null) { + if (DEBUG) Log.d(TAG, "keyguard host is null, creating it..."); + + mKeyguardHost = new ViewManagerHost(mContext); + + int flags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN + | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR + | WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN + | WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; + + if (!mNeedsInput) { + flags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; + } + if (ActivityManager.isHighEndGfx()) { + flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; + } + + final int stretch = ViewGroup.LayoutParams.MATCH_PARENT; + final int type = isActivity ? WindowManager.LayoutParams.TYPE_APPLICATION + : WindowManager.LayoutParams.TYPE_KEYGUARD; + WindowManager.LayoutParams lp = new WindowManager.LayoutParams( + stretch, stretch, type, flags, PixelFormat.TRANSLUCENT); + lp.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; + lp.windowAnimations = com.android.internal.R.style.Animation_LockScreen; + if (ActivityManager.isHighEndGfx()) { + lp.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; + lp.privateFlags |= + WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_HARDWARE_ACCELERATED; + } + lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SET_NEEDS_MENU_KEY; + if (isActivity) { + lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS; + } + lp.inputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_DISABLE_USER_ACTIVITY; + lp.setTitle(isActivity ? "KeyguardMock" : "Keyguard"); + mWindowLayoutParams = lp; + mViewManager.addView(mKeyguardHost, lp); + } + + if (force || mKeyguardView == null) { + inflateKeyguardView(options); + mKeyguardView.requestFocus(); + } + updateUserActivityTimeoutInWindowLayoutParams(); + mViewManager.updateViewLayout(mKeyguardHost, mWindowLayoutParams); + + mKeyguardHost.restoreHierarchyState(mStateContainer); + } + + private void inflateKeyguardView(Bundle options) { + View v = mKeyguardHost.findViewById(R.id.keyguard_host_view); + if (v != null) { + mKeyguardHost.removeView(v); + } + // TODO: Remove once b/7094175 is fixed + if (false) Slog.d(TAG, "inflateKeyguardView: b/7094175 mContext.config=" + + mContext.getResources().getConfiguration()); + final LayoutInflater inflater = LayoutInflater.from(mContext); + View view = inflater.inflate(R.layout.keyguard_host_view, mKeyguardHost, true); + mKeyguardView = (KeyguardHostView) view.findViewById(R.id.keyguard_host_view); + mKeyguardView.setLockPatternUtils(mLockPatternUtils); + mKeyguardView.setViewMediatorCallback(mViewMediatorCallback); + mKeyguardView.initializeSwitchingUserState(options != null && + options.getBoolean(IS_SWITCHING_USER)); + + // HACK + // The keyguard view will have set up window flags in onFinishInflate before we set + // the view mediator callback. Make sure it knows the correct IME state. + if (mViewMediatorCallback != null) { + KeyguardPasswordView kpv = (KeyguardPasswordView) mKeyguardView.findViewById( + R.id.keyguard_password_view); + + if (kpv != null) { + mViewMediatorCallback.setNeedsInput(kpv.needsInput()); + } + } + + if (options != null) { + int widgetToShow = options.getInt(LockPatternUtils.KEYGUARD_SHOW_APPWIDGET, + AppWidgetManager.INVALID_APPWIDGET_ID); + if (widgetToShow != AppWidgetManager.INVALID_APPWIDGET_ID) { + mKeyguardView.goToWidget(widgetToShow); + } + } + } + + public void updateUserActivityTimeout() { + updateUserActivityTimeoutInWindowLayoutParams(); + mViewManager.updateViewLayout(mKeyguardHost, mWindowLayoutParams); + } + + private void updateUserActivityTimeoutInWindowLayoutParams() { + // Use the user activity timeout requested by the keyguard view, if any. + if (mKeyguardView != null) { + long timeout = mKeyguardView.getUserActivityTimeout(); + if (timeout >= 0) { + mWindowLayoutParams.userActivityTimeout = timeout; + return; + } + } + + // Otherwise, use the default timeout. + mWindowLayoutParams.userActivityTimeout = KeyguardViewMediator.AWAKE_INTERVAL_DEFAULT_MS; + } + + private void maybeEnableScreenRotation(boolean enableScreenRotation) { + // TODO: move this outside + if (enableScreenRotation) { + if (DEBUG) Log.d(TAG, "Rotation sensor for lock screen On!"); + mWindowLayoutParams.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_USER; + } else { + if (DEBUG) Log.d(TAG, "Rotation sensor for lock screen Off!"); + mWindowLayoutParams.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR; + } + mViewManager.updateViewLayout(mKeyguardHost, mWindowLayoutParams); + } + + public void setNeedsInput(boolean needsInput) { + mNeedsInput = needsInput; + if (mWindowLayoutParams != null) { + if (needsInput) { + mWindowLayoutParams.flags &= + ~WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; + } else { + mWindowLayoutParams.flags |= + WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; + } + + try { + mViewManager.updateViewLayout(mKeyguardHost, mWindowLayoutParams); + } catch (java.lang.IllegalArgumentException e) { + // TODO: Ensure this method isn't called on views that are changing... + Log.w(TAG,"Can't update input method on " + mKeyguardHost + " window not attached"); + } + } + } + + /** + * Reset the state of the view. + */ + public synchronized void reset(Bundle options) { + if (DEBUG) Log.d(TAG, "reset()"); + // User might have switched, check if we need to go back to keyguard + // TODO: It's preferable to stay and show the correct lockscreen or unlock if none + maybeCreateKeyguardLocked(shouldEnableScreenRotation(), true, options); + } + + public synchronized void onScreenTurnedOff() { + if (DEBUG) Log.d(TAG, "onScreenTurnedOff()"); + mScreenOn = false; + if (mKeyguardView != null) { + mKeyguardView.onScreenTurnedOff(); + } + } + + public synchronized void onScreenTurnedOn( + final KeyguardViewManager.ShowListener showListener) { + if (DEBUG) Log.d(TAG, "onScreenTurnedOn()"); + mScreenOn = true; + if (mKeyguardView != null) { + mKeyguardView.onScreenTurnedOn(); + + // Caller should wait for this window to be shown before turning + // on the screen. + if (showListener != null) { + if (mKeyguardHost.getVisibility() == View.VISIBLE) { + // Keyguard may be in the process of being shown, but not yet + // updated with the window manager... give it a chance to do so. + mKeyguardHost.post(new Runnable() { + @Override + public void run() { + if (mKeyguardHost.getVisibility() == View.VISIBLE) { + showListener.onShown(mKeyguardHost.getWindowToken()); + } else { + showListener.onShown(null); + } + } + }); + } else { + showListener.onShown(null); + } + } + } else if (showListener != null) { + showListener.onShown(null); + } + } + + public synchronized void verifyUnlock() { + if (DEBUG) Log.d(TAG, "verifyUnlock()"); + show(null); + mKeyguardView.verifyUnlock(); + } + + /** + * A key has woken the device. We use this to potentially adjust the state + * of the lock screen based on the key. + * + * The 'Tq' suffix is per the documentation in {@link android.view.WindowManagerPolicy}. + * Be sure not to take any action that takes a long time; any significant + * action should be posted to a handler. + * + * @param keyCode The wake key. May be {@link KeyEvent#KEYCODE_UNKNOWN} if waking + * for a reason other than a key press. + */ + public boolean wakeWhenReadyTq(int keyCode) { + if (DEBUG) Log.d(TAG, "wakeWhenReady(" + keyCode + ")"); + if (mKeyguardView != null) { + mKeyguardView.wakeWhenReadyTq(keyCode); + return true; + } + Log.w(TAG, "mKeyguardView is null in wakeWhenReadyTq"); + return false; + } + + /** + * Hides the keyguard view + */ + public synchronized void hide() { + if (DEBUG) Log.d(TAG, "hide()"); + + if (mKeyguardHost != null) { + mKeyguardHost.setVisibility(View.GONE); + + // We really only want to preserve keyguard state for configuration changes. Hence + // we should clear state of widgets (e.g. Music) when we hide keyguard so it can + // start with a fresh state when we return. + mStateContainer.clear(); + + // Don't do this right away, so we can let the view continue to animate + // as it goes away. + if (mKeyguardView != null) { + final KeyguardViewBase lastView = mKeyguardView; + mKeyguardView = null; + mKeyguardHost.postDelayed(new Runnable() { + @Override + public void run() { + synchronized (KeyguardViewManager.this) { + lastView.cleanUp(); + mKeyguardHost.removeView(lastView); + } + } + }, 500); + } + } + } + + /** + * Dismisses the keyguard by going to the next screen or making it gone. + */ + public synchronized void dismiss() { + if (mScreenOn) { + mKeyguardView.dismiss(); + } + } + + /** + * @return Whether the keyguard is showing + */ + public synchronized boolean isShowing() { + return (mKeyguardHost != null && mKeyguardHost.getVisibility() == View.VISIBLE); + } + + public void showAssistant() { + if (mKeyguardView != null) { + mKeyguardView.showAssistant(); + } + } +} diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardViewMediator.java b/packages/Keyguard/src/com/android/keyguard/KeyguardViewMediator.java new file mode 100644 index 0000000..8e10528 --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardViewMediator.java @@ -0,0 +1,1433 @@ +/* + * 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.internal.policy.impl.keyguard; + +import static android.provider.Settings.System.SCREEN_OFF_TIMEOUT; + +import android.app.Activity; +import android.app.ActivityManagerNative; +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.app.SearchManager; +import android.app.StatusBarManager; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.media.AudioManager; +import android.media.SoundPool; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.PowerManager; +import android.os.RemoteException; +import android.os.SystemClock; +import android.os.SystemProperties; +import android.os.UserHandle; +import android.os.UserManager; +import android.provider.Settings; +import android.telephony.TelephonyManager; +import android.util.EventLog; +import android.util.Log; +import android.view.KeyEvent; +import android.view.WindowManager; +import android.view.WindowManagerPolicy; + +import com.android.internal.telephony.IccCardConstants; +import com.android.internal.widget.LockPatternUtils; + + +/** + * Mediates requests related to the keyguard. This includes queries about the + * state of the keyguard, power management events that effect whether the keyguard + * should be shown or reset, callbacks to the phone window manager to notify + * it of when the keyguard is showing, and events from the keyguard view itself + * stating that the keyguard was succesfully unlocked. + * + * Note that the keyguard view is shown when the screen is off (as appropriate) + * so that once the screen comes on, it will be ready immediately. + * + * Example queries about the keyguard: + * - is {movement, key} one that should wake the keygaurd? + * - is the keyguard showing? + * - are input events restricted due to the state of the keyguard? + * + * Callbacks to the phone window manager: + * - the keyguard is showing + * + * Example external events that translate to keyguard view changes: + * - screen turned off -> reset the keyguard, and show it so it will be ready + * next time the screen turns on + * - keyboard is slid open -> if the keyguard is not secure, hide it + * + * Events from the keyguard view: + * - user succesfully unlocked keyguard -> hide keyguard view, and no longer + * restrict input events. + * + * Note: in addition to normal power managment events that effect the state of + * whether the keyguard should be showing, external apps and services may request + * that the keyguard be disabled via {@link #setKeyguardEnabled(boolean)}. When + * false, this will override all other conditions for turning on the keyguard. + * + * Threading and synchronization: + * This class is created by the initialization routine of the {@link WindowManagerPolicy}, + * and runs on its thread. The keyguard UI is created from that thread in the + * constructor of this class. The apis may be called from other threads, including the + * {@link com.android.server.input.InputManagerService}'s and {@link android.view.WindowManager}'s. + * Therefore, methods on this class are synchronized, and any action that is pointed + * directly to the keyguard UI is posted to a {@link Handler} to ensure it is taken on the UI + * thread of the keyguard. + */ +public class KeyguardViewMediator { + private static final int KEYGUARD_DISPLAY_TIMEOUT_DELAY_DEFAULT = 30000; + final static boolean DEBUG = false; + private final static boolean DBG_WAKE = false; + + private final static String TAG = "KeyguardViewMediator"; + + private static final String DELAYED_KEYGUARD_ACTION = + "com.android.internal.policy.impl.PhoneWindowManager.DELAYED_KEYGUARD"; + + // used for handler messages + private static final int SHOW = 2; + private static final int HIDE = 3; + private static final int RESET = 4; + private static final int VERIFY_UNLOCK = 5; + private static final int NOTIFY_SCREEN_OFF = 6; + private static final int NOTIFY_SCREEN_ON = 7; + private static final int WAKE_WHEN_READY = 8; + private static final int KEYGUARD_DONE = 9; + private static final int KEYGUARD_DONE_DRAWING = 10; + private static final int KEYGUARD_DONE_AUTHENTICATING = 11; + private static final int SET_HIDDEN = 12; + private static final int KEYGUARD_TIMEOUT = 13; + private static final int SHOW_ASSISTANT = 14; + + /** + * The default amount of time we stay awake (used for all key input) + */ + protected static final int AWAKE_INTERVAL_DEFAULT_MS = 10000; + + /** + * How long to wait after the screen turns off due to timeout before + * turning on the keyguard (i.e, the user has this much time to turn + * the screen back on without having to face the keyguard). + */ + private static final int KEYGUARD_LOCK_AFTER_DELAY_DEFAULT = 5000; + + /** + * How long we'll wait for the {@link ViewMediatorCallback#keyguardDoneDrawing()} + * callback before unblocking a call to {@link #setKeyguardEnabled(boolean)} + * that is reenabling the keyguard. + */ + private static final int KEYGUARD_DONE_DRAWING_TIMEOUT_MS = 2000; + + /** + * Allow the user to expand the status bar when the keyguard is engaged + * (without a pattern or password). + */ + private static final boolean ENABLE_INSECURE_STATUS_BAR_EXPAND = true; + + /** The stream type that the lock sounds are tied to. */ + private int mMasterStreamType; + + private Context mContext; + private AlarmManager mAlarmManager; + private AudioManager mAudioManager; + private StatusBarManager mStatusBarManager; + private boolean mShowLockIcon; + private boolean mShowingLockIcon; + private boolean mSwitchingUser; + + private boolean mSystemReady; + + // Whether the next call to playSounds() should be skipped. Defaults to + // true because the first lock (on boot) should be silent. + private boolean mSuppressNextLockSound = true; + + + /** High level access to the power manager for WakeLocks */ + private PowerManager mPM; + + /** UserManager for querying number of users */ + private UserManager mUserManager; + + /** SearchManager for determining whether or not search assistant is available */ + private SearchManager mSearchManager; + + /** + * Used to keep the device awake while to ensure the keyguard finishes opening before + * we sleep. + */ + private PowerManager.WakeLock mShowKeyguardWakeLock; + + /** + * Does not turn on screen, held while a call to {@link KeyguardViewManager#wakeWhenReadyTq(int)} + * is called to make sure the device doesn't sleep before it has a chance to poke + * the wake lock. + * @see #wakeWhenReady(int) + */ + private PowerManager.WakeLock mWakeAndHandOff; + + private KeyguardViewManager mKeyguardViewManager; + + // these are protected by synchronized (this) + + /** + * External apps (like the phone app) can tell us to disable the keygaurd. + */ + private boolean mExternallyEnabled = true; + + /** + * Remember if an external call to {@link #setKeyguardEnabled} with value + * false caused us to hide the keyguard, so that we need to reshow it once + * the keygaurd is reenabled with another call with value true. + */ + private boolean mNeedToReshowWhenReenabled = false; + + // cached value of whether we are showing (need to know this to quickly + // answer whether the input should be restricted) + private boolean mShowing = false; + + // true if the keyguard is hidden by another window + private boolean mHidden = false; + + /** + * Helps remember whether the screen has turned on since the last time + * it turned off due to timeout. see {@link #onScreenTurnedOff(int)} + */ + private int mDelayedShowingSequence; + + /** + * If the user has disabled the keyguard, then requests to exit, this is + * how we'll ultimately let them know whether it was successful. We use this + * var being non-null as an indicator that there is an in progress request. + */ + private WindowManagerPolicy.OnKeyguardExitResult mExitSecureCallback; + + // the properties of the keyguard + + private KeyguardUpdateMonitor mUpdateMonitor; + + private boolean mScreenOn; + + // last known state of the cellular connection + private String mPhoneState = TelephonyManager.EXTRA_STATE_IDLE; + + /** + * we send this intent when the keyguard is dismissed. + */ + private Intent mUserPresentIntent; + + /** + * {@link #setKeyguardEnabled} waits on this condition when it reenables + * the keyguard. + */ + private boolean mWaitingUntilKeyguardVisible = false; + private LockPatternUtils mLockPatternUtils; + private boolean mKeyguardDonePending = false; + + private SoundPool mLockSounds; + private int mLockSoundId; + private int mUnlockSoundId; + private int mLockSoundStreamId; + + /** + * The volume applied to the lock/unlock sounds. + */ + private final float mLockSoundVolume; + + /** + * The callback used by the keyguard view to tell the {@link KeyguardViewMediator} + * various things. + */ + public interface ViewMediatorCallback { + + /** + * Wake the device immediately. + */ + void wakeUp(); + + /** + * Reports user activity and requests that the screen stay on. + */ + void userActivity(); + + /** + * Reports user activity and requests that the screen stay on for at least + * the specified amount of time. + * @param millis The amount of time in millis. This value is currently ignored. + */ + void userActivity(long millis); + + /** + * Report that the keyguard is done. + * @param authenticated Whether the user securely got past the keyguard. + * the only reason for this to be false is if the keyguard was instructed + * to appear temporarily to verify the user is supposed to get past the + * keyguard, and the user fails to do so. + */ + void keyguardDone(boolean authenticated); + + /** + * Report that the keyguard is done drawing. + */ + void keyguardDoneDrawing(); + + /** + * Tell ViewMediator that the current view needs IME input + * @param needsInput + */ + void setNeedsInput(boolean needsInput); + + /** + * Tell view mediator that the keyguard view's desired user activity timeout + * has changed and needs to be reapplied to the window. + */ + void onUserActivityTimeoutChanged(); + + /** + * Report that the keyguard is dismissable, pending the next keyguardDone call. + */ + void keyguardDonePending(); + } + + KeyguardUpdateMonitorCallback mUpdateCallback = new KeyguardUpdateMonitorCallback() { + + @Override + public void onUserSwitching(int userId) { + // Note that the mLockPatternUtils user has already been updated from setCurrentUser. + // We need to force a reset of the views, since lockNow (called by + // ActivityManagerService) will not reconstruct the keyguard if it is already showing. + synchronized (KeyguardViewMediator.this) { + mSwitchingUser = true; + resetStateLocked(null); + adjustStatusBarLocked(); + // Disable face unlock when the user switches. + KeyguardUpdateMonitor.getInstance(mContext).setAlternateUnlockEnabled(false); + } + } + + @Override + public void onUserSwitchComplete(int userId) { + mSwitchingUser = false; + } + + @Override + public void onUserRemoved(int userId) { + mLockPatternUtils.removeUser(userId); + } + + @Override + void onPhoneStateChanged(int phoneState) { + synchronized (KeyguardViewMediator.this) { + if (TelephonyManager.CALL_STATE_IDLE == phoneState // call ending + && !mScreenOn // screen off + && mExternallyEnabled) { // not disabled by any app + + // note: this is a way to gracefully reenable the keyguard when the call + // ends and the screen is off without always reenabling the keyguard + // each time the screen turns off while in call (and having an occasional ugly + // flicker while turning back on the screen and disabling the keyguard again). + if (DEBUG) Log.d(TAG, "screen is off and call ended, let's make sure the " + + "keyguard is showing"); + doKeyguardLocked(); + } + } + }; + + @Override + public void onClockVisibilityChanged() { + adjustStatusBarLocked(); + } + + @Override + public void onDeviceProvisioned() { + sendUserPresentBroadcast(); + } + + @Override + public void onSimStateChanged(IccCardConstants.State simState) { + if (DEBUG) Log.d(TAG, "onSimStateChanged: " + simState); + + switch (simState) { + case NOT_READY: + case ABSENT: + // only force lock screen in case of missing sim if user hasn't + // gone through setup wizard + synchronized (this) { + if (!mUpdateMonitor.isDeviceProvisioned()) { + if (!isShowing()) { + if (DEBUG) Log.d(TAG, "ICC_ABSENT isn't showing," + + " we need to show the keyguard since the " + + "device isn't provisioned yet."); + doKeyguardLocked(); + } else { + resetStateLocked(null); + } + } + } + break; + case PIN_REQUIRED: + case PUK_REQUIRED: + synchronized (this) { + if (!isShowing()) { + if (DEBUG) Log.d(TAG, "INTENT_VALUE_ICC_LOCKED and keygaurd isn't " + + "showing; need to show keyguard so user can enter sim pin"); + doKeyguardLocked(); + } else { + resetStateLocked(null); + } + } + break; + case PERM_DISABLED: + synchronized (this) { + if (!isShowing()) { + if (DEBUG) Log.d(TAG, "PERM_DISABLED and " + + "keygaurd isn't showing."); + doKeyguardLocked(); + } else { + if (DEBUG) Log.d(TAG, "PERM_DISABLED, resetStateLocked to" + + "show permanently disabled message in lockscreen."); + resetStateLocked(null); + } + } + break; + case READY: + synchronized (this) { + if (isShowing()) { + resetStateLocked(null); + } + } + break; + } + } + + }; + + ViewMediatorCallback mViewMediatorCallback = new ViewMediatorCallback() { + public void wakeUp() { + KeyguardViewMediator.this.wakeUp(); + } + + public void userActivity() { + KeyguardViewMediator.this.userActivity(); + } + + public void userActivity(long holdMs) { + KeyguardViewMediator.this.userActivity(holdMs); + } + + public void keyguardDone(boolean authenticated) { + KeyguardViewMediator.this.keyguardDone(authenticated, true); + } + + public void keyguardDoneDrawing() { + mHandler.sendEmptyMessage(KEYGUARD_DONE_DRAWING); + } + + @Override + public void setNeedsInput(boolean needsInput) { + mKeyguardViewManager.setNeedsInput(needsInput); + } + + @Override + public void onUserActivityTimeoutChanged() { + mKeyguardViewManager.updateUserActivityTimeout(); + } + + @Override + public void keyguardDonePending() { + mKeyguardDonePending = true; + } + }; + + public void wakeUp() { + mPM.wakeUp(SystemClock.uptimeMillis()); + } + + public void userActivity() { + userActivity(AWAKE_INTERVAL_DEFAULT_MS); + } + + public void userActivity(long holdMs) { + // We ignore the hold time. Eventually we should remove it. + // Instead, the keyguard window has an explicit user activity timeout set on it. + mPM.userActivity(SystemClock.uptimeMillis(), false); + } + + /** + * Construct a KeyguardViewMediator + * @param context + * @param lockPatternUtils optional mock interface for LockPatternUtils + */ + public KeyguardViewMediator(Context context, LockPatternUtils lockPatternUtils) { + mContext = context; + mPM = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); + mShowKeyguardWakeLock = mPM.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "show keyguard"); + mShowKeyguardWakeLock.setReferenceCounted(false); + + mWakeAndHandOff = mPM.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "keyguardWakeAndHandOff"); + mWakeAndHandOff.setReferenceCounted(false); + + mContext.registerReceiver(mBroadcastReceiver, new IntentFilter(DELAYED_KEYGUARD_ACTION)); + + mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + + mUpdateMonitor = KeyguardUpdateMonitor.getInstance(context); + + mLockPatternUtils = lockPatternUtils != null + ? lockPatternUtils : new LockPatternUtils(mContext); + mLockPatternUtils.setCurrentUser(UserHandle.USER_OWNER); + + WindowManager wm = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); + + mKeyguardViewManager = new KeyguardViewManager(context, wm, mViewMediatorCallback, + mLockPatternUtils); + + mUserPresentIntent = new Intent(Intent.ACTION_USER_PRESENT); + mUserPresentIntent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING + | Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); + + final ContentResolver cr = mContext.getContentResolver(); + mShowLockIcon = (Settings.System.getInt(cr, "show_status_bar_lock", 0) == 1); + + mScreenOn = mPM.isScreenOn(); + + mLockSounds = new SoundPool(1, AudioManager.STREAM_SYSTEM, 0); + String soundPath = Settings.Global.getString(cr, Settings.Global.LOCK_SOUND); + if (soundPath != null) { + mLockSoundId = mLockSounds.load(soundPath, 1); + } + if (soundPath == null || mLockSoundId == 0) { + Log.w(TAG, "failed to load lock sound from " + soundPath); + } + soundPath = Settings.Global.getString(cr, Settings.Global.UNLOCK_SOUND); + if (soundPath != null) { + mUnlockSoundId = mLockSounds.load(soundPath, 1); + } + if (soundPath == null || mUnlockSoundId == 0) { + Log.w(TAG, "failed to load unlock sound from " + soundPath); + } + int lockSoundDefaultAttenuation = context.getResources().getInteger( + com.android.internal.R.integer.config_lockSoundVolumeDb); + mLockSoundVolume = (float)Math.pow(10, (float)lockSoundDefaultAttenuation/20); + } + + /** + * Let us know that the system is ready after startup. + */ + public void onSystemReady() { + mSearchManager = (SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE); + synchronized (this) { + if (DEBUG) Log.d(TAG, "onSystemReady"); + mSystemReady = true; + mUpdateMonitor.registerCallback(mUpdateCallback); + + // Suppress biometric unlock right after boot until things have settled if it is the + // selected security method, otherwise unsuppress it. It must be unsuppressed if it is + // not the selected security method for the following reason: if the user starts + // without a screen lock selected, the biometric unlock would be suppressed the first + // time they try to use it. + // + // Note that the biometric unlock will still not show if it is not the selected method. + // Calling setAlternateUnlockEnabled(true) simply says don't suppress it if it is the + // selected method. + if (mLockPatternUtils.usingBiometricWeak() + && mLockPatternUtils.isBiometricWeakInstalled()) { + if (DEBUG) Log.d(TAG, "suppressing biometric unlock during boot"); + mUpdateMonitor.setAlternateUnlockEnabled(false); + } else { + mUpdateMonitor.setAlternateUnlockEnabled(true); + } + + doKeyguardLocked(); + } + // Most services aren't available until the system reaches the ready state, so we + // send it here when the device first boots. + maybeSendUserPresentBroadcast(); + } + + /** + * Called to let us know the screen was turned off. + * @param why either {@link WindowManagerPolicy#OFF_BECAUSE_OF_USER}, + * {@link WindowManagerPolicy#OFF_BECAUSE_OF_TIMEOUT} or + * {@link WindowManagerPolicy#OFF_BECAUSE_OF_PROX_SENSOR}. + */ + public void onScreenTurnedOff(int why) { + synchronized (this) { + mScreenOn = false; + if (DEBUG) Log.d(TAG, "onScreenTurnedOff(" + why + ")"); + + mKeyguardDonePending = false; + + // Lock immediately based on setting if secure (user has a pin/pattern/password). + // This also "locks" the device when not secure to provide easy access to the + // camera while preventing unwanted input. + final boolean lockImmediately = + mLockPatternUtils.getPowerButtonInstantlyLocks() || !mLockPatternUtils.isSecure(); + + if (mExitSecureCallback != null) { + if (DEBUG) Log.d(TAG, "pending exit secure callback cancelled"); + mExitSecureCallback.onKeyguardExitResult(false); + mExitSecureCallback = null; + if (!mExternallyEnabled) { + hideLocked(); + } + } else if (mShowing) { + notifyScreenOffLocked(); + resetStateLocked(null); + } else if (why == WindowManagerPolicy.OFF_BECAUSE_OF_TIMEOUT + || (why == WindowManagerPolicy.OFF_BECAUSE_OF_USER && !lockImmediately)) { + doKeyguardLaterLocked(); + } else if (why == WindowManagerPolicy.OFF_BECAUSE_OF_PROX_SENSOR) { + // Do not enable the keyguard if the prox sensor forced the screen off. + } else { + doKeyguardLocked(); + } + } + } + + private void doKeyguardLaterLocked() { + // if the screen turned off because of timeout or the user hit the power button + // and we don't need to lock immediately, set an alarm + // to enable it a little bit later (i.e, give the user a chance + // to turn the screen back on within a certain window without + // having to unlock the screen) + final ContentResolver cr = mContext.getContentResolver(); + + // From DisplaySettings + long displayTimeout = Settings.System.getInt(cr, SCREEN_OFF_TIMEOUT, + KEYGUARD_DISPLAY_TIMEOUT_DELAY_DEFAULT); + + // From SecuritySettings + final long lockAfterTimeout = Settings.Secure.getInt(cr, + Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT, + KEYGUARD_LOCK_AFTER_DELAY_DEFAULT); + + // From DevicePolicyAdmin + final long policyTimeout = mLockPatternUtils.getDevicePolicyManager() + .getMaximumTimeToLock(null, mLockPatternUtils.getCurrentUser()); + + long timeout; + if (policyTimeout > 0) { + // policy in effect. Make sure we don't go beyond policy limit. + displayTimeout = Math.max(displayTimeout, 0); // ignore negative values + timeout = Math.min(policyTimeout - displayTimeout, lockAfterTimeout); + } else { + timeout = lockAfterTimeout; + } + + if (timeout <= 0) { + // Lock now + mSuppressNextLockSound = true; + doKeyguardLocked(); + } else { + // Lock in the future + long when = SystemClock.elapsedRealtime() + timeout; + Intent intent = new Intent(DELAYED_KEYGUARD_ACTION); + intent.putExtra("seq", mDelayedShowingSequence); + PendingIntent sender = PendingIntent.getBroadcast(mContext, + 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); + mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, when, sender); + if (DEBUG) Log.d(TAG, "setting alarm to turn off keyguard, seq = " + + mDelayedShowingSequence); + } + } + + private void cancelDoKeyguardLaterLocked() { + mDelayedShowingSequence++; + } + + /** + * Let's us know the screen was turned on. + */ + public void onScreenTurnedOn(KeyguardViewManager.ShowListener showListener) { + synchronized (this) { + mScreenOn = true; + cancelDoKeyguardLaterLocked(); + if (DEBUG) Log.d(TAG, "onScreenTurnedOn, seq = " + mDelayedShowingSequence); + if (showListener != null) { + notifyScreenOnLocked(showListener); + } + } + maybeSendUserPresentBroadcast(); + } + + private void maybeSendUserPresentBroadcast() { + if (mSystemReady && mLockPatternUtils.isLockScreenDisabled() + && mUserManager.getUsers(true).size() == 1) { + // Lock screen is disabled because the user has set the preference to "None". + // In this case, send out ACTION_USER_PRESENT here instead of in + // handleKeyguardDone() + sendUserPresentBroadcast(); + } + } + + /** + * A dream started. We should lock after the usual screen-off lock timeout but only + * if there is a secure lock pattern. + */ + public void onDreamingStarted() { + synchronized (this) { + if (mScreenOn && mLockPatternUtils.isSecure()) { + doKeyguardLaterLocked(); + } + } + } + + /** + * A dream stopped. + */ + public void onDreamingStopped() { + synchronized (this) { + if (mScreenOn) { + cancelDoKeyguardLaterLocked(); + } + } + } + + /** + * Same semantics as {@link WindowManagerPolicy#enableKeyguard}; provide + * a way for external stuff to override normal keyguard behavior. For instance + * the phone app disables the keyguard when it receives incoming calls. + */ + public void setKeyguardEnabled(boolean enabled) { + synchronized (this) { + if (DEBUG) Log.d(TAG, "setKeyguardEnabled(" + enabled + ")"); + + mExternallyEnabled = enabled; + + if (!enabled && mShowing) { + if (mExitSecureCallback != null) { + if (DEBUG) Log.d(TAG, "in process of verifyUnlock request, ignoring"); + // we're in the process of handling a request to verify the user + // can get past the keyguard. ignore extraneous requests to disable / reenable + return; + } + + // hiding keyguard that is showing, remember to reshow later + if (DEBUG) Log.d(TAG, "remembering to reshow, hiding keyguard, " + + "disabling status bar expansion"); + mNeedToReshowWhenReenabled = true; + hideLocked(); + } else if (enabled && mNeedToReshowWhenReenabled) { + // reenabled after previously hidden, reshow + if (DEBUG) Log.d(TAG, "previously hidden, reshowing, reenabling " + + "status bar expansion"); + mNeedToReshowWhenReenabled = false; + + if (mExitSecureCallback != null) { + if (DEBUG) Log.d(TAG, "onKeyguardExitResult(false), resetting"); + mExitSecureCallback.onKeyguardExitResult(false); + mExitSecureCallback = null; + resetStateLocked(null); + } else { + showLocked(null); + + // block until we know the keygaurd is done drawing (and post a message + // to unblock us after a timeout so we don't risk blocking too long + // and causing an ANR). + mWaitingUntilKeyguardVisible = true; + mHandler.sendEmptyMessageDelayed(KEYGUARD_DONE_DRAWING, KEYGUARD_DONE_DRAWING_TIMEOUT_MS); + if (DEBUG) Log.d(TAG, "waiting until mWaitingUntilKeyguardVisible is false"); + while (mWaitingUntilKeyguardVisible) { + try { + wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + if (DEBUG) Log.d(TAG, "done waiting for mWaitingUntilKeyguardVisible"); + } + } + } + } + + /** + * @see android.app.KeyguardManager#exitKeyguardSecurely + */ + public void verifyUnlock(WindowManagerPolicy.OnKeyguardExitResult callback) { + synchronized (this) { + if (DEBUG) Log.d(TAG, "verifyUnlock"); + if (!mUpdateMonitor.isDeviceProvisioned()) { + // don't allow this api when the device isn't provisioned + if (DEBUG) Log.d(TAG, "ignoring because device isn't provisioned"); + callback.onKeyguardExitResult(false); + } else if (mExternallyEnabled) { + // this only applies when the user has externally disabled the + // keyguard. this is unexpected and means the user is not + // using the api properly. + Log.w(TAG, "verifyUnlock called when not externally disabled"); + callback.onKeyguardExitResult(false); + } else if (mExitSecureCallback != null) { + // already in progress with someone else + callback.onKeyguardExitResult(false); + } else { + mExitSecureCallback = callback; + verifyUnlockLocked(); + } + } + } + + /** + * Is the keyguard currently showing? + */ + public boolean isShowing() { + return mShowing; + } + + /** + * Is the keyguard currently showing and not being force hidden? + */ + public boolean isShowingAndNotHidden() { + return mShowing && !mHidden; + } + + /** + * Notify us when the keyguard is hidden by another window + */ + public void setHidden(boolean isHidden) { + if (DEBUG) Log.d(TAG, "setHidden " + isHidden); + mUpdateMonitor.sendKeyguardVisibilityChanged(!isHidden); + mHandler.removeMessages(SET_HIDDEN); + Message msg = mHandler.obtainMessage(SET_HIDDEN, (isHidden ? 1 : 0), 0); + mHandler.sendMessage(msg); + } + + /** + * Handles SET_HIDDEN message sent by setHidden() + */ + private void handleSetHidden(boolean isHidden) { + synchronized (KeyguardViewMediator.this) { + if (mHidden != isHidden) { + mHidden = isHidden; + updateActivityLockScreenState(); + adjustStatusBarLocked(); + } + } + } + + /** + * Used by PhoneWindowManager to enable the keyguard due to a user activity timeout. + * This must be safe to call from any thread and with any window manager locks held. + */ + public void doKeyguardTimeout(Bundle options) { + mHandler.removeMessages(KEYGUARD_TIMEOUT); + Message msg = mHandler.obtainMessage(KEYGUARD_TIMEOUT, options); + mHandler.sendMessage(msg); + } + + /** + * Given the state of the keyguard, is the input restricted? + * Input is restricted when the keyguard is showing, or when the keyguard + * was suppressed by an app that disabled the keyguard or we haven't been provisioned yet. + */ + public boolean isInputRestricted() { + return mShowing || mNeedToReshowWhenReenabled || !mUpdateMonitor.isDeviceProvisioned(); + } + + private void doKeyguardLocked() { + doKeyguardLocked(null); + } + + /** + * Enable the keyguard if the settings are appropriate. + */ + private void doKeyguardLocked(Bundle options) { + // if another app is disabling us, don't show + if (!mExternallyEnabled) { + if (DEBUG) Log.d(TAG, "doKeyguard: not showing because externally disabled"); + + // note: we *should* set mNeedToReshowWhenReenabled=true here, but that makes + // for an occasional ugly flicker in this situation: + // 1) receive a call with the screen on (no keyguard) or make a call + // 2) screen times out + // 3) user hits key to turn screen back on + // instead, we reenable the keyguard when we know the screen is off and the call + // ends (see the broadcast receiver below) + // TODO: clean this up when we have better support at the window manager level + // for apps that wish to be on top of the keyguard + return; + } + + // if the keyguard is already showing, don't bother + if (mKeyguardViewManager.isShowing()) { + if (DEBUG) Log.d(TAG, "doKeyguard: not showing because it is already showing"); + return; + } + + // if the setup wizard hasn't run yet, don't show + final boolean requireSim = !SystemProperties.getBoolean("keyguard.no_require_sim", + false); + final boolean provisioned = mUpdateMonitor.isDeviceProvisioned(); + final IccCardConstants.State state = mUpdateMonitor.getSimState(); + final boolean lockedOrMissing = state.isPinLocked() + || ((state == IccCardConstants.State.ABSENT + || state == IccCardConstants.State.PERM_DISABLED) + && requireSim); + + if (!lockedOrMissing && !provisioned) { + if (DEBUG) Log.d(TAG, "doKeyguard: not showing because device isn't provisioned" + + " and the sim is not locked or missing"); + return; + } + + if (mUserManager.getUsers(true).size() < 2 + && mLockPatternUtils.isLockScreenDisabled() && !lockedOrMissing) { + if (DEBUG) Log.d(TAG, "doKeyguard: not showing because lockscreen is off"); + return; + } + + if (DEBUG) Log.d(TAG, "doKeyguard: showing the lock screen"); + showLocked(options); + } + + /** + * Dismiss the keyguard through the security layers. + */ + public void dismiss() { + if (mShowing && !mHidden) { + mKeyguardViewManager.dismiss(); + } + } + + /** + * Send message to keyguard telling it to reset its state. + * @param options options about how to show the keyguard + * @see #handleReset() + */ + private void resetStateLocked(Bundle options) { + if (DEBUG) Log.d(TAG, "resetStateLocked"); + Message msg = mHandler.obtainMessage(RESET, options); + mHandler.sendMessage(msg); + } + + /** + * Send message to keyguard telling it to verify unlock + * @see #handleVerifyUnlock() + */ + private void verifyUnlockLocked() { + if (DEBUG) Log.d(TAG, "verifyUnlockLocked"); + mHandler.sendEmptyMessage(VERIFY_UNLOCK); + } + + + /** + * Send a message to keyguard telling it the screen just turned on. + * @see #onScreenTurnedOff(int) + * @see #handleNotifyScreenOff + */ + private void notifyScreenOffLocked() { + if (DEBUG) Log.d(TAG, "notifyScreenOffLocked"); + mHandler.sendEmptyMessage(NOTIFY_SCREEN_OFF); + } + + /** + * Send a message to keyguard telling it the screen just turned on. + * @see #onScreenTurnedOn() + * @see #handleNotifyScreenOn + */ + private void notifyScreenOnLocked(KeyguardViewManager.ShowListener showListener) { + if (DEBUG) Log.d(TAG, "notifyScreenOnLocked"); + Message msg = mHandler.obtainMessage(NOTIFY_SCREEN_ON, showListener); + mHandler.sendMessage(msg); + } + + /** + * Send message to keyguard telling it about a wake key so it can adjust + * its state accordingly and then poke the wake lock when it is ready. + * @param keyCode The wake key. + * @see #handleWakeWhenReady + * @see #onWakeKeyWhenKeyguardShowingTq(int) + */ + private void wakeWhenReady(int keyCode) { + if (DBG_WAKE) Log.d(TAG, "wakeWhenReady(" + keyCode + ")"); + + /** + * acquire the handoff lock that will keep the cpu running. this will + * be released once the keyguard has set itself up and poked the other wakelock + * in {@link #handleWakeWhenReady(int)} + */ + mWakeAndHandOff.acquire(); + + Message msg = mHandler.obtainMessage(WAKE_WHEN_READY, keyCode, 0); + mHandler.sendMessage(msg); + } + + /** + * Send message to keyguard telling it to show itself + * @see #handleShow() + */ + private void showLocked(Bundle options) { + if (DEBUG) Log.d(TAG, "showLocked"); + // ensure we stay awake until we are finished displaying the keyguard + mShowKeyguardWakeLock.acquire(); + Message msg = mHandler.obtainMessage(SHOW, options); + mHandler.sendMessage(msg); + } + + /** + * Send message to keyguard telling it to hide itself + * @see #handleHide() + */ + private void hideLocked() { + if (DEBUG) Log.d(TAG, "hideLocked"); + Message msg = mHandler.obtainMessage(HIDE); + mHandler.sendMessage(msg); + } + + public boolean isSecure() { + return mLockPatternUtils.isSecure() + || KeyguardUpdateMonitor.getInstance(mContext).isSimPinSecure(); + } + + /** + * Update the newUserId. Call while holding WindowManagerService lock. + * NOTE: Should only be called by KeyguardViewMediator in response to the user id changing. + * + * @param newUserId The id of the incoming user. + */ + public void setCurrentUser(int newUserId) { + mLockPatternUtils.setCurrentUser(newUserId); + } + + private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (DELAYED_KEYGUARD_ACTION.equals(intent.getAction())) { + final int sequence = intent.getIntExtra("seq", 0); + if (DEBUG) Log.d(TAG, "received DELAYED_KEYGUARD_ACTION with seq = " + + sequence + ", mDelayedShowingSequence = " + mDelayedShowingSequence); + synchronized (KeyguardViewMediator.this) { + if (mDelayedShowingSequence == sequence) { + // Don't play lockscreen SFX if the screen went off due to timeout. + mSuppressNextLockSound = true; + doKeyguardLocked(); + } + } + } + } + }; + + /** + * When a key is received when the screen is off and the keyguard is showing, + * we need to decide whether to actually turn on the screen, and if so, tell + * the keyguard to prepare itself and poke the wake lock when it is ready. + * + * The 'Tq' suffix is per the documentation in {@link WindowManagerPolicy}. + * Be sure not to take any action that takes a long time; any significant + * action should be posted to a handler. + * + * @param keyCode The keycode of the key that woke the device + */ + public void onWakeKeyWhenKeyguardShowingTq(int keyCode) { + if (DEBUG) Log.d(TAG, "onWakeKeyWhenKeyguardShowing(" + keyCode + ")"); + + // give the keyguard view manager a chance to adjust the state of the + // keyguard based on the key that woke the device before poking + // the wake lock + wakeWhenReady(keyCode); + } + + /** + * When a wake motion such as an external mouse movement is received when the screen + * is off and the keyguard is showing, we need to decide whether to actually turn + * on the screen, and if so, tell the keyguard to prepare itself and poke the wake + * lock when it is ready. + * + * The 'Tq' suffix is per the documentation in {@link WindowManagerPolicy}. + * Be sure not to take any action that takes a long time; any significant + * action should be posted to a handler. + */ + public void onWakeMotionWhenKeyguardShowingTq() { + if (DEBUG) Log.d(TAG, "onWakeMotionWhenKeyguardShowing()"); + + // give the keyguard view manager a chance to adjust the state of the + // keyguard based on the key that woke the device before poking + // the wake lock + wakeWhenReady(KeyEvent.KEYCODE_UNKNOWN); + } + + public void keyguardDone(boolean authenticated, boolean wakeup) { + mKeyguardDonePending = false; + synchronized (this) { + EventLog.writeEvent(70000, 2); + if (DEBUG) Log.d(TAG, "keyguardDone(" + authenticated + ")"); + Message msg = mHandler.obtainMessage(KEYGUARD_DONE); + msg.arg1 = wakeup ? 1 : 0; + mHandler.sendMessage(msg); + + if (authenticated) { + mUpdateMonitor.clearFailedUnlockAttempts(); + } + + if (mExitSecureCallback != null) { + mExitSecureCallback.onKeyguardExitResult(authenticated); + mExitSecureCallback = null; + + if (authenticated) { + // after succesfully exiting securely, no need to reshow + // the keyguard when they've released the lock + mExternallyEnabled = true; + mNeedToReshowWhenReenabled = false; + } + } + } + } + + /** + * This handler will be associated with the policy thread, which will also + * be the UI thread of the keyguard. Since the apis of the policy, and therefore + * this class, can be called by other threads, any action that directly + * interacts with the keyguard ui should be posted to this handler, rather + * than called directly. + */ + private Handler mHandler = new Handler(Looper.myLooper(), null, true /*async*/) { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case SHOW: + handleShow((Bundle) msg.obj); + return ; + case HIDE: + handleHide(); + return ; + case RESET: + handleReset((Bundle) msg.obj); + return ; + case VERIFY_UNLOCK: + handleVerifyUnlock(); + return; + case NOTIFY_SCREEN_OFF: + handleNotifyScreenOff(); + return; + case NOTIFY_SCREEN_ON: + handleNotifyScreenOn((KeyguardViewManager.ShowListener)msg.obj); + return; + case WAKE_WHEN_READY: + handleWakeWhenReady(msg.arg1); + return; + case KEYGUARD_DONE: + handleKeyguardDone(msg.arg1 != 0); + return; + case KEYGUARD_DONE_DRAWING: + handleKeyguardDoneDrawing(); + return; + case KEYGUARD_DONE_AUTHENTICATING: + keyguardDone(true, true); + return; + case SET_HIDDEN: + handleSetHidden(msg.arg1 != 0); + break; + case KEYGUARD_TIMEOUT: + synchronized (KeyguardViewMediator.this) { + doKeyguardLocked((Bundle) msg.obj); + } + break; + case SHOW_ASSISTANT: + handleShowAssistant(); + break; + } + } + }; + + /** + * @see #keyguardDone + * @see #KEYGUARD_DONE + */ + private void handleKeyguardDone(boolean wakeup) { + if (DEBUG) Log.d(TAG, "handleKeyguardDone"); + handleHide(); + if (wakeup) { + wakeUp(); + } + + sendUserPresentBroadcast(); + } + + private void sendUserPresentBroadcast() { + if (!(mContext instanceof Activity)) { + final UserHandle currentUser = new UserHandle(mLockPatternUtils.getCurrentUser()); + mContext.sendBroadcastAsUser(mUserPresentIntent, currentUser); + } + } + + /** + * @see #keyguardDoneDrawing + * @see #KEYGUARD_DONE_DRAWING + */ + private void handleKeyguardDoneDrawing() { + synchronized(this) { + if (false) Log.d(TAG, "handleKeyguardDoneDrawing"); + if (mWaitingUntilKeyguardVisible) { + if (DEBUG) Log.d(TAG, "handleKeyguardDoneDrawing: notifying mWaitingUntilKeyguardVisible"); + mWaitingUntilKeyguardVisible = false; + notifyAll(); + + // there will usually be two of these sent, one as a timeout, and one + // as a result of the callback, so remove any remaining messages from + // the queue + mHandler.removeMessages(KEYGUARD_DONE_DRAWING); + } + } + } + + private void playSounds(boolean locked) { + // User feedback for keyguard. + + if (mSuppressNextLockSound) { + mSuppressNextLockSound = false; + return; + } + + final ContentResolver cr = mContext.getContentResolver(); + if (Settings.System.getInt(cr, Settings.System.LOCKSCREEN_SOUNDS_ENABLED, 1) == 1) { + final int whichSound = locked + ? mLockSoundId + : mUnlockSoundId; + mLockSounds.stop(mLockSoundStreamId); + // Init mAudioManager + if (mAudioManager == null) { + mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); + if (mAudioManager == null) return; + mMasterStreamType = mAudioManager.getMasterStreamType(); + } + // If the stream is muted, don't play the sound + if (mAudioManager.isStreamMute(mMasterStreamType)) return; + + mLockSoundStreamId = mLockSounds.play(whichSound, + mLockSoundVolume, mLockSoundVolume, 1/*priortiy*/, 0/*loop*/, 1.0f/*rate*/); + } + } + + private void updateActivityLockScreenState() { + try { + ActivityManagerNative.getDefault().setLockScreenShown( + mShowing && !mHidden); + } catch (RemoteException e) { + } + } + + /** + * Handle message sent by {@link #showLocked}. + * @see #SHOW + */ + private void handleShow(Bundle options) { + synchronized (KeyguardViewMediator.this) { + if (DEBUG) Log.d(TAG, "handleShow"); + if (!mSystemReady) return; + + mKeyguardViewManager.show(options); + mShowing = true; + mKeyguardDonePending = false; + updateActivityLockScreenState(); + adjustStatusBarLocked(); + userActivity(); + try { + ActivityManagerNative.getDefault().closeSystemDialogs("lock"); + } catch (RemoteException e) { + } + + // Do this at the end to not slow down display of the keyguard. + playSounds(true); + + mShowKeyguardWakeLock.release(); + } + } + + /** + * Handle message sent by {@link #hideLocked()} + * @see #HIDE + */ + private void handleHide() { + synchronized (KeyguardViewMediator.this) { + if (DEBUG) Log.d(TAG, "handleHide"); + if (mWakeAndHandOff.isHeld()) { + Log.w(TAG, "attempt to hide the keyguard while waking, ignored"); + return; + } + + // only play "unlock" noises if not on a call (since the incall UI + // disables the keyguard) + if (TelephonyManager.EXTRA_STATE_IDLE.equals(mPhoneState)) { + playSounds(false); + } + + mKeyguardViewManager.hide(); + mShowing = false; + mKeyguardDonePending = false; + updateActivityLockScreenState(); + adjustStatusBarLocked(); + } + } + + private void adjustStatusBarLocked() { + if (mStatusBarManager == null) { + mStatusBarManager = (StatusBarManager) + mContext.getSystemService(Context.STATUS_BAR_SERVICE); + } + if (mStatusBarManager == null) { + Log.w(TAG, "Could not get status bar manager"); + } else { + if (mShowLockIcon) { + // Give feedback to user when secure keyguard is active and engaged + if (mShowing && isSecure()) { + if (!mShowingLockIcon) { + String contentDescription = mContext.getString( + com.android.internal.R.string.status_bar_device_locked); + mStatusBarManager.setIcon("secure", + com.android.internal.R.drawable.stat_sys_secure, 0, + contentDescription); + mShowingLockIcon = true; + } + } else { + if (mShowingLockIcon) { + mStatusBarManager.removeIcon("secure"); + mShowingLockIcon = false; + } + } + } + + // Disable aspects of the system/status/navigation bars that must not be re-enabled by + // windows that appear on top, ever + int flags = StatusBarManager.DISABLE_NONE; + if (mShowing) { + // Permanently disable components not available when keyguard is enabled + // (like recents). Temporary enable/disable (e.g. the "back" button) are + // done in KeyguardHostView. + flags |= StatusBarManager.DISABLE_RECENT; + if (isSecure() || !ENABLE_INSECURE_STATUS_BAR_EXPAND) { + // showing secure lockscreen; disable expanding. + flags |= StatusBarManager.DISABLE_EXPAND; + } + if (isSecure()) { + // showing secure lockscreen; disable ticker. + flags |= StatusBarManager.DISABLE_NOTIFICATION_TICKER; + } + if (!isAssistantAvailable()) { + flags |= StatusBarManager.DISABLE_SEARCH; + } + } + + if (DEBUG) { + Log.d(TAG, "adjustStatusBarLocked: mShowing=" + mShowing + " mHidden=" + mHidden + + " isSecure=" + isSecure() + " --> flags=0x" + Integer.toHexString(flags)); + } + + if (!(mContext instanceof Activity)) { + mStatusBarManager.disable(flags); + } + } + } + + /** + * Handle message sent by {@link #wakeWhenReady(int)} + * @param keyCode The key that woke the device. + * @see #WAKE_WHEN_READY + */ + private void handleWakeWhenReady(int keyCode) { + synchronized (KeyguardViewMediator.this) { + if (DBG_WAKE) Log.d(TAG, "handleWakeWhenReady(" + keyCode + ")"); + + // this should result in a call to 'poke wakelock' which will set a timeout + // on releasing the wakelock + if (!mKeyguardViewManager.wakeWhenReadyTq(keyCode)) { + // poke wakelock ourselves if keyguard is no longer active + Log.w(TAG, "mKeyguardViewManager.wakeWhenReadyTq did not poke wake lock, so poke it ourselves"); + userActivity(); + } + + /** + * Now that the keyguard is ready and has poked the wake lock, we can + * release the handoff wakelock + */ + mWakeAndHandOff.release(); + } + } + + /** + * Handle message sent by {@link #resetStateLocked(Bundle)} + * @see #RESET + */ + private void handleReset(Bundle options) { + if (options == null) { + options = new Bundle(); + } + options.putBoolean(KeyguardViewManager.IS_SWITCHING_USER, mSwitchingUser); + synchronized (KeyguardViewMediator.this) { + if (DEBUG) Log.d(TAG, "handleReset"); + mKeyguardViewManager.reset(options); + } + } + + /** + * Handle message sent by {@link #verifyUnlock} + * @see #VERIFY_UNLOCK + */ + private void handleVerifyUnlock() { + synchronized (KeyguardViewMediator.this) { + if (DEBUG) Log.d(TAG, "handleVerifyUnlock"); + mKeyguardViewManager.verifyUnlock(); + mShowing = true; + updateActivityLockScreenState(); + } + } + + /** + * Handle message sent by {@link #notifyScreenOffLocked()} + * @see #NOTIFY_SCREEN_OFF + */ + private void handleNotifyScreenOff() { + synchronized (KeyguardViewMediator.this) { + if (DEBUG) Log.d(TAG, "handleNotifyScreenOff"); + mKeyguardViewManager.onScreenTurnedOff(); + } + } + + /** + * Handle message sent by {@link #notifyScreenOnLocked()} + * @see #NOTIFY_SCREEN_ON + */ + private void handleNotifyScreenOn(KeyguardViewManager.ShowListener showListener) { + synchronized (KeyguardViewMediator.this) { + if (DEBUG) Log.d(TAG, "handleNotifyScreenOn"); + mKeyguardViewManager.onScreenTurnedOn(showListener); + } + } + + public boolean isDismissable() { + return mKeyguardDonePending || !isSecure(); + } + + public void showAssistant() { + Message msg = mHandler.obtainMessage(SHOW_ASSISTANT); + mHandler.sendMessage(msg); + } + + public void handleShowAssistant() { + mKeyguardViewManager.showAssistant(); + } + + private boolean isAssistantAvailable() { + return mSearchManager != null + && mSearchManager.getAssistIntent(mContext, false, UserHandle.USER_CURRENT) != null; + } +} diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardViewStateManager.java b/packages/Keyguard/src/com/android/keyguard/KeyguardViewStateManager.java new file mode 100644 index 0000000..0a166e1 --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardViewStateManager.java @@ -0,0 +1,333 @@ +/* + * 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.internal.policy.impl.keyguard; + +import android.appwidget.AppWidgetManager; +import android.os.Handler; +import android.os.Looper; +import android.view.View; + +public class KeyguardViewStateManager implements + SlidingChallengeLayout.OnChallengeScrolledListener, + ChallengeLayout.OnBouncerStateChangedListener { + + private KeyguardWidgetPager mKeyguardWidgetPager; + private ChallengeLayout mChallengeLayout; + private KeyguardHostView mKeyguardHostView; + private int[] mTmpPoint = new int[2]; + private int[] mTmpLoc = new int[2]; + + private KeyguardSecurityView mKeyguardSecurityContainer; + private static final int SCREEN_ON_HINT_DURATION = 1000; + private static final int SCREEN_ON_RING_HINT_DELAY = 300; + Handler mMainQueue = new Handler(Looper.myLooper()); + + // transport control states + static final int TRANSPORT_GONE = 0; + static final int TRANSPORT_INVISIBLE = 1; + static final int TRANSPORT_VISIBLE = 2; + + private int mTransportState = TRANSPORT_GONE; + + int mLastScrollState = SlidingChallengeLayout.SCROLL_STATE_IDLE; + + // Paged view state + private int mPageListeningToSlider = -1; + private int mCurrentPage = -1; + private int mPageIndexOnPageBeginMoving = -1; + + int mChallengeTop = 0; + + public KeyguardViewStateManager(KeyguardHostView hostView) { + mKeyguardHostView = hostView; + } + + public void setPagedView(KeyguardWidgetPager pagedView) { + mKeyguardWidgetPager = pagedView; + updateEdgeSwiping(); + } + + public void setChallengeLayout(ChallengeLayout layout) { + mChallengeLayout = layout; + updateEdgeSwiping(); + } + + private void updateEdgeSwiping() { + if (mChallengeLayout != null && mKeyguardWidgetPager != null) { + if (mChallengeLayout.isChallengeOverlapping()) { + mKeyguardWidgetPager.setOnlyAllowEdgeSwipes(true); + } else { + mKeyguardWidgetPager.setOnlyAllowEdgeSwipes(false); + } + } + } + + public boolean isChallengeShowing() { + if (mChallengeLayout != null) { + return mChallengeLayout.isChallengeShowing(); + } + return false; + } + + public boolean isChallengeOverlapping() { + if (mChallengeLayout != null) { + return mChallengeLayout.isChallengeOverlapping(); + } + return false; + } + + public void setSecurityViewContainer(KeyguardSecurityView container) { + mKeyguardSecurityContainer = container; + } + + public void showBouncer(boolean show) { + mChallengeLayout.showBouncer(); + } + + public boolean isBouncing() { + return mChallengeLayout.isBouncing(); + } + + public void fadeOutSecurity(int duration) { + ((View) mKeyguardSecurityContainer).animate().alpha(0).setDuration(duration); + } + + public void fadeInSecurity(int duration) { + ((View) mKeyguardSecurityContainer).animate().alpha(1f).setDuration(duration); + } + + public void onPageBeginMoving() { + if (mChallengeLayout.isChallengeOverlapping() && + mChallengeLayout instanceof SlidingChallengeLayout) { + SlidingChallengeLayout scl = (SlidingChallengeLayout) mChallengeLayout; + scl.fadeOutChallenge(); + mPageIndexOnPageBeginMoving = mKeyguardWidgetPager.getCurrentPage(); + } + // We use mAppWidgetToShow to show a particular widget after you add it-- + // once the user swipes a page we clear that behavior + if (mKeyguardHostView != null) { + mKeyguardHostView.clearAppWidgetToShow(); + mKeyguardHostView.setOnDismissAction(null); + } + if (mHideHintsRunnable != null) { + mMainQueue.removeCallbacks(mHideHintsRunnable); + mHideHintsRunnable = null; + } + } + + public void onPageEndMoving() { + mPageIndexOnPageBeginMoving = -1; + } + + public void onPageSwitching(View newPage, int newPageIndex) { + if (mKeyguardWidgetPager != null && mChallengeLayout instanceof SlidingChallengeLayout) { + boolean isCameraPage = newPage instanceof CameraWidgetFrame; + ((SlidingChallengeLayout) mChallengeLayout).setChallengeInteractive(!isCameraPage); + } + + // If the page we're settling to is the same as we started on, and the action of + // moving the page hid the security, we restore it immediately. + if (mPageIndexOnPageBeginMoving == mKeyguardWidgetPager.getNextPage() && + mChallengeLayout instanceof SlidingChallengeLayout) { + SlidingChallengeLayout scl = (SlidingChallengeLayout) mChallengeLayout; + scl.fadeInChallenge(); + mKeyguardWidgetPager.setWidgetToResetOnPageFadeOut(-1); + } + mPageIndexOnPageBeginMoving = -1; + } + + public void onPageSwitched(View newPage, int newPageIndex) { + // Reset the previous page size and ensure the current page is sized appropriately. + // We only modify the page state if it is not currently under control by the slider. + // This prevents conflicts. + + // If the page hasn't switched, don't bother with any of this + if (mCurrentPage == newPageIndex) return; + + if (mKeyguardWidgetPager != null && mChallengeLayout != null) { + KeyguardWidgetFrame prevPage = mKeyguardWidgetPager.getWidgetPageAt(mCurrentPage); + if (prevPage != null && mCurrentPage != mPageListeningToSlider && mCurrentPage + != mKeyguardWidgetPager.getWidgetToResetOnPageFadeOut()) { + prevPage.resetSize(); + } + + KeyguardWidgetFrame newCurPage = mKeyguardWidgetPager.getWidgetPageAt(newPageIndex); + boolean challengeOverlapping = mChallengeLayout.isChallengeOverlapping(); + if (challengeOverlapping && !newCurPage.isSmall() + && mPageListeningToSlider != newPageIndex) { + newCurPage.shrinkWidget(); + } + } + + mCurrentPage = newPageIndex; + } + + private int getChallengeTopRelativeToFrame(KeyguardWidgetFrame frame, int top) { + mTmpPoint[0] = 0; + mTmpPoint[1] = top; + mapPoint((View) mChallengeLayout, frame, mTmpPoint); + return mTmpPoint[1]; + } + + /** + * Simple method to map a point from one view's coordinates to another's. Note: this method + * doesn't account for transforms, so if the views will be transformed, this should not be used. + * + * @param fromView The view to which the point is relative + * @param toView The view into which the point should be mapped + * @param pt The point + */ + private void mapPoint(View fromView, View toView, int pt[]) { + fromView.getLocationInWindow(mTmpLoc); + + int x = mTmpLoc[0]; + int y = mTmpLoc[1]; + + toView.getLocationInWindow(mTmpLoc); + int vX = mTmpLoc[0]; + int vY = mTmpLoc[1]; + + pt[0] += x - vX; + pt[1] += y - vY; + } + + private void userActivity() { + if (mKeyguardHostView != null) { + mKeyguardHostView.onUserActivityTimeoutChanged(); + mKeyguardHostView.userActivity(); + } + } + + @Override + public void onScrollStateChanged(int scrollState) { + if (mKeyguardWidgetPager == null || mChallengeLayout == null) return; + + boolean challengeOverlapping = mChallengeLayout.isChallengeOverlapping(); + + if (scrollState == SlidingChallengeLayout.SCROLL_STATE_IDLE) { + KeyguardWidgetFrame frame = mKeyguardWidgetPager.getWidgetPageAt(mPageListeningToSlider); + if (frame == null) return; + + if (!challengeOverlapping) { + if (!mKeyguardWidgetPager.isPageMoving()) { + frame.resetSize(); + userActivity(); + } else { + mKeyguardWidgetPager.setWidgetToResetOnPageFadeOut(mPageListeningToSlider); + } + } + if (frame.isSmall()) { + // This is to make sure that if the scroller animation gets cut off midway + // that the frame doesn't stay in a partial down position. + frame.setFrameHeight(frame.getSmallFrameHeight()); + } + if (scrollState != SlidingChallengeLayout.SCROLL_STATE_FADING) { + frame.hideFrame(this); + } + updateEdgeSwiping(); + + if (mChallengeLayout.isChallengeShowing()) { + mKeyguardSecurityContainer.onResume(KeyguardSecurityView.VIEW_REVEALED); + } else { + mKeyguardSecurityContainer.onPause(); + } + mPageListeningToSlider = -1; + } else if (mLastScrollState == SlidingChallengeLayout.SCROLL_STATE_IDLE) { + // Whether dragging or settling, if the last state was idle, we use this signal + // to update the current page who will receive events from the sliding challenge. + // We resize the frame as appropriate. + mPageListeningToSlider = mKeyguardWidgetPager.getNextPage(); + KeyguardWidgetFrame frame = mKeyguardWidgetPager.getWidgetPageAt(mPageListeningToSlider); + if (frame == null) return; + + // Skip showing the frame and shrinking the widget if we are + if (!mChallengeLayout.isBouncing()) { + if (scrollState != SlidingChallengeLayout.SCROLL_STATE_FADING) { + frame.showFrame(this); + } + + // As soon as the security begins sliding, the widget becomes small (if it wasn't + // small to begin with). + if (!frame.isSmall()) { + // We need to fetch the final page, in case the pages are in motion. + mPageListeningToSlider = mKeyguardWidgetPager.getNextPage(); + frame.shrinkWidget(false); + } + } else { + if (!frame.isSmall()) { + // We need to fetch the final page, in case the pages are in motion. + mPageListeningToSlider = mKeyguardWidgetPager.getNextPage(); + } + } + + // View is on the move. Pause the security view until it completes. + mKeyguardSecurityContainer.onPause(); + } + mLastScrollState = scrollState; + } + + @Override + public void onScrollPositionChanged(float scrollPosition, int challengeTop) { + mChallengeTop = challengeTop; + KeyguardWidgetFrame frame = mKeyguardWidgetPager.getWidgetPageAt(mPageListeningToSlider); + if (frame != null && mLastScrollState != SlidingChallengeLayout.SCROLL_STATE_FADING) { + frame.adjustFrame(getChallengeTopRelativeToFrame(frame, mChallengeTop)); + } + } + + private Runnable mHideHintsRunnable = new Runnable() { + @Override + public void run() { + if (mKeyguardWidgetPager != null) { + mKeyguardWidgetPager.hideOutlinesAndSidePages(); + } + } + }; + + public void showUsabilityHints() { + mMainQueue.postDelayed( new Runnable() { + @Override + public void run() { + mKeyguardSecurityContainer.showUsabilityHint(); + } + } , SCREEN_ON_RING_HINT_DELAY); + mKeyguardWidgetPager.showInitialPageHints(); + if (mHideHintsRunnable != null) { + mMainQueue.postDelayed(mHideHintsRunnable, SCREEN_ON_HINT_DURATION); + } + } + + public void setTransportState(int state) { + mTransportState = state; + } + + public int getTransportState() { + return mTransportState; + } + + // ChallengeLayout.OnBouncerStateChangedListener + @Override + public void onBouncerStateChanged(boolean bouncerActive) { + if (bouncerActive) { + mKeyguardWidgetPager.zoomOutToBouncer(); + } else { + mKeyguardWidgetPager.zoomInFromBouncer(); + if (mKeyguardHostView != null) { + mKeyguardHostView.setOnDismissAction(null); + } + } + } +} diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardWidgetCarousel.java b/packages/Keyguard/src/com/android/keyguard/KeyguardWidgetCarousel.java new file mode 100644 index 0000000..257fd27 --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardWidgetCarousel.java @@ -0,0 +1,290 @@ +/* + * 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.internal.policy.impl.keyguard; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; + +import com.android.internal.R; + +import java.util.ArrayList; + +public class KeyguardWidgetCarousel extends KeyguardWidgetPager { + + private float mAdjacentPagesAngle; + private static float MAX_SCROLL_PROGRESS = 1.3f; + private static float CAMERA_DISTANCE = 10000; + protected AnimatorSet mChildrenTransformsAnimator; + float[] mTmpTransform = new float[3]; + + public KeyguardWidgetCarousel(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public KeyguardWidgetCarousel(Context context) { + this(context, null, 0); + } + + public KeyguardWidgetCarousel(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + mAdjacentPagesAngle = context.getResources().getInteger(R.integer.kg_carousel_angle); + } + + protected float getMaxScrollProgress() { + return MAX_SCROLL_PROGRESS; + } + + public float getAlphaForPage(int screenCenter, int index, boolean showSidePages) { + View child = getChildAt(index); + if (child == null) return 0f; + + boolean inVisibleRange = index >= getNextPage() - 1 && index <= getNextPage() + 1; + float scrollProgress = getScrollProgress(screenCenter, child, index); + + if (isOverScrollChild(index, scrollProgress)) { + return 1.0f; + } else if ((showSidePages && inVisibleRange) || index == getNextPage()) { + scrollProgress = getBoundedScrollProgress(screenCenter, child, index); + float alpha = 1.0f - 1.0f * Math.abs(scrollProgress / MAX_SCROLL_PROGRESS); + return alpha; + } else { + return 0f; + } + } + + public float getOutlineAlphaForPage(int screenCenter, int index, boolean showSidePages) { + boolean inVisibleRange = index >= getNextPage() - 1 && index <= getNextPage() + 1; + if (inVisibleRange) { + return super.getOutlineAlphaForPage(screenCenter, index, showSidePages); + } else { + return 0f; + } + } + + private void updatePageAlphaValues(int screenCenter) { + if (mChildrenOutlineFadeAnimation != null) { + mChildrenOutlineFadeAnimation.cancel(); + mChildrenOutlineFadeAnimation = null; + } + boolean showSidePages = mShowingInitialHints || isPageMoving(); + if (!isReordering(false)) { + for (int i = 0; i < getChildCount(); i++) { + KeyguardWidgetFrame child = getWidgetPageAt(i); + if (child != null) { + float outlineAlpha = getOutlineAlphaForPage(screenCenter, i, showSidePages); + float contentAlpha = getAlphaForPage(screenCenter, i,showSidePages); + child.setBackgroundAlpha(outlineAlpha); + child.setContentAlpha(contentAlpha); + } + } + } + } + + public void showInitialPageHints() { + mShowingInitialHints = true; + int count = getChildCount(); + for (int i = 0; i < count; i++) { + boolean inVisibleRange = i >= getNextPage() - 1 && i <= getNextPage() + 1; + KeyguardWidgetFrame child = getWidgetPageAt(i); + if (inVisibleRange) { + child.setBackgroundAlpha(KeyguardWidgetFrame.OUTLINE_ALPHA_MULTIPLIER); + child.setContentAlpha(1f); + } else { + child.setBackgroundAlpha(0f); + child.setContentAlpha(0f); + } + } + } + + @Override + protected void screenScrolled(int screenCenter) { + mScreenCenter = screenCenter; + updatePageAlphaValues(screenCenter); + if (isReordering(false)) return; + for (int i = 0; i < getChildCount(); i++) { + KeyguardWidgetFrame v = getWidgetPageAt(i); + float scrollProgress = getScrollProgress(screenCenter, v, i); + float boundedProgress = getBoundedScrollProgress(screenCenter, v, i); + if (v == mDragView || v == null) continue; + v.setCameraDistance(CAMERA_DISTANCE); + + if (isOverScrollChild(i, scrollProgress)) { + v.setRotationY(- OVERSCROLL_MAX_ROTATION * scrollProgress); + v.setOverScrollAmount(Math.abs(scrollProgress), scrollProgress < 0); + } else { + int width = v.getMeasuredWidth(); + float pivotX = (width / 2f) + boundedProgress * (width / 2f); + float pivotY = v.getMeasuredHeight() / 2; + float rotationY = - mAdjacentPagesAngle * boundedProgress; + v.setPivotX(pivotX); + v.setPivotY(pivotY); + v.setRotationY(rotationY); + v.setOverScrollAmount(0f, false); + } + float alpha = v.getAlpha(); + // If the view has 0 alpha, we set it to be invisible so as to prevent + // it from accepting touches + if (alpha == 0) { + v.setVisibility(INVISIBLE); + } else if (v.getVisibility() != VISIBLE) { + v.setVisibility(VISIBLE); + } + } + } + + void animatePagesToNeutral() { + if (mChildrenTransformsAnimator != null) { + mChildrenTransformsAnimator.cancel(); + mChildrenTransformsAnimator = null; + } + + int count = getChildCount(); + PropertyValuesHolder alpha; + PropertyValuesHolder outlineAlpha; + PropertyValuesHolder rotationY; + ArrayList<Animator> anims = new ArrayList<Animator>(); + + for (int i = 0; i < count; i++) { + KeyguardWidgetFrame child = getWidgetPageAt(i); + boolean inVisibleRange = (i >= mCurrentPage - 1 && i <= mCurrentPage + 1); + if (!inVisibleRange) { + child.setRotationY(0f); + } + alpha = PropertyValuesHolder.ofFloat("contentAlpha", 1.0f); + outlineAlpha = PropertyValuesHolder.ofFloat("backgroundAlpha", + KeyguardWidgetFrame.OUTLINE_ALPHA_MULTIPLIER); + rotationY = PropertyValuesHolder.ofFloat("rotationY", 0f); + ObjectAnimator a = ObjectAnimator.ofPropertyValuesHolder(child, alpha, outlineAlpha, rotationY); + child.setVisibility(VISIBLE); + if (!inVisibleRange) { + a.setInterpolator(mSlowFadeInterpolator); + } + anims.add(a); + } + + int duration = REORDERING_ZOOM_IN_OUT_DURATION; + mChildrenTransformsAnimator = new AnimatorSet(); + mChildrenTransformsAnimator.playTogether(anims); + + mChildrenTransformsAnimator.setDuration(duration); + mChildrenTransformsAnimator.start(); + } + + private void getTransformForPage(int screenCenter, int index, float[] transform) { + View child = getChildAt(index); + float boundedProgress = getBoundedScrollProgress(screenCenter, child, index); + float rotationY = - mAdjacentPagesAngle * boundedProgress; + int width = child.getMeasuredWidth(); + float pivotX = (width / 2f) + boundedProgress * (width / 2f); + float pivotY = child.getMeasuredHeight() / 2; + + transform[0] = pivotX; + transform[1] = pivotY; + transform[2] = rotationY; + } + + Interpolator mFastFadeInterpolator = new Interpolator() { + Interpolator mInternal = new DecelerateInterpolator(1.5f); + float mFactor = 2.5f; + @Override + public float getInterpolation(float input) { + return mInternal.getInterpolation(Math.min(mFactor * input, 1f)); + } + }; + + Interpolator mSlowFadeInterpolator = new Interpolator() { + Interpolator mInternal = new AccelerateInterpolator(1.5f); + float mFactor = 1.3f; + @Override + public float getInterpolation(float input) { + input -= (1 - 1 / mFactor); + input = mFactor * Math.max(input, 0f); + return mInternal.getInterpolation(input); + } + }; + + void animatePagesToCarousel() { + if (mChildrenTransformsAnimator != null) { + mChildrenTransformsAnimator.cancel(); + mChildrenTransformsAnimator = null; + } + + int count = getChildCount(); + PropertyValuesHolder alpha; + PropertyValuesHolder outlineAlpha; + PropertyValuesHolder rotationY; + PropertyValuesHolder pivotX; + PropertyValuesHolder pivotY; + ArrayList<Animator> anims = new ArrayList<Animator>(); + + for (int i = 0; i < count; i++) { + KeyguardWidgetFrame child = getWidgetPageAt(i); + float finalAlpha = getAlphaForPage(mScreenCenter, i, true); + float finalOutlineAlpha = getOutlineAlphaForPage(mScreenCenter, i, true); + getTransformForPage(mScreenCenter, i, mTmpTransform); + + boolean inVisibleRange = (i >= mCurrentPage - 1 && i <= mCurrentPage + 1); + + ObjectAnimator a; + alpha = PropertyValuesHolder.ofFloat("contentAlpha", finalAlpha); + outlineAlpha = PropertyValuesHolder.ofFloat("backgroundAlpha", finalOutlineAlpha); + pivotX = PropertyValuesHolder.ofFloat("pivotX", mTmpTransform[0]); + pivotY = PropertyValuesHolder.ofFloat("pivotY", mTmpTransform[1]); + rotationY = PropertyValuesHolder.ofFloat("rotationY", mTmpTransform[2]); + + if (inVisibleRange) { + // for the central pages we animate into a rotated state + a = ObjectAnimator.ofPropertyValuesHolder(child, alpha, outlineAlpha, + pivotX, pivotY, rotationY); + } else { + a = ObjectAnimator.ofPropertyValuesHolder(child, alpha, outlineAlpha); + a.setInterpolator(mFastFadeInterpolator); + } + anims.add(a); + } + + int duration = REORDERING_ZOOM_IN_OUT_DURATION; + mChildrenTransformsAnimator = new AnimatorSet(); + mChildrenTransformsAnimator.playTogether(anims); + + mChildrenTransformsAnimator.setDuration(duration); + mChildrenTransformsAnimator.start(); + } + + protected void reorderStarting() { + mViewStateManager.fadeOutSecurity(REORDERING_ZOOM_IN_OUT_DURATION); + animatePagesToNeutral(); + } + + protected boolean zoomIn(final Runnable onCompleteRunnable) { + animatePagesToCarousel(); + return super.zoomIn(onCompleteRunnable); + } + + @Override + protected void onEndReordering() { + super.onEndReordering(); + mViewStateManager.fadeInSecurity(REORDERING_ZOOM_IN_OUT_DURATION); + } +} diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardWidgetFrame.java b/packages/Keyguard/src/com/android/keyguard/KeyguardWidgetFrame.java new file mode 100644 index 0000000..babb9cb --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardWidgetFrame.java @@ -0,0 +1,529 @@ +/* + * 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.internal.policy.impl.keyguard; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.appwidget.AppWidgetHostView; +import android.appwidget.AppWidgetManager; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.Shader; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.widget.FrameLayout; + +import com.android.internal.R; + +public class KeyguardWidgetFrame extends FrameLayout { + private final static PorterDuffXfermode sAddBlendMode = + new PorterDuffXfermode(PorterDuff.Mode.ADD); + + static final float OUTLINE_ALPHA_MULTIPLIER = 0.6f; + static final int HOVER_OVER_DELETE_DROP_TARGET_OVERLAY_COLOR = 0x99FF0000; + + // Temporarily disable this for the time being until we know why the gfx is messing up + static final boolean ENABLE_HOVER_OVER_DELETE_DROP_TARGET_OVERLAY = true; + + private int mGradientColor; + private LinearGradient mForegroundGradient; + private LinearGradient mLeftToRightGradient; + private LinearGradient mRightToLeftGradient; + private Paint mGradientPaint = new Paint(); + boolean mLeftToRight = true; + + private float mOverScrollAmount = 0f; + private final Rect mForegroundRect = new Rect(); + private int mForegroundAlpha = 0; + private CheckLongPressHelper mLongPressHelper; + private Animator mFrameFade; + private boolean mIsSmall = false; + private Handler mWorkerHandler; + + private float mBackgroundAlpha; + private float mContentAlpha; + private float mBackgroundAlphaMultiplier = 1.0f; + private Drawable mBackgroundDrawable; + private Rect mBackgroundRect = new Rect(); + + // These variables are all needed in order to size things properly before we're actually + // measured. + private int mSmallWidgetHeight; + private int mSmallFrameHeight; + private boolean mWidgetLockedSmall = false; + private int mMaxChallengeTop = -1; + private int mFrameStrokeAdjustment; + private boolean mPerformAppWidgetSizeUpdateOnBootComplete; + + // This will hold the width value before we've actually been measured + private int mFrameHeight; + + private boolean mIsHoveringOverDeleteDropTarget; + + // Multiple callers may try and adjust the alpha of the frame. When a caller shows + // the outlines, we give that caller control, and nobody else can fade them out. + // This prevents animation conflicts. + private Object mBgAlphaController; + + public KeyguardWidgetFrame(Context context) { + this(context, null, 0); + } + + public KeyguardWidgetFrame(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public KeyguardWidgetFrame(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + mLongPressHelper = new CheckLongPressHelper(this); + + Resources res = context.getResources(); + // TODO: this padding should really correspond to the padding embedded in the background + // drawable (ie. outlines). + float density = res.getDisplayMetrics().density; + int padding = (int) (res.getDisplayMetrics().density * 8); + setPadding(padding, padding, padding, padding); + + mFrameStrokeAdjustment = 2 + (int) (2 * density); + + // This will be overriden on phones based on the current security mode, however on tablets + // we need to specify a height. + mSmallWidgetHeight = + res.getDimensionPixelSize(com.android.internal.R.dimen.kg_small_widget_height); + mBackgroundDrawable = res.getDrawable(R.drawable.kg_widget_bg_padded); + mGradientColor = res.getColor(com.android.internal.R.color.kg_widget_pager_gradient); + mGradientPaint.setXfermode(sAddBlendMode); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + cancelLongPress(); + KeyguardUpdateMonitor.getInstance(mContext).removeCallback(mUpdateMonitorCallbacks); + + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + KeyguardUpdateMonitor.getInstance(mContext).registerCallback(mUpdateMonitorCallbacks); + } + + private KeyguardUpdateMonitorCallback mUpdateMonitorCallbacks = + new KeyguardUpdateMonitorCallback() { + @Override + public void onBootCompleted() { + if (mPerformAppWidgetSizeUpdateOnBootComplete) { + performAppWidgetSizeCallbacksIfNecessary(); + mPerformAppWidgetSizeUpdateOnBootComplete = false; + } + } + }; + + void setIsHoveringOverDeleteDropTarget(boolean isHovering) { + if (ENABLE_HOVER_OVER_DELETE_DROP_TARGET_OVERLAY) { + if (mIsHoveringOverDeleteDropTarget != isHovering) { + mIsHoveringOverDeleteDropTarget = isHovering; + invalidate(); + } + } + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + // Watch for longpress events at this level to make sure + // users can always pick up this widget + switch (ev.getAction()) { + case MotionEvent.ACTION_DOWN: + mLongPressHelper.postCheckForLongPress(ev); + break; + case MotionEvent.ACTION_MOVE: + mLongPressHelper.onMove(ev); + break; + case MotionEvent.ACTION_POINTER_DOWN: + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mLongPressHelper.cancelLongPress(); + break; + } + + // Otherwise continue letting touch events fall through to children + return false; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + // Watch for longpress events at this level to make sure + // users can always pick up this widget + switch (ev.getAction()) { + case MotionEvent.ACTION_MOVE: + mLongPressHelper.onMove(ev); + break; + case MotionEvent.ACTION_POINTER_DOWN: + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mLongPressHelper.cancelLongPress(); + break; + } + + // We return true here to ensure that we will get cancel / up signal + // even if none of our children have requested touch. + return true; + } + + @Override + public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { + super.requestDisallowInterceptTouchEvent(disallowIntercept); + cancelLongPress(); + } + + @Override + public void cancelLongPress() { + super.cancelLongPress(); + mLongPressHelper.cancelLongPress(); + } + + + private void drawGradientOverlay(Canvas c) { + mGradientPaint.setShader(mForegroundGradient); + mGradientPaint.setAlpha(mForegroundAlpha); + c.drawRect(mForegroundRect, mGradientPaint); + } + + private void drawHoveringOverDeleteOverlay(Canvas c) { + if (mIsHoveringOverDeleteDropTarget) { + c.drawColor(HOVER_OVER_DELETE_DROP_TARGET_OVERLAY_COLOR); + } + } + + protected void drawBg(Canvas canvas) { + if (mBackgroundAlpha > 0.0f) { + Drawable bg = mBackgroundDrawable; + + bg.setAlpha((int) (mBackgroundAlpha * mBackgroundAlphaMultiplier * 255)); + bg.setBounds(mBackgroundRect); + bg.draw(canvas); + } + } + + @Override + protected void dispatchDraw(Canvas canvas) { + if (ENABLE_HOVER_OVER_DELETE_DROP_TARGET_OVERLAY) { + canvas.save(); + } + drawBg(canvas); + super.dispatchDraw(canvas); + drawGradientOverlay(canvas); + if (ENABLE_HOVER_OVER_DELETE_DROP_TARGET_OVERLAY) { + drawHoveringOverDeleteOverlay(canvas); + canvas.restore(); + } + } + + /** + * Because this view has fading outlines, it is essential that we enable hardware + * layers on the content (child) so that updating the alpha of the outlines doesn't + * result in the content layer being recreated. + */ + public void enableHardwareLayersForContent() { + View widget = getContent(); + if (widget != null) { + widget.setLayerType(LAYER_TYPE_HARDWARE, null); + } + } + + /** + * Because this view has fading outlines, it is essential that we enable hardware + * layers on the content (child) so that updating the alpha of the outlines doesn't + * result in the content layer being recreated. + */ + public void disableHardwareLayersForContent() { + View widget = getContent(); + if (widget != null) { + widget.setLayerType(LAYER_TYPE_NONE, null); + } + } + + public void enableHardwareLayers() { + setLayerType(LAYER_TYPE_HARDWARE, null); + } + + public void disableHardwareLayers() { + setLayerType(LAYER_TYPE_NONE, null); + } + + public View getContent() { + return getChildAt(0); + } + + public int getContentAppWidgetId() { + View content = getContent(); + if (content instanceof AppWidgetHostView) { + return ((AppWidgetHostView) content).getAppWidgetId(); + } else if (content instanceof KeyguardStatusView) { + return ((KeyguardStatusView) content).getAppWidgetId(); + } else { + return AppWidgetManager.INVALID_APPWIDGET_ID; + } + } + + public float getBackgroundAlpha() { + return mBackgroundAlpha; + } + + public void setBackgroundAlphaMultiplier(float multiplier) { + if (Float.compare(mBackgroundAlphaMultiplier, multiplier) != 0) { + mBackgroundAlphaMultiplier = multiplier; + invalidate(); + } + } + + public float getBackgroundAlphaMultiplier() { + return mBackgroundAlphaMultiplier; + } + + public void setBackgroundAlpha(float alpha) { + if (Float.compare(mBackgroundAlpha, alpha) != 0) { + mBackgroundAlpha = alpha; + invalidate(); + } + } + + public float getContentAlpha() { + return mContentAlpha; + } + + public void setContentAlpha(float alpha) { + mContentAlpha = alpha; + View content = getContent(); + if (content != null) { + content.setAlpha(alpha); + } + } + + /** + * Depending on whether the security is up, the widget size needs to change + * + * @param height The height of the widget, -1 for full height + */ + private void setWidgetHeight(int height) { + boolean needLayout = false; + View widget = getContent(); + if (widget != null) { + LayoutParams lp = (LayoutParams) widget.getLayoutParams(); + if (lp.height != height) { + needLayout = true; + lp.height = height; + } + } + if (needLayout) { + requestLayout(); + } + } + + public void setMaxChallengeTop(int top) { + boolean dirty = mMaxChallengeTop != top; + mMaxChallengeTop = top; + mSmallWidgetHeight = top - getPaddingTop(); + mSmallFrameHeight = top + getPaddingBottom(); + if (dirty && mIsSmall) { + setWidgetHeight(mSmallWidgetHeight); + setFrameHeight(mSmallFrameHeight); + } else if (dirty && mWidgetLockedSmall) { + setWidgetHeight(mSmallWidgetHeight); + } + } + + public boolean isSmall() { + return mIsSmall; + } + + public void adjustFrame(int challengeTop) { + int frameHeight = challengeTop + getPaddingBottom(); + setFrameHeight(frameHeight); + } + + public void shrinkWidget(boolean alsoShrinkFrame) { + mIsSmall = true; + setWidgetHeight(mSmallWidgetHeight); + + if (alsoShrinkFrame) { + setFrameHeight(mSmallFrameHeight); + } + } + + public int getSmallFrameHeight() { + return mSmallFrameHeight; + } + + public void shrinkWidget() { + shrinkWidget(true); + } + + public void setWidgetLockedSmall(boolean locked) { + if (locked) { + setWidgetHeight(mSmallWidgetHeight); + } + mWidgetLockedSmall = locked; + } + + public void resetSize() { + mIsSmall = false; + if (!mWidgetLockedSmall) { + setWidgetHeight(LayoutParams.MATCH_PARENT); + } + setFrameHeight(getMeasuredHeight()); + } + + public void setFrameHeight(int height) { + mFrameHeight = height; + mBackgroundRect.set(0, 0, getMeasuredWidth(), Math.min(mFrameHeight, getMeasuredHeight())); + mForegroundRect.set(mFrameStrokeAdjustment, mFrameStrokeAdjustment,getMeasuredWidth() - + mFrameStrokeAdjustment, Math.min(getMeasuredHeight(), mFrameHeight) - + mFrameStrokeAdjustment); + updateGradient(); + invalidate(); + } + + public void hideFrame(Object caller) { + fadeFrame(caller, false, 0f, KeyguardWidgetPager.CHILDREN_OUTLINE_FADE_OUT_DURATION); + } + + public void showFrame(Object caller) { + fadeFrame(caller, true, OUTLINE_ALPHA_MULTIPLIER, + KeyguardWidgetPager.CHILDREN_OUTLINE_FADE_IN_DURATION); + } + + public void fadeFrame(Object caller, boolean takeControl, float alpha, int duration) { + if (takeControl) { + mBgAlphaController = caller; + } + + if (mBgAlphaController != caller && mBgAlphaController != null) { + return; + } + + if (mFrameFade != null) { + mFrameFade.cancel(); + mFrameFade = null; + } + PropertyValuesHolder bgAlpha = PropertyValuesHolder.ofFloat("backgroundAlpha", alpha); + mFrameFade = ObjectAnimator.ofPropertyValuesHolder(this, bgAlpha); + mFrameFade.setDuration(duration); + mFrameFade.start(); + } + + private void updateGradient() { + float x0 = mLeftToRight ? 0 : mForegroundRect.width(); + float x1 = mLeftToRight ? mForegroundRect.width(): 0; + mLeftToRightGradient = new LinearGradient(x0, 0f, x1, 0f, + mGradientColor, 0, Shader.TileMode.CLAMP); + mRightToLeftGradient = new LinearGradient(x1, 0f, x0, 0f, + mGradientColor, 0, Shader.TileMode.CLAMP); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + + if (!mIsSmall) { + mFrameHeight = h; + } + + // mFrameStrokeAdjustment is a cludge to prevent the overlay from drawing outside the + // rounded rect background. + mForegroundRect.set(mFrameStrokeAdjustment, mFrameStrokeAdjustment, + w - mFrameStrokeAdjustment, Math.min(h, mFrameHeight) - mFrameStrokeAdjustment); + + mBackgroundRect.set(0, 0, getMeasuredWidth(), Math.min(h, mFrameHeight)); + updateGradient(); + invalidate(); + } + + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + performAppWidgetSizeCallbacksIfNecessary(); + } + + private void performAppWidgetSizeCallbacksIfNecessary() { + View content = getContent(); + if (!(content instanceof AppWidgetHostView)) return; + + if (!KeyguardUpdateMonitor.getInstance(mContext).hasBootCompleted()) { + mPerformAppWidgetSizeUpdateOnBootComplete = true; + return; + } + + // TODO: there's no reason to force the AppWidgetHostView to catch duplicate size calls. + // We can do that even more cheaply here. It's not an issue right now since we're in the + // system process and hence no binder calls. + AppWidgetHostView awhv = (AppWidgetHostView) content; + float density = getResources().getDisplayMetrics().density; + + int width = (int) (content.getMeasuredWidth() / density); + int height = (int) (content.getMeasuredHeight() / density); + awhv.updateAppWidgetSize(null, width, height, width, height, true); + } + + void setOverScrollAmount(float r, boolean left) { + if (Float.compare(mOverScrollAmount, r) != 0) { + mOverScrollAmount = r; + mForegroundGradient = left ? mLeftToRightGradient : mRightToLeftGradient; + mForegroundAlpha = (int) Math.round((0.5f * r * 255)); + + // We bump up the alpha of the outline to hide the fact that the overlay is drawing + // over the rounded part of the frame. + float bgAlpha = Math.min(OUTLINE_ALPHA_MULTIPLIER + r * (1 - OUTLINE_ALPHA_MULTIPLIER), + 1f); + setBackgroundAlpha(bgAlpha); + invalidate(); + } + } + + public void onActive(boolean isActive) { + // hook for subclasses + } + + public boolean onUserInteraction(MotionEvent event) { + // hook for subclasses + return false; + } + + public void onBouncerShowing(boolean showing) { + // hook for subclasses + } + + public void setWorkerHandler(Handler workerHandler) { + mWorkerHandler = workerHandler; + } + + public Handler getWorkerHandler() { + return mWorkerHandler; + } + +} diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardWidgetPager.java b/packages/Keyguard/src/com/android/keyguard/KeyguardWidgetPager.java new file mode 100644 index 0000000..ad5e257 --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardWidgetPager.java @@ -0,0 +1,920 @@ +/* + * 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.internal.policy.impl.keyguard; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.animation.TimeInterpolator; +import android.appwidget.AppWidgetHostView; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProviderInfo; +import android.content.Context; +import android.os.Handler; +import android.os.HandlerThread; +import android.text.format.DateFormat; +import android.util.AttributeSet; +import android.util.Slog; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnLongClickListener; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.view.animation.DecelerateInterpolator; +import android.widget.FrameLayout; +import android.widget.TextClock; + +import com.android.internal.widget.LockPatternUtils; + +import java.util.ArrayList; +import java.util.TimeZone; + +public class KeyguardWidgetPager extends PagedView implements PagedView.PageSwitchListener, + OnLongClickListener, ChallengeLayout.OnBouncerStateChangedListener { + + ZInterpolator mZInterpolator = new ZInterpolator(0.5f); + private static float CAMERA_DISTANCE = 10000; + protected static float OVERSCROLL_MAX_ROTATION = 30; + private static final boolean PERFORM_OVERSCROLL_ROTATION = true; + + private static final int FLAG_HAS_LOCAL_HOUR = 0x1; + private static final int FLAG_HAS_LOCAL_MINUTE = 0x2; + + protected KeyguardViewStateManager mViewStateManager; + private LockPatternUtils mLockPatternUtils; + + // Related to the fading in / out background outlines + public static final int CHILDREN_OUTLINE_FADE_OUT_DURATION = 375; + public static final int CHILDREN_OUTLINE_FADE_IN_DURATION = 100; + protected AnimatorSet mChildrenOutlineFadeAnimation; + protected int mScreenCenter; + private boolean mHasMeasure = false; + boolean showHintsAfterLayout = false; + + private static final long CUSTOM_WIDGET_USER_ACTIVITY_TIMEOUT = 30000; + private static final String TAG = "KeyguardWidgetPager"; + private boolean mCenterSmallWidgetsVertically; + + private int mPage = 0; + private Callbacks mCallbacks; + + private int mWidgetToResetAfterFadeOut; + protected boolean mShowingInitialHints = false; + + // A temporary handle to the Add-Widget view + private View mAddWidgetView; + private int mLastWidthMeasureSpec; + private int mLastHeightMeasureSpec; + + // Bouncer + private int mBouncerZoomInOutDuration = 250; + private float BOUNCER_SCALE_FACTOR = 0.67f; + + // Background worker thread: used here for persistence, also made available to widget frames + private final HandlerThread mBackgroundWorkerThread; + private final Handler mBackgroundWorkerHandler; + + public KeyguardWidgetPager(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public KeyguardWidgetPager(Context context) { + this(null, null, 0); + } + + public KeyguardWidgetPager(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + if (getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); + } + + setPageSwitchListener(this); + + mBackgroundWorkerThread = new HandlerThread("KeyguardWidgetPager Worker"); + mBackgroundWorkerThread.start(); + mBackgroundWorkerHandler = new Handler(mBackgroundWorkerThread.getLooper()); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + // Clean up the worker thread + mBackgroundWorkerThread.quit(); + } + + public void setViewStateManager(KeyguardViewStateManager viewStateManager) { + mViewStateManager = viewStateManager; + } + + public void setLockPatternUtils(LockPatternUtils l) { + mLockPatternUtils = l; + } + + @Override + public void onPageSwitching(View newPage, int newPageIndex) { + if (mViewStateManager != null) { + mViewStateManager.onPageSwitching(newPage, newPageIndex); + } + } + + @Override + public void onPageSwitched(View newPage, int newPageIndex) { + boolean showingClock = false; + if (newPage instanceof ViewGroup) { + ViewGroup vg = (ViewGroup) newPage; + if (vg.getChildAt(0) instanceof KeyguardStatusView) { + showingClock = true; + } + } + + if (newPage != null && + findClockInHierarchy(newPage) == (FLAG_HAS_LOCAL_HOUR | FLAG_HAS_LOCAL_MINUTE)) { + showingClock = true; + } + + // Disable the status bar clock if we're showing the default status widget + if (showingClock) { + setSystemUiVisibility(getSystemUiVisibility() | View.STATUS_BAR_DISABLE_CLOCK); + } else { + setSystemUiVisibility(getSystemUiVisibility() & ~View.STATUS_BAR_DISABLE_CLOCK); + } + + // Extend the display timeout if the user switches pages + if (mPage != newPageIndex) { + int oldPageIndex = mPage; + mPage = newPageIndex; + userActivity(); + KeyguardWidgetFrame oldWidgetPage = getWidgetPageAt(oldPageIndex); + if (oldWidgetPage != null) { + oldWidgetPage.onActive(false); + } + KeyguardWidgetFrame newWidgetPage = getWidgetPageAt(newPageIndex); + if (newWidgetPage != null) { + newWidgetPage.onActive(true); + newWidgetPage.requestAccessibilityFocus(); + } + if (mParent != null && AccessibilityManager.getInstance(mContext).isEnabled()) { + AccessibilityEvent event = AccessibilityEvent.obtain( + AccessibilityEvent.TYPE_VIEW_SCROLLED); + onInitializeAccessibilityEvent(event); + onPopulateAccessibilityEvent(event); + mParent.requestSendAccessibilityEvent(this, event); + } + } + if (mViewStateManager != null) { + mViewStateManager.onPageSwitched(newPage, newPageIndex); + } + } + + @Override + public void sendAccessibilityEvent(int eventType) { + if (eventType != AccessibilityEvent.TYPE_VIEW_SCROLLED || isPageMoving()) { + super.sendAccessibilityEvent(eventType); + } + } + + private void updateWidgetFramesImportantForAccessibility() { + final int pageCount = getPageCount(); + for (int i = 0; i < pageCount; i++) { + KeyguardWidgetFrame frame = getWidgetPageAt(i); + updateWidgetFrameImportantForAccessibility(frame); + } + } + + private void updateWidgetFrameImportantForAccessibility(KeyguardWidgetFrame frame) { + if (frame.getContentAlpha() <= 0) { + frame.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); + } else { + frame.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); + } + } + + private void userActivity() { + if (mCallbacks != null) { + mCallbacks.onUserActivityTimeoutChanged(); + mCallbacks.userActivity(); + } + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + return captureUserInteraction(ev) || super.onTouchEvent(ev); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + return captureUserInteraction(ev) || super.onInterceptTouchEvent(ev); + } + + private boolean captureUserInteraction(MotionEvent ev) { + KeyguardWidgetFrame currentWidgetPage = getWidgetPageAt(getCurrentPage()); + return currentWidgetPage != null && currentWidgetPage.onUserInteraction(ev); + } + + public void showPagingFeedback() { + // Nothing yet. + } + + public long getUserActivityTimeout() { + View page = getPageAt(mPage); + if (page instanceof ViewGroup) { + ViewGroup vg = (ViewGroup) page; + View view = vg.getChildAt(0); + if (!(view instanceof KeyguardStatusView) + && !(view instanceof KeyguardMultiUserSelectorView)) { + return CUSTOM_WIDGET_USER_ACTIVITY_TIMEOUT; + } + } + return -1; + } + + public void setCallbacks(Callbacks callbacks) { + mCallbacks = callbacks; + } + + public interface Callbacks { + public void userActivity(); + public void onUserActivityTimeoutChanged(); + public void onAddView(View v); + public void onRemoveView(View v, boolean deletePermanently); + public void onRemoveViewAnimationCompleted(); + } + + public void addWidget(View widget) { + addWidget(widget, -1); + } + + public void onRemoveView(View v, final boolean deletePermanently) { + final int appWidgetId = ((KeyguardWidgetFrame) v).getContentAppWidgetId(); + if (mCallbacks != null) { + mCallbacks.onRemoveView(v, deletePermanently); + } + mBackgroundWorkerHandler.post(new Runnable() { + @Override + public void run() { + mLockPatternUtils.removeAppWidget(appWidgetId); + } + }); + } + + @Override + public void onRemoveViewAnimationCompleted() { + if (mCallbacks != null) { + mCallbacks.onRemoveViewAnimationCompleted(); + } + } + + public void onAddView(View v, final int index) { + final int appWidgetId = ((KeyguardWidgetFrame) v).getContentAppWidgetId(); + final int[] pagesRange = new int[mTempVisiblePagesRange.length]; + getVisiblePages(pagesRange); + boundByReorderablePages(true, pagesRange); + if (mCallbacks != null) { + mCallbacks.onAddView(v); + } + // Subtract from the index to take into account pages before the reorderable + // pages (e.g. the "add widget" page) + mBackgroundWorkerHandler.post(new Runnable() { + @Override + public void run() { + mLockPatternUtils.addAppWidget(appWidgetId, index - pagesRange[0]); + } + }); + } + + /* + * We wrap widgets in a special frame which handles drawing the over scroll foreground. + */ + public void addWidget(View widget, int pageIndex) { + KeyguardWidgetFrame frame; + // All views contained herein should be wrapped in a KeyguardWidgetFrame + if (!(widget instanceof KeyguardWidgetFrame)) { + frame = new KeyguardWidgetFrame(getContext()); + FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT); + lp.gravity = Gravity.TOP; + + // The framework adds a default padding to AppWidgetHostView. We don't need this padding + // for the Keyguard, so we override it to be 0. + widget.setPadding(0, 0, 0, 0); + frame.addView(widget, lp); + + // We set whether or not this widget supports vertical resizing. + if (widget instanceof AppWidgetHostView) { + AppWidgetHostView awhv = (AppWidgetHostView) widget; + AppWidgetProviderInfo info = awhv.getAppWidgetInfo(); + if ((info.resizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0) { + frame.setWidgetLockedSmall(false); + } else { + // Lock the widget to be small. + frame.setWidgetLockedSmall(true); + if (mCenterSmallWidgetsVertically) { + lp.gravity = Gravity.CENTER; + } + } + } + } else { + frame = (KeyguardWidgetFrame) widget; + } + + ViewGroup.LayoutParams pageLp = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); + frame.setOnLongClickListener(this); + frame.setWorkerHandler(mBackgroundWorkerHandler); + + if (pageIndex == -1) { + addView(frame, pageLp); + } else { + addView(frame, pageIndex, pageLp); + } + + // Update the frame content description. + View content = (widget == frame) ? frame.getContent() : widget; + if (content != null) { + String contentDescription = mContext.getString( + com.android.internal.R.string.keyguard_accessibility_widget, + content.getContentDescription()); + frame.setContentDescription(contentDescription); + } + updateWidgetFrameImportantForAccessibility(frame); + } + + /** + * Use addWidget() instead. + * @deprecated + */ + @Override + public void addView(View child, int index) { + enforceKeyguardWidgetFrame(child); + super.addView(child, index); + } + + /** + * Use addWidget() instead. + * @deprecated + */ + @Override + public void addView(View child, int width, int height) { + enforceKeyguardWidgetFrame(child); + super.addView(child, width, height); + } + + /** + * Use addWidget() instead. + * @deprecated + */ + @Override + public void addView(View child, LayoutParams params) { + enforceKeyguardWidgetFrame(child); + super.addView(child, params); + } + + /** + * Use addWidget() instead. + * @deprecated + */ + @Override + public void addView(View child, int index, LayoutParams params) { + enforceKeyguardWidgetFrame(child); + super.addView(child, index, params); + } + + private void enforceKeyguardWidgetFrame(View child) { + if (!(child instanceof KeyguardWidgetFrame)) { + throw new IllegalArgumentException( + "KeyguardWidgetPager children must be KeyguardWidgetFrames"); + } + } + + public KeyguardWidgetFrame getWidgetPageAt(int index) { + // This is always a valid cast as we've guarded the ability to + return (KeyguardWidgetFrame) getChildAt(index); + } + + protected void onUnhandledTap(MotionEvent ev) { + showPagingFeedback(); + } + + @Override + protected void onPageBeginMoving() { + if (mViewStateManager != null) { + mViewStateManager.onPageBeginMoving(); + } + if (!isReordering(false)) { + showOutlinesAndSidePages(); + } + userActivity(); + } + + @Override + protected void onPageEndMoving() { + if (mViewStateManager != null) { + mViewStateManager.onPageEndMoving(); + } + + // In the reordering case, the pages will be faded appropriately on completion + // of the zoom in animation. + if (!isReordering(false)) { + hideOutlinesAndSidePages(); + } + } + + protected void enablePageContentLayers() { + int children = getChildCount(); + for (int i = 0; i < children; i++) { + getWidgetPageAt(i).enableHardwareLayersForContent(); + } + } + + protected void disablePageContentLayers() { + int children = getChildCount(); + for (int i = 0; i < children; i++) { + getWidgetPageAt(i).disableHardwareLayersForContent(); + } + } + + /* + * This interpolator emulates the rate at which the perceived scale of an object changes + * as its distance from a camera increases. When this interpolator is applied to a scale + * animation on a view, it evokes the sense that the object is shrinking due to moving away + * from the camera. + */ + static class ZInterpolator implements TimeInterpolator { + private float focalLength; + + public ZInterpolator(float foc) { + focalLength = foc; + } + + public float getInterpolation(float input) { + return (1.0f - focalLength / (focalLength + input)) / + (1.0f - focalLength / (focalLength + 1.0f)); + } + } + + @Override + protected void overScroll(float amount) { + acceleratedOverScroll(amount); + } + + float backgroundAlphaInterpolator(float r) { + return Math.min(1f, r); + } + + private void updatePageAlphaValues(int screenCenter) { + } + + public float getAlphaForPage(int screenCenter, int index, boolean showSidePages) { + if (showSidePages) { + return 1f; + } else { + return index == mCurrentPage ? 1.0f : 0f; + } + } + + public float getOutlineAlphaForPage(int screenCenter, int index, boolean showSidePages) { + if (showSidePages) { + return getAlphaForPage(screenCenter, index, showSidePages) + * KeyguardWidgetFrame.OUTLINE_ALPHA_MULTIPLIER; + } else { + return 0f; + } + } + + protected boolean isOverScrollChild(int index, float scrollProgress) { + boolean isInOverscroll = mOverScrollX < 0 || mOverScrollX > mMaxScrollX; + return (isInOverscroll && (index == 0 && scrollProgress < 0 || + index == getChildCount() - 1 && scrollProgress > 0)); + } + + @Override + protected void screenScrolled(int screenCenter) { + mScreenCenter = screenCenter; + updatePageAlphaValues(screenCenter); + for (int i = 0; i < getChildCount(); i++) { + KeyguardWidgetFrame v = getWidgetPageAt(i); + if (v == mDragView) continue; + if (v != null) { + float scrollProgress = getScrollProgress(screenCenter, v, i); + + v.setCameraDistance(mDensity * CAMERA_DISTANCE); + + if (isOverScrollChild(i, scrollProgress) && PERFORM_OVERSCROLL_ROTATION) { + float pivotX = v.getMeasuredWidth() / 2; + float pivotY = v.getMeasuredHeight() / 2; + v.setPivotX(pivotX); + v.setPivotY(pivotY); + v.setRotationY(- OVERSCROLL_MAX_ROTATION * scrollProgress); + v.setOverScrollAmount(Math.abs(scrollProgress), scrollProgress < 0); + } else { + v.setRotationY(0f); + v.setOverScrollAmount(0, false); + } + + float alpha = v.getAlpha(); + // If the view has 0 alpha, we set it to be invisible so as to prevent + // it from accepting touches + if (alpha == 0) { + v.setVisibility(INVISIBLE); + } else if (v.getVisibility() != VISIBLE) { + v.setVisibility(VISIBLE); + } + } + } + } + + public boolean isWidgetPage(int pageIndex) { + if (pageIndex < 0 || pageIndex >= getChildCount()) { + return false; + } + View v = getChildAt(pageIndex); + if (v != null && v instanceof KeyguardWidgetFrame) { + KeyguardWidgetFrame kwf = (KeyguardWidgetFrame) v; + return kwf.getContentAppWidgetId() != AppWidgetManager.INVALID_APPWIDGET_ID; + } + return false; + } + + /** + * Returns the bounded set of pages that are re-orderable. The range is fully inclusive. + */ + @Override + void boundByReorderablePages(boolean isReordering, int[] range) { + if (isReordering) { + // Remove non-widget pages from the range + while (range[1] >= range[0] && !isWidgetPage(range[1])) { + range[1]--; + } + while (range[0] <= range[1] && !isWidgetPage(range[0])) { + range[0]++; + } + } + } + + protected void reorderStarting() { + showOutlinesAndSidePages(); + } + + @Override + protected void onStartReordering() { + super.onStartReordering(); + enablePageContentLayers(); + reorderStarting(); + } + + @Override + protected void onEndReordering() { + super.onEndReordering(); + hideOutlinesAndSidePages(); + } + + void showOutlinesAndSidePages() { + animateOutlinesAndSidePages(true); + } + + void hideOutlinesAndSidePages() { + animateOutlinesAndSidePages(false); + } + + public void showInitialPageHints() { + mShowingInitialHints = true; + int count = getChildCount(); + for (int i = 0; i < count; i++) { + KeyguardWidgetFrame child = getWidgetPageAt(i); + if (i != mCurrentPage) { + child.setBackgroundAlpha(KeyguardWidgetFrame.OUTLINE_ALPHA_MULTIPLIER); + child.setContentAlpha(0f); + } else { + child.setBackgroundAlpha(0f); + child.setContentAlpha(1f); + } + } + } + + @Override + void setCurrentPage(int currentPage) { + super.setCurrentPage(currentPage); + updateWidgetFramesImportantForAccessibility(); + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + mHasMeasure = false; + } + + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + mLastWidthMeasureSpec = widthMeasureSpec; + mLastHeightMeasureSpec = heightMeasureSpec; + + int maxChallengeTop = -1; + View parent = (View) getParent(); + boolean challengeShowing = false; + // Widget pages need to know where the top of the sliding challenge is so that they + // now how big the widget should be when the challenge is up. We compute it here and + // then propagate it to each of our children. + if (parent.getParent() instanceof SlidingChallengeLayout) { + SlidingChallengeLayout scl = (SlidingChallengeLayout) parent.getParent(); + int top = scl.getMaxChallengeTop(); + + // This is a bit evil, but we need to map a coordinate relative to the SCL into a + // coordinate relative to our children, hence we subtract the top padding.s + maxChallengeTop = top - getPaddingTop(); + challengeShowing = scl.isChallengeShowing(); + + int count = getChildCount(); + for (int i = 0; i < count; i++) { + KeyguardWidgetFrame frame = getWidgetPageAt(i); + frame.setMaxChallengeTop(maxChallengeTop); + // On the very first measure pass, if the challenge is showing, we need to make sure + // that the widget on the current page is small. + if (challengeShowing && i == mCurrentPage && !mHasMeasure) { + frame.shrinkWidget(); + } + } + } + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + mHasMeasure = true; + } + + void animateOutlinesAndSidePages(final boolean show) { + animateOutlinesAndSidePages(show, -1); + } + + public void setWidgetToResetOnPageFadeOut(int widget) { + mWidgetToResetAfterFadeOut = widget; + } + + public int getWidgetToResetOnPageFadeOut() { + return mWidgetToResetAfterFadeOut; + } + + void animateOutlinesAndSidePages(final boolean show, int duration) { + if (mChildrenOutlineFadeAnimation != null) { + mChildrenOutlineFadeAnimation.cancel(); + mChildrenOutlineFadeAnimation = null; + } + int count = getChildCount(); + PropertyValuesHolder alpha; + ArrayList<Animator> anims = new ArrayList<Animator>(); + + if (duration == -1) { + duration = show ? CHILDREN_OUTLINE_FADE_IN_DURATION : + CHILDREN_OUTLINE_FADE_OUT_DURATION; + } + + int curPage = getNextPage(); + for (int i = 0; i < count; i++) { + float finalContentAlpha; + if (show) { + finalContentAlpha = getAlphaForPage(mScreenCenter, i, true); + } else if (!show && i == curPage) { + finalContentAlpha = 1f; + } else { + finalContentAlpha = 0f; + } + KeyguardWidgetFrame child = getWidgetPageAt(i); + + alpha = PropertyValuesHolder.ofFloat("contentAlpha", finalContentAlpha); + ObjectAnimator a = ObjectAnimator.ofPropertyValuesHolder(child, alpha); + anims.add(a); + + float finalOutlineAlpha = show ? getOutlineAlphaForPage(mScreenCenter, i, true) : 0f; + child.fadeFrame(this, show, finalOutlineAlpha, duration); + } + + mChildrenOutlineFadeAnimation = new AnimatorSet(); + mChildrenOutlineFadeAnimation.playTogether(anims); + + mChildrenOutlineFadeAnimation.setDuration(duration); + mChildrenOutlineFadeAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + if (show) { + enablePageContentLayers(); + } + } + + @Override + public void onAnimationEnd(Animator animation) { + if (!show) { + disablePageContentLayers(); + KeyguardWidgetFrame frame = getWidgetPageAt(mWidgetToResetAfterFadeOut); + if (frame != null && !(frame == getWidgetPageAt(mCurrentPage) && + mViewStateManager.isChallengeOverlapping())) { + frame.resetSize(); + } + mWidgetToResetAfterFadeOut = -1; + mShowingInitialHints = false; + } + updateWidgetFramesImportantForAccessibility(); + } + }); + mChildrenOutlineFadeAnimation.start(); + } + + @Override + public boolean onLongClick(View v) { + // Disallow long pressing to reorder if the challenge is showing + boolean isChallengeOverlapping = mViewStateManager.isChallengeShowing() && + mViewStateManager.isChallengeOverlapping(); + if (!isChallengeOverlapping && startReordering()) { + return true; + } + return false; + } + + public void removeWidget(View view) { + if (view instanceof KeyguardWidgetFrame) { + removeView(view); + } else { + // Assume view was wrapped by a KeyguardWidgetFrame in KeyguardWidgetPager#addWidget(). + // This supports legacy hard-coded "widgets" like KeyguardTransportControlView. + int pos = getWidgetPageIndex(view); + if (pos != -1) { + KeyguardWidgetFrame frame = (KeyguardWidgetFrame) getChildAt(pos); + frame.removeView(view); + removeView(frame); + } else { + Slog.w(TAG, "removeWidget() can't find:" + view); + } + } + } + + public int getWidgetPageIndex(View view) { + if (view instanceof KeyguardWidgetFrame) { + return indexOfChild(view); + } else { + // View was wrapped by a KeyguardWidgetFrame by KeyguardWidgetPager#addWidget() + return indexOfChild((KeyguardWidgetFrame)view.getParent()); + } + } + + @Override + protected void setPageHoveringOverDeleteDropTarget(int viewIndex, boolean isHovering) { + KeyguardWidgetFrame child = getWidgetPageAt(viewIndex); + child.setIsHoveringOverDeleteDropTarget(isHovering); + } + + // ChallengeLayout.OnBouncerStateChangedListener + @Override + public void onBouncerStateChanged(boolean bouncerActive) { + if (bouncerActive) { + zoomOutToBouncer(); + } else { + zoomInFromBouncer(); + } + } + + void setBouncerAnimationDuration(int duration) { + mBouncerZoomInOutDuration = duration; + } + + // Zoom in after the bouncer is dismissed + void zoomInFromBouncer() { + if (mZoomInOutAnim != null && mZoomInOutAnim.isRunning()) { + mZoomInOutAnim.cancel(); + } + final View currentPage = getPageAt(getCurrentPage()); + if (currentPage.getScaleX() < 1f || currentPage.getScaleY() < 1f) { + mZoomInOutAnim = new AnimatorSet(); + mZoomInOutAnim.playTogether( + ObjectAnimator.ofFloat(currentPage, "scaleX", 1f), + ObjectAnimator.ofFloat(currentPage , "scaleY", 1f)); + mZoomInOutAnim.setDuration(mBouncerZoomInOutDuration); + mZoomInOutAnim.setInterpolator(new DecelerateInterpolator(1.5f)); + mZoomInOutAnim.start(); + } + if (currentPage instanceof KeyguardWidgetFrame) { + ((KeyguardWidgetFrame)currentPage).onBouncerShowing(false); + } + } + + // Zoom out after the bouncer is initiated + void zoomOutToBouncer() { + if (mZoomInOutAnim != null && mZoomInOutAnim.isRunning()) { + mZoomInOutAnim.cancel(); + } + int curPage = getCurrentPage(); + View currentPage = getPageAt(curPage); + if (shouldSetTopAlignedPivotForWidget(curPage)) { + currentPage.setPivotY(0); + // Note: we are working around the issue that setting the x-pivot to the same value as it + // was does not actually work. + currentPage.setPivotX(0); + currentPage.setPivotX(currentPage.getMeasuredWidth() / 2); + } + if (!(currentPage.getScaleX() < 1f || currentPage.getScaleY() < 1f)) { + mZoomInOutAnim = new AnimatorSet(); + mZoomInOutAnim.playTogether( + ObjectAnimator.ofFloat(currentPage, "scaleX", BOUNCER_SCALE_FACTOR), + ObjectAnimator.ofFloat(currentPage, "scaleY", BOUNCER_SCALE_FACTOR)); + mZoomInOutAnim.setDuration(mBouncerZoomInOutDuration); + mZoomInOutAnim.setInterpolator(new DecelerateInterpolator(1.5f)); + mZoomInOutAnim.start(); + } + if (currentPage instanceof KeyguardWidgetFrame) { + ((KeyguardWidgetFrame)currentPage).onBouncerShowing(true); + } + } + + void setAddWidgetEnabled(boolean enabled) { + if (mAddWidgetView != null && enabled) { + addView(mAddWidgetView, 0); + // We need to force measure the PagedView so that the calls to update the scroll + // position below work + measure(mLastWidthMeasureSpec, mLastHeightMeasureSpec); + // Bump up the current page to account for the addition of the new page + setCurrentPage(mCurrentPage + 1); + mAddWidgetView = null; + } else if (mAddWidgetView == null && !enabled) { + View addWidget = findViewById(com.android.internal.R.id.keyguard_add_widget); + if (addWidget != null) { + mAddWidgetView = addWidget; + removeView(addWidget); + } + } + } + + boolean isAddPage(int pageIndex) { + View v = getChildAt(pageIndex); + return v != null && v.getId() == com.android.internal.R.id.keyguard_add_widget; + } + + boolean isCameraPage(int pageIndex) { + View v = getChildAt(pageIndex); + return v != null && v instanceof CameraWidgetFrame; + } + + @Override + protected boolean shouldSetTopAlignedPivotForWidget(int childIndex) { + return !isCameraPage(childIndex) && super.shouldSetTopAlignedPivotForWidget(childIndex); + } + + /** + * Search given {@link View} hierarchy for {@link TextClock} instances that + * show various time components. Returns combination of + * {@link #FLAG_HAS_LOCAL_HOUR} and {@link #FLAG_HAS_LOCAL_MINUTE}. + */ + private static int findClockInHierarchy(View view) { + if (view instanceof TextClock) { + return getClockFlags((TextClock) view); + } else if (view instanceof ViewGroup) { + int flags = 0; + final ViewGroup group = (ViewGroup) view; + final int size = group.getChildCount(); + for (int i = 0; i < size; i++) { + flags |= findClockInHierarchy(group.getChildAt(i)); + } + return flags; + } else { + return 0; + } + } + + /** + * Return combination of {@link #FLAG_HAS_LOCAL_HOUR} and + * {@link #FLAG_HAS_LOCAL_MINUTE} describing the time represented described + * by the given {@link TextClock}. + */ + private static int getClockFlags(TextClock clock) { + int flags = 0; + + final String timeZone = clock.getTimeZone(); + if (timeZone != null && !TimeZone.getDefault().equals(TimeZone.getTimeZone(timeZone))) { + // Ignore clocks showing another timezone + return 0; + } + + final CharSequence format = clock.getFormat(); + final char hour = clock.is24HourModeEnabled() ? DateFormat.HOUR_OF_DAY + : DateFormat.HOUR; + + if (DateFormat.hasDesignator(format, hour)) { + flags |= FLAG_HAS_LOCAL_HOUR; + } + if (DateFormat.hasDesignator(format, DateFormat.MINUTE)) { + flags |= FLAG_HAS_LOCAL_MINUTE; + } + + return flags; + } +} diff --git a/packages/Keyguard/src/com/android/keyguard/LiftToActivateListener.java b/packages/Keyguard/src/com/android/keyguard/LiftToActivateListener.java new file mode 100644 index 0000000..818108c --- /dev/null +++ b/packages/Keyguard/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.internal.policy.impl.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/Keyguard/src/com/android/keyguard/MultiPaneChallengeLayout.java b/packages/Keyguard/src/com/android/keyguard/MultiPaneChallengeLayout.java new file mode 100644 index 0000000..0ca46c3 --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/MultiPaneChallengeLayout.java @@ -0,0 +1,566 @@ +/* + * 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.internal.policy.impl.keyguard; + +import com.android.internal.R; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; + +public class MultiPaneChallengeLayout extends ViewGroup implements ChallengeLayout { + private static final String TAG = "MultiPaneChallengeLayout"; + + final int mOrientation; + private boolean mIsBouncing; + + public static final int HORIZONTAL = LinearLayout.HORIZONTAL; + public static final int VERTICAL = LinearLayout.VERTICAL; + public static final int ANIMATE_BOUNCE_DURATION = 350; + + private KeyguardSecurityContainer mChallengeView; + private View mUserSwitcherView; + private View mScrimView; + private OnBouncerStateChangedListener mBouncerListener; + + private final Rect mTempRect = new Rect(); + private final Rect mZeroPadding = new Rect(); + + private final DisplayMetrics mDisplayMetrics; + + private final OnClickListener mScrimClickListener = new OnClickListener() { + @Override + public void onClick(View v) { + hideBouncer(); + } + }; + + public MultiPaneChallengeLayout(Context context) { + this(context, null); + } + + public MultiPaneChallengeLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public MultiPaneChallengeLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + final TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.MultiPaneChallengeLayout, defStyleAttr, 0); + mOrientation = a.getInt(R.styleable.MultiPaneChallengeLayout_orientation, + HORIZONTAL); + a.recycle(); + + final Resources res = getResources(); + mDisplayMetrics = res.getDisplayMetrics(); + + setSystemUiVisibility(SYSTEM_UI_FLAG_LAYOUT_STABLE); + } + + @Override + public boolean isChallengeShowing() { + return true; + } + + @Override + public boolean isChallengeOverlapping() { + return false; + } + + @Override + public void showChallenge(boolean b) { + } + + @Override + public int getBouncerAnimationDuration() { + return ANIMATE_BOUNCE_DURATION; + } + + @Override + public void showBouncer() { + if (mIsBouncing) return; + mIsBouncing = true; + if (mScrimView != null) { + if (mChallengeView != null) { + mChallengeView.showBouncer(ANIMATE_BOUNCE_DURATION); + } + + Animator anim = ObjectAnimator.ofFloat(mScrimView, "alpha", 1f); + anim.setDuration(ANIMATE_BOUNCE_DURATION); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + mScrimView.setVisibility(VISIBLE); + } + }); + anim.start(); + } + if (mBouncerListener != null) { + mBouncerListener.onBouncerStateChanged(true); + } + } + + @Override + public void hideBouncer() { + if (!mIsBouncing) return; + mIsBouncing = false; + if (mScrimView != null) { + if (mChallengeView != null) { + mChallengeView.hideBouncer(ANIMATE_BOUNCE_DURATION); + } + + Animator anim = ObjectAnimator.ofFloat(mScrimView, "alpha", 0f); + anim.setDuration(ANIMATE_BOUNCE_DURATION); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mScrimView.setVisibility(INVISIBLE); + } + }); + anim.start(); + } + if (mBouncerListener != null) { + mBouncerListener.onBouncerStateChanged(false); + } + } + + @Override + public boolean isBouncing() { + return mIsBouncing; + } + + @Override + public void setOnBouncerStateChangedListener(OnBouncerStateChangedListener listener) { + mBouncerListener = listener; + } + + @Override + public void requestChildFocus(View child, View focused) { + if (mIsBouncing && child != mChallengeView) { + // Clear out of the bouncer if the user tries to move focus outside of + // the security challenge view. + hideBouncer(); + } + super.requestChildFocus(child, focused); + } + + void setScrimView(View scrim) { + if (mScrimView != null) { + mScrimView.setOnClickListener(null); + } + mScrimView = scrim; + mScrimView.setAlpha(mIsBouncing ? 1.0f : 0.0f); + mScrimView.setVisibility(mIsBouncing ? VISIBLE : INVISIBLE); + mScrimView.setFocusable(true); + mScrimView.setOnClickListener(mScrimClickListener); + } + + private int getVirtualHeight(LayoutParams lp, int height, int heightUsed) { + int virtualHeight = height; + final View root = getRootView(); + if (root != null) { + // This calculation is super dodgy and relies on several assumptions. + // Specifically that the root of the window will be padded in for insets + // and that the window is LAYOUT_IN_SCREEN. + virtualHeight = mDisplayMetrics.heightPixels - root.getPaddingTop(); + } + if (lp.childType == LayoutParams.CHILD_TYPE_WIDGET || + lp.childType == LayoutParams.CHILD_TYPE_USER_SWITCHER) { + // Always measure the widget pager/user switcher as if there were no IME insets + // on the window. We want to avoid resizing widgets when possible as it can + // be ugly/expensive. This lets us simply clip them instead. + return virtualHeight - heightUsed; + } else if (lp.childType == LayoutParams.CHILD_TYPE_PAGE_DELETE_DROP_TARGET) { + return height; + } + return Math.min(virtualHeight - heightUsed, height); + } + + @Override + protected void onMeasure(final int widthSpec, final int heightSpec) { + if (MeasureSpec.getMode(widthSpec) != MeasureSpec.EXACTLY || + MeasureSpec.getMode(heightSpec) != MeasureSpec.EXACTLY) { + throw new IllegalArgumentException( + "MultiPaneChallengeLayout must be measured with an exact size"); + } + + final int width = MeasureSpec.getSize(widthSpec); + final int height = MeasureSpec.getSize(heightSpec); + setMeasuredDimension(width, height); + + int widthUsed = 0; + int heightUsed = 0; + + // First pass. Find the challenge view and measure the user switcher, + // which consumes space in the layout. + mChallengeView = null; + mUserSwitcherView = null; + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + if (lp.childType == LayoutParams.CHILD_TYPE_CHALLENGE) { + if (mChallengeView != null) { + throw new IllegalStateException( + "There may only be one child of type challenge"); + } + if (!(child instanceof KeyguardSecurityContainer)) { + throw new IllegalArgumentException( + "Challenge must be a KeyguardSecurityContainer"); + } + mChallengeView = (KeyguardSecurityContainer) child; + } else if (lp.childType == LayoutParams.CHILD_TYPE_USER_SWITCHER) { + if (mUserSwitcherView != null) { + throw new IllegalStateException( + "There may only be one child of type userSwitcher"); + } + mUserSwitcherView = child; + + if (child.getVisibility() == GONE) continue; + + int adjustedWidthSpec = widthSpec; + int adjustedHeightSpec = heightSpec; + if (lp.maxWidth >= 0) { + adjustedWidthSpec = MeasureSpec.makeMeasureSpec( + Math.min(lp.maxWidth, width), MeasureSpec.EXACTLY); + } + if (lp.maxHeight >= 0) { + adjustedHeightSpec = MeasureSpec.makeMeasureSpec( + Math.min(lp.maxHeight, height), MeasureSpec.EXACTLY); + } + // measureChildWithMargins will resolve layout direction for the LayoutParams + measureChildWithMargins(child, adjustedWidthSpec, 0, adjustedHeightSpec, 0); + + // Only subtract out space from one dimension. Favor vertical. + // Offset by 1.5x to add some balance along the other edge. + if (Gravity.isVertical(lp.gravity)) { + heightUsed += child.getMeasuredHeight() * 1.5f; + } else if (Gravity.isHorizontal(lp.gravity)) { + widthUsed += child.getMeasuredWidth() * 1.5f; + } + } else if (lp.childType == LayoutParams.CHILD_TYPE_SCRIM) { + setScrimView(child); + child.measure(widthSpec, heightSpec); + } + } + + // Second pass. Measure everything that's left. + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + if (lp.childType == LayoutParams.CHILD_TYPE_USER_SWITCHER || + lp.childType == LayoutParams.CHILD_TYPE_SCRIM || + child.getVisibility() == GONE) { + // Don't need to measure GONE children, and the user switcher was already measured. + continue; + } + + final int virtualHeight = getVirtualHeight(lp, height, heightUsed); + + int adjustedWidthSpec; + int adjustedHeightSpec; + if (lp.centerWithinArea > 0) { + if (mOrientation == HORIZONTAL) { + adjustedWidthSpec = MeasureSpec.makeMeasureSpec( + (int) ((width - widthUsed) * lp.centerWithinArea + 0.5f), + MeasureSpec.EXACTLY); + adjustedHeightSpec = MeasureSpec.makeMeasureSpec( + virtualHeight, MeasureSpec.EXACTLY); + } else { + adjustedWidthSpec = MeasureSpec.makeMeasureSpec( + width - widthUsed, MeasureSpec.EXACTLY); + adjustedHeightSpec = MeasureSpec.makeMeasureSpec( + (int) (virtualHeight * lp.centerWithinArea + 0.5f), + MeasureSpec.EXACTLY); + } + } else { + adjustedWidthSpec = MeasureSpec.makeMeasureSpec( + width - widthUsed, MeasureSpec.EXACTLY); + adjustedHeightSpec = MeasureSpec.makeMeasureSpec( + virtualHeight, MeasureSpec.EXACTLY); + } + if (lp.maxWidth >= 0) { + adjustedWidthSpec = MeasureSpec.makeMeasureSpec( + Math.min(lp.maxWidth, MeasureSpec.getSize(adjustedWidthSpec)), + MeasureSpec.EXACTLY); + } + if (lp.maxHeight >= 0) { + adjustedHeightSpec = MeasureSpec.makeMeasureSpec( + Math.min(lp.maxHeight, MeasureSpec.getSize(adjustedHeightSpec)), + MeasureSpec.EXACTLY); + } + + measureChildWithMargins(child, adjustedWidthSpec, 0, adjustedHeightSpec, 0); + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + final Rect padding = mTempRect; + padding.left = getPaddingLeft(); + padding.top = getPaddingTop(); + padding.right = getPaddingRight(); + padding.bottom = getPaddingBottom(); + final int width = r - l; + final int height = b - t; + + // Reserve extra space in layout for the user switcher by modifying + // local padding during this layout pass + if (mUserSwitcherView != null && mUserSwitcherView.getVisibility() != GONE) { + layoutWithGravity(width, height, mUserSwitcherView, padding, true); + } + + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + // We did the user switcher above if we have one. + if (child == mUserSwitcherView || child.getVisibility() == GONE) continue; + + if (child == mScrimView) { + child.layout(0, 0, width, height); + continue; + } else if (lp.childType == LayoutParams.CHILD_TYPE_PAGE_DELETE_DROP_TARGET) { + layoutWithGravity(width, height, child, mZeroPadding, false); + continue; + } + + layoutWithGravity(width, height, child, padding, false); + } + } + + private void layoutWithGravity(int width, int height, View child, Rect padding, + boolean adjustPadding) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + final int heightUsed = padding.top + padding.bottom - getPaddingTop() - getPaddingBottom(); + height = getVirtualHeight(lp, height, heightUsed); + + final int gravity = Gravity.getAbsoluteGravity(lp.gravity, getLayoutDirection()); + + final boolean fixedLayoutSize = lp.centerWithinArea > 0; + final boolean fixedLayoutHorizontal = fixedLayoutSize && mOrientation == HORIZONTAL; + final boolean fixedLayoutVertical = fixedLayoutSize && mOrientation == VERTICAL; + + final int adjustedWidth; + final int adjustedHeight; + if (fixedLayoutHorizontal) { + final int paddedWidth = width - padding.left - padding.right; + adjustedWidth = (int) (paddedWidth * lp.centerWithinArea + 0.5f); + adjustedHeight = height; + } else if (fixedLayoutVertical) { + final int paddedHeight = height - getPaddingTop() - getPaddingBottom(); + adjustedWidth = width; + adjustedHeight = (int) (paddedHeight * lp.centerWithinArea + 0.5f); + } else { + adjustedWidth = width; + adjustedHeight = height; + } + + final boolean isVertical = Gravity.isVertical(gravity); + final boolean isHorizontal = Gravity.isHorizontal(gravity); + final int childWidth = child.getMeasuredWidth(); + final int childHeight = child.getMeasuredHeight(); + + int left = padding.left; + int top = padding.top; + int right = left + childWidth; + int bottom = top + childHeight; + switch (gravity & Gravity.VERTICAL_GRAVITY_MASK) { + case Gravity.TOP: + top = fixedLayoutVertical ? + padding.top + (adjustedHeight - childHeight) / 2 : padding.top; + bottom = top + childHeight; + if (adjustPadding && isVertical) { + padding.top = bottom; + padding.bottom += childHeight / 2; + } + break; + case Gravity.BOTTOM: + bottom = fixedLayoutVertical + ? padding.top + height - (adjustedHeight - childHeight) / 2 + : padding.top + height; + top = bottom - childHeight; + if (adjustPadding && isVertical) { + padding.bottom = height - top; + padding.top += childHeight / 2; + } + break; + case Gravity.CENTER_VERTICAL: + top = padding.top + (height - childHeight) / 2; + bottom = top + childHeight; + break; + } + switch (gravity & Gravity.HORIZONTAL_GRAVITY_MASK) { + case Gravity.LEFT: + left = fixedLayoutHorizontal ? + padding.left + (adjustedWidth - childWidth) / 2 : padding.left; + right = left + childWidth; + if (adjustPadding && isHorizontal && !isVertical) { + padding.left = right; + padding.right += childWidth / 2; + } + break; + case Gravity.RIGHT: + right = fixedLayoutHorizontal + ? width - padding.right - (adjustedWidth - childWidth) / 2 + : width - padding.right; + left = right - childWidth; + if (adjustPadding && isHorizontal && !isVertical) { + padding.right = width - left; + padding.left += childWidth / 2; + } + break; + case Gravity.CENTER_HORIZONTAL: + final int paddedWidth = width - padding.left - padding.right; + left = (paddedWidth - childWidth) / 2; + right = left + childWidth; + break; + } + child.layout(left, top, right, bottom); + } + + @Override + public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { + return new LayoutParams(getContext(), attrs, this); + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams ? new LayoutParams((LayoutParams) p) : + p instanceof MarginLayoutParams ? new LayoutParams((MarginLayoutParams) p) : + new LayoutParams(p); + } + + @Override + protected ViewGroup.LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams; + } + + public static class LayoutParams extends MarginLayoutParams { + + public float centerWithinArea = 0; + + public int childType = 0; + + public static final int CHILD_TYPE_NONE = 0; + public static final int CHILD_TYPE_WIDGET = 1; + public static final int CHILD_TYPE_CHALLENGE = 2; + public static final int CHILD_TYPE_USER_SWITCHER = 3; + public static final int CHILD_TYPE_SCRIM = 4; + public static final int CHILD_TYPE_PAGE_DELETE_DROP_TARGET = 7; + + public int gravity = Gravity.NO_GRAVITY; + + public int maxWidth = -1; + public int maxHeight = -1; + + public LayoutParams() { + this(WRAP_CONTENT, WRAP_CONTENT); + } + + LayoutParams(Context c, AttributeSet attrs, MultiPaneChallengeLayout parent) { + super(c, attrs); + + final TypedArray a = c.obtainStyledAttributes(attrs, + R.styleable.MultiPaneChallengeLayout_Layout); + + centerWithinArea = a.getFloat( + R.styleable.MultiPaneChallengeLayout_Layout_layout_centerWithinArea, 0); + childType = a.getInt(R.styleable.MultiPaneChallengeLayout_Layout_layout_childType, + CHILD_TYPE_NONE); + gravity = a.getInt(R.styleable.MultiPaneChallengeLayout_Layout_layout_gravity, + Gravity.NO_GRAVITY); + maxWidth = a.getDimensionPixelSize( + R.styleable.MultiPaneChallengeLayout_Layout_layout_maxWidth, -1); + maxHeight = a.getDimensionPixelSize( + R.styleable.MultiPaneChallengeLayout_Layout_layout_maxHeight, -1); + + // Default gravity settings based on type and parent orientation + if (gravity == Gravity.NO_GRAVITY) { + if (parent.mOrientation == HORIZONTAL) { + switch (childType) { + case CHILD_TYPE_WIDGET: + gravity = Gravity.LEFT | Gravity.CENTER_VERTICAL; + break; + case CHILD_TYPE_CHALLENGE: + gravity = Gravity.RIGHT | Gravity.CENTER_VERTICAL; + break; + case CHILD_TYPE_USER_SWITCHER: + gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; + break; + } + } else { + switch (childType) { + case CHILD_TYPE_WIDGET: + gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL; + break; + case CHILD_TYPE_CHALLENGE: + gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; + break; + case CHILD_TYPE_USER_SWITCHER: + gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; + break; + } + } + } + + a.recycle(); + } + + public LayoutParams(int width, int height) { + super(width, height); + } + + public LayoutParams(ViewGroup.LayoutParams source) { + super(source); + } + + public LayoutParams(MarginLayoutParams source) { + super(source); + } + + public LayoutParams(LayoutParams source) { + this((MarginLayoutParams) source); + + centerWithinArea = source.centerWithinArea; + childType = source.childType; + gravity = source.gravity; + maxWidth = source.maxWidth; + maxHeight = source.maxHeight; + } + } +} diff --git a/packages/Keyguard/src/com/android/keyguard/NumPadKey.java b/packages/Keyguard/src/com/android/keyguard/NumPadKey.java new file mode 100644 index 0000000..a0038bc --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/NumPadKey.java @@ -0,0 +1,128 @@ +/* + * 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.internal.policy.impl.keyguard; + +import android.content.Context; +import android.content.res.TypedArray; +import android.text.SpannableStringBuilder; +import android.text.style.TextAppearanceSpan; +import android.util.AttributeSet; +import android.view.HapticFeedbackConstants; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; + +import com.android.internal.R; +import com.android.internal.widget.LockPatternUtils; + +public class NumPadKey extends Button { + // list of "ABC", etc per digit, starting with '0' + static String sKlondike[]; + + int mDigit = -1; + int mTextViewResId; + TextView mTextView = null; + boolean mEnableHaptics; + + private View.OnClickListener mListener = new View.OnClickListener() { + @Override + public void onClick(View thisView) { + if (mTextView == null) { + if (mTextViewResId > 0) { + final View v = NumPadKey.this.getRootView().findViewById(mTextViewResId); + if (v != null && v instanceof TextView) { + mTextView = (TextView) v; + } + } + } + // check for time-based lockouts + if (mTextView != null && mTextView.isEnabled()) { + mTextView.append(String.valueOf(mDigit)); + } + doHapticKeyClick(); + } + }; + + 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) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NumPadKey); + mDigit = a.getInt(R.styleable.NumPadKey_digit, mDigit); + setTextViewResId(a.getResourceId(R.styleable.NumPadKey_textView, 0)); + + setOnClickListener(mListener); + setOnHoverListener(new LiftToActivateListener(context)); + setAccessibilityDelegate(new ObscureSpeechDelegate(context)); + + mEnableHaptics = new LockPatternUtils(context).isTactileFeedbackEnabled(); + + SpannableStringBuilder builder = new SpannableStringBuilder(); + builder.append(String.valueOf(mDigit)); + if (mDigit >= 0) { + if (sKlondike == null) { + sKlondike = context.getResources().getStringArray( + R.array.lockscreen_num_pad_klondike); + } + if (sKlondike != null && sKlondike.length > mDigit) { + final String extra = sKlondike[mDigit]; + final int extraLen = extra.length(); + if (extraLen > 0) { + builder.append(" "); + builder.append(extra); + builder.setSpan( + new TextAppearanceSpan(context, R.style.TextAppearance_NumPadKey_Klondike), + builder.length()-extraLen, builder.length(), 0); + } + } + } + setText(builder); + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + // Reset the "announced headset" flag when detached. + ObscureSpeechDelegate.sAnnouncedHeadset = false; + } + + public void setTextView(TextView tv) { + mTextView = tv; + } + + public void setTextViewResId(int resId) { + mTextView = null; + mTextViewResId = resId; + } + + // 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/Keyguard/src/com/android/keyguard/ObscureSpeechDelegate.java b/packages/Keyguard/src/com/android/keyguard/ObscureSpeechDelegate.java new file mode 100644 index 0000000..af043ab --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/ObscureSpeechDelegate.java @@ -0,0 +1,101 @@ +/* + * 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.internal.policy.impl.keyguard; + +import android.content.ContentResolver; +import android.content.Context; +import android.media.AudioManager; +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.getInt(mContentResolver, + Settings.Secure.ACCESSIBILITY_SPEAK_PASSWORD, 0) != 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/Keyguard/src/com/android/keyguard/PagedView.java b/packages/Keyguard/src/com/android/keyguard/PagedView.java new file mode 100644 index 0000000..539ec1a --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/PagedView.java @@ -0,0 +1,2564 @@ +/* + * 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.internal.policy.impl.keyguard; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.PointF; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.animation.AnimationUtils; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; +import android.view.animation.LinearInterpolator; +import android.widget.Scroller; + +import com.android.internal.R; + +import java.util.ArrayList; + +/** + * An abstraction of the original Workspace which supports browsing through a + * sequential list of "pages" + */ +public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarchyChangeListener { + private static final String TAG = "WidgetPagedView"; + private static final boolean DEBUG = false; + protected static final int INVALID_PAGE = -1; + + // the min drag distance for a fling to register, to prevent random page shifts + private static final int MIN_LENGTH_FOR_FLING = 25; + + protected static final int PAGE_SNAP_ANIMATION_DURATION = 750; + protected static final int SLOW_PAGE_SNAP_ANIMATION_DURATION = 950; + protected static final float NANOTIME_DIV = 1000000000.0f; + + private static final float OVERSCROLL_ACCELERATE_FACTOR = 2; + private static final float OVERSCROLL_DAMP_FACTOR = 0.14f; + + private static final float RETURN_TO_ORIGINAL_PAGE_THRESHOLD = 0.33f; + // The page is moved more than halfway, automatically move to the next page on touch up. + private static final float SIGNIFICANT_MOVE_THRESHOLD = 0.4f; + + // The following constants need to be scaled based on density. The scaled versions will be + // assigned to the corresponding member variables below. + private static final int FLING_THRESHOLD_VELOCITY = 500; + private static final int MIN_SNAP_VELOCITY = 1500; + private static final int MIN_FLING_VELOCITY = 250; + + // We are disabling touch interaction of the widget region for factory ROM. + private static final boolean DISABLE_TOUCH_INTERACTION = false; + private static final boolean DISABLE_TOUCH_SIDE_PAGES = true; + private static final boolean DISABLE_FLING_TO_DELETE = false; + + static final int AUTOMATIC_PAGE_SPACING = -1; + + protected int mFlingThresholdVelocity; + protected int mMinFlingVelocity; + protected int mMinSnapVelocity; + + protected float mDensity; + protected float mSmoothingTime; + protected float mTouchX; + + protected boolean mFirstLayout = true; + + protected int mCurrentPage; + protected int mChildCountOnLastMeasure; + + protected int mNextPage = INVALID_PAGE; + protected int mMaxScrollX; + protected Scroller mScroller; + private VelocityTracker mVelocityTracker; + + private float mParentDownMotionX; + private float mParentDownMotionY; + private float mDownMotionX; + private float mDownMotionY; + private float mDownScrollX; + protected float mLastMotionX; + protected float mLastMotionXRemainder; + protected float mLastMotionY; + protected float mTotalMotionX; + private int mLastScreenCenter = -1; + private int[] mChildOffsets; + private int[] mChildRelativeOffsets; + private int[] mChildOffsetsWithLayoutScale; + + protected final static int TOUCH_STATE_REST = 0; + protected final static int TOUCH_STATE_SCROLLING = 1; + protected final static int TOUCH_STATE_PREV_PAGE = 2; + protected final static int TOUCH_STATE_NEXT_PAGE = 3; + protected final static int TOUCH_STATE_REORDERING = 4; + + protected final static float ALPHA_QUANTIZE_LEVEL = 0.0001f; + + protected int mTouchState = TOUCH_STATE_REST; + protected boolean mForceScreenScrolled = false; + + protected OnLongClickListener mLongClickListener; + + protected int mTouchSlop; + private int mPagingTouchSlop; + private int mMaximumVelocity; + private int mMinimumWidth; + protected int mPageSpacing; + protected int mCellCountX = 0; + protected int mCellCountY = 0; + protected boolean mAllowOverScroll = true; + protected int mUnboundedScrollX; + protected int[] mTempVisiblePagesRange = new int[2]; + protected boolean mForceDrawAllChildrenNextFrame; + + // mOverScrollX is equal to getScrollX() when we're within the normal scroll range. Otherwise + // it is equal to the scaled overscroll position. We use a separate value so as to prevent + // the screens from continuing to translate beyond the normal bounds. + protected int mOverScrollX; + + // parameter that adjusts the layout to be optimized for pages with that scale factor + protected float mLayoutScale = 1.0f; + + protected static final int INVALID_POINTER = -1; + + protected int mActivePointerId = INVALID_POINTER; + + private PageSwitchListener mPageSwitchListener; + + protected ArrayList<Boolean> mDirtyPageContent; + + // If true, syncPages and syncPageItems will be called to refresh pages + protected boolean mContentIsRefreshable = true; + + // If true, modify alpha of neighboring pages as user scrolls left/right + protected boolean mFadeInAdjacentScreens = false; + + // It true, use a different slop parameter (pagingTouchSlop = 2 * touchSlop) for deciding + // to switch to a new page + protected boolean mUsePagingTouchSlop = true; + + // If true, the subclass should directly update scrollX itself in its computeScroll method + // (SmoothPagedView does this) + protected boolean mDeferScrollUpdate = false; + + protected boolean mIsPageMoving = false; + + // All syncs and layout passes are deferred until data is ready. + protected boolean mIsDataReady = true; + + // Scrolling indicator + private ValueAnimator mScrollIndicatorAnimator; + private View mScrollIndicator; + private int mScrollIndicatorPaddingLeft; + private int mScrollIndicatorPaddingRight; + private boolean mShouldShowScrollIndicator = false; + private boolean mShouldShowScrollIndicatorImmediately = false; + protected static final int sScrollIndicatorFadeInDuration = 150; + protected static final int sScrollIndicatorFadeOutDuration = 650; + protected static final int sScrollIndicatorFlashDuration = 650; + + // The viewport whether the pages are to be contained (the actual view may be larger than the + // viewport) + private Rect mViewport = new Rect(); + + // Reordering + // We use the min scale to determine how much to expand the actually PagedView measured + // dimensions such that when we are zoomed out, the view is not clipped + private int REORDERING_DROP_REPOSITION_DURATION = 200; + protected int REORDERING_REORDER_REPOSITION_DURATION = 300; + protected int REORDERING_ZOOM_IN_OUT_DURATION = 250; + private int REORDERING_SIDE_PAGE_HOVER_TIMEOUT = 300; + private float REORDERING_SIDE_PAGE_BUFFER_PERCENTAGE = 0.1f; + private long REORDERING_DELETE_DROP_TARGET_FADE_DURATION = 150; + private float mMinScale = 1f; + protected View mDragView; + protected AnimatorSet mZoomInOutAnim; + private Runnable mSidePageHoverRunnable; + private int mSidePageHoverIndex = -1; + // This variable's scope is only for the duration of startReordering() and endReordering() + private boolean mReorderingStarted = false; + // This variable's scope is for the duration of startReordering() and after the zoomIn() + // animation after endReordering() + private boolean mIsReordering; + // The runnable that settles the page after snapToPage and animateDragViewToOriginalPosition + private int NUM_ANIMATIONS_RUNNING_BEFORE_ZOOM_OUT = 2; + private int mPostReorderingPreZoomInRemainingAnimationCount; + private Runnable mPostReorderingPreZoomInRunnable; + + // Edge swiping + private boolean mOnlyAllowEdgeSwipes = false; + private boolean mDownEventOnEdge = false; + private int mEdgeSwipeRegionSize = 0; + + // Convenience/caching + private Matrix mTmpInvMatrix = new Matrix(); + private float[] mTmpPoint = new float[2]; + private Rect mTmpRect = new Rect(); + private Rect mAltTmpRect = new Rect(); + + // Fling to delete + private int FLING_TO_DELETE_FADE_OUT_DURATION = 350; + private float FLING_TO_DELETE_FRICTION = 0.035f; + // The degrees specifies how much deviation from the up vector to still consider a fling "up" + private float FLING_TO_DELETE_MAX_FLING_DEGREES = 65f; + protected int mFlingToDeleteThresholdVelocity = -1400; + // Drag to delete + private boolean mDeferringForDelete = false; + private int DELETE_SLIDE_IN_SIDE_PAGE_DURATION = 250; + private int DRAG_TO_DELETE_FADE_OUT_DURATION = 350; + + // Drop to delete + private View mDeleteDropTarget; + + // Bouncer + private boolean mTopAlignPageWhenShrinkingForBouncer = false; + + public interface PageSwitchListener { + void onPageSwitching(View newPage, int newPageIndex); + void onPageSwitched(View newPage, int newPageIndex); + } + + public PagedView(Context context) { + this(context, null); + } + + public PagedView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public PagedView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.PagedView, defStyle, 0); + setPageSpacing(a.getDimensionPixelSize(R.styleable.PagedView_pageSpacing, 0)); + mScrollIndicatorPaddingLeft = + a.getDimensionPixelSize(R.styleable.PagedView_scrollIndicatorPaddingLeft, 0); + mScrollIndicatorPaddingRight = + a.getDimensionPixelSize(R.styleable.PagedView_scrollIndicatorPaddingRight, 0); + a.recycle(); + + Resources r = getResources(); + mEdgeSwipeRegionSize = r.getDimensionPixelSize(R.dimen.kg_edge_swipe_region_size); + mTopAlignPageWhenShrinkingForBouncer = + r.getBoolean(R.bool.kg_top_align_page_shrink_on_bouncer_visible); + + setHapticFeedbackEnabled(false); + init(); + } + + /** + * Initializes various states for this workspace. + */ + protected void init() { + mDirtyPageContent = new ArrayList<Boolean>(); + mDirtyPageContent.ensureCapacity(32); + mScroller = new Scroller(getContext(), new ScrollInterpolator()); + mCurrentPage = 0; + + final ViewConfiguration configuration = ViewConfiguration.get(getContext()); + mTouchSlop = configuration.getScaledTouchSlop(); + mPagingTouchSlop = configuration.getScaledPagingTouchSlop(); + mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + mDensity = getResources().getDisplayMetrics().density; + + // Scale the fling-to-delete threshold by the density + mFlingToDeleteThresholdVelocity = + (int) (mFlingToDeleteThresholdVelocity * mDensity); + + mFlingThresholdVelocity = (int) (FLING_THRESHOLD_VELOCITY * mDensity); + mMinFlingVelocity = (int) (MIN_FLING_VELOCITY * mDensity); + mMinSnapVelocity = (int) (MIN_SNAP_VELOCITY * mDensity); + setOnHierarchyChangeListener(this); + } + + void setDeleteDropTarget(View v) { + mDeleteDropTarget = v; + } + + // Convenience methods to map points from self to parent and vice versa + float[] mapPointFromViewToParent(View v, float x, float y) { + mTmpPoint[0] = x; + mTmpPoint[1] = y; + v.getMatrix().mapPoints(mTmpPoint); + mTmpPoint[0] += v.getLeft(); + mTmpPoint[1] += v.getTop(); + return mTmpPoint; + } + float[] mapPointFromParentToView(View v, float x, float y) { + mTmpPoint[0] = x - v.getLeft(); + mTmpPoint[1] = y - v.getTop(); + v.getMatrix().invert(mTmpInvMatrix); + mTmpInvMatrix.mapPoints(mTmpPoint); + return mTmpPoint; + } + + void updateDragViewTranslationDuringDrag() { + float x = mLastMotionX - mDownMotionX + getScrollX() - mDownScrollX; + float y = mLastMotionY - mDownMotionY; + mDragView.setTranslationX(x); + mDragView.setTranslationY(y); + + if (DEBUG) Log.d(TAG, "PagedView.updateDragViewTranslationDuringDrag(): " + x + ", " + y); + } + + public void setMinScale(float f) { + mMinScale = f; + requestLayout(); + } + + @Override + public void setScaleX(float scaleX) { + super.setScaleX(scaleX); + if (isReordering(true)) { + float[] p = mapPointFromParentToView(this, mParentDownMotionX, mParentDownMotionY); + mLastMotionX = p[0]; + mLastMotionY = p[1]; + updateDragViewTranslationDuringDrag(); + } + } + + // Convenience methods to get the actual width/height of the PagedView (since it is measured + // to be larger to account for the minimum possible scale) + int getViewportWidth() { + return mViewport.width(); + } + int getViewportHeight() { + return mViewport.height(); + } + + // Convenience methods to get the offset ASSUMING that we are centering the pages in the + // PagedView both horizontally and vertically + int getViewportOffsetX() { + return (getMeasuredWidth() - getViewportWidth()) / 2; + } + int getViewportOffsetY() { + return (getMeasuredHeight() - getViewportHeight()) / 2; + } + + public void setPageSwitchListener(PageSwitchListener pageSwitchListener) { + mPageSwitchListener = pageSwitchListener; + if (mPageSwitchListener != null) { + mPageSwitchListener.onPageSwitched(getPageAt(mCurrentPage), mCurrentPage); + } + } + + /** + * Called by subclasses to mark that data is ready, and that we can begin loading and laying + * out pages. + */ + protected void setDataIsReady() { + mIsDataReady = true; + } + + protected boolean isDataReady() { + return mIsDataReady; + } + + /** + * Returns the index of the currently displayed page. + * + * @return The index of the currently displayed page. + */ + int getCurrentPage() { + return mCurrentPage; + } + + int getNextPage() { + return (mNextPage != INVALID_PAGE) ? mNextPage : mCurrentPage; + } + + int getPageCount() { + return getChildCount(); + } + + View getPageAt(int index) { + return getChildAt(index); + } + + protected int indexToPage(int index) { + return index; + } + + /** + * Updates the scroll of the current page immediately to its final scroll position. We use this + * in CustomizePagedView to allow tabs to share the same PagedView while resetting the scroll of + * the previous tab page. + */ + protected void updateCurrentPageScroll() { + int offset = getChildOffset(mCurrentPage); + int relOffset = getRelativeChildOffset(mCurrentPage); + int newX = offset - relOffset; + scrollTo(newX, 0); + mScroller.setFinalX(newX); + mScroller.forceFinished(true); + } + + /** + * Sets the current page. + */ + void setCurrentPage(int currentPage) { + notifyPageSwitching(currentPage); + if (!mScroller.isFinished()) { + mScroller.abortAnimation(); + } + // don't introduce any checks like mCurrentPage == currentPage here-- if we change the + // the default + if (getChildCount() == 0) { + return; + } + + mForceScreenScrolled = true; + mCurrentPage = Math.max(0, Math.min(currentPage, getPageCount() - 1)); + updateCurrentPageScroll(); + updateScrollingIndicator(); + notifyPageSwitched(); + invalidate(); + } + + public void setOnlyAllowEdgeSwipes(boolean enable) { + mOnlyAllowEdgeSwipes = enable; + } + + protected void notifyPageSwitching(int whichPage) { + if (mPageSwitchListener != null) { + mPageSwitchListener.onPageSwitching(getPageAt(whichPage), whichPage); + } + } + + protected void notifyPageSwitched() { + if (mPageSwitchListener != null) { + mPageSwitchListener.onPageSwitched(getPageAt(mCurrentPage), mCurrentPage); + } + } + + protected void pageBeginMoving() { + if (!mIsPageMoving) { + mIsPageMoving = true; + onPageBeginMoving(); + } + } + + protected void pageEndMoving() { + if (mIsPageMoving) { + mIsPageMoving = false; + onPageEndMoving(); + } + } + + protected boolean isPageMoving() { + return mIsPageMoving; + } + + // a method that subclasses can override to add behavior + protected void onPageBeginMoving() { + } + + // a method that subclasses can override to add behavior + protected void onPageEndMoving() { + } + + /** + * Registers the specified listener on each page contained in this workspace. + * + * @param l The listener used to respond to long clicks. + */ + @Override + public void setOnLongClickListener(OnLongClickListener l) { + mLongClickListener = l; + final int count = getPageCount(); + for (int i = 0; i < count; i++) { + getPageAt(i).setOnLongClickListener(l); + } + } + + @Override + public void scrollBy(int x, int y) { + scrollTo(mUnboundedScrollX + x, getScrollY() + y); + } + + @Override + public void scrollTo(int x, int y) { + mUnboundedScrollX = x; + + if (x < 0) { + super.scrollTo(0, y); + if (mAllowOverScroll) { + overScroll(x); + } + } else if (x > mMaxScrollX) { + super.scrollTo(mMaxScrollX, y); + if (mAllowOverScroll) { + overScroll(x - mMaxScrollX); + } + } else { + mOverScrollX = x; + super.scrollTo(x, y); + } + + mTouchX = x; + mSmoothingTime = System.nanoTime() / NANOTIME_DIV; + + // Update the last motion events when scrolling + if (isReordering(true)) { + float[] p = mapPointFromParentToView(this, mParentDownMotionX, mParentDownMotionY); + mLastMotionX = p[0]; + mLastMotionY = p[1]; + updateDragViewTranslationDuringDrag(); + } + } + + // we moved this functionality to a helper function so SmoothPagedView can reuse it + protected boolean computeScrollHelper() { + if (mScroller.computeScrollOffset()) { + // Don't bother scrolling if the page does not need to be moved + if (getScrollX() != mScroller.getCurrX() + || getScrollY() != mScroller.getCurrY() + || mOverScrollX != mScroller.getCurrX()) { + scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); + } + invalidate(); + return true; + } else if (mNextPage != INVALID_PAGE) { + mCurrentPage = Math.max(0, Math.min(mNextPage, getPageCount() - 1)); + mNextPage = INVALID_PAGE; + notifyPageSwitched(); + + // We don't want to trigger a page end moving unless the page has settled + // and the user has stopped scrolling + if (mTouchState == TOUCH_STATE_REST) { + pageEndMoving(); + } + + onPostReorderingAnimationCompleted(); + return true; + } + return false; + } + + @Override + public void computeScroll() { + computeScrollHelper(); + } + + protected boolean shouldSetTopAlignedPivotForWidget(int childIndex) { + return mTopAlignPageWhenShrinkingForBouncer; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (!mIsDataReady || getChildCount() == 0) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + return; + } + + // We measure the dimensions of the PagedView to be larger than the pages so that when we + // zoom out (and scale down), the view is still contained in the parent + View parent = (View) getParent(); + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + // NOTE: We multiply by 1.5f to account for the fact that depending on the offset of the + // viewport, we can be at most one and a half screens offset once we scale down + DisplayMetrics dm = getResources().getDisplayMetrics(); + int maxSize = Math.max(dm.widthPixels, dm.heightPixels); + int parentWidthSize = (int) (1.5f * maxSize); + int parentHeightSize = maxSize; + int scaledWidthSize = (int) (parentWidthSize / mMinScale); + int scaledHeightSize = (int) (parentHeightSize / mMinScale); + mViewport.set(0, 0, widthSize, heightSize); + + if (widthMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.UNSPECIFIED) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + return; + } + + // Return early if we aren't given a proper dimension + if (widthSize <= 0 || heightSize <= 0) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + return; + } + + /* Allow the height to be set as WRAP_CONTENT. This allows the particular case + * of the All apps view on XLarge displays to not take up more space then it needs. Width + * is still not allowed to be set as WRAP_CONTENT since many parts of the code expect + * each page to have the same width. + */ + final int verticalPadding = getPaddingTop() + getPaddingBottom(); + final int horizontalPadding = getPaddingLeft() + getPaddingRight(); + + // The children are given the same width and height as the workspace + // unless they were set to WRAP_CONTENT + if (DEBUG) Log.d(TAG, "PagedView.onMeasure(): " + widthSize + ", " + heightSize); + if (DEBUG) Log.d(TAG, "PagedView.scaledSize: " + scaledWidthSize + ", " + scaledHeightSize); + if (DEBUG) Log.d(TAG, "PagedView.parentSize: " + parentWidthSize + ", " + parentHeightSize); + if (DEBUG) Log.d(TAG, "PagedView.horizontalPadding: " + horizontalPadding); + if (DEBUG) Log.d(TAG, "PagedView.verticalPadding: " + verticalPadding); + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + // disallowing padding in paged view (just pass 0) + final View child = getPageAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + int childWidthMode; + if (lp.width == LayoutParams.WRAP_CONTENT) { + childWidthMode = MeasureSpec.AT_MOST; + } else { + childWidthMode = MeasureSpec.EXACTLY; + } + + int childHeightMode; + if (lp.height == LayoutParams.WRAP_CONTENT) { + childHeightMode = MeasureSpec.AT_MOST; + } else { + childHeightMode = MeasureSpec.EXACTLY; + } + + final int childWidthMeasureSpec = + MeasureSpec.makeMeasureSpec(widthSize - horizontalPadding, childWidthMode); + final int childHeightMeasureSpec = + MeasureSpec.makeMeasureSpec(heightSize - verticalPadding, childHeightMode); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + setMeasuredDimension(scaledWidthSize, scaledHeightSize); + + // We can't call getChildOffset/getRelativeChildOffset until we set the measured dimensions. + // We also wait until we set the measured dimensions before flushing the cache as well, to + // ensure that the cache is filled with good values. + invalidateCachedOffsets(); + + if (mChildCountOnLastMeasure != getChildCount() && !mDeferringForDelete) { + setCurrentPage(mCurrentPage); + } + mChildCountOnLastMeasure = getChildCount(); + + if (childCount > 0) { + if (DEBUG) Log.d(TAG, "getRelativeChildOffset(): " + getViewportWidth() + ", " + + getChildWidth(0)); + + // Calculate the variable page spacing if necessary + if (mPageSpacing == AUTOMATIC_PAGE_SPACING) { + // The gap between pages in the PagedView should be equal to the gap from the page + // to the edge of the screen (so it is not visible in the current screen). To + // account for unequal padding on each side of the paged view, we take the maximum + // of the left/right gap and use that as the gap between each page. + int offset = getRelativeChildOffset(0); + int spacing = Math.max(offset, widthSize - offset - + getChildAt(0).getMeasuredWidth()); + setPageSpacing(spacing); + } + } + + updateScrollingIndicatorPosition(); + + if (childCount > 0) { + mMaxScrollX = getChildOffset(childCount - 1) - getRelativeChildOffset(childCount - 1); + } else { + mMaxScrollX = 0; + } + } + + public void setPageSpacing(int pageSpacing) { + mPageSpacing = pageSpacing; + invalidateCachedOffsets(); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + if (!mIsDataReady || getChildCount() == 0) { + return; + } + + if (DEBUG) Log.d(TAG, "PagedView.onLayout()"); + final int childCount = getChildCount(); + + int offsetX = getViewportOffsetX(); + int offsetY = getViewportOffsetY(); + + // Update the viewport offsets + mViewport.offset(offsetX, offsetY); + + int childLeft = offsetX + getRelativeChildOffset(0); + for (int i = 0; i < childCount; i++) { + final View child = getPageAt(i); + int childTop = offsetY + getPaddingTop(); + if (child.getVisibility() != View.GONE) { + final int childWidth = getScaledMeasuredWidth(child); + final int childHeight = child.getMeasuredHeight(); + + if (DEBUG) Log.d(TAG, "\tlayout-child" + i + ": " + childLeft + ", " + childTop); + child.layout(childLeft, childTop, + childLeft + child.getMeasuredWidth(), childTop + childHeight); + childLeft += childWidth + mPageSpacing; + } + } + + if (mFirstLayout && mCurrentPage >= 0 && mCurrentPage < getChildCount()) { + setHorizontalScrollBarEnabled(false); + updateCurrentPageScroll(); + setHorizontalScrollBarEnabled(true); + mFirstLayout = false; + } + } + + protected void screenScrolled(int screenCenter) { + } + + @Override + public void onChildViewAdded(View parent, View child) { + // This ensures that when children are added, they get the correct transforms / alphas + // in accordance with any scroll effects. + mForceScreenScrolled = true; + invalidate(); + invalidateCachedOffsets(); + } + + @Override + public void onChildViewRemoved(View parent, View child) { + mForceScreenScrolled = true; + } + + protected void invalidateCachedOffsets() { + int count = getChildCount(); + if (count == 0) { + mChildOffsets = null; + mChildRelativeOffsets = null; + mChildOffsetsWithLayoutScale = null; + return; + } + + mChildOffsets = new int[count]; + mChildRelativeOffsets = new int[count]; + mChildOffsetsWithLayoutScale = new int[count]; + for (int i = 0; i < count; i++) { + mChildOffsets[i] = -1; + mChildRelativeOffsets[i] = -1; + mChildOffsetsWithLayoutScale[i] = -1; + } + } + + protected int getChildOffset(int index) { + if (index < 0 || index > getChildCount() - 1) return 0; + + int[] childOffsets = Float.compare(mLayoutScale, 1f) == 0 ? + mChildOffsets : mChildOffsetsWithLayoutScale; + + if (childOffsets != null && childOffsets[index] != -1) { + return childOffsets[index]; + } else { + if (getChildCount() == 0) + return 0; + + int offset = getRelativeChildOffset(0); + for (int i = 0; i < index; ++i) { + offset += getScaledMeasuredWidth(getPageAt(i)) + mPageSpacing; + } + if (childOffsets != null) { + childOffsets[index] = offset; + } + return offset; + } + } + + protected int getRelativeChildOffset(int index) { + if (index < 0 || index > getChildCount() - 1) return 0; + + if (mChildRelativeOffsets != null && mChildRelativeOffsets[index] != -1) { + return mChildRelativeOffsets[index]; + } else { + final int padding = getPaddingLeft() + getPaddingRight(); + final int offset = getPaddingLeft() + + (getViewportWidth() - padding - getChildWidth(index)) / 2; + if (mChildRelativeOffsets != null) { + mChildRelativeOffsets[index] = offset; + } + return offset; + } + } + + protected int getScaledMeasuredWidth(View child) { + // This functions are called enough times that it actually makes a difference in the + // profiler -- so just inline the max() here + final int measuredWidth = child.getMeasuredWidth(); + final int minWidth = mMinimumWidth; + final int maxWidth = (minWidth > measuredWidth) ? minWidth : measuredWidth; + return (int) (maxWidth * mLayoutScale + 0.5f); + } + + void boundByReorderablePages(boolean isReordering, int[] range) { + // Do nothing + } + + // TODO: Fix this + protected void getVisiblePages(int[] range) { + range[0] = 0; + range[1] = getPageCount() - 1; + + /* + final int pageCount = getChildCount(); + + if (pageCount > 0) { + final int screenWidth = getViewportWidth(); + int leftScreen = 0; + int rightScreen = 0; + int offsetX = getViewportOffsetX() + getScrollX(); + View currPage = getPageAt(leftScreen); + while (leftScreen < pageCount - 1 && + currPage.getX() + currPage.getWidth() - + currPage.getPaddingRight() < offsetX) { + leftScreen++; + currPage = getPageAt(leftScreen); + } + rightScreen = leftScreen; + currPage = getPageAt(rightScreen + 1); + while (rightScreen < pageCount - 1 && + currPage.getX() - currPage.getPaddingLeft() < offsetX + screenWidth) { + rightScreen++; + currPage = getPageAt(rightScreen + 1); + } + + // TEMP: this is a hacky way to ensure that animations to new pages are not clipped + // because we don't draw them while scrolling? + range[0] = Math.max(0, leftScreen - 1); + range[1] = Math.min(rightScreen + 1, getChildCount() - 1); + } else { + range[0] = -1; + range[1] = -1; + } + */ + } + + protected boolean shouldDrawChild(View child) { + return child.getAlpha() > 0; + } + + @Override + protected void dispatchDraw(Canvas canvas) { + int halfScreenSize = getViewportWidth() / 2; + // mOverScrollX is equal to getScrollX() when we're within the normal scroll range. + // Otherwise it is equal to the scaled overscroll position. + int screenCenter = mOverScrollX + halfScreenSize; + + if (screenCenter != mLastScreenCenter || mForceScreenScrolled) { + // set mForceScreenScrolled before calling screenScrolled so that screenScrolled can + // set it for the next frame + mForceScreenScrolled = false; + screenScrolled(screenCenter); + mLastScreenCenter = screenCenter; + } + + // Find out which screens are visible; as an optimization we only call draw on them + final int pageCount = getChildCount(); + if (pageCount > 0) { + getVisiblePages(mTempVisiblePagesRange); + final int leftScreen = mTempVisiblePagesRange[0]; + final int rightScreen = mTempVisiblePagesRange[1]; + if (leftScreen != -1 && rightScreen != -1) { + final long drawingTime = getDrawingTime(); + // Clip to the bounds + canvas.save(); + canvas.clipRect(getScrollX(), getScrollY(), getScrollX() + getRight() - getLeft(), + getScrollY() + getBottom() - getTop()); + + // Draw all the children, leaving the drag view for last + for (int i = pageCount - 1; i >= 0; i--) { + final View v = getPageAt(i); + if (v == mDragView) continue; + if (mForceDrawAllChildrenNextFrame || + (leftScreen <= i && i <= rightScreen && shouldDrawChild(v))) { + drawChild(canvas, v, drawingTime); + } + } + // Draw the drag view on top (if there is one) + if (mDragView != null) { + drawChild(canvas, mDragView, drawingTime); + } + + mForceDrawAllChildrenNextFrame = false; + canvas.restore(); + } + } + } + + @Override + public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) { + int page = indexToPage(indexOfChild(child)); + if (page != mCurrentPage || !mScroller.isFinished()) { + snapToPage(page); + return true; + } + return false; + } + + @Override + protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { + int focusablePage; + if (mNextPage != INVALID_PAGE) { + focusablePage = mNextPage; + } else { + focusablePage = mCurrentPage; + } + View v = getPageAt(focusablePage); + if (v != null) { + return v.requestFocus(direction, previouslyFocusedRect); + } + return false; + } + + @Override + public boolean dispatchUnhandledMove(View focused, int direction) { + if (direction == View.FOCUS_LEFT) { + if (getCurrentPage() > 0) { + snapToPage(getCurrentPage() - 1); + return true; + } + } else if (direction == View.FOCUS_RIGHT) { + if (getCurrentPage() < getPageCount() - 1) { + snapToPage(getCurrentPage() + 1); + return true; + } + } + return super.dispatchUnhandledMove(focused, direction); + } + + @Override + public void addFocusables(ArrayList<View> views, int direction, int focusableMode) { + if (mCurrentPage >= 0 && mCurrentPage < getPageCount()) { + getPageAt(mCurrentPage).addFocusables(views, direction, focusableMode); + } + if (direction == View.FOCUS_LEFT) { + if (mCurrentPage > 0) { + getPageAt(mCurrentPage - 1).addFocusables(views, direction, focusableMode); + } + } else if (direction == View.FOCUS_RIGHT){ + if (mCurrentPage < getPageCount() - 1) { + getPageAt(mCurrentPage + 1).addFocusables(views, direction, focusableMode); + } + } + } + + /** + * If one of our descendant views decides that it could be focused now, only + * pass that along if it's on the current page. + * + * This happens when live folders requery, and if they're off page, they + * end up calling requestFocus, which pulls it on page. + */ + @Override + public void focusableViewAvailable(View focused) { + View current = getPageAt(mCurrentPage); + View v = focused; + while (true) { + if (v == current) { + super.focusableViewAvailable(focused); + return; + } + if (v == this) { + return; + } + ViewParent parent = v.getParent(); + if (parent instanceof View) { + v = (View)v.getParent(); + } else { + return; + } + } + } + + /** + * Return true if a tap at (x, y) should trigger a flip to the previous page. + */ + protected boolean hitsPreviousPage(float x, float y) { + return (x < getViewportOffsetX() + getRelativeChildOffset(mCurrentPage) - mPageSpacing); + } + + /** + * Return true if a tap at (x, y) should trigger a flip to the next page. + */ + protected boolean hitsNextPage(float x, float y) { + return (x > (getViewportOffsetX() + getViewportWidth() - getRelativeChildOffset(mCurrentPage) + mPageSpacing)); + } + + /** Returns whether x and y originated within the buffered viewport */ + private boolean isTouchPointInViewportWithBuffer(int x, int y) { + mTmpRect.set(mViewport.left - mViewport.width() / 2, mViewport.top, + mViewport.right + mViewport.width() / 2, mViewport.bottom); + return mTmpRect.contains(x, y); + } + + /** Returns whether x and y originated within the current page view bounds */ + private boolean isTouchPointInCurrentPage(int x, int y) { + View v = getPageAt(getCurrentPage()); + if (v != null) { + mTmpRect.set((v.getLeft() - getScrollX()), 0, (v.getRight() - getScrollX()), + v.getBottom()); + return mTmpRect.contains(x, y); + } + return false; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (DISABLE_TOUCH_INTERACTION) { + return false; + } + + /* + * This method JUST determines whether we want to intercept the motion. + * If we return true, onTouchEvent will be called and we do the actual + * scrolling there. + */ + acquireVelocityTrackerAndAddMovement(ev); + + // Skip touch handling if there are no pages to swipe + if (getChildCount() <= 0) return super.onInterceptTouchEvent(ev); + + /* + * Shortcut the most recurring case: the user is in the dragging + * state and he is moving his finger. We want to intercept this + * motion. + */ + final int action = ev.getAction(); + if ((action == MotionEvent.ACTION_MOVE) && + (mTouchState == TOUCH_STATE_SCROLLING)) { + return true; + } + + switch (action & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_MOVE: { + /* + * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check + * whether the user has moved far enough from his original down touch. + */ + if (mActivePointerId != INVALID_POINTER) { + determineScrollingStart(ev); + break; + } + // if mActivePointerId is INVALID_POINTER, then we must have missed an ACTION_DOWN + // event. in that case, treat the first occurence of a move event as a ACTION_DOWN + // i.e. fall through to the next case (don't break) + // (We sometimes miss ACTION_DOWN events in Workspace because it ignores all events + // while it's small- this was causing a crash before we checked for INVALID_POINTER) + } + + case MotionEvent.ACTION_DOWN: { + final float x = ev.getX(); + final float y = ev.getY(); + // Remember location of down touch + mDownMotionX = x; + mDownMotionY = y; + mDownScrollX = getScrollX(); + mLastMotionX = x; + mLastMotionY = y; + float[] p = mapPointFromViewToParent(this, x, y); + mParentDownMotionX = p[0]; + mParentDownMotionY = p[1]; + mLastMotionXRemainder = 0; + mTotalMotionX = 0; + mActivePointerId = ev.getPointerId(0); + + // Determine if the down event is within the threshold to be an edge swipe + int leftEdgeBoundary = getViewportOffsetX() + mEdgeSwipeRegionSize; + int rightEdgeBoundary = getMeasuredWidth() - getViewportOffsetX() - mEdgeSwipeRegionSize; + if ((mDownMotionX <= leftEdgeBoundary || mDownMotionX >= rightEdgeBoundary)) { + mDownEventOnEdge = true; + } + + /* + * If being flinged and user touches the screen, initiate drag; + * otherwise don't. mScroller.isFinished should be false when + * being flinged. + */ + final int xDist = Math.abs(mScroller.getFinalX() - mScroller.getCurrX()); + final boolean finishedScrolling = (mScroller.isFinished() || xDist < mTouchSlop); + if (finishedScrolling) { + mTouchState = TOUCH_STATE_REST; + mScroller.abortAnimation(); + } else { + if (isTouchPointInViewportWithBuffer((int) mDownMotionX, (int) mDownMotionY)) { + mTouchState = TOUCH_STATE_SCROLLING; + } else { + mTouchState = TOUCH_STATE_REST; + } + } + + // check if this can be the beginning of a tap on the side of the pages + // to scroll the current page + if (!DISABLE_TOUCH_SIDE_PAGES) { + if (mTouchState != TOUCH_STATE_PREV_PAGE && mTouchState != TOUCH_STATE_NEXT_PAGE) { + if (getChildCount() > 0) { + if (hitsPreviousPage(x, y)) { + mTouchState = TOUCH_STATE_PREV_PAGE; + } else if (hitsNextPage(x, y)) { + mTouchState = TOUCH_STATE_NEXT_PAGE; + } + } + } + } + break; + } + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + resetTouchState(); + // Just intercept the touch event on up if we tap outside the strict viewport + if (!isTouchPointInCurrentPage((int) mLastMotionX, (int) mLastMotionY)) { + return true; + } + break; + + case MotionEvent.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + releaseVelocityTracker(); + break; + } + + /* + * The only time we want to intercept motion events is if we are in the + * drag mode. + */ + return mTouchState != TOUCH_STATE_REST; + } + + protected void determineScrollingStart(MotionEvent ev) { + determineScrollingStart(ev, 1.0f); + } + + /* + * Determines if we should change the touch state to start scrolling after the + * user moves their touch point too far. + */ + protected void determineScrollingStart(MotionEvent ev, float touchSlopScale) { + // Disallow scrolling if we don't have a valid pointer index + final int pointerIndex = ev.findPointerIndex(mActivePointerId); + if (pointerIndex == -1) return; + + // Disallow scrolling if we started the gesture from outside the viewport + final float x = ev.getX(pointerIndex); + final float y = ev.getY(pointerIndex); + if (!isTouchPointInViewportWithBuffer((int) x, (int) y)) return; + + // If we're only allowing edge swipes, we break out early if the down event wasn't + // at the edge. + if (mOnlyAllowEdgeSwipes && !mDownEventOnEdge) return; + + final int xDiff = (int) Math.abs(x - mLastMotionX); + final int yDiff = (int) Math.abs(y - mLastMotionY); + + final int touchSlop = Math.round(touchSlopScale * mTouchSlop); + boolean xPaged = xDiff > mPagingTouchSlop; + boolean xMoved = xDiff > touchSlop; + boolean yMoved = yDiff > touchSlop; + + if (xMoved || xPaged || yMoved) { + if (mUsePagingTouchSlop ? xPaged : xMoved) { + // Scroll if the user moved far enough along the X axis + mTouchState = TOUCH_STATE_SCROLLING; + mTotalMotionX += Math.abs(mLastMotionX - x); + mLastMotionX = x; + mLastMotionXRemainder = 0; + mTouchX = getViewportOffsetX() + getScrollX(); + mSmoothingTime = System.nanoTime() / NANOTIME_DIV; + pageBeginMoving(); + } + } + } + + protected float getMaxScrollProgress() { + return 1.0f; + } + + protected float getBoundedScrollProgress(int screenCenter, View v, int page) { + final int halfScreenSize = getViewportWidth() / 2; + + screenCenter = Math.min(mScrollX + halfScreenSize, screenCenter); + screenCenter = Math.max(halfScreenSize, screenCenter); + + return getScrollProgress(screenCenter, v, page); + } + + protected float getScrollProgress(int screenCenter, View v, int page) { + final int halfScreenSize = getViewportWidth() / 2; + + int totalDistance = getScaledMeasuredWidth(v) + mPageSpacing; + int delta = screenCenter - (getChildOffset(page) - + getRelativeChildOffset(page) + halfScreenSize); + + float scrollProgress = delta / (totalDistance * 1.0f); + scrollProgress = Math.min(scrollProgress, getMaxScrollProgress()); + scrollProgress = Math.max(scrollProgress, - getMaxScrollProgress()); + return scrollProgress; + } + + // This curve determines how the effect of scrolling over the limits of the page dimishes + // as the user pulls further and further from the bounds + private float overScrollInfluenceCurve(float f) { + f -= 1.0f; + return f * f * f + 1.0f; + } + + protected void acceleratedOverScroll(float amount) { + int screenSize = getViewportWidth(); + + // We want to reach the max over scroll effect when the user has + // over scrolled half the size of the screen + float f = OVERSCROLL_ACCELERATE_FACTOR * (amount / screenSize); + + if (f == 0) return; + + // Clamp this factor, f, to -1 < f < 1 + if (Math.abs(f) >= 1) { + f /= Math.abs(f); + } + + int overScrollAmount = (int) Math.round(f * screenSize); + if (amount < 0) { + mOverScrollX = overScrollAmount; + super.scrollTo(0, getScrollY()); + } else { + mOverScrollX = mMaxScrollX + overScrollAmount; + super.scrollTo(mMaxScrollX, getScrollY()); + } + invalidate(); + } + + protected void dampedOverScroll(float amount) { + int screenSize = getViewportWidth(); + + float f = (amount / screenSize); + + if (f == 0) return; + f = f / (Math.abs(f)) * (overScrollInfluenceCurve(Math.abs(f))); + + // Clamp this factor, f, to -1 < f < 1 + if (Math.abs(f) >= 1) { + f /= Math.abs(f); + } + + int overScrollAmount = (int) Math.round(OVERSCROLL_DAMP_FACTOR * f * screenSize); + if (amount < 0) { + mOverScrollX = overScrollAmount; + super.scrollTo(0, getScrollY()); + } else { + mOverScrollX = mMaxScrollX + overScrollAmount; + super.scrollTo(mMaxScrollX, getScrollY()); + } + invalidate(); + } + + protected void overScroll(float amount) { + dampedOverScroll(amount); + } + + protected float maxOverScroll() { + // Using the formula in overScroll, assuming that f = 1.0 (which it should generally not + // exceed). Used to find out how much extra wallpaper we need for the over scroll effect + float f = 1.0f; + f = f / (Math.abs(f)) * (overScrollInfluenceCurve(Math.abs(f))); + return OVERSCROLL_DAMP_FACTOR * f; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (DISABLE_TOUCH_INTERACTION) { + return false; + } + + // Skip touch handling if there are no pages to swipe + if (getChildCount() <= 0) return super.onTouchEvent(ev); + + acquireVelocityTrackerAndAddMovement(ev); + + final int action = ev.getAction(); + + switch (action & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: + /* + * If being flinged and user touches, stop the fling. isFinished + * will be false if being flinged. + */ + if (!mScroller.isFinished()) { + mScroller.abortAnimation(); + } + + // Remember where the motion event started + mDownMotionX = mLastMotionX = ev.getX(); + mDownMotionY = mLastMotionY = ev.getY(); + mDownScrollX = getScrollX(); + float[] p = mapPointFromViewToParent(this, mLastMotionX, mLastMotionY); + mParentDownMotionX = p[0]; + mParentDownMotionY = p[1]; + mLastMotionXRemainder = 0; + mTotalMotionX = 0; + mActivePointerId = ev.getPointerId(0); + + // Determine if the down event is within the threshold to be an edge swipe + int leftEdgeBoundary = getViewportOffsetX() + mEdgeSwipeRegionSize; + int rightEdgeBoundary = getMeasuredWidth() - getViewportOffsetX() - mEdgeSwipeRegionSize; + if ((mDownMotionX <= leftEdgeBoundary || mDownMotionX >= rightEdgeBoundary)) { + mDownEventOnEdge = true; + } + + if (mTouchState == TOUCH_STATE_SCROLLING) { + pageBeginMoving(); + } + break; + + case MotionEvent.ACTION_MOVE: + if (mTouchState == TOUCH_STATE_SCROLLING) { + // Scroll to follow the motion event + final int pointerIndex = ev.findPointerIndex(mActivePointerId); + final float x = ev.getX(pointerIndex); + final float deltaX = mLastMotionX + mLastMotionXRemainder - x; + + mTotalMotionX += Math.abs(deltaX); + + // Only scroll and update mLastMotionX if we have moved some discrete amount. We + // keep the remainder because we are actually testing if we've moved from the last + // scrolled position (which is discrete). + if (Math.abs(deltaX) >= 1.0f) { + mTouchX += deltaX; + mSmoothingTime = System.nanoTime() / NANOTIME_DIV; + if (!mDeferScrollUpdate) { + scrollBy((int) deltaX, 0); + if (DEBUG) Log.d(TAG, "onTouchEvent().Scrolling: " + deltaX); + } else { + invalidate(); + } + mLastMotionX = x; + mLastMotionXRemainder = deltaX - (int) deltaX; + } else { + awakenScrollBars(); + } + } else if (mTouchState == TOUCH_STATE_REORDERING) { + // Update the last motion position + mLastMotionX = ev.getX(); + mLastMotionY = ev.getY(); + + // Update the parent down so that our zoom animations take this new movement into + // account + float[] pt = mapPointFromViewToParent(this, mLastMotionX, mLastMotionY); + mParentDownMotionX = pt[0]; + mParentDownMotionY = pt[1]; + updateDragViewTranslationDuringDrag(); + + // Find the closest page to the touch point + final int dragViewIndex = indexOfChild(mDragView); + int bufferSize = (int) (REORDERING_SIDE_PAGE_BUFFER_PERCENTAGE * + getViewportWidth()); + int leftBufferEdge = (int) (mapPointFromViewToParent(this, mViewport.left, 0)[0] + + bufferSize); + int rightBufferEdge = (int) (mapPointFromViewToParent(this, mViewport.right, 0)[0] + - bufferSize); + + // Change the drag view if we are hovering over the drop target + boolean isHoveringOverDelete = isHoveringOverDeleteDropTarget( + (int) mParentDownMotionX, (int) mParentDownMotionY); + setPageHoveringOverDeleteDropTarget(dragViewIndex, isHoveringOverDelete); + + if (DEBUG) Log.d(TAG, "leftBufferEdge: " + leftBufferEdge); + if (DEBUG) Log.d(TAG, "rightBufferEdge: " + rightBufferEdge); + if (DEBUG) Log.d(TAG, "mLastMotionX: " + mLastMotionX); + if (DEBUG) Log.d(TAG, "mLastMotionY: " + mLastMotionY); + if (DEBUG) Log.d(TAG, "mParentDownMotionX: " + mParentDownMotionX); + if (DEBUG) Log.d(TAG, "mParentDownMotionY: " + mParentDownMotionY); + + float parentX = mParentDownMotionX; + int pageIndexToSnapTo = -1; + if (parentX < leftBufferEdge && dragViewIndex > 0) { + pageIndexToSnapTo = dragViewIndex - 1; + } else if (parentX > rightBufferEdge && dragViewIndex < getChildCount() - 1) { + pageIndexToSnapTo = dragViewIndex + 1; + } + + final int pageUnderPointIndex = pageIndexToSnapTo; + if (pageUnderPointIndex > -1 && !isHoveringOverDelete) { + mTempVisiblePagesRange[0] = 0; + mTempVisiblePagesRange[1] = getPageCount() - 1; + boundByReorderablePages(true, mTempVisiblePagesRange); + if (mTempVisiblePagesRange[0] <= pageUnderPointIndex && + pageUnderPointIndex <= mTempVisiblePagesRange[1] && + pageUnderPointIndex != mSidePageHoverIndex && mScroller.isFinished()) { + mSidePageHoverIndex = pageUnderPointIndex; + mSidePageHoverRunnable = new Runnable() { + @Override + public void run() { + // Update the down scroll position to account for the fact that the + // current page is moved + mDownScrollX = getChildOffset(pageUnderPointIndex) + - getRelativeChildOffset(pageUnderPointIndex); + + // Setup the scroll to the correct page before we swap the views + snapToPage(pageUnderPointIndex); + + // For each of the pages between the paged view and the drag view, + // animate them from the previous position to the new position in + // the layout (as a result of the drag view moving in the layout) + int shiftDelta = (dragViewIndex < pageUnderPointIndex) ? -1 : 1; + int lowerIndex = (dragViewIndex < pageUnderPointIndex) ? + dragViewIndex + 1 : pageUnderPointIndex; + int upperIndex = (dragViewIndex > pageUnderPointIndex) ? + dragViewIndex - 1 : pageUnderPointIndex; + for (int i = lowerIndex; i <= upperIndex; ++i) { + View v = getChildAt(i); + // dragViewIndex < pageUnderPointIndex, so after we remove the + // drag view all subsequent views to pageUnderPointIndex will + // shift down. + int oldX = getViewportOffsetX() + getChildOffset(i); + int newX = getViewportOffsetX() + getChildOffset(i + shiftDelta); + + // Animate the view translation from its old position to its new + // position + AnimatorSet anim = (AnimatorSet) v.getTag(); + if (anim != null) { + anim.cancel(); + } + + v.setTranslationX(oldX - newX); + anim = new AnimatorSet(); + anim.setDuration(REORDERING_REORDER_REPOSITION_DURATION); + anim.playTogether( + ObjectAnimator.ofFloat(v, "translationX", 0f)); + anim.start(); + v.setTag(anim); + } + + removeView(mDragView); + onRemoveView(mDragView, false); + addView(mDragView, pageUnderPointIndex); + onAddView(mDragView, pageUnderPointIndex); + mSidePageHoverIndex = -1; + } + }; + postDelayed(mSidePageHoverRunnable, REORDERING_SIDE_PAGE_HOVER_TIMEOUT); + } + } else { + removeCallbacks(mSidePageHoverRunnable); + mSidePageHoverIndex = -1; + } + } else { + determineScrollingStart(ev); + } + break; + + case MotionEvent.ACTION_UP: + if (mTouchState == TOUCH_STATE_SCROLLING) { + final int activePointerId = mActivePointerId; + final int pointerIndex = ev.findPointerIndex(activePointerId); + final float x = ev.getX(pointerIndex); + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + int velocityX = (int) velocityTracker.getXVelocity(activePointerId); + final int deltaX = (int) (x - mDownMotionX); + final int pageWidth = getScaledMeasuredWidth(getPageAt(mCurrentPage)); + boolean isSignificantMove = Math.abs(deltaX) > pageWidth * + SIGNIFICANT_MOVE_THRESHOLD; + + mTotalMotionX += Math.abs(mLastMotionX + mLastMotionXRemainder - x); + + boolean isFling = mTotalMotionX > MIN_LENGTH_FOR_FLING && + Math.abs(velocityX) > mFlingThresholdVelocity; + + // In the case that the page is moved far to one direction and then is flung + // in the opposite direction, we use a threshold to determine whether we should + // just return to the starting page, or if we should skip one further. + boolean returnToOriginalPage = false; + if (Math.abs(deltaX) > pageWidth * RETURN_TO_ORIGINAL_PAGE_THRESHOLD && + Math.signum(velocityX) != Math.signum(deltaX) && isFling) { + returnToOriginalPage = true; + } + + int finalPage; + // We give flings precedence over large moves, which is why we short-circuit our + // test for a large move if a fling has been registered. That is, a large + // move to the left and fling to the right will register as a fling to the right. + if (((isSignificantMove && deltaX > 0 && !isFling) || + (isFling && velocityX > 0)) && mCurrentPage > 0) { + finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage - 1; + snapToPageWithVelocity(finalPage, velocityX); + } else if (((isSignificantMove && deltaX < 0 && !isFling) || + (isFling && velocityX < 0)) && + mCurrentPage < getChildCount() - 1) { + finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage + 1; + snapToPageWithVelocity(finalPage, velocityX); + } else { + snapToDestination(); + } + } else if (mTouchState == TOUCH_STATE_PREV_PAGE) { + // at this point we have not moved beyond the touch slop + // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so + // we can just page + int nextPage = Math.max(0, mCurrentPage - 1); + if (nextPage != mCurrentPage) { + snapToPage(nextPage); + } else { + snapToDestination(); + } + } else if (mTouchState == TOUCH_STATE_NEXT_PAGE) { + // at this point we have not moved beyond the touch slop + // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so + // we can just page + int nextPage = Math.min(getChildCount() - 1, mCurrentPage + 1); + if (nextPage != mCurrentPage) { + snapToPage(nextPage); + } else { + snapToDestination(); + } + } else if (mTouchState == TOUCH_STATE_REORDERING) { + // Update the last motion position + mLastMotionX = ev.getX(); + mLastMotionY = ev.getY(); + + // Update the parent down so that our zoom animations take this new movement into + // account + float[] pt = mapPointFromViewToParent(this, mLastMotionX, mLastMotionY); + mParentDownMotionX = pt[0]; + mParentDownMotionY = pt[1]; + updateDragViewTranslationDuringDrag(); + boolean handledFling = false; + if (!DISABLE_FLING_TO_DELETE) { + // Check the velocity and see if we are flinging-to-delete + PointF flingToDeleteVector = isFlingingToDelete(); + if (flingToDeleteVector != null) { + onFlingToDelete(flingToDeleteVector); + handledFling = true; + } + } + if (!handledFling && isHoveringOverDeleteDropTarget((int) mParentDownMotionX, + (int) mParentDownMotionY)) { + onDropToDelete(); + } + } else { + onUnhandledTap(ev); + } + + // Remove the callback to wait for the side page hover timeout + removeCallbacks(mSidePageHoverRunnable); + // End any intermediate reordering states + resetTouchState(); + break; + + case MotionEvent.ACTION_CANCEL: + if (mTouchState == TOUCH_STATE_SCROLLING) { + snapToDestination(); + } + resetTouchState(); + break; + + case MotionEvent.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + break; + } + + return true; + } + + //public abstract void onFlingToDelete(View v); + public abstract void onRemoveView(View v, boolean deletePermanently); + public abstract void onRemoveViewAnimationCompleted(); + public abstract void onAddView(View v, int index); + + private void resetTouchState() { + releaseVelocityTracker(); + endReordering(); + mTouchState = TOUCH_STATE_REST; + mActivePointerId = INVALID_POINTER; + mDownEventOnEdge = false; + } + + protected void onUnhandledTap(MotionEvent ev) {} + + @Override + public boolean onGenericMotionEvent(MotionEvent event) { + if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) { + switch (event.getAction()) { + case MotionEvent.ACTION_SCROLL: { + // Handle mouse (or ext. device) by shifting the page depending on the scroll + final float vscroll; + final float hscroll; + if ((event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0) { + vscroll = 0; + hscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL); + } else { + vscroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL); + hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL); + } + if (hscroll != 0 || vscroll != 0) { + if (hscroll > 0 || vscroll > 0) { + scrollRight(); + } else { + scrollLeft(); + } + return true; + } + } + } + } + return super.onGenericMotionEvent(event); + } + + private void acquireVelocityTrackerAndAddMovement(MotionEvent ev) { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + } + + private void releaseVelocityTracker() { + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + private void onSecondaryPointerUp(MotionEvent ev) { + final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> + MotionEvent.ACTION_POINTER_INDEX_SHIFT; + final int pointerId = ev.getPointerId(pointerIndex); + if (pointerId == mActivePointerId) { + // This was our active pointer going up. Choose a new + // active pointer and adjust accordingly. + // TODO: Make this decision more intelligent. + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mLastMotionX = mDownMotionX = ev.getX(newPointerIndex); + mLastMotionY = ev.getY(newPointerIndex); + mLastMotionXRemainder = 0; + mActivePointerId = ev.getPointerId(newPointerIndex); + if (mVelocityTracker != null) { + mVelocityTracker.clear(); + } + } + } + + @Override + public void requestChildFocus(View child, View focused) { + super.requestChildFocus(child, focused); + int page = indexToPage(indexOfChild(child)); + if (page >= 0 && page != getCurrentPage() && !isInTouchMode()) { + snapToPage(page); + } + } + + protected int getChildIndexForRelativeOffset(int relativeOffset) { + final int childCount = getChildCount(); + int left; + int right; + for (int i = 0; i < childCount; ++i) { + left = getRelativeChildOffset(i); + right = (left + getScaledMeasuredWidth(getPageAt(i))); + if (left <= relativeOffset && relativeOffset <= right) { + return i; + } + } + return -1; + } + + protected int getChildWidth(int index) { + // This functions are called enough times that it actually makes a difference in the + // profiler -- so just inline the max() here + final int measuredWidth = getPageAt(index).getMeasuredWidth(); + final int minWidth = mMinimumWidth; + return (minWidth > measuredWidth) ? minWidth : measuredWidth; + } + + int getPageNearestToPoint(float x) { + int index = 0; + for (int i = 0; i < getChildCount(); ++i) { + if (x < getChildAt(i).getRight() - getScrollX()) { + return index; + } else { + index++; + } + } + return Math.min(index, getChildCount() - 1); + } + + int getPageNearestToCenterOfScreen() { + int minDistanceFromScreenCenter = Integer.MAX_VALUE; + int minDistanceFromScreenCenterIndex = -1; + int screenCenter = getViewportOffsetX() + getScrollX() + (getViewportWidth() / 2); + final int childCount = getChildCount(); + for (int i = 0; i < childCount; ++i) { + View layout = (View) getPageAt(i); + int childWidth = getScaledMeasuredWidth(layout); + int halfChildWidth = (childWidth / 2); + int childCenter = getViewportOffsetX() + getChildOffset(i) + halfChildWidth; + int distanceFromScreenCenter = Math.abs(childCenter - screenCenter); + if (distanceFromScreenCenter < minDistanceFromScreenCenter) { + minDistanceFromScreenCenter = distanceFromScreenCenter; + minDistanceFromScreenCenterIndex = i; + } + } + return minDistanceFromScreenCenterIndex; + } + + protected void snapToDestination() { + snapToPage(getPageNearestToCenterOfScreen(), PAGE_SNAP_ANIMATION_DURATION); + } + + private static class ScrollInterpolator implements Interpolator { + public ScrollInterpolator() { + } + + public float getInterpolation(float t) { + t -= 1.0f; + return t*t*t*t*t + 1; + } + } + + // We want the duration of the page snap animation to be influenced by the distance that + // the screen has to travel, however, we don't want this duration to be effected in a + // purely linear fashion. Instead, we use this method to moderate the effect that the distance + // of travel has on the overall snap duration. + float distanceInfluenceForSnapDuration(float f) { + f -= 0.5f; // center the values about 0. + f *= 0.3f * Math.PI / 2.0f; + return (float) Math.sin(f); + } + + protected void snapToPageWithVelocity(int whichPage, int velocity) { + whichPage = Math.max(0, Math.min(whichPage, getChildCount() - 1)); + int halfScreenSize = getViewportWidth() / 2; + + if (DEBUG) Log.d(TAG, "snapToPage.getChildOffset(): " + getChildOffset(whichPage)); + if (DEBUG) Log.d(TAG, "snapToPageWithVelocity.getRelativeChildOffset(): " + + getViewportWidth() + ", " + getChildWidth(whichPage)); + final int newX = getChildOffset(whichPage) - getRelativeChildOffset(whichPage); + int delta = newX - mUnboundedScrollX; + int duration = 0; + + if (Math.abs(velocity) < mMinFlingVelocity) { + // If the velocity is low enough, then treat this more as an automatic page advance + // as opposed to an apparent physical response to flinging + snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION); + return; + } + + // Here we compute a "distance" that will be used in the computation of the overall + // snap duration. This is a function of the actual distance that needs to be traveled; + // we keep this value close to half screen size in order to reduce the variance in snap + // duration as a function of the distance the page needs to travel. + float distanceRatio = Math.min(1f, 1.0f * Math.abs(delta) / (2 * halfScreenSize)); + float distance = halfScreenSize + halfScreenSize * + distanceInfluenceForSnapDuration(distanceRatio); + + velocity = Math.abs(velocity); + velocity = Math.max(mMinSnapVelocity, velocity); + + // we want the page's snap velocity to approximately match the velocity at which the + // user flings, so we scale the duration by a value near to the derivative of the scroll + // interpolator at zero, ie. 5. We use 4 to make it a little slower. + duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); + + snapToPage(whichPage, delta, duration); + } + + protected void snapToPage(int whichPage) { + snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION); + } + protected void snapToPageImmediately(int whichPage) { + snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION, true); + } + + protected void snapToPage(int whichPage, int duration) { + snapToPage(whichPage, duration, false); + } + protected void snapToPage(int whichPage, int duration, boolean immediate) { + whichPage = Math.max(0, Math.min(whichPage, getPageCount() - 1)); + + if (DEBUG) Log.d(TAG, "snapToPage.getChildOffset(): " + getChildOffset(whichPage)); + if (DEBUG) Log.d(TAG, "snapToPage.getRelativeChildOffset(): " + getViewportWidth() + ", " + + getChildWidth(whichPage)); + int newX = getChildOffset(whichPage) - getRelativeChildOffset(whichPage); + final int sX = mUnboundedScrollX; + final int delta = newX - sX; + snapToPage(whichPage, delta, duration, immediate); + } + + protected void snapToPage(int whichPage, int delta, int duration) { + snapToPage(whichPage, delta, duration, false); + } + protected void snapToPage(int whichPage, int delta, int duration, boolean immediate) { + mNextPage = whichPage; + notifyPageSwitching(whichPage); + View focusedChild = getFocusedChild(); + if (focusedChild != null && whichPage != mCurrentPage && + focusedChild == getPageAt(mCurrentPage)) { + focusedChild.clearFocus(); + } + + pageBeginMoving(); + awakenScrollBars(duration); + if (immediate) { + duration = 0; + } else if (duration == 0) { + duration = Math.abs(delta); + } + + if (!mScroller.isFinished()) mScroller.abortAnimation(); + mScroller.startScroll(mUnboundedScrollX, 0, delta, 0, duration); + + notifyPageSwitched(); + + // Trigger a compute() to finish switching pages if necessary + if (immediate) { + computeScroll(); + } + + mForceScreenScrolled = true; + invalidate(); + } + + public void scrollLeft() { + if (mScroller.isFinished()) { + if (mCurrentPage > 0) snapToPage(mCurrentPage - 1); + } else { + if (mNextPage > 0) snapToPage(mNextPage - 1); + } + } + + public void scrollRight() { + if (mScroller.isFinished()) { + if (mCurrentPage < getChildCount() -1) snapToPage(mCurrentPage + 1); + } else { + if (mNextPage < getChildCount() -1) snapToPage(mNextPage + 1); + } + } + + public int getPageForView(View v) { + int result = -1; + if (v != null) { + ViewParent vp = v.getParent(); + int count = getChildCount(); + for (int i = 0; i < count; i++) { + if (vp == getPageAt(i)) { + return i; + } + } + } + return result; + } + + public static class SavedState extends BaseSavedState { + int currentPage = -1; + + SavedState(Parcelable superState) { + super(superState); + } + + private SavedState(Parcel in) { + super(in); + currentPage = in.readInt(); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeInt(currentPage); + } + + public static final Parcelable.Creator<SavedState> CREATOR = + new Parcelable.Creator<SavedState>() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + protected View getScrollingIndicator() { + return null; + } + + protected boolean isScrollingIndicatorEnabled() { + return false; + } + + Runnable hideScrollingIndicatorRunnable = new Runnable() { + @Override + public void run() { + hideScrollingIndicator(false); + } + }; + + protected void flashScrollingIndicator(boolean animated) { + removeCallbacks(hideScrollingIndicatorRunnable); + showScrollingIndicator(!animated); + postDelayed(hideScrollingIndicatorRunnable, sScrollIndicatorFlashDuration); + } + + protected void showScrollingIndicator(boolean immediately) { + mShouldShowScrollIndicator = true; + mShouldShowScrollIndicatorImmediately = true; + if (getChildCount() <= 1) return; + if (!isScrollingIndicatorEnabled()) return; + + mShouldShowScrollIndicator = false; + getScrollingIndicator(); + if (mScrollIndicator != null) { + // Fade the indicator in + updateScrollingIndicatorPosition(); + mScrollIndicator.setVisibility(View.VISIBLE); + cancelScrollingIndicatorAnimations(); + if (immediately) { + mScrollIndicator.setAlpha(1f); + } else { + mScrollIndicatorAnimator = ObjectAnimator.ofFloat(mScrollIndicator, "alpha", 1f); + mScrollIndicatorAnimator.setDuration(sScrollIndicatorFadeInDuration); + mScrollIndicatorAnimator.start(); + } + } + } + + protected void cancelScrollingIndicatorAnimations() { + if (mScrollIndicatorAnimator != null) { + mScrollIndicatorAnimator.cancel(); + } + } + + protected void hideScrollingIndicator(boolean immediately) { + if (getChildCount() <= 1) return; + if (!isScrollingIndicatorEnabled()) return; + + getScrollingIndicator(); + if (mScrollIndicator != null) { + // Fade the indicator out + updateScrollingIndicatorPosition(); + cancelScrollingIndicatorAnimations(); + if (immediately) { + mScrollIndicator.setVisibility(View.INVISIBLE); + mScrollIndicator.setAlpha(0f); + } else { + mScrollIndicatorAnimator = ObjectAnimator.ofFloat(mScrollIndicator, "alpha", 0f); + mScrollIndicatorAnimator.setDuration(sScrollIndicatorFadeOutDuration); + mScrollIndicatorAnimator.addListener(new AnimatorListenerAdapter() { + private boolean cancelled = false; + @Override + public void onAnimationCancel(android.animation.Animator animation) { + cancelled = true; + } + @Override + public void onAnimationEnd(Animator animation) { + if (!cancelled) { + mScrollIndicator.setVisibility(View.INVISIBLE); + } + } + }); + mScrollIndicatorAnimator.start(); + } + } + } + + /** + * To be overridden by subclasses to determine whether the scroll indicator should stretch to + * fill its space on the track or not. + */ + protected boolean hasElasticScrollIndicator() { + return true; + } + + private void updateScrollingIndicator() { + if (getChildCount() <= 1) return; + if (!isScrollingIndicatorEnabled()) return; + + getScrollingIndicator(); + if (mScrollIndicator != null) { + updateScrollingIndicatorPosition(); + } + if (mShouldShowScrollIndicator) { + showScrollingIndicator(mShouldShowScrollIndicatorImmediately); + } + } + + private void updateScrollingIndicatorPosition() { + if (!isScrollingIndicatorEnabled()) return; + if (mScrollIndicator == null) return; + int numPages = getChildCount(); + int pageWidth = getViewportWidth(); + int lastChildIndex = Math.max(0, getChildCount() - 1); + int maxScrollX = getChildOffset(lastChildIndex) - getRelativeChildOffset(lastChildIndex); + int trackWidth = pageWidth - mScrollIndicatorPaddingLeft - mScrollIndicatorPaddingRight; + int indicatorWidth = mScrollIndicator.getMeasuredWidth() - + mScrollIndicator.getPaddingLeft() - mScrollIndicator.getPaddingRight(); + + float offset = Math.max(0f, Math.min(1f, (float) getScrollX() / maxScrollX)); + int indicatorSpace = trackWidth / numPages; + int indicatorPos = (int) (offset * (trackWidth - indicatorSpace)) + mScrollIndicatorPaddingLeft; + if (hasElasticScrollIndicator()) { + if (mScrollIndicator.getMeasuredWidth() != indicatorSpace) { + mScrollIndicator.getLayoutParams().width = indicatorSpace; + mScrollIndicator.requestLayout(); + } + } else { + int indicatorCenterOffset = indicatorSpace / 2 - indicatorWidth / 2; + indicatorPos += indicatorCenterOffset; + } + mScrollIndicator.setTranslationX(indicatorPos); + } + + // Animate the drag view back to the original position + void animateDragViewToOriginalPosition() { + if (mDragView != null) { + AnimatorSet anim = new AnimatorSet(); + anim.setDuration(REORDERING_DROP_REPOSITION_DURATION); + anim.playTogether( + ObjectAnimator.ofFloat(mDragView, "translationX", 0f), + ObjectAnimator.ofFloat(mDragView, "translationY", 0f)); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + onPostReorderingAnimationCompleted(); + } + }); + anim.start(); + } + } + + // "Zooms out" the PagedView to reveal more side pages + protected boolean zoomOut() { + if (mZoomInOutAnim != null && mZoomInOutAnim.isRunning()) { + mZoomInOutAnim.cancel(); + } + + if (!(getScaleX() < 1f || getScaleY() < 1f)) { + mZoomInOutAnim = new AnimatorSet(); + mZoomInOutAnim.setDuration(REORDERING_ZOOM_IN_OUT_DURATION); + mZoomInOutAnim.playTogether( + ObjectAnimator.ofFloat(this, "scaleX", mMinScale), + ObjectAnimator.ofFloat(this, "scaleY", mMinScale)); + mZoomInOutAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + // Show the delete drop target + if (mDeleteDropTarget != null) { + mDeleteDropTarget.setVisibility(View.VISIBLE); + mDeleteDropTarget.animate().alpha(1f) + .setDuration(REORDERING_DELETE_DROP_TARGET_FADE_DURATION) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + mDeleteDropTarget.setAlpha(0f); + } + }); + } + } + }); + mZoomInOutAnim.start(); + return true; + } + return false; + } + + protected void onStartReordering() { + if (AccessibilityManager.getInstance(mContext).isEnabled()) { + announceForAccessibility(mContext.getString( + R.string.keyguard_accessibility_widget_reorder_start)); + } + + // Set the touch state to reordering (allows snapping to pages, dragging a child, etc.) + mTouchState = TOUCH_STATE_REORDERING; + mIsReordering = true; + + // Mark all the non-widget pages as invisible + getVisiblePages(mTempVisiblePagesRange); + boundByReorderablePages(true, mTempVisiblePagesRange); + for (int i = 0; i < getPageCount(); ++i) { + if (i < mTempVisiblePagesRange[0] || i > mTempVisiblePagesRange[1]) { + getPageAt(i).setAlpha(0f); + } + } + + // We must invalidate to trigger a redraw to update the layers such that the drag view + // is always drawn on top + invalidate(); + } + + private void onPostReorderingAnimationCompleted() { + // Trigger the callback when reordering has settled + --mPostReorderingPreZoomInRemainingAnimationCount; + if (mPostReorderingPreZoomInRunnable != null && + mPostReorderingPreZoomInRemainingAnimationCount == 0) { + mPostReorderingPreZoomInRunnable.run(); + mPostReorderingPreZoomInRunnable = null; + } + } + + protected void onEndReordering() { + if (AccessibilityManager.getInstance(mContext).isEnabled()) { + announceForAccessibility(mContext.getString( + R.string.keyguard_accessibility_widget_reorder_end)); + } + mIsReordering = false; + + // Mark all the non-widget pages as visible again + getVisiblePages(mTempVisiblePagesRange); + boundByReorderablePages(true, mTempVisiblePagesRange); + for (int i = 0; i < getPageCount(); ++i) { + if (i < mTempVisiblePagesRange[0] || i > mTempVisiblePagesRange[1]) { + getPageAt(i).setAlpha(1f); + } + } + } + + public boolean startReordering() { + int dragViewIndex = getPageNearestToCenterOfScreen(); + mTempVisiblePagesRange[0] = 0; + mTempVisiblePagesRange[1] = getPageCount() - 1; + boundByReorderablePages(true, mTempVisiblePagesRange); + mReorderingStarted = true; + + // Check if we are within the reordering range + if (mTempVisiblePagesRange[0] <= dragViewIndex && + dragViewIndex <= mTempVisiblePagesRange[1]) { + if (zoomOut()) { + // Find the drag view under the pointer + mDragView = getChildAt(dragViewIndex); + + onStartReordering(); + } + return true; + } + return false; + } + + boolean isReordering(boolean testTouchState) { + boolean state = mIsReordering; + if (testTouchState) { + state &= (mTouchState == TOUCH_STATE_REORDERING); + } + return state; + } + void endReordering() { + // For simplicity, we call endReordering sometimes even if reordering was never started. + // In that case, we don't want to do anything. + if (!mReorderingStarted) return; + mReorderingStarted = false; + + // If we haven't flung-to-delete the current child, then we just animate the drag view + // back into position + final Runnable onCompleteRunnable = new Runnable() { + @Override + public void run() { + onEndReordering(); + } + }; + if (!mDeferringForDelete) { + mPostReorderingPreZoomInRunnable = new Runnable() { + public void run() { + zoomIn(onCompleteRunnable); + }; + }; + + mPostReorderingPreZoomInRemainingAnimationCount = + NUM_ANIMATIONS_RUNNING_BEFORE_ZOOM_OUT; + // Snap to the current page + snapToPage(indexOfChild(mDragView), 0); + // Animate the drag view back to the front position + animateDragViewToOriginalPosition(); + } else { + // Handled in post-delete-animation-callbacks + } + } + + // "Zooms in" the PagedView to highlight the current page + protected boolean zoomIn(final Runnable onCompleteRunnable) { + if (mZoomInOutAnim != null && mZoomInOutAnim.isRunning()) { + mZoomInOutAnim.cancel(); + } + if (getScaleX() < 1f || getScaleY() < 1f) { + mZoomInOutAnim = new AnimatorSet(); + mZoomInOutAnim.setDuration(REORDERING_ZOOM_IN_OUT_DURATION); + mZoomInOutAnim.playTogether( + ObjectAnimator.ofFloat(this, "scaleX", 1f), + ObjectAnimator.ofFloat(this, "scaleY", 1f)); + mZoomInOutAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + // Hide the delete drop target + if (mDeleteDropTarget != null) { + mDeleteDropTarget.animate().alpha(0f) + .setDuration(REORDERING_DELETE_DROP_TARGET_FADE_DURATION) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mDeleteDropTarget.setVisibility(View.GONE); + } + }); + } + } + @Override + public void onAnimationCancel(Animator animation) { + mDragView = null; + } + @Override + public void onAnimationEnd(Animator animation) { + mDragView = null; + if (onCompleteRunnable != null) { + onCompleteRunnable.run(); + } + } + }); + mZoomInOutAnim.start(); + return true; + } else { + if (onCompleteRunnable != null) { + onCompleteRunnable.run(); + } + } + return false; + } + + /* + * Flinging to delete - IN PROGRESS + */ + private PointF isFlingingToDelete() { + ViewConfiguration config = ViewConfiguration.get(getContext()); + mVelocityTracker.computeCurrentVelocity(1000, config.getScaledMaximumFlingVelocity()); + + if (mVelocityTracker.getYVelocity() < mFlingToDeleteThresholdVelocity) { + // Do a quick dot product test to ensure that we are flinging upwards + PointF vel = new PointF(mVelocityTracker.getXVelocity(), + mVelocityTracker.getYVelocity()); + PointF upVec = new PointF(0f, -1f); + float theta = (float) Math.acos(((vel.x * upVec.x) + (vel.y * upVec.y)) / + (vel.length() * upVec.length())); + if (theta <= Math.toRadians(FLING_TO_DELETE_MAX_FLING_DEGREES)) { + return vel; + } + } + return null; + } + + /** + * Creates an animation from the current drag view along its current velocity vector. + * For this animation, the alpha runs for a fixed duration and we update the position + * progressively. + */ + private static class FlingAlongVectorAnimatorUpdateListener implements AnimatorUpdateListener { + private View mDragView; + private PointF mVelocity; + private Rect mFrom; + private long mPrevTime; + private float mFriction; + + private final TimeInterpolator mAlphaInterpolator = new DecelerateInterpolator(0.75f); + + public FlingAlongVectorAnimatorUpdateListener(View dragView, PointF vel, Rect from, + long startTime, float friction) { + mDragView = dragView; + mVelocity = vel; + mFrom = from; + mPrevTime = startTime; + mFriction = 1f - (mDragView.getResources().getDisplayMetrics().density * friction); + } + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + float t = ((Float) animation.getAnimatedValue()).floatValue(); + long curTime = AnimationUtils.currentAnimationTimeMillis(); + + mFrom.left += (mVelocity.x * (curTime - mPrevTime) / 1000f); + mFrom.top += (mVelocity.y * (curTime - mPrevTime) / 1000f); + + mDragView.setTranslationX(mFrom.left); + mDragView.setTranslationY(mFrom.top); + mDragView.setAlpha(1f - mAlphaInterpolator.getInterpolation(t)); + + mVelocity.x *= mFriction; + mVelocity.y *= mFriction; + mPrevTime = curTime; + } + }; + + private Runnable createPostDeleteAnimationRunnable(final View dragView) { + return new Runnable() { + @Override + public void run() { + int dragViewIndex = indexOfChild(dragView); + + // For each of the pages around the drag view, animate them from the previous + // position to the new position in the layout (as a result of the drag view moving + // in the layout) + // NOTE: We can make an assumption here because we have side-bound pages that we + // will always have pages to animate in from the left + getVisiblePages(mTempVisiblePagesRange); + boundByReorderablePages(true, mTempVisiblePagesRange); + boolean isLastWidgetPage = (mTempVisiblePagesRange[0] == mTempVisiblePagesRange[1]); + boolean slideFromLeft = (isLastWidgetPage || + dragViewIndex > mTempVisiblePagesRange[0]); + + // Setup the scroll to the correct page before we swap the views + if (slideFromLeft) { + snapToPageImmediately(dragViewIndex - 1); + } + + int firstIndex = (isLastWidgetPage ? 0 : mTempVisiblePagesRange[0]); + int lastIndex = Math.min(mTempVisiblePagesRange[1], getPageCount() - 1); + int lowerIndex = (slideFromLeft ? firstIndex : dragViewIndex + 1 ); + int upperIndex = (slideFromLeft ? dragViewIndex - 1 : lastIndex); + ArrayList<Animator> animations = new ArrayList<Animator>(); + for (int i = lowerIndex; i <= upperIndex; ++i) { + View v = getChildAt(i); + // dragViewIndex < pageUnderPointIndex, so after we remove the + // drag view all subsequent views to pageUnderPointIndex will + // shift down. + int oldX = 0; + int newX = 0; + if (slideFromLeft) { + if (i == 0) { + // Simulate the page being offscreen with the page spacing + oldX = getViewportOffsetX() + getChildOffset(i) - getChildWidth(i) + - mPageSpacing; + } else { + oldX = getViewportOffsetX() + getChildOffset(i - 1); + } + newX = getViewportOffsetX() + getChildOffset(i); + } else { + oldX = getChildOffset(i) - getChildOffset(i - 1); + newX = 0; + } + + // Animate the view translation from its old position to its new + // position + AnimatorSet anim = (AnimatorSet) v.getTag(); + if (anim != null) { + anim.cancel(); + } + + // Note: Hacky, but we want to skip any optimizations to not draw completely + // hidden views + v.setAlpha(Math.max(v.getAlpha(), 0.01f)); + v.setTranslationX(oldX - newX); + anim = new AnimatorSet(); + anim.playTogether( + ObjectAnimator.ofFloat(v, "translationX", 0f), + ObjectAnimator.ofFloat(v, "alpha", 1f)); + animations.add(anim); + v.setTag(anim); + } + + AnimatorSet slideAnimations = new AnimatorSet(); + slideAnimations.playTogether(animations); + slideAnimations.setDuration(DELETE_SLIDE_IN_SIDE_PAGE_DURATION); + slideAnimations.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + final Runnable onCompleteRunnable = new Runnable() { + @Override + public void run() { + mDeferringForDelete = false; + onEndReordering(); + onRemoveViewAnimationCompleted(); + } + }; + zoomIn(onCompleteRunnable); + } + }); + slideAnimations.start(); + + removeView(dragView); + onRemoveView(dragView, true); + } + }; + } + + public void onFlingToDelete(PointF vel) { + final long startTime = AnimationUtils.currentAnimationTimeMillis(); + + // NOTE: Because it takes time for the first frame of animation to actually be + // called and we expect the animation to be a continuation of the fling, we have + // to account for the time that has elapsed since the fling finished. And since + // we don't have a startDelay, we will always get call to update when we call + // start() (which we want to ignore). + final TimeInterpolator tInterpolator = new TimeInterpolator() { + private int mCount = -1; + private long mStartTime; + private float mOffset; + /* Anonymous inner class ctor */ { + mStartTime = startTime; + } + + @Override + public float getInterpolation(float t) { + if (mCount < 0) { + mCount++; + } else if (mCount == 0) { + mOffset = Math.min(0.5f, (float) (AnimationUtils.currentAnimationTimeMillis() - + mStartTime) / FLING_TO_DELETE_FADE_OUT_DURATION); + mCount++; + } + return Math.min(1f, mOffset + t); + } + }; + + final Rect from = new Rect(); + final View dragView = mDragView; + from.left = (int) dragView.getTranslationX(); + from.top = (int) dragView.getTranslationY(); + AnimatorUpdateListener updateCb = new FlingAlongVectorAnimatorUpdateListener(dragView, vel, + from, startTime, FLING_TO_DELETE_FRICTION); + + final Runnable onAnimationEndRunnable = createPostDeleteAnimationRunnable(dragView); + + // Create and start the animation + ValueAnimator mDropAnim = new ValueAnimator(); + mDropAnim.setInterpolator(tInterpolator); + mDropAnim.setDuration(FLING_TO_DELETE_FADE_OUT_DURATION); + mDropAnim.setFloatValues(0f, 1f); + mDropAnim.addUpdateListener(updateCb); + mDropAnim.addListener(new AnimatorListenerAdapter() { + public void onAnimationEnd(Animator animation) { + onAnimationEndRunnable.run(); + } + }); + mDropAnim.start(); + mDeferringForDelete = true; + } + + /* Drag to delete */ + private boolean isHoveringOverDeleteDropTarget(int x, int y) { + if (mDeleteDropTarget != null) { + mAltTmpRect.set(0, 0, 0, 0); + View parent = (View) mDeleteDropTarget.getParent(); + if (parent != null) { + parent.getGlobalVisibleRect(mAltTmpRect); + } + mDeleteDropTarget.getGlobalVisibleRect(mTmpRect); + mTmpRect.offset(-mAltTmpRect.left, -mAltTmpRect.top); + return mTmpRect.contains(x, y); + } + return false; + } + + protected void setPageHoveringOverDeleteDropTarget(int viewIndex, boolean isHovering) {} + + private void onDropToDelete() { + final View dragView = mDragView; + + final float toScale = 0f; + final float toAlpha = 0f; + + // Create and start the complex animation + ArrayList<Animator> animations = new ArrayList<Animator>(); + AnimatorSet motionAnim = new AnimatorSet(); + motionAnim.setInterpolator(new DecelerateInterpolator(2)); + motionAnim.playTogether( + ObjectAnimator.ofFloat(dragView, "scaleX", toScale), + ObjectAnimator.ofFloat(dragView, "scaleY", toScale)); + animations.add(motionAnim); + + AnimatorSet alphaAnim = new AnimatorSet(); + alphaAnim.setInterpolator(new LinearInterpolator()); + alphaAnim.playTogether( + ObjectAnimator.ofFloat(dragView, "alpha", toAlpha)); + animations.add(alphaAnim); + + final Runnable onAnimationEndRunnable = createPostDeleteAnimationRunnable(dragView); + + AnimatorSet anim = new AnimatorSet(); + anim.playTogether(animations); + anim.setDuration(DRAG_TO_DELETE_FADE_OUT_DURATION); + anim.addListener(new AnimatorListenerAdapter() { + public void onAnimationEnd(Animator animation) { + onAnimationEndRunnable.run(); + } + }); + anim.start(); + + mDeferringForDelete = true; + } + + /* Accessibility */ + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setScrollable(getPageCount() > 1); + if (getCurrentPage() < getPageCount() - 1) { + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); + } + if (getCurrentPage() > 0) { + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); + } + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setScrollable(true); + if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) { + event.setFromIndex(mCurrentPage); + event.setToIndex(mCurrentPage); + event.setItemCount(getChildCount()); + } + } + + @Override + public boolean performAccessibilityAction(int action, Bundle arguments) { + if (super.performAccessibilityAction(action, arguments)) { + return true; + } + switch (action) { + case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: { + if (getCurrentPage() < getPageCount() - 1) { + scrollRight(); + return true; + } + } break; + case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: { + if (getCurrentPage() > 0) { + scrollLeft(); + return true; + } + } break; + } + return false; + } + + @Override + public boolean onHoverEvent(android.view.MotionEvent event) { + return true; + } +} diff --git a/packages/Keyguard/src/com/android/keyguard/SecurityMessageDisplay.java b/packages/Keyguard/src/com/android/keyguard/SecurityMessageDisplay.java new file mode 100644 index 0000000..7760279 --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/SecurityMessageDisplay.java @@ -0,0 +1,31 @@ +/* + * 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.internal.policy.impl.keyguard; + +public interface SecurityMessageDisplay { + public void setMessage(CharSequence msg, boolean important); + + public void setMessage(int resId, boolean important); + + public void setMessage(int resId, boolean important, Object... formatArgs); + + public void setTimeout(int timeout_ms); + + public void showBouncer(int animationDuration); + + public void hideBouncer(int animationDuration); +} diff --git a/packages/Keyguard/src/com/android/keyguard/SlidingChallengeLayout.java b/packages/Keyguard/src/com/android/keyguard/SlidingChallengeLayout.java new file mode 100644 index 0000000..073225f --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/SlidingChallengeLayout.java @@ -0,0 +1,1244 @@ +/* + * 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.internal.policy.impl.keyguard; + +import com.android.internal.R; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.FloatProperty; +import android.util.Log; +import android.util.Property; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityManager; +import android.view.animation.Interpolator; +import android.widget.Scroller; + +/** + * This layout handles interaction with the sliding security challenge views + * that overlay/resize other keyguard contents. + */ +public class SlidingChallengeLayout extends ViewGroup implements ChallengeLayout { + private static final String TAG = "SlidingChallengeLayout"; + private static final boolean DEBUG = false; + + // The drag handle is measured in dp above & below the top edge of the + // challenge view; these parameters change based on whether the challenge + // is open or closed. + private static final int DRAG_HANDLE_CLOSED_ABOVE = 8; // dp + private static final int DRAG_HANDLE_CLOSED_BELOW = 0; // dp + private static final int DRAG_HANDLE_OPEN_ABOVE = 8; // dp + private static final int DRAG_HANDLE_OPEN_BELOW = 0; // dp + + private static final int HANDLE_ANIMATE_DURATION = 250; // ms + + // Drawn to show the drag handle in closed state; crossfades to the challenge view + // when challenge is fully visible + private boolean mEdgeCaptured; + + private DisplayMetrics mDisplayMetrics; + + // Initialized during measurement from child layoutparams + private View mExpandChallengeView; + private KeyguardSecurityContainer mChallengeView; + private View mScrimView; + private View mWidgetsView; + + // Range: 0 (fully hidden) to 1 (fully visible) + private float mChallengeOffset = 1.f; + private boolean mChallengeShowing = true; + private boolean mChallengeShowingTargetState = true; + private boolean mWasChallengeShowing = true; + private boolean mIsBouncing = false; + + private final Scroller mScroller; + private ObjectAnimator mFader; + private int mScrollState; + private OnChallengeScrolledListener mScrollListener; + private OnBouncerStateChangedListener mBouncerListener; + + public static final int SCROLL_STATE_IDLE = 0; + public static final int SCROLL_STATE_DRAGGING = 1; + public static final int SCROLL_STATE_SETTLING = 2; + public static final int SCROLL_STATE_FADING = 3; + + private static final int CHALLENGE_FADE_OUT_DURATION = 100; + private static final int CHALLENGE_FADE_IN_DURATION = 160; + + private static final int MAX_SETTLE_DURATION = 600; // ms + + // ID of the pointer in charge of a current drag + private int mActivePointerId = INVALID_POINTER; + private static final int INVALID_POINTER = -1; + + // True if the user is currently dragging the slider + private boolean mDragging; + // True if the user may not drag until a new gesture begins + private boolean mBlockDrag; + + private VelocityTracker mVelocityTracker; + private int mMinVelocity; + private int mMaxVelocity; + private float mGestureStartX, mGestureStartY; // where did you first touch the screen? + private int mGestureStartChallengeBottom; // where was the challenge at that time? + + private int mDragHandleClosedBelow; // handle hitrect extension into the challenge view + private int mDragHandleClosedAbove; // extend the handle's hitrect this far above the line + private int mDragHandleOpenBelow; // handle hitrect extension into the challenge view + private int mDragHandleOpenAbove; // extend the handle's hitrect this far above the line + + private int mDragHandleEdgeSlop; + private int mChallengeBottomBound; // Number of pixels from the top of the challenge view + // that should remain on-screen + + private int mTouchSlop; + private int mTouchSlopSquare; + + float mHandleAlpha; + float mFrameAlpha; + float mFrameAnimationTarget = Float.MIN_VALUE; + private ObjectAnimator mHandleAnimation; + private ObjectAnimator mFrameAnimation; + + private boolean mHasGlowpad; + + // We have an internal and external version, and we and them together. + private boolean mChallengeInteractiveExternal = true; + private boolean mChallengeInteractiveInternal = true; + + static final Property<SlidingChallengeLayout, Float> HANDLE_ALPHA = + new FloatProperty<SlidingChallengeLayout>("handleAlpha") { + @Override + public void setValue(SlidingChallengeLayout view, float value) { + view.mHandleAlpha = value; + view.invalidate(); + } + + @Override + public Float get(SlidingChallengeLayout view) { + return view.mHandleAlpha; + } + }; + + // True if at least one layout pass has happened since the view was attached. + private boolean mHasLayout; + + private static final Interpolator sMotionInterpolator = new Interpolator() { + public float getInterpolation(float t) { + t -= 1.0f; + return t * t * t * t * t + 1.0f; + } + }; + + private static final Interpolator sHandleFadeInterpolator = new Interpolator() { + public float getInterpolation(float t) { + return t * t; + } + }; + + private final Runnable mEndScrollRunnable = new Runnable () { + public void run() { + completeChallengeScroll(); + } + }; + + private final OnClickListener mScrimClickListener = new OnClickListener() { + @Override + public void onClick(View v) { + hideBouncer(); + } + }; + + private final OnClickListener mExpandChallengeClickListener = new OnClickListener() { + @Override + public void onClick(View v) { + if (!isChallengeShowing()) { + showChallenge(true); + } + } + }; + + /** + * Listener interface that reports changes in scroll state of the challenge area. + */ + public interface OnChallengeScrolledListener { + /** + * The scroll state itself changed. + * + * <p>scrollState will be one of the following:</p> + * + * <ul> + * <li><code>SCROLL_STATE_IDLE</code> - The challenge area is stationary.</li> + * <li><code>SCROLL_STATE_DRAGGING</code> - The user is actively dragging + * the challenge area.</li> + * <li><code>SCROLL_STATE_SETTLING</code> - The challenge area is animating + * into place.</li> + * </ul> + * + * <p>Do not perform expensive operations (e.g. layout) + * while the scroll state is not <code>SCROLL_STATE_IDLE</code>.</p> + * + * @param scrollState The new scroll state of the challenge area. + */ + public void onScrollStateChanged(int scrollState); + + /** + * The precise position of the challenge area has changed. + * + * <p>NOTE: It is NOT safe to modify layout or call any View methods that may + * result in a requestLayout anywhere in your view hierarchy as a result of this call. + * It may be called during drawing.</p> + * + * @param scrollPosition New relative position of the challenge area. + * 1.f = fully visible/ready to be interacted with. + * 0.f = fully invisible/inaccessible to the user. + * @param challengeTop Position of the top edge of the challenge view in px in the + * SlidingChallengeLayout's coordinate system. + */ + public void onScrollPositionChanged(float scrollPosition, int challengeTop); + } + + public SlidingChallengeLayout(Context context) { + this(context, null); + } + + public SlidingChallengeLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SlidingChallengeLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + mScroller = new Scroller(context, sMotionInterpolator); + + final ViewConfiguration vc = ViewConfiguration.get(context); + mMinVelocity = vc.getScaledMinimumFlingVelocity(); + mMaxVelocity = vc.getScaledMaximumFlingVelocity(); + + final Resources res = getResources(); + mDragHandleEdgeSlop = res.getDimensionPixelSize(R.dimen.kg_edge_swipe_region_size); + + mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + mTouchSlopSquare = mTouchSlop * mTouchSlop; + + mDisplayMetrics = res.getDisplayMetrics(); + final float density = mDisplayMetrics.density; + + // top half of the lock icon, plus another 25% to be sure + mDragHandleClosedAbove = (int) (DRAG_HANDLE_CLOSED_ABOVE * density + 0.5f); + mDragHandleClosedBelow = (int) (DRAG_HANDLE_CLOSED_BELOW * density + 0.5f); + mDragHandleOpenAbove = (int) (DRAG_HANDLE_OPEN_ABOVE * density + 0.5f); + mDragHandleOpenBelow = (int) (DRAG_HANDLE_OPEN_BELOW * density + 0.5f); + + // how much space to account for in the handle when closed + mChallengeBottomBound = res.getDimensionPixelSize(R.dimen.kg_widget_pager_bottom_padding); + + setWillNotDraw(false); + setSystemUiVisibility(SYSTEM_UI_FLAG_LAYOUT_STABLE); + } + + public void setHandleAlpha(float alpha) { + if (mExpandChallengeView != null) { + mExpandChallengeView.setAlpha(alpha); + } + } + + public void setChallengeInteractive(boolean interactive) { + mChallengeInteractiveExternal = interactive; + if (mExpandChallengeView != null) { + mExpandChallengeView.setEnabled(interactive); + } + } + + void animateHandle(boolean visible) { + if (mHandleAnimation != null) { + mHandleAnimation.cancel(); + mHandleAnimation = null; + } + final float targetAlpha = visible ? 1.f : 0.f; + if (targetAlpha == mHandleAlpha) { + return; + } + mHandleAnimation = ObjectAnimator.ofFloat(this, HANDLE_ALPHA, targetAlpha); + mHandleAnimation.setInterpolator(sHandleFadeInterpolator); + mHandleAnimation.setDuration(HANDLE_ANIMATE_DURATION); + mHandleAnimation.start(); + } + + private void sendInitialListenerUpdates() { + if (mScrollListener != null) { + int challengeTop = mChallengeView != null ? mChallengeView.getTop() : 0; + mScrollListener.onScrollPositionChanged(mChallengeOffset, challengeTop); + mScrollListener.onScrollStateChanged(mScrollState); + } + } + + public void setOnChallengeScrolledListener(OnChallengeScrolledListener listener) { + mScrollListener = listener; + if (mHasLayout) { + sendInitialListenerUpdates(); + } + } + + public void setOnBouncerStateChangedListener(OnBouncerStateChangedListener listener) { + mBouncerListener = listener; + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + + mHasLayout = false; + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + removeCallbacks(mEndScrollRunnable); + mHasLayout = false; + } + + @Override + public void requestChildFocus(View child, View focused) { + if (mIsBouncing && child != mChallengeView) { + // Clear out of the bouncer if the user tries to move focus outside of + // the security challenge view. + hideBouncer(); + } + super.requestChildFocus(child, focused); + } + + // We want the duration of the page snap animation to be influenced by the distance that + // the screen has to travel, however, we don't want this duration to be effected in a + // purely linear fashion. Instead, we use this method to moderate the effect that the distance + // of travel has on the overall snap duration. + float distanceInfluenceForSnapDuration(float f) { + f -= 0.5f; // center the values about 0. + f *= 0.3f * Math.PI / 2.0f; + return (float) Math.sin(f); + } + + void setScrollState(int state) { + if (mScrollState != state) { + mScrollState = state; + + animateHandle(state == SCROLL_STATE_IDLE && !mChallengeShowing); + if (mScrollListener != null) { + mScrollListener.onScrollStateChanged(state); + } + } + } + + void completeChallengeScroll() { + setChallengeShowing(mChallengeShowingTargetState); + mChallengeOffset = mChallengeShowing ? 1.f : 0.f; + setScrollState(SCROLL_STATE_IDLE); + mChallengeInteractiveInternal = true; + mChallengeView.setLayerType(LAYER_TYPE_NONE, null); + } + + void setScrimView(View scrim) { + if (mScrimView != null) { + mScrimView.setOnClickListener(null); + } + mScrimView = scrim; + mScrimView.setVisibility(mIsBouncing ? VISIBLE : GONE); + mScrimView.setFocusable(true); + mScrimView.setOnClickListener(mScrimClickListener); + } + + /** + * Animate the bottom edge of the challenge view to the given position. + * + * @param y desired final position for the bottom edge of the challenge view in px + * @param velocity velocity in + */ + void animateChallengeTo(int y, int velocity) { + if (mChallengeView == null) { + // Nothing to do. + return; + } + + cancelTransitionsInProgress(); + + mChallengeInteractiveInternal = false; + mChallengeView.setLayerType(LAYER_TYPE_HARDWARE, null); + final int sy = mChallengeView.getBottom(); + final int dy = y - sy; + if (dy == 0) { + completeChallengeScroll(); + return; + } + + setScrollState(SCROLL_STATE_SETTLING); + + final int childHeight = mChallengeView.getHeight(); + final int halfHeight = childHeight / 2; + final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dy) / childHeight); + final float distance = halfHeight + halfHeight * + distanceInfluenceForSnapDuration(distanceRatio); + + int duration = 0; + velocity = Math.abs(velocity); + if (velocity > 0) { + duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); + } else { + final float childDelta = (float) Math.abs(dy) / childHeight; + duration = (int) ((childDelta + 1) * 100); + } + duration = Math.min(duration, MAX_SETTLE_DURATION); + + mScroller.startScroll(0, sy, 0, dy, duration); + postInvalidateOnAnimation(); + } + + private void setChallengeShowing(boolean showChallenge) { + if (mChallengeShowing == showChallenge) { + return; + } + mChallengeShowing = showChallenge; + + if (mExpandChallengeView == null || mChallengeView == null) { + // These might not be here yet if we haven't been through layout. + // If we haven't, the first layout pass will set everything up correctly + // based on mChallengeShowing as set above. + return; + } + + if (mChallengeShowing) { + mExpandChallengeView.setVisibility(View.INVISIBLE); + mChallengeView.setVisibility(View.VISIBLE); + if (AccessibilityManager.getInstance(mContext).isEnabled()) { + mChallengeView.requestAccessibilityFocus(); + mChallengeView.announceForAccessibility(mContext.getString( + R.string.keyguard_accessibility_unlock_area_expanded)); + } + } else { + mExpandChallengeView.setVisibility(View.VISIBLE); + mChallengeView.setVisibility(View.INVISIBLE); + if (AccessibilityManager.getInstance(mContext).isEnabled()) { + mExpandChallengeView.requestAccessibilityFocus(); + mChallengeView.announceForAccessibility(mContext.getString( + R.string.keyguard_accessibility_unlock_area_collapsed)); + } + } + } + + /** + * @return true if the challenge is at all visible. + */ + public boolean isChallengeShowing() { + return mChallengeShowing; + } + + @Override + public boolean isChallengeOverlapping() { + return mChallengeShowing; + } + + @Override + public boolean isBouncing() { + return mIsBouncing; + } + + @Override + public int getBouncerAnimationDuration() { + return HANDLE_ANIMATE_DURATION; + } + + @Override + public void showBouncer() { + if (mIsBouncing) return; + mWasChallengeShowing = mChallengeShowing; + mIsBouncing = true; + showChallenge(true); + if (mScrimView != null) { + Animator anim = ObjectAnimator.ofFloat(mScrimView, "alpha", 1f); + anim.setDuration(HANDLE_ANIMATE_DURATION); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + mScrimView.setVisibility(VISIBLE); + } + }); + anim.start(); + } + if (mChallengeView != null) { + mChallengeView.showBouncer(HANDLE_ANIMATE_DURATION); + } + + if (mBouncerListener != null) { + mBouncerListener.onBouncerStateChanged(true); + } + } + + @Override + public void hideBouncer() { + if (!mIsBouncing) return; + if (!mWasChallengeShowing) showChallenge(false); + mIsBouncing = false; + + if (mScrimView != null) { + Animator anim = ObjectAnimator.ofFloat(mScrimView, "alpha", 0f); + anim.setDuration(HANDLE_ANIMATE_DURATION); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mScrimView.setVisibility(GONE); + } + }); + anim.start(); + } + if (mChallengeView != null) { + mChallengeView.hideBouncer(HANDLE_ANIMATE_DURATION); + } + if (mBouncerListener != null) { + mBouncerListener.onBouncerStateChanged(false); + } + } + + private int getChallengeMargin(boolean expanded) { + return expanded && mHasGlowpad ? 0 : mDragHandleEdgeSlop; + } + + private float getChallengeAlpha() { + float x = mChallengeOffset - 1; + return x * x * x + 1.f; + } + + @Override + public void requestDisallowInterceptTouchEvent(boolean allowIntercept) { + // We'll intercept whoever we feel like! ...as long as it isn't a challenge view. + // If there are one or more pointers in the challenge view before we take over + // touch events, onInterceptTouchEvent will set mBlockDrag. + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + + final int action = ev.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_DOWN: + mGestureStartX = ev.getX(); + mGestureStartY = ev.getY(); + mBlockDrag = false; + break; + + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + resetTouch(); + break; + + case MotionEvent.ACTION_MOVE: + final int count = ev.getPointerCount(); + for (int i = 0; i < count; i++) { + final float x = ev.getX(i); + final float y = ev.getY(i); + if (!mIsBouncing && mActivePointerId == INVALID_POINTER + && (crossedDragHandle(x, y, mGestureStartY) + || (isInChallengeView(x, y) && + mScrollState == SCROLL_STATE_SETTLING))) { + mActivePointerId = ev.getPointerId(i); + mGestureStartX = x; + mGestureStartY = y; + mGestureStartChallengeBottom = getChallengeBottom(); + mDragging = true; + mChallengeView.setLayerType(LAYER_TYPE_HARDWARE, null); + } else if (mChallengeShowing && isInChallengeView(x, y)) { + mBlockDrag = true; + } + } + break; + } + + if (mBlockDrag || isChallengeInteractionBlocked()) { + mActivePointerId = INVALID_POINTER; + mDragging = false; + } + + return mDragging; + } + + private boolean isChallengeInteractionBlocked() { + return !mChallengeInteractiveExternal || !mChallengeInteractiveInternal; + } + + private void resetTouch() { + mVelocityTracker.recycle(); + mVelocityTracker = null; + mActivePointerId = INVALID_POINTER; + mDragging = mBlockDrag = false; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + + final int action = ev.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_DOWN: + mBlockDrag = false; + mGestureStartX = ev.getX(); + mGestureStartY = ev.getY(); + break; + + case MotionEvent.ACTION_CANCEL: + if (mDragging && !isChallengeInteractionBlocked()) { + showChallenge(0); + } + resetTouch(); + break; + + case MotionEvent.ACTION_POINTER_UP: + if (mActivePointerId != ev.getPointerId(ev.getActionIndex())) { + break; + } + case MotionEvent.ACTION_UP: + if (mDragging && !isChallengeInteractionBlocked()) { + mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity); + showChallenge((int) mVelocityTracker.getYVelocity(mActivePointerId)); + } + resetTouch(); + break; + + case MotionEvent.ACTION_MOVE: + if (!mDragging && !mBlockDrag && !mIsBouncing) { + final int count = ev.getPointerCount(); + for (int i = 0; i < count; i++) { + final float x = ev.getX(i); + final float y = ev.getY(i); + + if ((isInDragHandle(x, y) || crossedDragHandle(x, y, mGestureStartY) || + (isInChallengeView(x, y) && mScrollState == SCROLL_STATE_SETTLING)) + && mActivePointerId == INVALID_POINTER + && !isChallengeInteractionBlocked()) { + mGestureStartX = x; + mGestureStartY = y; + mActivePointerId = ev.getPointerId(i); + mGestureStartChallengeBottom = getChallengeBottom(); + mDragging = true; + mChallengeView.setLayerType(LAYER_TYPE_HARDWARE, null); + break; + } + } + } + // Not an else; this can be set above. + if (mDragging) { + // No-op if already in this state, but set it here in case we arrived + // at this point from either intercept or the above. + setScrollState(SCROLL_STATE_DRAGGING); + + final int index = ev.findPointerIndex(mActivePointerId); + if (index < 0) { + // Oops, bogus state. We lost some touch events somewhere. + // Just drop it with no velocity and let things settle. + resetTouch(); + showChallenge(0); + return true; + } + final float y = ev.getY(index); + final float pos = Math.min(y - mGestureStartY, + getLayoutBottom() - mChallengeBottomBound); + + moveChallengeTo(mGestureStartChallengeBottom + (int) pos); + } + break; + } + return true; + } + + /** + * The lifecycle of touch events is subtle and it's very easy to do something + * that will cause bugs that will be nasty to track when overriding this method. + * Normally one should always override onInterceptTouchEvent instead. + * + * To put it another way, don't try this at home. + */ + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + final int action = ev.getActionMasked(); + boolean handled = false; + if (action == MotionEvent.ACTION_DOWN) { + // Defensive programming: if we didn't get the UP or CANCEL, reset anyway. + mEdgeCaptured = false; + } + if (mWidgetsView != null && !mIsBouncing && (mEdgeCaptured || isEdgeSwipeBeginEvent(ev))) { + // Normally we would need to do a lot of extra stuff here. + // We can only get away with this because we haven't padded in + // the widget pager or otherwise transformed it during layout. + // We also don't support things like splitting MotionEvents. + + // We set handled to captured even if dispatch is returning false here so that + // we don't send a different view a busted or incomplete event stream. + handled = mEdgeCaptured |= mWidgetsView.dispatchTouchEvent(ev); + } + + if (!handled && !mEdgeCaptured) { + handled = super.dispatchTouchEvent(ev); + } + + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { + mEdgeCaptured = false; + } + + return handled; + } + + private boolean isEdgeSwipeBeginEvent(MotionEvent ev) { + if (ev.getActionMasked() != MotionEvent.ACTION_DOWN) { + return false; + } + + final float x = ev.getX(); + return x < mDragHandleEdgeSlop || x >= getWidth() - mDragHandleEdgeSlop; + } + + /** + * We only want to add additional vertical space to the drag handle when the panel is fully + * closed. + */ + private int getDragHandleSizeAbove() { + return isChallengeShowing() ? mDragHandleOpenAbove : mDragHandleClosedAbove; + } + private int getDragHandleSizeBelow() { + return isChallengeShowing() ? mDragHandleOpenBelow : mDragHandleClosedBelow; + } + + private boolean isInChallengeView(float x, float y) { + return isPointInView(x, y, mChallengeView); + } + + private boolean isInDragHandle(float x, float y) { + return isPointInView(x, y, mExpandChallengeView); + } + + private boolean isPointInView(float x, float y, View view) { + if (view == null) { + return false; + } + return x >= view.getLeft() && y >= view.getTop() + && x < view.getRight() && y < view.getBottom(); + } + + private boolean crossedDragHandle(float x, float y, float initialY) { + + final int challengeTop = mChallengeView.getTop(); + final boolean horizOk = x >= 0 && x < getWidth(); + + final boolean vertOk; + if (mChallengeShowing) { + vertOk = initialY < (challengeTop - getDragHandleSizeAbove()) && + y > challengeTop + getDragHandleSizeBelow(); + } else { + vertOk = initialY > challengeTop + getDragHandleSizeBelow() && + y < challengeTop - getDragHandleSizeAbove(); + } + return horizOk && vertOk; + } + + 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); + } + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + if (MeasureSpec.getMode(widthSpec) != MeasureSpec.EXACTLY || + MeasureSpec.getMode(heightSpec) != MeasureSpec.EXACTLY) { + throw new IllegalArgumentException( + "SlidingChallengeLayout must be measured with an exact size"); + } + + final int width = MeasureSpec.getSize(widthSpec); + final int height = MeasureSpec.getSize(heightSpec); + setMeasuredDimension(width, height); + + // Find one and only one challenge view. + final View oldChallengeView = mChallengeView; + final View oldExpandChallengeView = mChallengeView; + mChallengeView = null; + mExpandChallengeView = null; + final int count = getChildCount(); + + // First iteration through the children finds special children and sets any associated + // state. + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (lp.childType == LayoutParams.CHILD_TYPE_CHALLENGE) { + if (mChallengeView != null) { + throw new IllegalStateException( + "There may only be one child with layout_isChallenge=\"true\""); + } + if (!(child instanceof KeyguardSecurityContainer)) { + throw new IllegalArgumentException( + "Challenge must be a KeyguardSecurityContainer"); + } + mChallengeView = (KeyguardSecurityContainer) child; + if (mChallengeView != oldChallengeView) { + mChallengeView.setVisibility(mChallengeShowing ? VISIBLE : INVISIBLE); + } + // We're going to play silly games with the frame's background drawable later. + if (!mHasLayout) { + // Set up the margin correctly based on our content for the first run. + mHasGlowpad = child.findViewById(R.id.keyguard_selector_view) != null; + lp.leftMargin = lp.rightMargin = getChallengeMargin(true); + } + } else if (lp.childType == LayoutParams.CHILD_TYPE_EXPAND_CHALLENGE_HANDLE) { + if (mExpandChallengeView != null) { + throw new IllegalStateException( + "There may only be one child with layout_childType" + + "=\"expandChallengeHandle\""); + } + mExpandChallengeView = child; + if (mExpandChallengeView != oldExpandChallengeView) { + mExpandChallengeView.setVisibility(mChallengeShowing ? INVISIBLE : VISIBLE); + mExpandChallengeView.setOnClickListener(mExpandChallengeClickListener); + } + } else if (lp.childType == LayoutParams.CHILD_TYPE_SCRIM) { + setScrimView(child); + } else if (lp.childType == LayoutParams.CHILD_TYPE_WIDGETS) { + mWidgetsView = child; + } + } + + // We want to measure the challenge view first, since the KeyguardWidgetPager + // needs to do things its measure pass that are dependent on the challenge view + // having been measured. + if (mChallengeView != null && mChallengeView.getVisibility() != View.GONE) { + // This one's a little funny. If the IME is present - reported in the form + // of insets on the root view - we only give the challenge the space it would + // have had if the IME wasn't there in order to keep the rest of the layout stable. + // We base this on the layout_maxHeight on the challenge view. If it comes out + // negative or zero, either we didn't have a maxHeight or we're totally out of space, + // so give up and measure as if this rule weren't there. + int challengeHeightSpec = heightSpec; + final View root = getRootView(); + if (root != null) { + final LayoutParams lp = (LayoutParams) mChallengeView.getLayoutParams(); + final int specSize = MeasureSpec.getSize(heightSpec); + final int windowHeight = mDisplayMetrics.heightPixels - root.getPaddingTop(); + final int diff = windowHeight - specSize; + final int maxChallengeHeight = lp.maxHeight - diff; + if (maxChallengeHeight > 0) { + challengeHeightSpec = makeChildMeasureSpec(maxChallengeHeight, lp.height); + } + } + measureChildWithMargins(mChallengeView, widthSpec, 0, challengeHeightSpec, 0); + } + + // Measure the rest of the children + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() == GONE) { + continue; + } + // Don't measure the challenge view twice! + if (child == mChallengeView) continue; + + // Measure children. Widget frame measures special, so that we can ignore + // insets for the IME. + int parentWidthSpec = widthSpec, parentHeightSpec = heightSpec; + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (lp.childType == LayoutParams.CHILD_TYPE_WIDGETS) { + final View root = getRootView(); + if (root != null) { + // This calculation is super dodgy and relies on several assumptions. + // Specifically that the root of the window will be padded in for insets + // and that the window is LAYOUT_IN_SCREEN. + final int windowWidth = mDisplayMetrics.widthPixels; + final int windowHeight = mDisplayMetrics.heightPixels - root.getPaddingTop(); + parentWidthSpec = MeasureSpec.makeMeasureSpec( + windowWidth, MeasureSpec.EXACTLY); + parentHeightSpec = MeasureSpec.makeMeasureSpec( + windowHeight, MeasureSpec.EXACTLY); + } + } + measureChildWithMargins(child, parentWidthSpec, 0, parentHeightSpec, 0); + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + final int paddingLeft = getPaddingLeft(); + final int paddingTop = getPaddingTop(); + final int paddingRight = getPaddingRight(); + final int paddingBottom = getPaddingBottom(); + final int width = r - l; + final int height = b - t; + + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + + if (child.getVisibility() == GONE) continue; + + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + if (lp.childType == LayoutParams.CHILD_TYPE_CHALLENGE) { + // Challenge views pin to the bottom, offset by a portion of their height, + // and center horizontally. + final int center = (paddingLeft + width - paddingRight) / 2; + final int childWidth = child.getMeasuredWidth(); + final int childHeight = child.getMeasuredHeight(); + final int left = center - childWidth / 2; + final int layoutBottom = height - paddingBottom - lp.bottomMargin; + // We use the top of the challenge view to position the handle, so + // we never want less than the handle size showing at the bottom. + final int bottom = layoutBottom + (int) ((childHeight - mChallengeBottomBound) + * (1 - mChallengeOffset)); + child.setAlpha(getChallengeAlpha()); + child.layout(left, bottom - childHeight, left + childWidth, bottom); + } else if (lp.childType == LayoutParams.CHILD_TYPE_EXPAND_CHALLENGE_HANDLE) { + final int center = (paddingLeft + width - paddingRight) / 2; + final int left = center - child.getMeasuredWidth() / 2; + final int right = left + child.getMeasuredWidth(); + final int bottom = height - paddingBottom - lp.bottomMargin; + final int top = bottom - child.getMeasuredHeight(); + child.layout(left, top, right, bottom); + } else { + // Non-challenge views lay out from the upper left, layered. + child.layout(paddingLeft + lp.leftMargin, + paddingTop + lp.topMargin, + paddingLeft + child.getMeasuredWidth(), + paddingTop + child.getMeasuredHeight()); + } + } + + if (!mHasLayout) { + mHasLayout = true; + } + } + + @Override + public void draw(Canvas c) { + super.draw(c); + if (DEBUG) { + final Paint debugPaint = new Paint(); + debugPaint.setColor(0x40FF00CC); + // show the isInDragHandle() rect + c.drawRect(mDragHandleEdgeSlop, + mChallengeView.getTop() - getDragHandleSizeAbove(), + getWidth() - mDragHandleEdgeSlop, + mChallengeView.getTop() + getDragHandleSizeBelow(), + debugPaint); + } + } + + public void computeScroll() { + super.computeScroll(); + + if (!mScroller.isFinished()) { + if (mChallengeView == null) { + // Can't scroll if the view is missing. + Log.e(TAG, "Challenge view missing in computeScroll"); + mScroller.abortAnimation(); + return; + } + + mScroller.computeScrollOffset(); + moveChallengeTo(mScroller.getCurrY()); + + if (mScroller.isFinished()) { + post(mEndScrollRunnable); + } + } + } + + private void cancelTransitionsInProgress() { + if (!mScroller.isFinished()) { + mScroller.abortAnimation(); + completeChallengeScroll(); + } + if (mFader != null) { + mFader.cancel(); + } + } + + public void fadeInChallenge() { + fadeChallenge(true); + } + + public void fadeOutChallenge() { + fadeChallenge(false); + } + + public void fadeChallenge(final boolean show) { + if (mChallengeView != null) { + + cancelTransitionsInProgress(); + float alpha = show ? 1f : 0f; + int duration = show ? CHALLENGE_FADE_IN_DURATION : CHALLENGE_FADE_OUT_DURATION; + mFader = ObjectAnimator.ofFloat(mChallengeView, "alpha", alpha); + mFader.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + onFadeStart(show); + } + @Override + public void onAnimationEnd(Animator animation) { + onFadeEnd(show); + } + }); + mFader.setDuration(duration); + mFader.start(); + } + } + + private int getMaxChallengeBottom() { + if (mChallengeView == null) return 0; + final int layoutBottom = getLayoutBottom(); + final int challengeHeight = mChallengeView.getMeasuredHeight(); + + return (layoutBottom + challengeHeight - mChallengeBottomBound); + } + + private int getMinChallengeBottom() { + return getLayoutBottom(); + } + + + private void onFadeStart(boolean show) { + mChallengeInteractiveInternal = false; + mChallengeView.setLayerType(LAYER_TYPE_HARDWARE, null); + + if (show) { + moveChallengeTo(getMinChallengeBottom()); + } + + setScrollState(SCROLL_STATE_FADING); + } + + private void onFadeEnd(boolean show) { + mChallengeInteractiveInternal = true; + setChallengeShowing(show); + + if (!show) { + moveChallengeTo(getMaxChallengeBottom()); + } + + mChallengeView.setLayerType(LAYER_TYPE_NONE, null); + mFader = null; + setScrollState(SCROLL_STATE_IDLE); + } + + public int getMaxChallengeTop() { + if (mChallengeView == null) return 0; + + final int layoutBottom = getLayoutBottom(); + final int challengeHeight = mChallengeView.getMeasuredHeight(); + return layoutBottom - challengeHeight; + } + + /** + * Move the bottom edge of mChallengeView to a new position and notify the listener + * if it represents a change in position. Changes made through this method will + * be stable across layout passes. If this method is called before first layout of + * this SlidingChallengeLayout it will have no effect. + * + * @param bottom New bottom edge in px in this SlidingChallengeLayout's coordinate system. + * @return true if the challenge view was moved + */ + private boolean moveChallengeTo(int bottom) { + if (mChallengeView == null || !mHasLayout) { + return false; + } + + final int layoutBottom = getLayoutBottom(); + final int challengeHeight = mChallengeView.getHeight(); + + bottom = Math.max(getMinChallengeBottom(), + Math.min(bottom, getMaxChallengeBottom())); + + float offset = 1.f - (float) (bottom - layoutBottom) / + (challengeHeight - mChallengeBottomBound); + mChallengeOffset = offset; + if (offset > 0 && !mChallengeShowing) { + setChallengeShowing(true); + } + + mChallengeView.layout(mChallengeView.getLeft(), + bottom - mChallengeView.getHeight(), mChallengeView.getRight(), bottom); + + mChallengeView.setAlpha(getChallengeAlpha()); + if (mScrollListener != null) { + mScrollListener.onScrollPositionChanged(offset, mChallengeView.getTop()); + } + postInvalidateOnAnimation(); + return true; + } + + /** + * The bottom edge of this SlidingChallengeLayout's coordinate system; will coincide with + * the bottom edge of mChallengeView when the challenge is fully opened. + */ + private int getLayoutBottom() { + final int bottomMargin = (mChallengeView == null) + ? 0 + : ((LayoutParams) mChallengeView.getLayoutParams()).bottomMargin; + final int layoutBottom = getMeasuredHeight() - getPaddingBottom() - bottomMargin; + return layoutBottom; + } + + /** + * The bottom edge of mChallengeView; essentially, where the sliding challenge 'is'. + */ + private int getChallengeBottom() { + if (mChallengeView == null) return 0; + + return mChallengeView.getBottom(); + } + + /** + * Show or hide the challenge view, animating it if necessary. + * @param show true to show, false to hide + */ + public void showChallenge(boolean show) { + showChallenge(show, 0); + if (!show) { + // Block any drags in progress so that callers can use this to disable dragging + // for other touch interactions. + mBlockDrag = true; + } + } + + private void showChallenge(int velocity) { + boolean show = false; + if (Math.abs(velocity) > mMinVelocity) { + show = velocity < 0; + } else { + show = mChallengeOffset >= 0.5f; + } + showChallenge(show, velocity); + } + + private void showChallenge(boolean show, int velocity) { + if (mChallengeView == null) { + setChallengeShowing(false); + return; + } + + if (mHasLayout) { + mChallengeShowingTargetState = show; + final int layoutBottom = getLayoutBottom(); + animateChallengeTo(show ? layoutBottom : + layoutBottom + mChallengeView.getHeight() - mChallengeBottomBound, velocity); + } + } + + @Override + public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { + return new LayoutParams(getContext(), attrs); + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams ? new LayoutParams((LayoutParams) p) : + p instanceof MarginLayoutParams ? new LayoutParams((MarginLayoutParams) p) : + new LayoutParams(p); + } + + @Override + protected ViewGroup.LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams; + } + + public static class LayoutParams extends MarginLayoutParams { + public int childType = CHILD_TYPE_NONE; + public static final int CHILD_TYPE_NONE = 0; + public static final int CHILD_TYPE_CHALLENGE = 2; + public static final int CHILD_TYPE_SCRIM = 4; + public static final int CHILD_TYPE_WIDGETS = 5; + public static final int CHILD_TYPE_EXPAND_CHALLENGE_HANDLE = 6; + + public int maxHeight; + + public LayoutParams() { + this(MATCH_PARENT, WRAP_CONTENT); + } + + public LayoutParams(int width, int height) { + super(width, height); + } + + public LayoutParams(android.view.ViewGroup.LayoutParams source) { + super(source); + } + + public LayoutParams(MarginLayoutParams source) { + super(source); + } + + public LayoutParams(LayoutParams source) { + super(source); + + childType = source.childType; + } + + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + + final TypedArray a = c.obtainStyledAttributes(attrs, + R.styleable.SlidingChallengeLayout_Layout); + childType = a.getInt(R.styleable.SlidingChallengeLayout_Layout_layout_childType, + CHILD_TYPE_NONE); + maxHeight = a.getDimensionPixelSize( + R.styleable.SlidingChallengeLayout_Layout_layout_maxHeight, 0); + a.recycle(); + } + } +} |