diff options
author | Ricardo Cerqueira <ricardo@cyngn.com> | 2015-11-05 02:00:43 +0000 |
---|---|---|
committer | Ricardo Cerqueira <ricardo@cyngn.com> | 2015-11-05 15:44:26 +0000 |
commit | 90852d4675c9b7df6faf036efb7fe73b5f816293 (patch) | |
tree | 18ea2755ab5ea68669a7951d5a07daadc4870b0a /src/com/android | |
parent | 3dbac3fff036f5a464f6322200c59f9934ebf610 (diff) | |
parent | 310423e73156dab1c7c8f50dc67fdbc326cd37f7 (diff) | |
download | packages_apps_Settings-90852d4675c9b7df6faf036efb7fe73b5f816293.zip packages_apps_Settings-90852d4675c9b7df6faf036efb7fe73b5f816293.tar.gz packages_apps_Settings-90852d4675c9b7df6faf036efb7fe73b5f816293.tar.bz2 |
Merge tag 'android-6.0.0_r26' into HEAD
Android 6.0.0 release 26
Conflicts:
res/values-fr/strings.xml
res/values-it/strings.xml
res/values-ja/strings.xml
res/values-nl/strings.xml
res/values-pl/strings.xml
res/values-ro/strings.xml
res/values-ru/strings.xml
res/values-zh-rCN/strings.xml
res/values/bools.xml
res/values/strings.xml
res/xml/development_prefs.xml
src/com/android/settings/DevelopmentSettings.java
src/com/android/settings/DeviceInfoSettings.java
src/com/android/settings/DisplaySettings.java
Change-Id: I9fd9e793cf6097d950f8a1e30771c8bdf5067906
Diffstat (limited to 'src/com/android')
41 files changed, 2404 insertions, 361 deletions
diff --git a/src/com/android/settings/ApnSettings.java b/src/com/android/settings/ApnSettings.java index 202b43e..1ffd9bd 100644 --- a/src/com/android/settings/ApnSettings.java +++ b/src/com/android/settings/ApnSettings.java @@ -33,12 +33,14 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; +import android.os.PersistableBundle; import android.os.UserHandle; import android.os.UserManager; import android.preference.Preference; import android.preference.PreferenceGroup; import android.preference.PreferenceScreen; import android.provider.Telephony; +import android.telephony.CarrierConfigManager; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.text.TextUtils; @@ -112,6 +114,8 @@ public class ApnSettings extends SettingsPreferenceFragment implements private boolean mUnavailable; + private boolean mHideImsApn; + private final BroadcastReceiver mMobileStateReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -163,6 +167,11 @@ public class ApnSettings extends SettingsPreferenceFragment implements mSubscriptionInfo = SubscriptionManager.from(activity).getActiveSubscriptionInfo(subId); mUiccController = UiccController.getInstance(); + + CarrierConfigManager configManager = (CarrierConfigManager) + getSystemService(Context.CARRIER_CONFIG_SERVICE); + PersistableBundle b = configManager.getConfig(); + mHideImsApn = b.getBoolean(CarrierConfigManager.KEY_HIDE_IMS_APN_BOOL); } @Override @@ -228,9 +237,12 @@ public class ApnSettings extends SettingsPreferenceFragment implements final String mccmnc = mSubscriptionInfo == null ? "" : tm.getIccOperatorNumericForData(mSubscriptionInfo.getSubscriptionId()); Log.d(TAG, "mccmnc = " + mccmnc); - final String where = "numeric=\"" + String where = "numeric=\"" + mccmnc + "\" AND NOT (type='ia' AND (apn=\"\" OR apn IS NULL))"; + if (mHideImsApn) { + where = where + " AND NOT (type='ims')"; + } Cursor cursor = getContentResolver().query(Telephony.Carriers.CONTENT_URI, new String[] { "_id", "name", "apn", "type", "mvno_type", "mvno_match_data", "read_only"}, where, diff --git a/src/com/android/settings/ChooseLockGeneric.java b/src/com/android/settings/ChooseLockGeneric.java index 708b871..44a5c51 100644 --- a/src/com/android/settings/ChooseLockGeneric.java +++ b/src/com/android/settings/ChooseLockGeneric.java @@ -109,11 +109,18 @@ public class ChooseLockGeneric extends SettingsActivity { @Override public void onRemovalSucceeded(Fingerprint fingerprint) { Log.v(TAG, "Fingerprint removed: " + fingerprint.getFingerId()); + if (mFingerprintManager.getEnrolledFingerprints().size() == 0) { + finish(); + } } @Override public void onRemovalError(Fingerprint fp, int errMsgId, CharSequence errString) { - Toast.makeText(getActivity(), errString, Toast.LENGTH_SHORT); + Activity activity = getActivity(); + if (activity != null) { + Toast.makeText(getActivity(), errString, Toast.LENGTH_SHORT); + } + finish(); } }; @@ -355,6 +362,10 @@ public class ChooseLockGeneric extends SettingsActivity { boolean visible = true; if (KEY_UNLOCK_SET_OFF.equals(key)) { enabled = quality <= DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED; + if (getResources().getBoolean(R.bool.config_hide_none_security_option)) { + enabled = false; + visible = false; + } } else if (KEY_UNLOCK_SET_NONE.equals(key)) { enabled = quality <= DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED; } else if (KEY_UNLOCK_SET_PATTERN.equals(key)) { @@ -491,18 +502,19 @@ public class ChooseLockGeneric extends SettingsActivity { mChooseLockSettingsHelper.utils().clearLock(UserHandle.myUserId()); mChooseLockSettingsHelper.utils().setLockScreenDisabled(disabled, UserHandle.myUserId()); - removeAllFingerprintTemplates(); + removeAllFingerprintTemplatesAndFinish(); getActivity().setResult(Activity.RESULT_OK); - finish(); } else { - removeAllFingerprintTemplates(); - finish(); + removeAllFingerprintTemplatesAndFinish(); } } - private void removeAllFingerprintTemplates() { - if (mFingerprintManager != null && mFingerprintManager.isHardwareDetected()) { + private void removeAllFingerprintTemplatesAndFinish() { + if (mFingerprintManager != null && mFingerprintManager.isHardwareDetected() + && mFingerprintManager.getEnrolledFingerprints().size() > 0) { mFingerprintManager.remove(new Fingerprint(null, 0, 0, 0), mRemovalCallback); + } else { + finish(); } } diff --git a/src/com/android/settings/ChooseLockPassword.java b/src/com/android/settings/ChooseLockPassword.java index 64aaaca..54c3620 100644 --- a/src/com/android/settings/ChooseLockPassword.java +++ b/src/com/android/settings/ChooseLockPassword.java @@ -17,11 +17,11 @@ package com.android.settings; import com.android.internal.logging.MetricsLogger; -import com.android.internal.widget.LockPatternChecker; import com.android.internal.widget.LockPatternUtils; import com.android.internal.widget.PasswordEntryKeyboardHelper; import com.android.internal.widget.PasswordEntryKeyboardView; import com.android.internal.widget.TextViewInputDisabler; +import com.android.internal.widget.LockPatternUtils.RequestThrottledException; import com.android.settings.notification.RedactionInterstitial; import android.app.Activity; @@ -30,7 +30,6 @@ import android.app.admin.DevicePolicyManager; import android.content.Context; import android.content.Intent; import android.inputmethodservice.KeyboardView; -import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.Message; @@ -122,12 +121,15 @@ public class ChooseLockPassword extends SettingsActivity { } public static class ChooseLockPasswordFragment extends InstrumentedFragment - implements OnClickListener, OnEditorActionListener, TextWatcher { + implements OnClickListener, OnEditorActionListener, TextWatcher, + SaveAndFinishWorker.Listener { private static final String KEY_FIRST_PIN = "first_pin"; private static final String KEY_UI_STAGE = "ui_stage"; private static final String KEY_CURRENT_PASSWORD = "current_password"; + private static final String FRAGMENT_TAG_SAVE_AND_FINISH = "save_and_finish_worker"; private String mCurrentPassword; + private String mChosenPassword; private boolean mHasChallenge; private long mChallenge; private TextView mPasswordEntry; @@ -141,11 +143,11 @@ public class ChooseLockPassword extends SettingsActivity { private int mPasswordMinNumeric = 0; private int mPasswordMinNonLetter = 0; private LockPatternUtils mLockPatternUtils; - private AsyncTask<?, ?, ?> mPendingLockCheck; + private SaveAndFinishWorker mSaveAndFinishWorker; private int mRequestedQuality = DevicePolicyManager.PASSWORD_QUALITY_NUMERIC; private ChooseLockSettingsHelper mChooseLockSettingsHelper; private Stage mUiStage = Stage.Introduction; - private boolean mDone = false; + private TextView mHeaderText; private String mFirstPin; private KeyboardView mKeyboardView; @@ -301,8 +303,11 @@ public class ChooseLockPassword extends SettingsActivity { if (mCurrentPassword == null) { mCurrentPassword = savedInstanceState.getString(KEY_CURRENT_PASSWORD); } + + // Re-attach to the exiting worker if there is one. + mSaveAndFinishWorker = (SaveAndFinishWorker) getFragmentManager().findFragmentByTag( + FRAGMENT_TAG_SAVE_AND_FINISH); } - mDone = false; if (activity instanceof SettingsActivity) { final SettingsActivity sa = (SettingsActivity) activity; int id = mIsAlphaMode ? R.string.lockpassword_choose_your_password_header @@ -321,16 +326,18 @@ public class ChooseLockPassword extends SettingsActivity { public void onResume() { super.onResume(); updateStage(mUiStage); - mPasswordEntryInputDisabler.setInputEnabled(true); - mKeyboardView.requestFocus(); + if (mSaveAndFinishWorker != null) { + mSaveAndFinishWorker.setListener(this); + } else { + mKeyboardView.requestFocus(); + } } @Override public void onPause() { mHandler.removeMessages(MSG_SHOW_ERROR); - if (mPendingLockCheck != null) { - mPendingLockCheck.cancel(false); - mPendingLockCheck = null; + if (mSaveAndFinishWorker != null) { + mSaveAndFinishWorker.setListener(null); } super.onPause(); @@ -479,36 +486,22 @@ public class ChooseLockPassword extends SettingsActivity { } public void handleNext() { - if (mDone) return; - - final String pin = mPasswordEntry.getText().toString(); - if (TextUtils.isEmpty(pin)) { + if (mSaveAndFinishWorker != null) return; + mChosenPassword = mPasswordEntry.getText().toString(); + if (TextUtils.isEmpty(mChosenPassword)) { return; } String errorMsg = null; if (mUiStage == Stage.Introduction) { - errorMsg = validatePassword(pin); + errorMsg = validatePassword(mChosenPassword); if (errorMsg == null) { - mFirstPin = pin; + mFirstPin = mChosenPassword; mPasswordEntry.setText(""); updateStage(Stage.NeedToConfirm); } } else if (mUiStage == Stage.NeedToConfirm) { - if (mFirstPin.equals(pin)) { - boolean wasSecureBefore = mLockPatternUtils.isSecure(UserHandle.myUserId()); - final boolean required = getActivity().getIntent().getBooleanExtra( - EncryptionInterstitial.EXTRA_REQUIRE_PASSWORD, true); - mLockPatternUtils.setCredentialRequiredToDecrypt(required); - mLockPatternUtils.saveLockPassword(pin, mCurrentPassword, mRequestedQuality, - UserHandle.myUserId()); - - if (mHasChallenge) { - startVerifyPassword(pin, wasSecureBefore); - return; - } else { - getActivity().setResult(RESULT_FINISHED); - } - finishConfirmStage(wasSecureBefore); + if (mFirstPin.equals(mChosenPassword)) { + startSaveAndFinish(); } else { CharSequence tmp = mPasswordEntry.getText(); if (tmp != null) { @@ -522,50 +515,6 @@ public class ChooseLockPassword extends SettingsActivity { } } - private void startVerifyPassword(final String pin, final boolean wasSecureBefore) { - mPasswordEntryInputDisabler.setInputEnabled(false); - setNextEnabled(false); - if (mPendingLockCheck != null) { - mPendingLockCheck.cancel(false); - } - - mPendingLockCheck = LockPatternChecker.verifyPassword( - mLockPatternUtils, - pin, - mChallenge, - UserHandle.myUserId(), - new LockPatternChecker.OnVerifyCallback() { - @Override - public void onVerified(byte[] token, int timeoutMs) { - if (token == null) { - Log.e(TAG, "critical: no token returned from known good password"); - } - - mPasswordEntryInputDisabler.setInputEnabled(true); - setNextEnabled(true); - mPendingLockCheck = null; - - Intent intent = new Intent(); - intent.putExtra( - ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, - token); - getActivity().setResult(RESULT_FINISHED, intent); - finishConfirmStage(wasSecureBefore); - } - }); - } - - private void finishConfirmStage(boolean wasSecureBefore) { - getActivity().finish(); - mDone = true; - if (!wasSecureBefore) { - Intent intent = getRedactionInterstitialIntent(getActivity()); - if (intent != null) { - startActivity(intent); - } - } - } - protected void setNextEnabled(boolean enabled) { mNextButton.setEnabled(enabled); } @@ -609,6 +558,7 @@ public class ChooseLockPassword extends SettingsActivity { * Update the hint based on current Stage and length of password entry */ private void updateUi() { + final boolean canInput = mSaveAndFinishWorker == null; String password = mPasswordEntry.getText().toString(); final int length = password.length(); if (mUiStage == Stage.Introduction) { @@ -629,9 +579,10 @@ public class ChooseLockPassword extends SettingsActivity { } } else { mHeaderText.setText(mIsAlphaMode ? mUiStage.alphaHint : mUiStage.numericHint); - setNextEnabled(length > 0); + setNextEnabled(canInput && length > 0); } setNextText(mUiStage.buttonText); + mPasswordEntryInputDisabler.setInputEnabled(canInput); } public void afterTextChanged(Editable s) { @@ -649,5 +600,83 @@ public class ChooseLockPassword extends SettingsActivity { public void onTextChanged(CharSequence s, int start, int before, int count) { } + + private void startSaveAndFinish() { + if (mSaveAndFinishWorker != null) { + Log.w(TAG, "startSaveAndFinish with an existing SaveAndFinishWorker."); + return; + } + + mPasswordEntryInputDisabler.setInputEnabled(false); + setNextEnabled(false); + + mSaveAndFinishWorker = new SaveAndFinishWorker(); + getFragmentManager().beginTransaction().add(mSaveAndFinishWorker, + FRAGMENT_TAG_SAVE_AND_FINISH).commit(); + mSaveAndFinishWorker.setListener(this); + + final boolean required = getActivity().getIntent().getBooleanExtra( + EncryptionInterstitial.EXTRA_REQUIRE_PASSWORD, true); + mSaveAndFinishWorker.start(mLockPatternUtils, required, mHasChallenge, mChallenge, + mChosenPassword, mCurrentPassword, mRequestedQuality); + } + + @Override + public void onChosenLockSaveFinished(boolean wasSecureBefore, Intent resultData) { + getActivity().setResult(RESULT_FINISHED, resultData); + getActivity().finish(); + + if (!wasSecureBefore) { + Intent intent = getRedactionInterstitialIntent(getActivity()); + if (intent != null) { + startActivity(intent); + } + } + } + } + + private static class SaveAndFinishWorker extends SaveChosenLockWorkerBase { + + private String mChosenPassword; + private String mCurrentPassword; + private int mRequestedQuality; + + public void start(LockPatternUtils utils, boolean required, + boolean hasChallenge, long challenge, + String chosenPassword, String currentPassword, int requestedQuality) { + prepare(utils, required, hasChallenge, challenge); + + mChosenPassword = chosenPassword; + mCurrentPassword = currentPassword; + mRequestedQuality = requestedQuality; + + start(); + } + + @Override + protected Intent saveAndVerifyInBackground() { + Intent result = null; + final int userId = UserHandle.myUserId(); + mUtils.saveLockPassword(mChosenPassword, mCurrentPassword, mRequestedQuality, + userId); + + if (mHasChallenge) { + byte[] token; + try { + token = mUtils.verifyPassword(mChosenPassword, mChallenge, userId); + } catch (RequestThrottledException e) { + token = null; + } + + if (token == null) { + Log.e(TAG, "critical: no token returned for known good password."); + } + + result = new Intent(); + result.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token); + } + + return result; + } } } diff --git a/src/com/android/settings/ChooseLockPattern.java b/src/com/android/settings/ChooseLockPattern.java index 087a23e..1dd24f2 100644 --- a/src/com/android/settings/ChooseLockPattern.java +++ b/src/com/android/settings/ChooseLockPattern.java @@ -19,8 +19,8 @@ package com.android.settings; import com.android.internal.logging.MetricsLogger; import com.google.android.collect.Lists; import com.android.internal.widget.LinearLayoutWithDefaultTouchRecepient; -import com.android.internal.widget.LockPatternChecker; import com.android.internal.widget.LockPatternUtils; +import com.android.internal.widget.LockPatternUtils.RequestThrottledException; import com.android.internal.widget.LockPatternView; import com.android.internal.widget.LockPatternView.Cell; import com.android.settings.notification.RedactionInterstitial; @@ -31,7 +31,6 @@ import android.app.Activity; import android.app.Fragment; import android.content.Context; import android.content.Intent; -import android.os.AsyncTask; import android.os.Bundle; import android.os.UserHandle; import android.util.Log; @@ -125,7 +124,7 @@ public class ChooseLockPattern extends SettingsActivity { } public static class ChooseLockPatternFragment extends InstrumentedFragment - implements View.OnClickListener { + implements View.OnClickListener, SaveAndFinishWorker.Listener { public static final int CONFIRM_EXISTING_REQUEST = 55; @@ -137,6 +136,8 @@ public class ChooseLockPattern extends SettingsActivity { private static final int ID_EMPTY_MESSAGE = -1; + private static final String FRAGMENT_TAG_SAVE_AND_FINISH = "save_and_finish_worker"; + private String mCurrentPattern; private boolean mHasChallenge; private long mChallenge; @@ -346,7 +347,6 @@ public class ChooseLockPattern extends SettingsActivity { } private Stage mUiStage = Stage.Introduction; - private boolean mDone = false; private Runnable mClearPatternRunnable = new Runnable() { public void run() { @@ -355,7 +355,7 @@ public class ChooseLockPattern extends SettingsActivity { }; private ChooseLockSettingsHelper mChooseLockSettingsHelper; - private AsyncTask<?, ?, ?> mPendingLockCheck; + private SaveAndFinishWorker mSaveAndFinishWorker; private static final String KEY_UI_STAGE = "uiStage"; private static final String KEY_PATTERN_CHOICE = "chosenPattern"; @@ -434,22 +434,29 @@ public class ChooseLockPattern extends SettingsActivity { mCurrentPattern = savedInstanceState.getString(KEY_CURRENT_PATTERN); } updateStage(Stage.values()[savedInstanceState.getInt(KEY_UI_STAGE)]); + + // Re-attach to the exiting worker if there is one. + mSaveAndFinishWorker = (SaveAndFinishWorker) getFragmentManager().findFragmentByTag( + FRAGMENT_TAG_SAVE_AND_FINISH); } - mDone = false; } @Override public void onResume() { super.onResume(); - mLockPatternView.enableInput(); + updateStage(mUiStage); + + if (mSaveAndFinishWorker != null) { + setRightButtonEnabled(false); + mSaveAndFinishWorker.setListener(this); + } } @Override public void onPause() { super.onPause(); - if (mPendingLockCheck != null) { - mPendingLockCheck.cancel(false); - mPendingLockCheck = null; + if (mSaveAndFinishWorker != null) { + mSaveAndFinishWorker.setListener(null); } } @@ -483,7 +490,7 @@ public class ChooseLockPattern extends SettingsActivity { throw new IllegalStateException("expected ui stage " + Stage.ChoiceConfirmed + " when button is " + RightButtonMode.Confirm); } - saveChosenPatternAndFinish(); + startSaveAndFinish(); } else if (mUiStage.rightMode == RightButtonMode.Ok) { if (mUiStage != Stage.HelpScreen) { throw new IllegalStateException("Help screen is only mode with ok button, " @@ -570,7 +577,7 @@ public class ChooseLockPattern extends SettingsActivity { setRightButtonText(stage.rightMode.text); setRightButtonEnabled(stage.rightMode.enabled); - // same for whether the patten is enabled + // same for whether the pattern is enabled if (stage.patternEnabled) { mLockPatternView.enableInput(); } else { @@ -615,7 +622,6 @@ public class ChooseLockPattern extends SettingsActivity { } } - // clear the wrong pattern unless they have started a new one // already private void postClearPatternRunnable() { @@ -623,77 +629,90 @@ public class ChooseLockPattern extends SettingsActivity { mLockPatternView.postDelayed(mClearPatternRunnable, WRONG_PATTERN_CLEAR_TIMEOUT_MS); } - private void saveChosenPatternAndFinish() { - if (mDone) return; - LockPatternUtils utils = mChooseLockSettingsHelper.utils(); - final boolean lockVirgin = !utils.isPatternEverChosen(UserHandle.myUserId()); + private void startSaveAndFinish() { + if (mSaveAndFinishWorker != null) { + Log.w(TAG, "startSaveAndFinish with an existing SaveAndFinishWorker."); + return; + } + + setRightButtonEnabled(false); - boolean wasSecureBefore = utils.isSecure(UserHandle.myUserId()); + mSaveAndFinishWorker = new SaveAndFinishWorker(); + getFragmentManager().beginTransaction().add(mSaveAndFinishWorker, + FRAGMENT_TAG_SAVE_AND_FINISH).commit(); + mSaveAndFinishWorker.setListener(this); final boolean required = getActivity().getIntent().getBooleanExtra( EncryptionInterstitial.EXTRA_REQUIRE_PASSWORD, true); + mSaveAndFinishWorker.start(mChooseLockSettingsHelper.utils(), required, + mHasChallenge, mChallenge, mChosenPattern, mCurrentPattern); + } - utils.setCredentialRequiredToDecrypt(required); - utils.saveLockPattern(mChosenPattern, mCurrentPattern, UserHandle.myUserId()); + @Override + public void onChosenLockSaveFinished(boolean wasSecureBefore, Intent resultData) { + getActivity().setResult(RESULT_FINISHED, resultData); + getActivity().finish(); - if (lockVirgin) { - utils.setVisiblePatternEnabled(true, UserHandle.myUserId()); + if (!wasSecureBefore) { + Intent intent = getRedactionInterstitialIntent(getActivity()); + if (intent != null) { + startActivity(intent); + } } + } + } + + private static class SaveAndFinishWorker extends SaveChosenLockWorkerBase { + + private List<LockPatternView.Cell> mChosenPattern; + private String mCurrentPattern; + private boolean mLockVirgin; + + public void start(LockPatternUtils utils, boolean credentialRequired, + boolean hasChallenge, long challenge, + List<LockPatternView.Cell> chosenPattern, String currentPattern) { + prepare(utils, credentialRequired, hasChallenge, challenge); + + mCurrentPattern = currentPattern; + mChosenPattern = chosenPattern; + + mLockVirgin = !mUtils.isPatternEverChosen(UserHandle.myUserId()); + + start(); + } + + @Override + protected Intent saveAndVerifyInBackground() { + Intent result = null; + final int userId = UserHandle.myUserId(); + mUtils.saveLockPattern(mChosenPattern, mCurrentPattern, userId); if (mHasChallenge) { - startVerifyPattern(utils, wasSecureBefore); - } else { - if (!wasSecureBefore) { - Intent intent = getRedactionInterstitialIntent(getActivity()); - if (intent != null) { - startActivity(intent); - } + byte[] token; + try { + token = mUtils.verifyPattern(mChosenPattern, mChallenge, userId); + } catch (RequestThrottledException e) { + token = null; + } + + if (token == null) { + Log.e(TAG, "critical: no token returned for known good pattern"); } - getActivity().setResult(RESULT_FINISHED); - doFinish(); - } - } - private void startVerifyPattern(LockPatternUtils utils, final boolean wasSecureBefore) { - mLockPatternView.disableInput(); - if (mPendingLockCheck != null) { - mPendingLockCheck.cancel(false); + result = new Intent(); + result.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token); } - mPendingLockCheck = LockPatternChecker.verifyPattern( - utils, - mChosenPattern, - mChallenge, - UserHandle.myUserId(), - new LockPatternChecker.OnVerifyCallback() { - @Override - public void onVerified(byte[] token, int timeoutMs) { - if (token == null) { - Log.e(TAG, "critical: no token returned for known good pattern"); - } - - mLockPatternView.enableInput(); - mPendingLockCheck = null; - - if (!wasSecureBefore) { - Intent intent = getRedactionInterstitialIntent(getActivity()); - if (intent != null) { - startActivity(intent); - } - } - - Intent intent = new Intent(); - intent.putExtra( - ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token); - getActivity().setResult(RESULT_FINISHED, intent); - doFinish(); - } - }); + return result; } - private void doFinish() { - getActivity().finish(); - mDone = true; + @Override + protected void finish(Intent resultData) { + if (mLockVirgin) { + mUtils.setVisiblePatternEnabled(true, UserHandle.myUserId()); + } + + super.finish(resultData); } } } diff --git a/src/com/android/settings/ChooseLockSettingsHelper.java b/src/com/android/settings/ChooseLockSettingsHelper.java index 327e622..665bffe 100644 --- a/src/com/android/settings/ChooseLockSettingsHelper.java +++ b/src/com/android/settings/ChooseLockSettingsHelper.java @@ -158,10 +158,19 @@ public final class ChooseLockSettingsHelper { intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_HAS_CHALLENGE, hasChallenge); intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE, challenge); intent.setClassName(ConfirmDeviceCredentialBaseFragment.PACKAGE, activityClass.getName()); - if (mFragment != null) { - mFragment.startActivityForResult(intent, request); + if (external) { + intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); + if (mFragment != null) { + mFragment.startActivity(intent); + } else { + mActivity.startActivity(intent); + } } else { - mActivity.startActivityForResult(intent, request); + if (mFragment != null) { + mFragment.startActivityForResult(intent, request); + } else { + mActivity.startActivityForResult(intent, request); + } } return true; } diff --git a/src/com/android/settings/ColorModePreference.java b/src/com/android/settings/ColorModePreference.java new file mode 100644 index 0000000..14ffe48 --- /dev/null +++ b/src/com/android/settings/ColorModePreference.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2015 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.settings; + +import android.app.AlertDialog.Builder; +import android.content.Context; +import android.content.res.Resources; +import android.hardware.display.DisplayManager; +import android.hardware.display.DisplayManager.DisplayListener; +import android.os.Handler; +import android.os.Looper; +import android.preference.DialogPreference; +import android.preference.SwitchPreference; +import android.util.AttributeSet; +import android.view.Display; +import android.view.Display.ColorTransform; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Checkable; +import android.widget.LinearLayout; +import android.widget.TextView; + +import java.util.ArrayList; + +public class ColorModePreference extends SwitchPreference implements DisplayListener { + + private DisplayManager mDisplayManager; + private Display mDisplay; + + private int mCurrentIndex; + private ArrayList<ColorTransformDescription> mDescriptions; + + public ColorModePreference(Context context, AttributeSet attrs) { + super(context, attrs); + mDisplayManager = getContext().getSystemService(DisplayManager.class); + } + + public int getTransformsCount() { + return mDescriptions.size(); + } + + public void startListening() { + mDisplayManager.registerDisplayListener(this, new Handler(Looper.getMainLooper())); + } + + public void stopListening() { + mDisplayManager.unregisterDisplayListener(this); + } + + @Override + public void onDisplayAdded(int displayId) { + if (displayId == Display.DEFAULT_DISPLAY) { + updateCurrentAndSupported(); + } + } + + @Override + public void onDisplayChanged(int displayId) { + if (displayId == Display.DEFAULT_DISPLAY) { + updateCurrentAndSupported(); + } + } + + @Override + public void onDisplayRemoved(int displayId) { + } + + public void updateCurrentAndSupported() { + mDisplay = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY); + + mDescriptions = new ArrayList<>(); + + Resources resources = getContext().getResources(); + int[] transforms = resources.getIntArray( + com.android.internal.R.array.config_colorTransforms); + String[] titles = resources.getStringArray(R.array.color_mode_names); + String[] descriptions = resources.getStringArray(R.array.color_mode_descriptions); + // Map the resource information describing color transforms. + for (int i = 0; i < transforms.length; i++) { + if (transforms[i] != -1 && i != 1 /* Skip Natural for now. */) { + ColorTransformDescription desc = new ColorTransformDescription(); + desc.colorTransform = transforms[i]; + desc.title = titles[i]; + desc.summary = descriptions[i]; + mDescriptions.add(desc); + } + } + // Match up a ColorTransform to every description. + ColorTransform[] supportedColorTransforms = mDisplay.getSupportedColorTransforms(); + for (int i = 0; i < supportedColorTransforms.length; i++) { + for (int j = 0; j < mDescriptions.size(); j++) { + if (mDescriptions.get(j).colorTransform + == supportedColorTransforms[i].getColorTransform() + && mDescriptions.get(j).transform == null) { + mDescriptions.get(j).transform = supportedColorTransforms[i]; + break; + } + } + } + // Remove any extras that don't have a transform for some reason. + for (int i = 0; i < mDescriptions.size(); i++) { + if (mDescriptions.get(i).transform == null) { + mDescriptions.remove(i--); + } + } + + ColorTransform currentTransform = mDisplay.getColorTransform(); + mCurrentIndex = -1; + for (int i = 0; i < mDescriptions.size(); i++) { + if (mDescriptions.get(i).colorTransform == currentTransform.getColorTransform()) { + mCurrentIndex = i; + break; + } + } + setChecked(mCurrentIndex == 1); + } + + @Override + protected boolean persistBoolean(boolean value) { + // Right now this is a switch, so we only support two modes. + if (mDescriptions.size() == 2) { + ColorTransformDescription desc = mDescriptions.get(value ? 1 : 0); + + mDisplay.requestColorTransform(desc.transform); + mCurrentIndex = mDescriptions.indexOf(desc); + } + + return true; + } + + private static class ColorTransformDescription { + private int colorTransform; + private String title; + private String summary; + private ColorTransform transform; + } +} diff --git a/src/com/android/settings/ConfirmDeviceCredentialActivity.java b/src/com/android/settings/ConfirmDeviceCredentialActivity.java index da39a0f..86935c3 100644 --- a/src/com/android/settings/ConfirmDeviceCredentialActivity.java +++ b/src/com/android/settings/ConfirmDeviceCredentialActivity.java @@ -58,23 +58,12 @@ public class ConfirmDeviceCredentialActivity extends Activity { String title = intent.getStringExtra(KeyguardManager.EXTRA_TITLE); String details = intent.getStringExtra(KeyguardManager.EXTRA_DESCRIPTION); - // Ignore rotates and ensure we only launch this once - if (savedInstanceState == null) { - ChooseLockSettingsHelper helper = new ChooseLockSettingsHelper(this); - if (!helper.launchConfirmationActivity(0 /* request code */, null /* title */, title, - details, false /* returnCredentials */, true /* isExternal */)) { - Log.d(TAG, "No pattern, password or PIN set."); - setResult(Activity.RESULT_OK); - finish(); - } + ChooseLockSettingsHelper helper = new ChooseLockSettingsHelper(this); + if (!helper.launchConfirmationActivity(0 /* request code */, null /* title */, title, + details, false /* returnCredentials */, true /* isExternal */)) { + Log.d(TAG, "No pattern, password or PIN set."); + setResult(Activity.RESULT_OK); } - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - boolean credentialsConfirmed = (resultCode == Activity.RESULT_OK); - Log.d(TAG, "Device credentials confirmed: " + credentialsConfirmed); - setResult(credentialsConfirmed ? Activity.RESULT_OK : Activity.RESULT_CANCELED); finish(); } } diff --git a/src/com/android/settings/ConfirmDeviceCredentialBaseActivity.java b/src/com/android/settings/ConfirmDeviceCredentialBaseActivity.java index 176efbc..d9af800 100644 --- a/src/com/android/settings/ConfirmDeviceCredentialBaseActivity.java +++ b/src/com/android/settings/ConfirmDeviceCredentialBaseActivity.java @@ -19,6 +19,7 @@ package com.android.settings; import android.app.Fragment; import android.app.KeyguardManager; import android.os.Bundle; +import android.os.Handler; import android.view.MenuItem; import android.view.WindowManager; @@ -28,6 +29,7 @@ public abstract class ConfirmDeviceCredentialBaseActivity extends SettingsActivi private boolean mDark; private boolean mEnterAnimationPending; private boolean mFirstTimeVisible = true; + private final Handler mHandler = new Handler(); @Override protected void onCreate(Bundle savedState) { @@ -67,6 +69,7 @@ public abstract class ConfirmDeviceCredentialBaseActivity extends SettingsActivi mFirstTimeVisible = false; prepareEnterAnimation(); mEnterAnimationPending = true; + mHandler.postDelayed(mEnterAnimationCompleteTimeoutRunnable, 1000); } } @@ -82,6 +85,7 @@ public abstract class ConfirmDeviceCredentialBaseActivity extends SettingsActivi public void onEnterAnimationComplete() { super.onEnterAnimationComplete(); if (mEnterAnimationPending) { + mHandler.removeCallbacks(mEnterAnimationCompleteTimeoutRunnable); startEnterAnimation(); mEnterAnimationPending = false; } @@ -94,4 +98,15 @@ public abstract class ConfirmDeviceCredentialBaseActivity extends SettingsActivi public void startEnterAnimation() { getFragment().startEnterAnimation(); } + + /** + * Workaround for a bug in window manager which results that onEnterAnimationComplete doesn't + * get called in all cases. + */ + private final Runnable mEnterAnimationCompleteTimeoutRunnable = new Runnable() { + @Override + public void run() { + onEnterAnimationComplete(); + } + }; } diff --git a/src/com/android/settings/ConfirmLockPassword.java b/src/com/android/settings/ConfirmLockPassword.java index 1c42045..cce29dd 100644 --- a/src/com/android/settings/ConfirmLockPassword.java +++ b/src/com/android/settings/ConfirmLockPassword.java @@ -16,10 +16,8 @@ package com.android.settings; -import android.os.UserHandle; import android.text.TextUtils; import com.android.internal.logging.MetricsLogger; -import com.android.internal.util.ArrayUtils; import com.android.internal.widget.LockPatternChecker; import com.android.internal.widget.LockPatternUtils; import com.android.internal.widget.TextViewInputDisabler; @@ -78,19 +76,20 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { } public static class ConfirmLockPasswordFragment extends ConfirmDeviceCredentialBaseFragment - implements OnClickListener, OnEditorActionListener { - private static final String KEY_NUM_WRONG_CONFIRM_ATTEMPTS - = "confirm_lock_password_fragment.key_num_wrong_confirm_attempts"; + implements OnClickListener, OnEditorActionListener, + CredentialCheckResultTracker.Listener { private static final long ERROR_MESSAGE_TIMEOUT = 3000; + private static final String FRAGMENT_TAG_CHECK_LOCK_RESULT = "check_lock_result"; private TextView mPasswordEntry; private TextViewInputDisabler mPasswordEntryInputDisabler; private LockPatternUtils mLockPatternUtils; private AsyncTask<?, ?, ?> mPendingLockCheck; + private CredentialCheckResultTracker mCredentialCheckResultTracker; + private boolean mDisappearing = false; private TextView mHeaderTextView; private TextView mDetailsTextView; private TextView mErrorTextView; private Handler mHandler = new Handler(); - private int mNumWrongConfirmAttempts; private CountDownTimer mCountdownTimer; private boolean mIsAlpha; private InputMethodManager mImm; @@ -110,11 +109,6 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { super.onCreate(savedInstanceState); mLockPatternUtils = new LockPatternUtils(getActivity()); mEffectiveUserId = Utils.getEffectiveUserId(getActivity()); - - if (savedInstanceState != null) { - mNumWrongConfirmAttempts = savedInstanceState.getInt( - KEY_NUM_WRONG_CONFIRM_ATTEMPTS, 0); - } } @Override @@ -165,6 +159,15 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { 0.5f /* delayScale */, AnimationUtils.loadInterpolator( getContext(), android.R.interpolator.fast_out_linear_in)); setAccessibilityTitle(mHeaderTextView.getText()); + + mCredentialCheckResultTracker = (CredentialCheckResultTracker) getFragmentManager() + .findFragmentByTag(FRAGMENT_TAG_CHECK_LOCK_RESULT); + if (mCredentialCheckResultTracker == null) { + mCredentialCheckResultTracker = new CredentialCheckResultTracker(); + getFragmentManager().beginTransaction().add(mCredentialCheckResultTracker, + FRAGMENT_TAG_CHECK_LOCK_RESULT).commit(); + } + return view; } @@ -227,10 +230,7 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { mCountdownTimer.cancel(); mCountdownTimer = null; } - if (mPendingLockCheck != null) { - mPendingLockCheck.cancel(false); - mPendingLockCheck = null; - } + mCredentialCheckResultTracker.setListener(null); } @Override @@ -243,21 +243,17 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { super.onResume(); long deadline = mLockPatternUtils.getLockoutAttemptDeadline(mEffectiveUserId); if (deadline != 0) { + mCredentialCheckResultTracker.clearResult(); handleAttemptLockout(deadline); } else { resetState(); } - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.putInt(KEY_NUM_WRONG_CONFIRM_ATTEMPTS, mNumWrongConfirmAttempts); + mCredentialCheckResultTracker.setListener(this); } @Override protected void authenticationSucceeded() { - startDisappearAnimation(new Intent()); + mCredentialCheckResultTracker.setResult(true, new Intent(), 0, mEffectiveUserId); } @Override @@ -298,11 +294,12 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { } private void handleNext() { - mPasswordEntryInputDisabler.setInputEnabled(false); - if (mPendingLockCheck != null) { - mPendingLockCheck.cancel(false); + if (mPendingLockCheck != null || mDisappearing) { + return; } + mPasswordEntryInputDisabler.setInputEnabled(false); + final String pin = mPasswordEntry.getText().toString(); final boolean verifyChallenge = getActivity().getIntent().getBooleanExtra( ChooseLockSettingsHelper.EXTRA_KEY_HAS_CHALLENGE, false); @@ -317,7 +314,7 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { return; } - onPasswordChecked(false, intent, 0, mEffectiveUserId); + mCredentialCheckResultTracker.setResult(false, intent, 0, mEffectiveUserId); } private boolean isInternalActivity() { @@ -344,7 +341,8 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token); } - onPasswordChecked(matched, intent, timeoutMs, localEffectiveUserId); + mCredentialCheckResultTracker.setResult(matched, intent, timeoutMs, + localEffectiveUserId); } }); } @@ -366,16 +364,27 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { intent.putExtra( ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD, pin); } - onPasswordChecked(matched, intent, timeoutMs, localEffectiveUserId); + mCredentialCheckResultTracker.setResult(matched, intent, timeoutMs, + localEffectiveUserId); } }); } private void startDisappearAnimation(final Intent intent) { + if (mDisappearing) { + return; + } + mDisappearing = true; + if (getActivity().getThemeResId() == R.style.Theme_ConfirmDeviceCredentialsDark) { mDisappearAnimationUtils.startAnimation(getActiveViews(), new Runnable() { @Override public void run() { + // Bail if there is no active activity. + if (getActivity() == null || getActivity().isFinishing()) { + return; + } + getActivity().setResult(RESULT_OK, intent); getActivity().finish(); getActivity().overridePendingTransition( @@ -405,6 +414,12 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { } } + @Override + public void onCredentialChecked(boolean matched, Intent intent, int timeoutMs, + int effectiveUserId) { + onPasswordChecked(matched, intent, timeoutMs, effectiveUserId); + } + private void handleAttemptLockout(long elapsedRealtimeDeadline) { long elapsedRealtime = SystemClock.elapsedRealtime(); mPasswordEntry.setEnabled(false); @@ -424,7 +439,6 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { public void onFinish() { resetState(); mErrorTextView.setText(""); - mNumWrongConfirmAttempts = 0; } }.start(); } diff --git a/src/com/android/settings/ConfirmLockPattern.java b/src/com/android/settings/ConfirmLockPattern.java index 51cde60..94ba01a 100644 --- a/src/com/android/settings/ConfirmLockPattern.java +++ b/src/com/android/settings/ConfirmLockPattern.java @@ -32,7 +32,6 @@ import android.os.CountDownTimer; import android.os.SystemClock; import android.os.AsyncTask; import android.os.Bundle; -import android.os.UserHandle; import android.os.storage.StorageManager; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; @@ -76,17 +75,18 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { } public static class ConfirmLockPatternFragment extends ConfirmDeviceCredentialBaseFragment - implements AppearAnimationCreator<Object> { + implements AppearAnimationCreator<Object>, CredentialCheckResultTracker.Listener { // how long we wait to clear a wrong pattern private static final int WRONG_PATTERN_CLEAR_TIMEOUT_MS = 2000; - private static final String KEY_NUM_WRONG_ATTEMPTS = "num_wrong_attempts"; + private static final String FRAGMENT_TAG_CHECK_LOCK_RESULT = "check_lock_result"; private LockPatternView mLockPatternView; private LockPatternUtils mLockPatternUtils; private AsyncTask<?, ?, ?> mPendingLockCheck; - private int mNumWrongConfirmAttempts; + private CredentialCheckResultTracker mCredentialCheckResultTracker; + private boolean mDisappearing = false; private CountDownTimer mCountdownTimer; private TextView mHeaderTextView; @@ -148,9 +148,7 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { mLockPatternView.setOnPatternListener(mConfirmExistingLockPatternListener); updateStage(Stage.NeedToUnlock); - if (savedInstanceState != null) { - mNumWrongConfirmAttempts = savedInstanceState.getInt(KEY_NUM_WRONG_ATTEMPTS); - } else { + if (savedInstanceState == null) { // on first launch, if no lock pattern is set, then finish with // success (don't want user to get stuck confirming something that // doesn't exist). @@ -174,13 +172,21 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { } }); setAccessibilityTitle(mHeaderTextView.getText()); + + mCredentialCheckResultTracker = (CredentialCheckResultTracker) getFragmentManager() + .findFragmentByTag(FRAGMENT_TAG_CHECK_LOCK_RESULT); + if (mCredentialCheckResultTracker == null) { + mCredentialCheckResultTracker = new CredentialCheckResultTracker(); + getFragmentManager().beginTransaction().add(mCredentialCheckResultTracker, + FRAGMENT_TAG_CHECK_LOCK_RESULT).commit(); + } + return view; } @Override public void onSaveInstanceState(Bundle outState) { // deliberately not calling super since we are managing this in full - outState.putInt(KEY_NUM_WRONG_ATTEMPTS, mNumWrongConfirmAttempts); } @Override @@ -190,10 +196,7 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { if (mCountdownTimer != null) { mCountdownTimer.cancel(); } - if (mPendingLockCheck != null) { - mPendingLockCheck.cancel(false); - mPendingLockCheck = null; - } + mCredentialCheckResultTracker.setListener(null); } @Override @@ -208,13 +211,14 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { // if the user is currently locked out, enforce it. long deadline = mLockPatternUtils.getLockoutAttemptDeadline(mEffectiveUserId); if (deadline != 0) { + mCredentialCheckResultTracker.clearResult(); handleAttemptLockout(deadline); } else if (!mLockPatternView.isEnabled()) { // The deadline has passed, but the timer was cancelled. Or the pending lock // check was cancelled. Need to clean up. - mNumWrongConfirmAttempts = 0; updateStage(Stage.NeedToUnlock); } + mCredentialCheckResultTracker.setListener(this); } @Override @@ -317,16 +321,26 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { @Override protected void authenticationSucceeded() { - startDisappearAnimation(new Intent()); + mCredentialCheckResultTracker.setResult(true, new Intent(), 0, mEffectiveUserId); } private void startDisappearAnimation(final Intent intent) { + if (mDisappearing) { + return; + } + mDisappearing = true; + if (getActivity().getThemeResId() == R.style.Theme_ConfirmDeviceCredentialsDark) { mLockPatternView.clearPattern(); mDisappearAnimationUtils.startAnimation2d(getActiveViews(), new Runnable() { @Override public void run() { + // Bail if there is no active activity. + if (getActivity() == null || getActivity().isFinishing()) { + return; + } + getActivity().setResult(RESULT_OK, intent); getActivity().finish(); getActivity().overridePendingTransition( @@ -370,11 +384,12 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { } public void onPatternDetected(List<LockPatternView.Cell> pattern) { - mLockPatternView.setEnabled(false); - if (mPendingLockCheck != null) { - mPendingLockCheck.cancel(false); + if (mPendingLockCheck != null || mDisappearing) { + return; } + mLockPatternView.setEnabled(false); + final boolean verifyChallenge = getActivity().getIntent().getBooleanExtra( ChooseLockSettingsHelper.EXTRA_KEY_HAS_CHALLENGE, false); Intent intent = new Intent(); @@ -388,7 +403,7 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { return; } - onPatternChecked(pattern, false, intent, 0, mEffectiveUserId); + mCredentialCheckResultTracker.setResult(false, intent, 0, mEffectiveUserId); } private boolean isInternalActivity() { @@ -416,8 +431,8 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token); } - onPatternChecked(pattern, - matched, intent, timeoutMs, localEffectiveUserId); + mCredentialCheckResultTracker.setResult(matched, intent, timeoutMs, + localEffectiveUserId); } }); } @@ -425,7 +440,7 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { private void startCheckPattern(final List<LockPatternView.Cell> pattern, final Intent intent) { if (pattern.size() < LockPatternUtils.MIN_PATTERN_REGISTER_FAIL) { - onPatternChecked(pattern, false, intent, 0, mEffectiveUserId); + mCredentialCheckResultTracker.setResult(false, intent, 0, mEffectiveUserId); return; } @@ -444,29 +459,35 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD, LockPatternUtils.patternToString(pattern)); } - onPatternChecked(pattern, matched, intent, timeoutMs, + mCredentialCheckResultTracker.setResult(matched, intent, timeoutMs, localEffectiveUserId); } }); } + }; - private void onPatternChecked(List<LockPatternView.Cell> pattern, - boolean matched, Intent intent, int timeoutMs, int effectiveUserId) { - mLockPatternView.setEnabled(true); - if (matched) { - startDisappearAnimation(intent); + private void onPatternChecked(boolean matched, Intent intent, int timeoutMs, + int effectiveUserId) { + mLockPatternView.setEnabled(true); + if (matched) { + startDisappearAnimation(intent); + } else { + if (timeoutMs > 0) { + long deadline = mLockPatternUtils.setLockoutAttemptDeadline( + effectiveUserId, timeoutMs); + handleAttemptLockout(deadline); } else { - if (timeoutMs > 0) { - long deadline = mLockPatternUtils.setLockoutAttemptDeadline( - effectiveUserId, timeoutMs); - handleAttemptLockout(deadline); - } else { - updateStage(Stage.NeedToUnlockWrong); - postClearPatternRunnable(); - } + updateStage(Stage.NeedToUnlockWrong); + postClearPatternRunnable(); } } - }; + } + + @Override + public void onCredentialChecked(boolean matched, Intent intent, int timeoutMs, + int effectiveUserId) { + onPatternChecked(matched, intent, timeoutMs, effectiveUserId); + } private void handleAttemptLockout(long elapsedRealtimeDeadline) { updateStage(Stage.LockedOut); @@ -485,7 +506,6 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { @Override public void onFinish() { - mNumWrongConfirmAttempts = 0; updateStage(Stage.NeedToUnlock); } }.start(); diff --git a/src/com/android/settings/CredentialCheckResultTracker.java b/src/com/android/settings/CredentialCheckResultTracker.java new file mode 100644 index 0000000..5c55073 --- /dev/null +++ b/src/com/android/settings/CredentialCheckResultTracker.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2015 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.settings; + +import android.app.Fragment; +import android.content.Intent; +import android.os.Bundle; + +/** + * An invisible retained fragment to track lock check result. + */ +public class CredentialCheckResultTracker extends Fragment { + + private Listener mListener; + private boolean mHasResult = false; + + private boolean mResultMatched; + private Intent mResultData; + private int mResultTimeoutMs; + private int mResultEffectiveUserId; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setRetainInstance(true); + } + + public void setListener(Listener listener) { + if (mListener == listener) { + return; + } + + mListener = listener; + if (mListener != null && mHasResult) { + mListener.onCredentialChecked(mResultMatched, mResultData, mResultTimeoutMs, + mResultEffectiveUserId); + } + } + + public void setResult(boolean matched, Intent intent, int timeoutMs, int effectiveUserId) { + mResultMatched = matched; + mResultData = intent; + mResultTimeoutMs = timeoutMs; + mResultEffectiveUserId = effectiveUserId; + + mHasResult = true; + if (mListener != null) { + mListener.onCredentialChecked(mResultMatched, mResultData, mResultTimeoutMs, + mResultEffectiveUserId); + } + } + + public void clearResult() { + mHasResult = false; + mResultMatched = false; + mResultData = null; + mResultTimeoutMs = 0; + mResultEffectiveUserId = 0; + } + + interface Listener { + public void onCredentialChecked(boolean matched, Intent intent, int timeoutMs, + int effectiveUserId); + } +} diff --git a/src/com/android/settings/DevelopmentSettings.java b/src/com/android/settings/DevelopmentSettings.java index 916788d..d27d89b 100644 --- a/src/com/android/settings/DevelopmentSettings.java +++ b/src/com/android/settings/DevelopmentSettings.java @@ -162,6 +162,7 @@ public class DevelopmentSettings extends SettingsPreferenceFragment private static final String USB_CONFIGURATION_KEY = "select_usb_configuration"; private static final String WIFI_LEGACY_DHCP_CLIENT_KEY = "legacy_dhcp_client"; private static final String MOBILE_DATA_ALWAYS_ON = "mobile_data_always_on"; + private static final String KEY_COLOR_MODE = "color_mode"; private static final String INACTIVE_APPS_KEY = "inactive_apps"; @@ -269,6 +270,7 @@ public class DevelopmentSettings extends SettingsPreferenceFragment private ListPreference mRootAccess; private Object mSelectedRootValue; private PreferenceScreen mDevelopmentTools; + private ColorModePreference mColorModePreference; private final ArrayList<Preference> mAllPrefs = new ArrayList<Preference>(); @@ -439,6 +441,13 @@ public class DevelopmentSettings extends SettingsPreferenceFragment mDevelopmentTools = (PreferenceScreen) findPreference(DEVELOPMENT_TOOLS); mAllPrefs.add(mDevelopmentTools); + + mColorModePreference = (ColorModePreference) findPreference(KEY_COLOR_MODE); + mColorModePreference.updateCurrentAndSupported(); + if (mColorModePreference.getTransformsCount() < 2) { + removePreference(KEY_COLOR_MODE); + mColorModePreference = null; + } } private ListPreference addListPreference(String prefKey) { @@ -556,6 +565,19 @@ public class DevelopmentSettings extends SettingsPreferenceFragment setPrefsEnabledState(mLastEnabledState); } mSwitchBar.show(); + + if (mColorModePreference != null) { + mColorModePreference.startListening(); + mColorModePreference.updateCurrentAndSupported(); + } + } + + @Override + public void onPause() { + super.onPause(); + if (mColorModePreference != null) { + mColorModePreference.stopListening(); + } } @Override diff --git a/src/com/android/settings/DeviceInfoSettings.java b/src/com/android/settings/DeviceInfoSettings.java index fd3dae1..0cccc01 100644 --- a/src/com/android/settings/DeviceInfoSettings.java +++ b/src/com/android/settings/DeviceInfoSettings.java @@ -65,6 +65,7 @@ public class DeviceInfoSettings extends SettingsPreferenceFragment implements In private static final String FILENAME_PROC_VERSION = "/proc/version"; private static final String FILENAME_MSV = "/sys/board_properties/soc/msv"; + private static final String KEY_MANUAL = "manual"; private static final String KEY_REGULATORY_INFO = "regulatory_info"; private static final String KEY_SYSTEM_UPDATE_SETTINGS = "system_update_settings"; private static final String PROPERTY_URL_SAFETYLEGAL = "ro.url.safetylegal"; @@ -188,6 +189,9 @@ public class DeviceInfoSettings extends SettingsPreferenceFragment implements In removePreferenceIfBoolFalse(KEY_UPDATE_SETTING, R.bool.config_additional_system_update_setting_enable); + // Remove manual entry if none present. + removePreferenceIfBoolFalse(KEY_MANUAL, R.bool.config_show_manual); + // Remove regulatory information if none present or config_show_regulatory_info is disabled final Intent intent = new Intent(Settings.ACTION_SHOW_REGULATORY_INFO); if (getPackageManager().queryIntentActivities(intent, 0).isEmpty() diff --git a/src/com/android/settings/DisplaySettings.java b/src/com/android/settings/DisplaySettings.java index 6d716f0..6582c26 100644 --- a/src/com/android/settings/DisplaySettings.java +++ b/src/com/android/settings/DisplaySettings.java @@ -24,6 +24,9 @@ import com.android.internal.view.RotationPolicy; import com.android.settings.DropDownPreference.Callback; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; + +import static android.provider.Settings.Secure.CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED; +import static android.provider.Settings.Secure.CAMERA_GESTURE_DISABLED; import static android.provider.Settings.Secure.DOUBLE_TAP_TO_WAKE; import static android.provider.Settings.Secure.DOZE_ENABLED; import static android.provider.Settings.Secure.WAKE_GESTURE_ENABLED; @@ -85,6 +88,9 @@ public class DisplaySettings extends SettingsPreferenceFragment implements private static final String KEY_AUTO_BRIGHTNESS = "auto_brightness"; private static final String KEY_AUTO_ROTATE = "auto_rotate"; private static final String KEY_NIGHT_MODE = "night_mode"; + private static final String KEY_CAMERA_GESTURE = "camera_gesture"; + private static final String KEY_CAMERA_DOUBLE_TAP_POWER_GESTURE + = "camera_double_tap_power_gesture"; private static final String KEY_PROXIMITY_WAKE = "proximity_on_wake"; private static final String KEY_DISPLAY_ROTATION = "display_rotation"; private static final String KEY_WAKE_WHEN_PLUGGED_OR_UNPLUGGED = "wake_when_plugged_or_unplugged"; @@ -120,6 +126,8 @@ public class DisplaySettings extends SettingsPreferenceFragment implements updateDisplayRotationPreferenceDescription(); } }; + private SwitchPreference mCameraGesturePreference; + private SwitchPreference mCameraDoubleTapPowerGesturePreference; @Override protected int getMetricsCategory() { @@ -182,6 +190,21 @@ public class DisplaySettings extends SettingsPreferenceFragment implements removePreference(KEY_DOZE); } + if (isCameraGestureAvailable(getResources())) { + mCameraGesturePreference = (SwitchPreference) findPreference(KEY_CAMERA_GESTURE); + mCameraGesturePreference.setOnPreferenceChangeListener(this); + } else { + removePreference(KEY_CAMERA_GESTURE); + } + + if (isCameraDoubleTapPowerGestureAvailable(getResources())) { + mCameraDoubleTapPowerGesturePreference + = (SwitchPreference) findPreference(KEY_CAMERA_DOUBLE_TAP_POWER_GESTURE); + mCameraDoubleTapPowerGesturePreference.setOnPreferenceChangeListener(this); + } else { + removePreference(KEY_CAMERA_DOUBLE_TAP_POWER_GESTURE); + } + mNightModePreference = (ListPreference) findPreference(KEY_NIGHT_MODE); if (mNightModePreference != null) { final UiModeManager uiManager = (UiModeManager) getSystemService( @@ -275,6 +298,18 @@ public class DisplaySettings extends SettingsPreferenceFragment implements mDisplayRotationPreference.setSummary(summary); } + private static boolean isCameraGestureAvailable(Resources res) { + boolean configSet = res.getInteger( + com.android.internal.R.integer.config_cameraLaunchGestureSensorType) != -1; + return configSet && + !SystemProperties.getBoolean("gesture.disable_camera_launch", false); + } + + private static boolean isCameraDoubleTapPowerGestureAvailable(Resources res) { + return res.getBoolean( + com.android.internal.R.bool.config_cameraDoubleTapPowerGestureEnabled); + } + private void updateTimeoutPreferenceDescription(long currentTimeout) { ListPreference preference = mScreenTimeoutPreference; String summary; @@ -421,6 +456,19 @@ public class DisplaySettings extends SettingsPreferenceFragment implements int value = Settings.Secure.getInt(getContentResolver(), DOUBLE_TAP_TO_WAKE, 0); mTapToWakePreference.setChecked(value != 0); } + + // Update camera gesture #1 if it is available. + if (mCameraGesturePreference != null) { + int value = Settings.Secure.getInt(getContentResolver(), CAMERA_GESTURE_DISABLED, 0); + mCameraGesturePreference.setChecked(value == 0); + } + + // Update camera gesture #2 if it is available. + if (mCameraDoubleTapPowerGesturePreference != null) { + int value = Settings.Secure.getInt( + getContentResolver(), CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED, 0); + mCameraDoubleTapPowerGesturePreference.setChecked(value == 0); + } } private void updateScreenSaverSummary() { @@ -499,6 +547,16 @@ public class DisplaySettings extends SettingsPreferenceFragment implements boolean value = (Boolean) objValue; Settings.Secure.putInt(getContentResolver(), DOUBLE_TAP_TO_WAKE, value ? 1 : 0); } + if (preference == mCameraGesturePreference) { + boolean value = (Boolean) objValue; + Settings.Secure.putInt(getContentResolver(), CAMERA_GESTURE_DISABLED, + value ? 0 : 1 /* Backwards because setting is for disabling */); + } + if (preference == mCameraDoubleTapPowerGesturePreference) { + boolean value = (Boolean) objValue; + Settings.Secure.putInt(getContentResolver(), CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED, + value ? 0 : 1 /* Backwards because setting is for disabling */); + } if (preference == mNightModePreference) { try { final int value = Integer.parseInt((String) objValue); @@ -569,6 +627,12 @@ public class DisplaySettings extends SettingsPreferenceFragment implements com.android.internal.R.bool.config_proximityCheckOnWake)) { result.add(KEY_PROXIMITY_WAKE); } + if (!isCameraGestureAvailable(context.getResources())) { + result.add(KEY_CAMERA_GESTURE); + } + if (!isCameraDoubleTapPowerGestureAvailable(context.getResources())) { + result.add(KEY_CAMERA_DOUBLE_TAP_POWER_GESTURE); + } return result; } }; diff --git a/src/com/android/settings/HotspotOffReceiver.java b/src/com/android/settings/HotspotOffReceiver.java index 06ced1f..f3c3fee 100644 --- a/src/com/android/settings/HotspotOffReceiver.java +++ b/src/com/android/settings/HotspotOffReceiver.java @@ -5,6 +5,7 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.net.wifi.WifiManager; +import android.util.Log; import com.android.settingslib.TetherUtil; @@ -14,11 +15,15 @@ import com.android.settingslib.TetherUtil; */ public class HotspotOffReceiver extends BroadcastReceiver { + private static final String TAG = "HotspotOffReceiver"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + @Override public void onReceive(Context context, Intent intent) { if (WifiManager.WIFI_AP_STATE_CHANGED_ACTION.equals(intent.getAction())) { WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); if (wifiManager.getWifiApState() == WifiManager.WIFI_AP_STATE_DISABLED) { + if (DEBUG) Log.d(TAG, "TetherService.cancelRecheckAlarmIfNecessary called"); // The hotspot has been turned off, we don't need to recheck tethering. TetherService.cancelRecheckAlarmIfNecessary(context, TetherUtil.TETHERING_WIFI); } diff --git a/src/com/android/settings/InstrumentedActivity.java b/src/com/android/settings/InstrumentedActivity.java new file mode 100644 index 0000000..4a0e03a --- /dev/null +++ b/src/com/android/settings/InstrumentedActivity.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2015 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.settings; + +import com.android.internal.logging.MetricsLogger; + +import android.app.Activity; + +/** + * Instrumented activity that logs visibility state. + */ +public abstract class InstrumentedActivity extends Activity { + /** + * Declare the view of this category. + * + * Categories are defined in {@link com.android.internal.logging.MetricsLogger} + * or if there is no relevant existing category you may define one in + * {@link com.android.settings.InstrumentedFragment}. + */ + protected abstract int getMetricsCategory(); + + @Override + public void onResume() { + super.onResume(); + MetricsLogger.visible(this, getMetricsCategory()); + } + + @Override + public void onPause() { + super.onPause(); + MetricsLogger.hidden(this, getMetricsCategory()); + } +} diff --git a/src/com/android/settings/ManualDisplayActivity.java b/src/com/android/settings/ManualDisplayActivity.java new file mode 100644 index 0000000..8be4fee --- /dev/null +++ b/src/com/android/settings/ManualDisplayActivity.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2015 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.settings; + +import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.Intent; +import android.content.res.Resources; +import android.net.Uri; +import android.os.Bundle; +import android.os.SystemProperties; +import android.text.TextUtils; +import android.util.Log; +import android.widget.Toast; + +import java.io.File; + +/** + * The "dialog" that shows from "Manual" in the Settings app. + */ +public class ManualDisplayActivity extends Activity { + private static final String TAG = "SettingsManualActivity"; + + private static final String DEFAULT_MANUAL_PATH = "/system/etc/MANUAL.html.gz"; + private static final String PROPERTY_MANUAL_PATH = "ro.config.manual_path"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Resources resources = getResources(); + + if (!resources.getBoolean(R.bool.config_show_manual)) { + finish(); // No manual to display for this device + } + + final String path = SystemProperties.get(PROPERTY_MANUAL_PATH, DEFAULT_MANUAL_PATH); + if (TextUtils.isEmpty(path)) { + Log.e(TAG, "The system property for the manual is empty"); + showErrorAndFinish(); + return; + } + + final File file = new File(path); + if (!file.exists() || file.length() == 0) { + Log.e(TAG, "Manual file " + path + " does not exist"); + showErrorAndFinish(); + return; + } + + final Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(Uri.fromFile(file), "text/html"); + + intent.putExtra(Intent.EXTRA_TITLE, getString(R.string.settings_manual_activity_title)); + intent.addCategory(Intent.CATEGORY_DEFAULT); + intent.setPackage("com.android.htmlviewer"); + + try { + startActivity(intent); + finish(); + } catch (ActivityNotFoundException e) { + Log.e(TAG, "Failed to find viewer", e); + showErrorAndFinish(); + } + } + + private void showErrorAndFinish() { + Toast.makeText(this, R.string.settings_manual_activity_unavailable, Toast.LENGTH_LONG) + .show(); + finish(); + } +} diff --git a/src/com/android/settings/SaveChosenLockWorkerBase.java b/src/com/android/settings/SaveChosenLockWorkerBase.java new file mode 100644 index 0000000..a40871a --- /dev/null +++ b/src/com/android/settings/SaveChosenLockWorkerBase.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2015 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.settings; + +import android.app.Fragment; +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.UserHandle; + +import com.android.internal.widget.LockPatternUtils; + +/** + * An invisible retained worker fragment to track the AsyncWork that saves (and optionally + * verifies if a challenge is given) the chosen lock credential (pattern/pin/password). + */ +abstract class SaveChosenLockWorkerBase extends Fragment { + + private Listener mListener; + private boolean mFinished; + private Intent mResultData; + + protected LockPatternUtils mUtils; + protected boolean mHasChallenge; + protected long mChallenge; + protected boolean mWasSecureBefore; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setRetainInstance(true); + } + + public void setListener(Listener listener) { + if (mListener == listener) { + return; + } + + mListener = listener; + if (mFinished && mListener != null) { + mListener.onChosenLockSaveFinished(mWasSecureBefore, mResultData); + } + } + + protected void prepare(LockPatternUtils utils, boolean credentialRequired, + boolean hasChallenge, long challenge) { + mUtils = utils; + + mHasChallenge = hasChallenge; + mChallenge = challenge; + mWasSecureBefore = mUtils.isSecure(UserHandle.myUserId()); + + mUtils.setCredentialRequiredToDecrypt(credentialRequired); + + mFinished = false; + mResultData = null; + } + + protected void start() { + new Task().execute(); + } + + /** + * Executes the save and verify work in background. + * @return Intent with challenge token or null. + */ + protected abstract Intent saveAndVerifyInBackground(); + + protected void finish(Intent resultData) { + mFinished = true; + mResultData = resultData; + if (mListener != null) { + mListener.onChosenLockSaveFinished(mWasSecureBefore, mResultData); + } + } + + private class Task extends AsyncTask<Void, Void, Intent> { + @Override + protected Intent doInBackground(Void... params){ + return saveAndVerifyInBackground(); + } + + @Override + protected void onPostExecute(Intent resultData) { + finish(resultData); + } + } + + interface Listener { + public void onChosenLockSaveFinished(boolean wasSecureBefore, Intent resultData); + } +} diff --git a/src/com/android/settings/SecuritySettings.java b/src/com/android/settings/SecuritySettings.java index 6e679b6..892d61f 100644 --- a/src/com/android/settings/SecuritySettings.java +++ b/src/com/android/settings/SecuritySettings.java @@ -361,6 +361,8 @@ public class SecuritySettings extends SettingsPreferenceFragment fingerprintCount, fingerprintCount)); clazz = FingerprintSettings.class.getName(); } else { + fingerprintPreference.setSummary( + R.string.security_settings_fingerprint_preference_summary_none); clazz = FingerprintEnrollIntroduction.class.getName(); } intent.setClassName("com.android.settings", clazz); @@ -795,10 +797,16 @@ public class SecuritySettings extends SettingsPreferenceFragment FingerprintManager fpm = (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE); if (fpm.isHardwareDetected()) { + // This catches the title which can be overloaded in an overlay data = new SearchIndexableRaw(context); data.title = res.getString(R.string.security_settings_fingerprint_preference_title); data.screenTitle = screenTitle; result.add(data); + // Fallback for when the above doesn't contain "fingerprint" + data = new SearchIndexableRaw(context); + data.title = res.getString(R.string.fingerprint_manage_category_title); + data.screenTitle = screenTitle; + result.add(data); } // Credential storage diff --git a/src/com/android/settings/SettingsPreferenceFragment.java b/src/com/android/settings/SettingsPreferenceFragment.java index baf04d4..b957c38 100644 --- a/src/com/android/settings/SettingsPreferenceFragment.java +++ b/src/com/android/settings/SettingsPreferenceFragment.java @@ -505,7 +505,10 @@ public abstract class SettingsPreferenceFragment extends InstrumentedPreferenceF } public void finish() { - getActivity().onBackPressed(); + Activity activity = getActivity(); + if (activity != null) { + activity.onBackPressed(); + } } public boolean startFragment(Fragment caller, String fragmentClass, int titleRes, diff --git a/src/com/android/settings/TetherService.java b/src/com/android/settings/TetherService.java index 459dc27..346a175 100644 --- a/src/com/android/settings/TetherService.java +++ b/src/com/android/settings/TetherService.java @@ -69,7 +69,7 @@ public class TetherService extends Service { @Override public void onCreate() { super.onCreate(); - if (DEBUG) Log.d(TAG, "Creating WifiProvisionService"); + if (DEBUG) Log.d(TAG, "Creating TetherService"); String provisionResponse = getResources().getString( com.android.internal.R.string.config_mobile_hotspot_provision_response); registerReceiver(mReceiver, new IntentFilter(provisionResponse), @@ -89,21 +89,28 @@ public class TetherService extends Service { mCurrentTethers.add(type); } } + if (intent.hasExtra(TetherUtil.EXTRA_REM_TETHER_TYPE)) { - int type = intent.getIntExtra(TetherUtil.EXTRA_REM_TETHER_TYPE, - TetherUtil.TETHERING_INVALID); - if (DEBUG) Log.d(TAG, "Removing tether " + type); - int index = mCurrentTethers.indexOf(type); - if (index >= 0) { - mCurrentTethers.remove(index); - // If we are currently in the middle of a check, we may need to adjust the - // index accordingly. - if (index <= mCurrentTypeIndex && mCurrentTypeIndex > 0) { - mCurrentTypeIndex--; + if (!mInProvisionCheck) { + int type = intent.getIntExtra(TetherUtil.EXTRA_REM_TETHER_TYPE, + TetherUtil.TETHERING_INVALID); + int index = mCurrentTethers.indexOf(type); + if (DEBUG) Log.d(TAG, "Removing tether " + type + ", index " + index); + if (index >= 0) { + mCurrentTethers.remove(index); + // If we are currently in the middle of a check, we may need to adjust the + // index accordingly. + if (DEBUG) Log.d(TAG, "mCurrentTypeIndex: " + mCurrentTypeIndex); + if (index <= mCurrentTypeIndex && mCurrentTypeIndex > 0) { + mCurrentTypeIndex--; + } } + cancelAlarmIfNecessary(); + } else { + if (DEBUG) Log.d(TAG, "Don't cancel alarm during provisioning"); } - cancelAlarmIfNecessary(); } + // Only set the alarm if we have one tether, meaning the one just added, // to avoid setting it when it was already set previously for another // type. @@ -125,7 +132,7 @@ public class TetherService extends Service { } // We want to be started if we are killed accidently, so that we can be sure we finish // the check. - return START_STICKY; + return START_REDELIVER_INTENT; } @Override @@ -137,7 +144,7 @@ public class TetherService extends Service { SharedPreferences prefs = getSharedPreferences(PREFS, MODE_PRIVATE); prefs.edit().putString(KEY_TETHERS, tethersToString(mCurrentTethers)).commit(); - if (DEBUG) Log.d(TAG, "Destroying WifiProvisionService"); + if (DEBUG) Log.d(TAG, "Destroying TetherService"); unregisterReceiver(mReceiver); super.onDestroy(); } @@ -199,15 +206,17 @@ public class TetherService extends Service { } private void startProvisioning(int index) { - String provisionAction = getResources().getString( - com.android.internal.R.string.config_mobile_hotspot_provision_app_no_ui); - if (DEBUG) Log.d(TAG, "Sending provisioning broadcast: " + provisionAction + " type: " - + mCurrentTethers.get(index)); - Intent intent = new Intent(provisionAction); - intent.putExtra(TETHER_CHOICE, mCurrentTethers.get(index)); - intent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND); - sendBroadcast(intent); - mInProvisionCheck = true; + if (index < mCurrentTethers.size()) { + String provisionAction = getResources().getString( + com.android.internal.R.string.config_mobile_hotspot_provision_app_no_ui); + if (DEBUG) Log.d(TAG, "Sending provisioning broadcast: " + provisionAction + " type: " + + mCurrentTethers.get(index)); + Intent intent = new Intent(provisionAction); + intent.putExtra(TETHER_CHOICE, mCurrentTethers.get(index)); + intent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND); + sendBroadcast(intent); + mInProvisionCheck = true; + } } public static void scheduleRecheckAlarm(Context context, int type) { @@ -261,9 +270,14 @@ public class TetherService extends Service { if (DEBUG) Log.d(TAG, "Got provision result " + intent); String provisionResponse = context.getResources().getString( com.android.internal.R.string.config_mobile_hotspot_provision_response); + if (provisionResponse.equals(intent.getAction())) { - mInProvisionCheck = false; + if (!mInProvisionCheck) { + Log.e(TAG, "Unexpected provision response " + intent); + return; + } int checkType = mCurrentTethers.get(mCurrentTypeIndex); + mInProvisionCheck = false; if (intent.getIntExtra(EXTRA_RESULT, RESULT_DEFAULT) == RESULT_OK) { if (checkType == TetherUtil.TETHERING_WIFI && mEnableWifiAfterCheck) { enableWifiTetheringIfNeeded(); @@ -282,7 +296,8 @@ public class TetherService extends Service { break; } } - if (++mCurrentTypeIndex == mCurrentTethers.size()) { + + if (++mCurrentTypeIndex >= mCurrentTethers.size()) { // We are done with all checks, time to die. stopSelf(); } else { diff --git a/src/com/android/settings/deviceinfo/UsbBackend.java b/src/com/android/settings/deviceinfo/UsbBackend.java new file mode 100644 index 0000000..210e0a0 --- /dev/null +++ b/src/com/android/settings/deviceinfo/UsbBackend.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2015 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.settings.deviceinfo; + +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.hardware.usb.UsbManager; +import android.hardware.usb.UsbPort; +import android.hardware.usb.UsbPortStatus; +import android.os.UserManager; + +public class UsbBackend { + + private static final int MODE_POWER_MASK = 0x01; + public static final int MODE_POWER_SINK = 0x00; + public static final int MODE_POWER_SOURCE = 0x01; + + private static final int MODE_DATA_MASK = 0x03 << 1; + public static final int MODE_DATA_NONE = 0x00 << 1; + public static final int MODE_DATA_MTP = 0x01 << 1; + public static final int MODE_DATA_PTP = 0x02 << 1; + public static final int MODE_DATA_MIDI = 0x03 << 1; + + private final boolean mRestricted; + + private UserManager mUserManager; + private UsbManager mUsbManager; + private UsbPort mPort; + private UsbPortStatus mPortStatus; + + private boolean mIsUnlocked; + + public UsbBackend(Context context) { + Intent intent = context.registerReceiver(null, + new IntentFilter(UsbManager.ACTION_USB_STATE)); + mIsUnlocked = intent.getBooleanExtra(UsbManager.USB_DATA_UNLOCKED, false); + + mUserManager = UserManager.get(context); + mUsbManager = context.getSystemService(UsbManager.class); + + mRestricted = mUserManager.hasUserRestriction(UserManager.DISALLOW_USB_FILE_TRANSFER); + UsbPort[] ports = mUsbManager.getPorts(); + // For now look for a connected port, in the future we should identify port in the + // notification and pick based on that. + final int N = ports.length; + for (int i = 0; i < N; i++) { + UsbPortStatus status = mUsbManager.getPortStatus(ports[i]); + if (status.isConnected()) { + mPort = ports[i]; + mPortStatus = status; + break; + } + } + } + + public int getCurrentMode() { + if (mPort != null) { + int power = mPortStatus.getCurrentPowerRole() == UsbPort.POWER_ROLE_SOURCE + ? MODE_POWER_SOURCE : MODE_POWER_SINK; + return power | getUsbDataMode(); + } + return MODE_POWER_SINK | getUsbDataMode(); + } + + public int getUsbDataMode() { + if (!mIsUnlocked) { + return MODE_DATA_NONE; + } else if (mUsbManager.isFunctionEnabled(UsbManager.USB_FUNCTION_MTP)) { + return MODE_DATA_MTP; + } else if (mUsbManager.isFunctionEnabled(UsbManager.USB_FUNCTION_PTP)) { + return MODE_DATA_PTP; + } else if (mUsbManager.isFunctionEnabled(UsbManager.USB_FUNCTION_MIDI)) { + return MODE_DATA_MIDI; + } + return MODE_DATA_NONE; // ... + } + + private void setUsbFunction(int mode) { + switch (mode) { + case MODE_DATA_MTP: + mUsbManager.setCurrentFunction(UsbManager.USB_FUNCTION_MTP); + mUsbManager.setUsbDataUnlocked(true); + break; + case MODE_DATA_PTP: + mUsbManager.setCurrentFunction(UsbManager.USB_FUNCTION_PTP); + mUsbManager.setUsbDataUnlocked(true); + break; + case MODE_DATA_MIDI: + mUsbManager.setCurrentFunction(UsbManager.USB_FUNCTION_MIDI); + mUsbManager.setUsbDataUnlocked(true); + break; + default: + mUsbManager.setCurrentFunction(null); + mUsbManager.setUsbDataUnlocked(false); + break; + } + } + + public void setMode(int mode) { + if (mPort != null) { + int powerRole = modeToPower(mode); + // If we aren't using any data modes and we support host mode, then go to host mode + // so maybe? the other device can provide data if it wants, otherwise go into device + // mode because we have no choice. + int dataRole = (mode & MODE_DATA_MASK) == MODE_DATA_NONE + && mPortStatus.isRoleCombinationSupported(powerRole, UsbPort.DATA_ROLE_HOST) + ? UsbPort.DATA_ROLE_HOST : UsbPort.DATA_ROLE_DEVICE; + mUsbManager.setPortRoles(mPort, powerRole, dataRole); + } + setUsbFunction(mode & MODE_DATA_MASK); + } + + private int modeToPower(int mode) { + return (mode & MODE_POWER_MASK) == MODE_POWER_SOURCE + ? UsbPort.POWER_ROLE_SOURCE : UsbPort.POWER_ROLE_SINK; + } + + public boolean isModeSupported(int mode) { + if (mRestricted && (mode & MODE_DATA_MASK) != MODE_DATA_NONE + && (mode & MODE_DATA_MASK) != MODE_DATA_MIDI) { + // No USB data modes are supported. + return false; + } + if (mPort != null) { + int power = modeToPower(mode); + if ((mode & MODE_DATA_MASK) != 0) { + // We have a port and data, need to be in device mode. + return mPortStatus.isRoleCombinationSupported(power, + UsbPort.DATA_ROLE_DEVICE); + } else { + // No data needed, we can do this power mode in either device or host. + return mPortStatus.isRoleCombinationSupported(power, UsbPort.DATA_ROLE_DEVICE) + || mPortStatus.isRoleCombinationSupported(power, UsbPort.DATA_ROLE_HOST); + } + } + // No port, support sink modes only. + return (mode & MODE_POWER_MASK) != MODE_POWER_SOURCE; + } +}
\ No newline at end of file diff --git a/src/com/android/settings/deviceinfo/UsbModeChooserActivity.java b/src/com/android/settings/deviceinfo/UsbModeChooserActivity.java index 4bdabd5..77fc388 100644 --- a/src/com/android/settings/deviceinfo/UsbModeChooserActivity.java +++ b/src/com/android/settings/deviceinfo/UsbModeChooserActivity.java @@ -20,13 +20,14 @@ import android.annotation.Nullable; import android.app.Activity; import android.app.ActivityManager; import android.app.AlertDialog; -import android.content.Context; import android.content.DialogInterface; -import android.content.Intent; -import android.content.IntentFilter; -import android.hardware.usb.UsbManager; import android.os.Bundle; -import android.os.UserManager; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Checkable; +import android.widget.LinearLayout; +import android.widget.TextView; import com.android.settings.R; @@ -36,83 +37,102 @@ import com.android.settings.R; */ public class UsbModeChooserActivity extends Activity { - private UsbManager mUsbManager; - private String[] mFunctions; - private boolean mIsUnlocked; + public static final int[] DEFAULT_MODES = { + UsbBackend.MODE_POWER_SINK | UsbBackend.MODE_DATA_NONE, + UsbBackend.MODE_POWER_SOURCE | UsbBackend.MODE_DATA_NONE, + UsbBackend.MODE_POWER_SINK | UsbBackend.MODE_DATA_MTP, + UsbBackend.MODE_POWER_SINK | UsbBackend.MODE_DATA_PTP, + UsbBackend.MODE_POWER_SINK | UsbBackend.MODE_DATA_MIDI + }; + + private UsbBackend mBackend; + private AlertDialog mDialog; + private LayoutInflater mLayoutInflater; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { - Intent i = getBaseContext().registerReceiver(null, new IntentFilter(UsbManager.ACTION_USB_STATE)); - mIsUnlocked = i.getBooleanExtra(UsbManager.USB_DATA_UNLOCKED, false); super.onCreate(savedInstanceState); - mUsbManager = (UsbManager) getSystemService(Context.USB_SERVICE); - boolean isFileTransferRestricted = ((UserManager) getSystemService(Context.USER_SERVICE)) - .hasUserRestriction(UserManager.DISALLOW_USB_FILE_TRANSFER); - CharSequence[] items; - if (isFileTransferRestricted) { - items = new CharSequence[] { getText(R.string.usb_use_charging_only), getText(R.string.usb_use_MIDI)}; - mFunctions = new String[] { null, UsbManager.USB_FUNCTION_MIDI }; - } else { - items = new CharSequence[] { - getText(R.string.usb_use_charging_only), getText(R.string.usb_use_file_transfers), - getText(R.string.usb_use_photo_transfers), getText(R.string.usb_use_MIDI)}; - mFunctions = new String[] { null, UsbManager.USB_FUNCTION_MTP, - UsbManager.USB_FUNCTION_PTP, UsbManager.USB_FUNCTION_MIDI }; - } - final AlertDialog levelDialog; - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.usb_use); + mLayoutInflater = LayoutInflater.from(this); - builder.setSingleChoiceItems(items, getCurrentFunction(), - new DialogInterface.OnClickListener() { + mDialog = new AlertDialog.Builder(this) + .setTitle(R.string.usb_use) + .setView(R.layout.usb_dialog_container) + .setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + finish(); + } + }) + .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - if (!ActivityManager.isUserAMonkey()) { - setCurrentFunction(which); - } - dialog.dismiss(); - UsbModeChooserActivity.this.finish(); + finish(); } - }); - builder.setOnDismissListener(new DialogInterface.OnDismissListener() { - @Override - public void onDismiss(DialogInterface dialog) { - UsbModeChooserActivity.this.finish(); + }).create(); + mDialog.show(); + + LinearLayout container = (LinearLayout) mDialog.findViewById(R.id.container); + + mBackend = new UsbBackend(this); + int current = mBackend.getCurrentMode(); + for (int i = 0; i < DEFAULT_MODES.length; i++) { + if (mBackend.isModeSupported(DEFAULT_MODES[i])) { + inflateOption(DEFAULT_MODES[i], current == DEFAULT_MODES[i], container); } - }); - builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { + } + } + + private void inflateOption(final int mode, boolean selected, LinearLayout container) { + View v = mLayoutInflater.inflate(R.layout.radio_with_summary, container, false); + + ((TextView) v.findViewById(android.R.id.title)).setText(getTitle(mode)); + ((TextView) v.findViewById(android.R.id.summary)).setText(getSummary(mode)); + + v.setOnClickListener(new OnClickListener() { @Override - public void onClick(DialogInterface dialog, int which) { - UsbModeChooserActivity.this.finish(); + public void onClick(View v) { + if (!ActivityManager.isUserAMonkey()) { + mBackend.setMode(mode); + } + mDialog.dismiss(); + finish(); } }); - levelDialog = builder.create(); - levelDialog.show(); + ((Checkable) v).setChecked(selected); + container.addView(v); } - private int getCurrentFunction() { - if (!mIsUnlocked) { - return 0; - } - - for (int i = 1; i < mFunctions.length; i++) { - if (mUsbManager.isFunctionEnabled(mFunctions[i])) { - return i; - } + private static int getSummary(int mode) { + switch (mode) { + case UsbBackend.MODE_POWER_SINK | UsbBackend.MODE_DATA_NONE: + return R.string.usb_use_charging_only_desc; + case UsbBackend.MODE_POWER_SOURCE | UsbBackend.MODE_DATA_NONE: + return R.string.usb_use_power_only_desc; + case UsbBackend.MODE_POWER_SINK | UsbBackend.MODE_DATA_MTP: + return R.string.usb_use_file_transfers_desc; + case UsbBackend.MODE_POWER_SINK | UsbBackend.MODE_DATA_PTP: + return R.string.usb_use_photo_transfers_desc; + case UsbBackend.MODE_POWER_SINK | UsbBackend.MODE_DATA_MIDI: + return R.string.usb_use_MIDI_desc; } return 0; } - private void setCurrentFunction(int which) { - if (which == 0) { - mUsbManager.setCurrentFunction(null); - mUsbManager.setUsbDataUnlocked(false); - return; + private static int getTitle(int mode) { + switch (mode) { + case UsbBackend.MODE_POWER_SINK | UsbBackend.MODE_DATA_NONE: + return R.string.usb_use_charging_only; + case UsbBackend.MODE_POWER_SOURCE | UsbBackend.MODE_DATA_NONE: + return R.string.usb_use_power_only; + case UsbBackend.MODE_POWER_SINK | UsbBackend.MODE_DATA_MTP: + return R.string.usb_use_file_transfers; + case UsbBackend.MODE_POWER_SINK | UsbBackend.MODE_DATA_PTP: + return R.string.usb_use_photo_transfers; + case UsbBackend.MODE_POWER_SINK | UsbBackend.MODE_DATA_MIDI: + return R.string.usb_use_MIDI; } - - mUsbManager.setCurrentFunction(mFunctions[which]); - mUsbManager.setUsbDataUnlocked(true); + return 0; } } diff --git a/src/com/android/settings/fingerprint/FingerprintEnrollBase.java b/src/com/android/settings/fingerprint/FingerprintEnrollBase.java index 430f220..d164800 100644 --- a/src/com/android/settings/fingerprint/FingerprintEnrollBase.java +++ b/src/com/android/settings/fingerprint/FingerprintEnrollBase.java @@ -28,6 +28,7 @@ import android.widget.Button; import android.widget.TextView; import com.android.settings.ChooseLockSettingsHelper; +import com.android.settings.InstrumentedActivity; import com.android.settings.R; import com.android.setupwizardlib.SetupWizardLayout; import com.android.setupwizardlib.view.NavigationBar; @@ -35,24 +36,11 @@ import com.android.setupwizardlib.view.NavigationBar; /** * Base activity for all fingerprint enrollment steps. */ -public class FingerprintEnrollBase extends Activity implements View.OnClickListener { - - /** - * Used by the choose fingerprint wizard to indicate the wizard is - * finished, and each activity in the wizard should finish. - * <p> - * Previously, each activity in the wizard would finish itself after - * starting the next activity. However, this leads to broken 'Back' - * behavior. So, now an activity does not finish itself until it gets this - * result. - */ - protected static final int RESULT_FINISHED = RESULT_FIRST_USER; - - /** - * Used by the enrolling screen during setup wizard to skip over setting up fingerprint, which - * will be useful if the user accidentally entered this flow. - */ - protected static final int RESULT_SKIP = RESULT_FIRST_USER + 1; +public abstract class FingerprintEnrollBase extends InstrumentedActivity + implements View.OnClickListener { + static final int RESULT_FINISHED = FingerprintSettings.RESULT_FINISHED; + static final int RESULT_SKIP = FingerprintSettings.RESULT_SKIP; + static final int RESULT_TIMEOUT = FingerprintSettings.RESULT_TIMEOUT; protected byte[] mToken; diff --git a/src/com/android/settings/fingerprint/FingerprintEnrollEnrolling.java b/src/com/android/settings/fingerprint/FingerprintEnrollEnrolling.java index 5d4edbd..fe0bb63 100644 --- a/src/com/android/settings/fingerprint/FingerprintEnrollEnrolling.java +++ b/src/com/android/settings/fingerprint/FingerprintEnrollEnrolling.java @@ -20,6 +20,7 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; +import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.app.DialogFragment; @@ -29,6 +30,7 @@ import android.content.res.ColorStateList; import android.graphics.drawable.Animatable2; import android.graphics.drawable.AnimatedVectorDrawable; import android.graphics.drawable.Drawable; +import android.hardware.fingerprint.FingerprintManager; import android.os.Bundle; import android.view.MotionEvent; import android.view.View; @@ -38,6 +40,7 @@ import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.TextView; +import com.android.internal.logging.MetricsLogger; import com.android.settings.ChooseLockSettingsHelper; import com.android.settings.R; @@ -244,9 +247,22 @@ public class FingerprintEnrollEnrolling extends FingerprintEnrollBase } @Override - public void onEnrollmentError(CharSequence errString) { - showError(errString); + public void onEnrollmentError(int errMsgId, CharSequence errString) { + int msgId; + switch (errMsgId) { + case FingerprintManager.FINGERPRINT_ERROR_TIMEOUT: + // This message happens when the underlying crypto layer decides to revoke the + // enrollment auth token. + msgId = R.string.security_settings_fingerprint_enroll_error_timeout_dialog_message; + break; + default: + // There's nothing specific to tell the user about. Ask them to try again. + msgId = R.string.security_settings_fingerprint_enroll_error_generic_dialog_message; + break; + } + showErrorDialog(getText(msgId), errMsgId); stopIconAnimation(); + mErrorText.removeCallbacks(mTouchAgainRunnable); } @Override @@ -277,6 +293,11 @@ public class FingerprintEnrollEnrolling extends FingerprintEnrollBase return PROGRESS_BAR_MAX * progress / (steps + 1); } + private void showErrorDialog(CharSequence msg, int msgId) { + ErrorDialog dlg = ErrorDialog.newInstance(msg, msgId); + dlg.show(getFragmentManager(), ErrorDialog.class.getName()); + } + private void showIconTouchDialog() { mIconTouchCount = 0; new IconTouchDialog().show(getFragmentManager(), null /* tag */); @@ -380,6 +401,11 @@ public class FingerprintEnrollEnrolling extends FingerprintEnrollBase } }; + @Override + protected int getMetricsCategory() { + return MetricsLogger.FINGERPRINT_ENROLLING; + } + public static class IconTouchDialog extends DialogFragment { @Override @@ -397,4 +423,49 @@ public class FingerprintEnrollEnrolling extends FingerprintEnrollBase return builder.create(); } } + + public static class ErrorDialog extends DialogFragment { + + /** + * Create a new instance of ErrorDialog. + * + * @param msg the string to show for message text + * @param msgId the FingerprintManager error id so we know the cause + * @return a new ErrorDialog + */ + static ErrorDialog newInstance(CharSequence msg, int msgId) { + ErrorDialog dlg = new ErrorDialog(); + Bundle args = new Bundle(); + args.putCharSequence("error_msg", msg); + args.putInt("error_id", msgId); + dlg.setArguments(args); + return dlg; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + CharSequence errorString = getArguments().getCharSequence("error_msg"); + final int errMsgId = getArguments().getInt("error_id"); + builder.setTitle(R.string.security_settings_fingerprint_enroll_error_dialog_title) + .setMessage(errorString) + .setCancelable(false) + .setPositiveButton(R.string.security_settings_fingerprint_enroll_dialog_ok, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + boolean wasTimeout = + errMsgId == FingerprintManager.FINGERPRINT_ERROR_TIMEOUT; + Activity activity = getActivity(); + activity.setResult(wasTimeout ? + RESULT_TIMEOUT : RESULT_FINISHED); + activity.finish(); + } + }); + AlertDialog dialog = builder.create(); + dialog.setCanceledOnTouchOutside(false); + return dialog; + } + } } diff --git a/src/com/android/settings/fingerprint/FingerprintEnrollFindSensor.java b/src/com/android/settings/fingerprint/FingerprintEnrollFindSensor.java index bbed42c..63d9335 100644 --- a/src/com/android/settings/fingerprint/FingerprintEnrollFindSensor.java +++ b/src/com/android/settings/fingerprint/FingerprintEnrollFindSensor.java @@ -16,11 +16,11 @@ package com.android.settings.fingerprint; -import android.content.Context; import android.content.Intent; import android.hardware.fingerprint.FingerprintManager; import android.os.Bundle; +import com.android.internal.logging.MetricsLogger; import com.android.settings.ChooseLockSettingsHelper; import com.android.settings.R; @@ -31,15 +31,19 @@ public class FingerprintEnrollFindSensor extends FingerprintEnrollBase { private static final int CONFIRM_REQUEST = 1; private static final int ENROLLING = 2; + public static final String EXTRA_KEY_LAUNCHED_CONFIRM = "launched_confirm_lock"; private FingerprintLocationAnimationView mAnimation; + private boolean mLaunchedConfirmLock; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.fingerprint_enroll_find_sensor); setHeaderText(R.string.security_settings_fingerprint_enroll_find_sensor_title); - if (mToken == null) { + mLaunchedConfirmLock = savedInstanceState != null && savedInstanceState.getBoolean( + EXTRA_KEY_LAUNCHED_CONFIRM); + if (mToken == null && !mLaunchedConfirmLock) { launchConfirmLock(); } mAnimation = (FingerprintLocationAnimationView) findViewById( @@ -59,6 +63,12 @@ public class FingerprintEnrollFindSensor extends FingerprintEnrollBase { } @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean(EXTRA_KEY_LAUNCHED_CONFIRM, mLaunchedConfirmLock); + } + + @Override protected void onNextButtonClick() { startActivityForResult(getEnrollingIntent(), ENROLLING); } @@ -79,6 +89,9 @@ public class FingerprintEnrollFindSensor extends FingerprintEnrollBase { } else if (resultCode == RESULT_SKIP) { setResult(RESULT_SKIP); finish(); + } else if (resultCode == RESULT_TIMEOUT) { + setResult(RESULT_TIMEOUT); + finish(); } else { FingerprintManager fpm = getSystemService(FingerprintManager.class); int enrolled = fpm.getEnrolledFingerprints().size(); @@ -103,6 +116,13 @@ public class FingerprintEnrollFindSensor extends FingerprintEnrollBase { // This shouldn't happen, as we should only end up at this step if a lock thingy is // already set. finish(); + } else { + mLaunchedConfirmLock = true; } } + + @Override + protected int getMetricsCategory() { + return MetricsLogger.FINGERPRINT_FIND_SENSOR; + } } diff --git a/src/com/android/settings/fingerprint/FingerprintEnrollFinish.java b/src/com/android/settings/fingerprint/FingerprintEnrollFinish.java index 29f14d7..6691e20 100644 --- a/src/com/android/settings/fingerprint/FingerprintEnrollFinish.java +++ b/src/com/android/settings/fingerprint/FingerprintEnrollFinish.java @@ -25,6 +25,7 @@ import android.preference.Preference; import android.view.View; import android.widget.Button; +import com.android.internal.logging.MetricsLogger; import com.android.settings.R; import com.android.settings.fingerprint.FingerprintSettings.FingerprintPreference; @@ -70,4 +71,9 @@ public class FingerprintEnrollFinish extends FingerprintEnrollBase { } super.onClick(v); } + + @Override + protected int getMetricsCategory() { + return MetricsLogger.FINGERPRINT_ENROLL_FINISH; + } } diff --git a/src/com/android/settings/fingerprint/FingerprintEnrollIntroduction.java b/src/com/android/settings/fingerprint/FingerprintEnrollIntroduction.java index de5bf24..beb1a8f 100644 --- a/src/com/android/settings/fingerprint/FingerprintEnrollIntroduction.java +++ b/src/com/android/settings/fingerprint/FingerprintEnrollIntroduction.java @@ -22,6 +22,7 @@ import android.os.Bundle; import android.os.UserHandle; import android.view.View; +import com.android.internal.logging.MetricsLogger; import com.android.settings.ChooseLockSettingsHelper; import com.android.settings.HelpUtils; import com.android.settings.R; @@ -92,4 +93,9 @@ public class FingerprintEnrollIntroduction extends FingerprintEnrollBase { getString(R.string.help_url_fingerprint), getClass().getName()); startActivity(helpIntent); } + + @Override + protected int getMetricsCategory() { + return MetricsLogger.FINGERPRINT_ENROLL_INTRO; + } } diff --git a/src/com/android/settings/fingerprint/FingerprintEnrollOnboard.java b/src/com/android/settings/fingerprint/FingerprintEnrollOnboard.java index e81b0ff..0990459 100644 --- a/src/com/android/settings/fingerprint/FingerprintEnrollOnboard.java +++ b/src/com/android/settings/fingerprint/FingerprintEnrollOnboard.java @@ -21,6 +21,7 @@ import android.content.Intent; import android.hardware.fingerprint.FingerprintManager; import android.os.Bundle; +import com.android.internal.logging.MetricsLogger; import com.android.settings.ChooseLockGeneric; import com.android.settings.ChooseLockSettingsHelper; import com.android.settings.R; @@ -82,4 +83,9 @@ public class FingerprintEnrollOnboard extends FingerprintEnrollBase { protected Intent getFindSensorIntent() { return new Intent(this, FingerprintEnrollFindSensor.class); } + + @Override + protected int getMetricsCategory() { + return MetricsLogger.FINGERPRINT_ENROLL_ONBOARD; + } } diff --git a/src/com/android/settings/fingerprint/FingerprintEnrollSidecar.java b/src/com/android/settings/fingerprint/FingerprintEnrollSidecar.java index 6a47dc4..5feb08c 100644 --- a/src/com/android/settings/fingerprint/FingerprintEnrollSidecar.java +++ b/src/com/android/settings/fingerprint/FingerprintEnrollSidecar.java @@ -24,12 +24,14 @@ import android.os.Bundle; import android.os.CancellationSignal; import android.os.Handler; +import com.android.internal.logging.MetricsLogger; import com.android.settings.ChooseLockSettingsHelper; +import com.android.settings.InstrumentedFragment; /** * Sidecar fragment to handle the state around fingerprint enrollment. */ -public class FingerprintEnrollSidecar extends Fragment { +public class FingerprintEnrollSidecar extends InstrumentedFragment { private int mEnrollmentSteps = -1; private int mEnrollmentRemaining = 0; @@ -128,7 +130,7 @@ public class FingerprintEnrollSidecar extends Fragment { @Override public void onEnrollmentError(int errMsgId, CharSequence errString) { if (mListener != null) { - mListener.onEnrollmentError(errString); + mListener.onEnrollmentError(errMsgId, errString); } } }; @@ -140,9 +142,14 @@ public class FingerprintEnrollSidecar extends Fragment { } }; + @Override + protected int getMetricsCategory() { + return MetricsLogger.FINGERPRINT_ENROLL_SIDECAR; + } + public interface Listener { void onEnrollmentHelp(CharSequence helpString); - void onEnrollmentError(CharSequence errString); + void onEnrollmentError(int errMsgId, CharSequence errString); void onEnrollmentProgressChange(int steps, int remaining); } } diff --git a/src/com/android/settings/fingerprint/FingerprintSettings.java b/src/com/android/settings/fingerprint/FingerprintSettings.java index a5c9963..cacd5dd 100644 --- a/src/com/android/settings/fingerprint/FingerprintSettings.java +++ b/src/com/android/settings/fingerprint/FingerprintSettings.java @@ -70,8 +70,9 @@ import java.util.List; * Settings screen for fingerprints */ public class FingerprintSettings extends SubSettings { + /** - * Used by the FP settings wizard to indicate the wizard is + * Used by the choose fingerprint wizard to indicate the wizard is * finished, and each activity in the wizard should finish. * <p> * Previously, each activity in the wizard would finish itself after @@ -79,7 +80,21 @@ public class FingerprintSettings extends SubSettings { * behavior. So, now an activity does not finish itself until it gets this * result. */ - static final int RESULT_FINISHED = RESULT_FIRST_USER; + protected static final int RESULT_FINISHED = RESULT_FIRST_USER; + + /** + * Used by the enrolling screen during setup wizard to skip over setting up fingerprint, which + * will be useful if the user accidentally entered this flow. + */ + protected static final int RESULT_SKIP = RESULT_FIRST_USER + 1; + + /** + * Like {@link #RESULT_FINISHED} except this one indicates enrollment failed because the + * device was left idle. This is used to clear the credential token to require the user to + * re-enter their pin/pattern/password before continuing. + */ + protected static final int RESULT_TIMEOUT = RESULT_FIRST_USER + 2; + private static final long LOCKOUT_DURATION = 30000; // time we have to wait for fp to reset, ms @Override @@ -181,6 +196,7 @@ public class FingerprintSettings extends SubSettings { case MSG_REFRESH_FINGERPRINT_TEMPLATES: removeFingerprintPreference(msg.arg1); updateAddPreference(); + retryFingerprint(); break; case MSG_FINGER_AUTH_SUCCESS: mFingerprintCancel = null; @@ -440,6 +456,12 @@ public class FingerprintSettings extends SubSettings { ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN); } } + } else if (requestCode == ADD_FINGERPRINT_REQUEST) { + if (resultCode == RESULT_TIMEOUT) { + Activity activity = getActivity(); + activity.setResult(RESULT_TIMEOUT); + activity.finish(); + } } if (mToken == null) { @@ -480,10 +502,10 @@ public class FingerprintSettings extends SubSettings { highlight.setHotspot(centerX, centerY); view.setBackground(highlight); view.setPressed(true); + view.setPressed(false); mHandler.postDelayed(new Runnable() { @Override public void run() { - view.setPressed(false); view.setBackground(null); } }, RESET_HIGHLIGHT_DELAY_MS); @@ -556,6 +578,9 @@ public class FingerprintSettings extends SubSettings { if (DEBUG) { Log.v(TAG, "rename " + name + " to " + newName); } + MetricsLogger.action(getContext(), + MetricsLogger.ACTION_FINGERPRINT_RENAME, + mFp.getFingerId()); FingerprintSettingsFragment parent = (FingerprintSettingsFragment) getTargetFragment(); @@ -598,6 +623,8 @@ public class FingerprintSettings extends SubSettings { private void onDeleteClick(DialogInterface dialog) { if (DEBUG) Log.v(TAG, "Removing fpId=" + mFp.getFingerId()); + MetricsLogger.action(getContext(), MetricsLogger.ACTION_FINGERPRINT_DELETE, + mFp.getFingerId()); FingerprintSettingsFragment parent = (FingerprintSettingsFragment) getTargetFragment(); if (parent.mFingerprintManager.getEnrolledFingerprints().size() > 1) { diff --git a/src/com/android/settings/fingerprint/SetupFingerprintEnrollEnrolling.java b/src/com/android/settings/fingerprint/SetupFingerprintEnrollEnrolling.java index fc7b803..b57bb8d 100644 --- a/src/com/android/settings/fingerprint/SetupFingerprintEnrollEnrolling.java +++ b/src/com/android/settings/fingerprint/SetupFingerprintEnrollEnrolling.java @@ -16,18 +16,29 @@ package com.android.settings.fingerprint; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.app.FragmentManager; +import android.content.DialogInterface; import android.content.Intent; import android.content.res.Resources; +import android.os.Bundle; import android.view.View; import android.widget.Button; +import com.android.internal.logging.MetricsLogger; import com.android.settings.R; import com.android.settings.SetupWizardUtils; +import com.android.setupwizardlib.util.SystemBarHelper; import com.android.setupwizardlib.view.NavigationBar; public class SetupFingerprintEnrollEnrolling extends FingerprintEnrollEnrolling implements NavigationBar.NavigationBarListener { + private static final String TAG_DIALOG = "dialog"; + @Override protected Intent getFinishIntent() { final Intent intent = new Intent(this, SetupFingerprintEnrollFinish.class); @@ -68,7 +79,53 @@ public class SetupFingerprintEnrollEnrolling extends FingerprintEnrollEnrolling @Override public void onNavigateNext() { - setResult(RESULT_SKIP); - finish(); + new SkipDialog().show(getFragmentManager(), TAG_DIALOG); + } + + @Override + protected int getMetricsCategory() { + return MetricsLogger.FINGERPRINT_ENROLLING_SETUP; + } + + public static class SkipDialog extends DialogFragment { + + @Override + public void show(FragmentManager manager, String tag) { + if (manager.findFragmentByTag(tag) == null) { + super.show(manager, tag); + } + } + + public SkipDialog() { + // no-arg constructor for fragment + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final AlertDialog dialog = new AlertDialog.Builder(getActivity()) + .setTitle(R.string.setup_fingerprint_enroll_enrolling_skip_title) + .setMessage(R.string.setup_fingerprint_enroll_enrolling_skip_message) + .setCancelable(false) + .setPositiveButton(R.string.wifi_skip_anyway, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int id) { + Activity activity = getActivity(); + if (activity != null) { + activity.setResult(RESULT_SKIP); + activity.finish(); + } + } + }) + .setNegativeButton(R.string.wifi_dont_skip, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int id) { + } + }) + .create(); + SystemBarHelper.hideSystemBars(dialog); + return dialog; + } } } diff --git a/src/com/android/settings/fingerprint/SetupFingerprintEnrollFindSensor.java b/src/com/android/settings/fingerprint/SetupFingerprintEnrollFindSensor.java index 17c0671..1483c83 100644 --- a/src/com/android/settings/fingerprint/SetupFingerprintEnrollFindSensor.java +++ b/src/com/android/settings/fingerprint/SetupFingerprintEnrollFindSensor.java @@ -21,6 +21,7 @@ import android.content.res.Resources; import android.view.View; import android.widget.Button; +import com.android.internal.logging.MetricsLogger; import com.android.settings.ChooseLockSettingsHelper; import com.android.settings.R; import com.android.settings.SetupWizardUtils; @@ -70,4 +71,9 @@ public class SetupFingerprintEnrollFindSensor extends FingerprintEnrollFindSenso public void onNavigateNext() { onNextButtonClick(); } + + @Override + protected int getMetricsCategory() { + return MetricsLogger.FINGERPRINT_FIND_SENSOR_SETUP; + } } diff --git a/src/com/android/settings/fingerprint/SetupFingerprintEnrollFinish.java b/src/com/android/settings/fingerprint/SetupFingerprintEnrollFinish.java index 351cd57..8f37a11 100644 --- a/src/com/android/settings/fingerprint/SetupFingerprintEnrollFinish.java +++ b/src/com/android/settings/fingerprint/SetupFingerprintEnrollFinish.java @@ -22,6 +22,7 @@ import android.view.View; import android.widget.Button; import android.widget.TextView; +import com.android.internal.logging.MetricsLogger; import com.android.settings.ChooseLockSettingsHelper; import com.android.settings.R; import com.android.settings.SetupWizardUtils; @@ -78,4 +79,9 @@ public class SetupFingerprintEnrollFinish extends FingerprintEnrollFinish public void onNavigateNext() { onNextButtonClick(); } + + @Override + protected int getMetricsCategory() { + return MetricsLogger.FINGERPRINT_ENROLL_FINISH_SETUP; + } } diff --git a/src/com/android/settings/fingerprint/SetupFingerprintEnrollIntroduction.java b/src/com/android/settings/fingerprint/SetupFingerprintEnrollIntroduction.java index 416d53f..c7e39e5 100644 --- a/src/com/android/settings/fingerprint/SetupFingerprintEnrollIntroduction.java +++ b/src/com/android/settings/fingerprint/SetupFingerprintEnrollIntroduction.java @@ -21,6 +21,7 @@ import android.content.res.Resources; import android.view.View; import android.widget.Button; +import com.android.internal.logging.MetricsLogger; import com.android.settings.R; import com.android.settings.SetupWizardUtils; import com.android.setupwizardlib.view.NavigationBar; @@ -74,4 +75,9 @@ public class SetupFingerprintEnrollIntroduction extends FingerprintEnrollIntrodu public void onNavigateNext() { onNextButtonClick(); } + + @Override + protected int getMetricsCategory() { + return MetricsLogger.FINGERPRINT_ENROLL_INTRO_SETUP; + } } diff --git a/src/com/android/settings/fingerprint/SetupFingerprintEnrollOnboard.java b/src/com/android/settings/fingerprint/SetupFingerprintEnrollOnboard.java index bee0cde..7fca35a 100644 --- a/src/com/android/settings/fingerprint/SetupFingerprintEnrollOnboard.java +++ b/src/com/android/settings/fingerprint/SetupFingerprintEnrollOnboard.java @@ -21,6 +21,7 @@ import android.content.res.Resources; import android.view.View; import android.widget.Button; +import com.android.internal.logging.MetricsLogger; import com.android.settings.R; import com.android.settings.SetupChooseLockGeneric; import com.android.settings.SetupWizardUtils; @@ -75,4 +76,9 @@ public class SetupFingerprintEnrollOnboard extends FingerprintEnrollOnboard public void onNavigateNext() { onNextButtonClick(); } + + @Override + protected int getMetricsCategory() { + return MetricsLogger.FINGERPRINT_ENROLL_ONBOARD_SETUP; + } } diff --git a/src/com/android/settings/fuelgauge/FakeUid.java b/src/com/android/settings/fuelgauge/FakeUid.java index aaa30a2..7fd66c5 100644 --- a/src/com/android/settings/fuelgauge/FakeUid.java +++ b/src/com/android/settings/fuelgauge/FakeUid.java @@ -246,7 +246,7 @@ public class FakeUid extends Uid { } @Override - public long getTimeAtCpuSpeed(int step, int which) { + public long getTimeAtCpuSpeed(int cluster, int step, int which) { return 0; } diff --git a/src/com/android/settings/widget/ExploreByTouchHelper.java b/src/com/android/settings/widget/ExploreByTouchHelper.java new file mode 100644 index 0000000..b64a74c --- /dev/null +++ b/src/com/android/settings/widget/ExploreByTouchHelper.java @@ -0,0 +1,724 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.widget; + +import android.content.Context; +import android.graphics.Rect; +import android.os.Bundle; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeProvider; + +import java.util.LinkedList; +import java.util.List; + +/** + * Copied from setup wizard, which is in turn a modified copy of + * com.android.internal.ExploreByTouchHelper with the following modifications: + * + * - Make accessibility calls to the views, instead of to the accessibility delegate directly to + * make sure those methods for View subclasses are called. + * + * ExploreByTouchHelper is a utility class for implementing accessibility + * support in custom {@link android.view.View}s that represent a collection of View-like + * logical items. It extends {@link android.view.accessibility.AccessibilityNodeProvider} and + * simplifies many aspects of providing information to accessibility services + * and managing accessibility focus. This class does not currently support + * hierarchies of logical items. + * <p> + * This should be applied to the parent view using + * {@link android.view.View#setAccessibilityDelegate}: + * + * <pre> + * mAccessHelper = ExploreByTouchHelper.create(someView, mAccessHelperCallback); + * ViewCompat.setAccessibilityDelegate(someView, mAccessHelper); + * </pre> + */ +public abstract class ExploreByTouchHelper extends View.AccessibilityDelegate { + /** Virtual node identifier value for invalid nodes. */ + public static final int INVALID_ID = Integer.MIN_VALUE; + + /** Default class name used for virtual views. */ + private static final String DEFAULT_CLASS_NAME = View.class.getName(); + + // Temporary, reusable data structures. + private final Rect mTempScreenRect = new Rect(); + private final Rect mTempParentRect = new Rect(); + private final Rect mTempVisibleRect = new Rect(); + private final int[] mTempGlobalRect = new int[2]; + + /** View's context **/ + private Context mContext; + + /** System accessibility manager, used to check state and send events. */ + private final AccessibilityManager mManager; + + /** View whose internal structure is exposed through this helper. */ + private final View mView; + + /** Node provider that handles creating nodes and performing actions. */ + private ExploreByTouchNodeProvider mNodeProvider; + + /** Virtual view id for the currently focused logical item. */ + private int mFocusedVirtualViewId = INVALID_ID; + + /** Virtual view id for the currently hovered logical item. */ + private int mHoveredVirtualViewId = INVALID_ID; + + /** + * Factory method to create a new {@link com.google.android.setupwizard.util.ExploreByTouchHelper}. + * + * @param forView View whose logical children are exposed by this helper. + */ + public ExploreByTouchHelper(View forView) { + if (forView == null) { + throw new IllegalArgumentException("View may not be null"); + } + + mView = forView; + mContext = forView.getContext(); + mManager = (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE); + } + + /** + * Returns the {@link android.view.accessibility.AccessibilityNodeProvider} for this helper. + * + * @param host View whose logical children are exposed by this helper. + * @return The accessibility node provider for this helper. + */ + @Override + public AccessibilityNodeProvider getAccessibilityNodeProvider(View host) { + if (mNodeProvider == null) { + mNodeProvider = new ExploreByTouchNodeProvider(); + } + return mNodeProvider; + } + + /** + * Dispatches hover {@link android.view.MotionEvent}s to the virtual view hierarchy when + * the Explore by Touch feature is enabled. + * <p> + * This method should be called by overriding + * {@link android.view.View#dispatchHoverEvent}: + * + * <pre>@Override + * public boolean dispatchHoverEvent(MotionEvent event) { + * if (mHelper.dispatchHoverEvent(this, event) { + * return true; + * } + * return super.dispatchHoverEvent(event); + * } + * </pre> + * + * @param event The hover event to dispatch to the virtual view hierarchy. + * @return Whether the hover event was handled. + */ + public boolean dispatchHoverEvent(MotionEvent event) { + if (!mManager.isEnabled() || !mManager.isTouchExplorationEnabled()) { + return false; + } + + switch (event.getAction()) { + case MotionEvent.ACTION_HOVER_MOVE: + case MotionEvent.ACTION_HOVER_ENTER: + final int virtualViewId = getVirtualViewAt(event.getX(), event.getY()); + updateHoveredVirtualView(virtualViewId); + return (virtualViewId != INVALID_ID); + case MotionEvent.ACTION_HOVER_EXIT: + if (mFocusedVirtualViewId != INVALID_ID) { + updateHoveredVirtualView(INVALID_ID); + return true; + } + return false; + default: + return false; + } + } + + /** + * Populates an event of the specified type with information about an item + * and attempts to send it up through the view hierarchy. + * <p> + * You should call this method after performing a user action that normally + * fires an accessibility event, such as clicking on an item. + * + * <pre>public void performItemClick(T item) { + * ... + * sendEventForVirtualViewId(item.id, AccessibilityEvent.TYPE_VIEW_CLICKED); + * } + * </pre> + * + * @param virtualViewId The virtual view id for which to send an event. + * @param eventType The type of event to send. + * @return true if the event was sent successfully. + */ + public boolean sendEventForVirtualView(int virtualViewId, int eventType) { + if ((virtualViewId == INVALID_ID) || !mManager.isEnabled()) { + return false; + } + + final ViewParent parent = mView.getParent(); + if (parent == null) { + return false; + } + + final AccessibilityEvent event = createEvent(virtualViewId, eventType); + return parent.requestSendAccessibilityEvent(mView, event); + } + + /** + * Notifies the accessibility framework that the properties of the parent + * view have changed. + * <p> + * You <b>must</b> call this method after adding or removing items from the + * parent view. + */ + public void invalidateRoot() { + invalidateVirtualView(View.NO_ID); + } + + /** + * Notifies the accessibility framework that the properties of a particular + * item have changed. + * <p> + * You <b>must</b> call this method after changing any of the properties set + * in {@link #onPopulateNodeForVirtualView}. + * + * @param virtualViewId The virtual view id to invalidate. + */ + public void invalidateVirtualView(int virtualViewId) { + sendEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + } + + /** + * Returns the virtual view id for the currently focused item, + * + * @return A virtual view id, or {@link #INVALID_ID} if no item is + * currently focused. + */ + public int getFocusedVirtualView() { + return mFocusedVirtualViewId; + } + + /** + * Sets the currently hovered item, sending hover accessibility events as + * necessary to maintain the correct state. + * + * @param virtualViewId The virtual view id for the item currently being + * hovered, or {@link #INVALID_ID} if no item is hovered within + * the parent view. + */ + private void updateHoveredVirtualView(int virtualViewId) { + if (mHoveredVirtualViewId == virtualViewId) { + return; + } + + final int previousVirtualViewId = mHoveredVirtualViewId; + mHoveredVirtualViewId = virtualViewId; + + // Stay consistent with framework behavior by sending ENTER/EXIT pairs + // in reverse order. This is accurate as of API 18. + sendEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); + sendEventForVirtualView(previousVirtualViewId, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); + } + + /** + * Constructs and returns an {@link android.view.accessibility.AccessibilityEvent} for the specified + * virtual view id, which includes the host view ({@link android.view.View#NO_ID}). + * + * @param virtualViewId The virtual view id for the item for which to + * construct an event. + * @param eventType The type of event to construct. + * @return An {@link android.view.accessibility.AccessibilityEvent} populated with information about + * the specified item. + */ + private AccessibilityEvent createEvent(int virtualViewId, int eventType) { + switch (virtualViewId) { + case View.NO_ID: + return createEventForHost(eventType); + default: + return createEventForChild(virtualViewId, eventType); + } + } + + /** + * Constructs and returns an {@link android.view.accessibility.AccessibilityEvent} for the host node. + * + * @param eventType The type of event to construct. + * @return An {@link android.view.accessibility.AccessibilityEvent} populated with information about + * the specified item. + */ + private AccessibilityEvent createEventForHost(int eventType) { + final AccessibilityEvent event = AccessibilityEvent.obtain(eventType); + mView.onInitializeAccessibilityEvent(event); + return event; + } + + /** + * Constructs and returns an {@link android.view.accessibility.AccessibilityEvent} populated with + * information about the specified item. + * + * @param virtualViewId The virtual view id for the item for which to + * construct an event. + * @param eventType The type of event to construct. + * @return An {@link android.view.accessibility.AccessibilityEvent} populated with information about + * the specified item. + */ + private AccessibilityEvent createEventForChild(int virtualViewId, int eventType) { + final AccessibilityEvent event = AccessibilityEvent.obtain(eventType); + event.setEnabled(true); + event.setClassName(DEFAULT_CLASS_NAME); + + // Allow the client to populate the event. + onPopulateEventForVirtualView(virtualViewId, event); + + // Make sure the developer is following the rules. + if (event.getText().isEmpty() && (event.getContentDescription() == null)) { + throw new RuntimeException("Callbacks must add text or a content description in " + + "populateEventForVirtualViewId()"); + } + + // Don't allow the client to override these properties. + event.setPackageName(mView.getContext().getPackageName()); + event.setSource(mView, virtualViewId); + + return event; + } + + /** + * Constructs and returns an {@link android.view.accessibility.AccessibilityNodeInfo} for the + * specified virtual view id, which includes the host view + * ({@link android.view.View#NO_ID}). + * + * @param virtualViewId The virtual view id for the item for which to + * construct a node. + * @return An {@link android.view.accessibility.AccessibilityNodeInfo} populated with information + * about the specified item. + */ + private AccessibilityNodeInfo createNode(int virtualViewId) { + switch (virtualViewId) { + case View.NO_ID: + return createNodeForHost(); + default: + return createNodeForChild(virtualViewId); + } + } + + /** + * Constructs and returns an {@link android.view.accessibility.AccessibilityNodeInfo} for the + * host view populated with its virtual descendants. + * + * @return An {@link android.view.accessibility.AccessibilityNodeInfo} for the parent node. + */ + private AccessibilityNodeInfo createNodeForHost() { + final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain(mView); + mView.onInitializeAccessibilityNodeInfo(node); + + // Add the virtual descendants. + final LinkedList<Integer> virtualViewIds = new LinkedList<Integer>(); + getVisibleVirtualViews(virtualViewIds); + + for (Integer childVirtualViewId : virtualViewIds) { + node.addChild(mView, childVirtualViewId); + } + + return node; + } + + /** + * Constructs and returns an {@link android.view.accessibility.AccessibilityNodeInfo} for the + * specified item. Automatically manages accessibility focus actions. + * <p> + * Allows the implementing class to specify most node properties, but + * overrides the following: + * <ul> + * <li>{@link android.view.accessibility.AccessibilityNodeInfo#setPackageName} + * <li>{@link android.view.accessibility.AccessibilityNodeInfo#setClassName} + * <li>{@link android.view.accessibility.AccessibilityNodeInfo#setParent(android.view.View)} + * <li>{@link android.view.accessibility.AccessibilityNodeInfo#setSource(android.view.View, int)} + * <li>{@link android.view.accessibility.AccessibilityNodeInfo#setVisibleToUser} + * <li>{@link android.view.accessibility.AccessibilityNodeInfo#setBoundsInScreen(android.graphics.Rect)} + * </ul> + * <p> + * Uses the bounds of the parent view and the parent-relative bounding + * rectangle specified by + * {@link android.view.accessibility.AccessibilityNodeInfo#getBoundsInParent} to automatically + * update the following properties: + * <ul> + * <li>{@link android.view.accessibility.AccessibilityNodeInfo#setVisibleToUser} + * <li>{@link android.view.accessibility.AccessibilityNodeInfo#setBoundsInParent} + * </ul> + * + * @param virtualViewId The virtual view id for item for which to construct + * a node. + * @return An {@link android.view.accessibility.AccessibilityNodeInfo} for the specified item. + */ + private AccessibilityNodeInfo createNodeForChild(int virtualViewId) { + final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain(); + + // Ensure the client has good defaults. + node.setEnabled(true); + node.setClassName(DEFAULT_CLASS_NAME); + + // Allow the client to populate the node. + onPopulateNodeForVirtualView(virtualViewId, node); + + // Make sure the developer is following the rules. + if ((node.getText() == null) && (node.getContentDescription() == null)) { + throw new RuntimeException("Callbacks must add text or a content description in " + + "populateNodeForVirtualViewId()"); + } + + node.getBoundsInParent(mTempParentRect); + if (mTempParentRect.isEmpty()) { + throw new RuntimeException("Callbacks must set parent bounds in " + + "populateNodeForVirtualViewId()"); + } + + final int actions = node.getActions(); + if ((actions & AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS) != 0) { + throw new RuntimeException("Callbacks must not add ACTION_ACCESSIBILITY_FOCUS in " + + "populateNodeForVirtualViewId()"); + } + if ((actions & AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS) != 0) { + throw new RuntimeException("Callbacks must not add ACTION_CLEAR_ACCESSIBILITY_FOCUS in " + + "populateNodeForVirtualViewId()"); + } + + // Don't allow the client to override these properties. + node.setPackageName(mView.getContext().getPackageName()); + node.setSource(mView, virtualViewId); + node.setParent(mView); + + // Manage internal accessibility focus state. + if (mFocusedVirtualViewId == virtualViewId) { + node.setAccessibilityFocused(true); + node.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS); + } else { + node.setAccessibilityFocused(false); + node.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS); + } + + // Set the visibility based on the parent bound. + if (intersectVisibleToUser(mTempParentRect)) { + node.setVisibleToUser(true); + node.setBoundsInParent(mTempParentRect); + } + + // Calculate screen-relative bound. + mView.getLocationOnScreen(mTempGlobalRect); + final int offsetX = mTempGlobalRect[0]; + final int offsetY = mTempGlobalRect[1]; + mTempScreenRect.set(mTempParentRect); + mTempScreenRect.offset(offsetX, offsetY); + node.setBoundsInScreen(mTempScreenRect); + + return node; + } + + private boolean performAction(int virtualViewId, int action, Bundle arguments) { + switch (virtualViewId) { + case View.NO_ID: + return performActionForHost(action, arguments); + default: + return performActionForChild(virtualViewId, action, arguments); + } + } + + private boolean performActionForHost(int action, Bundle arguments) { + return mView.performAccessibilityAction(action, arguments); + } + + private boolean performActionForChild(int virtualViewId, int action, Bundle arguments) { + switch (action) { + case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: + case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: + return manageFocusForChild(virtualViewId, action, arguments); + default: + return onPerformActionForVirtualView(virtualViewId, action, arguments); + } + } + + private boolean manageFocusForChild(int virtualViewId, int action, Bundle arguments) { + switch (action) { + case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: + return requestAccessibilityFocus(virtualViewId); + case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: + return clearAccessibilityFocus(virtualViewId); + default: + return false; + } + } + + /** + * Computes whether the specified {@link android.graphics.Rect} intersects with the visible + * portion of its parent {@link android.view.View}. Modifies {@code localRect} to contain + * only the visible portion. + * + * @param localRect A rectangle in local (parent) coordinates. + * @return Whether the specified {@link android.graphics.Rect} is visible on the screen. + */ + private boolean intersectVisibleToUser(Rect localRect) { + // Missing or empty bounds mean this view is not visible. + if ((localRect == null) || localRect.isEmpty()) { + return false; + } + + // Attached to invisible window means this view is not visible. + if (mView.getWindowVisibility() != View.VISIBLE) { + return false; + } + + // An invisible predecessor means that this view is not visible. + ViewParent viewParent = mView.getParent(); + while (viewParent instanceof View) { + final View view = (View) viewParent; + if ((view.getAlpha() <= 0) || (view.getVisibility() != View.VISIBLE)) { + return false; + } + viewParent = view.getParent(); + } + + // A null parent implies the view is not visible. + if (viewParent == null) { + return false; + } + + // If no portion of the parent is visible, this view is not visible. + if (!mView.getLocalVisibleRect(mTempVisibleRect)) { + return false; + } + + // Check if the view intersects the visible portion of the parent. + return localRect.intersect(mTempVisibleRect); + } + + /** + * Returns whether this virtual view is accessibility focused. + * + * @return True if the view is accessibility focused. + */ + private boolean isAccessibilityFocused(int virtualViewId) { + return (mFocusedVirtualViewId == virtualViewId); + } + + /** + * Attempts to give accessibility focus to a virtual view. + * <p> + * A virtual view will not actually take focus if + * {@link android.view.accessibility.AccessibilityManager#isEnabled()} returns false, + * {@link android.view.accessibility.AccessibilityManager#isTouchExplorationEnabled()} returns false, + * or the view already has accessibility focus. + * + * @param virtualViewId The id of the virtual view on which to place + * accessibility focus. + * @return Whether this virtual view actually took accessibility focus. + */ + private boolean requestAccessibilityFocus(int virtualViewId) { + final AccessibilityManager accessibilityManager = + (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE); + + if (!mManager.isEnabled() + || !accessibilityManager.isTouchExplorationEnabled()) { + return false; + } + // TODO: Check virtual view visibility. + if (!isAccessibilityFocused(virtualViewId)) { + mFocusedVirtualViewId = virtualViewId; + // TODO: Only invalidate virtual view bounds. + mView.invalidate(); + sendEventForVirtualView(virtualViewId, + AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); + return true; + } + return false; + } + + /** + * Attempts to clear accessibility focus from a virtual view. + * + * @param virtualViewId The id of the virtual view from which to clear + * accessibility focus. + * @return Whether this virtual view actually cleared accessibility focus. + */ + private boolean clearAccessibilityFocus(int virtualViewId) { + if (isAccessibilityFocused(virtualViewId)) { + mFocusedVirtualViewId = INVALID_ID; + mView.invalidate(); + sendEventForVirtualView(virtualViewId, + AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); + return true; + } + return false; + } + + /** + * Provides a mapping between view-relative coordinates and logical + * items. + * + * @param x The view-relative x coordinate + * @param y The view-relative y coordinate + * @return virtual view identifier for the logical item under + * coordinates (x,y) + */ + protected abstract int getVirtualViewAt(float x, float y); + + /** + * Populates a list with the view's visible items. The ordering of items + * within {@code virtualViewIds} specifies order of accessibility focus + * traversal. + * + * @param virtualViewIds The list to populate with visible items + */ + protected abstract void getVisibleVirtualViews(List<Integer> virtualViewIds); + + /** + * Populates an {@link android.view.accessibility.AccessibilityEvent} with information about the + * specified item. + * <p> + * Implementations <b>must</b> populate the following required fields: + * <ul> + * <li>event text, see {@link android.view.accessibility.AccessibilityEvent#getText} or + * {@link android.view.accessibility.AccessibilityEvent#setContentDescription} + * </ul> + * <p> + * The helper class automatically populates the following fields with + * default values, but implementations may optionally override them: + * <ul> + * <li>item class name, set to android.view.View, see + * {@link android.view.accessibility.AccessibilityEvent#setClassName} + * </ul> + * <p> + * The following required fields are automatically populated by the + * helper class and may not be overridden: + * <ul> + * <li>package name, set to the package of the host view's + * {@link android.content.Context}, see {@link android.view.accessibility.AccessibilityEvent#setPackageName} + * <li>event source, set to the host view and virtual view identifier, + * see {@link android.view.accessibility.AccessibilityRecord#setSource(android.view.View, int)} + * </ul> + * + * @param virtualViewId The virtual view id for the item for which to + * populate the event + * @param event The event to populate + */ + protected abstract void onPopulateEventForVirtualView( + int virtualViewId, AccessibilityEvent event); + + /** + * Populates an {@link android.view.accessibility.AccessibilityNodeInfo} with information + * about the specified item. + * <p> + * Implementations <b>must</b> populate the following required fields: + * <ul> + * <li>event text, see {@link android.view.accessibility.AccessibilityNodeInfo#setText} or + * {@link android.view.accessibility.AccessibilityNodeInfo#setContentDescription} + * <li>bounds in parent coordinates, see + * {@link android.view.accessibility.AccessibilityNodeInfo#setBoundsInParent} + * </ul> + * <p> + * The helper class automatically populates the following fields with + * default values, but implementations may optionally override them: + * <ul> + * <li>enabled state, set to true, see + * {@link android.view.accessibility.AccessibilityNodeInfo#setEnabled} + * <li>item class name, identical to the class name set by + * {@link #onPopulateEventForVirtualView}, see + * {@link android.view.accessibility.AccessibilityNodeInfo#setClassName} + * </ul> + * <p> + * The following required fields are automatically populated by the + * helper class and may not be overridden: + * <ul> + * <li>package name, identical to the package name set by + * {@link #onPopulateEventForVirtualView}, see + * {@link android.view.accessibility.AccessibilityNodeInfo#setPackageName} + * <li>node source, identical to the event source set in + * {@link #onPopulateEventForVirtualView}, see + * {@link android.view.accessibility.AccessibilityNodeInfo#setSource(android.view.View, int)} + * <li>parent view, set to the host view, see + * {@link android.view.accessibility.AccessibilityNodeInfo#setParent(android.view.View)} + * <li>visibility, computed based on parent-relative bounds, see + * {@link android.view.accessibility.AccessibilityNodeInfo#setVisibleToUser} + * <li>accessibility focus, computed based on internal helper state, see + * {@link android.view.accessibility.AccessibilityNodeInfo#setAccessibilityFocused} + * <li>bounds in screen coordinates, computed based on host view bounds, + * see {@link android.view.accessibility.AccessibilityNodeInfo#setBoundsInScreen} + * </ul> + * <p> + * Additionally, the helper class automatically handles accessibility + * focus management by adding the appropriate + * {@link android.view.accessibility.AccessibilityNodeInfo#ACTION_ACCESSIBILITY_FOCUS} or + * {@link android.view.accessibility.AccessibilityNodeInfo#ACTION_CLEAR_ACCESSIBILITY_FOCUS} + * action. Implementations must <b>never</b> manually add these actions. + * <p> + * The helper class also automatically modifies parent- and + * screen-relative bounds to reflect the portion of the item visible + * within its parent. + * + * @param virtualViewId The virtual view identifier of the item for + * which to populate the node + * @param node The node to populate + */ + protected abstract void onPopulateNodeForVirtualView( + int virtualViewId, AccessibilityNodeInfo node); + + /** + * Performs the specified accessibility action on the item associated + * with the virtual view identifier. See + * {@link android.view.accessibility.AccessibilityNodeInfo#performAction(int, android.os.Bundle)} for + * more information. + * <p> + * Implementations <b>must</b> handle any actions added manually in + * {@link #onPopulateNodeForVirtualView}. + * <p> + * The helper class automatically handles focus management resulting + * from {@link android.view.accessibility.AccessibilityNodeInfo#ACTION_ACCESSIBILITY_FOCUS} + * and + * {@link android.view.accessibility.AccessibilityNodeInfo#ACTION_CLEAR_ACCESSIBILITY_FOCUS} + * actions. + * + * @param virtualViewId The virtual view identifier of the item on which + * to perform the action + * @param action The accessibility action to perform + * @param arguments (Optional) A bundle with additional arguments, or + * null + * @return true if the action was performed + */ + protected abstract boolean onPerformActionForVirtualView( + int virtualViewId, int action, Bundle arguments); + + /** + * Exposes a virtual view hierarchy to the accessibility framework. Only + * used in API 16+. + */ + private class ExploreByTouchNodeProvider extends AccessibilityNodeProvider { + @Override + public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { + return ExploreByTouchHelper.this.createNode(virtualViewId); + } + + @Override + public boolean performAction(int virtualViewId, int action, Bundle arguments) { + return ExploreByTouchHelper.this.performAction(virtualViewId, action, arguments); + } + } +} diff --git a/src/com/android/settings/widget/LinkAccessibilityHelper.java b/src/com/android/settings/widget/LinkAccessibilityHelper.java new file mode 100644 index 0000000..2d4d585 --- /dev/null +++ b/src/com/android/settings/widget/LinkAccessibilityHelper.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.widget; + +import android.graphics.Rect; +import android.os.Bundle; +import android.text.Layout; +import android.text.Spanned; +import android.text.style.ClickableSpan; +import android.util.Log; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.TextView; + +import java.util.List; + +/** + * Copied from setup wizard. + */ +public class LinkAccessibilityHelper extends ExploreByTouchHelper { + + private static final String TAG = "LinkAccessibilityHelper"; + + private final TextView mView; + private final Rect mTempRect = new Rect(); + + public LinkAccessibilityHelper(TextView view) { + super(view); + mView = view; + } + + @Override + protected int getVirtualViewAt(float x, float y) { + final CharSequence text = mView.getText(); + if (text instanceof Spanned) { + final Spanned spannedText = (Spanned) text; + final int offset = mView.getOffsetForPosition(x, y); + ClickableSpan[] linkSpans = spannedText.getSpans(offset, offset, ClickableSpan.class); + if (linkSpans.length == 1) { + ClickableSpan linkSpan = linkSpans[0]; + return spannedText.getSpanStart(linkSpan); + } + } + return INVALID_ID; + } + + @Override + protected void getVisibleVirtualViews(List<Integer> virtualViewIds) { + final CharSequence text = mView.getText(); + if (text instanceof Spanned) { + final Spanned spannedText = (Spanned) text; + ClickableSpan[] linkSpans = spannedText.getSpans(0, spannedText.length(), + ClickableSpan.class); + for (ClickableSpan span : linkSpans) { + virtualViewIds.add(spannedText.getSpanStart(span)); + } + } + } + + @Override + protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) { + final ClickableSpan span = getSpanForOffset(virtualViewId); + if (span != null) { + event.setContentDescription(getTextForSpan(span)); + } else { + Log.e(TAG, "ClickableSpan is null for offset: " + virtualViewId); + event.setContentDescription(mView.getText()); + } + } + + @Override + protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo info) { + final ClickableSpan span = getSpanForOffset(virtualViewId); + if (span != null) { + info.setContentDescription(getTextForSpan(span)); + } else { + Log.e(TAG, "ClickableSpan is null for offset: " + virtualViewId); + info.setContentDescription(mView.getText()); + } + info.setFocusable(true); + info.setClickable(true); + getBoundsForSpan(span, mTempRect); + if (!mTempRect.isEmpty()) { + info.setBoundsInParent(getBoundsForSpan(span, mTempRect)); + } else { + Log.e(TAG, "LinkSpan bounds is empty for: " + virtualViewId); + mTempRect.set(0, 0, 1, 1); + info.setBoundsInParent(mTempRect); + } + info.addAction(AccessibilityNodeInfo.ACTION_CLICK); + } + + @Override + protected boolean onPerformActionForVirtualView(int virtualViewId, int action, + Bundle arguments) { + if (action == AccessibilityNodeInfo.ACTION_CLICK) { + ClickableSpan span = getSpanForOffset(virtualViewId); + if (span != null) { + span.onClick(mView); + return true; + } else { + Log.e(TAG, "LinkSpan is null for offset: " + virtualViewId); + } + } + return false; + } + + private ClickableSpan getSpanForOffset(int offset) { + CharSequence text = mView.getText(); + if (text instanceof Spanned) { + Spanned spannedText = (Spanned) text; + ClickableSpan[] spans = spannedText.getSpans(offset, offset, ClickableSpan.class); + if (spans.length == 1) { + return spans[0]; + } + } + return null; + } + + private CharSequence getTextForSpan(ClickableSpan span) { + CharSequence text = mView.getText(); + if (text instanceof Spanned) { + Spanned spannedText = (Spanned) text; + return spannedText.subSequence(spannedText.getSpanStart(span), + spannedText.getSpanEnd(span)); + } + return text; + } + + // Find the bounds of a span. If it spans multiple lines, it will only return the bounds for the + // section on the first line. + private Rect getBoundsForSpan(ClickableSpan span, Rect outRect) { + CharSequence text = mView.getText(); + outRect.setEmpty(); + if (text instanceof Spanned) { + Spanned spannedText = (Spanned) text; + final int spanStart = spannedText.getSpanStart(span); + final int spanEnd = spannedText.getSpanEnd(span); + final Layout layout = mView.getLayout(); + final float xStart = layout.getPrimaryHorizontal(spanStart); + final float xEnd = layout.getPrimaryHorizontal(spanEnd); + final int lineStart = layout.getLineForOffset(spanStart); + final int lineEnd = layout.getLineForOffset(spanEnd); + layout.getLineBounds(lineStart, outRect); + outRect.left = (int) xStart; + if (lineEnd == lineStart) { + outRect.right = (int) xEnd; + } // otherwise just leave it at the end of the start line + + // Offset for padding + outRect.offset(mView.getTotalPaddingLeft(), mView.getTotalPaddingTop()); + } + return outRect; + } +} diff --git a/src/com/android/settings/widget/LinkTextView.java b/src/com/android/settings/widget/LinkTextView.java new file mode 100644 index 0000000..ab72fcf --- /dev/null +++ b/src/com/android/settings/widget/LinkTextView.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.widget; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.widget.TextView; + +/** + * Copied from setup wizard. + */ +public class LinkTextView extends TextView { + + private LinkAccessibilityHelper mAccessibilityHelper; + + public LinkTextView(Context context) { + this(context, null); + } + + public LinkTextView(Context context, AttributeSet attrs) { + super(context, attrs); + mAccessibilityHelper = new LinkAccessibilityHelper(this); + setAccessibilityDelegate(mAccessibilityHelper); + } + + @Override + protected boolean dispatchHoverEvent(@NonNull MotionEvent event) { + if (mAccessibilityHelper.dispatchHoverEvent(event)) { + return true; + } + return super.dispatchHoverEvent(event); + } +} diff --git a/src/com/android/settings/wifi/WifiConfigController.java b/src/com/android/settings/wifi/WifiConfigController.java index c74c56a..ab7999f 100644 --- a/src/com/android/settings/wifi/WifiConfigController.java +++ b/src/com/android/settings/wifi/WifiConfigController.java @@ -482,9 +482,17 @@ public class WifiConfigController implements TextWatcher, String clientCert = (String) mEapUserCertSpinner.getSelectedItem(); if (clientCert.equals(unspecifiedCert)) clientCert = ""; config.enterpriseConfig.setClientCertificateAlias(clientCert); - config.enterpriseConfig.setIdentity(mEapIdentityView.getText().toString()); - config.enterpriseConfig.setAnonymousIdentity( - mEapAnonymousView.getText().toString()); + if (eapMethod == Eap.SIM || eapMethod == Eap.AKA || eapMethod == Eap.AKA_PRIME) { + config.enterpriseConfig.setIdentity(""); + config.enterpriseConfig.setAnonymousIdentity(""); + } else if (eapMethod == Eap.PWD) { + config.enterpriseConfig.setIdentity(mEapIdentityView.getText().toString()); + config.enterpriseConfig.setAnonymousIdentity(""); + } else { + config.enterpriseConfig.setIdentity(mEapIdentityView.getText().toString()); + config.enterpriseConfig.setAnonymousIdentity( + mEapAnonymousView.getText().toString()); + } if (mPasswordView.isShown()) { // For security reasons, a previous password is not displayed to user. |