diff options
3 files changed, 206 insertions, 150 deletions
diff --git a/policy/src/com/android/internal/policy/impl/BiometricSensorUnlock.java b/policy/src/com/android/internal/policy/impl/BiometricSensorUnlock.java new file mode 100644 index 0000000..d445d5c --- /dev/null +++ b/policy/src/com/android/internal/policy/impl/BiometricSensorUnlock.java @@ -0,0 +1,52 @@ +/* + * 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; + +import android.view.View; + +interface BiometricSensorUnlock { + // Returns 'true' if the biometric sensor is available and is selected by user. + public boolean installedAndSelected(); + + // Returns 'true' if the biometric sensor has started its unlock procedure but has not yet + // accepted or rejected the user. + public boolean isRunning(); + + // Show the interface, but don't start the unlock procedure. The interface should disappear + // after the specified timeout. If the timeout is 0, the interface shows until another event, + // such as calling hide(), causes it to disappear. + public void show(long timeoutMilliseconds); + + // Hide the interface, if any, exposing the lockscreen. + public void hide(); + + // Stop the unlock procedure if running. Returns 'true' if it was in fact running. + public boolean stop(); + + // Start the unlock procedure. Returns ‘false’ if it can’t be started or if the backup should + // be used. + public boolean start(boolean suppressBiometricUnlock); + + // Provide a view to work within. + public void initializeAreaView(View topView); + + // Clean up any resources used by the biometric unlock. + public void cleanUp(); + + // Returns the Device Policy Manager quality (e.g. PASSWORD_QUALITY_BIOMETRIC_WEAK). + public int getQuality(); +} diff --git a/policy/src/com/android/internal/policy/impl/FaceUnlock.java b/policy/src/com/android/internal/policy/impl/FaceUnlock.java index 31fbaaf..7b0a086 100644 --- a/policy/src/com/android/internal/policy/impl/FaceUnlock.java +++ b/policy/src/com/android/internal/policy/impl/FaceUnlock.java @@ -21,6 +21,7 @@ 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; @@ -33,7 +34,7 @@ import android.telephony.TelephonyManager; import android.util.Log; import android.view.View; -public class FaceUnlock implements Handler.Callback { +public class FaceUnlock implements BiometricSensorUnlock, Handler.Callback { private static final boolean DEBUG = false; private static final String TAG = "FULLockscreen"; @@ -52,10 +53,6 @@ public class FaceUnlock implements Handler.Callback { private boolean mServiceRunning = false; private final Object mServiceRunningLock = new Object(); - // Long enough to stay visible while dialer comes up - // Short enough to not be visible if the user goes back immediately - private final int VIEW_AREA_EMERGENCY_DIALER_TIMEOUT = 1000; - // Long enough to stay visible while the service starts // Short enough to not have to wait long for backup if service fails to start or crashes // The service can take a couple of seconds to start on the first try after boot @@ -80,22 +77,65 @@ public class FaceUnlock implements Handler.Callback { mHandler = new Handler(this); } - public void cleanUp() { - if (mService != null) { - try { - mService.unregisterCallback(mFaceLockCallback); - } catch (RemoteException e) { - // Not much we can do + // Indicates whether FaceLock is in use + public boolean installedAndSelected() { + return (mLockPatternUtils.usingBiometricWeak() && + mLockPatternUtils.isBiometricWeakInstalled()); + } + + public boolean isRunning() { + return mServiceRunning; + } + + // Shows the FaceLock area for a period of time + public void show(long timeoutMillis) { + showArea(); + if (timeoutMillis > 0) + mHandler.sendEmptyMessageDelayed(MSG_HIDE_AREA_VIEW, timeoutMillis); + } + + // Hides the FaceLock area immediately + public void hide() { + // Remove messages to prevent a delayed show message from undo-ing the hide + removeAreaDisplayMessages(); + mHandler.sendEmptyMessage(MSG_HIDE_AREA_VIEW); + } + + // Tells FaceLock to stop and then unbinds from the FaceLock service + public boolean stop() { + boolean wasRunning = false; + if (installedAndSelected()) { + stopUi(); + + if (mBoundToService) { + wasRunning = true; + if (DEBUG) Log.d(TAG, "before unbind from FaceLock service"); + if (mService != null) { + try { + mService.unregisterCallback(mFaceLockCallback); + } catch (RemoteException e) { + // Not much we can do + } + } + mContext.unbindService(mConnection); + if (DEBUG) Log.d(TAG, "after unbind from FaceLock service"); + 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 FaceLock when not bound"); } - stop(); - mService = null; } + + return wasRunning; } - /** When screen is turned on and focused, need to bind to FaceLock service if we are using - * FaceLock, but only if we're not dealing with a call - */ - public void activateIfAble(boolean hasOverlay) { + /** + * When screen is turned on and focused, need to bind to FaceLock service if we are using + * FaceLock, but only if we're not dealing with a call + */ + public boolean start(boolean suppressBiometricUnlock) { final boolean tooManyFaceUnlockTries = mUpdateMonitor.getMaxFaceUnlockAttemptsReached(); final int failedBackupAttempts = mUpdateMonitor.getFailedAttempts(); final boolean backupIsTimedOut = @@ -103,42 +143,31 @@ public class FaceUnlock implements Handler.Callback { if (tooManyFaceUnlockTries) Log.i(TAG, "tooManyFaceUnlockTries: " + tooManyFaceUnlockTries); if (mUpdateMonitor.getPhoneState() == TelephonyManager.CALL_STATE_IDLE && installedAndSelected() - && !hasOverlay + && !suppressBiometricUnlock && !tooManyFaceUnlockTries && !backupIsTimedOut) { bind(); // Show FaceLock area, but only for a little bit so lockpattern will become visible if // FaceLock fails to start or crashes - showAreaWithTimeout(VIEW_AREA_SERVICE_TIMEOUT); + show(VIEW_AREA_SERVICE_TIMEOUT); // When switching between portrait and landscape view while FaceLock is running, the // screen will eventually go dark unless we poke the wakelock when FaceLock is // restarted mKeyguardScreenCallback.pokeWakelock(); } else { - hideArea(); + hide(); + return false; } - } - - public boolean isServiceRunning() { - return mServiceRunning; - } - public int viewAreaEmergencyDialerTimeout() { - return VIEW_AREA_EMERGENCY_DIALER_TIMEOUT; - } - - // Indicates whether FaceLock is in use - public boolean installedAndSelected() { - return (mLockPatternUtils.usingBiometricWeak() && - mLockPatternUtils.isBiometricWeakInstalled()); + return true; } // Takes care of FaceLock area when layout is created - public void initializeAreaView(View view) { + public void initializeAreaView(View topView) { if (installedAndSelected()) { - mAreaView = view.findViewById(R.id.faceLockAreaView); + mAreaView = topView.findViewById(R.id.faceLockAreaView); if (mAreaView == null) { Log.e(TAG, "Layout does not have areaView and FaceLock is enabled"); } @@ -147,13 +176,20 @@ public class FaceUnlock implements Handler.Callback { } } - // Stops FaceLock if it is running and reports back whether it was running or not - public boolean stopIfRunning() { - if (installedAndSelected() && mBoundToService) { - stopAndUnbind(); - return true; + public void cleanUp() { + if (mService != null) { + try { + mService.unregisterCallback(mFaceLockCallback); + } catch (RemoteException e) { + // Not much we can do + } + stopUi(); + mService = null; } - return false; + } + + public int getQuality() { + return DevicePolicyManager.PASSWORD_QUALITY_BIOMETRIC_WEAK; } // Handles covering or exposing FaceLock area on the client side when FaceLock starts or stops @@ -186,28 +222,15 @@ public class FaceUnlock implements Handler.Callback { } // Shows the FaceLock area immediately - public void showArea() { + private void showArea() { // Remove messages to prevent a delayed hide message from undo-ing the show removeAreaDisplayMessages(); mHandler.sendEmptyMessage(MSG_SHOW_AREA_VIEW); } - // Hides the FaceLock area immediately - public void hideArea() { - // Remove messages to prevent a delayed show message from undo-ing the hide - removeAreaDisplayMessages(); - mHandler.sendEmptyMessage(MSG_HIDE_AREA_VIEW); - } - - // Shows the FaceLock area for a period of time - public void showAreaWithTimeout(long timeoutMillis) { - showArea(); - mHandler.sendEmptyMessageDelayed(MSG_HIDE_AREA_VIEW, timeoutMillis); - } - // Binds to FaceLock service. This call does not tell it to start, but it causes the service // to call the onServiceConnected callback, which then starts FaceLock. - public void bind() { + private void bind() { if (installedAndSelected()) { if (!mBoundToService) { if (DEBUG) Log.d(TAG, "before bind to FaceLock service"); @@ -223,32 +246,6 @@ public class FaceUnlock implements Handler.Callback { } } - // Tells FaceLock to stop and then unbinds from the FaceLock service - public void stopAndUnbind() { - if (installedAndSelected()) { - stop(); - - if (mBoundToService) { - if (DEBUG) Log.d(TAG, "before unbind from FaceLock service"); - if (mService != null) { - try { - mService.unregisterCallback(mFaceLockCallback); - } catch (RemoteException e) { - // Not much we can do - } - } - mContext.unbindService(mConnection); - if (DEBUG) Log.d(TAG, "after unbind from FaceLock service"); - 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 FaceLock when not bound"); - } - } - } - private ServiceConnection mConnection = new ServiceConnection() { // Completes connection, registers callback and starts FaceLock when service is bound @Override @@ -268,7 +265,7 @@ public class FaceUnlock implements Handler.Callback { int[] position; position = new int[2]; mAreaView.getLocationInWindow(position); - start(mAreaView.getWindowToken(), position[0], position[1], + startUi(mAreaView.getWindowToken(), position[0], position[1], mAreaView.getWidth(), mAreaView.getHeight()); } } @@ -286,7 +283,7 @@ public class FaceUnlock implements Handler.Callback { }; // Tells the FaceLock service to start displaying its UI and perform recognition - public void start(IBinder windowToken, int x, int y, int w, int h) { + private void startUi(IBinder windowToken, int x, int y, int w, int h) { if (installedAndSelected()) { synchronized (mServiceRunningLock) { if (!mServiceRunning) { @@ -300,14 +297,14 @@ public class FaceUnlock implements Handler.Callback { } mServiceRunning = true; } else { - if (DEBUG) Log.w(TAG, "start() attempted while running"); + if (DEBUG) Log.w(TAG, "startUi() attempted while running"); } } } } // Tells the FaceLock service to stop displaying its UI and stop recognition - public void stop() { + private void stopUi() { if (installedAndSelected()) { // Note that attempting to stop FaceLock when it's not running is not an issue. // FaceLock can return, which stops it and then we try to stop it when the @@ -333,7 +330,7 @@ public class FaceUnlock implements Handler.Callback { public void unlock() { if (DEBUG) Log.d(TAG, "FaceLock unlock()"); showArea(); // Keep fallback covered - stopAndUnbind(); + stop(); mKeyguardScreenCallback.keyguardDone(true); mKeyguardScreenCallback.reportSuccessfulUnlockAttempt(); @@ -344,8 +341,8 @@ public class FaceUnlock implements Handler.Callback { @Override public void cancel() { if (DEBUG) Log.d(TAG, "FaceLock cancel()"); - hideArea(); // Expose fallback - stopAndUnbind(); + hide(); // Expose fallback + stop(); mKeyguardScreenCallback.pokeWakelock(BACKUP_LOCK_TIMEOUT); } @@ -355,8 +352,8 @@ public class FaceUnlock implements Handler.Callback { public void reportFailedAttempt() { if (DEBUG) Log.d(TAG, "FaceLock reportFailedAttempt()"); mUpdateMonitor.reportFailedFaceUnlockAttempt(); - hideArea(); // Expose fallback - stopAndUnbind(); + hide(); // Expose fallback + stop(); mKeyguardScreenCallback.pokeWakelock(BACKUP_LOCK_TIMEOUT); } @@ -364,7 +361,7 @@ public class FaceUnlock implements Handler.Callback { @Override public void exposeFallback() { if (DEBUG) Log.d(TAG, "FaceLock exposeFallback()"); - hideArea(); // Expose fallback + hide(); // Expose fallback } // Allows the Face Unlock service to poke the wake lock to keep the lockscreen alive diff --git a/policy/src/com/android/internal/policy/impl/LockPatternKeyguardView.java b/policy/src/com/android/internal/policy/impl/LockPatternKeyguardView.java index 39ede89..c382646 100644 --- a/policy/src/com/android/internal/policy/impl/LockPatternKeyguardView.java +++ b/policy/src/com/android/internal/policy/impl/LockPatternKeyguardView.java @@ -101,15 +101,17 @@ public class LockPatternKeyguardView extends KeyguardViewBase { private boolean mShowLockBeforeUnlock = false; - // The following were added to support FaceLock - private FaceUnlock mFaceUnlock; - private final Object mFaceLockStartupLock = new Object(); + // Interface to a biometric sensor that can optionally be used to unlock the device + private BiometricSensorUnlock mBiometricUnlock; + private final Object mBiometricUnlockStartupLock = new Object(); + // Long enough to stay visible while dialer comes up + // Short enough to not be visible if the user goes back immediately + private final int BIOMETRIC_AREA_EMERGENCY_DIALER_TIMEOUT = 1000; private boolean mRequiresSim; - //True if we have some sort of overlay on top of the Lockscreen - //Also true if we've activated a phone call, either emergency dialing or incoming - //This resets when the phone is turned off with no current call - private boolean mHasOverlay; + // True if the biometric unlock should not be displayed. For example, if there is an overlay on + // lockscreen or the user is plugging in / unplugging the device. + private boolean mSupressBiometricUnlock; //True if a dialog is currently displaying on top of this window //Unlike other overlays, this does not close with a power button cycle private boolean mHasDialog = false; @@ -308,15 +310,15 @@ public class LockPatternKeyguardView extends KeyguardViewBase { } public void takeEmergencyCallAction() { - mHasOverlay = true; + mSupressBiometricUnlock = true; - // Continue showing FaceLock area until dialer comes up or call is resumed - if (mFaceUnlock.installedAndSelected() && mFaceUnlock.isServiceRunning()) { - mFaceUnlock.showAreaWithTimeout(mFaceUnlock.viewAreaEmergencyDialerTimeout()); + if (mBiometricUnlock.installedAndSelected() && mBiometricUnlock.isRunning()) { + // Continue covering backup lock until dialer comes up or call is resumed + mBiometricUnlock.show(BIOMETRIC_AREA_EMERGENCY_DIALER_TIMEOUT); } - // FaceLock must be stopped if it is running when emergency call is pressed - mFaceUnlock.stopAndUnbind(); + // The biometric unlock must be stopped if it is running when emergency call is pressed + mBiometricUnlock.stop(); pokeWakelock(EMERGENCY_CALL_TIMEOUT); if (TelephonyManager.getDefault().getCallState() @@ -421,7 +423,7 @@ public class LockPatternKeyguardView extends KeyguardViewBase { LockPatternUtils lockPatternUtils, KeyguardWindowController controller) { super(context, callback); - mFaceUnlock = new FaceUnlock(context, updateMonitor, lockPatternUtils, + mBiometricUnlock = new FaceUnlock(context, updateMonitor, lockPatternUtils, mKeyguardScreenCallback); mConfiguration = context.getResources().getConfiguration(); mEnableFallback = false; @@ -429,7 +431,7 @@ public class LockPatternKeyguardView extends KeyguardViewBase { mUpdateMonitor = updateMonitor; mLockPatternUtils = lockPatternUtils; mWindowController = controller; - mHasOverlay = false; + mSupressBiometricUnlock = false; mPluggedIn = mUpdateMonitor.isDevicePluggedIn(); mScreenOn = ((PowerManager)context.getSystemService(Context.POWER_SERVICE)).isScreenOn(); @@ -528,8 +530,8 @@ public class LockPatternKeyguardView extends KeyguardViewBase { if (DEBUG) Log.d(TAG, "screen off"); mScreenOn = false; mForgotPattern = false; - mHasOverlay = mUpdateMonitor.getPhoneState() != TelephonyManager.CALL_STATE_IDLE || - mHasDialog; + mSupressBiometricUnlock = + mUpdateMonitor.getPhoneState() != TelephonyManager.CALL_STATE_IDLE || mHasDialog; // Emulate activity life-cycle for both lock and unlock screen. if (mLockScreen != null) { @@ -541,25 +543,25 @@ public class LockPatternKeyguardView extends KeyguardViewBase { saveWidgetState(); - // When screen is turned off, need to unbind from FaceLock service if using FaceLock - mFaceUnlock.stopAndUnbind(); + // The biometric unlock must stop when screen turns off. + mBiometricUnlock.stop(); } @Override public void onScreenTurnedOn() { if (DEBUG) Log.d(TAG, "screen on"); - boolean runFaceLock = false; - //Make sure to start facelock iff the screen is both on and focused - synchronized(mFaceLockStartupLock) { + boolean startBiometricUnlock = false; + // Start the biometric unlock if and only if the screen is both on and focused + synchronized(mBiometricUnlockStartupLock) { mScreenOn = true; - runFaceLock = mWindowFocused; + startBiometricUnlock = mWindowFocused; } show(); restoreWidgetState(); - if (runFaceLock) mFaceUnlock.activateIfAble(mHasOverlay); + if (startBiometricUnlock) mBiometricUnlock.start(mSupressBiometricUnlock); } private void saveWidgetState() { @@ -578,25 +580,26 @@ public class LockPatternKeyguardView extends KeyguardViewBase { } } - /** Unbind from facelock if something covers this window (such as an alarm) - * bind to facelock if the lockscreen window just came into focus, and the screen is on + /** + * Stop the biometric unlock if something covers this window (such as an alarm) + * Start the biometric unlock if the lockscreen window just came into focus and the screen is on */ @Override public void onWindowFocusChanged (boolean hasWindowFocus) { if (DEBUG) Log.d(TAG, hasWindowFocus ? "focused" : "unfocused"); - boolean runFaceLock = false; - //Make sure to start facelock iff the screen is both on and focused - synchronized(mFaceLockStartupLock) { - if(mScreenOn && !mWindowFocused) runFaceLock = hasWindowFocus; + boolean startBiometricUnlock = false; + // Start the biometric unlock if and only if the screen is both on and focused + synchronized(mBiometricUnlockStartupLock) { + if (mScreenOn && !mWindowFocused) startBiometricUnlock = hasWindowFocus; mWindowFocused = hasWindowFocus; } if (!hasWindowFocus) { - mHasOverlay = true; - mFaceUnlock.stopAndUnbind(); - mFaceUnlock.hideArea(); + mSupressBiometricUnlock = true; + mBiometricUnlock.stop(); + mBiometricUnlock.hide(); } else { mHasDialog = false; - if (runFaceLock) mFaceUnlock.activateIfAble(mHasOverlay); + if (startBiometricUnlock) mBiometricUnlock.start(mSupressBiometricUnlock); } } @@ -610,14 +613,14 @@ public class LockPatternKeyguardView extends KeyguardViewBase { ((KeyguardScreen) mUnlockScreen).onResume(); } - if (mFaceUnlock.installedAndSelected() && !mHasOverlay) { + if (mBiometricUnlock.installedAndSelected() && !mSupressBiometricUnlock) { // Note that show() gets called before the screen turns off to set it up for next time - // it is turned on. We don't want to set a timeout on the FaceLock area here because it - // may be gone by the time the screen is turned on again. We set the timeout when the - // screen turns on instead. - mFaceUnlock.showArea(); + // it is turned on. We don't want to set a timeout on the biometric unlock here because + // it may be gone by the time the screen is turned on again. We set the timeout when + // the screen turns on instead. + mBiometricUnlock.show(0); } else { - mFaceUnlock.hideArea(); + mBiometricUnlock.hide(); } } @@ -651,9 +654,9 @@ public class LockPatternKeyguardView extends KeyguardViewBase { removeCallbacks(mRecreateRunnable); - // When view is hidden, need to unbind from FaceLock service if we are using FaceLock + // When view is hidden, we need to stop the biometric unlock // e.g., when device becomes unlocked - mFaceUnlock.stopAndUnbind(); + mBiometricUnlock.stop(); super.onDetachedFromWindow(); } @@ -670,16 +673,19 @@ public class LockPatternKeyguardView extends KeyguardViewBase { InfoCallbackImpl mInfoCallback = new InfoCallbackImpl() { - /** When somebody plugs in or unplugs the device, we don't want to display faceunlock */ + /** + * When somebody plugs in or unplugs the device, we don't want to display the biometric + * unlock. + */ @Override public void onRefreshBatteryInfo(boolean showBatteryInfo, boolean pluggedIn, int batteryLevel) { - mHasOverlay |= mPluggedIn != pluggedIn; + mSupressBiometricUnlock |= mPluggedIn != pluggedIn; mPluggedIn = pluggedIn; - //If it's already running, don't close it down: the unplug didn't start it - if (!mFaceUnlock.isServiceRunning()) { - mFaceUnlock.stopAndUnbind(); - mFaceUnlock.hideArea(); + // If it's already running, don't close it down: the unplug didn't start it + if (!mBiometricUnlock.isRunning()) { + mBiometricUnlock.stop(); + mBiometricUnlock.hide(); } } @@ -690,20 +696,20 @@ public class LockPatternKeyguardView extends KeyguardViewBase { | (mUpdateMonitor.isClockVisible() ? View.STATUS_BAR_DISABLE_CLOCK : 0)); } - //We need to stop faceunlock when a phonecall comes in + // We need to stop the biometric unlock when a phone call comes in @Override public void onPhoneStateChanged(int phoneState) { if (DEBUG) Log.d(TAG, "phone state: " + phoneState); if(phoneState == TelephonyManager.CALL_STATE_RINGING) { - mHasOverlay = true; - mFaceUnlock.stopAndUnbind(); - mFaceUnlock.hideArea(); + mSupressBiometricUnlock = true; + mBiometricUnlock.stop(); + mBiometricUnlock.hide(); } } @Override public void onUserChanged(int userId) { - mFaceUnlock.stopAndUnbind(); + mBiometricUnlock.stop(); mLockPatternUtils.setCurrentUser(userId); updateScreen(getInitialMode(), true); } @@ -766,7 +772,7 @@ public class LockPatternKeyguardView extends KeyguardViewBase { mUnlockScreen = null; } mUpdateMonitor.removeCallback(this); - mFaceUnlock.cleanUp(); + mBiometricUnlock.cleanUp(); } private boolean isSecure() { @@ -816,10 +822,10 @@ public class LockPatternKeyguardView extends KeyguardViewBase { final UnlockMode unlockMode = getUnlockMode(); if (mode == Mode.UnlockScreen && unlockMode != UnlockMode.Unknown) { if (force || mUnlockScreen == null || unlockMode != mUnlockScreenMode) { - boolean restartFaceLock = mFaceUnlock.stopIfRunning(); + boolean restartBiometricUnlock = mBiometricUnlock.stop(); recreateUnlockScreen(unlockMode); - if (restartFaceLock) { - mFaceUnlock.activateIfAble(mHasOverlay); + if (restartBiometricUnlock) { + mBiometricUnlock.start(mSupressBiometricUnlock); } } } @@ -933,7 +939,8 @@ public class LockPatternKeyguardView extends KeyguardViewBase { throw new IllegalArgumentException("unknown unlock mode " + unlockMode); } initializeTransportControlView(unlockView); - mFaceUnlock.initializeAreaView(unlockView); // Only shows view if FaceLock is enabled + // Only shows view if the biometric unlock is enabled + mBiometricUnlock.initializeAreaView(unlockView); mUnlockScreenMode = unlockMode; return unlockView; |
