diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2008-10-21 07:00:00 -0700 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2008-10-21 07:00:00 -0700 |
commit | 58096b60fc111564c663d685d3b147ea4a5f3832 (patch) | |
tree | aae45bf0df3fc5b9d22622f42ba0d90c363ec7e9 | |
download | base-release-1.0.tar.gz |
Initial Contributionandroid-1.0release-1.0
25 files changed, 9054 insertions, 0 deletions
diff --git a/phone/Android.mk b/phone/Android.mk new file mode 100644 index 0000000..462431f --- /dev/null +++ b/phone/Android.mk @@ -0,0 +1,12 @@ +LOCAL_PATH:= $(call my-dir) + +# the library +# ============================================================ +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := \ + $(call all-subdir-java-files) + +LOCAL_MODULE:= android.policy + +include $(BUILD_JAVA_LIBRARY) diff --git a/phone/com/android/internal/policy/impl/AccountUnlockScreen.java b/phone/com/android/internal/policy/impl/AccountUnlockScreen.java new file mode 100644 index 0000000..98cb548 --- /dev/null +++ b/phone/com/android/internal/policy/impl/AccountUnlockScreen.java @@ -0,0 +1,190 @@ +/* + * 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; + +import com.android.internal.R; +import com.android.internal.widget.LockPatternUtils; + +import android.accounts.AccountsServiceConstants; +import android.accounts.IAccountsService; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.graphics.Rect; +import android.os.IBinder; +import android.os.RemoteException; +import android.text.InputFilter; +import android.text.LoginFilter; +import android.util.Log; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.RelativeLayout; +import android.widget.TextView; + +/** + * 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). + * + * <p>This class is useful only on platforms that support the + * IAccountsService. + */ +public class AccountUnlockScreen extends RelativeLayout implements KeyguardScreen, + View.OnClickListener, ServiceConnection { + + + private static final String LOCK_PATTERN_PACKAGE = "com.android.settings"; + private static final String LOCK_PATTERN_CLASS = + "com.android.settings.ChooseLockPattern"; + + private final KeyguardScreenCallback mCallback; + private final LockPatternUtils mLockPatternUtils; + private IAccountsService mAccountsService; + + private TextView mTopHeader; + private TextView mInstructions; + private EditText mLogin; + private EditText mPassword; + private Button mOk; + private Button mEmergencyCall; + + /** + * AccountUnlockScreen constructor. + * + * @throws IllegalStateException if the IAccountsService is not + * available on the current platform. + */ + public AccountUnlockScreen(Context context, + KeyguardScreenCallback callback, + LockPatternUtils lockPatternUtils) { + super(context); + mCallback = callback; + mLockPatternUtils = lockPatternUtils; + + LayoutInflater.from(context).inflate( + R.layout.keyguard_screen_glogin_unlock, this, true); + + mTopHeader = (TextView) findViewById(R.id.topHeader); + + mInstructions = (TextView) findViewById(R.id.instructions); + + mLogin = (EditText) findViewById(R.id.login); + mLogin.setFilters(new InputFilter[] { new LoginFilter.UsernameFilterGeneric() } ); + + mPassword = (EditText) findViewById(R.id.password); + + mOk = (Button) findViewById(R.id.ok); + mOk.setOnClickListener(this); + + mEmergencyCall = (Button) findViewById(R.id.emergencyCall); + mEmergencyCall.setOnClickListener(this); + + Log.v("AccountUnlockScreen", "debug: Connecting to accounts service"); + final boolean connected = mContext.bindService(AccountsServiceConstants.SERVICE_INTENT, + this, Context.BIND_AUTO_CREATE); + if (!connected) { + Log.v("AccountUnlockScreen", "debug: Couldn't connect to accounts service"); + throw new IllegalStateException("couldn't bind to accounts service"); + } + } + + @Override + protected boolean onRequestFocusInDescendants(int direction, + Rect previouslyFocusedRect) { + // send focus to the login field + return mLogin.requestFocus(direction, previouslyFocusedRect); + } + + /** {@inheritDoc} */ + public void onPause() { + + } + + /** {@inheritDoc} */ + public void onResume() { + // start fresh + mLogin.setText(""); + mPassword.setText(""); + mLogin.requestFocus(); + } + + /** {@inheritDoc} */ + public void cleanUp() { + mContext.unbindService(this); + } + + /** {@inheritDoc} */ + public void onClick(View v) { + if (v == mOk) { + if (checkPassword()) { + // clear out forgotten password + mLockPatternUtils.setPermanentlyLocked(false); + + // 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.startActivity(intent); + + // close the keyguard + mCallback.keyguardDone(true); + } else { + mInstructions.setText(R.string.lockscreen_glogin_invalid_input); + mPassword.setText(""); + } + } + + if (v == mEmergencyCall) { + mCallback.takeEmergencyCallAction(); + } + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (event.getAction() == KeyEvent.ACTION_DOWN + && event.getKeyCode() == KeyEvent.KEYCODE_BACK) { + mCallback.goToLockScreen(); + return true; + } + return super.dispatchKeyEvent(event); + } + + private boolean checkPassword() { + final String login = mLogin.getText().toString(); + final String password = mPassword.getText().toString(); + try { + return mAccountsService.shouldUnlock(login, password); + } catch (RemoteException e) { + return false; + } + } + + /** {@inheritDoc} */ + public void onServiceConnected(ComponentName name, IBinder service) { + Log.v("AccountUnlockScreen", "debug: About to grab as interface"); + mAccountsService = IAccountsService.Stub.asInterface(service); + } + + /** {@inheritDoc} */ + public void onServiceDisconnected(ComponentName name) { + mAccountsService = null; + } +} diff --git a/phone/com/android/internal/policy/impl/GlobalActions.java b/phone/com/android/internal/policy/impl/GlobalActions.java new file mode 100644 index 0000000..7f8e57b --- /dev/null +++ b/phone/com/android/internal/policy/impl/GlobalActions.java @@ -0,0 +1,414 @@ +/* + * 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; + +import com.android.internal.R; +import com.google.android.collect.Lists; + +import android.app.AlertDialog; +import android.app.StatusBarManager; +import android.content.Context; +import android.content.BroadcastReceiver; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.DialogInterface; +import android.media.AudioManager; +import android.os.LocalPowerManager; +import android.os.Handler; +import android.os.Message; +import android.os.SystemClock; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.BaseAdapter; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import java.util.ArrayList; + +/** + * Helper to show the global actions dialog. Each item is an {@link Action} that + * may show depending on whether the keyguard is showing, and whether the device + * is provisioned. + */ +class GlobalActions implements DialogInterface.OnDismissListener, DialogInterface.OnClickListener { + + private StatusBarManager mStatusBar; + + private final Context mContext; + private final LocalPowerManager mPowerManager; + private final AudioManager mAudioManager; + private ArrayList<Action> mItems; + private AlertDialog mDialog; + + private ToggleAction mSilentModeToggle; + + private MyAdapter mAdapter; + + private boolean mKeyguardShowing = false; + private boolean mDeviceProvisioned = false; + + /** + * @param context everything needs a context :) + * @param powerManager used to turn the screen off (the lock action). + */ + public GlobalActions(Context context, LocalPowerManager powerManager) { + mContext = context; + mPowerManager = powerManager; + mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); + + // receive broadcasts + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); + context.registerReceiver(mBroadcastReceiver, filter); + } + + /** + * Show the global actions dialog (creating if necessary) + * @param keyguardShowing True if keyguard is showing + */ + public void showDialog(boolean keyguardShowing, boolean isDeviceProvisioned) { + mKeyguardShowing = keyguardShowing; + mDeviceProvisioned = isDeviceProvisioned; + if (mDialog == null) { + mStatusBar = (StatusBarManager)mContext.getSystemService(Context.STATUS_BAR_SERVICE); + mDialog = createDialog(); + } + prepareDialog(); + + mStatusBar.disable(StatusBarManager.DISABLE_EXPAND); + mDialog.show(); + } + + /** + * Create the global actions dialog. + * @return A new dialog. + */ + private AlertDialog createDialog() { + + mSilentModeToggle = new ToggleAction( + R.drawable.ic_lock_silent_mode, + R.drawable.ic_lock_silent_mode_off, + R.string.global_action_toggle_silent_mode, + R.string.global_action_silent_mode_on_status, + R.string.global_action_silent_mode_off_status) { + + void onToggle(boolean on) { + mAudioManager.setRingerMode(on ? AudioManager.RINGER_MODE_SILENT + : AudioManager.RINGER_MODE_NORMAL); + } + + public boolean showDuringKeyguard() { + return true; + } + + public boolean showBeforeProvisioning() { + return false; + } + }; + + mItems = Lists.newArrayList( + /* Disabled pending bug 1304831 -- key or touch events wake up device before it + * can go to sleep. + // first: lock screen + new SinglePressAction(com.android.internal.R.drawable.ic_lock_lock, R.string.global_action_lock) { + + public void onPress() { + mPowerManager.goToSleep(SystemClock.uptimeMillis() + 1); + } + + public boolean showDuringKeyguard() { + return false; + } + + public boolean showBeforeProvisioning() { + return false; + } + }, + */ + // next: silent mode + mSilentModeToggle, + // last: power off + new SinglePressAction(com.android.internal.R.drawable.ic_lock_power_off, R.string.global_action_power_off) { + + public void onPress() { + // shutdown by making sure radio and power are handled accordingly. + ShutdownThread.shutdownAfterDisablingRadio(mContext, true); + } + + public boolean showDuringKeyguard() { + return true; + } + + public boolean showBeforeProvisioning() { + return true; + } + }); + + mAdapter = new MyAdapter(); + + final AlertDialog.Builder ab = new AlertDialog.Builder(mContext); + + ab.setAdapter(mAdapter, this) + .setInverseBackgroundForced(true) + .setTitle(R.string.global_actions); + + final AlertDialog dialog = ab.create(); + dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG); + dialog.getWindow().setFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND, + WindowManager.LayoutParams.FLAG_BLUR_BEHIND); + + dialog.setOnDismissListener(this); + + return dialog; + } + + private void prepareDialog() { + // TODO: May need another 'Vibrate' toggle button, but for now treat them the same + final boolean silentModeOn = + mAudioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL; + mSilentModeToggle.updateState(silentModeOn); + mAdapter.notifyDataSetChanged(); + if (mKeyguardShowing) { + mDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); + } else { + mDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG); + } + } + + /** {@inheritDoc} */ + public void onDismiss(DialogInterface dialog) { + mStatusBar.disable(StatusBarManager.DISABLE_NONE); + } + + /** {@inheritDoc} */ + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + mAdapter.getItem(which).onPress(); + } + + + /** + * The adapter used for the list within the global actions dialog, taking + * into account whether the keyguard is showing via + * {@link GlobalActions#mKeyguardShowing} and whether the device is provisioned + * via {@link GlobalActions#mDeviceProvisioned}. + */ + private class MyAdapter extends BaseAdapter { + + public int getCount() { + int count = 0; + + for (int i = 0; i < mItems.size(); i++) { + final Action action = mItems.get(i); + + if (mKeyguardShowing && !action.showDuringKeyguard()) { + continue; + } + if (!mDeviceProvisioned && !action.showBeforeProvisioning()) { + continue; + } + count++; + } + return count; + } + + public Action getItem(int position) { + + int filteredPos = 0; + for (int i = 0; i < mItems.size(); i++) { + final Action action = mItems.get(i); + if (mKeyguardShowing && !action.showDuringKeyguard()) { + continue; + } + if (!mDeviceProvisioned && !action.showBeforeProvisioning()) { + continue; + } + if (filteredPos == position) { + return action; + } + filteredPos++; + } + + throw new IllegalArgumentException("position " + position + " out of " + + "range of showable actions, filtered count = " + + "= " + getCount() + ", keyguardshowing=" + mKeyguardShowing + + ", provisioned=" + mDeviceProvisioned); + } + + + public long getItemId(int position) { + return position; + } + + public View getView(int position, View convertView, ViewGroup parent) { + Action action = getItem(position); + return action.create(mContext, (LinearLayout) convertView, LayoutInflater.from(mContext)); + } + } + + // note: the scheme below made more sense when we were planning on having + // 8 different things in the global actions dialog. seems overkill with + // only 3 items now, but may as well keep this flexible approach so it will + // be easy should someone decide at the last minute to include something + // else, such as 'enable wifi', or 'enable bluetooth' + + /** + * What each item in the global actions dialog must be able to support. + */ + private interface Action { + LinearLayout create(Context context, LinearLayout convertView, LayoutInflater inflater); + + void onPress(); + + /** + * @return whether this action should appear in the dialog when the keygaurd + * is showing. + */ + boolean showDuringKeyguard(); + + /** + * @return whether this action should appear in the dialog before the + * device is provisioned. + */ + boolean showBeforeProvisioning(); + } + + /** + * A single press action maintains no state, just responds to a press + * and takes an action. + */ + private static abstract class SinglePressAction implements Action { + private final int mIconResId; + private final int mMessageResId; + + protected SinglePressAction(int iconResId, int messageResId) { + mIconResId = iconResId; + mMessageResId = messageResId; + } + + abstract public void onPress(); + + public LinearLayout create(Context context, LinearLayout convertView, LayoutInflater inflater) { + LinearLayout v = (LinearLayout) ((convertView != null) ? + convertView : + inflater.inflate(R.layout.global_actions_item, null)); + + ImageView icon = (ImageView) v.findViewById(R.id.icon); + TextView messageView = (TextView) v.findViewById(R.id.message); + + v.findViewById(R.id.status).setVisibility(View.GONE); + + icon.setImageDrawable(context.getResources().getDrawable(mIconResId)); + messageView.setText(mMessageResId); + + return v; + } + } + + /** + * A toggle action knows whether it is on or off, and displays an icon + * and status message accordingly. + */ + static abstract class ToggleAction implements Action { + + private boolean mOn = false; + + // prefs + private final int mEnabledIconResId; + private final int mDisabledIconResid; + private final int mMessageResId; + private final int mEnabledStatusMessageResId; + private final int mDisabledStatusMessageResId; + + /** + * @param enabledIconResId The icon for when this action is on. + * @param disabledIconResid The icon for when this action is off. + * @param essage The general information message, e.g 'Silent Mode' + * @param enabledStatusMessageResId The on status message, e.g 'sound disabled' + * @param disabledStatusMessageResId The off status message, e.g. 'sound enabled' + */ + public ToggleAction(int enabledIconResId, + int disabledIconResid, + int essage, + int enabledStatusMessageResId, + int disabledStatusMessageResId) { + mEnabledIconResId = enabledIconResId; + mDisabledIconResid = disabledIconResid; + mMessageResId = essage; + mEnabledStatusMessageResId = enabledStatusMessageResId; + mDisabledStatusMessageResId = disabledStatusMessageResId; + } + + public LinearLayout create(Context context, LinearLayout convertView, + LayoutInflater inflater) { + LinearLayout v = (LinearLayout) ((convertView != null) ? + convertView : + inflater.inflate(R + .layout.global_actions_item, null)); + + ImageView icon = (ImageView) v.findViewById(R.id.icon); + TextView messageView = (TextView) v.findViewById(R.id.message); + TextView statusView = (TextView) v.findViewById(R.id.status); + + messageView.setText(mMessageResId); + + icon.setImageDrawable(context.getResources().getDrawable( + (mOn ? mEnabledIconResId : mDisabledIconResid))); + statusView.setText(mOn ? mEnabledStatusMessageResId : mDisabledStatusMessageResId); + statusView.setVisibility(View.VISIBLE); + + return v; + } + + public void onPress() { + updateState(!mOn); + onToggle(mOn); + } + + abstract void onToggle(boolean on); + + public void updateState(boolean on) { + mOn = on; + } + } + + private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action)) { + String reason = intent.getStringExtra(PhoneWindowManager.SYSTEM_DIALOG_REASON_KEY); + if (! PhoneWindowManager.SYSTEM_DIALOG_REASON_GLOBAL_ACTIONS.equals(reason)) { + mHandler.sendEmptyMessage(MESSAGE_DISMISS); + } + } + } + }; + + private static final int MESSAGE_DISMISS = 0; + private Handler mHandler = new Handler() { + public void handleMessage(Message msg) { + if (msg.what == MESSAGE_DISMISS) { + if (mDialog != null) { + mDialog.dismiss(); + } + } + } + }; +} diff --git a/phone/com/android/internal/policy/impl/KeyguardScreen.java b/phone/com/android/internal/policy/impl/KeyguardScreen.java new file mode 100644 index 0000000..739664b --- /dev/null +++ b/phone/com/android/internal/policy/impl/KeyguardScreen.java @@ -0,0 +1,40 @@ +/* + * 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; + +/** + * Common interface of each {@link android.view.View} that is a screen of + * {@link LockPatternKeyguardView}. + */ +public interface KeyguardScreen { + + /** + * This screen is no longer in front of the user. + */ + void onPause(); + + /** + * This screen is going to be in front of the user. + */ + void onResume(); + + /** + * This view is going away; a hook to do cleanup. + */ + void cleanUp(); + +} diff --git a/phone/com/android/internal/policy/impl/KeyguardScreenCallback.java b/phone/com/android/internal/policy/impl/KeyguardScreenCallback.java new file mode 100644 index 0000000..832288b --- /dev/null +++ b/phone/com/android/internal/policy/impl/KeyguardScreenCallback.java @@ -0,0 +1,61 @@ +/* + * 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; + +/** + * Within a keyguard, there may be several screens that need a callback + * to the host keyguard view. + */ +public interface KeyguardScreenCallback extends KeyguardViewCallback { + + /** + * Transition to the lock screen. + */ + void goToLockScreen(); + + /** + * Transitino to th unlock screen. + */ + void goToUnlockScreen(); + + /** + * @return Whether the keyguard requires some sort of PIN. + */ + boolean isSecure(); + + /** + * @return Whether we are in a mode where we only want to verify the + * user can get past the keyguard. + */ + boolean isVerifyUnlockOnly(); + + /** + * Stay on me, but recreate me (so I can use a different layout). + */ + void recreateMe(); + + /** + * Take action to send an emergency call. + */ + void takeEmergencyCallAction(); + + /** + * Report that the user had a failed attempt unlocking via the pattern. + */ + void reportFailedPatternAttempt(); + +} diff --git a/phone/com/android/internal/policy/impl/KeyguardUpdateMonitor.java b/phone/com/android/internal/policy/impl/KeyguardUpdateMonitor.java new file mode 100644 index 0000000..ab7c744 --- /dev/null +++ b/phone/com/android/internal/policy/impl/KeyguardUpdateMonitor.java @@ -0,0 +1,552 @@ +/* + * 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; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Configuration; +import android.database.ContentObserver; +import static android.os.BatteryManager.BATTERY_STATUS_CHARGING; +import static android.os.BatteryManager.BATTERY_STATUS_FULL; +import static android.os.BatteryManager.BATTERY_STATUS_UNKNOWN; +import android.os.Handler; +import android.os.Message; +import android.provider.Settings; +import android.provider.Telephony; +import static android.provider.Telephony.Intents.EXTRA_PLMN; +import static android.provider.Telephony.Intents.EXTRA_SHOW_PLMN; +import static android.provider.Telephony.Intents.EXTRA_SHOW_SPN; +import static android.provider.Telephony.Intents.EXTRA_SPN; +import static android.provider.Telephony.Intents.SPN_STRINGS_UPDATED_ACTION; +import com.android.internal.telephony.SimCard; +import com.android.internal.telephony.TelephonyIntents; +import android.util.Log; +import com.android.internal.R; +import com.google.android.collect.Lists; + +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 #getFailedAttempts()}, {@link #reportFailedAttempt()} + * and {@link #clearFailedAttempts()}. Maybe we should rename this 'KeyguardContext'... + */ +public class KeyguardUpdateMonitor { + + static private final String TAG = "KeyguardUpdateMonitor"; + static private final boolean DEBUG = false; + + private static final int LOW_BATTERY_THRESHOLD = 20; + + private final Context mContext; + + private SimCard.State mSimState = SimCard.State.READY; + private boolean mInPortrait; + private boolean mKeyboardOpen; + + private boolean mDevicePluggedIn; + + private boolean mDeviceProvisioned; + + private int mBatteryLevel; + + private CharSequence mTelephonyPlmn; + private CharSequence mTelephonySpn; + + private int mFailedAttempts = 0; + + private Handler mHandler; + + private ArrayList<ConfigurationChangeCallback> mConfigurationChangeCallbacks + = Lists.newArrayList(); + private ArrayList<InfoCallback> mInfoCallbacks = Lists.newArrayList(); + private ArrayList<SimStateCallback> mSimStateCallbacks = Lists.newArrayList(); + private ContentObserver mContentObserver; + + + // messages for the handler + private static final int MSG_CONFIGURATION_CHANGED = 300; + 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; + + + /** + * 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 SimCard.State simState; + + private SimArgs(Intent intent) { + if (!TelephonyIntents.ACTION_SIM_STATE_CHANGED.equals(intent.getAction())) { + throw new IllegalArgumentException("only handles intent ACTION_SIM_STATE_CHANGED"); + } + String stateExtra = intent.getStringExtra(SimCard.INTENT_KEY_SIM_STATE); + if (SimCard.INTENT_VALUE_SIM_ABSENT.equals(stateExtra)) { + this.simState = SimCard.State.ABSENT; + } else if (SimCard.INTENT_VALUE_SIM_READY.equals(stateExtra)) { + this.simState = SimCard.State.READY; + } else if (SimCard.INTENT_VALUE_SIM_LOCKED.equals(stateExtra)) { + final String lockedReason = intent + .getStringExtra(SimCard.INTENT_KEY_LOCKED_REASON); + if (SimCard.INTENT_VALUE_LOCKED_ON_PIN.equals(lockedReason)) { + this.simState = SimCard.State.PIN_REQUIRED; + } else if (SimCard.INTENT_VALUE_LOCKED_ON_PUK.equals(lockedReason)) { + this.simState = SimCard.State.PUK_REQUIRED; + } else { + this.simState = SimCard.State.UNKNOWN; + } + } else if (SimCard.INTENT_VALUE_LOCKED_NETWORK.equals(stateExtra)) { + this.simState = SimCard.State.NETWORK_LOCKED; + } else { + this.simState = SimCard.State.UNKNOWN; + } + } + + public String toString() { + return simState.toString(); + } + } + + public KeyguardUpdateMonitor(Context context) { + mContext = context; + + mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_CONFIGURATION_CHANGED: + handleConfigurationChange(); + break; + case MSG_TIME_UPDATE: + handleTimeUpdate(); + break; + case MSG_BATTERY_UPDATE: + handleBatteryUpdate(msg.arg1, msg.arg2); + break; + case MSG_CARRIER_INFO_UPDATE: + handleCarrierInfoUpdate(); + break; + case MSG_SIM_STATE_CHANGE: + handleSimStateChange((SimArgs) msg.obj); + break; + } + } + }; + + mDeviceProvisioned = Settings.System.getInt( + mContext.getContentResolver(), Settings.System.DEVICE_PROVISIONED, 0) != 0; + + // Since device can't be un-provisioned, we only need to register a content observer + // to update mDeviceProvisioned when we are... + if (!mDeviceProvisioned) { + mContentObserver = new ContentObserver(mHandler) { + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + mDeviceProvisioned = Settings.System.getInt(mContext.getContentResolver(), + Settings.System.DEVICE_PROVISIONED, 0) != 0; + if (mDeviceProvisioned && mContentObserver != null) { + // We don't need the observer anymore... + mContext.getContentResolver().unregisterContentObserver(mContentObserver); + mContentObserver = null; + } + if (DEBUG) Log.d(TAG, "DEVICE_PROVISIONED state = " + mDeviceProvisioned); + } + }; + + mContext.getContentResolver().registerContentObserver( + Settings.System.getUriFor(Settings.System.DEVICE_PROVISIONED), + false, mContentObserver); + + // prevent a race condition between where we check the flag and where we register the + // observer by grabbing the value once again... + mDeviceProvisioned = Settings.System.getInt(mContext.getContentResolver(), + Settings.System.DEVICE_PROVISIONED, 0) != 0; + } + + mInPortrait = queryInPortrait(); + mKeyboardOpen = queryKeyboardOpen(); + + // take a guess to start + mSimState = SimCard.State.READY; + mDevicePluggedIn = true; + mBatteryLevel = 100; + + mTelephonyPlmn = getDefaultPlmn(); + + // setup receiver + final IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); + 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(SPN_STRINGS_UPDATED_ACTION); + context.registerReceiver(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_CONFIGURATION_CHANGED.equals(action)) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_CONFIGURATION_CHANGED)); + } else 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 (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 pluggedInStatus = intent + .getIntExtra("status", BATTERY_STATUS_UNKNOWN); + int batteryLevel = intent.getIntExtra("level", 0); + final Message msg = mHandler.obtainMessage( + MSG_BATTERY_UPDATE, + pluggedInStatus, + batteryLevel); + mHandler.sendMessage(msg); + } else if (TelephonyIntents.ACTION_SIM_STATE_CHANGED.equals(action)){ + mHandler.sendMessage(mHandler.obtainMessage( + MSG_SIM_STATE_CHANGE, + new SimArgs(intent))); + } + } + }, filter); + } + + /** + * Handle {@link #MSG_CONFIGURATION_CHANGED} + */ + private void handleConfigurationChange() { + if (DEBUG) Log.d(TAG, "handleConfigurationChange"); + + final boolean inPortrait = queryInPortrait(); + if (mInPortrait != inPortrait) { + mInPortrait = inPortrait; + for (int i = 0; i < mConfigurationChangeCallbacks.size(); i++) { + mConfigurationChangeCallbacks.get(i).onOrientationChange(inPortrait); + } + } + + final boolean keyboardOpen = queryKeyboardOpen(); + if (mKeyboardOpen != keyboardOpen) { + mKeyboardOpen = keyboardOpen; + for (int i = 0; i < mConfigurationChangeCallbacks.size(); i++) { + mConfigurationChangeCallbacks.get(i).onKeyboardChange(keyboardOpen); + } + } + } + + /** + * Handle {@link #MSG_TIME_UPDATE} + */ + private void handleTimeUpdate() { + if (DEBUG) Log.d(TAG, "handleTimeUpdate"); + for (int i = 0; i < mInfoCallbacks.size(); i++) { + mInfoCallbacks.get(i).onTimeChanged(); + } + } + + /** + * Handle {@link #MSG_BATTERY_UPDATE} + */ + private void handleBatteryUpdate(int pluggedInStatus, int batteryLevel) { + if (DEBUG) Log.d(TAG, "handleBatteryUpdate"); + final boolean pluggedIn = isPluggedIn(pluggedInStatus); + + if (isBatteryUpdateInteresting(pluggedIn, batteryLevel)) { + mBatteryLevel = batteryLevel; + mDevicePluggedIn = pluggedIn; + for (int i = 0; i < mInfoCallbacks.size(); i++) { + mInfoCallbacks.get(i).onRefreshBatteryInfo( + shouldShowBatteryInfo(), pluggedIn, batteryLevel); + } + } + + // shut down gracefully if our battery is critically low and we are not powered + if (batteryLevel == 0 && + pluggedInStatus != BATTERY_STATUS_CHARGING && + pluggedInStatus != BATTERY_STATUS_UNKNOWN) { + ShutdownThread.shutdownAfterDisablingRadio(mContext, false); + } + } + + /** + * Handle {@link #MSG_CARRIER_INFO_UPDATE} + */ + private void handleCarrierInfoUpdate() { + if (DEBUG) Log.d(TAG, "handleCarrierInfoUpdate: plmn = " + mTelephonyPlmn + + ", spn = " + mTelephonySpn); + + for (int i = 0; i < mInfoCallbacks.size(); i++) { + mInfoCallbacks.get(i).onRefreshCarrierInfo(mTelephonyPlmn, mTelephonySpn); + } + } + + /** + * Handle {@link #MSG_SIM_STATE_CHANGE} + */ + private void handleSimStateChange(SimArgs simArgs) { + final SimCard.State state = simArgs.simState; + + if (DEBUG) { + Log.d(TAG, "handleSimStateChange: intentValue = " + simArgs + " " + + "state resolved to " + state.toString()); + } + + if (state != SimCard.State.UNKNOWN && state != mSimState) { + mSimState = state; + for (int i = 0; i < mSimStateCallbacks.size(); i++) { + mSimStateCallbacks.get(i).onSimStateChanged(state); + } + } + } + + /** + * @param status One of the statuses of {@link android.os.BatteryManager} + * @return Whether the status maps to a status for being plugged in. + */ + private boolean isPluggedIn(int status) { + return status == BATTERY_STATUS_CHARGING || status == BATTERY_STATUS_FULL; + } + + private boolean isBatteryUpdateInteresting(boolean pluggedIn, int batteryLevel) { + // change in plug is always interesting + if (mDevicePluggedIn != pluggedIn) { + return true; + } + + // change in battery level while plugged in + if (pluggedIn && mBatteryLevel != batteryLevel) { + return true; + } + + if (!pluggedIn) { + // not plugged in and going below threshold + if (batteryLevel < LOW_BATTERY_THRESHOLD + && mBatteryLevel >= LOW_BATTERY_THRESHOLD) { + return true; + } + // not plugged in and going above threshold (sounds impossible, but, meh...) + if (mBatteryLevel < LOW_BATTERY_THRESHOLD + && batteryLevel >= LOW_BATTERY_THRESHOLD) { + return true; + } + } + return false; + } + + /** + * What is the current orientation? + */ + boolean queryInPortrait() { + final Configuration configuration = mContext.getResources().getConfiguration(); + return configuration.orientation == Configuration.ORIENTATION_PORTRAIT; + } + + /** + * Is the keyboard currently open? + */ + boolean queryKeyboardOpen() { + final Configuration configuration = mContext.getResources().getConfiguration(); + + return configuration.keyboardHidden == Configuration.KEYBOARDHIDDEN_NO; + } + + /** + * @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 getTelephonyPlmnFrom(Intent intent) { + if (intent.getBooleanExtra(EXTRA_SHOW_PLMN, false)) { + final String plmn = intent.getStringExtra(EXTRA_PLMN); + if (plmn != null) { + return plmn; + } else { + return 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(EXTRA_SHOW_SPN, false)) { + final String spn = intent.getStringExtra(EXTRA_SPN); + if (spn != null) { + return spn; + } + } + return null; + } + + /** + * Remove the given observer from being registered from any of the kinds + * of callbacks. + * @param observer The observer to remove (an instance of {@link ConfigurationChangeCallback}, + * {@link InfoCallback} or {@link SimStateCallback} + */ + public void removeCallback(Object observer) { + mConfigurationChangeCallbacks.remove(observer); + mInfoCallbacks.remove(observer); + mSimStateCallbacks.remove(observer); + } + + /** + * Callback for configuration changes. + */ + interface ConfigurationChangeCallback { + void onOrientationChange(boolean inPortrait); + + void onKeyboardChange(boolean isKeyboardOpen); + } + + /** + * Callback for general information releveant to lock screen. + */ + interface InfoCallback { + void onRefreshBatteryInfo(boolean showBatteryInfo, boolean pluggedIn, int batteryLevel); + void onTimeChanged(); + + /** + * @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); + } + + /** + * Callback to notify of sim state change. + */ + interface SimStateCallback { + void onSimStateChanged(SimCard.State simState); + } + + /** + * Register to receive notifications about configuration changes. + * @param callback The callback. + */ + public void registerConfigurationChangeCallback(ConfigurationChangeCallback callback) { + mConfigurationChangeCallbacks.add(callback); + } + + /** + * Register to receive notifications about general keyguard information + * (see {@link InfoCallback}. + * @param callback The callback. + */ + public void registerInfoCallback(InfoCallback callback) { + mInfoCallbacks.add(callback); + } + + /** + * Register to be notified of sim state changes. + * @param callback The callback. + */ + public void registerSimStateCallback(SimStateCallback callback) { + mSimStateCallbacks.add(callback); + } + + public SimCard.State getSimState() { + return mSimState; + } + + /** + * Report that the user succesfully entered the sim pin so we + * have the information earlier than waiting for the intent + * broadcast from the telephony code. + */ + public void reportSimPinUnlocked() { + mSimState = SimCard.State.READY; + } + + public boolean isInPortrait() { + return mInPortrait; + } + + public boolean isKeyboardOpen() { + return mKeyboardOpen; + } + + public boolean isDevicePluggedIn() { + return mDevicePluggedIn; + } + + public int getBatteryLevel() { + return mBatteryLevel; + } + + public boolean shouldShowBatteryInfo() { + return mDevicePluggedIn || mBatteryLevel < LOW_BATTERY_THRESHOLD; + } + + 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 getFailedAttempts() { + return mFailedAttempts; + } + + public void clearFailedAttempts() { + mFailedAttempts = 0; + } + + public void reportFailedAttempt() { + mFailedAttempts++; + } +} diff --git a/phone/com/android/internal/policy/impl/KeyguardViewBase.java b/phone/com/android/internal/policy/impl/KeyguardViewBase.java new file mode 100644 index 0000000..491300e --- /dev/null +++ b/phone/com/android/internal/policy/impl/KeyguardViewBase.java @@ -0,0 +1,175 @@ +/* + * 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; + +import android.content.Context; +import android.content.Intent; +import android.media.AudioManager; +import android.view.KeyEvent; +import android.view.View; +import android.widget.FrameLayout; + +/** + * Base class for keyguard views. {@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 KeyguardViewCallback mCallback; + private AudioManager mAudioManager; + + public KeyguardViewBase(Context context) { + super(context); + } + + // used to inject callback + void setCallback(KeyguardViewCallback callback) { + mCallback = callback; + } + + public KeyguardViewCallback getCallback() { + return mCallback; + } + + /** + * 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 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. + */ + 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(); + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (shouldEventKeepScreenOnWhileKeyguardShowing(event)) { + mCallback.pokeWakelock(); + } + + if (interceptMediaKey(event)) { + return true; + } + return super.dispatchKeyEvent(event); + } + + private boolean shouldEventKeepScreenOnWhileKeyguardShowing(KeyEvent event) { + if (event.getAction() != KeyEvent.ACTION_DOWN) { + return false; + } + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_DPAD_DOWN: + case KeyEvent.KEYCODE_DPAD_LEFT: + case KeyEvent.KEYCODE_DPAD_RIGHT: + case KeyEvent.KEYCODE_DPAD_UP: + return false; + default: + return true; + } + } + + /** + * Allows the media keys to work when the keygaurd is showing. + * The media keys should be of no interest to the actualy keygaurd 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_HEADSETHOOK: { + Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON, null); + intent.putExtra(Intent.EXTRA_KEY_EVENT, event); + getContext().sendOrderedBroadcast(intent, null); + return true; + } + + case KeyEvent.KEYCODE_VOLUME_UP: + case KeyEvent.KEYCODE_VOLUME_DOWN: { + synchronized (this) { + if (mAudioManager == null) { + mAudioManager = (AudioManager) getContext().getSystemService( + Context.AUDIO_SERVICE); + } + } + // Volume buttons should only function for music. + if (mAudioManager.isMusicActive()) { + mAudioManager.adjustStreamVolume( + AudioManager.STREAM_MUSIC, + keyCode == KeyEvent.KEYCODE_VOLUME_UP + ? AudioManager.ADJUST_RAISE + : AudioManager.ADJUST_LOWER, + 0); + } + // Don't execute default volume behavior + return true; + } + } + } else if (event.getAction() == KeyEvent.ACTION_UP) { + switch (keyCode) { + case KeyEvent.KEYCODE_HEADSETHOOK: { + Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON, null); + intent.putExtra(Intent.EXTRA_KEY_EVENT, event); + getContext().sendOrderedBroadcast(intent, null); + return true; + } + } + } + return false; + } + +} diff --git a/phone/com/android/internal/policy/impl/KeyguardViewCallback.java b/phone/com/android/internal/policy/impl/KeyguardViewCallback.java new file mode 100644 index 0000000..b376d65 --- /dev/null +++ b/phone/com/android/internal/policy/impl/KeyguardViewCallback.java @@ -0,0 +1,49 @@ +/* + * 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; + +/** + * The callback used by the keyguard view to tell the {@link KeyguardViewMediator} + * various things. + */ +public interface KeyguardViewCallback { + + /** + * Request the wakelock to be poked for the default amount of time. + */ + void pokeWakelock(); + + /** + * Request the wakelock to be poked for a specific amount of time. + * @param millis The amount of time in millis. + */ + void pokeWakelock(int 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(); +} diff --git a/phone/com/android/internal/policy/impl/KeyguardViewManager.java b/phone/com/android/internal/policy/impl/KeyguardViewManager.java new file mode 100644 index 0000000..a48f4f9 --- /dev/null +++ b/phone/com/android/internal/policy/impl/KeyguardViewManager.java @@ -0,0 +1,200 @@ +/* + * 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; + +import com.android.internal.R; + +import android.content.Context; +import android.graphics.PixelFormat; +import android.graphics.Canvas; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewManager; +import android.view.WindowManager; +import android.widget.FrameLayout; + +/** + * Manages creating, showing, hiding and resetting the keyguard. Calls back + * via {@link com.android.internal.policy.impl.KeyguardViewCallback} 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 = false; + private static String TAG = "KeyguardViewManager"; + + private final Context mContext; + private final ViewManager mViewManager; + private final KeyguardViewCallback mCallback; + private final KeyguardViewProperties mKeyguardViewProperties; + + private final KeyguardUpdateMonitor mUpdateMonitor; + + private FrameLayout mKeyguardHost; + private KeyguardViewBase mKeyguardView; + + private boolean mScreenOn = false; + + /** + * @param context Used to create views. + * @param viewManager Keyguard will be attached to this. + * @param callback Used to notify of changes. + */ + public KeyguardViewManager(Context context, ViewManager viewManager, + KeyguardViewCallback callback, KeyguardViewProperties keyguardViewProperties, KeyguardUpdateMonitor updateMonitor) { + mContext = context; + mViewManager = viewManager; + mCallback = callback; + mKeyguardViewProperties = keyguardViewProperties; + + mUpdateMonitor = updateMonitor; + } + + /** + * Helper class to host the keyguard view. + */ + private static class KeyguardViewHost extends FrameLayout { + private final KeyguardViewCallback mCallback; + + private KeyguardViewHost(Context context, KeyguardViewCallback callback) { + super(context); + mCallback = callback; + } + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + mCallback.keyguardDoneDrawing(); + } + } + + /** + * Show the keyguard. Will handle creating and attaching to the view manager + * lazily. + */ + public synchronized void show() { + if (DEBUG) Log.d(TAG, "show()"); + + if (mKeyguardHost == null) { + if (DEBUG) Log.d(TAG, "keyguard host is null, creating it..."); + + mKeyguardHost = new KeyguardViewHost(mContext, mCallback); + + final int stretch = ViewGroup.LayoutParams.FILL_PARENT; + int flags = WindowManager.LayoutParams.FLAG_DITHER + | WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN; + WindowManager.LayoutParams lp = new WindowManager.LayoutParams( + stretch, stretch, WindowManager.LayoutParams.TYPE_KEYGUARD, + flags, PixelFormat.OPAQUE); + lp.setTitle("Keyguard"); + + mViewManager.addView(mKeyguardHost, lp); + } + + if (mKeyguardView == null) { + if (DEBUG) Log.d(TAG, "keyguard view is null, creating it..."); + mKeyguardView = mKeyguardViewProperties.createKeyguardView(mContext, mUpdateMonitor); + mKeyguardView.setId(R.id.lock_screen); + mKeyguardView.setCallback(mCallback); + + final ViewGroup.LayoutParams lp = new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.FILL_PARENT); + + mKeyguardHost.addView(mKeyguardView, lp); + + if (mScreenOn) { + mKeyguardView.onScreenTurnedOn(); + } + } + + mKeyguardHost.setVisibility(View.VISIBLE); + mKeyguardView.requestFocus(); + } + + /** + * Reset the state of the view. + */ + public synchronized void reset() { + if (DEBUG) Log.d(TAG, "reset()"); + if (mKeyguardView != null) { + mKeyguardView.reset(); + } + } + + public synchronized void onScreenTurnedOff() { + if (DEBUG) Log.d(TAG, "onScreenTurnedOff()"); + mScreenOn = false; + if (mKeyguardView != null) { + mKeyguardView.onScreenTurnedOff(); + } + } + + public synchronized void onScreenTurnedOn() { + if (DEBUG) Log.d(TAG, "onScreenTurnedOn()"); + mScreenOn = true; + if (mKeyguardView != null) { + mKeyguardView.onScreenTurnedOn(); + } + } + + public synchronized void verifyUnlock() { + if (DEBUG) Log.d(TAG, "verifyUnlock()"); + show(); + 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. + */ + public void wakeWhenReadyTq(int keyCode) { + if (DEBUG) Log.d(TAG, "wakeWhenReady(" + keyCode + ")"); + if (mKeyguardView != null) { + mKeyguardView.wakeWhenReadyTq(keyCode); + } + } + + /** + * Hides the keyguard view + */ + public synchronized void hide() { + if (DEBUG) Log.d(TAG, "hide()"); + if (mKeyguardHost != null) { + mKeyguardHost.setVisibility(View.INVISIBLE); + if (mKeyguardView != null) { + mKeyguardHost.removeView(mKeyguardView); + mKeyguardView.cleanUp(); + mKeyguardView = null; + } + } + } + + /** + * @return Whether the keyguard is showing + */ + public synchronized boolean isShowing() { + return (mKeyguardHost != null && mKeyguardHost.getVisibility() == View.VISIBLE); + } +} diff --git a/phone/com/android/internal/policy/impl/KeyguardViewMediator.java b/phone/com/android/internal/policy/impl/KeyguardViewMediator.java new file mode 100644 index 0000000..ac816b0 --- /dev/null +++ b/phone/com/android/internal/policy/impl/KeyguardViewMediator.java @@ -0,0 +1,902 @@ +/* + * 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; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.app.StatusBarManager; +import static android.app.StatusBarManager.DISABLE_NONE; +import static android.app.StatusBarManager.DISABLE_EXPAND; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Handler; +import android.os.LocalPowerManager; +import android.os.Message; +import android.os.PowerManager; +import android.os.SystemClock; +import android.util.Config; +import android.util.Log; +import android.util.EventLog; +import android.view.KeyEvent; +import android.view.WindowManagerImpl; +import android.view.WindowManagerPolicy; +import com.android.internal.telephony.SimCard; +import com.android.internal.telephony.TelephonyIntents; +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.KeyInputQueue}'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 implements KeyguardViewCallback, + KeyguardUpdateMonitor.ConfigurationChangeCallback, KeyguardUpdateMonitor.SimStateCallback { + private final static boolean DEBUG = false && Config.LOGD; + private final static boolean DBG_WAKE = DEBUG || true; + + 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 TIMEOUT = 1; + 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; + + /** + * The default amount of time we stay awake (used for all key input) + */ + protected static final int AWAKE_INTERVAL_DEFAULT_MS = 5000; + + + /** + * The default amount of time we stay awake (used for all key input) when + * the keyboard is open + */ + protected static final int AWAKE_INTERVAL_DEFAULT_KEYBOARD_OPEN_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_DELAY_MS = 0; + + /** + * How long we'll wait for the {@link KeyguardViewCallback#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; + + private Context mContext; + private AlarmManager mAlarmManager; + + /** Low level access to the power manager for enableUserActivity. Having this + * requires that we run in the system process. */ + LocalPowerManager mRealPowerManager; + + /** High level access to the power manager for WakeLocks */ + private PowerManager mPM; + + /** + * Used to keep the device awake while the keyguard is showing, i.e for + * calls to {@link #pokeWakelock()} + */ + private PowerManager.WakeLock mWakeLock; + + /** + * 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 #wakeWhenReadyLocked(int) + */ + private PowerManager.WakeLock mWakeAndHandOff; + + /** + * Used to disable / reenable status bar expansion. + */ + private StatusBarManager mStatusBarManager; + + 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; + + /** + * 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; + + private int mWakelockSequence; + + private PhoneWindowManager mCallback; + + /** + * 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 KeyguardViewProperties mKeyguardViewProperties; + + private KeyguardUpdateMonitor mUpdateMonitor; + + private boolean mKeyboardOpen = false; + + /** + * {@link #setKeyguardEnabled} waits on this condition when it reenables + * the keyguard. + */ + private boolean mWaitingUntilKeyguardVisible = false; + + public KeyguardViewMediator(Context context, PhoneWindowManager callback, + LocalPowerManager powerManager) { + mContext = context; + + mRealPowerManager = powerManager; + mPM = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + mWakeLock = mPM.newWakeLock( + PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP, + "keyguard"); + mWakeLock.setReferenceCounted(false); + + mWakeAndHandOff = mPM.newWakeLock( + PowerManager.PARTIAL_WAKE_LOCK, + "keyguardWakeAndHandOff"); + mWakeAndHandOff.setReferenceCounted(false); + + IntentFilter filter = new IntentFilter(); + filter.addAction(DELAYED_KEYGUARD_ACTION); + filter.addAction(TelephonyIntents.ACTION_SIM_STATE_CHANGED); + filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); + context.registerReceiver(mBroadCastReceiver, filter); + mAlarmManager = (AlarmManager) context + .getSystemService(Context.ALARM_SERVICE); + mCallback = callback; + + mUpdateMonitor = new KeyguardUpdateMonitor(context); + + mUpdateMonitor.registerConfigurationChangeCallback(this); + mUpdateMonitor.registerSimStateCallback(this); + + mKeyguardViewProperties = + new LockPatternKeyguardViewProperties( + new LockPatternUtils(mContext.getContentResolver()), + mUpdateMonitor); + + mKeyguardViewManager = new KeyguardViewManager( + context, WindowManagerImpl.getDefault(), this, + mKeyguardViewProperties, mUpdateMonitor); + + } + + /** + * Let us know that the system is ready after startup. + */ + public void onSystemReady() { + synchronized (this) { + if (DEBUG) Log.d(TAG, "onSystemReady"); + doKeyguard(); + } + } + + /** + * Called to let us know the screen was turned off. + * @param why either {@link WindowManagerPolicy#OFF_BECAUSE_OF_USER} or + * {@link WindowManagerPolicy#OFF_BECAUSE_OF_TIMEOUT}. + */ + public void onScreenTurnedOff(int why) { + synchronized (this) { + if (DEBUG) Log.d(TAG, "onScreenTurnedOff(" + why + ")"); + + 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(); + } else if (why == WindowManagerPolicy.OFF_BECAUSE_OF_TIMEOUT) { + // if the screen turned off because of timeout, 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) + long when = SystemClock.elapsedRealtime() + KEYGUARD_DELAY_MS; + 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); + } else { + doKeyguard(); + } + } + } + + /** + * Let's us know the screen was turned on. + */ + public void onScreenTurnedOn() { + synchronized (this) { + mDelayedShowingSequence++; + if (DEBUG) Log.d(TAG, "onScreenTurnedOn, seq = " + mDelayedShowingSequence); + notifyScreenOnLocked(); + } + } + + /** + * 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; + setStatusBarExpandable(false); + hideLocked(); + } else if (enabled && mNeedToReshowWhenReenabled) { + // reenabled after previously hidden, reshow + if (DEBUG) Log.d(TAG, "previously hidden, reshowing, reenabling " + + "status bar expansion"); + mNeedToReshowWhenReenabled = false; + setStatusBarExpandable(true); + + if (mExitSecureCallback != null) { + if (DEBUG) Log.d(TAG, "onKeyguardExitResult(false), resetting"); + mExitSecureCallback.onKeyguardExitResult(false); + mExitSecureCallback = null; + resetStateLocked(); + } else { + showLocked(); + + // 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(); + } + } + } + + + private void setStatusBarExpandable(boolean isExpandable) { + if (mStatusBarManager == null) { + mStatusBarManager = + (StatusBarManager) mContext.getSystemService(Context.STATUS_BAR_SERVICE); + } + mStatusBarManager.disable(isExpandable ? DISABLE_NONE : DISABLE_EXPAND); + } + + /** + * Is the keyguard currently showing? + */ + public boolean isShowing() { + return mShowing; + } + + /** + * 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(); + } + + + /** + * Enable the keyguard if the settings are appropriate. + */ + private void doKeyguard() { + synchronized (this) { + // if another app is disabling us, don't show + if (!mExternallyEnabled) { + if (DEBUG) Log.d(TAG, "doKeyguard: not showing because externally disabled"); + 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 provisioned = mUpdateMonitor.isDeviceProvisioned(); + final SimCard.State state = mUpdateMonitor.getSimState(); + final boolean lockedOrMissing = state.isPinLocked() || (state == SimCard.State.ABSENT); + 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 (DEBUG) Log.d(TAG, "doKeyguard: showing the lock screen"); + showLocked(); + } + } + + /** + * Send message to keyguard telling it to reset its state. + * @see #handleReset() + */ + private void resetStateLocked() { + if (DEBUG) Log.d(TAG, "resetStateLocked"); + Message msg = mHandler.obtainMessage(RESET); + 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() { + if (DEBUG) Log.d(TAG, "notifyScreenOnLocked"); + mHandler.sendEmptyMessage(NOTIFY_SCREEN_ON); + } + + /** + * 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 wakeWhenReadyLocked(int keyCode) { + if (DBG_WAKE) Log.d(TAG, "wakeWhenReadyLocked(" + 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() { + if (DEBUG) Log.d(TAG, "showLocked"); + Message msg = mHandler.obtainMessage(SHOW); + 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); + } + + /** + * {@link KeyguardUpdateMonitor} callbacks. + */ + + /** {@inheritDoc} */ + public void onOrientationChange(boolean inPortrait) { + + } + + /** {@inheritDoc} */ + public void onKeyboardChange(boolean isKeyboardOpen) { + mKeyboardOpen = isKeyboardOpen; + + if (mKeyboardOpen && !mKeyguardViewProperties.isSecure() + && mKeyguardViewManager.isShowing()) { + if (DEBUG) Log.d(TAG, "bypassing keyguard on sliding open of keyboard with non-secure keyguard"); + keyguardDone(true); + } + } + + /** {@inheritDoc} */ + public void onSimStateChanged(SimCard.State simState) { + if (DEBUG) Log.d(TAG, "onSimStateChanged: " + simState); + + switch (simState) { + case ABSENT: + // only force lock screen in case of missing sim if user hasn't + // gone through setup wizard + if (!mUpdateMonitor.isDeviceProvisioned()) { + if (!isShowing()) { + if (DEBUG) Log.d(TAG, "INTENT_VALUE_SIM_ABSENT and keygaurd isn't showing, we need " + + "to show the keyguard since the device isn't provisioned yet."); + doKeyguard(); + } else { + resetStateLocked(); + } + } + break; + case PIN_REQUIRED: + case PUK_REQUIRED: + if (!isShowing()) { + if (DEBUG) Log.d(TAG, "INTENT_VALUE_SIM_LOCKED and keygaurd isn't showing, we need " + + "to show the keyguard so the user can enter their sim pin"); + doKeyguard(); + } else { + resetStateLocked(); + } + + break; + case READY: + if (isShowing()) { + resetStateLocked(); + } + break; + } + } + + private BroadcastReceiver mBroadCastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (action.equals(DELAYED_KEYGUARD_ACTION)) { + + int sequence = intent.getIntExtra("seq", 0); + + if (false) Log.d(TAG, "received DELAYED_KEYGUARD_ACTION with seq = " + + sequence + ", mDelayedShowingSequence = " + mDelayedShowingSequence); + + if (mDelayedShowingSequence == sequence) { + doKeyguard(); + } + } + } + }; + + + /** + * 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 + * @return Whether we poked the wake lock (and turned the screen on) + */ + public boolean onWakeKeyWhenKeyguardShowingTq(int keyCode) { + if (DEBUG) Log.d(TAG, "onWakeKeyWhenKeyguardShowing(" + keyCode + ")"); + + if (isWakeKeyWhenKeyguardShowing(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 + wakeWhenReadyLocked(keyCode); + return true; + } else { + return false; + } + } + + private boolean isWakeKeyWhenKeyguardShowing(int keyCode) { + switch (keyCode) { + case KeyEvent.KEYCODE_VOLUME_UP: + case KeyEvent.KEYCODE_VOLUME_DOWN: + case KeyEvent.KEYCODE_HEADSETHOOK: + case KeyEvent.KEYCODE_CAMERA: + return false; + } + return true; + } + + /** + * Callbacks from {@link KeyguardViewManager}. + */ + + /** {@inheritDoc} */ + public void pokeWakelock() { + pokeWakelock(mKeyboardOpen ? + AWAKE_INTERVAL_DEFAULT_KEYBOARD_OPEN_MS : AWAKE_INTERVAL_DEFAULT_MS); + } + + /** {@inheritDoc} */ + public void pokeWakelock(int holdMs) { + synchronized (this) { + if (DBG_WAKE) Log.d(TAG, "pokeWakelock(" + holdMs + ")"); + mWakeLock.acquire(); + mHandler.removeMessages(TIMEOUT); + mWakelockSequence++; + Message msg = mHandler.obtainMessage(TIMEOUT, mWakelockSequence, 0); + mHandler.sendMessageDelayed(msg, holdMs); + } + } + + /** + * {@inheritDoc} + * + * @see #handleKeyguardDone + */ + public void keyguardDone(boolean authenticated) { + synchronized (this) { + EventLog.writeEvent(70000, 2); + if (DEBUG) Log.d(TAG, "keyguardDone(" + authenticated + ")"); + Message msg = mHandler.obtainMessage(KEYGUARD_DONE); + mHandler.sendMessage(msg); + + if (authenticated) { + mUpdateMonitor.clearFailedAttempts(); + } + + 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; + setStatusBarExpandable(true); + } + } + } + } + + /** + * {@inheritDoc} + * + * @see #handleKeyguardDoneDrawing + */ + public void keyguardDoneDrawing() { + mHandler.sendEmptyMessage(KEYGUARD_DONE_DRAWING); + } + + /** + * 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() { + @Override + public void handleMessage(Message msg) + { + switch (msg.what) + { + case TIMEOUT: + handleTimeout(msg.arg1); + return ; + case SHOW: + handleShow(); + return ; + case HIDE: + handleHide(); + return ; + case RESET: + handleReset(); + return ; + case VERIFY_UNLOCK: + handleVerifyUnlock(); + return; + case NOTIFY_SCREEN_OFF: + handleNotifyScreenOff(); + return; + case NOTIFY_SCREEN_ON: + handleNotifyScreenOn(); + return; + case WAKE_WHEN_READY: + handleWakeWhenReady(msg.arg1); + return; + case KEYGUARD_DONE: + handleKeyguardDone(); + return; + case KEYGUARD_DONE_DRAWING: + handleKeyguardDoneDrawing(); + } + } + }; + + /** + * @see #keyguardDone + * @see #KEYGUARD_DONE + */ + private void handleKeyguardDone() { + if (DEBUG) Log.d(TAG, "handleKeyguardDone"); + handleHide(); + mPM.userActivity(SystemClock.uptimeMillis(), true); + mWakeLock.release(); + } + + /** + * @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); + } + } + } + + /** + * Handles the message sent by {@link #pokeWakelock} + * @param seq used to determine if anything has changed since the message + * was sent. + * @see #TIMEOUT + */ + private void handleTimeout(int seq) { + synchronized (KeyguardViewMediator.this) { + if (DEBUG) Log.d(TAG, "handleTimeout"); + if (seq == mWakelockSequence) { + mWakeLock.release(); + } + } + } + + /** + * Handle message sent by {@link #showLocked}. + * @see #SHOW + */ + private void handleShow() { + synchronized (KeyguardViewMediator.this) { + if (DEBUG) Log.d(TAG, "handleShow"); + // while we're showing, we control the wake state, so ask the power + // manager not to honor request for userActivity. + mRealPowerManager.enableUserActivity(false); + + mCallback.onKeyguardShow(); + mKeyguardViewManager.show(); + mShowing = true; + } + } + + /** + * Handle message sent by {@link #hideLocked()} + * @see #HIDE + */ + private void handleHide() { + synchronized (KeyguardViewMediator.this) { + if (DEBUG) Log.d(TAG, "handleHide"); + // When we go away, tell the poewr manager to honor requests from userActivity. + mRealPowerManager.enableUserActivity(true); + + mKeyguardViewManager.hide(); + mShowing = false; + } + } + + /** + * Handle message sent by {@link #wakeWhenReadyLocked(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 + mKeyguardViewManager.wakeWhenReadyTq(keyCode); + + /** + * Now that the keyguard is ready and has poked the wake lock, we can + * release the handoff wakelock + */ + mWakeAndHandOff.release(); + + if (!mWakeLock.isHeld()) { + Log.w(TAG, "mKeyguardViewManager.wakeWhenReadyTq did not poke wake lock"); + } + } + } + + /** + * Handle message sent by {@link #resetStateLocked()} + * @see #RESET + */ + private void handleReset() { + synchronized (KeyguardViewMediator.this) { + if (DEBUG) Log.d(TAG, "handleReset"); + mKeyguardViewManager.reset(); + } + } + + /** + * Handle message sent by {@link #verifyUnlock} + * @see #RESET + */ + private void handleVerifyUnlock() { + synchronized (KeyguardViewMediator.this) { + if (DEBUG) Log.d(TAG, "handleVerifyUnlock"); + mKeyguardViewManager.verifyUnlock(); + } + } + + /** + * 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() { + synchronized (KeyguardViewMediator.this) { + if (DEBUG) Log.d(TAG, "handleNotifyScreenOn"); + mKeyguardViewManager.onScreenTurnedOn(); + } + } +} diff --git a/phone/com/android/internal/policy/impl/KeyguardViewProperties.java b/phone/com/android/internal/policy/impl/KeyguardViewProperties.java new file mode 100644 index 0000000..a449653 --- /dev/null +++ b/phone/com/android/internal/policy/impl/KeyguardViewProperties.java @@ -0,0 +1,43 @@ +/* + * 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; + +import android.content.Context; + +/** + * Defines operations necessary for showing a keyguard, including how to create + * it, and various properties that are useful to be able to query independant + * of whether the keyguard instance is around or not. + */ +public interface KeyguardViewProperties { + + /** + * Create a keyguard view. + * @param context the context to use when creating the view. + * @param updateMonitor configuration may be based on this. + * @return the view. + */ + KeyguardViewBase createKeyguardView(Context context, KeyguardUpdateMonitor updateMonitor); + + /** + * Would the keyguard be secure right now? + * @return Whether the keyguard is currently secure, meaning it will block + * the user from getting past it until the user enters some sort of PIN. + */ + boolean isSecure(); + +} diff --git a/phone/com/android/internal/policy/impl/LockPatternKeyguardView.java b/phone/com/android/internal/policy/impl/LockPatternKeyguardView.java new file mode 100644 index 0000000..66a4c4e --- /dev/null +++ b/phone/com/android/internal/policy/impl/LockPatternKeyguardView.java @@ -0,0 +1,514 @@ +/* + * 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; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.Intent; +import android.os.SystemProperties; +import com.android.internal.telephony.SimCard; +import android.text.TextUtils; +import android.util.Log; +import android.view.KeyEvent; +import android.view.View; +import android.view.WindowManager; +import com.android.internal.R; +import com.android.internal.widget.LockPatternUtils; + +/** + * The host view for all of the screens of the pattern unlock screen. There are + * two {@link Mode}s of operation, lock and unlock. This will show the appropriate + * screen, and listen for callbacks via {@link com.android.internal.policy.impl.KeyguardScreenCallback + * from the current screen. + * + * This view, in turn, communicates back to {@link com.android.internal.policy.impl.KeyguardViewManager} + * via its {@link com.android.internal.policy.impl.KeyguardViewCallback}, as appropriate. + */ +public class LockPatternKeyguardView extends KeyguardViewBase { + + // intent action for launching emergency dialer activity. + static final String ACTION_EMERGENCY_DIAL = "com.android.phone.EmergencyDialer.DIAL"; + + private static final boolean DEBUG = false; + private static final String TAG = "LockPatternKeyguardView"; + + private final KeyguardUpdateMonitor mUpdateMonitor; + private View mLockScreen; + private View mUnlockScreen; + + private boolean mScreenOn = false; + + /** + * The current {@link KeyguardScreen} will use this to communicate back to us. + */ + KeyguardScreenCallback mKeyguardScreenCallback; + + + private boolean mRequiresSim; + + + /** + * Either a lock screen (an informational keyguard screen), or an unlock + * screen (a means for unlocking the device) is shown at any given time. + */ + enum Mode { + LockScreen, + UnlockScreen + } + + /** + * The different types screens available for {@link Mode#UnlockScreen}. + * @see com.android.internal.policy.impl.LockPatternKeyguardView#getUnlockMode() + */ + enum UnlockMode { + + /** + * Unlock by drawing a pattern. + */ + Pattern, + + /** + * Unlock by entering a sim pin. + */ + SimPin, + + /** + * Unlock by entering an account's login and password. + */ + Account + } + + /** + * The current mode. + */ + private Mode mMode = Mode.LockScreen; + + /** + * Keeps track of what mode the current unlock screen is + */ + private UnlockMode mUnlockScreenMode; + + /** + * If true, it means we are in the process of verifying that the user + * can get past the lock screen per {@link #verifyUnlock()} + */ + private boolean mIsVerifyUnlockOnly = false; + + + /** + * Used to lookup the state of the lock pattern + */ + private final LockPatternUtils mLockPatternUtils; + + + /** + * @return Whether we are stuck on the lock screen because the sim is + * missing. + */ + private boolean stuckOnLockScreenBecauseSimMissing() { + return mRequiresSim + && (!mUpdateMonitor.isDeviceProvisioned()) + && (mUpdateMonitor.getSimState() == SimCard.State.ABSENT); + } + + /** + * @param context Used to inflate, and create views. + * @param updateMonitor Knows the state of the world, and passed along to each + * screen so they can use the knowledge, and also register for callbacks + * on dynamic information. + * @param lockPatternUtils Used to look up state of lock pattern. + */ + public LockPatternKeyguardView( + Context context, + KeyguardUpdateMonitor updateMonitor, + LockPatternUtils lockPatternUtils) { + super(context); + + mRequiresSim = + TextUtils.isEmpty(SystemProperties.get("keyguard.no_require_sim")); + + mUpdateMonitor = updateMonitor; + mLockPatternUtils = lockPatternUtils; + + mMode = getInitialMode(); + mKeyguardScreenCallback = new KeyguardScreenCallback() { + + public void goToLockScreen() { + if (mIsVerifyUnlockOnly) { + // navigating away from unlock screen during verify mode means + // we are done and the user failed to authenticate. + mIsVerifyUnlockOnly = false; + getCallback().keyguardDone(false); + } else { + updateScreen(Mode.LockScreen); + } + } + + public void goToUnlockScreen() { + final SimCard.State simState = mUpdateMonitor.getSimState(); + if (stuckOnLockScreenBecauseSimMissing() + || (simState == SimCard.State.PUK_REQUIRED)){ + // stuck on lock screen when sim missing or puk'd + return; + } + if (!isSecure()) { + getCallback().keyguardDone(true); + } else { + updateScreen(Mode.UnlockScreen); + } + } + + public boolean isSecure() { + return LockPatternKeyguardView.this.isSecure(); + } + + public boolean isVerifyUnlockOnly() { + return mIsVerifyUnlockOnly; + } + + public void recreateMe() { + recreateScreens(); + } + + public void takeEmergencyCallAction() { + Intent intent = new Intent(ACTION_EMERGENCY_DIAL); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + getContext().startActivity(intent); + } + + public void pokeWakelock() { + getCallback().pokeWakelock(); + } + + public void pokeWakelock(int millis) { + getCallback().pokeWakelock(millis); + } + + public void keyguardDone(boolean authenticated) { + getCallback().keyguardDone(authenticated); + } + + public void keyguardDoneDrawing() { + // irrelevant to keyguard screen, they shouldn't be calling this + } + + public void reportFailedPatternAttempt() { + mUpdateMonitor.reportFailedAttempt(); + final int failedAttempts = mUpdateMonitor.getFailedAttempts(); + if (failedAttempts == + (LockPatternUtils.FAILED_ATTEMPTS_BEFORE_RESET - LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT)) { + showAlmostAtAccountLoginDialog(); + } else if (failedAttempts >= LockPatternUtils.FAILED_ATTEMPTS_BEFORE_RESET) { + mLockPatternUtils.setPermanentlyLocked(true); + updateScreen(mMode); + } else if ((failedAttempts % LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT) + == 0) { + showTimeoutDialog(); + } + } + }; + + /** + * We'll get key events the current screen doesn't use. see + * {@link KeyguardViewBase#onKeyDown(int, android.view.KeyEvent)} + */ + setFocusableInTouchMode(true); + setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); + + // create both the lock and unlock screen so they are quickly available + // when the screen turns on + mLockScreen = createLockScreen(); + addView(mLockScreen); + final UnlockMode unlockMode = getUnlockMode(); + mUnlockScreen = createUnlockScreenFor(unlockMode); + mUnlockScreenMode = unlockMode; + addView(mUnlockScreen); + updateScreen(mMode); + } + + @Override + public void reset() { + mIsVerifyUnlockOnly = false; + updateScreen(getInitialMode()); + } + + @Override + public void onScreenTurnedOff() { + mScreenOn = false; + if (mMode == Mode.LockScreen) { + ((KeyguardScreen) mLockScreen).onPause(); + } else { + ((KeyguardScreen) mUnlockScreen).onPause(); + } + } + + @Override + public void onScreenTurnedOn() { + mScreenOn = true; + if (mMode == Mode.LockScreen) { + ((KeyguardScreen) mLockScreen).onResume(); + } else { + ((KeyguardScreen) mUnlockScreen).onResume(); + } + } + + + private void recreateScreens() { + if (mLockScreen.getVisibility() == View.VISIBLE) { + ((KeyguardScreen) mLockScreen).onPause(); + } + ((KeyguardScreen) mLockScreen).cleanUp(); + removeViewInLayout(mLockScreen); + + mLockScreen = createLockScreen(); + mLockScreen.setVisibility(View.INVISIBLE); + addView(mLockScreen); + + if (mUnlockScreen.getVisibility() == View.VISIBLE) { + ((KeyguardScreen) mUnlockScreen).onPause(); + } + ((KeyguardScreen) mUnlockScreen).cleanUp(); + removeViewInLayout(mUnlockScreen); + + final UnlockMode unlockMode = getUnlockMode(); + mUnlockScreen = createUnlockScreenFor(unlockMode); + mUnlockScreen.setVisibility(View.INVISIBLE); + mUnlockScreenMode = unlockMode; + addView(mUnlockScreen); + + updateScreen(mMode); + } + + + @Override + public void wakeWhenReadyTq(int keyCode) { + if (DEBUG) Log.d(TAG, "onWakeKey"); + if (keyCode == KeyEvent.KEYCODE_MENU && isSecure() && (mMode == Mode.LockScreen) + && (mUpdateMonitor.getSimState() != SimCard.State.PUK_REQUIRED)) { + if (DEBUG) Log.d(TAG, "switching screens to unlock screen because wake key was MENU"); + updateScreen(Mode.UnlockScreen); + getCallback().pokeWakelock(); + } else { + if (DEBUG) Log.d(TAG, "poking wake lock immediately"); + getCallback().pokeWakelock(); + } + } + + @Override + public void verifyUnlock() { + if (!isSecure()) { + // non-secure keyguard screens are successfull by default + getCallback().keyguardDone(true); + } else if (mUnlockScreenMode != UnlockMode.Pattern) { + // can only verify unlock when in pattern mode + getCallback().keyguardDone(false); + } else { + // otherwise, go to the unlock screen, see if they can verify it + mIsVerifyUnlockOnly = true; + updateScreen(Mode.UnlockScreen); + } + } + + @Override + public void cleanUp() { + ((KeyguardScreen) mLockScreen).onPause(); + ((KeyguardScreen) mLockScreen).cleanUp(); + ((KeyguardScreen) mUnlockScreen).onPause(); + ((KeyguardScreen) mUnlockScreen).cleanUp(); + } + + private boolean isSecure() { + UnlockMode unlockMode = getUnlockMode(); + if (unlockMode == UnlockMode.Pattern) { + return mLockPatternUtils.isLockPatternEnabled(); + } else if (unlockMode == UnlockMode.SimPin) { + return mUpdateMonitor.getSimState() == SimCard.State.PIN_REQUIRED + || mUpdateMonitor.getSimState() == SimCard.State.PUK_REQUIRED; + } else if (unlockMode == UnlockMode.Account) { + return true; + } else { + throw new IllegalStateException("unknown unlock mode " + unlockMode); + } + } + + private void updateScreen(final Mode mode) { + + mMode = mode; + + final View goneScreen = (mode == Mode.LockScreen) ? mUnlockScreen : mLockScreen; + final View visibleScreen = (mode == Mode.LockScreen) + ? mLockScreen : getUnlockScreenForCurrentUnlockMode(); + + + if (mScreenOn) { + if (goneScreen.getVisibility() == View.VISIBLE) { + ((KeyguardScreen) goneScreen).onPause(); + } + if (visibleScreen.getVisibility() != View.VISIBLE) { + ((KeyguardScreen) visibleScreen).onResume(); + } + } + + goneScreen.setVisibility(View.GONE); + visibleScreen.setVisibility(View.VISIBLE); + + if (!visibleScreen.requestFocus()) { + throw new IllegalStateException("keyguard screen must be able to take " + + "focus when shown " + visibleScreen.getClass().getCanonicalName()); + } + } + + View createLockScreen() { + return new LockScreen( + mContext, + mLockPatternUtils, + mUpdateMonitor, + mKeyguardScreenCallback); + } + + View createUnlockScreenFor(UnlockMode unlockMode) { + if (unlockMode == UnlockMode.Pattern) { + return new UnlockScreen( + mContext, + mLockPatternUtils, + mUpdateMonitor, + mKeyguardScreenCallback, + mUpdateMonitor.getFailedAttempts()); + } else if (unlockMode == UnlockMode.SimPin) { + return new SimUnlockScreen( + mContext, + mUpdateMonitor, + mKeyguardScreenCallback); + } else if (unlockMode == UnlockMode.Account) { + try { + return new AccountUnlockScreen( + mContext, + mKeyguardScreenCallback, + mLockPatternUtils); + } catch (IllegalStateException e) { + Log.i(TAG, "Couldn't instantiate AccountUnlockScreen" + + " (IAccountsService isn't available)"); + // TODO: Need a more general way to provide a + // platform-specific fallback UI here. + // For now, if we can't display the account login + // unlock UI, just bring back the regular "Pattern" unlock mode. + + // (We do this by simply returning a regular UnlockScreen + // here. This means that the user will still see the + // regular pattern unlock UI, regardless of the value of + // mUnlockScreenMode or whether or not we're in the + // "permanently locked" state.) + return createUnlockScreenFor(UnlockMode.Pattern); + } + } else { + throw new IllegalArgumentException("unknown unlock mode " + unlockMode); + } + } + + private View getUnlockScreenForCurrentUnlockMode() { + final UnlockMode unlockMode = getUnlockMode(); + + // if a screen exists for the correct mode, we're done + if (unlockMode == mUnlockScreenMode) { + return mUnlockScreen; + } + + // remember the mode + mUnlockScreenMode = unlockMode; + + // unlock mode has changed and we have an existing old unlock screen + // to clean up + if (mScreenOn && (mUnlockScreen.getVisibility() == View.VISIBLE)) { + ((KeyguardScreen) mUnlockScreen).onPause(); + } + ((KeyguardScreen) mUnlockScreen).cleanUp(); + removeViewInLayout(mUnlockScreen); + + // create the new one + mUnlockScreen = createUnlockScreenFor(unlockMode); + mUnlockScreen.setVisibility(View.INVISIBLE); + addView(mUnlockScreen); + return mUnlockScreen; + } + + /** + * Given the current state of things, what should be the initial mode of + * the lock screen (lock or unlock). + */ + private Mode getInitialMode() { + final SimCard.State simState = mUpdateMonitor.getSimState(); + if (stuckOnLockScreenBecauseSimMissing() || (simState == SimCard.State.PUK_REQUIRED)) { + return Mode.LockScreen; + } else if (mUpdateMonitor.isKeyboardOpen() && isSecure()) { + return Mode.UnlockScreen; + } else { + return Mode.LockScreen; + } + } + + /** + * Given the current state of things, what should the unlock screen be? + */ + private UnlockMode getUnlockMode() { + final SimCard.State simState = mUpdateMonitor.getSimState(); + if (simState == SimCard.State.PIN_REQUIRED || simState == SimCard.State.PUK_REQUIRED) { + return UnlockMode.SimPin; + } else { + return mLockPatternUtils.isPermanentlyLocked() ? + UnlockMode.Account: + UnlockMode.Pattern; + } + } + + private void showTimeoutDialog() { + int timeoutInSeconds = (int) LockPatternUtils.FAILED_ATTEMPT_TIMEOUT_MS / 1000; + String message = mContext.getString( + R.string.lockscreen_too_many_failed_attempts_dialog_message, + mUpdateMonitor.getFailedAttempts(), + timeoutInSeconds); + final AlertDialog dialog = new AlertDialog.Builder(mContext) + .setTitle(null) + .setMessage(message) + .setNeutralButton(R.string.ok, null) + .create(); + dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); + dialog.getWindow().setFlags( + WindowManager.LayoutParams.FLAG_BLUR_BEHIND, + WindowManager.LayoutParams.FLAG_BLUR_BEHIND); + dialog.show(); + } + + private void showAlmostAtAccountLoginDialog() { + int timeoutInSeconds = (int) LockPatternUtils.FAILED_ATTEMPT_TIMEOUT_MS / 1000; + String message = mContext.getString( + R.string.lockscreen_failed_attempts_almost_glogin, + LockPatternUtils.FAILED_ATTEMPTS_BEFORE_RESET - LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT, + LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT, + timeoutInSeconds); + final AlertDialog dialog = new AlertDialog.Builder(mContext) + .setTitle(null) + .setMessage(message) + .setNeutralButton(R.string.ok, null) + .create(); + dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); + dialog.getWindow().setFlags( + WindowManager.LayoutParams.FLAG_BLUR_BEHIND, + WindowManager.LayoutParams.FLAG_BLUR_BEHIND); + dialog.show(); + } +} diff --git a/phone/com/android/internal/policy/impl/LockPatternKeyguardViewProperties.java b/phone/com/android/internal/policy/impl/LockPatternKeyguardViewProperties.java new file mode 100644 index 0000000..697c038 --- /dev/null +++ b/phone/com/android/internal/policy/impl/LockPatternKeyguardViewProperties.java @@ -0,0 +1,65 @@ +/* + * 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; + +import com.android.internal.widget.LockPatternUtils; + +import android.content.Context; +import com.android.internal.telephony.SimCard; + +/** + * Knows how to create a lock pattern keyguard view, and answer questions about + * it (even if it hasn't been created, per the interface specs). + */ +public class LockPatternKeyguardViewProperties implements KeyguardViewProperties { + + private final LockPatternUtils mLockPatternUtils; + private final KeyguardUpdateMonitor mUpdateMonitor; + + /** + * @param lockPatternUtils Used to know whether the pattern enabled, and passed + * onto the keygaurd view when it is created. + * @param updateMonitor Used to know whether the sim pin is enabled, and passed + * onto the keyguard view when it is created. + */ + public LockPatternKeyguardViewProperties(LockPatternUtils lockPatternUtils, + KeyguardUpdateMonitor updateMonitor) { + mLockPatternUtils = lockPatternUtils; + mUpdateMonitor = updateMonitor; + } + + public KeyguardViewBase createKeyguardView(Context context, + KeyguardUpdateMonitor updateMonitor) { + return new LockPatternKeyguardView(context, updateMonitor, mLockPatternUtils); + } + + public boolean isSecure() { + return isLockPatternSecure() || isSimPinSecure(); + } + + private boolean isLockPatternSecure() { + return mLockPatternUtils.isLockPatternEnabled() && mLockPatternUtils + .savedPatternExists(); + } + + private boolean isSimPinSecure() { + final SimCard.State simState = mUpdateMonitor.getSimState(); + return (simState == SimCard.State.PIN_REQUIRED || simState == SimCard.State.PUK_REQUIRED + || simState == SimCard.State.ABSENT); + } + +} diff --git a/phone/com/android/internal/policy/impl/LockScreen.java b/phone/com/android/internal/policy/impl/LockScreen.java new file mode 100644 index 0000000..c717cc3 --- /dev/null +++ b/phone/com/android/internal/policy/impl/LockScreen.java @@ -0,0 +1,367 @@ +/* + * 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; + +import com.android.internal.R; +import com.android.internal.widget.LockPatternUtils; + +import android.content.Context; +import android.pim.DateFormat; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import com.android.internal.telephony.SimCard; + +import java.util.Date; + +/** + * The screen within {@link LockPatternKeyguardView} that shows general + * information about the device depending on its state, and how to get + * past it, as applicable. + */ +class LockScreen extends LinearLayout implements KeyguardScreen, KeyguardUpdateMonitor.InfoCallback, + KeyguardUpdateMonitor.SimStateCallback, KeyguardUpdateMonitor.ConfigurationChangeCallback { + private final LockPatternUtils mLockPatternUtils; + private final KeyguardUpdateMonitor mUpdateMonitor; + private final KeyguardScreenCallback mCallback; + + private TextView mHeaderSimOk1; + private TextView mHeaderSimOk2; + + private TextView mHeaderSimBad1; + private TextView mHeaderSimBad2; + + private TextView mTime; + private TextView mDate; + + private ViewGroup mBatteryInfoGroup; + private ImageView mBatteryInfoIcon; + private TextView mBatteryInfoText; + private View mBatteryInfoSpacer; + + private ViewGroup mNextAlarmGroup; + private TextView mAlarmText; + private View mAlarmSpacer; + + private ViewGroup mScreenLockedMessageGroup; + + private TextView mLockInstructions; + + private Button mEmergencyCallButton; + + /** + * false means sim is missing or PUK'd + */ + private boolean mSimOk = true; + + // are we showing battery information? + private boolean mShowingBatteryInfo = false; + + // last known plugged in state + private boolean mPluggedIn = false; + + // last known battery level + private int mBatteryLevel = 100; + + + private View[] mOnlyVisibleWhenSimOk; + + private View[] mOnlyVisibleWhenSimNotOk; + + /** + * @param context Used to setup the view. + * @param lockPatternUtils Used to know the state of the lock pattern settings. + * @param updateMonitor Used to register for updates on various keyguard related + * state, and query the initial state at setup. + * @param callback Used to communicate back to the host keyguard view. + */ + LockScreen(Context context, LockPatternUtils lockPatternUtils, + KeyguardUpdateMonitor updateMonitor, + KeyguardScreenCallback callback) { + super(context); + mLockPatternUtils = lockPatternUtils; + mUpdateMonitor = updateMonitor; + mCallback = callback; + + final LayoutInflater inflater = LayoutInflater.from(context); + inflater.inflate(R.layout.keyguard_screen_lock, this, true); + + mSimOk = isSimOk(updateMonitor.getSimState()); + mShowingBatteryInfo = updateMonitor.shouldShowBatteryInfo(); + mPluggedIn = updateMonitor.isDevicePluggedIn(); + mBatteryLevel = updateMonitor.getBatteryLevel(); + + mHeaderSimOk1 = (TextView) findViewById(R.id.headerSimOk1); + mHeaderSimOk2 = (TextView) findViewById(R.id.headerSimOk2); + + mHeaderSimBad1 = (TextView) findViewById(R.id.headerSimBad1); + mHeaderSimBad2 = (TextView) findViewById(R.id.headerSimBad2); + + mTime = (TextView) findViewById(R.id.time); + mDate = (TextView) findViewById(R.id.date); + + mBatteryInfoGroup = (ViewGroup) findViewById(R.id.batteryInfo); + mBatteryInfoIcon = (ImageView) findViewById(R.id.batteryInfoIcon); + mBatteryInfoText = (TextView) findViewById(R.id.batteryInfoText); + mBatteryInfoSpacer = findViewById(R.id.batteryInfoSpacer); + + mNextAlarmGroup = (ViewGroup) findViewById(R.id.nextAlarmInfo); + mAlarmText = (TextView) findViewById(R.id.nextAlarmText); + mAlarmSpacer = findViewById(R.id.nextAlarmSpacer); + + mScreenLockedMessageGroup = (ViewGroup) findViewById(R.id.screenLockedInfo); + + mLockInstructions = (TextView) findViewById(R.id.lockInstructions); + + mEmergencyCallButton = (Button) findViewById(R.id.emergencyCallButton); + + mEmergencyCallButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + mCallback.takeEmergencyCallAction(); + } + }); + + mOnlyVisibleWhenSimOk = new View[] { + mHeaderSimOk1, + mHeaderSimOk2, + mBatteryInfoGroup, + mBatteryInfoSpacer, + mNextAlarmGroup, + mAlarmSpacer, + mScreenLockedMessageGroup, + mLockInstructions + }; + + mOnlyVisibleWhenSimNotOk = new View[] { + mHeaderSimBad1, + mHeaderSimBad2, + mEmergencyCallButton + }; + + setFocusable(true); + setFocusableInTouchMode(true); + setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); + + refreshBatteryDisplay(); + refreshAlarmDisplay(); + refreshTimeAndDateDisplay(); + refreshUnlockIntructions(); + refreshViewsWRTSimOk(); + refreshSimOkHeaders(mUpdateMonitor.getTelephonyPlmn(), mUpdateMonitor.getTelephonySpn()); + + updateMonitor.registerInfoCallback(this); + updateMonitor.registerSimStateCallback(this); + updateMonitor.registerConfigurationChangeCallback(this); + } + + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_MENU) { + mCallback.goToUnlockScreen(); + } + return false; + } + + private void refreshViewsWRTSimOk() { + if (mSimOk) { + for (int i = 0; i < mOnlyVisibleWhenSimOk.length; i++) { + final View view = mOnlyVisibleWhenSimOk[i]; + if (view == null) throw new RuntimeException("index " + i + " null"); + view.setVisibility(View.VISIBLE); + } + for (int i = 0; i < mOnlyVisibleWhenSimNotOk.length; i++) { + final View view = mOnlyVisibleWhenSimNotOk[i]; + view.setVisibility(View.GONE); + } + refreshSimOkHeaders(mUpdateMonitor.getTelephonyPlmn(), mUpdateMonitor.getTelephonySpn()); + refreshAlarmDisplay(); + refreshBatteryDisplay(); + } else { + for (int i = 0; i < mOnlyVisibleWhenSimOk.length; i++) { + final View view = mOnlyVisibleWhenSimOk[i]; + view.setVisibility(View.GONE); + } + for (int i = 0; i < mOnlyVisibleWhenSimNotOk.length; i++) { + final View view = mOnlyVisibleWhenSimNotOk[i]; + view.setVisibility(View.VISIBLE); + } + refreshSimBadInfo(); + } + } + + private void refreshSimBadInfo() { + final SimCard.State simState = mUpdateMonitor.getSimState(); + if (simState == SimCard.State.PUK_REQUIRED) { + mHeaderSimBad1.setText(R.string.lockscreen_sim_puk_locked_message); + mHeaderSimBad2.setText(R.string.lockscreen_sim_puk_locked_instructions); + } else if (simState == SimCard.State.ABSENT) { + mHeaderSimBad1.setText(R.string.lockscreen_missing_sim_message); + mHeaderSimBad2.setVisibility(View.GONE); + //mHeaderSimBad2.setText(R.string.lockscreen_missing_sim_instructions); + } else { + mHeaderSimBad1.setVisibility(View.GONE); + mHeaderSimBad2.setVisibility(View.GONE); + } + } + + private void refreshUnlockIntructions() { + if (mLockPatternUtils.isLockPatternEnabled() + || mUpdateMonitor.getSimState() == SimCard.State.PIN_REQUIRED) { + mLockInstructions.setText(R.string.lockscreen_instructions_when_pattern_enabled); + } else { + mLockInstructions.setText(R.string.lockscreen_instructions_when_pattern_disabled); + } + } + + private void refreshAlarmDisplay() { + String nextAlarmText = mLockPatternUtils.getNextAlarm(); + if (nextAlarmText != null && mSimOk) { + setAlarmInfoVisible(true); + mAlarmText.setText(nextAlarmText); + } else { + setAlarmInfoVisible(false); + } + } + + private void setAlarmInfoVisible(boolean visible) { + final int visibilityFlag = visible ? View.VISIBLE : View.GONE; + mNextAlarmGroup.setVisibility(visibilityFlag); + mAlarmSpacer.setVisibility(visibilityFlag); + } + + + public void onRefreshBatteryInfo(boolean showBatteryInfo, boolean pluggedIn, + int batteryLevel) { + mShowingBatteryInfo = showBatteryInfo; + mPluggedIn = pluggedIn; + mBatteryLevel = batteryLevel; + + refreshBatteryDisplay(); + } + + private void refreshBatteryDisplay() { + if (!mShowingBatteryInfo || !mSimOk) { + mBatteryInfoGroup.setVisibility(View.GONE); + mBatteryInfoSpacer.setVisibility(View.GONE); + return; + } + mBatteryInfoGroup.setVisibility(View.VISIBLE); + mBatteryInfoSpacer.setVisibility(View.VISIBLE); + + if (mPluggedIn) { + mBatteryInfoIcon.setImageResource(R.drawable.ic_lock_idle_charging); + mBatteryInfoText.setText( + getContext().getString(R.string.lockscreen_plugged_in, mBatteryLevel)); + } else { + mBatteryInfoIcon.setImageResource(R.drawable.ic_lock_idle_low_battery); + mBatteryInfoText.setText(R.string.lockscreen_low_battery); + } + } + + public void onTimeChanged() { + refreshTimeAndDateDisplay(); + } + + private void refreshTimeAndDateDisplay() { + Date now = new Date(); + mTime.setText(DateFormat.getTimeFormat(getContext()).format(now)); + mDate.setText(DateFormat.getDateFormat(getContext()).format(now)); + } + + public void onRefreshCarrierInfo(CharSequence plmn, CharSequence spn) { + refreshSimOkHeaders(plmn, spn); + } + + private void refreshSimOkHeaders(CharSequence plmn, CharSequence spn) { + final SimCard.State simState = mUpdateMonitor.getSimState(); + if (simState == SimCard.State.READY) { + if (plmn != null) { + mHeaderSimOk1.setVisibility(View.VISIBLE); + mHeaderSimOk1.setText(plmn); + } else { + mHeaderSimOk2.setVisibility(View.GONE); + } + + if (spn != null) { + mHeaderSimOk2.setVisibility(View.VISIBLE); + mHeaderSimOk2.setText(spn); + } else { + mHeaderSimOk2.setVisibility(View.GONE); + } + } else if (simState == SimCard.State.PIN_REQUIRED) { + mHeaderSimOk1.setVisibility(View.VISIBLE); + mHeaderSimOk1.setText(R.string.lockscreen_sim_locked_message); + mHeaderSimOk2.setVisibility(View.GONE); + } else if (simState == SimCard.State.ABSENT) { + mHeaderSimOk1.setVisibility(View.VISIBLE); + mHeaderSimOk1.setText(R.string.lockscreen_missing_sim_message_short); + mHeaderSimOk2.setVisibility(View.GONE); + } else if (simState == SimCard.State.NETWORK_LOCKED) { + mHeaderSimOk1.setVisibility(View.VISIBLE); + mHeaderSimOk1.setText(R.string.lockscreen_network_locked_message); + mHeaderSimOk2.setVisibility(View.GONE); + } + } + + public void onSimStateChanged(SimCard.State simState) { + mSimOk = isSimOk(simState); + refreshViewsWRTSimOk(); + } + + /** + * @return Whether the sim state is ok, meaning we don't need to show + * a special screen with the emergency call button and keep them from + * doing anything else. + */ + private boolean isSimOk(SimCard.State simState) { + boolean missingAndNotProvisioned = (!mUpdateMonitor.isDeviceProvisioned() + && simState == SimCard.State.ABSENT); + return !(missingAndNotProvisioned || simState == SimCard.State.PUK_REQUIRED); + } + + public void onOrientationChange(boolean inPortrait) { + mCallback.pokeWakelock(); + } + + public void onKeyboardChange(boolean isKeyboardOpen) { + if (isKeyboardOpen) { + mCallback.goToUnlockScreen(); + } + } + + + /** {@inheritDoc} */ + public void onPause() { + + } + + /** {@inheritDoc} */ + public void onResume() { + + } + + /** {@inheritDoc} */ + public void cleanUp() { + mUpdateMonitor.removeCallback(this); + } +} diff --git a/phone/com/android/internal/policy/impl/PhoneLayoutInflater.java b/phone/com/android/internal/policy/impl/PhoneLayoutInflater.java new file mode 100644 index 0000000..6bf4beb --- /dev/null +++ b/phone/com/android/internal/policy/impl/PhoneLayoutInflater.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2006 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 java.util.Map; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.view.LayoutInflater; + +public class PhoneLayoutInflater extends LayoutInflater { + private static final String[] sClassPrefixList = { + "android.widget.", + "android.webkit." + }; + + /** + * Instead of instantiating directly, you should retrieve an instance + * through {@link Context#getSystemService} + * + * @param context The Context in which in which to find resources and other + * application-specific things. + * + * @see Context#getSystemService + */ + public PhoneLayoutInflater(Context context) { + super(context); + } + + protected PhoneLayoutInflater(LayoutInflater original, Context newContext) { + super(original, newContext); + } + + /** Override onCreateView to instantiate names that correspond to the + widgets known to the Widget factory. If we don't find a match, + call through to our super class. + */ + @Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException { + for (String prefix : sClassPrefixList) { + try { + View view = createView(name, prefix, attrs); + if (view != null) { + return view; + } + } catch (ClassNotFoundException e) { + // In this case we want to let the base class take a crack + // at it. + } + } + + return super.onCreateView(name, attrs); + } + + public LayoutInflater cloneInContext(Context newContext) { + return new PhoneLayoutInflater(this, newContext); + } +} + diff --git a/phone/com/android/internal/policy/impl/PhoneWindow.java b/phone/com/android/internal/policy/impl/PhoneWindow.java new file mode 100644 index 0000000..2d5bd31 --- /dev/null +++ b/phone/com/android/internal/policy/impl/PhoneWindow.java @@ -0,0 +1,2572 @@ +/* + * 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; + +import com.android.internal.view.menu.ContextMenuBuilder; +import com.android.internal.view.menu.MenuBuilder; +import com.android.internal.view.menu.MenuDialogHelper; +import com.android.internal.view.menu.MenuView; +import com.android.internal.view.menu.SubMenuBuilder; + +import android.app.KeyguardManager; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.media.AudioManager; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; +import android.provider.CallLog.Calls; +import android.util.AndroidRuntimeException; +import android.util.Config; +import android.util.EventLog; +import android.util.Log; +import android.util.SparseArray; +import android.view.Gravity; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import static android.view.ViewGroup.LayoutParams.FILL_PARENT; +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; +import android.view.ViewManager; +import android.view.VolumePanel; +import android.view.Window; +import android.view.WindowManager; +import static android.view.WindowManager.LayoutParams.FLAG_FULLSCREEN; +import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; +import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; + +/** + * Android-specific Window. + * <p> + * todo: need to pull the generic functionality out into a base class + * in android.widget. + */ +public class PhoneWindow extends Window implements MenuBuilder.Callback { + private final static String TAG = "PhoneWindow"; + + private final static boolean SWEEP_OPEN_MENU = false; + + /** + * Simple callback used by the context menu and its submenus. The options + * menu submenus do not use this (their behavior is more complex). + */ + ContextMenuCallback mContextMenuCallback = new ContextMenuCallback(FEATURE_CONTEXT_MENU); + + // This is the top-level view of the window, containing the window decor. + private DecorView mDecor; + + // This is the view in which the window contents are placed. It is either + // mDecor itself, or a child of mDecor where the contents go. + private ViewGroup mContentParent; + + private boolean mIsFloating; + + private LayoutInflater mLayoutInflater; + + private TextView mTitleView; + + private DrawableFeatureState[] mDrawables; + + private PanelFeatureState[] mPanels; + + /** + * The panel that is prepared or opened (the most recent one if there are + * multiple panels). Shortcuts will go to this panel. It gets set in + * {@link #preparePanel} and cleared in {@link #closePanel}. + */ + private PanelFeatureState mPreparedPanel; + + /** + * The keycode that is currently held down (as a modifier) for chording. If + * this is 0, there is no key held down. + */ + private int mPanelChordingKey; + + private ImageView mLeftIconView; + + private ImageView mRightIconView; + + private ProgressBar mCircularProgressBar; + + private ProgressBar mHorizontalProgressBar; + + private int mBackgroundResource = 0; + + private Drawable mBackgroundDrawable; + + private int mFrameResource = 0; + + private int mTextColor = 0; + + private CharSequence mTitle = null; + + private int mTitleColor = 0; + + private ContextMenuBuilder mContextMenu; + private MenuDialogHelper mContextMenuHelper; + + private int mVolumeControlStreamType = AudioManager.USE_DEFAULT_STREAM_TYPE; + private long mVolumeKeyUpTime; + + private KeyguardManager mKeyguardManager = null; + + private boolean mSearchKeyDownReceived; + + private final Handler mKeycodeCallTimeoutHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + if (!mKeycodeCallTimeoutActive) return; + mKeycodeCallTimeoutActive = false; + // launch the VoiceDialer + Intent intent = new Intent(Intent.ACTION_VOICE_COMMAND); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + try { + getContext().startActivity(intent); + } catch (ActivityNotFoundException e) { + startCallActivity(); + } + } + }; + private boolean mKeycodeCallTimeoutActive = false; + + // Ideally the call and camera buttons would share a common base class, and each implement their + // own onShortPress() and onLongPress() methods, but to reduce the chance of regressions I'm + // keeping them separate for now. + private final Handler mKeycodeCameraTimeoutHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + if (!mKeycodeCameraTimeoutActive) return; + mKeycodeCameraTimeoutActive = false; + // Broadcast an intent that the Camera button was longpressed + Intent intent = new Intent(Intent.ACTION_CAMERA_BUTTON, null); + intent.putExtra(Intent.EXTRA_KEY_EVENT, (KeyEvent) msg.obj); + getContext().sendOrderedBroadcast(intent, null); + } + }; + private boolean mKeycodeCameraTimeoutActive = false; + + public PhoneWindow(Context context) { + super(context); + mLayoutInflater = LayoutInflater.from(context); + } + + @Override + public final void setContainer(Window container) { + super.setContainer(container); + } + + @Override + public boolean requestFeature(int featureId) { + if (mContentParent != null) { + throw new AndroidRuntimeException("requestFeature() must be called before adding content"); + } + final int features = getFeatures(); + if ((features != DEFAULT_FEATURES) && (featureId == FEATURE_CUSTOM_TITLE)) { + + /* Another feature is enabled and the user is trying to enable the custom title feature */ + throw new AndroidRuntimeException("You cannot combine custom titles with other title features"); + } + if (((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) && (featureId != FEATURE_CUSTOM_TITLE)) { + + /* Custom title feature is enabled and the user is trying to enable another feature */ + throw new AndroidRuntimeException("You cannot combine custom titles with other title features"); + } + /* FEATURE_OPENGL disabled for 1.0 + if (featureId == FEATURE_OPENGL) { + getAttributes().memoryType = WindowManager.LayoutParams.MEMORY_TYPE_GPU; + } + */ + return super.requestFeature(featureId); + } + + @Override + public void setContentView(int layoutResID) { + if (mContentParent == null) { + installDecor(); + } else { + mContentParent.removeAllViews(); + } + mLayoutInflater.inflate(layoutResID, mContentParent); + final Callback cb = getCallback(); + if (cb != null) { + cb.onContentChanged(); + } + } + + @Override + public void setContentView(View view) { + setContentView(view, new ViewGroup.LayoutParams(FILL_PARENT, FILL_PARENT)); + } + + @Override + public void setContentView(View view, ViewGroup.LayoutParams params) { + if (mContentParent == null) { + installDecor(); + } else { + mContentParent.removeAllViews(); + } + mContentParent.addView(view, params); + final Callback cb = getCallback(); + if (cb != null) { + cb.onContentChanged(); + } + } + + @Override + public void addContentView(View view, ViewGroup.LayoutParams params) { + if (mContentParent == null) { + installDecor(); + } + mContentParent.addView(view, params); + final Callback cb = getCallback(); + if (cb != null) { + cb.onContentChanged(); + } + } + + @Override + public View getCurrentFocus() { + return mDecor != null ? mDecor.findFocus() : null; + } + + @Override + public boolean isFloating() { + return mIsFloating; + } + + /** + * Return a LayoutInflater instance that can be used to inflate XML view layout + * resources for use in this Window. + * + * @return LayoutInflater The shared LayoutInflater. + */ + @Override + public LayoutInflater getLayoutInflater() { + return mLayoutInflater; + } + + @Override + public void setTitle(CharSequence title) { + if (mTitleView != null) { + mTitleView.setText(title); + } + mTitle = title; + } + + @Override + public void setTitleColor(int textColor) { + if (mTitleView != null) { + mTitleView.setTextColor(textColor); + } + mTitleColor = textColor; + } + + /** + * Prepares the panel to either be opened or chorded. This creates the Menu + * instance for the panel and populates it via the Activity callbacks. + * + * @param st The panel state to prepare. + * @param event The event that triggered the preparing of the panel. + * @return Whether the panel was prepared. If the panel should not be shown, + * returns false. + */ + public final boolean preparePanel(PanelFeatureState st, KeyEvent event) { + // Already prepared (isPrepared will be reset to false later) + if (st.isPrepared) + return true; + + if ((mPreparedPanel != null) && (mPreparedPanel != st)) { + // Another Panel is prepared and possibly open, so close it + closePanel(mPreparedPanel, false); + } + + final Callback cb = getCallback(); + + if (cb != null) { + st.createdPanelView = cb.onCreatePanelView(st.featureId); + } + + if (st.createdPanelView == null) { + // Init the panel state's menu--return false if init failed + if (st.menu == null) { + if (!initializePanelMenu(st) || (st.menu == null)) { + return false; + } + // Call callback, and return if it doesn't want to display menu + if ((cb == null) || !cb.onCreatePanelMenu(st.featureId, st.menu)) { + // Ditch the menu created above + st.menu = null; + + return false; + } + } + + // Callback and return if the callback does not want to show the menu + if (!cb.onPreparePanel(st.featureId, st.createdPanelView, st.menu)) { + return false; + } + + // Set the proper keymap + KeyCharacterMap kmap = KeyCharacterMap.load(event != null ? event.getDeviceId() : 0); + st.qwertyMode = kmap.getKeyboardType() != KeyCharacterMap.NUMERIC; + st.menu.setQwertyMode(st.qwertyMode); + } + + // Set other state + st.isPrepared = true; + st.isHandled = false; + mPreparedPanel = st; + + return true; + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false); + if ((st != null) && (st.menu != null)) { + final MenuBuilder menuBuilder = (MenuBuilder) st.menu; + + if (st.isOpen) { + // Freeze state + final Bundle state = new Bundle(); + menuBuilder.saveHierarchyState(state); + + // Remove the menu views since they need to be recreated + // according to the new configuration + clearMenuViews(st); + + // Re-open the same menu + reopenMenu(false); + + // Restore state + menuBuilder.restoreHierarchyState(state); + + } else { + // Clear menu views so on next menu opening, it will use + // the proper layout + clearMenuViews(st); + } + } + + } + + private static void clearMenuViews(PanelFeatureState st) { + + // This can be called on config changes, so we should make sure + // the views will be reconstructed based on the new orientation, etc. + + // Allow the callback to create a new panel view + st.createdPanelView = null; + + // Causes the decor view to be recreated + st.refreshDecorView = true; + + ((MenuBuilder) st.menu).clearMenuViews(); + } + + @Override + public final void openPanel(int featureId, KeyEvent event) { + openPanel(getPanelState(featureId, true), event); + } + + private void openPanel(PanelFeatureState st, KeyEvent event) { + // System.out.println("Open panel: isOpen=" + st.isOpen); + + // Already open, return + if (st.isOpen) { + return; + } + + Callback cb = getCallback(); + if ((cb != null) && (!cb.onMenuOpened(st.featureId, st.menu))) { + // Callback doesn't want the menu to open, reset any state + closePanel(st, true); + return; + } + + final WindowManager wm = getWindowManager(); + if (wm == null) { + return; + } + + // Prepare panel (should have been done before, but just in case) + if (!preparePanel(st, event)) { + return; + } + + if (st.decorView == null || st.refreshDecorView) { + if (st.decorView == null) { + // Initialize the panel decor, this will populate st.decorView + if (!initializePanelDecor(st) || (st.decorView == null)) + return; + } else if (st.refreshDecorView && (st.decorView.getChildCount() > 0)) { + // Decor needs refreshing, so remove its views + st.decorView.removeAllViews(); + } + + // This will populate st.shownPanelView + if (!initializePanelContent(st) || (st.shownPanelView == null)) { + return; + } + + ViewGroup.LayoutParams lp = st.shownPanelView.getLayoutParams(); + if (lp == null) { + lp = new ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT); + } + + int backgroundResId; + if (lp.width == ViewGroup.LayoutParams.FILL_PARENT) { + // If the contents is fill parent for the width, set the + // corresponding background + backgroundResId = st.fullBackground; + } else { + // Otherwise, set the normal panel background + backgroundResId = st.background; + } + st.decorView.setWindowBackground(getContext().getResources().getDrawable( + backgroundResId)); + + + st.decorView.addView(st.shownPanelView, lp); + + /* + * Give focus to the view, if it or one of its children does not + * already have it. + */ + if (!st.shownPanelView.hasFocus()) { + st.shownPanelView.requestFocus(); + } + } + + st.isOpen = true; + st.isHandled = false; + + WindowManager.LayoutParams lp = new WindowManager.LayoutParams( + WRAP_CONTENT, WRAP_CONTENT, + st.x, st.y, WindowManager.LayoutParams.TYPE_APPLICATION_PANEL, + WindowManager.LayoutParams.FLAG_DITHER, + st.decorView.mDefaultOpacity); + + lp.gravity = st.gravity; + lp.windowAnimations = st.windowAnimations; + wm.addView(st.decorView, lp); + // Log.v(TAG, "Adding main menu to window manager."); + } + + @Override + public final void closePanel(int featureId) { + closePanel(getPanelState(featureId, true), true); + } + + /** + * Closes the given panel. + * + * @param st The panel to be closed. + * @param doCallback Whether to notify the callback that the panel was + * closed. If the panel is in the process of re-opening or + * opening another panel (e.g., menu opening a sub menu), the + * callback should not happen and this variable should be false. + * In addition, this method internally will only perform the + * callback if the panel is open. + */ + public final void closePanel(PanelFeatureState st, boolean doCallback) { + // System.out.println("Close panel: isOpen=" + st.isOpen); + final ViewManager wm = getWindowManager(); + if ((wm != null) && st.isOpen) { + if (st.decorView != null) { + wm.removeView(st.decorView); + // Log.v(TAG, "Removing main menu from window manager."); + } + + if (doCallback) { + callOnPanelClosed(st.featureId, st, null); + } + } + st.isPrepared = false; + st.isHandled = false; + st.isOpen = false; + + // This view is no longer shown, so null it out + st.shownPanelView = null; + + if (st.isInExpandedMode) { + // Next time the menu opens, it should not be in expanded mode, so + // force a refresh of the decor + st.refreshDecorView = true; + st.isInExpandedMode = false; + } + + if (mPreparedPanel == st) { + mPreparedPanel = null; + mPanelChordingKey = 0; + } + } + + @Override + public final void togglePanel(int featureId, KeyEvent event) { + PanelFeatureState st = getPanelState(featureId, true); + if (st.isOpen) { + closePanel(st, true); + } else { + openPanel(st, event); + } + } + + /** + * Called when the panel key is pushed down. + * @param featureId The feature ID of the relevant panel (defaults to FEATURE_OPTIONS_PANEL}. + * @param event The key event. + * @return Whether the key was handled. + */ + public final boolean onKeyDownPanel(int featureId, KeyEvent event) { + // The panel key was pushed, so set the chording key + mPanelChordingKey = event.getKeyCode(); + + PanelFeatureState st = getPanelState(featureId, true); + if (!st.isOpen) { + return preparePanel(st, event); + } + + return false; + } + + /** + * Called when the panel key is released. + * @param featureId The feature ID of the relevant panel (defaults to FEATURE_OPTIONS_PANEL}. + * @param event The key event. + */ + public final void onKeyUpPanel(int featureId, KeyEvent event) { + // The panel key was released, so clear the chording key + mPanelChordingKey = 0; + + boolean playSoundEffect = false; + PanelFeatureState st = getPanelState(featureId, true); + if (st.isOpen || st.isHandled) { + + // Play the sound effect if the user closed an open menu (and not if + // they just released a menu shortcut) + playSoundEffect = st.isOpen; + + // Close menu + closePanel(st, true); + + } else if (st.isPrepared) { + + // Write 'menu opened' to event log + EventLog.writeEvent(50001, 0); + + // Show menu + openPanel(st, event); + + playSoundEffect = true; + } + + if (playSoundEffect) { + AudioManager audioManager = (AudioManager) getContext().getSystemService( + Context.AUDIO_SERVICE); + if (audioManager != null) { + audioManager.playSoundEffect(AudioManager.FX_KEY_CLICK); + } else { + Log.w(TAG, "Couldn't get audio manager"); + } + } + } + + @Override + public final void closeAllPanels() { + final ViewManager wm = getWindowManager(); + if (wm == null) { + return; + } + + final PanelFeatureState[] panels = mPanels; + final int N = panels != null ? panels.length : 0; + for (int i = 0; i < N; i++) { + final PanelFeatureState panel = panels[i]; + if (panel != null) { + closePanel(panel, true); + } + } + + closeContextMenu(); + } + + private synchronized void closeContextMenu() { + mContextMenu = null; + + if (mContextMenuHelper != null) { + mContextMenuHelper.dismiss(); + mContextMenuHelper = null; + } + } + + @Override + public boolean performPanelShortcut(int featureId, int keyCode, KeyEvent event, int flags) { + return performPanelShortcut(getPanelState(featureId, true), keyCode, event, flags); + } + + private boolean performPanelShortcut(PanelFeatureState st, int keyCode, KeyEvent event, + int flags) { + if (event.isSystem() || (st == null)) { + return false; + } + + boolean handled = false; + + // Only try to perform menu shortcuts if preparePanel returned true (possible false + // return value from application not wanting to show the menu). + if ((st.isPrepared || preparePanel(st, event)) && st.menu != null) { + // The menu is prepared now, perform the shortcut on it + handled = st.menu.performShortcut(keyCode, event, flags); + } + + if (handled) { + // Mark as handled + st.isHandled = true; + + if ((flags & Menu.FLAG_PERFORM_NO_CLOSE) == 0) { + closePanel(st, true); + } + } + + return handled; + } + + @Override + public boolean performPanelIdentifierAction(int featureId, int id, int flags) { + + PanelFeatureState st = getPanelState(featureId, true); + if (!preparePanel(st, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MENU))) { + return false; + } + if (st.menu == null) { + return false; + } + + boolean res = st.menu.performIdentifierAction(id, flags); + + closePanel(st, true); + + return res; + } + + public PanelFeatureState findMenuPanel(Menu menu) { + final PanelFeatureState[] panels = mPanels; + final int N = panels != null ? panels.length : 0; + for (int i = 0; i < N; i++) { + final PanelFeatureState panel = panels[i]; + if (panel != null && panel.menu == menu) { + return panel; + } + } + return null; + } + + public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) { + final Callback cb = getCallback(); + if (cb != null) { + final PanelFeatureState panel = findMenuPanel(menu.getRootMenu()); + if (panel != null) { + return cb.onMenuItemSelected(panel.featureId, item); + } + } + return false; + } + + public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { + final PanelFeatureState panel = findMenuPanel(menu); + if (panel != null) { + // Close the panel and only do the callback if the menu is being + // closed + // completely, not if opening a sub menu + closePanel(panel, allMenusAreClosing); + } + } + + public void onCloseSubMenu(SubMenuBuilder subMenu) { + final Menu parentMenu = subMenu.getRootMenu(); + final PanelFeatureState panel = findMenuPanel(parentMenu); + + // Callback + if (panel != null) { + callOnPanelClosed(panel.featureId, panel, parentMenu); + closePanel(panel, true); + } + } + + public boolean onSubMenuSelected(final SubMenuBuilder subMenu) { + if (!subMenu.hasVisibleItems()) { + return true; + } + + // The window manager will give us a valid window token + new MenuDialogHelper(subMenu).show(null); + + return true; + } + + public void onMenuModeChange(MenuBuilder menu) { + reopenMenu(true); + } + + private void reopenMenu(boolean toggleMenuMode) { + PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, true); + + // Save the future expanded mode state since closePanel will reset it + boolean newExpandedMode = toggleMenuMode ? !st.isInExpandedMode : st.isInExpandedMode; + + st.refreshDecorView = true; + closePanel(st, false); + + // Set the expanded mode state + st.isInExpandedMode = newExpandedMode; + + openPanel(st, null); + } + + /** + * Initializes the menu associated with the given panel feature state. You + * must at the very least set PanelFeatureState.menu to the Menu to be + * associated with the given panel state. The default implementation creates + * a new menu for the panel state. + * + * @param st The panel whose menu is being initialized. + * @return Whether the initialization was successful. + */ + protected boolean initializePanelMenu(final PanelFeatureState st) { + final MenuBuilder menu = new MenuBuilder(getContext()); + + menu.setCallback(this); + st.setMenu(menu); + + return true; + } + + /** + * Perform initial setup of a panel. This should at the very least set the + * style information in the PanelFeatureState and must set + * PanelFeatureState.decor to the panel's window decor view. + * + * @param st The panel being initialized. + */ + protected boolean initializePanelDecor(PanelFeatureState st) { + st.decorView = new DecorView(getContext(), st.featureId); + st.gravity = Gravity.CENTER | Gravity.BOTTOM; + st.setStyle(getContext()); + + return true; + } + + /** + * Initializes the panel associated with the panel feature state. You must + * at the very least set PanelFeatureState.panel to the View implementing + * its contents. The default implementation gets the panel from the menu. + * + * @param st The panel state being initialized. + * @return Whether the initialization was successful. + */ + protected boolean initializePanelContent(PanelFeatureState st) { + + if (st.createdPanelView != null) { + st.shownPanelView = st.createdPanelView; + return true; + } + + final MenuBuilder menu = (MenuBuilder)st.menu; + if (menu == null) { + return false; + } + + st.shownPanelView = menu.getMenuView((st.isInExpandedMode) ? MenuBuilder.TYPE_EXPANDED + : MenuBuilder.TYPE_ICON, st.decorView); + + if (st.shownPanelView != null) { + // Use the menu View's default animations if it has any + final int defaultAnimations = ((MenuView) st.shownPanelView).getWindowAnimations(); + if (defaultAnimations != 0) { + st.windowAnimations = defaultAnimations; + } + return true; + } else { + return false; + } + } + + @Override + public boolean performContextMenuIdentifierAction(int id, int flags) { + return (mContextMenu != null) ? mContextMenu.performIdentifierAction(id, flags) : false; + } + + @Override + public final void setBackgroundDrawable(Drawable drawable) { + if (drawable != mBackgroundDrawable) { + mBackgroundResource = 0; + mBackgroundDrawable = drawable; + if (mDecor != null) { + mDecor.setWindowBackground(drawable); + } + } + } + + @Override + public final void setFeatureDrawableResource(int featureId, int resId) { + if (resId != 0) { + DrawableFeatureState st = getDrawableState(featureId, true); + if (st.resid != resId) { + st.resid = resId; + st.uri = null; + st.local = getContext().getResources().getDrawable(resId); + updateDrawable(featureId, st, false); + } + } else { + setFeatureDrawable(featureId, null); + } + } + + @Override + public final void setFeatureDrawableUri(int featureId, Uri uri) { + if (uri != null) { + DrawableFeatureState st = getDrawableState(featureId, true); + if (st.uri == null || !st.uri.equals(uri)) { + st.resid = 0; + st.uri = uri; + st.local = loadImageURI(uri); + updateDrawable(featureId, st, false); + } + } else { + setFeatureDrawable(featureId, null); + } + } + + @Override + public final void setFeatureDrawable(int featureId, Drawable drawable) { + DrawableFeatureState st = getDrawableState(featureId, true); + st.resid = 0; + st.uri = null; + if (st.local != drawable) { + st.local = drawable; + updateDrawable(featureId, st, false); + } + } + + @Override + public void setFeatureDrawableAlpha(int featureId, int alpha) { + DrawableFeatureState st = getDrawableState(featureId, true); + if (st.alpha != alpha) { + st.alpha = alpha; + updateDrawable(featureId, st, false); + } + } + + protected final void setFeatureDefaultDrawable(int featureId, Drawable drawable) { + DrawableFeatureState st = getDrawableState(featureId, true); + if (st.def != drawable) { + st.def = drawable; + updateDrawable(featureId, st, false); + } + } + + @Override + public final void setFeatureInt(int featureId, int value) { + // XXX Should do more management (as with drawable features) to + // deal with interactions between multiple window policies. + updateInt(featureId, value, false); + } + + /** + * Update the state of a drawable feature. This should be called, for every + * drawable feature supported, as part of onActive(), to make sure that the + * contents of a containing window is properly updated. + * + * @see #onActive + * @param featureId The desired drawable feature to change. + * @param fromActive Always true when called from onActive(). + */ + protected final void updateDrawable(int featureId, boolean fromActive) { + final DrawableFeatureState st = getDrawableState(featureId, false); + if (st != null) { + updateDrawable(featureId, st, fromActive); + } + } + + /** + * Called when a Drawable feature changes, for the window to update its + * graphics. + * + * @param featureId The feature being changed. + * @param drawable The new Drawable to show, or null if none. + * @param alpha The new alpha blending of the Drawable. + */ + protected void onDrawableChanged(int featureId, Drawable drawable, int alpha) { + ImageView view; + if (featureId == FEATURE_LEFT_ICON) { + view = getLeftIconView(); + } else if (featureId == FEATURE_RIGHT_ICON) { + view = getRightIconView(); + } else { + return; + } + + if (drawable != null) { + drawable.setAlpha(alpha); + view.setImageDrawable(drawable); + view.setVisibility(View.VISIBLE); + } else { + view.setVisibility(View.GONE); + } + } + + /** + * Called when an int feature changes, for the window to update its + * graphics. + * + * @param featureId The feature being changed. + * @param value The new integer value. + */ + protected void onIntChanged(int featureId, int value) { + if (featureId == FEATURE_PROGRESS || featureId == FEATURE_INDETERMINATE_PROGRESS) { + updateProgressBars(value); + } else if (featureId == FEATURE_CUSTOM_TITLE) { + FrameLayout titleContainer = (FrameLayout) findViewById(com.android.internal.R.id.title_container); + if (titleContainer != null) { + mLayoutInflater.inflate(value, titleContainer); + } + } + } + + /** + * Updates the progress bars that are shown in the title bar. + * + * @param value Can be one of {@link Window#PROGRESS_VISIBILITY_ON}, + * {@link Window#PROGRESS_VISIBILITY_OFF}, + * {@link Window#PROGRESS_INDETERMINATE_ON}, + * {@link Window#PROGRESS_INDETERMINATE_OFF}, or a value + * starting at {@link Window#PROGRESS_START} through + * {@link Window#PROGRESS_END} for setting the default + * progress (if {@link Window#PROGRESS_END} is given, + * the progress bar widgets in the title will be hidden after an + * animation), a value between + * {@link Window#PROGRESS_SECONDARY_START} - + * {@link Window#PROGRESS_SECONDARY_END} for the + * secondary progress (if + * {@link Window#PROGRESS_SECONDARY_END} is given, the + * progress bar widgets will still be shown with the secondary + * progress bar will be completely filled in.) + */ + private void updateProgressBars(int value) { + ProgressBar circularProgressBar = getCircularProgressBar(true); + ProgressBar horizontalProgressBar = getHorizontalProgressBar(true); + + final int features = getLocalFeatures(); + if (value == PROGRESS_VISIBILITY_ON) { + if ((features & (1 << FEATURE_PROGRESS)) != 0) { + int level = horizontalProgressBar.getProgress(); + int visibility = (horizontalProgressBar.isIndeterminate() || level < 10000) ? + View.VISIBLE : View.INVISIBLE; + horizontalProgressBar.setVisibility(visibility); + } + if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) { + circularProgressBar.setVisibility(View.VISIBLE); + } + } else if (value == PROGRESS_VISIBILITY_OFF) { + if ((features & (1 << FEATURE_PROGRESS)) != 0) { + horizontalProgressBar.setVisibility(View.GONE); + } + if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) { + circularProgressBar.setVisibility(View.GONE); + } + } else if (value == PROGRESS_INDETERMINATE_ON) { + horizontalProgressBar.setIndeterminate(true); + } else if (value == PROGRESS_INDETERMINATE_OFF) { + horizontalProgressBar.setIndeterminate(false); + } else if (PROGRESS_START <= value && value <= PROGRESS_END) { + // We want to set the progress value before testing for visibility + // so that when the progress bar becomes visible again, it has the + // correct level. + horizontalProgressBar.setProgress(value - PROGRESS_START); + + if (value < PROGRESS_END) { + showProgressBars(horizontalProgressBar, circularProgressBar); + } else { + hideProgressBars(horizontalProgressBar, circularProgressBar); + } + } else if (PROGRESS_SECONDARY_START <= value && value <= PROGRESS_SECONDARY_END) { + horizontalProgressBar.setSecondaryProgress(value - PROGRESS_SECONDARY_START); + + showProgressBars(horizontalProgressBar, circularProgressBar); + } + + } + + private void showProgressBars(ProgressBar horizontalProgressBar, ProgressBar spinnyProgressBar) { + final int features = getLocalFeatures(); + if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0 && + spinnyProgressBar.getVisibility() == View.INVISIBLE) { + spinnyProgressBar.setVisibility(View.VISIBLE); + } + // Only show the progress bars if the primary progress is not complete + if ((features & (1 << FEATURE_PROGRESS)) != 0 && + horizontalProgressBar.getProgress() < 10000) { + horizontalProgressBar.setVisibility(View.VISIBLE); + } + } + + private void hideProgressBars(ProgressBar horizontalProgressBar, ProgressBar spinnyProgressBar) { + final int features = getLocalFeatures(); + Animation anim = AnimationUtils.loadAnimation(getContext(), com.android.internal.R.anim.fade_out); + anim.setDuration(1000); + if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0 && + spinnyProgressBar.getVisibility() == View.VISIBLE) { + spinnyProgressBar.startAnimation(anim); + spinnyProgressBar.setVisibility(View.INVISIBLE); + } + if ((features & (1 << FEATURE_PROGRESS)) != 0 && + horizontalProgressBar.getVisibility() == View.VISIBLE) { + horizontalProgressBar.startAnimation(anim); + horizontalProgressBar.setVisibility(View.INVISIBLE); + } + } + + /** + * Request that key events come to this activity. Use this if your activity + * has no views with focus, but the activity still wants a chance to process + * key events. + */ + @Override + public void takeKeyEvents(boolean get) { + mDecor.setFocusable(get); + } + + @Override + public boolean superDispatchKeyEvent(KeyEvent event) { + return mDecor.superDispatchKeyEvent(event); + } + + @Override + public boolean superDispatchTouchEvent(MotionEvent event) { + return mDecor.superDispatchTouchEvent(event); + } + + @Override + public boolean superDispatchTrackballEvent(MotionEvent event) { + return mDecor.superDispatchTrackballEvent(event); + } + + /** + * A key was pressed down and not handled by anything else in the window. + * + * @see #onKeyUp + * @see android.view.KeyEvent + */ + protected boolean onKeyDown(int featureId, int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_VOLUME_UP: + case KeyEvent.KEYCODE_VOLUME_DOWN: { + AudioManager audioManager = (AudioManager) getContext().getSystemService( + Context.AUDIO_SERVICE); + if (audioManager != null) { + /* + * Adjust the volume in on key down since it is more + * responsive to the user. + */ + audioManager.adjustSuggestedStreamVolume( + keyCode == KeyEvent.KEYCODE_VOLUME_UP + ? AudioManager.ADJUST_RAISE + : AudioManager.ADJUST_LOWER, + mVolumeControlStreamType, + AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_VIBRATE); + } + return true; + } + + case KeyEvent.KEYCODE_HEADSETHOOK: { + Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON, null); + intent.putExtra(Intent.EXTRA_KEY_EVENT, event); + getContext().sendOrderedBroadcast(intent, null); + return true; + } + + case KeyEvent.KEYCODE_CAMERA: { + if (getKeyguardManager().inKeyguardRestrictedInputMode()) { + break; + } + if (event.getRepeatCount() > 0) break; + mKeycodeCameraTimeoutActive = true; + mKeycodeCameraTimeoutHandler.removeMessages(0); + Message message = mKeycodeCameraTimeoutHandler.obtainMessage(0); + message.obj = event; + mKeycodeCameraTimeoutHandler.sendMessageDelayed(message, + ViewConfiguration.getLongPressTimeout()); + return true; + } + + case KeyEvent.KEYCODE_MENU: { + if (event.getRepeatCount() > 0) break; + onKeyDownPanel((featureId < 0) ? FEATURE_OPTIONS_PANEL : featureId, event); + return true; + } + + case KeyEvent.KEYCODE_BACK: { + if (event.getRepeatCount() > 0) break; + if (featureId < 0) break; + if (featureId == FEATURE_OPTIONS_PANEL) { + PanelFeatureState st = getPanelState(featureId, false); + if (st != null && st.isInExpandedMode) { + // If the user is in an expanded menu and hits back, it + // should go back to the icon menu + reopenMenu(true); + return true; + } + } + closePanel(featureId); + return true; + } + + case KeyEvent.KEYCODE_CALL: { + if (getKeyguardManager().inKeyguardRestrictedInputMode()) { + break; + } + if (event.getRepeatCount() > 0) break; + mKeycodeCallTimeoutActive = true; + mKeycodeCallTimeoutHandler.removeMessages(0); + mKeycodeCallTimeoutHandler.sendMessageDelayed( + mKeycodeCallTimeoutHandler.obtainMessage(0), + ViewConfiguration.getLongPressTimeout()); + return true; + } + + case KeyEvent.KEYCODE_SEARCH: { + if (event.getRepeatCount() == 0) { + mSearchKeyDownReceived = true; + } + break; + } + } + + return false; + } + + /** + * @return A handle to the keyguard manager. + */ + private KeyguardManager getKeyguardManager() { + if (mKeyguardManager == null) { + mKeyguardManager = (KeyguardManager) getContext().getSystemService(Context.KEYGUARD_SERVICE); + } + return mKeyguardManager; + } + + /** + * A key was released and not handled by anything else in the window. + * + * @see #onKeyDown + * @see android.view.KeyEvent + */ + protected boolean onKeyUp(int featureId, int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_VOLUME_UP: + case KeyEvent.KEYCODE_VOLUME_DOWN: { + AudioManager audioManager = (AudioManager) getContext().getSystemService( + Context.AUDIO_SERVICE); + if (audioManager != null) { + /* + * Play a sound. This is done on key up since we don't want the + * sound to play when a user holds down volume down to mute. + */ + audioManager.adjustSuggestedStreamVolume( + AudioManager.ADJUST_SAME, + mVolumeControlStreamType, + AudioManager.FLAG_PLAY_SOUND); + mVolumeKeyUpTime = SystemClock.uptimeMillis(); + } + return true; + } + + case KeyEvent.KEYCODE_MENU: { + onKeyUpPanel(featureId < 0 ? FEATURE_OPTIONS_PANEL : featureId, + event); + return true; + } + + case KeyEvent.KEYCODE_HEADSETHOOK: { + Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON, null); + intent.putExtra(Intent.EXTRA_KEY_EVENT, event); + getContext().sendOrderedBroadcast(intent, null); + return true; + } + + case KeyEvent.KEYCODE_CAMERA: { + if (getKeyguardManager().inKeyguardRestrictedInputMode()) { + break; + } + if (event.getRepeatCount() > 0) break; // Can a key up event repeat? + mKeycodeCameraTimeoutHandler.removeMessages(0); + if (!mKeycodeCameraTimeoutActive) break; + mKeycodeCameraTimeoutActive = false; + // Add short press behavior here if desired + return true; + } + + case KeyEvent.KEYCODE_CALL: { + if (getKeyguardManager().inKeyguardRestrictedInputMode()) { + break; + } + if (event.getRepeatCount() > 0) break; + mKeycodeCallTimeoutHandler.removeMessages(0); + if (!mKeycodeCallTimeoutActive) break; + mKeycodeCallTimeoutActive = false; + startCallActivity(); + return true; + } + + case KeyEvent.KEYCODE_SEARCH: { + /* + * Do this in onKeyUp since the Search key is also used for + * chording quick launch shortcuts. + */ + if (getKeyguardManager().inKeyguardRestrictedInputMode() || + !mSearchKeyDownReceived) { + break; + } + mSearchKeyDownReceived = false; + launchDefaultSearch(); + return true; + } + } + + return false; + } + + private void startCallActivity() { + Intent intent = new Intent(Intent.ACTION_CALL_BUTTON); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + getContext().startActivity(intent); + } + + @Override + protected void onActive() { + } + + @Override + public final View getDecorView() { + if (mDecor == null) { + installDecor(); + } + return mDecor; + } + + @Override + public final View peekDecorView() { + return mDecor; + } + + static private final String FOCUSED_ID_TAG = "android:focusedViewId"; + static private final String VIEWS_TAG = "android:views"; + static private final String PANELS_TAG = "android:Panels"; + + /** {@inheritDoc} */ + @Override + public Bundle saveHierarchyState() { + Bundle outState = new Bundle(); + if (mContentParent == null) { + return outState; + } + + SparseArray<Parcelable> states = new SparseArray<Parcelable>(); + mContentParent.saveHierarchyState(states); + outState.putSparseParcelableArray(VIEWS_TAG, states); + + // save the focused view id + View focusedView = mContentParent.findFocus(); + if (focusedView != null) { + if (focusedView.getId() != View.NO_ID) { + outState.putInt(FOCUSED_ID_TAG, focusedView.getId()); + } else { + if (Config.LOGD) { + Log.d(TAG, "couldn't save which view has focus because the focused view " + + focusedView + " has no id."); + } + } + } + + // save the panels + SparseArray<Parcelable> panelStates = new SparseArray<Parcelable>(); + savePanelState(panelStates); + if (panelStates.size() > 0) { + outState.putSparseParcelableArray(PANELS_TAG, panelStates); + } + + return outState; + } + + /** {@inheritDoc} */ + @Override + public void restoreHierarchyState(Bundle savedInstanceState) { + if (mContentParent == null) { + return; + } + + SparseArray<Parcelable> savedStates + = savedInstanceState.getSparseParcelableArray(VIEWS_TAG); + if (savedStates != null) { + mContentParent.restoreHierarchyState(savedStates); + } + + // restore the focused view + int focusedViewId = savedInstanceState.getInt(FOCUSED_ID_TAG, View.NO_ID); + if (focusedViewId != View.NO_ID) { + View needsFocus = mContentParent.findViewById(focusedViewId); + if (needsFocus != null) { + needsFocus.requestFocus(); + } else { + Log.w(TAG, + "Previously focused view reported id " + focusedViewId + + " during save, but can't be found during restore."); + } + } + + // restore the panels + SparseArray<Parcelable> panelStates = savedInstanceState.getSparseParcelableArray(PANELS_TAG); + if (panelStates != null) { + restorePanelState(panelStates); + } + } + + /** + * Invoked when the panels should freeze their state. + * + * @param icicles Save state into this. This is usually indexed by the + * featureId. This will be given to {@link #restorePanelState} in the + * future. + */ + private void savePanelState(SparseArray<Parcelable> icicles) { + PanelFeatureState[] panels = mPanels; + if (panels == null) { + return; + } + + for (int curFeatureId = panels.length - 1; curFeatureId >= 0; curFeatureId--) { + if (panels[curFeatureId] != null) { + icicles.put(curFeatureId, panels[curFeatureId].onSaveInstanceState()); + } + } + } + + /** + * Invoked when the panels should thaw their state from a previously frozen state. + * + * @param icicles The state saved by {@link #savePanelState} that needs to be thawed. + */ + private void restorePanelState(SparseArray<Parcelable> icicles) { + PanelFeatureState st; + for (int curFeatureId = icicles.size() - 1; curFeatureId >= 0; curFeatureId--) { + st = getPanelState(curFeatureId, false /* required */); + if (st == null) { + // The panel must not have been required, and is currently not around, skip it + continue; + } + + st.onRestoreInstanceState(icicles.get(curFeatureId)); + } + + /* + * Implementation note: call openPanelsAfterRestore later to actually open the + * restored panels. + */ + } + + /** + * Opens the panels that have had their state restored. This should be + * called sometime after {@link #restorePanelState} when it is safe to add + * to the window manager. + */ + private void openPanelsAfterRestore() { + PanelFeatureState[] panels = mPanels; + + if (panels == null) { + return; + } + + PanelFeatureState st; + for (int i = panels.length - 1; i >= 0; i--) { + st = panels[i]; + if ((st != null) && st.isOpen) { + // Clear st.isOpen (openPanel will not open if it's already open) + st.isOpen = false; + openPanel(st, null); + } + } + } + + private final class DecorView extends FrameLayout { + /* package */int mDefaultOpacity = PixelFormat.OPAQUE; + + /** The feature ID of the panel, or -1 if this is the application's DecorView */ + private final int mFeatureId; + + private final Rect mDrawingBounds = new Rect(); + + private final Rect mBackgroundPadding = new Rect(); + + private final Rect mFramePadding = new Rect(); + + private final Rect mFrameOffsets = new Rect(); + + private final Paint mBlackPaint = new Paint(); + + private boolean mChanging; + + private Drawable mMenuBackground; + private boolean mWatchingForMenu; + private int mDownY; + + public DecorView(Context context, int featureId) { + super(context); + mFeatureId = featureId; + mBlackPaint.setColor(0xFF000000); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + final int keyCode = event.getKeyCode(); + final boolean isDown = event.getAction() == KeyEvent.ACTION_DOWN; + + /* + * If the user hits another key within the play sound delay, then + * cancel the sound + */ + if (keyCode != KeyEvent.KEYCODE_VOLUME_DOWN && keyCode != KeyEvent.KEYCODE_VOLUME_UP + && mVolumeKeyUpTime + VolumePanel.PLAY_SOUND_DELAY + > SystemClock.uptimeMillis()) { + /* + * The user has hit another key during the delay (e.g., 300ms) + * since the last volume key up, so cancel any sounds. + */ + AudioManager audioManager = (AudioManager) getContext().getSystemService( + Context.AUDIO_SERVICE); + if (audioManager != null) { + audioManager.adjustSuggestedStreamVolume(AudioManager.ADJUST_SAME, + mVolumeControlStreamType, AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE); + } + } + + if (isDown && (event.getRepeatCount() == 0)) { + // First handle chording of panel key: if a panel key is held + // but not released, try to execute a shortcut in it. + if ((mPanelChordingKey > 0) && (mPanelChordingKey != keyCode)) { + // Perform the shortcut (mPreparedPanel can be null since + // global shortcuts (such as search) don't rely on a + // prepared panel or menu). + boolean handled = performPanelShortcut(mPreparedPanel, keyCode, event, + Menu.FLAG_PERFORM_NO_CLOSE); + + if (!handled) { + /* + * If not handled, then pass it to the view hierarchy + * and anyone else that may be interested. + */ + handled = dispatchKeyShortcut(event); + + if (handled && mPreparedPanel != null) { + mPreparedPanel.isHandled = true; + } + } + + if (handled) { + return true; + } + } + + // If a panel is open, perform a shortcut on it without the + // chorded panel key + if ((mPreparedPanel != null) && mPreparedPanel.isOpen) { + if (performPanelShortcut(mPreparedPanel, keyCode, event, 0)) { + return true; + } + } + } + + final Callback cb = getCallback(); + final boolean handled = cb != null && mFeatureId < 0 ? cb.dispatchKeyEvent(event) + : super.dispatchKeyEvent(event); + if (handled) { + return true; + } + return isDown ? PhoneWindow.this.onKeyDown(mFeatureId, event.getKeyCode(), event) + : PhoneWindow.this.onKeyUp(mFeatureId, event.getKeyCode(), event); + } + + private boolean dispatchKeyShortcut(KeyEvent event) { + View focusedView = findFocus(); + return focusedView == null ? false : focusedView.dispatchKeyShortcutEvent(event); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + final Callback cb = getCallback(); + return cb != null && mFeatureId < 0 ? cb.dispatchTouchEvent(ev) : super + .dispatchTouchEvent(ev); + } + + @Override + public boolean dispatchTrackballEvent(MotionEvent ev) { + final Callback cb = getCallback(); + return cb != null && mFeatureId < 0 ? cb.dispatchTrackballEvent(ev) : super + .dispatchTrackballEvent(ev); + } + + public boolean superDispatchKeyEvent(KeyEvent event) { + return super.dispatchKeyEvent(event); + } + + public boolean superDispatchTouchEvent(MotionEvent event) { + return super.dispatchTouchEvent(event); + } + + public boolean superDispatchTrackballEvent(MotionEvent event) { + return super.dispatchTrackballEvent(event); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + return onInterceptTouchEvent(event); + } + + private boolean isOutOfBounds(int x, int y) { + return x < -5 || y < -5 || x > (getWidth() + 5) + || y > (getHeight() + 5); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + int action = event.getAction(); + if (mFeatureId >= 0) { + if (action == MotionEvent.ACTION_DOWN) { + int x = (int)event.getX(); + int y = (int)event.getY(); + if (isOutOfBounds(x, y)) { + closePanel(mFeatureId); + return true; + } + } + } + + if (!SWEEP_OPEN_MENU) { + return false; + } + + if (mFeatureId >= 0) { + if (action == MotionEvent.ACTION_DOWN) { + Log.i(TAG, "Watchiing!"); + mWatchingForMenu = true; + mDownY = (int) event.getY(); + return false; + } + + if (!mWatchingForMenu) { + return false; + } + + int y = (int)event.getY(); + if (action == MotionEvent.ACTION_MOVE) { + if (y > (mDownY+30)) { + Log.i(TAG, "Closing!"); + closePanel(mFeatureId); + mWatchingForMenu = false; + return true; + } + } else if (action == MotionEvent.ACTION_UP) { + mWatchingForMenu = false; + } + + return false; + } + + //Log.i(TAG, "Intercept: action=" + action + " y=" + event.getY() + // + " (in " + getHeight() + ")"); + + if (action == MotionEvent.ACTION_DOWN) { + int y = (int)event.getY(); + if (y >= (getHeight()-5) && !hasChildren()) { + Log.i(TAG, "Watchiing!"); + mWatchingForMenu = true; + } + return false; + } + + if (!mWatchingForMenu) { + return false; + } + + int y = (int)event.getY(); + if (action == MotionEvent.ACTION_MOVE) { + if (y < (getHeight()-30)) { + Log.i(TAG, "Opening!"); + openPanel(FEATURE_OPTIONS_PANEL, new KeyEvent( + KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MENU)); + mWatchingForMenu = false; + return true; + } + } else if (action == MotionEvent.ACTION_UP) { + mWatchingForMenu = false; + } + + return false; + } + + @Override + protected boolean setFrame(int l, int t, int r, int b) { + boolean changed = super.setFrame(l, t, r, b); + if (changed) { + final Rect drawingBounds = mDrawingBounds; + getDrawingRect(drawingBounds); + + Drawable fg = getForeground(); + if (fg != null) { + final Rect frameOffsets = mFrameOffsets; + drawingBounds.left += frameOffsets.left; + drawingBounds.top += frameOffsets.top; + drawingBounds.right -= frameOffsets.right; + drawingBounds.bottom -= frameOffsets.bottom; + fg.setBounds(drawingBounds); + final Rect framePadding = mFramePadding; + drawingBounds.left += framePadding.left - frameOffsets.left; + drawingBounds.top += framePadding.top - frameOffsets.top; + drawingBounds.right -= framePadding.right - frameOffsets.right; + drawingBounds.bottom -= framePadding.bottom - frameOffsets.bottom; + } + + Drawable bg = getBackground(); + if (bg != null) { + bg.setBounds(drawingBounds); + } + + if (SWEEP_OPEN_MENU) { + if (mMenuBackground == null && mFeatureId < 0 + && getAttributes().height + == WindowManager.LayoutParams.FILL_PARENT) { + mMenuBackground = getContext().getResources().getDrawable( + com.android.internal.R.drawable.menu_background); + } + if (mMenuBackground != null) { + mMenuBackground.setBounds(drawingBounds.left, + drawingBounds.bottom-6, drawingBounds.right, + drawingBounds.bottom+20); + } + } + } + return changed; + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + + if (mMenuBackground != null) { + mMenuBackground.draw(canvas); + } + } + + + @Override + public boolean showContextMenuForChild(View originalView) { + // Reuse the context menu builder + if (mContextMenu == null) { + mContextMenu = new ContextMenuBuilder(getContext()); + mContextMenu.setCallback(mContextMenuCallback); + } else { + mContextMenu.clearAll(); + } + + mContextMenuHelper = mContextMenu.show(originalView, originalView.getWindowToken()); + return mContextMenuHelper != null; + } + + public void startChanging() { + mChanging = true; + } + + public void finishChanging() { + mChanging = false; + drawableChanged(); + } + + public void setWindowBackground(Drawable drawable) { + if (getBackground() != drawable) { + setBackgroundDrawable(drawable); + if (drawable != null) { + drawable.getPadding(mBackgroundPadding); + } else { + mBackgroundPadding.setEmpty(); + } + drawableChanged(); + } + } + + public void setWindowFrame(Drawable drawable) { + if (getForeground() != drawable) { + setForeground(drawable); + if (drawable != null) { + drawable.getPadding(mFramePadding); + } else { + mFramePadding.setEmpty(); + } + drawableChanged(); + } + } + + @Override + protected boolean fitSystemWindows(Rect insets) { + mFrameOffsets.set(insets); + if (getForeground() != null) { + drawableChanged(); + } + return super.fitSystemWindows(insets); + } + + private void drawableChanged() { + if (mChanging) { + return; + } + + setPadding(mFramePadding.left + mBackgroundPadding.left, mFramePadding.top + + mBackgroundPadding.top, mFramePadding.right + mBackgroundPadding.right, + mFramePadding.bottom + mBackgroundPadding.bottom); + requestLayout(); + invalidate(); + + int opacity = PixelFormat.OPAQUE; + + // Note: if there is no background, we will assume opaque. The + // common case seems to be that an application sets there to be + // no background so it can draw everything itself. For that, + // we would like to assume OPAQUE and let the app force it to + // the slower TRANSLUCENT mode if that is really what it wants. + Drawable bg = getBackground(); + Drawable fg = getForeground(); + if (bg != null) { + if (fg == null) { + opacity = bg.getOpacity(); + } else if (mFramePadding.left <= 0 && mFramePadding.top <= 0 + && mFramePadding.right <= 0 && mFramePadding.bottom <= 0) { + // If the frame padding is zero, then we can be opaque + // if either the frame -or- the background is opaque. + int fop = fg.getOpacity(); + int bop = bg.getOpacity(); + if (Config.LOGV) + Log.v(TAG, "Background opacity: " + bop + ", Frame opacity: " + fop); + if (fop == PixelFormat.OPAQUE || bop == PixelFormat.OPAQUE) { + opacity = PixelFormat.OPAQUE; + } else if (fop == PixelFormat.UNKNOWN) { + opacity = bop; + } else if (bop == PixelFormat.UNKNOWN) { + opacity = fop; + } else { + opacity = Drawable.resolveOpacity(fop, bop); + } + } else { + // For now we have to assume translucent if there is a + // frame with padding... there is no way to tell if the + // frame and background together will draw all pixels. + if (Config.LOGV) + Log.v(TAG, "Padding: " + mFramePadding); + opacity = PixelFormat.TRANSLUCENT; + } + } + + if (Config.LOGV) + Log.v(TAG, "Background: " + bg + ", Frame: " + fg); + if (Config.LOGV) + Log.v(TAG, "Selected default opacity: " + opacity); + + mDefaultOpacity = opacity; + if (mFeatureId < 0) { + setDefaultWindowFormat(opacity); + } + } + + @Override + public void onWindowFocusChanged(boolean hasWindowFocus) { + super.onWindowFocusChanged(hasWindowFocus); + + // no KEYCODE_CALL events active across focus changes + mKeycodeCallTimeoutHandler.removeMessages(0); + mKeycodeCallTimeoutActive = false; + mKeycodeCameraTimeoutHandler.removeMessages(0); + mKeycodeCameraTimeoutActive = false; + + // If the user is chording a menu shortcut, release the chord since + // this window lost focus + if (!hasWindowFocus && mPanelChordingKey > 0) { + closePanel(FEATURE_OPTIONS_PANEL); + } + + final Callback cb = getCallback(); + if (cb != null && mFeatureId < 0) { + cb.onWindowFocusChanged(hasWindowFocus); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + if (mFeatureId == -1) { + /* + * The main window has been attached, try to restore any panels + * that may have been open before. This is called in cases where + * an activity is being killed for configuration change and the + * menu was open. When the activity is recreated, the menu + * should be shown again. + */ + openPanelsAfterRestore(); + } + } + } + + protected DecorView generateDecor() { + return new DecorView(getContext(), -1); + } + + protected void setFeatureFromAttrs(int featureId, TypedArray attrs, + int drawableAttr, int alphaAttr) { + Drawable d = attrs.getDrawable(drawableAttr); + if (d != null) { + requestFeature(featureId); + setFeatureDefaultDrawable(featureId, d); + } + if ((getFeatures() & (1 << featureId)) != 0) { + int alpha = attrs.getInt(alphaAttr, -1); + if (alpha >= 0) { + setFeatureDrawableAlpha(featureId, alpha); + } + } + } + + protected ViewGroup generateLayout(DecorView decor) { + // Apply data from current theme. + + final Context c = getContext(); + TypedArray a = getWindowStyle(); + + if (false) { + System.out.println("From style:"); + String s = "Attrs:"; + for (int i = 0; i < com.android.internal.R.styleable.Window.length; i++) { + s = s + " " + Integer.toHexString(com.android.internal.R.styleable.Window[i]) + "=" + + a.getString(i); + } + System.out.println(s); + } + + mIsFloating = a.getBoolean(com.android.internal.R.styleable.Window_windowIsFloating, false); + int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR) + & (~getForcedWindowFlags()); + if (mIsFloating) { + setLayout(WRAP_CONTENT, WRAP_CONTENT); + setFlags(0, flagsToUpdate); + + /* All dialogs should have the window dimmed */ + WindowManager.LayoutParams params = getAttributes(); + TypedArray attrs = c.obtainStyledAttributes( + com.android.internal.R.styleable.Theme); + params.flags |= WindowManager.LayoutParams.FLAG_DIM_BEHIND; + params.dimAmount = attrs.getFloat( + android.R.styleable.Theme_backgroundDimAmount, 0.5f); + attrs.recycle(); + } else { + setFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR, flagsToUpdate); + } + + if (a.getBoolean(com.android.internal.R.styleable.Window_windowNoTitle, false)) { + requestFeature(FEATURE_NO_TITLE); + } + + if (a.getBoolean(com.android.internal.R.styleable.Window_windowFullscreen, false)) { + setFlags(FLAG_FULLSCREEN, FLAG_FULLSCREEN&(~getForcedWindowFlags())); + } + + // The rest are only done if this window is not embedded; otherwise, + // the values are inherited from our container. + if (getContainer() == null) { + if (mBackgroundDrawable == null) { + if (mBackgroundResource == 0) { + mBackgroundResource = a.getResourceId( + com.android.internal.R.styleable.Window_windowBackground, 0); + } + if (mFrameResource == 0) { + mFrameResource = a.getResourceId(com.android.internal.R.styleable.Window_windowFrame, 0); + } + if (false) { + System.out.println("Background: " + + Integer.toHexString(mBackgroundResource) + " Frame: " + + Integer.toHexString(mFrameResource)); + } + } + mTextColor = a.getColor(com.android.internal.R.styleable.Window_textColor, 0xFF000000); + } + + // Inflate the window decor. + + int layoutResource; + int features = getLocalFeatures(); + // System.out.println("Features: 0x" + Integer.toHexString(features)); + if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) { + if (mIsFloating) { + layoutResource = com.android.internal.R.layout.dialog_title_icons; + } else { + layoutResource = com.android.internal.R.layout.screen_title_icons; + } + // System.out.println("Title Icons!"); + } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0) { + // Special case for a window with only a progress bar (and title). + // XXX Need to have a no-title version of embedded windows. + layoutResource = com.android.internal.R.layout.screen_progress; + // System.out.println("Progress!"); + } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) { + // Special case for a window with a custom title. + // If the window is floating, we need a dialog layout + if (mIsFloating) { + layoutResource = com.android.internal.R.layout.dialog_custom_title; + } else { + layoutResource = com.android.internal.R.layout.screen_custom_title; + } + } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) { + // If no other features and not embedded, only need a title. + // If the window is floating, we need a dialog layout + if (mIsFloating) { + layoutResource = com.android.internal.R.layout.dialog_title; + } else { + layoutResource = com.android.internal.R.layout.screen_title; + } + // System.out.println("Title!"); + } else { + // Embedded, so no decoration is needed. + layoutResource = com.android.internal.R.layout.screen_simple; + // System.out.println("Simple!"); + } + + mDecor.startChanging(); + + View in = mLayoutInflater.inflate(layoutResource, null); + decor.addView(in, new ViewGroup.LayoutParams(FILL_PARENT, FILL_PARENT)); + + ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); + if (contentParent == null) { + throw new RuntimeException("Window couldn't find content container view"); + } + + if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) { + ProgressBar progress = getCircularProgressBar(false); + if (progress != null) { + progress.setIndeterminate(true); + } + } + + // Remaining setup -- of background and title -- that only applies + // to top-level windows. + if (getContainer() == null) { + Drawable drawable = mBackgroundDrawable; + if (mBackgroundResource != 0) { + drawable = getContext().getResources().getDrawable(mBackgroundResource); + } + mDecor.setWindowBackground(drawable); + drawable = null; + if (mFrameResource != 0) { + drawable = getContext().getResources().getDrawable(mFrameResource); + } + mDecor.setWindowFrame(drawable); + + // System.out.println("Text=" + Integer.toHexString(mTextColor) + + // " Sel=" + Integer.toHexString(mTextSelectedColor) + + // " Title=" + Integer.toHexString(mTitleColor)); + + if (mTitleColor == 0) { + mTitleColor = mTextColor; + } + + if (mTitle != null) { + setTitle(mTitle); + } + setTitleColor(mTitleColor); + } + + mDecor.finishChanging(); + + return contentParent; + } + + private void installDecor() { + if (mDecor == null) { + mDecor = generateDecor(); + mDecor.setIsRootNamespace(true); + } + if (mContentParent == null) { + mContentParent = generateLayout(mDecor); + + mTitleView = (TextView)findViewById(com.android.internal.R.id.title); + if (mTitleView != null) { + if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) { + View titleContainer = findViewById(com.android.internal.R.id.title_container); + if (titleContainer != null) { + titleContainer.setVisibility(View.GONE); + } else { + mTitleView.setVisibility(View.GONE); + } + if (mContentParent instanceof FrameLayout) { + ((FrameLayout)mContentParent).setForeground(null); + } + } else { + mTitleView.setText(mTitle); + } + } + } + } + + private Drawable loadImageURI(Uri uri) { + try { + return Drawable.createFromStream( + getContext().getContentResolver().openInputStream(uri), null); + } catch (Exception e) { + Log.w(TAG, "Unable to open content: " + uri); + } + return null; + } + + private DrawableFeatureState getDrawableState(int featureId, boolean required) { + if ((getFeatures() & (1 << featureId)) == 0) { + if (!required) { + return null; + } + throw new RuntimeException("The feature has not been requested"); + } + + DrawableFeatureState[] ar; + if ((ar = mDrawables) == null || ar.length <= featureId) { + DrawableFeatureState[] nar = new DrawableFeatureState[featureId + 1]; + if (ar != null) { + System.arraycopy(ar, 0, nar, 0, ar.length); + } + mDrawables = ar = nar; + } + + DrawableFeatureState st = ar[featureId]; + if (st == null) { + ar[featureId] = st = new DrawableFeatureState(featureId); + } + return st; + } + + /** + * Gets a panel's state based on its feature ID. + * + * @param featureId The feature ID of the panel. + * @param required Whether the panel is required (if it is required and it + * isn't in our features, this throws an exception). + * @return The panel state. + */ + private PanelFeatureState getPanelState(int featureId, boolean required) { + return getPanelState(featureId, required, null); + } + + /** + * Gets a panel's state based on its feature ID. + * + * @param featureId The feature ID of the panel. + * @param required Whether the panel is required (if it is required and it + * isn't in our features, this throws an exception). + * @param convertPanelState Optional: If the panel state does not exist, use + * this as the panel state. + * @return The panel state. + */ + private PanelFeatureState getPanelState(int featureId, boolean required, + PanelFeatureState convertPanelState) { + if ((getFeatures() & (1 << featureId)) == 0) { + if (!required) { + return null; + } + throw new RuntimeException("The feature has not been requested"); + } + + PanelFeatureState[] ar; + if ((ar = mPanels) == null || ar.length <= featureId) { + PanelFeatureState[] nar = new PanelFeatureState[featureId + 1]; + if (ar != null) { + System.arraycopy(ar, 0, nar, 0, ar.length); + } + mPanels = ar = nar; + } + + PanelFeatureState st = ar[featureId]; + if (st == null) { + ar[featureId] = st = (convertPanelState != null) + ? convertPanelState + : new PanelFeatureState(featureId); + } + return st; + } + + @Override + public final void setChildDrawable(int featureId, Drawable drawable) { + DrawableFeatureState st = getDrawableState(featureId, true); + st.child = drawable; + updateDrawable(featureId, st, false); + } + + @Override + public final void setChildInt(int featureId, int value) { + updateInt(featureId, value, false); + } + + @Override + public boolean isShortcutKey(int keyCode, KeyEvent event) { + PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, true); + return st.menu != null && st.menu.isShortcutKey(keyCode, event); + } + + private void updateDrawable(int featureId, DrawableFeatureState st, boolean fromResume) { + // Do nothing if the decor is not yet installed... an update will + // need to be forced when we eventually become active. + if (mContentParent == null) { + return; + } + + final int featureMask = 1 << featureId; + + if ((getFeatures() & featureMask) == 0 && !fromResume) { + return; + } + + Drawable drawable = null; + if (st != null) { + drawable = st.child; + if (drawable == null) + drawable = st.local; + if (drawable == null) + drawable = st.def; + } + if ((getLocalFeatures() & featureMask) == 0) { + if (getContainer() != null) { + if (isActive() || fromResume) { + getContainer().setChildDrawable(featureId, drawable); + } + } + } else if (st != null && (st.cur != drawable || st.curAlpha != st.alpha)) { + // System.out.println("Drawable changed: old=" + st.cur + // + ", new=" + drawable); + st.cur = drawable; + st.curAlpha = st.alpha; + onDrawableChanged(featureId, drawable, st.alpha); + } + } + + private void updateInt(int featureId, int value, boolean fromResume) { + + // Do nothing if the decor is not yet installed... an update will + // need to be forced when we eventually become active. + if (mContentParent == null) { + return; + } + + final int featureMask = 1 << featureId; + + if ((getFeatures() & featureMask) == 0 && !fromResume) { + return; + } + + if ((getLocalFeatures() & featureMask) == 0) { + if (getContainer() != null) { + getContainer().setChildInt(featureId, value); + } + } else { + onIntChanged(featureId, value); + } + } + + private ImageView getLeftIconView() { + if (mLeftIconView != null) { + return mLeftIconView; + } + if (mContentParent == null) { + installDecor(); + } + return (mLeftIconView = (ImageView)findViewById(com.android.internal.R.id.left_icon)); + } + + private ProgressBar getCircularProgressBar(boolean shouldInstallDecor) { + if (mCircularProgressBar != null) { + return mCircularProgressBar; + } + if (mContentParent == null && shouldInstallDecor) { + installDecor(); + } + mCircularProgressBar = (ProgressBar)findViewById(com.android.internal.R.id.progress_circular); + mCircularProgressBar.setVisibility(View.INVISIBLE); + return mCircularProgressBar; + } + + private ProgressBar getHorizontalProgressBar(boolean shouldInstallDecor) { + if (mHorizontalProgressBar != null) { + return mHorizontalProgressBar; + } + if (mContentParent == null && shouldInstallDecor) { + installDecor(); + } + mHorizontalProgressBar = (ProgressBar)findViewById(com.android.internal.R.id.progress_horizontal); + mHorizontalProgressBar.setVisibility(View.INVISIBLE); + return mHorizontalProgressBar; + } + + private ImageView getRightIconView() { + if (mRightIconView != null) { + return mRightIconView; + } + if (mContentParent == null) { + installDecor(); + } + return (mRightIconView = (ImageView)findViewById(com.android.internal.R.id.right_icon)); + } + + /** + * Helper method for calling the {@link Callback#onPanelClosed(int, Menu)} + * callback. This method will grab whatever extra state is needed for the + * callback that isn't given in the parameters. If the panel is not open, + * this will not perform the callback. + * + * @param featureId Feature ID of the panel that was closed. Must be given. + * @param panel Panel that was closed. Optional but useful if there is no + * menu given. + * @param menu The menu that was closed. Optional, but give if you have. + */ + private void callOnPanelClosed(int featureId, PanelFeatureState panel, Menu menu) { + final Callback cb = getCallback(); + if (cb == null) + return; + + // Try to get a menu + if (menu == null) { + // Need a panel to grab the menu, so try to get that + if (panel == null) { + if ((featureId >= 0) && (featureId < mPanels.length)) { + panel = mPanels[featureId]; + } + } + + if (panel != null) { + // menu still may be null, which is okay--we tried our best + menu = panel.menu; + } + } + + // If the panel is not open, do not callback + if ((panel != null) && (!panel.isOpen)) + return; + + cb.onPanelClosed(featureId, menu); + } + + /** + * Helper method for adding launch-search to most applications. Opens the + * search window using default settings. + * + * @return true if search window opened + */ + private boolean launchDefaultSearch() { + final Callback cb = getCallback(); + if (cb == null) { + return false; + } else { + return cb.onSearchRequested(); + } + } + + @Override + public void setVolumeControlStream(int streamType) { + mVolumeControlStreamType = streamType; + } + + @Override + public int getVolumeControlStream() { + return mVolumeControlStreamType; + } + + private static final class DrawableFeatureState { + DrawableFeatureState(int _featureId) { + featureId = _featureId; + } + + final int featureId; + + int resid; + + Uri uri; + + Drawable local; + + Drawable child; + + Drawable def; + + Drawable cur; + + int alpha = 255; + + int curAlpha = 255; + } + + private static final class PanelFeatureState { + + /** Feature ID for this panel. */ + int featureId; + + // Information pulled from the style for this panel. + + int background; + + /** The background when the panel spans the entire available width. */ + int fullBackground; + + int gravity; + + int x; + + int y; + + int windowAnimations; + + /** Dynamic state of the panel. */ + DecorView decorView; + + /** The panel that was returned by onCreatePanelView(). */ + View createdPanelView; + + /** The panel that we are actually showing. */ + View shownPanelView; + + /** Use {@link #setMenu} to set this. */ + Menu menu; + + /** + * Whether the panel has been prepared (see + * {@link PhoneWindow#preparePanel}). + */ + boolean isPrepared; + + /** + * Whether an item's action has been performed. This happens in obvious + * scenarios (user clicks on menu item), but can also happen with + * chording menu+(shortcut key). + */ + boolean isHandled; + + boolean isOpen; + + /** + * True if the menu is in expanded mode, false if the menu is in icon + * mode + */ + boolean isInExpandedMode; + + public boolean qwertyMode; + + boolean refreshDecorView; + + /** + * Contains the state of the menu when told to freeze. + */ + Bundle frozenMenuState; + + PanelFeatureState(int featureId) { + this.featureId = featureId; + + refreshDecorView = false; + } + + void setStyle(Context context) { + TypedArray a = context.obtainStyledAttributes(com.android.internal.R.styleable.Theme); + background = a.getResourceId( + com.android.internal.R.styleable.Theme_panelBackground, 0); + fullBackground = a.getResourceId( + com.android.internal.R.styleable.Theme_panelFullBackground, 0); + windowAnimations = a.getResourceId( + com.android.internal.R.styleable.Theme_windowAnimationStyle, 0); + a.recycle(); + } + + void setMenu(Menu menu) { + this.menu = menu; + + if (frozenMenuState != null) { + ((MenuBuilder) menu).restoreHierarchyState(frozenMenuState); + frozenMenuState = null; + } + } + + Parcelable onSaveInstanceState() { + SavedState savedState = new SavedState(); + savedState.featureId = featureId; + savedState.isOpen = isOpen; + savedState.isInExpandedMode = isInExpandedMode; + + if (menu != null) { + savedState.menuState = new Bundle(); + ((MenuBuilder) menu).saveHierarchyState(savedState.menuState); + } + + return savedState; + } + + void onRestoreInstanceState(Parcelable state) { + SavedState savedState = (SavedState) state; + featureId = savedState.featureId; + isOpen = savedState.isOpen; + isInExpandedMode = savedState.isInExpandedMode; + frozenMenuState = savedState.menuState; + + /* + * A LocalActivityManager keeps the same instance of this class around. + * The first time the menu is being shown after restoring, the + * Activity.onCreateOptionsMenu should be called. But, if it is the + * same instance then menu != null and we won't call that method. + * So, clear this. Also clear any cached views. + */ + menu = null; + createdPanelView = null; + shownPanelView = null; + decorView = null; + } + + private static class SavedState implements Parcelable { + int featureId; + boolean isOpen; + boolean isInExpandedMode; + Bundle menuState; + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(featureId); + dest.writeInt(isOpen ? 1 : 0); + dest.writeInt(isInExpandedMode ? 1 : 0); + + if (isOpen) { + dest.writeBundle(menuState); + } + } + + private static SavedState readFromParcel(Parcel source) { + SavedState savedState = new SavedState(); + savedState.featureId = source.readInt(); + savedState.isOpen = source.readInt() == 1; + savedState.isInExpandedMode = source.readInt() == 1; + + if (savedState.isOpen) { + savedState.menuState = source.readBundle(); + } + + return savedState; + } + + public static final Parcelable.Creator<SavedState> CREATOR + = new Parcelable.Creator<SavedState>() { + public SavedState createFromParcel(Parcel in) { + return readFromParcel(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + } + + /** + * Simple implementation of MenuBuilder.Callback that: + * <li> Opens a submenu when selected. + * <li> Calls back to the callback's onMenuItemSelected when an item is + * selected. + */ + private final class ContextMenuCallback implements MenuBuilder.Callback { + private int mFeatureId; + + public ContextMenuCallback(int featureId) { + mFeatureId = featureId; + } + + public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { + if (allMenusAreClosing) { + Callback callback = getCallback(); + if (callback != null) callback.onPanelClosed(mFeatureId, menu); + + if (menu == mContextMenu) { + closeContextMenu(); + } + } + } + + public void onCloseSubMenu(SubMenuBuilder menu) { + Callback callback = getCallback(); + if (callback != null) callback.onPanelClosed(mFeatureId, menu.getRootMenu()); + } + + public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) { + Callback callback = getCallback(); + return (callback != null) && callback.onMenuItemSelected(mFeatureId, item); + + } + + public void onMenuModeChange(MenuBuilder menu) { + } + + public boolean onSubMenuSelected(SubMenuBuilder subMenu) { + // Set a simple callback for the submenu + subMenu.setCallback(this); + + // The window manager will give us a valid window token + new MenuDialogHelper(subMenu).show(null); + + return true; + } + } +} diff --git a/phone/com/android/internal/policy/impl/PhoneWindowManager.java b/phone/com/android/internal/policy/impl/PhoneWindowManager.java new file mode 100644 index 0000000..250d2d4 --- /dev/null +++ b/phone/com/android/internal/policy/impl/PhoneWindowManager.java @@ -0,0 +1,1409 @@ +/* + * Copyright (C) 2006 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.app.Activity; +import android.app.ActivityManagerNative; +import android.app.IActivityManager; +import android.app.IStatusBar; +import android.content.BroadcastReceiver; +import android.content.ContentQueryMap; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.database.Cursor; +import android.graphics.Rect; +import android.os.Handler; +import android.os.IBinder; +import android.os.LocalPowerManager; +import android.os.PowerManager; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemClock; +import android.os.SystemProperties; +import android.provider.Settings; +import static android.provider.Settings.System.END_BUTTON_BEHAVIOR; + +import com.android.internal.policy.PolicyManager; +import com.android.internal.telephony.ITelephony; +import android.util.Config; +import android.util.EventLog; +import android.util.Log; +import android.view.IWindowManager; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.OrientationListener; +import android.view.RawInputEvent; +import android.view.Surface; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.Window; +import android.view.WindowManager; +import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW; +import static android.view.WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN; +import static android.view.WindowManager.LayoutParams.FLAG_FULLSCREEN; +import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; +import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR; +import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; +import static android.view.WindowManager.LayoutParams.LAST_APPLICATION_WINDOW; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL; +import static android.view.WindowManager.LayoutParams.TYPE_KEYGUARD; +import static android.view.WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG; +import static android.view.WindowManager.LayoutParams.TYPE_PHONE; +import static android.view.WindowManager.LayoutParams.TYPE_PRIORITY_PHONE; +import static android.view.WindowManager.LayoutParams.TYPE_SEARCH_BAR; +import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR; +import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL; +import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_ALERT; +import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_ERROR; +import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY; +import static android.view.WindowManager.LayoutParams.TYPE_TOAST; +import android.view.WindowManagerImpl; +import android.view.WindowManagerPolicy; +import android.media.IAudioService; +import android.media.AudioManager; + +import java.util.Observable; +import java.util.Observer; + +/** + * WindowManagerPolicy implementation for the Android phone UI. + */ +public class PhoneWindowManager implements WindowManagerPolicy { + private static final String TAG = "WindowManager"; + private static final boolean DEBUG = false; + private static final boolean localLOGV = DEBUG ? Config.LOGD : Config.LOGV; + private static final boolean SHOW_STARTING_ANIMATIONS = true; + private static final boolean SHOW_PROCESSES_ON_ALT_MENU = false; + + private static final int APPLICATION_LAYER = 1; + private static final int PHONE_LAYER = 2; + private static final int SEARCH_BAR_LAYER = 3; + private static final int STATUS_BAR_PANEL_LAYER = 4; + // toasts and the plugged-in battery thing + private static final int TOAST_LAYER = 5; + private static final int STATUS_BAR_LAYER = 6; + // SIM errors and unlock. Not sure if this really should be in a high layer. + private static final int PRIORITY_PHONE_LAYER = 7; + // like the ANR / app crashed dialogs + private static final int SYSTEM_ALERT_LAYER = 8; + // system-level error dialogs + private static final int SYSTEM_ERROR_LAYER = 9; + // the keyguard; nothing on top of these can take focus, since they are + // responsible for power management when displayed. + private static final int KEYGUARD_LAYER = 10; + private static final int KEYGUARD_DIALOG_LAYER = 11; + // things in here CAN NOT take focus, but are shown on top of everything else. + private static final int SYSTEM_OVERLAY_LAYER = 12; + + private static final int APPLICATION_PANEL_SUBLAYER = 1; + private static final int APPLICATION_MEDIA_SUBLAYER = -1; + private static final int APPLICATION_SUB_PANEL_SUBLAYER = 2; + + private static final float SLIDE_TOUCH_EVENT_SIZE_LIMIT = 0.6f; + + static public final String SYSTEM_DIALOG_REASON_KEY = "reason"; + static public final String SYSTEM_DIALOG_REASON_GLOBAL_ACTIONS = "globalactions"; + static public final String SYSTEM_DIALOG_REASON_RECENT_APPS = "recentapps"; + + private Context mContext; + private IWindowManager mWindowManager; + private LocalPowerManager mPowerManager; + + /** If true, hitting shift & menu will broadcast Intent.ACTION_BUG_REPORT */ + private boolean mEnableShiftMenuBugReports = false; + + private WindowState mStatusBar = null; + private WindowState mSearchBar = null; + private WindowState mKeyguard = null; + private KeyguardViewMediator mKeyguardMediator; + private GlobalActions mGlobalActions; + private boolean mShouldTurnOffOnKeyUp; + private RecentApplicationsDialog mRecentAppsDialog; + private Handler mHandler; + + private boolean mLidOpen; + private int mSensorOrientation = OrientationListener.ORIENTATION_UNKNOWN; + private int mSensorRotation = -1; + private boolean mScreenOn = false; + private boolean mOrientationSensorEnabled = false; + private int mCurrentAppOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; + + private int mW, mH; + private int mCurLeft, mCurTop, mCurRight, mCurBottom; + private WindowState mTopFullscreenOpaqueWindowState; + private boolean mForceStatusBar; + private boolean mHomePressed; + private Intent mHomeIntent; + private boolean mSearchKeyPressed; + private boolean mConsumeSearchKeyUp; + + private static final int ENDCALL_HOME = 0x1; + private static final int ENDCALL_SLEEPS = 0x2; + private static final int DEFAULT_ENDCALL_BEHAVIOR = ENDCALL_SLEEPS; + private int mEndcallBehavior; + + private ShortcutManager mShortcutManager; + private PowerManager.WakeLock mBroadcastWakeLock; + + private class SettingsObserver implements Observer { + private ContentQueryMap mSettings; + + void observe() { + ContentResolver resolver = mContext.getContentResolver(); + Cursor settingsCursor = resolver.query(Settings.System.CONTENT_URI, null, + Settings.System.NAME + "=?", + new String[] { END_BUTTON_BEHAVIOR}, + null); + mSettings = new ContentQueryMap(settingsCursor, Settings.System.NAME, true, mHandler); + mSettings.addObserver(this); + + // pretend that the settings changed so we will get their initial state + update(mSettings, null); + } + + private int getInt(String name, int def) { + ContentValues row = mSettings.getValues(name); + if (row != null) { + Integer ret = row.getAsInteger(Settings.System.VALUE); + if(ret == null) { + return def; + } + return ret; + } else { + return def; + } + } + + public void update(Observable o, Object arg) { + mEndcallBehavior = getInt(END_BUTTON_BEHAVIOR, DEFAULT_ENDCALL_BEHAVIOR); + } + } + + private class MyOrientationListener extends OrientationListener { + + MyOrientationListener(Context context) { + super(context); + } + + @Override + public void onOrientationChanged(int orientation) { + // ignore orientation changes unless the value is in a range that + // matches portrait or landscape + // portrait range is 270+45 to 359 and 0 to 45 + // landscape range is 270-45 to 270+45 + if ((orientation >= 0 && orientation <= 45) || (orientation >= 270 - 45)) { + mSensorOrientation = orientation; + int rotation = (orientation >= 270 - 45 + && orientation <= 270 + 45) + ? Surface.ROTATION_90 : Surface.ROTATION_0; + if (rotation != mSensorRotation) { + if(localLOGV) Log.i(TAG, "onOrientationChanged, rotation changed from "+rotation+" to "+mSensorRotation); + // Update window manager. The lid rotation hasn't changed, + // but we want it to re-evaluate the final rotation in case + // it needs to call back and get the sensor orientation. + mSensorRotation = rotation; + try { + mWindowManager.setRotation(USE_LAST_ROTATION, false); + } catch (RemoteException e) { + // Ignore + } + } + } + } + } + private MyOrientationListener mOrientationListener; + + /* + * Various use cases for invoking this function + * screen turning off, should always disable listeners if already enabled + * screen turned on and current app has sensor based orientation, enable listeners + * if not already enabled + * screen turned on and current app does not have sensor orientation, disable listeners if + * already enabled + * screen turning on and current app has sensor based orientation, enable listeners if needed + * screen turning on and current app has nosensor based orientation, do nothing + */ + private void updateOrientationListener() { + //Could have been invoked due to screen turning on or off or + //change of the currently visible window's orientation + if(localLOGV) Log.i(TAG, "Screen status="+mScreenOn+ + ", current orientation="+mCurrentAppOrientation+ + ", SensorEnabled="+mOrientationSensorEnabled); + boolean disable = true; + if(mScreenOn) { + if(mCurrentAppOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR) { + disable = false; + //enable listener if not already enabled + if(!mOrientationSensorEnabled) { + mOrientationListener.enable(); + if(localLOGV) Log.i(TAG, "Enabling listeners"); + mOrientationSensorEnabled = true; + } + } + } + //check if sensors need to be disabled + if(disable && mOrientationSensorEnabled) { + mOrientationListener.disable(); + if(localLOGV) Log.i(TAG, "Disabling listeners"); + mOrientationSensorEnabled = false; + } + } + + private Runnable mEndCallLongPress = new Runnable() { + public void run() { + mShouldTurnOffOnKeyUp = false; + sendCloseSystemWindows(SYSTEM_DIALOG_REASON_GLOBAL_ACTIONS); + showGlobalActionsDialog(); + } + }; + + private void showGlobalActionsDialog() { + if (mGlobalActions == null) { + mGlobalActions = new GlobalActions(mContext, mPowerManager); + } + final boolean keyguardShowing = mKeyguardMediator.isShowing(); + mGlobalActions.showDialog(keyguardShowing, isDeviceProvisioned()); + if (keyguardShowing) { + // since it took two seconds of long press to bring this up, + // poke the wake lock so they have some time to see the dialog. + mKeyguardMediator.pokeWakelock(); + } + } + + private boolean isDeviceProvisioned() { + return Settings.System.getInt( + mContext.getContentResolver(), Settings.System.DEVICE_PROVISIONED, 0) != 0; + } + + /** + * When a home-key longpress expires, close other system windows and launch the recent apps + */ + private Runnable mHomeLongPress = new Runnable() { + public void run() { + /* + * Eat the longpress so it won't dismiss the recent apps dialog when + * the user lets go of the home key + */ + mHomePressed = false; + sendCloseSystemWindows(SYSTEM_DIALOG_REASON_RECENT_APPS); + showRecentAppsDialog(); + } + }; + + /** + * Create (if necessary) and launch the recent apps dialog + */ + private void showRecentAppsDialog() { + if (mRecentAppsDialog == null) { + mRecentAppsDialog = new RecentApplicationsDialog(mContext); + } + mRecentAppsDialog.show(); + } + + /** {@inheritDoc} */ + public void init(Context context, IWindowManager windowManager, + LocalPowerManager powerManager) { + mContext = context; + mWindowManager = windowManager; + mPowerManager = powerManager; + mKeyguardMediator = new KeyguardViewMediator(context, this, powerManager); + mHandler = new Handler(); + mOrientationListener = new MyOrientationListener(mContext); + SettingsObserver settingsObserver = new SettingsObserver(); + settingsObserver.observe(); + mShortcutManager = new ShortcutManager(context, mHandler); + mShortcutManager.observe(); + mHomeIntent = new Intent(Intent.ACTION_MAIN, null); + mHomeIntent.addCategory(Intent.CATEGORY_HOME); + mHomeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); + PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE); + mBroadcastWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, + "PhoneWindowManager.mBroadcastWakeLock"); + mEnableShiftMenuBugReports = "1".equals(SystemProperties.get("ro.debuggable")); + } + + /** {@inheritDoc} */ + public int checkAddPermission(WindowManager.LayoutParams attrs) { + int type = attrs.type; + if (type < WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW + || type > WindowManager.LayoutParams.LAST_SYSTEM_WINDOW) { + return WindowManagerImpl.ADD_OKAY; + } + String permission = null; + switch (type) { + case TYPE_TOAST: + // XXX right now the app process has complete control over + // this... should introduce a token to let the system + // monitor/control what they are doing. + break; + case TYPE_PHONE: + case TYPE_PRIORITY_PHONE: + case TYPE_SYSTEM_ALERT: + case TYPE_SYSTEM_ERROR: + case TYPE_SYSTEM_OVERLAY: + permission = android.Manifest.permission.SYSTEM_ALERT_WINDOW; + break; + default: + permission = android.Manifest.permission.INTERNAL_SYSTEM_WINDOW; + } + if (permission != null) { + if (mContext.checkCallingOrSelfPermission(permission) + != PackageManager.PERMISSION_GRANTED) { + return WindowManagerImpl.ADD_PERMISSION_DENIED; + } + } + return WindowManagerImpl.ADD_OKAY; + } + + public void adjustWindowParamsLw(WindowManager.LayoutParams attrs) { + switch (attrs.type) { + case TYPE_SYSTEM_OVERLAY: + case TYPE_TOAST: + // These types of windows can't receive input events. + attrs.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; + break; + } + } + + private void readLidState() { + try { + int sw = mWindowManager.getSwitchState(0); + if (sw >= 0) { + mLidOpen = sw == 0; + } + } catch (RemoteException e) { + // Ignore + } + } + + /** {@inheritDoc} */ + public void adjustConfigurationLw(Configuration config) { + readLidState(); + mPowerManager.setKeyboardVisibility(mLidOpen); + config.keyboardHidden = mLidOpen + ? Configuration.KEYBOARDHIDDEN_NO + : Configuration.KEYBOARDHIDDEN_YES; + if (keyguardIsShowingTq()) { + if (mLidOpen) { + // only do this if it's opening -- closing the device shouldn't turn it + // off, but it also shouldn't turn it on. + mKeyguardMediator.pokeWakelock(); + } + } else { + mPowerManager.userActivity(SystemClock.uptimeMillis(), false, + LocalPowerManager.OTHER_EVENT); + } + } + + public boolean isCheekPressedAgainstScreen(MotionEvent ev) { + if(ev.getSize() > SLIDE_TOUCH_EVENT_SIZE_LIMIT) { + return true; + } + int size = ev.getHistorySize(); + for(int i = 0; i < size; i++) { + if(ev.getHistoricalSize(i) > SLIDE_TOUCH_EVENT_SIZE_LIMIT) { + return true; + } + } + return false; + } + + /** {@inheritDoc} */ + public int windowTypeToLayerLw(int type) { + if (type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) { + return APPLICATION_LAYER; + } + switch (type) { + case TYPE_APPLICATION_PANEL: + return APPLICATION_LAYER; + case TYPE_APPLICATION_SUB_PANEL: + return APPLICATION_LAYER; + case TYPE_STATUS_BAR: + return STATUS_BAR_LAYER; + case TYPE_STATUS_BAR_PANEL: + return STATUS_BAR_PANEL_LAYER; + case TYPE_SEARCH_BAR: + return SEARCH_BAR_LAYER; + case TYPE_PHONE: + return PHONE_LAYER; + case TYPE_KEYGUARD: + return KEYGUARD_LAYER; + case TYPE_KEYGUARD_DIALOG: + return KEYGUARD_DIALOG_LAYER; + case TYPE_SYSTEM_ALERT: + return SYSTEM_ALERT_LAYER; + case TYPE_SYSTEM_ERROR: + return SYSTEM_ERROR_LAYER; + case TYPE_SYSTEM_OVERLAY: + return SYSTEM_OVERLAY_LAYER; + case TYPE_PRIORITY_PHONE: + return PRIORITY_PHONE_LAYER; + case TYPE_TOAST: + return TOAST_LAYER; + } + Log.e(TAG, "Unknown window type: " + type); + return APPLICATION_LAYER; + } + + /** {@inheritDoc} */ + public int subWindowTypeToLayerLw(int type) { + switch (type) { + case TYPE_APPLICATION_PANEL: + return APPLICATION_PANEL_SUBLAYER; + case TYPE_APPLICATION_MEDIA: + return APPLICATION_MEDIA_SUBLAYER; + case TYPE_APPLICATION_SUB_PANEL: + return APPLICATION_SUB_PANEL_SUBLAYER; + } + Log.e(TAG, "Unknown sub-window type: " + type); + return 0; + } + + /** {@inheritDoc} */ + public View addStartingWindow(IBinder appToken, String packageName, + int theme, CharSequence nonLocalizedLabel, + int labelRes, int icon) { + if (!SHOW_STARTING_ANIMATIONS) { + return null; + } + if (packageName == null) { + return null; + } + + Context context = mContext; + boolean setTheme = false; + //Log.i(TAG, "addStartingWindow " + packageName + ": nonLocalizedLabel=" + // + nonLocalizedLabel + " theme=" + Integer.toHexString(theme)); + if (theme != 0 || labelRes != 0) { + try { + context = context.createPackageContext(packageName, 0); + if (theme != 0) { + context.setTheme(theme); + setTheme = true; + } + } catch (PackageManager.NameNotFoundException e) { + // Ignore + } + } + if (!setTheme) { + context.setTheme(com.android.internal.R.style.Theme); + } + + Window win = PolicyManager.makeNewWindow(context); + Resources r = context.getResources(); + win.setTitle(r.getText(labelRes, nonLocalizedLabel)); + + win.setType( + WindowManager.LayoutParams.TYPE_APPLICATION_STARTING); + win.setFlags( + WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE| + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE| + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); + + win.setLayout(WindowManager.LayoutParams.FILL_PARENT, + WindowManager.LayoutParams.FILL_PARENT); + + final WindowManager.LayoutParams params = win.getAttributes(); + params.token = appToken; + params.packageName = packageName; + params.windowAnimations = win.getWindowStyle().getResourceId( + com.android.internal.R.styleable.Window_windowAnimationStyle, 0); + params.setTitle("Starting " + packageName); + + try { + WindowManagerImpl wm = (WindowManagerImpl) + context.getSystemService(Context.WINDOW_SERVICE); + View view = win.getDecorView(); + + if (win.isFloating()) { + // Whoops, there is no way to display an animation/preview + // of such a thing! After all that work... let's skip it. + // (Note that we must do this here because it is in + // getDecorView() where the theme is evaluated... maybe + // we should peek the floating attribute from the theme + // earlier.) + return null; + } + + if (localLOGV) Log.v( + TAG, "Adding starting window for " + packageName + + " / " + appToken + ": " + + (view.getParent() != null ? view : null)); + + wm.addView(view, params); + + // Only return the view if it was successfully added to the + // window manager... which we can tell by it having a parent. + return view.getParent() != null ? view : null; + } catch (WindowManagerImpl.BadTokenException e) { + // ignore + Log.w(TAG, appToken + " already running, starting window not displayed"); + } + + return null; + } + + /** {@inheritDoc} */ + public void removeStartingWindow(IBinder appToken, View window) { + // RuntimeException e = new RuntimeException(); + // Log.i(TAG, "remove " + appToken + " " + window, e); + + if (localLOGV) Log.v( + TAG, "Removing starting window for " + appToken + ": " + window); + + if (window != null) { + WindowManagerImpl wm = (WindowManagerImpl) mContext.getSystemService(Context.WINDOW_SERVICE); + wm.removeView(window); + } + } + + /** + * Preflight adding a window to the system. + * + * Currently enforces that three window types are singletons: + * <ul> + * <li>STATUS_BAR_TYPE</li> + * <li>SEARCH_BAR_TYPE</li> + * <li>KEYGUARD_TYPE</li> + * </ul> + * + * @param win The window to be added + * @param attrs Information about the window to be added + * + * @return If ok, WindowManagerImpl.ADD_OKAY. If too many singletons, WindowManagerImpl.ADD_MULTIPLE_SINGLETON + */ + public int prepareAddWindowLw(WindowState win, WindowManager.LayoutParams attrs) { + switch (attrs.type) { + case TYPE_STATUS_BAR: + if (mStatusBar != null) { + return WindowManagerImpl.ADD_MULTIPLE_SINGLETON; + } + mStatusBar = win; + break; + case TYPE_SEARCH_BAR: + if (mSearchBar != null) { + return WindowManagerImpl.ADD_MULTIPLE_SINGLETON; + } + mSearchBar = win; + break; + case TYPE_KEYGUARD: + if (mKeyguard != null) { + return WindowManagerImpl.ADD_MULTIPLE_SINGLETON; + } + mKeyguard = win; + break; + } + return WindowManagerImpl.ADD_OKAY; + } + + /** {@inheritDoc} */ + public void removeWindowLw(WindowState win) { + if (mStatusBar == win) { + mStatusBar = null; + } + else if (mSearchBar == win) { + mSearchBar = null; + } + else if (mKeyguard == win) { + mKeyguard = null; + } + } + + private static final boolean PRINT_ANIM = false; + + /** {@inheritDoc} */ + public int selectAnimationLw(WindowState win, int transit) { + if (PRINT_ANIM) Log.i(TAG, "selectAnimation in " + win + + ": transit=" + transit); + if (transit == TRANSIT_PREVIEW_DONE) { + if (win.hasAppShownWindows()) { + if (PRINT_ANIM) Log.i(TAG, "**** STARTING EXIT"); + return com.android.internal.R.anim.app_starting_exit; + } + } + + return 0; + } + + private static ITelephony getPhoneInterface() { + return ITelephony.Stub.asInterface(ServiceManager.checkService(Context.TELEPHONY_SERVICE)); + } + + private static IAudioService getAudioInterface() { + return IAudioService.Stub.asInterface(ServiceManager.checkService(Context.AUDIO_SERVICE)); + } + + private boolean keyguardOn() { + return keyguardIsShowingTq() || inKeyguardRestrictedKeyInputMode(); + } + + /** {@inheritDoc} */ + public boolean interceptKeyTi(WindowState win, int code, int metaKeys, boolean down, + int repeatCount) { + boolean keyguardOn = keyguardOn(); + + if (false) { + Log.d(TAG, "interceptKeyTi code=" + code + " down=" + down + " repeatCount=" + + repeatCount + " keyguardOn=" + keyguardOn); + } + + // Clear a pending HOME longpress if the user releases Home + // TODO: This could probably be inside the next bit of logic, but that code + // turned out to be a bit fragile so I'm doing it here explicitly, for now. + if ((code == KeyEvent.KEYCODE_HOME) && !down) { + mHandler.removeCallbacks(mHomeLongPress); + } + + // If the HOME button is currently being held, then we do special + // chording with it. + if (mHomePressed) { + + // If we have released the home key, and didn't do anything else + // while it was pressed, then it is time to go home! + if (code == KeyEvent.KEYCODE_HOME) { + if (!down) { + mHomePressed = false; + + // If an incoming call is ringing, HOME is totally disabled. + // (The user is already on the InCallScreen at this point, + // and his ONLY options are to answer or reject the call.) + boolean incomingRinging = false; + try { + ITelephony phoneServ = getPhoneInterface(); + if (phoneServ != null) { + incomingRinging = phoneServ.isRinging(); + } else { + Log.w(TAG, "Unable to find ITelephony interface"); + } + } catch (RemoteException ex) { + Log.w(TAG, "RemoteException from getPhoneInterface()", ex); + } + + if (incomingRinging) { + Log.i(TAG, "Ignoring HOME; there's a ringing incoming call."); + } else { + launchHomeFromHotKey(); + } + } + } + + return true; + } + + // First we always handle the home key here, so applications + // can never break it, although if keyguard is on, we do let + // it handle it, because that gives us the correct 5 second + // timeout. + if (code == KeyEvent.KEYCODE_HOME) { + + // If a system window has focus, then it doesn't make sense + // right now to interact with applications. + WindowManager.LayoutParams attrs = win != null ? win.getAttrs() : null; + if (attrs != null) { + int type = attrs.type; + if (type >= WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW + && type <= WindowManager.LayoutParams.LAST_SYSTEM_WINDOW) { + // Only do this once, so home-key-longpress doesn't close itself + if (repeatCount == 0 && down) { + sendCloseSystemWindows(); + } + return false; + } + } + + if (down && repeatCount == 0) { + if (!keyguardOn) { + mHandler.postDelayed(mHomeLongPress, ViewConfiguration.getGlobalActionKeyTimeout()); + } + mHomePressed = true; + } + return true; + } else if (code == KeyEvent.KEYCODE_MENU) { + // Hijack modified menu keys for debugging features + final int chordBug = KeyEvent.META_SHIFT_ON; + + if (down && repeatCount == 0) { + if (mEnableShiftMenuBugReports && (metaKeys & chordBug) == chordBug) { + Intent intent = new Intent(Intent.ACTION_BUG_REPORT); + mContext.sendOrderedBroadcast(intent, null); + return true; + } else if (SHOW_PROCESSES_ON_ALT_MENU && + (metaKeys & KeyEvent.META_ALT_ON) == KeyEvent.META_ALT_ON) { + Intent service = new Intent(); + service.setClassName(mContext, "com.android.server.LoadAverageService"); + ContentResolver res = mContext.getContentResolver(); + boolean shown = Settings.System.getInt( + res, Settings.System.SHOW_PROCESSES, 0) != 0; + if (!shown) { + mContext.startService(service); + } else { + mContext.stopService(service); + } + Settings.System.putInt( + res, Settings.System.SHOW_PROCESSES, shown ? 0 : 1); + return true; + } + } + } else if (code == KeyEvent.KEYCODE_NOTIFICATION) { + if (down) { + // this key doesn't exist on current hardware, but if a device + // didn't have a touchscreen, it would want one of these to open + // the status bar. + IStatusBar sbs = IStatusBar.Stub.asInterface(ServiceManager.getService("statusbar")); + if (sbs != null) { + try { + sbs.toggle(); + } catch (RemoteException e) { + // we're screwed anyway, since it's in this process + throw new RuntimeException(e); + } + } + } + return true; + } else if (code == KeyEvent.KEYCODE_SEARCH) { + if (down) { + if (repeatCount == 0) { + mSearchKeyPressed = true; + } + } else { + mSearchKeyPressed = false; + + if (mConsumeSearchKeyUp) { + // Consume the up-event + mConsumeSearchKeyUp = false; + return true; + } + } + } + + // Shortcuts are invoked through Search+key, so intercept those here + if (mSearchKeyPressed) { + if (down && repeatCount == 0 && !keyguardOn) { + Intent shortcutIntent = mShortcutManager.getIntent(code, metaKeys); + if (shortcutIntent != null) { + shortcutIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mContext.startActivity(shortcutIntent); + + /* + * We launched an app, so the up-event of the search key + * should be consumed + */ + mConsumeSearchKeyUp = true; + return true; + } + } + } + + return false; + } + + /** + * A home key -> launch home action was detected. Take the appropriate action + * given the situation with the keyguard. + */ + private void launchHomeFromHotKey() { + if (mKeyguardMediator.isShowing()) { + // don't launch home if keyguard showing + } else if (mKeyguardMediator.isInputRestricted()) { + // when in keyguard restricted mode, must first verify unlock + // before launching home + mKeyguardMediator.verifyUnlock(new OnKeyguardExitResult() { + public void onKeyguardExitResult(boolean success) { + if (success) { + mContext.startActivity(mHomeIntent); + sendCloseSystemWindows(); + } + } + }); + } else { + // no keyguard stuff to worry about, just launch home! + mContext.startActivity(mHomeIntent); + sendCloseSystemWindows(); + } + } + + public void getCoveredInsetHintLw(WindowManager.LayoutParams attrs, Rect coveredInset) { + final int fl = attrs.flags; + + if ((fl & + (FLAG_LAYOUT_IN_SCREEN | FLAG_FULLSCREEN | FLAG_LAYOUT_INSET_DECOR)) + == (FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR)) { + coveredInset.set(mCurLeft, mCurTop, mW - mCurRight, mH - mCurBottom); + } else { + coveredInset.setEmpty(); + } + } + + /** {@inheritDoc} */ + public void beginLayoutLw(int displayWidth, int displayHeight) { + mW = displayWidth; + mH = displayHeight; + mCurLeft = 0; + mCurTop = 0; + mCurRight = displayWidth; + mCurBottom = displayHeight; + + // decide where the status bar goes ahead of time + if (mStatusBar != null) { + mStatusBar.computeFrameLw(0, 0, displayWidth, displayHeight, + 0, 0, displayWidth, displayHeight); + mCurTop = mStatusBar.getFrameLw().bottom; + } + } + + /** {@inheritDoc} */ + public void layoutWindowLw(WindowState win, WindowManager.LayoutParams attrs, WindowState attached) { + // we've already done the status bar + if (win == mStatusBar) { + return; + } + + final int fl = attrs.flags; + + int dl, dt, dr, db; + if ((fl & FLAG_LAYOUT_IN_SCREEN) == 0) { + // Make sure this window doesn't intrude into the status bar. + dl = mCurLeft; + dt = mCurTop; + dr = mCurRight; + db = mCurBottom; + } else { + dl = 0; + dt = 0; + dr = mW; + db = mH; + } + + if ((fl & + (FLAG_LAYOUT_IN_SCREEN | FLAG_FULLSCREEN | FLAG_LAYOUT_INSET_DECOR)) + == (FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR)) { + win.setCoveredInsetsLw(mCurLeft, mCurTop, mW - mCurRight, mH - mCurBottom); + } else { + win.setCoveredInsetsLw(0, 0, 0, 0); + } + + int pl, pt, pr, pb; + if (attached != null && (fl & (FLAG_LAYOUT_IN_SCREEN)) == 0) { + final Rect r = attached.getFrameLw(); + pl = r.left; + pt = r.top; + pr = r.right; + pb = r.bottom; + } else { + pl = dl; + pt = dt; + pr = dr; + pb = db; + } + + if ((fl & FLAG_LAYOUT_NO_LIMITS) != 0) { + dl = -100000; + dt = -100000; + dr = 100000; + db = 100000; + } + + win.computeFrameLw(pl, pt, pr, pb, dl, dt, dr, db); + } + + /** {@inheritDoc} */ + public void finishLayoutLw() { + } + + /** {@inheritDoc} */ + public void beginAnimationLw(int displayWidth, int displayHeight) { + mTopFullscreenOpaqueWindowState = null; + mForceStatusBar = false; + } + + /** {@inheritDoc} */ + public void animatingWindowLw(WindowState win, + WindowManager.LayoutParams attrs) { + if (mTopFullscreenOpaqueWindowState == null + && attrs.type >= FIRST_APPLICATION_WINDOW + && attrs.type <= LAST_APPLICATION_WINDOW + && win.fillsScreenLw(mW, mH, true) + && win.isDisplayedLw()) { + mTopFullscreenOpaqueWindowState = win; + } else if ((attrs.flags & FLAG_FORCE_NOT_FULLSCREEN) != 0) { + mForceStatusBar = true; + } + } + + /** {@inheritDoc} */ + public boolean finishAnimationLw() { + if (mStatusBar != null) { + if (mForceStatusBar) { + mStatusBar.showLw(); + } else if (mTopFullscreenOpaqueWindowState != null) { + WindowManager.LayoutParams lp = + mTopFullscreenOpaqueWindowState.getAttrs(); + boolean hideStatusBar = + (lp.flags & WindowManager.LayoutParams.FLAG_FULLSCREEN) != 0; + if (hideStatusBar) { + mStatusBar.hideLw(); + } else { + mStatusBar.showLw(); + } + } + } + return false; + } + + /** {@inheritDoc} */ + public boolean preprocessInputEventTq(RawInputEvent event) { + switch (event.type) { + case RawInputEvent.EV_SW: + if (event.keycode == 0) { + // lid changed state + mLidOpen = event.value == 0; + updateRotation(); + } + } + return false; + } + + + /** {@inheritDoc} */ + public boolean isAppSwitchKeyTqTiLwLi(int keycode) { + return keycode == KeyEvent.KEYCODE_HOME + || keycode == KeyEvent.KEYCODE_ENDCALL; + } + + /** {@inheritDoc} */ + public boolean isMovementKeyTi(int keycode) { + switch (keycode) { + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_DPAD_DOWN: + case KeyEvent.KEYCODE_DPAD_LEFT: + case KeyEvent.KEYCODE_DPAD_RIGHT: + return true; + } + return false; + } + + + /** + * @return Whether a telephone call is in progress right now. + */ + private boolean isInCall() { + final ITelephony phone = getPhoneInterface(); + if (phone == null) { + Log.w(TAG, "couldn't get ITelephony reference"); + return false; + } + try { + return phone.isOffhook(); + } catch (RemoteException e) { + Log.w(TAG, "ITelephony.isOffhhook threw RemoteException " + e); + return false; + } + } + + /** + * @return Whether music is being played right now. + */ + private boolean isMusicActive() { + final IAudioService audio = getAudioInterface(); + if (audio == null) { + Log.w(TAG, "isMusicActive: couldn't get IAudioService reference"); + return false; + } + try { + return audio.isMusicActive(); + } catch (RemoteException e) { + Log.w(TAG, "IAudioService.isMusicActive() threw RemoteException " + e); + return false; + } + } + + /** + * Tell the audio service to adjust the volume appropriate to the event. + * @param keycode + */ + private void sendVolToMusic(int keycode) { + final IAudioService audio = getAudioInterface(); + if (audio == null) { + Log.w(TAG, "sendVolToMusic: couldn't get IAudioService reference"); + return; + } + try { + // since audio is playing, we shouldn't have to hold a wake lock + // during the call, but we do it as a precaution for the rare possibility + // that the music stops right before we call this + mBroadcastWakeLock.acquire(); + audio.adjustStreamVolume( + AudioManager.STREAM_MUSIC, + keycode == KeyEvent.KEYCODE_VOLUME_UP + ? AudioManager.ADJUST_RAISE + : AudioManager.ADJUST_LOWER, + 0); + } catch (RemoteException e) { + Log.w(TAG, "IAudioService.adjustStreamVolume() threw RemoteException " + e); + } finally { + mBroadcastWakeLock.release(); + } + } + + /** {@inheritDoc} */ + public int interceptKeyTq(RawInputEvent event, boolean screenIsOn) { + int result = ACTION_PASS_TO_USER; + final boolean isWakeKey = isWakeKeyTq(event); + final boolean keyguardShowing = keyguardIsShowingTq(); + + if (keyguardShowing) { + if (screenIsOn) { + // when the screen is on, always give the event to the keyguard + result |= ACTION_PASS_TO_USER; + } else { + // otherwise, don't pass it to the user + result &= ~ACTION_PASS_TO_USER; + + final boolean isKeyDown = + (event.type == RawInputEvent.EV_KEY) && (event.value != 0); + if (isWakeKey && isKeyDown) { + + // tell the mediator about a wake key, it may decide to + // turn on the screen depending on whether the key is + // appropriate. + if (!mKeyguardMediator.onWakeKeyWhenKeyguardShowingTq(event.keycode) + && (event.keycode == KeyEvent.KEYCODE_VOLUME_DOWN + || event.keycode == KeyEvent.KEYCODE_VOLUME_UP)) { + if (isInCall()) { + // if the keyguard didn't wake the device, we are in call, and + // it is a volume key, turn on the screen so that the user + // can more easily adjust the in call volume. + mKeyguardMediator.pokeWakelock(); + } else if (isMusicActive()) { + // when keyguard is showing and screen off, we need + // to handle the volume key for music here + sendVolToMusic(event.keycode); + } + } + } + } + } else if (!screenIsOn) { + if (isWakeKey) { + // a wake key has a sole purpose of waking the device; don't pass + // it to the user + result |= ACTION_POKE_USER_ACTIVITY; + result &= ~ACTION_PASS_TO_USER; + } + } + + int type = event.type; + int code = event.keycode; + boolean down = event.value != 0; + + if (type == RawInputEvent.EV_KEY) { + if (code == KeyEvent.KEYCODE_ENDCALL) { + if (down) { + boolean hungUp = false; + // key repeats are generated by the window manager, and we don't see them + // here, so unless the driver is doing something it shouldn't be, we know + // this is the real press event. + try { + ITelephony phoneServ = getPhoneInterface(); + if (phoneServ != null) { + hungUp = phoneServ.endCall(); + } else { + Log.w(TAG, "!!! Unable to find ITelephony interface !!!"); + } + } catch (RemoteException ex) { + Log.w(TAG, "ITelephony.endCall() threw RemoteException" + ex); + } + if (hungUp || !screenIsOn) { + mShouldTurnOffOnKeyUp = false; + } else { + // only try to turn off the screen if we didn't already hang up + mShouldTurnOffOnKeyUp = true; + mHandler.postDelayed(mEndCallLongPress, + ViewConfiguration.getGlobalActionKeyTimeout()); + result &= ~ACTION_PASS_TO_USER; + } + } else { + mHandler.removeCallbacks(mEndCallLongPress); + if (mShouldTurnOffOnKeyUp) { + mShouldTurnOffOnKeyUp = false; + boolean gohome = (mEndcallBehavior & ENDCALL_HOME) != 0; + boolean sleeps = (mEndcallBehavior & ENDCALL_SLEEPS) != 0; + if (keyguardShowing + || (sleeps && !gohome) + || (gohome && !goHome() && sleeps)) { + // they must already be on the keyguad or home screen, + // go to sleep instead + Log.d(TAG, "I'm tired mEndcallBehavior=0x" + + Integer.toHexString(mEndcallBehavior)); + result &= ~ACTION_POKE_USER_ACTIVITY; + result |= ACTION_GO_TO_SLEEP; + } + result &= ~ACTION_PASS_TO_USER; + } + } + } else if (code == KeyEvent.KEYCODE_HEADSETHOOK) { + // This key needs to be handled even if the screen is off. + // If others need to be handled while it's off, this is a reasonable + // pattern to follow. + if ((result & ACTION_PASS_TO_USER) == 0) { + // Only do this if we would otherwise not pass it to the user. In that + // case, the PhoneWindow class will do the same thing, except it will + // only do it if the showing app doesn't process the key on its own. + KeyEvent keyEvent = new KeyEvent(event.when, event.when, + down ? KeyEvent.ACTION_DOWN : KeyEvent.ACTION_UP, + KeyEvent.KEYCODE_HEADSETHOOK, 0); + mBroadcastWakeLock.acquire(); + mHandler.post(new PassHeadsetKey(keyEvent)); + } + } + } + + return result; + } + + class PassHeadsetKey implements Runnable { + KeyEvent mKeyEvent; + + PassHeadsetKey(KeyEvent keyEvent) { + mKeyEvent = keyEvent; + } + + public void run() { + Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON, null); + intent.putExtra(Intent.EXTRA_KEY_EVENT, mKeyEvent); + mContext.sendOrderedBroadcast(intent, null, mBroadcastDone, + mHandler, Activity.RESULT_OK, null, null); + } + } + + private BroadcastReceiver mBroadcastDone = new BroadcastReceiver() { + public void onReceive(Context context, Intent intent) { + mBroadcastWakeLock.release(); + } + }; + + /** {@inheritDoc} */ + public boolean isWakeRelMovementTq(int device, int classes, + RawInputEvent event) { + // if it's tagged with one of the wake bits, it wakes up the device + return ((event.flags & (FLAG_WAKE | FLAG_WAKE_DROPPED)) != 0); + } + + /** {@inheritDoc} */ + public boolean isWakeAbsMovementTq(int device, int classes, + RawInputEvent event) { + // if it's tagged with one of the wake bits, it wakes up the device + return ((event.flags & (FLAG_WAKE | FLAG_WAKE_DROPPED)) != 0); + } + + /** + * Given the current state of the world, should this key wake up the device? + */ + protected boolean isWakeKeyTq(RawInputEvent event) { + // There are not key maps for trackball devices, but we'd still + // like to have pressing it wake the device up, so force it here. + int keycode = event.keycode; + int flags = event.flags; + if (keycode == RawInputEvent.BTN_MOUSE) { + flags |= WindowManagerPolicy.FLAG_WAKE; + } + return (flags + & (WindowManagerPolicy.FLAG_WAKE | WindowManagerPolicy.FLAG_WAKE_DROPPED)) != 0; + } + + /** {@inheritDoc} */ + public void screenTurnedOff(int why) { + EventLog.writeEvent(70000, 0); + mKeyguardMediator.onScreenTurnedOff(why); + mScreenOn = false; + updateOrientationListener(); + } + + /** {@inheritDoc} */ + public void screenTurnedOn() { + EventLog.writeEvent(70000, 1); + mKeyguardMediator.onScreenTurnedOn(); + mScreenOn = true; + updateOrientationListener(); + } + + /** {@inheritDoc} */ + public void enableKeyguard(boolean enabled) { + mKeyguardMediator.setKeyguardEnabled(enabled); + } + + /** {@inheritDoc} */ + public void exitKeyguardSecurely(OnKeyguardExitResult callback) { + mKeyguardMediator.verifyUnlock(callback); + } + + /** {@inheritDoc} */ + public boolean keyguardIsShowingTq() { + return mKeyguardMediator.isShowing(); + } + + /** {@inheritDoc} */ + public boolean inKeyguardRestrictedKeyInputMode() { + return mKeyguardMediator.isInputRestricted(); + } + + /** + * Callback from {@link KeyguardViewMediator} + */ + public void onKeyguardShow() { + sendCloseSystemWindows(); + } + + private void sendCloseSystemWindows() { + sendCloseSystemWindows(null); + } + + private void sendCloseSystemWindows(String reason) { + Intent intent = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); + if (reason != null) { + intent.putExtra(SYSTEM_DIALOG_REASON_KEY, reason); + } + mContext.sendBroadcast(intent); + } + + public int rotationForOrientation(int orientation) { + switch (orientation) { + case ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE: + //always return landscape if orientation set to landscape + return Surface.ROTATION_90; + case ActivityInfo.SCREEN_ORIENTATION_PORTRAIT: + //always return portrait if orientation set to portrait + return Surface.ROTATION_0; + case ActivityInfo.SCREEN_ORIENTATION_SENSOR: + if(mOrientationSensorEnabled) { + //consider only sensor based orientation keyboard slide ignored + return mSensorRotation >= 0 ? mSensorRotation : Surface.ROTATION_0; + } + //if orientation sensor is disabled fall back to default behaviour + //based on lid + } + // case for nosensor meaning ignore sensor and consider only lid + // or orientation sensor disabled + //or case.unspecified + if(mLidOpen) { + return Surface.ROTATION_90; + } else { + return Surface.ROTATION_0; + } + } + + /** {@inheritDoc} */ + public void systemReady() { + try { + int menuState = mWindowManager.getKeycodeState(KeyEvent.KEYCODE_MENU); + Log.i(TAG, "Menu key state: " + menuState); + if (menuState > 0) { + // If the user is holding the menu key code, then we are + // going to boot into safe mode. + ActivityManagerNative.getDefault().enterSafeMode(); + } else { + // tell the keyguard + mKeyguardMediator.onSystemReady(); + android.os.SystemProperties.set("dev.bootcomplete", "1"); + } + } catch (RemoteException e) { + // Ignore + } + } + + /** {@inheritDoc} */ + public void enableScreenAfterBoot() { + readLidState(); + updateRotation(); + } + + private void updateRotation() { + mPowerManager.setKeyboardVisibility(mLidOpen); + int rotation= Surface.ROTATION_0; + if (mLidOpen) { + // always use landscape if lid is open + rotation = Surface.ROTATION_90; + } + //if lid is closed orientation will be portrait + try { + //set orientation on WindowManager + mWindowManager.setRotation(rotation, true); + } catch (RemoteException e) { + // Ignore + } + if (keyguardIsShowingTq()) { + if (mLidOpen) { + // only do this if it's opening -- closing the device shouldn't turn it + // off, but it also shouldn't turn it on. + mKeyguardMediator.pokeWakelock(); + } + } else { + // Light up the keyboard if we are sliding up. + if (mLidOpen) { + mPowerManager.userActivity(SystemClock.uptimeMillis(), false, + LocalPowerManager.BUTTON_EVENT); + } else { + mPowerManager.userActivity(SystemClock.uptimeMillis(), false, + LocalPowerManager.OTHER_EVENT); + } + } + } + + /** + * goes to the home screen + * @return whether it did anything + */ + boolean goHome() { + if (false) { + // This code always brings home to the front. + mContext.startActivity(mHomeIntent); + } else { + // This code brings home to the front or, if it is already + // at the front, puts the device to sleep. + try { + int result = ActivityManagerNative.getDefault() + .startActivity(null, mHomeIntent, + mHomeIntent.resolveTypeIfNeeded(mContext.getContentResolver()), + null, 0, null, null, 0, true /* onlyIfNeeded*/, false); + if (result == IActivityManager.START_RETURN_INTENT_TO_CALLER) { + return false; + } + } catch (RemoteException ex) { + // bummer, the activity manager, which is in this process, is dead + } + } + sendCloseSystemWindows(); + return true; + } + + public void setCurrentOrientation(int newOrientation) { + if(newOrientation != mCurrentAppOrientation) { + mCurrentAppOrientation = newOrientation; + updateOrientationListener(); + } + } +} diff --git a/phone/com/android/internal/policy/impl/Policy.java b/phone/com/android/internal/policy/impl/Policy.java new file mode 100644 index 0000000..f537186 --- /dev/null +++ b/phone/com/android/internal/policy/impl/Policy.java @@ -0,0 +1,45 @@ +/* + * 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; + +import android.content.Context; + +import com.android.internal.policy.IPolicy; +import com.android.internal.policy.impl.PhoneLayoutInflater; +import com.android.internal.policy.impl.PhoneWindow; +import com.android.internal.policy.impl.PhoneWindowManager; + +/** + * {@hide} + */ + +// Simple implementation of the policy interface that spawns the right +// set of objects +public class Policy implements IPolicy { + + public PhoneWindow makeNewWindow(Context context) { + return new PhoneWindow(context); + } + + public PhoneLayoutInflater makeNewLayoutInflater(Context context) { + return new PhoneLayoutInflater(context); + } + + public PhoneWindowManager makeNewWindowManager() { + return new PhoneWindowManager(); + } +} diff --git a/phone/com/android/internal/policy/impl/PowerDialog.java b/phone/com/android/internal/policy/impl/PowerDialog.java new file mode 100644 index 0000000..ce9363e --- /dev/null +++ b/phone/com/android/internal/policy/impl/PowerDialog.java @@ -0,0 +1,177 @@ +/* + * 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; + +import com.android.internal.R; + +import android.app.Dialog; +import android.app.StatusBarManager; +import android.content.Context; +import android.os.Bundle; +import android.os.RemoteException; +import android.os.IServiceManager; +import android.os.LocalPowerManager; +import android.os.ServiceManager; +import android.os.ServiceManagerNative; +import android.os.SystemClock; +import com.android.internal.telephony.ITelephony; +import android.view.KeyEvent; +import android.util.Log; +import android.view.View; +import android.view.WindowManager; +import android.view.View.OnClickListener; +import android.view.View.OnKeyListener; +import android.widget.Button; + +/** + * @deprecated use {@link GlobalActions} instead. + */ +public class PowerDialog extends Dialog implements OnClickListener, + OnKeyListener { + private static final String TAG = "PowerDialog"; + + static private StatusBarManager sStatusBar; + private Button mKeyguard; + private Button mPower; + private Button mRadioPower; + private Button mSilent; + + private LocalPowerManager mPowerManager; + + public PowerDialog(Context context, LocalPowerManager powerManager) { + super(context); + mPowerManager = powerManager; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Context context = getContext(); + + if (sStatusBar == null) { + sStatusBar = (StatusBarManager)context.getSystemService(Context.STATUS_BAR_SERVICE); + } + + setContentView(com.android.internal.R.layout.power_dialog); + + getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG); + getWindow().setFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND, + WindowManager.LayoutParams.FLAG_BLUR_BEHIND); + + setTitle(context.getText(R.string.power_dialog)); + + mKeyguard = (Button) findViewById(R.id.keyguard); + mPower = (Button) findViewById(R.id.off); + mRadioPower = (Button) findViewById(R.id.radio_power); + mSilent = (Button) findViewById(R.id.silent); + + if (mKeyguard != null) { + mKeyguard.setOnKeyListener(this); + mKeyguard.setOnClickListener(this); + } + if (mPower != null) { + mPower.setOnClickListener(this); + } + if (mRadioPower != null) { + mRadioPower.setOnClickListener(this); + } + if (mSilent != null) { + mSilent.setOnClickListener(this); + // XXX: HACK for now hide the silent until we get mute support + mSilent.setVisibility(View.GONE); + } + + CharSequence text; + + // set the keyguard button's text + text = context.getText(R.string.screen_lock); + mKeyguard.setText(text); + mKeyguard.requestFocus(); + + try { + ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone")); + if (phone != null) { + text = phone.isRadioOn() ? context + .getText(R.string.turn_off_radio) : context + .getText(R.string.turn_on_radio); + } + } catch (RemoteException ex) { + // ignore it + } + + mRadioPower.setText(text); + } + + public void onClick(View v) { + this.dismiss(); + if (v == mPower) { + // shutdown by making sure radio and power are handled accordingly. + ShutdownThread.shutdownAfterDisablingRadio(getContext(), true); + } else if (v == mRadioPower) { + try { + ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone")); + if (phone != null) { + phone.toggleRadioOnOff(); + } + } catch (RemoteException ex) { + // ignore it + } + } else if (v == mSilent) { + // do something + } else if (v == mKeyguard) { + if (v.isInTouchMode()) { + // only in touch mode for the reasons explained in onKey. + this.dismiss(); + mPowerManager.goToSleep(SystemClock.uptimeMillis() + 1); + } + } + } + + public boolean onKey(View v, int keyCode, KeyEvent event) { + // The activate keyguard button needs to put the device to sleep on the + // key up event. If we try to put it to sleep on the click or down + // action + // the the up action will cause the device to wake back up. + + // Log.i(TAG, "keyCode: " + keyCode + " action: " + event.getAction()); + if (keyCode != KeyEvent.KEYCODE_DPAD_CENTER + || event.getAction() != KeyEvent.ACTION_UP) { + // Log.i(TAG, "getting out of dodge..."); + return false; + } + + // Log.i(TAG, "Clicked mKeyguard! dimissing dialog"); + this.dismiss(); + // Log.i(TAG, "onKey: turning off the screen..."); + // XXX: This is a hack for now + mPowerManager.goToSleep(event.getEventTime() + 1); + return true; + } + + public void show() { + super.show(); + Log.d(TAG, "show... disabling expand"); + sStatusBar.disable(StatusBarManager.DISABLE_EXPAND); + } + + public void dismiss() { + super.dismiss(); + Log.d(TAG, "dismiss... reenabling expand"); + sStatusBar.disable(StatusBarManager.DISABLE_NONE); + } +} diff --git a/phone/com/android/internal/policy/impl/RecentApplicationsDialog.java b/phone/com/android/internal/policy/impl/RecentApplicationsDialog.java new file mode 100644 index 0000000..0c4a7dc --- /dev/null +++ b/phone/com/android/internal/policy/impl/RecentApplicationsDialog.java @@ -0,0 +1,253 @@ +/* + * 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; + +import android.app.ActivityManager; +import android.app.Dialog; +import android.app.StatusBarManager; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; +import android.view.View.OnClickListener; +import android.widget.ImageView; +import android.widget.TextView; + +import java.util.List; + +public class RecentApplicationsDialog extends Dialog implements OnClickListener { + // Elements for debugging support +// private static final String LOG_TAG = "RecentApplicationsDialog"; + private static final boolean DBG_FORCE_EMPTY_LIST = false; + + static private StatusBarManager sStatusBar; + + private static final int NUM_BUTTONS = 6; + private static final int MAX_RECENT_TASKS = NUM_BUTTONS * 2; // allow for some discards + + final View[] mButtons = new View[NUM_BUTTONS]; + View mNoAppsText; + IntentFilter mBroadcastIntentFilter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); + + public RecentApplicationsDialog(Context context) { + super(context); + } + + /** + * We create the recent applications dialog just once, and it stays around (hidden) + * until activated by the user. + * + * @see PhoneWindowManager#showRecentAppsDialog + */ + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Context context = getContext(); + + if (sStatusBar == null) { + sStatusBar = (StatusBarManager)context.getSystemService(Context.STATUS_BAR_SERVICE); + } + + Window theWindow = getWindow(); + theWindow.requestFeature(Window.FEATURE_NO_TITLE); + theWindow.setType(WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG); + theWindow.setFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND, + WindowManager.LayoutParams.FLAG_BLUR_BEHIND); + + setContentView(com.android.internal.R.layout.recent_apps_dialog); + + mButtons[0] = findViewById(com.android.internal.R.id.button1); + mButtons[1] = findViewById(com.android.internal.R.id.button2); + mButtons[2] = findViewById(com.android.internal.R.id.button3); + mButtons[3] = findViewById(com.android.internal.R.id.button4); + mButtons[4] = findViewById(com.android.internal.R.id.button5); + mButtons[5] = findViewById(com.android.internal.R.id.button6); + mNoAppsText = findViewById(com.android.internal.R.id.no_applications_message); + + for (View b : mButtons) { + b.setOnClickListener(this); + } + } + + /** + * Handler for user clicks. If a button was clicked, launch the corresponding activity. + */ + public void onClick(View v) { + + for (View b : mButtons) { + if (b == v) { + // prepare a launch intent and send it + Intent intent = (Intent)b.getTag(); + intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY); + getContext().startActivity(intent); + } + } + dismiss(); + } + + /** + * Set up and show the recent activities dialog. + */ + @Override + public void onStart() { + super.onStart(); + reloadButtons(); + if (sStatusBar != null) { + sStatusBar.disable(StatusBarManager.DISABLE_EXPAND); + } + + // receive broadcasts + getContext().registerReceiver(mBroadcastReceiver, mBroadcastIntentFilter); + } + + /** + * Dismiss the recent activities dialog. + */ + @Override + public void onStop() { + super.onStop(); + + // dump extra memory we're hanging on to + for (View b : mButtons) { + setButtonAppearance(b, null, null); + b.setTag(null); + } + + if (sStatusBar != null) { + sStatusBar.disable(StatusBarManager.DISABLE_NONE); + } + + // stop receiving broadcasts + getContext().unregisterReceiver(mBroadcastReceiver); + } + + /** + * Reload the 6 buttons with recent activities + */ + private void reloadButtons() { + + final Context context = getContext(); + final PackageManager pm = context.getPackageManager(); + final ActivityManager am = (ActivityManager) + context.getSystemService(Context.ACTIVITY_SERVICE); + final List<ActivityManager.RecentTaskInfo> recentTasks = + am.getRecentTasks(MAX_RECENT_TASKS, 0); + + ResolveInfo homeInfo = pm.resolveActivity( + new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME), + 0); + + // Performance note: Our android performance guide says to prefer Iterator when + // using a List class, but because we know that getRecentTasks() always returns + // an ArrayList<>, we'll use a simple index instead. + int button = 0; + int numTasks = recentTasks.size(); + for (int i = 0; i < numTasks && (button < NUM_BUTTONS); ++i) { + final ActivityManager.RecentTaskInfo info = recentTasks.get(i); + + // for debug purposes only, disallow first result to create empty lists + if (DBG_FORCE_EMPTY_LIST && (i == 0)) continue; + + Intent intent = new Intent(info.baseIntent); + if (info.origActivity != null) { + intent.setComponent(info.origActivity); + } + + // Skip the current home activity. + if (homeInfo != null) { + if (homeInfo.activityInfo.packageName.equals( + intent.getComponent().getPackageName()) + && homeInfo.activityInfo.name.equals( + intent.getComponent().getClassName())) { + continue; + } + } + + intent.setFlags((intent.getFlags()&~Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) + | Intent.FLAG_ACTIVITY_NEW_TASK); + final ResolveInfo resolveInfo = pm.resolveActivity(intent, 0); + if (resolveInfo != null) { + final ActivityInfo activityInfo = resolveInfo.activityInfo; + final String title = activityInfo.loadLabel(pm).toString(); + final Drawable icon = activityInfo.loadIcon(pm); + + if (title != null && title.length() > 0 && icon != null) { + final View b = mButtons[button]; + setButtonAppearance(b, title, icon); + b.setTag(intent); + b.setVisibility(View.VISIBLE); + b.setPressed(false); + b.clearFocus(); + ++button; + } + } + } + + // handle the case of "no icons to show" + mNoAppsText.setVisibility((button == 0) ? View.VISIBLE : View.GONE); + + // hide the rest + for ( ; button < NUM_BUTTONS; ++button) { + mButtons[button].setVisibility(View.GONE); + } + } + + /** + * Adjust appearance of each icon-button + */ + private void setButtonAppearance(View theButton, final String theTitle, final Drawable icon) { + TextView tv = (TextView) theButton.findViewById(com.android.internal.R.id.label); + tv.setText(theTitle); + ImageView iv = (ImageView) theButton.findViewById(com.android.internal.R.id.icon); + iv.setImageDrawable(icon); + } + + /** + * This is the listener for the ACTION_CLOSE_SYSTEM_DIALOGS intent. It's an indication that + * we should close ourselves immediately, in order to allow a higher-priority UI to take over + * (e.g. phone call received). + * + * TODO: This is a really heavyweight solution for something that should be so simple. + * For example, we already have a handler, in our superclass, why aren't we sharing that? + * I think we need to investigate simplifying this entire methodology, or perhaps boosting + * it up into the Dialog class. + */ + private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action)) { + String reason = intent.getStringExtra(PhoneWindowManager.SYSTEM_DIALOG_REASON_KEY); + if (! PhoneWindowManager.SYSTEM_DIALOG_REASON_RECENT_APPS.equals(reason)) { + dismiss(); + } + } + } + }; +} diff --git a/phone/com/android/internal/policy/impl/ShortcutManager.java b/phone/com/android/internal/policy/impl/ShortcutManager.java new file mode 100644 index 0000000..d86ac44 --- /dev/null +++ b/phone/com/android/internal/policy/impl/ShortcutManager.java @@ -0,0 +1,120 @@ +/* + * 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; + +import android.content.Context; +import android.content.Intent; +import android.database.ContentObserver; +import android.database.Cursor; +import android.os.Handler; +import android.provider.Settings; +import android.util.Log; +import android.util.SparseArray; +import android.view.KeyCharacterMap; + +import java.net.URISyntaxException; + +/** + * Manages quick launch shortcuts by: + * <li> Keeping the local copy in sync with the database (this is an observer) + * <li> Returning a shortcut-matching intent to clients + */ +class ShortcutManager extends ContentObserver { + + private static final String TAG = "ShortcutManager"; + + private static final int COLUMN_SHORTCUT = 0; + private static final int COLUMN_INTENT = 1; + private static final String[] sProjection = new String[] { + Settings.Bookmarks.SHORTCUT, Settings.Bookmarks.INTENT + }; + + private Context mContext; + private Cursor mCursor; + /** Map of a shortcut to its intent. */ + private SparseArray<Intent> mShortcutIntents; + + public ShortcutManager(Context context, Handler handler) { + super(handler); + + mContext = context; + mShortcutIntents = new SparseArray<Intent>(); + } + + /** Observes the provider of shortcut+intents */ + public void observe() { + mCursor = mContext.getContentResolver().query( + Settings.Bookmarks.CONTENT_URI, sProjection, null, null, null); + mCursor.registerContentObserver(this); + updateShortcuts(); + } + + @Override + public void onChange(boolean selfChange) { + updateShortcuts(); + } + + private void updateShortcuts() { + Cursor c = mCursor; + if (!c.requery()) { + Log.e(TAG, "ShortcutObserver could not re-query shortcuts."); + return; + } + + mShortcutIntents.clear(); + while (c.moveToNext()) { + int shortcut = c.getInt(COLUMN_SHORTCUT); + if (shortcut == 0) continue; + String intentURI = c.getString(COLUMN_INTENT); + Intent intent = null; + try { + intent = Intent.getIntent(intentURI); + } catch (URISyntaxException e) { + Log.w(TAG, "Intent URI for shortcut invalid.", e); + } + if (intent == null) continue; + mShortcutIntents.put(shortcut, intent); + } + } + + /** + * Gets the shortcut intent for a given keycode+modifier. Make sure you + * strip whatever modifier is used for invoking shortcuts (for example, + * if 'Sym+A' should invoke a shortcut on 'A', you should strip the + * 'Sym' bit from the modifiers before calling this method. + * <p> + * This will first try an exact match (with modifiers), and then try a + * match without modifiers (primary character on a key). + * + * @param keyCode The keycode of the key pushed. + * @param modifiers The modifiers without any that are used for chording + * to invoke a shortcut. + * @return The intent that matches the shortcut, or null if not found. + */ + public Intent getIntent(int keyCode, int modifiers) { + KeyCharacterMap kcm = KeyCharacterMap.load(KeyCharacterMap.BUILT_IN_KEYBOARD); + // First try the exact keycode (with modifiers) + int shortcut = kcm.get(keyCode, modifiers); + Intent intent = shortcut != 0 ? mShortcutIntents.get(shortcut) : null; + if (intent != null) return intent; + + // Next try the keycode without modifiers (the primary character on that key) + shortcut = Character.toLowerCase(kcm.get(keyCode, 0)); + return shortcut != 0 ? mShortcutIntents.get(shortcut) : null; + } + +} diff --git a/phone/com/android/internal/policy/impl/ShutdownThread.java b/phone/com/android/internal/policy/impl/ShutdownThread.java new file mode 100644 index 0000000..994b1d5 --- /dev/null +++ b/phone/com/android/internal/policy/impl/ShutdownThread.java @@ -0,0 +1,138 @@ +/* + * 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; + +import android.app.ProgressDialog; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.RemoteException; +import android.os.Power; +import android.os.ServiceManager; +import android.os.SystemClock; +import com.android.internal.telephony.ITelephony; +import android.util.Log; +import android.view.WindowManager; + + +final class ShutdownThread extends Thread { + // constants + private static final String TAG = "ShutdownThread"; + private static final int MAX_NUM_PHONE_STATE_READS = 16; + private static final int PHONE_STATE_POLL_SLEEP_MSEC = 500; + private static final ITelephony sPhone = + ITelephony.Stub.asInterface(ServiceManager.checkService("phone")); + + // state tracking + private static Object sIsStartedGuard = new Object(); + private static boolean sIsStarted = false; + + // static instance of this thread + private static final ShutdownThread sInstance = new ShutdownThread(); + + private ShutdownThread() { + } + + /** + * request a shutdown. + * + * @param context Context used to display the shutdown progress dialog. + */ + public static void shutdownAfterDisablingRadio(final Context context, boolean confirm){ + // ensure that only one thread is trying to power down. + // any additional calls are just returned + synchronized (sIsStartedGuard){ + if (sIsStarted) { + Log.d(TAG, "Request to shutdown already running, returning."); + return; + } + } + + Log.d(TAG, "Notifying thread to start radio shutdown"); + + if (confirm) { + final AlertDialog dialog = new AlertDialog.Builder(context) + .setIcon(android.R.drawable.ic_dialog_alert) + .setTitle(com.android.internal.R.string.power_off) + .setMessage(com.android.internal.R.string.shutdown_confirm) + .setPositiveButton(com.android.internal.R.string.yes, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + beginShutdownSequence(context); + } + }) + .setNegativeButton(com.android.internal.R.string.no, null) + .create(); + dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); + dialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND); + dialog.show(); + } else { + beginShutdownSequence(context); + } + } + + private static void beginShutdownSequence(Context context) { + synchronized (sIsStartedGuard) { + sIsStarted = true; + } + + // throw up an indeterminate system dialog to indicate radio is + // shutting down. + ProgressDialog pd = new ProgressDialog(context); + pd.setTitle(context.getText(com.android.internal.R.string.power_off)); + pd.setMessage(context.getText(com.android.internal.R.string.shutdown_progress)); + pd.setIndeterminate(true); + pd.setCancelable(false); + pd.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); + pd.getWindow().addFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND); + + pd.show(); + + // start the thread that initiates shutdown + sInstance.start(); + } + + /** + * Makes sure we handle the shutdown gracefully. + * Shuts off power regardless of radio state if the alloted time has passed. + */ + public void run() { + //shutdown the phone radio if possible. + if (sPhone != null) { + try { + //shutdown radio + sPhone.setRadio(false); + + for (int i = 0; i < MAX_NUM_PHONE_STATE_READS; i++){ + // poll radio up to 64 times, with a 0.5 sec delay between each call, + // totaling 32 sec. + if (!sPhone.isRadioOn()) { + Log.d(TAG, "Radio shutdown complete."); + break; + } + SystemClock.sleep(PHONE_STATE_POLL_SLEEP_MSEC); + } + } catch (RemoteException ex) { + Log.e(TAG, "RemoteException caught from failed radio shutdown.", ex); + } + } + + //shutdown power + Log.d(TAG, "Shutting down power."); + Power.shutdown(); + } +} diff --git a/phone/com/android/internal/policy/impl/SimUnlockScreen.java b/phone/com/android/internal/policy/impl/SimUnlockScreen.java new file mode 100644 index 0000000..4c71392 --- /dev/null +++ b/phone/com/android/internal/policy/impl/SimUnlockScreen.java @@ -0,0 +1,351 @@ +/* + * 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; + +import android.app.Dialog; +import android.app.ProgressDialog; +import android.content.Context; +import android.os.RemoteException; +import android.os.ServiceManager; +import com.android.internal.telephony.ITelephony; +import android.text.Editable; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.WindowManager; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TextView; +import com.android.internal.R; + +/** + * Displays a dialer like interface to unlock the SIM PIN. + */ +public class SimUnlockScreen extends LinearLayout implements KeyguardScreen, View.OnClickListener, + KeyguardUpdateMonitor.ConfigurationChangeCallback { + + private static final int DIGIT_PRESS_WAKE_MILLIS = 5000; + + private final KeyguardUpdateMonitor mUpdateMonitor; + private final KeyguardScreenCallback mCallback; + + private final boolean mCreatedWithKeyboardOpen; + + private TextView mHeaderText; + private EditText mPinText; + + private TextView mOkButton; + private TextView mEmergencyCallButton; + + private View mBackSpaceButton; + + private final int[] mEnteredPin = {0, 0, 0, 0, 0, 0, 0, 0}; + private int mEnteredDigits = 0; + + private ProgressDialog mSimUnlockProgressDialog = null; + + private static final char[] DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}; + + public SimUnlockScreen(Context context, KeyguardUpdateMonitor updateMonitor, + KeyguardScreenCallback callback) { + super(context); + mUpdateMonitor = updateMonitor; + mCallback = callback; + mCreatedWithKeyboardOpen = mUpdateMonitor.isKeyboardOpen(); + + if (mCreatedWithKeyboardOpen) { + LayoutInflater.from(context).inflate(R.layout.keyguard_screen_sim_pin_landscape, this, true); + } else { + LayoutInflater.from(context).inflate(R.layout.keyguard_screen_sim_pin_portrait, this, true); + new TouchInput(); + } + + mHeaderText = (TextView) findViewById(R.id.headerText); + mPinText = (EditText) findViewById(R.id.pinDisplay); + mBackSpaceButton = findViewById(R.id.backspace); + mBackSpaceButton.setOnClickListener(this); + + mEmergencyCallButton = (TextView) findViewById(R.id.emergencyCall); + mOkButton = (TextView) findViewById(R.id.ok); + + mHeaderText.setText(R.string.keyguard_password_enter_pin_code); + mPinText.setFocusable(false); + + mEmergencyCallButton.setOnClickListener(this); + mOkButton.setOnClickListener(this); + + mUpdateMonitor.registerConfigurationChangeCallback(this); + setFocusableInTouchMode(true); + } + + /** {@inheritDoc} */ + public void onPause() { + + } + + /** {@inheritDoc} */ + public void onResume() { + // start fresh + mHeaderText.setText(R.string.keyguard_password_enter_pin_code); + mPinText.setText(""); + } + + /** {@inheritDoc} */ + public void cleanUp() { + mUpdateMonitor.removeCallback(this); + } + + + /** + * 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 onSimLockChangedResponse(boolean success); + + @Override + public void run() { + try { + final boolean result = ITelephony.Stub.asInterface(ServiceManager + .checkService("phone")).supplyPin(mPin); + post(new Runnable() { + public void run() { + onSimLockChangedResponse(result); + } + }); + } catch (RemoteException e) { + post(new Runnable() { + public void run() { + onSimLockChangedResponse(false); + } + }); + } + } + } + + public void onClick(View v) { + if (v == mBackSpaceButton) { + final Editable digits = mPinText.getText(); + final int len = digits.length(); + if (len > 0) { + digits.delete(len-1, len); + mEnteredDigits--; + } + mCallback.pokeWakelock(); + } else if (v == mEmergencyCallButton) { + mCallback.takeEmergencyCallAction(); + } else if (v == mOkButton) { + checkPin(); + } + } + + private Dialog getSimUnlockProgressDialog() { + if (mSimUnlockProgressDialog == null) { + mSimUnlockProgressDialog = new ProgressDialog(mContext); + mSimUnlockProgressDialog.setMessage( + mContext.getString(R.string.lockscreen_sim_unlock_progress_dialog_message)); + mSimUnlockProgressDialog.setIndeterminate(true); + mSimUnlockProgressDialog.setCancelable(false); + mSimUnlockProgressDialog.getWindow().setType( + WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); + mSimUnlockProgressDialog.getWindow().setFlags( + WindowManager.LayoutParams.FLAG_BLUR_BEHIND, + WindowManager.LayoutParams.FLAG_BLUR_BEHIND); + } + return mSimUnlockProgressDialog; + } + + private void checkPin() { + + // make sure that the pin is at least 4 digits long. + if (mEnteredDigits < 4) { + // otherwise, display a message to the user, and don't submit. + mHeaderText.setText(R.string.invalidPin); + mPinText.setText(""); + mEnteredDigits = 0; + mCallback.pokeWakelock(); + return; + } + getSimUnlockProgressDialog().show(); + + new CheckSimPin(mPinText.getText().toString()) { + void onSimLockChangedResponse(boolean success) { + getSimUnlockProgressDialog().hide(); + if (success) { + // before closing the keyguard, report back that + // the sim is unlocked so it knows right away + mUpdateMonitor.reportSimPinUnlocked(); + mCallback.goToUnlockScreen(); + } else { + mHeaderText.setText(R.string.keyguard_password_wrong_pin_code); + mPinText.setText(""); + mEnteredDigits = 0; + mCallback.pokeWakelock(); + } + } + }.start(); + } + + + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + mCallback.goToLockScreen(); + return true; + } + + final char match = event.getMatch(DIGITS); + if (match != 0) { + reportDigit(match - '0'); + return true; + } + if (keyCode == KeyEvent.KEYCODE_DEL) { + if (mEnteredDigits > 0) { + mPinText.onKeyDown(keyCode, event); + mEnteredDigits--; + } + return true; + } + + if (keyCode == KeyEvent.KEYCODE_ENTER) { + checkPin(); + return true; + } + + return false; + } + + private void reportDigit(int digit) { + if (mEnteredDigits == 0) { + mPinText.setText(""); + } + if (mEnteredDigits == 8) { + return; + } + mPinText.append(Integer.toString(digit)); + mEnteredPin[mEnteredDigits++] = digit; + } + + public void onOrientationChange(boolean inPortrait) {} + + public void onKeyboardChange(boolean isKeyboardOpen) { + if (isKeyboardOpen != mCreatedWithKeyboardOpen) { + mCallback.recreateMe(); + } + } + + /** + * Helper class to handle input from touch dialer. Only relevant when + * the keyboard is shut. + */ + private class TouchInput implements View.OnClickListener { + private TextView mZero; + private TextView mOne; + private TextView mTwo; + private TextView mThree; + private TextView mFour; + private TextView mFive; + private TextView mSix; + private TextView mSeven; + private TextView mEight; + private TextView mNine; + private TextView mCancelButton; + + private TouchInput() { + mZero = (TextView) findViewById(R.id.zero); + mOne = (TextView) findViewById(R.id.one); + mTwo = (TextView) findViewById(R.id.two); + mThree = (TextView) findViewById(R.id.three); + mFour = (TextView) findViewById(R.id.four); + mFive = (TextView) findViewById(R.id.five); + mSix = (TextView) findViewById(R.id.six); + mSeven = (TextView) findViewById(R.id.seven); + mEight = (TextView) findViewById(R.id.eight); + mNine = (TextView) findViewById(R.id.nine); + mCancelButton = (TextView) findViewById(R.id.cancel); + + mZero.setText("0"); + mOne.setText("1"); + mTwo.setText("2"); + mThree.setText("3"); + mFour.setText("4"); + mFive.setText("5"); + mSix.setText("6"); + mSeven.setText("7"); + mEight.setText("8"); + mNine.setText("9"); + + mZero.setOnClickListener(this); + mOne.setOnClickListener(this); + mTwo.setOnClickListener(this); + mThree.setOnClickListener(this); + mFour.setOnClickListener(this); + mFive.setOnClickListener(this); + mSix.setOnClickListener(this); + mSeven.setOnClickListener(this); + mEight.setOnClickListener(this); + mNine.setOnClickListener(this); + mCancelButton.setOnClickListener(this); + } + + + public void onClick(View v) { + if (v == mCancelButton) { + mCallback.goToLockScreen(); + return; + } + + final int digit = checkDigit(v); + if (digit >= 0) { + mCallback.pokeWakelock(DIGIT_PRESS_WAKE_MILLIS); + reportDigit(digit); + } + } + + private int checkDigit(View v) { + int digit = -1; + if (v == mZero) { + digit = 0; + } else if (v == mOne) { + digit = 1; + } else if (v == mTwo) { + digit = 2; + } else if (v == mThree) { + digit = 3; + } else if (v == mFour) { + digit = 4; + } else if (v == mFive) { + digit = 5; + } else if (v == mSix) { + digit = 6; + } else if (v == mSeven) { + digit = 7; + } else if (v == mEight) { + digit = 8; + } else if (v == mNine) { + digit = 9; + } + return digit; + } + } +} diff --git a/phone/com/android/internal/policy/impl/UnlockScreen.java b/phone/com/android/internal/policy/impl/UnlockScreen.java new file mode 100644 index 0000000..bd0b6d5 --- /dev/null +++ b/phone/com/android/internal/policy/impl/UnlockScreen.java @@ -0,0 +1,327 @@ +/* + * 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; + +import android.content.Context; +import android.os.CountDownTimer; +import android.os.SystemClock; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.MotionEvent; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; +import com.android.internal.R; +import com.android.internal.widget.LinearLayoutWithDefaultTouchRecepient; +import com.android.internal.widget.LockPatternUtils; +import com.android.internal.widget.LockPatternView; + +import java.util.List; + +/** + * This is the screen that shows the 9 circle unlock widget and instructs + * the user how to unlock their device, or make an emergency call. + */ +class UnlockScreen extends LinearLayoutWithDefaultTouchRecepient + implements KeyguardScreen, KeyguardUpdateMonitor.ConfigurationChangeCallback { + + private static final String TAG = "UnlockScreen"; + + // how long before we clear the wrong pattern + private static final int PATTERN_CLEAR_TIMEOUT_MS = 2000; + + // how long we stay awake once the user is ready to enter a pattern + private static final int UNLOCK_PATTERN_WAKE_INTERVAL_MS = 7000; + + private int mFailedPatternAttemptsSinceLastTimeout = 0; + private int mTotalFailedPatternAttempts = 0; + private CountDownTimer mCountdownTimer = null; + + private final LockPatternUtils mLockPatternUtils; + private final KeyguardUpdateMonitor mUpdateMonitor; + private final KeyguardScreenCallback mCallback; + + private boolean mCreatedInPortrait; + + private ImageView mUnlockIcon; + private TextView mUnlockHeader; + private LockPatternView mLockPatternView; + + private ViewGroup mFooterNormal; + private ViewGroup mFooterForgotPattern; + + /** + * Keeps track of the last time we poked the wake lock during dispatching + * of the touch event, initalized to something gauranteed to make us + * poke it 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 Button mForgotPatternButton; + + + enum FooterMode { + Normal, + ForgotLockPattern, + VerifyUnlocked + } + + private void updateFooter(FooterMode mode) { + switch (mode) { + case Normal: + mFooterNormal.setVisibility(View.VISIBLE); + mFooterForgotPattern.setVisibility(View.GONE); + break; + case ForgotLockPattern: + mFooterNormal.setVisibility(View.GONE); + mFooterForgotPattern.setVisibility(View.VISIBLE); + break; + case VerifyUnlocked: + mFooterNormal.setVisibility(View.GONE); + mFooterForgotPattern.setVisibility(View.GONE); + } + } + + /** + * @param context The context. + * @param lockPatternUtils Used to lookup lock pattern settings. + * @param updateMonitor Used to lookup state affecting keyguard. + * @param callback Used to notify the manager when we're done, etc. + * @param totalFailedAttempts The current number of failed attempts. + */ + UnlockScreen(Context context, + LockPatternUtils lockPatternUtils, + KeyguardUpdateMonitor updateMonitor, + KeyguardScreenCallback callback, + int totalFailedAttempts) { + super(context); + mLockPatternUtils = lockPatternUtils; + mUpdateMonitor = updateMonitor; + mCallback = callback; + mTotalFailedPatternAttempts = totalFailedAttempts; + mFailedPatternAttemptsSinceLastTimeout = totalFailedAttempts % LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT; + + if (mUpdateMonitor.isInPortrait()) { + LayoutInflater.from(context).inflate(R.layout.keyguard_screen_unlock_portrait, this, true); + } else { + LayoutInflater.from(context).inflate(R.layout.keyguard_screen_unlock_landscape, this, true); + } + + mUnlockIcon = (ImageView) findViewById(R.id.unlockLockIcon); + + mLockPatternView = (LockPatternView) findViewById(R.id.lockPattern); + mUnlockHeader = (TextView) findViewById(R.id.headerText); + + mUnlockHeader.setText(R.string.lockscreen_pattern_instructions); + + mFooterNormal = (ViewGroup) findViewById(R.id.footerNormal); + mFooterForgotPattern = (ViewGroup) findViewById(R.id.footerForgotPattern); + + // emergency call buttons + final OnClickListener emergencyClick = new OnClickListener() { + public void onClick(View v) { + mCallback.takeEmergencyCallAction(); + } + }; + Button emergencyAlone = (Button) findViewById(R.id.emergencyCallAlone); + emergencyAlone.setFocusable(false); // touch only! + emergencyAlone.setOnClickListener(emergencyClick); + Button emergencyTogether = (Button) findViewById(R.id.emergencyCallTogether); + emergencyTogether.setFocusable(false); + emergencyTogether.setOnClickListener(emergencyClick); + + mForgotPatternButton = (Button) findViewById(R.id.forgotPattern); + mForgotPatternButton.setText(R.string.lockscreen_forgot_pattern_button_text); + mForgotPatternButton.setOnClickListener(new OnClickListener() { + + public void onClick(View v) { + mLockPatternUtils.setPermanentlyLocked(true); + mCallback.goToUnlockScreen(); + } + }); + + // make it so unhandled touch events within the unlock screen go to the + // lock pattern view. + setDefaultTouchRecepient(mLockPatternView); + + 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()); + + // assume normal footer mode for now + updateFooter(FooterMode.Normal); + + mCreatedInPortrait = updateMonitor.isInPortrait(); + updateMonitor.registerConfigurationChangeCallback(this); + setFocusableInTouchMode(true); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + mCallback.goToLockScreen(); + return true; + } + return false; + } + + @Override + public boolean dispatchTouchEvent(MotionEvent 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 boolean result = super.dispatchTouchEvent(ev); + if (result && + ((SystemClock.elapsedRealtime() - mLastPokeTime) + > (UNLOCK_PATTERN_WAKE_INTERVAL_MS - 100))) { + mLastPokeTime = SystemClock.elapsedRealtime(); + mCallback.pokeWakelock(UNLOCK_PATTERN_WAKE_INTERVAL_MS); + } + return result; + } + + /** {@inheritDoc} */ + public void onOrientationChange(boolean inPortrait) { + if (inPortrait != mCreatedInPortrait) { + mCallback.recreateMe(); + } + } + + /** {@inheritDoc} */ + public void onKeyboardChange(boolean isKeyboardOpen) {} + + /** {@inheritDoc} */ + public void onPause() { + if (mCountdownTimer != null) { + mCountdownTimer.cancel(); + mCountdownTimer = null; + } + } + + /** {@inheritDoc} */ + public void onResume() { + // reset header + mUnlockHeader.setText(R.string.lockscreen_pattern_instructions); + mUnlockIcon.setVisibility(View.VISIBLE); + + // 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); + } + + // the footer depends on how many total attempts the user has failed + if (mCallback.isVerifyUnlockOnly()) { + updateFooter(FooterMode.VerifyUnlocked); + } else if (mTotalFailedPatternAttempts < LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT) { + updateFooter(FooterMode.Normal); + } else { + updateFooter(FooterMode.ForgotLockPattern); + } + } + + /** {@inheritDoc} */ + public void cleanUp() { + mUpdateMonitor.removeCallback(this); + } + + private class UnlockPatternListener + implements LockPatternView.OnPatternListener { + + public void onPatternStart() { + mLockPatternView.removeCallbacks(mCancelPatternRunnable); + } + + public void onPatternCleared() { + } + + public void onPatternDetected(List<LockPatternView.Cell> pattern) { + if (mLockPatternUtils.checkPattern(pattern)) { + mLockPatternView + .setDisplayMode(LockPatternView.DisplayMode.Correct); + mUnlockIcon.setVisibility(View.GONE); + mUnlockHeader.setText(R.string.lockscreen_pattern_correct); + mCallback.keyguardDone(true); + } else { + mCallback.pokeWakelock(UNLOCK_PATTERN_WAKE_INTERVAL_MS); + mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong); + if (pattern.size() >= LockPatternUtils.MIN_PATTERN_REGISTER_FAIL) { + mTotalFailedPatternAttempts++; + mFailedPatternAttemptsSinceLastTimeout++; + mCallback.reportFailedPatternAttempt(); + } + if (mFailedPatternAttemptsSinceLastTimeout >= LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT) { + long deadline = SystemClock.elapsedRealtime() + LockPatternUtils.FAILED_ATTEMPT_TIMEOUT_MS; + mLockPatternUtils.setLockoutAttemptDeadline(deadline); + handleAttemptLockout(deadline); + return; + } + mUnlockIcon.setVisibility(View.VISIBLE); + mUnlockHeader.setText(R.string.lockscreen_pattern_wrong); + mLockPatternView.postDelayed( + mCancelPatternRunnable, + PATTERN_CLEAR_TIMEOUT_MS); + } + } + } + + private void handleAttemptLockout(long elapsedRealtimeDeadline) { + mLockPatternView.clearPattern(); + mLockPatternView.setEnabled(false); + long elapsedRealtime = SystemClock.elapsedRealtime(); + mCountdownTimer = new CountDownTimer(elapsedRealtimeDeadline - elapsedRealtime, 1000) { + + @Override + public void onTick(long millisUntilFinished) { + int secondsRemaining = (int) (millisUntilFinished / 1000); + mUnlockHeader.setText(getContext().getString( + R.string.lockscreen_too_many_failed_attempts_countdown, + secondsRemaining)); + } + + @Override + public void onFinish() { + mLockPatternView.setEnabled(true); + mUnlockHeader.setText(R.string.lockscreen_pattern_instructions); + mUnlockIcon.setVisibility(View.VISIBLE); + mFailedPatternAttemptsSinceLastTimeout = 0; + updateFooter(FooterMode.ForgotLockPattern); + } + }.start(); + } + +} diff --git a/phone/com/android/internal/policy/impl/package.html b/phone/com/android/internal/policy/impl/package.html new file mode 100644 index 0000000..c9f96a6 --- /dev/null +++ b/phone/com/android/internal/policy/impl/package.html @@ -0,0 +1,5 @@ +<body> + +{@hide} + +</body> |