diff options
author | Xin Li <delphij@google.com> | 2019-09-05 16:53:18 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2019-09-05 16:53:18 +0000 |
commit | d7a8e234e9b94ff36cd271555a0bbf8a01a10932 (patch) | |
tree | b6c98b0e3e76f797dbc57e77a8aac776ca1d1324 /src | |
parent | 1e53efc4c48ec80421bb0542b770332ab8efdab5 (diff) | |
parent | 7195e05dfaf1c848a7af9b844cd8bd05e9c220fe (diff) | |
download | packages_apps_Settings-d7a8e234e9b94ff36cd271555a0bbf8a01a10932.tar.gz packages_apps_Settings-d7a8e234e9b94ff36cd271555a0bbf8a01a10932.tar.bz2 packages_apps_Settings-d7a8e234e9b94ff36cd271555a0bbf8a01a10932.zip |
Merge "DO NOT MERGE - Merge Android 10 into master"
Diffstat (limited to 'src')
1643 files changed, 74713 insertions, 38349 deletions
diff --git a/src/com/android/settings/AccessiblePreferenceCategory.java b/src/com/android/settings/AccessiblePreferenceCategory.java index 494dee5b93..6e1a3d5f40 100644 --- a/src/com/android/settings/AccessiblePreferenceCategory.java +++ b/src/com/android/settings/AccessiblePreferenceCategory.java @@ -17,6 +17,7 @@ package com.android.settings; import android.content.Context; + import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceViewHolder; diff --git a/src/com/android/settings/AirplaneModeEnabler.java b/src/com/android/settings/AirplaneModeEnabler.java index f14409624d..dde7ce0fcf 100644 --- a/src/com/android/settings/AirplaneModeEnabler.java +++ b/src/com/android/settings/AirplaneModeEnabler.java @@ -16,6 +16,7 @@ package com.android.settings; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; import android.database.ContentObserver; @@ -26,7 +27,6 @@ import android.os.SystemProperties; import android.os.UserHandle; import android.provider.Settings; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.telephony.PhoneStateIntentReceiver; import com.android.internal.telephony.TelephonyProperties; import com.android.settingslib.WirelessUtils; @@ -129,7 +129,7 @@ public class AirplaneModeEnabler { SystemProperties.get(TelephonyProperties.PROPERTY_INECM_MODE))) { // In ECM mode, do not update database at this point } else { - mMetricsFeatureProvider.action(mContext, MetricsEvent.ACTION_AIRPLANE_TOGGLE, + mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_AIRPLANE_TOGGLE, isAirplaneModeOn); setAirplaneModeOn(isAirplaneModeOn); } diff --git a/src/com/android/settings/AllowBindAppWidgetActivity.java b/src/com/android/settings/AllowBindAppWidgetActivity.java index 52e7870e2b..1cfeb497be 100644 --- a/src/com/android/settings/AllowBindAppWidgetActivity.java +++ b/src/com/android/settings/AllowBindAppWidgetActivity.java @@ -16,7 +16,6 @@ package com.android.settings; -import android.app.AlertDialog; import android.appwidget.AppWidgetManager; import android.content.ComponentName; import android.content.Context; @@ -30,6 +29,8 @@ import android.util.Log; import android.view.LayoutInflater; import android.widget.CheckBox; +import androidx.appcompat.app.AlertDialog; + import com.android.internal.app.AlertActivity; import com.android.internal.app.AlertController; diff --git a/src/com/android/settings/BandMode.java b/src/com/android/settings/BandMode.java index e172a5c6fd..6987d92e94 100644 --- a/src/com/android/settings/BandMode.java +++ b/src/com/android/settings/BandMode.java @@ -1,7 +1,6 @@ package com.android.settings; import android.app.Activity; -import android.app.AlertDialog; import android.content.DialogInterface; import android.os.AsyncResult; import android.os.Bundle; @@ -10,15 +9,15 @@ import android.os.Message; import android.util.Log; import android.view.View; import android.view.Window; -import android.view.WindowManager; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.ListView; +import androidx.appcompat.app.AlertDialog; + import com.android.internal.telephony.Phone; import com.android.internal.telephony.PhoneFactory; - /** * Radio Band Mode Selection Class * diff --git a/src/com/android/settings/BrightnessPreference.java b/src/com/android/settings/BrightnessPreference.java index 86d8336d25..b3cf433578 100644 --- a/src/com/android/settings/BrightnessPreference.java +++ b/src/com/android/settings/BrightnessPreference.java @@ -19,9 +19,10 @@ package com.android.settings; import android.content.Context; import android.content.Intent; import android.os.UserHandle; -import androidx.preference.Preference; import android.util.AttributeSet; +import androidx.preference.Preference; + public class BrightnessPreference extends Preference { public BrightnessPreference(Context context, AttributeSet attrs) { diff --git a/src/com/android/settings/BugreportPreference.java b/src/com/android/settings/BugreportPreference.java index 6371e3a57b..1652ad24c7 100644 --- a/src/com/android/settings/BugreportPreference.java +++ b/src/com/android/settings/BugreportPreference.java @@ -17,7 +17,7 @@ package com.android.settings; import android.app.ActivityManager; -import android.app.AlertDialog.Builder; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.DialogInterface; import android.os.RemoteException; @@ -27,11 +27,12 @@ import android.view.View; import android.widget.CheckedTextView; import android.widget.TextView; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.appcompat.app.AlertDialog.Builder; + import com.android.settings.overlay.FeatureFactory; -import com.android.settingslib.CustomDialogPreference; +import com.android.settingslib.CustomDialogPreferenceCompat; -public class BugreportPreference extends CustomDialogPreference { +public class BugreportPreference extends CustomDialogPreferenceCompat { private static final String TAG = "BugreportPreference"; @@ -84,12 +85,12 @@ public class BugreportPreference extends CustomDialogPreference { if (mFullTitle.isChecked()) { Log.v(TAG, "Taking full bugreport right away"); FeatureFactory.getFactory(context).getMetricsFeatureProvider().action(context, - MetricsEvent.ACTION_BUGREPORT_FROM_SETTINGS_FULL); + SettingsEnums.ACTION_BUGREPORT_FROM_SETTINGS_FULL); takeBugreport(ActivityManager.BUGREPORT_OPTION_FULL); } else { Log.v(TAG, "Taking interactive bugreport right away"); FeatureFactory.getFactory(context).getMetricsFeatureProvider().action(context, - MetricsEvent.ACTION_BUGREPORT_FROM_SETTINGS_INTERACTIVE); + SettingsEnums.ACTION_BUGREPORT_FROM_SETTINGS_INTERACTIVE); takeBugreport(ActivityManager.BUGREPORT_OPTION_INTERACTIVE); } } diff --git a/src/com/android/settings/CancellablePreference.java b/src/com/android/settings/CancellablePreference.java index 92c100327c..4288e8b60f 100644 --- a/src/com/android/settings/CancellablePreference.java +++ b/src/com/android/settings/CancellablePreference.java @@ -16,13 +16,14 @@ package com.android.settings; import android.content.Context; -import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; import android.util.AttributeSet; import android.view.View; import android.view.View.OnClickListener; import android.widget.ImageView; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + public class CancellablePreference extends Preference implements OnClickListener { private boolean mCancellable; diff --git a/src/com/android/settings/CredentialStorage.java b/src/com/android/settings/CredentialStorage.java deleted file mode 100644 index 0a559c2bd9..0000000000 --- a/src/com/android/settings/CredentialStorage.java +++ /dev/null @@ -1,604 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settings; - -import android.app.Activity; -import android.app.AlertDialog; -import android.app.admin.DevicePolicyManager; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.UserInfo; -import android.content.res.Resources; -import android.os.AsyncTask; -import android.os.Bundle; -import android.os.Process; -import android.os.RemoteException; -import android.os.UserHandle; -import android.os.UserManager; -import android.security.Credentials; -import android.security.KeyChain; -import android.security.KeyChain.KeyChainConnection; -import android.security.KeyStore; -import android.text.Editable; -import android.text.TextUtils; -import android.text.TextWatcher; -import android.util.Log; -import android.view.View; -import android.widget.Button; -import android.widget.TextView; -import android.widget.Toast; - -import com.android.internal.widget.LockPatternUtils; -import com.android.org.bouncycastle.asn1.ASN1InputStream; -import com.android.org.bouncycastle.asn1.pkcs.PrivateKeyInfo; -import com.android.settings.password.ChooseLockSettingsHelper; -import com.android.settings.security.ConfigureKeyGuardDialog; -import com.android.settings.vpn2.VpnUtils; - -import java.io.ByteArrayInputStream; -import java.io.IOException; - -import sun.security.util.ObjectIdentifier; -import sun.security.x509.AlgorithmId; - -/** - * CredentialStorage handles KeyStore reset, unlock, and install. - * - * CredentialStorage has a pretty convoluted state machine to migrate - * from the old style separate keystore password to a new key guard - * based password, as well as to deal with setting up the key guard if - * necessary. - * - * KeyStore: UNINITALIZED - * KeyGuard: OFF - * Action: set up key guard - * Notes: factory state - * - * KeyStore: UNINITALIZED - * KeyGuard: ON - * Action: confirm key guard - * Notes: user had key guard but no keystore and upgraded from pre-ICS - * OR user had key guard and pre-ICS keystore password which was then reset - * - * KeyStore: LOCKED - * KeyGuard: OFF/ON - * Action: old unlock dialog - * Notes: assume old password, need to use it to unlock. - * if unlock, ensure key guard before install. - * if reset, treat as UNINITALIZED/OFF - * - * KeyStore: UNLOCKED - * KeyGuard: OFF - * Action: set up key guard - * Notes: ensure key guard, then proceed - * - * KeyStore: UNLOCKED - * keyguard: ON - * Action: normal unlock/install - * Notes: this is the common case - */ -public final class CredentialStorage extends Activity { - - private static final String TAG = "CredentialStorage"; - - public static final String ACTION_UNLOCK = "com.android.credentials.UNLOCK"; - public static final String ACTION_INSTALL = "com.android.credentials.INSTALL"; - public static final String ACTION_RESET = "com.android.credentials.RESET"; - - // This is the minimum acceptable password quality. If the current password quality is - // lower than this, keystore should not be activated. - public static final int MIN_PASSWORD_QUALITY = DevicePolicyManager.PASSWORD_QUALITY_SOMETHING; - - private static final int CONFIRM_KEY_GUARD_REQUEST = 1; - private static final int CONFIRM_CLEAR_SYSTEM_CREDENTIAL_REQUEST = 2; - - private final KeyStore mKeyStore = KeyStore.getInstance(); - - /** - * When non-null, the bundle containing credentials to install. - */ - private Bundle mInstallBundle; - - /** - * After unsuccessful KeyStore.unlock, the number of unlock - * attempts remaining before the KeyStore will reset itself. - * - * Reset to -1 on successful unlock or reset. - */ - private int mRetriesRemaining = -1; - - @Override - protected void onResume() { - super.onResume(); - - Intent intent = getIntent(); - String action = intent.getAction(); - UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE); - if (!userManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_CREDENTIALS)) { - if (ACTION_RESET.equals(action)) { - new ResetDialog(); - } else { - if (ACTION_INSTALL.equals(action) && checkCallerIsCertInstallerOrSelfInProfile()) { - mInstallBundle = intent.getExtras(); - } - // ACTION_UNLOCK also handled here in addition to ACTION_INSTALL - handleUnlockOrInstall(); - } - } else { - // Users can set a screen lock if there is none even if they can't modify the - // credentials store. - if (ACTION_UNLOCK.equals(action) && mKeyStore.state() == KeyStore.State.UNINITIALIZED) { - ensureKeyGuard(); - } else { - finish(); - } - } - } - - /** - * Based on the current state of the KeyStore and key guard, try to - * make progress on unlocking or installing to the keystore. - */ - private void handleUnlockOrInstall() { - // something already decided we are done, do not proceed - if (isFinishing()) { - return; - } - switch (mKeyStore.state()) { - case UNINITIALIZED: { - ensureKeyGuard(); - return; - } - case LOCKED: { - new UnlockDialog(); - return; - } - case UNLOCKED: { - if (!checkKeyGuardQuality()) { - final ConfigureKeyGuardDialog dialog = new ConfigureKeyGuardDialog(); - dialog.show(getFragmentManager(), ConfigureKeyGuardDialog.TAG); - return; - } - installIfAvailable(); - finish(); - return; - } - } - } - - /** - * Make sure the user enters the key guard to set or change the - * keystore password. This can be used in UNINITIALIZED to set the - * keystore password or UNLOCKED to change the password (as is the - * case after unlocking with an old-style password). - */ - private void ensureKeyGuard() { - if (!checkKeyGuardQuality()) { - // key guard not setup, doing so will initialize keystore - final ConfigureKeyGuardDialog dialog = new ConfigureKeyGuardDialog(); - dialog.show(getFragmentManager(), ConfigureKeyGuardDialog.TAG); - // will return to onResume after Activity - return; - } - // force key guard confirmation - if (confirmKeyGuard(CONFIRM_KEY_GUARD_REQUEST)) { - // will return password value via onActivityResult - return; - } - finish(); - } - - /** - * Returns true if the currently set key guard matches our minimum quality requirements. - */ - private boolean checkKeyGuardQuality() { - int credentialOwner = - UserManager.get(this).getCredentialOwnerProfile(UserHandle.myUserId()); - int quality = new LockPatternUtils(this).getActivePasswordQuality(credentialOwner); - return (quality >= MIN_PASSWORD_QUALITY); - } - - private boolean isHardwareBackedKey(byte[] keyData) { - try { - ASN1InputStream bIn = new ASN1InputStream(new ByteArrayInputStream(keyData)); - PrivateKeyInfo pki = PrivateKeyInfo.getInstance(bIn.readObject()); - String algOid = pki.getPrivateKeyAlgorithm().getAlgorithm().getId(); - String algName = new AlgorithmId(new ObjectIdentifier(algOid)).getName(); - - return KeyChain.isBoundKeyAlgorithm(algName); - } catch (IOException e) { - Log.e(TAG, "Failed to parse key data"); - return false; - } - } - - /** - * Install credentials if available, otherwise do nothing. - */ - private void installIfAvailable() { - if (mInstallBundle == null || mInstallBundle.isEmpty()) { - return; - } - - Bundle bundle = mInstallBundle; - mInstallBundle = null; - - final int uid = bundle.getInt(Credentials.EXTRA_INSTALL_AS_UID, KeyStore.UID_SELF); - - if (uid != KeyStore.UID_SELF && !UserHandle.isSameUser(uid, Process.myUid())) { - int dstUserId = UserHandle.getUserId(uid); - int myUserId = UserHandle.myUserId(); - - // Restrict install target to the wifi uid. - if (uid != Process.WIFI_UID) { - Log.e(TAG, "Failed to install credentials as uid " + uid + ": cross-user installs" - + " may only target wifi uids"); - return; - } - - Intent installIntent = new Intent(ACTION_INSTALL) - .setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT) - .putExtras(bundle); - startActivityAsUser(installIntent, new UserHandle(dstUserId)); - return; - } - - if (bundle.containsKey(Credentials.EXTRA_USER_PRIVATE_KEY_NAME)) { - String key = bundle.getString(Credentials.EXTRA_USER_PRIVATE_KEY_NAME); - byte[] value = bundle.getByteArray(Credentials.EXTRA_USER_PRIVATE_KEY_DATA); - - int flags = KeyStore.FLAG_ENCRYPTED; - if (uid == Process.WIFI_UID && isHardwareBackedKey(value)) { - // Hardware backed keystore is secure enough to allow for WIFI stack - // to enable access to secure networks without user intervention - Log.d(TAG, "Saving private key with FLAG_NONE for WIFI_UID"); - flags = KeyStore.FLAG_NONE; - } - - if (!mKeyStore.importKey(key, value, uid, flags)) { - Log.e(TAG, "Failed to install " + key + " as uid " + uid); - return; - } - // The key was prepended USER_PRIVATE_KEY by the CredentialHelper. However, - // KeyChain internally uses the raw alias name and only prepends USER_PRIVATE_KEY - // to the key name when interfacing with KeyStore. - // This is generally a symptom of CredentialStorage and CredentialHelper relying - // on internal implementation details of KeyChain and imitating its functionality - // rather than delegating to KeyChain for the certificate installation. - if (uid == Process.SYSTEM_UID || uid == KeyStore.UID_SELF) { - new MarkKeyAsUserSelectable( - key.replaceFirst("^" + Credentials.USER_PRIVATE_KEY, "")).execute(); - } - } - - int flags = KeyStore.FLAG_NONE; - - if (bundle.containsKey(Credentials.EXTRA_USER_CERTIFICATE_NAME)) { - String certName = bundle.getString(Credentials.EXTRA_USER_CERTIFICATE_NAME); - byte[] certData = bundle.getByteArray(Credentials.EXTRA_USER_CERTIFICATE_DATA); - - if (!mKeyStore.put(certName, certData, uid, flags)) { - Log.e(TAG, "Failed to install " + certName + " as uid " + uid); - return; - } - } - - if (bundle.containsKey(Credentials.EXTRA_CA_CERTIFICATES_NAME)) { - String caListName = bundle.getString(Credentials.EXTRA_CA_CERTIFICATES_NAME); - byte[] caListData = bundle.getByteArray(Credentials.EXTRA_CA_CERTIFICATES_DATA); - - if (!mKeyStore.put(caListName, caListData, uid, flags)) { - Log.e(TAG, "Failed to install " + caListName + " as uid " + uid); - return; - } - } - - // Send the broadcast. - Intent broadcast = new Intent(KeyChain.ACTION_KEYCHAIN_CHANGED); - sendBroadcast(broadcast); - - setResult(RESULT_OK); - } - - /** - * Prompt for reset confirmation, resetting on confirmation, finishing otherwise. - */ - private class ResetDialog - implements DialogInterface.OnClickListener, DialogInterface.OnDismissListener { - private boolean mResetConfirmed; - - private ResetDialog() { - AlertDialog dialog = new AlertDialog.Builder(CredentialStorage.this) - .setTitle(android.R.string.dialog_alert_title) - .setMessage(R.string.credentials_reset_hint) - .setPositiveButton(android.R.string.ok, this) - .setNegativeButton(android.R.string.cancel, this) - .create(); - dialog.setOnDismissListener(this); - dialog.show(); - } - - @Override - public void onClick(DialogInterface dialog, int button) { - mResetConfirmed = (button == DialogInterface.BUTTON_POSITIVE); - } - - @Override - public void onDismiss(DialogInterface dialog) { - if (mResetConfirmed) { - mResetConfirmed = false; - if (confirmKeyGuard(CONFIRM_CLEAR_SYSTEM_CREDENTIAL_REQUEST)) { - // will return password value via onActivityResult - return; - } - } - finish(); - } - } - - /** - * Background task to handle reset of both keystore and user installed CAs. - */ - private class ResetKeyStoreAndKeyChain extends AsyncTask<Void, Void, Boolean> { - - @Override - protected Boolean doInBackground(Void... unused) { - - // Clear all the users credentials could have been installed in for this user. - new LockPatternUtils(CredentialStorage.this).resetKeyStore(UserHandle.myUserId()); - - try { - KeyChainConnection keyChainConnection = KeyChain.bind(CredentialStorage.this); - try { - return keyChainConnection.getService().reset(); - } catch (RemoteException e) { - return false; - } finally { - keyChainConnection.close(); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return false; - } - } - - @Override - protected void onPostExecute(Boolean success) { - if (success) { - Toast.makeText(CredentialStorage.this, - R.string.credentials_erased, Toast.LENGTH_SHORT).show(); - clearLegacyVpnIfEstablished(); - } else { - Toast.makeText(CredentialStorage.this, - R.string.credentials_not_erased, Toast.LENGTH_SHORT).show(); - } - finish(); - } - } - - private void clearLegacyVpnIfEstablished() { - boolean isDone = VpnUtils.disconnectLegacyVpn(getApplicationContext()); - if (isDone) { - Toast.makeText(CredentialStorage.this, R.string.vpn_disconnected, - Toast.LENGTH_SHORT).show(); - } - } - - /** - * Background task to mark a given key alias as user-selectable, so that - * it can be selected by users from the Certificate Selection prompt. - */ - private class MarkKeyAsUserSelectable extends AsyncTask<Void, Void, Boolean> { - final String mAlias; - - public MarkKeyAsUserSelectable(String alias) { - mAlias = alias; - } - - @Override - protected Boolean doInBackground(Void... unused) { - try (KeyChainConnection keyChainConnection = KeyChain.bind(CredentialStorage.this)) { - keyChainConnection.getService().setUserSelectable(mAlias, true); - return true; - } catch (RemoteException e) { - Log.w(TAG, "Failed to mark key " + mAlias + " as user-selectable."); - return false; - } catch (InterruptedException e) { - Log.w(TAG, "Failed to mark key " + mAlias + " as user-selectable."); - Thread.currentThread().interrupt(); - return false; - } - } - } - - /** - * Check that the caller is either certinstaller or Settings running in a profile of this user. - */ - private boolean checkCallerIsCertInstallerOrSelfInProfile() { - if (TextUtils.equals("com.android.certinstaller", getCallingPackage())) { - // CertInstaller is allowed to install credentials if it has the same signature as - // Settings package. - return getPackageManager().checkSignatures( - getCallingPackage(), getPackageName()) == PackageManager.SIGNATURE_MATCH; - } - - final int launchedFromUserId; - try { - int launchedFromUid = android.app.ActivityManager.getService() - .getLaunchedFromUid(getActivityToken()); - if (launchedFromUid == -1) { - Log.e(TAG, ACTION_INSTALL + " must be started with startActivityForResult"); - return false; - } - if (!UserHandle.isSameApp(launchedFromUid, Process.myUid())) { - // Not the same app - return false; - } - launchedFromUserId = UserHandle.getUserId(launchedFromUid); - } catch (RemoteException re) { - // Error talking to ActivityManager, just give up - return false; - } - - UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE); - UserInfo parentInfo = userManager.getProfileParent(launchedFromUserId); - if (parentInfo == null || parentInfo.id != UserHandle.myUserId()) { - // Caller is not running in a profile of this user - return false; - } - return true; - } - - /** - * Confirm existing key guard, returning password via onActivityResult. - */ - private boolean confirmKeyGuard(int requestCode) { - Resources res = getResources(); - boolean launched = new ChooseLockSettingsHelper(this) - .launchConfirmationActivity(requestCode, - res.getText(R.string.credentials_title), true); - return launched; - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - /** - * Receive key guard password initiated by confirmKeyGuard. - */ - if (requestCode == CONFIRM_KEY_GUARD_REQUEST) { - if (resultCode == Activity.RESULT_OK) { - String password = data.getStringExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD); - if (!TextUtils.isEmpty(password)) { - // success - mKeyStore.unlock(password); - // return to onResume - return; - } - } - // failed confirmation, bail - finish(); - } else if (requestCode == CONFIRM_CLEAR_SYSTEM_CREDENTIAL_REQUEST) { - if (resultCode == Activity.RESULT_OK) { - new ResetKeyStoreAndKeyChain().execute(); - return; - } - // failed confirmation, bail - finish(); - } - } - - /** - * Prompt for unlock with old-style password. - * - * On successful unlock, ensure migration to key guard before continuing. - * On unsuccessful unlock, retry by calling handleUnlockOrInstall. - */ - private class UnlockDialog implements TextWatcher, - DialogInterface.OnClickListener, DialogInterface.OnDismissListener { - private boolean mUnlockConfirmed; - - private final Button mButton; - private final TextView mOldPassword; - private final TextView mError; - - private UnlockDialog() { - View view = View.inflate(CredentialStorage.this, R.layout.credentials_dialog, null); - - CharSequence text; - if (mRetriesRemaining == -1) { - text = getResources().getText(R.string.credentials_unlock_hint); - } else if (mRetriesRemaining > 3) { - text = getResources().getText(R.string.credentials_wrong_password); - } else if (mRetriesRemaining == 1) { - text = getResources().getText(R.string.credentials_reset_warning); - } else { - text = getString(R.string.credentials_reset_warning_plural, mRetriesRemaining); - } - - ((TextView) view.findViewById(R.id.hint)).setText(text); - mOldPassword = (TextView) view.findViewById(R.id.old_password); - mOldPassword.setVisibility(View.VISIBLE); - mOldPassword.addTextChangedListener(this); - mError = (TextView) view.findViewById(R.id.error); - - AlertDialog dialog = new AlertDialog.Builder(CredentialStorage.this) - .setView(view) - .setTitle(R.string.credentials_unlock) - .setPositiveButton(android.R.string.ok, this) - .setNegativeButton(android.R.string.cancel, this) - .create(); - dialog.setOnDismissListener(this); - dialog.show(); - mButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE); - mButton.setEnabled(false); - } - - @Override - public void afterTextChanged(Editable editable) { - mButton.setEnabled(mOldPassword == null || mOldPassword.getText().length() > 0); - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - } - - @Override - public void onClick(DialogInterface dialog, int button) { - mUnlockConfirmed = (button == DialogInterface.BUTTON_POSITIVE); - } - - @Override - public void onDismiss(DialogInterface dialog) { - if (mUnlockConfirmed) { - mUnlockConfirmed = false; - mError.setVisibility(View.VISIBLE); - mKeyStore.unlock(mOldPassword.getText().toString()); - int error = mKeyStore.getLastError(); - if (error == KeyStore.NO_ERROR) { - mRetriesRemaining = -1; - Toast.makeText(CredentialStorage.this, - R.string.credentials_enabled, - Toast.LENGTH_SHORT).show(); - // aha, now we are unlocked, switch to key guard. - // we'll end up back in onResume to install - ensureKeyGuard(); - } else if (error == KeyStore.UNINITIALIZED) { - mRetriesRemaining = -1; - Toast.makeText(CredentialStorage.this, - R.string.credentials_erased, - Toast.LENGTH_SHORT).show(); - // we are reset, we can now set new password with key guard - handleUnlockOrInstall(); - } else if (error >= KeyStore.WRONG_PASSWORD) { - // we need to try again - mRetriesRemaining = error - KeyStore.WRONG_PASSWORD + 1; - handleUnlockOrInstall(); - } - return; - } - finish(); - } - } -} diff --git a/src/com/android/settings/CryptKeeper.java b/src/com/android/settings/CryptKeeper.java index ca54b07dcd..05054b4a55 100644 --- a/src/com/android/settings/CryptKeeper.java +++ b/src/com/android/settings/CryptKeeper.java @@ -772,9 +772,10 @@ public class CryptKeeper extends Activity implements TextView.OnEditorActionList if (imeSwitcher != null && hasMultipleEnabledIMEsOrSubtypes(imm, false)) { imeSwitcher.setVisibility(View.VISIBLE); imeSwitcher.setOnClickListener(new OnClickListener() { - @Override + @Override public void onClick(View v) { - imm.showInputMethodPicker(false /* showAuxiliarySubtypes */); + imm.showInputMethodPickerFromSystem(false /* showAuxiliarySubtypes */, + v.getDisplay().getDisplayId()); } }); } diff --git a/src/com/android/settings/CryptKeeperConfirm.java b/src/com/android/settings/CryptKeeperConfirm.java index 227120089e..49d027ba49 100644 --- a/src/com/android/settings/CryptKeeperConfirm.java +++ b/src/com/android/settings/CryptKeeperConfirm.java @@ -19,6 +19,7 @@ package com.android.settings; import android.annotation.Nullable; import android.app.Activity; import android.app.StatusBarManager; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; import android.os.Bundle; @@ -34,10 +35,10 @@ import android.view.View; import android.view.ViewGroup; import android.widget.Button; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.widget.LockPatternUtils; import com.android.settings.core.InstrumentedFragment; +import java.util.Arrays; import java.util.Locale; public class CryptKeeperConfirm extends InstrumentedFragment { @@ -46,7 +47,7 @@ public class CryptKeeperConfirm extends InstrumentedFragment { @Override public int getMetricsCategory() { - return MetricsEvent.CRYPT_KEEPER_CONFIRM; + return SettingsEnums.CRYPT_KEEPER_CONFIRM; } public static class Blank extends Activity { @@ -87,7 +88,12 @@ public class CryptKeeperConfirm extends InstrumentedFragment { IStorageManager storageManager = IStorageManager.Stub.asInterface(service); try { Bundle args = getIntent().getExtras(); - storageManager.encryptStorage(args.getInt("type", -1), args.getString("password")); + // TODO(b/120484642): Update vold to accept a password as a byte array + byte[] passwordBytes = args.getByteArray("password"); + String password = passwordBytes != null ? new String(passwordBytes) : null; + Arrays.fill(passwordBytes, (byte) 0); + storageManager.encryptStorage(args.getInt("type", -1), + password); } catch (Exception e) { Log.e("CryptKeeper", "Error while encrypting...", e); } diff --git a/src/com/android/settings/CustomListPreference.java b/src/com/android/settings/CustomListPreference.java index cb87440f7a..978858b1bc 100644 --- a/src/com/android/settings/CustomListPreference.java +++ b/src/com/android/settings/CustomListPreference.java @@ -16,20 +16,21 @@ package com.android.settings; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.Fragment; -import android.app.FragmentTransaction; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.os.Bundle; -import androidx.preference.ListPreferenceDialogFragment; -import androidx.preference.ListPreference; import android.util.AttributeSet; -import com.android.internal.logging.nano.MetricsProto; +import androidx.appcompat.app.AlertDialog.Builder; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentTransaction; +import androidx.preference.ListPreference; +import androidx.preference.ListPreferenceDialogFragmentCompat; + import com.android.settings.core.instrumentation.InstrumentedDialogFragment; public class CustomListPreference extends ListPreference { @@ -39,11 +40,11 @@ public class CustomListPreference extends ListPreference { } public CustomListPreference(Context context, AttributeSet attrs, int defStyleAttr, - int defStyleRes) { + int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } - protected void onPrepareDialogBuilder(AlertDialog.Builder builder, + protected void onPrepareDialogBuilder(Builder builder, DialogInterface.OnClickListener listener) { } @@ -63,7 +64,7 @@ public class CustomListPreference extends ListPreference { * * @param value the value the user is about to choose * @return the message to show in a confirmation dialog, or {@code null} to - * not request confirmation + * not request confirmation */ protected CharSequence getConfirmationMessage(String value) { return null; @@ -72,15 +73,17 @@ public class CustomListPreference extends ListPreference { protected void onDialogStateRestored(Dialog dialog, Bundle savedInstanceState) { } - public static class CustomListPreferenceDialogFragment extends ListPreferenceDialogFragment { + public static class CustomListPreferenceDialogFragment extends + ListPreferenceDialogFragmentCompat { private static final java.lang.String KEY_CLICKED_ENTRY_INDEX = "settings.CustomListPrefDialog.KEY_CLICKED_ENTRY_INDEX"; private int mClickedDialogEntryIndex; - public static ListPreferenceDialogFragment newInstance(String key) { - final ListPreferenceDialogFragment fragment = new CustomListPreferenceDialogFragment(); + public static ListPreferenceDialogFragmentCompat newInstance(String key) { + final ListPreferenceDialogFragmentCompat fragment = + new CustomListPreferenceDialogFragment(); final Bundle b = new Bundle(1); b.putString(ARG_KEY, key); fragment.setArguments(b); @@ -92,7 +95,7 @@ public class CustomListPreference extends ListPreference { } @Override - protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { + protected void onPrepareDialogBuilder(Builder builder) { super.onPrepareDialogBuilder(builder); mClickedDialogEntryIndex = getCustomizablePreference() .findIndexOfValue(getCustomizablePreference().getValue()); @@ -201,7 +204,7 @@ public class CustomListPreference extends ListPreference { public static class ConfirmDialogFragment extends InstrumentedDialogFragment { @Override public Dialog onCreateDialog(Bundle savedInstanceState) { - return new AlertDialog.Builder(getActivity()) + return new Builder(getActivity()) .setMessage(getArguments().getCharSequence(Intent.EXTRA_TEXT)) .setPositiveButton(android.R.string.ok, new OnClickListener() { @Override @@ -218,7 +221,7 @@ public class CustomListPreference extends ListPreference { @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DIALOG_CUSTOM_LIST_CONFIRMATION; + return SettingsEnums.DIALOG_CUSTOM_LIST_CONFIRMATION; } } } diff --git a/src/com/android/settings/DateTimeSettings.java b/src/com/android/settings/DateTimeSettings.java index 2a4934c3f6..40b20052ab 100644 --- a/src/com/android/settings/DateTimeSettings.java +++ b/src/com/android/settings/DateTimeSettings.java @@ -18,12 +18,11 @@ package com.android.settings; import android.app.Activity; import android.app.Dialog; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; -import android.os.UserManager; import android.provider.SearchIndexableResource; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.dashboard.SummaryLoader; import com.android.settings.datetime.AutoTimeFormatPreferenceController; @@ -38,11 +37,13 @@ import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.datetime.ZoneGetter; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.Calendar; import java.util.List; +@SearchIndexable public class DateTimeSettings extends DashboardFragment implements TimePreferenceController.TimePreferenceHost, DatePreferenceController.DatePreferenceHost { @@ -53,7 +54,7 @@ public class DateTimeSettings extends DashboardFragment implements @Override public int getMetricsCategory() { - return MetricsEvent.DATE_TIME; + return SettingsEnums.DATE_TIME; } @Override @@ -69,7 +70,7 @@ public class DateTimeSettings extends DashboardFragment implements @Override public void onAttach(Context context) { super.onAttach(context); - getLifecycle().addObserver(new TimeChangeListenerMixin(context, this)); + getSettingsLifecycle().addObserver(new TimeChangeListenerMixin(context, this)); } @Override @@ -126,9 +127,9 @@ public class DateTimeSettings extends DashboardFragment implements public int getDialogMetricsCategory(int dialogId) { switch (dialogId) { case DatePreferenceController.DIALOG_DATEPICKER: - return MetricsEvent.DIALOG_DATE_PICKER; + return SettingsEnums.DIALOG_DATE_PICKER; case TimePreferenceController.DIALOG_TIMEPICKER: - return MetricsEvent.DIALOG_TIME_PICKER; + return SettingsEnums.DIALOG_TIME_PICKER; default: return 0; } @@ -184,10 +185,6 @@ public class DateTimeSettings extends DashboardFragment implements public List<SearchIndexableResource> getXmlResourcesToIndex( Context context, boolean enabled) { List<SearchIndexableResource> result = new ArrayList<>(); - // Remove data/time settings from search in demo mode - if (UserManager.isDeviceInDemoMode(context)) { - return result; - } SearchIndexableResource sir = new SearchIndexableResource(context); sir.xmlResId = R.xml.date_time_prefs; diff --git a/src/com/android/settings/DeviceAdminSettings.java b/src/com/android/settings/DeviceAdminSettings.java deleted file mode 100644 index bb53018dfd..0000000000 --- a/src/com/android/settings/DeviceAdminSettings.java +++ /dev/null @@ -1,461 +0,0 @@ -/* - * Copyright (C) 2010 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.app.AppGlobals; -import android.app.ListFragment; -import android.app.admin.DeviceAdminInfo; -import android.app.admin.DeviceAdminReceiver; -import android.app.admin.DevicePolicyManager; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.ActivityInfo; -import android.content.pm.IPackageManager; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.content.res.Resources; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.os.RemoteException; -import android.os.UserHandle; -import android.os.UserManager; -import android.util.Log; -import android.util.SparseArray; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; -import android.widget.ImageView; -import android.widget.ListView; -import android.widget.Switch; -import android.widget.TextView; - -import com.android.internal.logging.nano.MetricsProto; -import com.android.settings.overlay.FeatureFactory; -import com.android.settingslib.core.instrumentation.Instrumentable; -import com.android.settingslib.core.instrumentation.VisibilityLoggerMixin; - -import org.xmlpull.v1.XmlPullParserException; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; - -public class DeviceAdminSettings extends ListFragment implements Instrumentable { - static final String TAG = "DeviceAdminSettings"; - - private VisibilityLoggerMixin mVisibilityLoggerMixin; - private DevicePolicyManager mDPM; - private UserManager mUm; - - private static class DeviceAdminListItem implements Comparable<DeviceAdminListItem> { - public DeviceAdminInfo info; - - // These aren't updated so they keep a stable sort order if user activates / de-activates - // an admin. - public String name; - public boolean active; - - public int compareTo(DeviceAdminListItem other) { - // Sort active admins first, then by name. - if (this.active != other.active) { - return this.active ? -1 : 1; - } - return this.name.compareTo(other.name); - } - } - - /** - * Internal collection of device admin info objects for all profiles associated with the current - * user. - */ - private final ArrayList<DeviceAdminListItem> - mAdmins = new ArrayList<DeviceAdminListItem>(); - - private String mDeviceOwnerPkg; - private SparseArray<ComponentName> mProfileOwnerComponents = new SparseArray<ComponentName>(); - - private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - // Refresh the list, if state change has been received. It could be that checkboxes - // need to be updated - if (DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED.equals( - intent.getAction())) { - updateList(); - } - } - }; - - @Override - public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DEVICE_ADMIN_SETTINGS; - } - - @Override - public void onCreate(Bundle icicle) { - super.onCreate(icicle); - mVisibilityLoggerMixin = new VisibilityLoggerMixin(getMetricsCategory(), - FeatureFactory.getFactory(getContext()).getMetricsFeatureProvider()); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - mDPM = (DevicePolicyManager) getActivity().getSystemService(Context.DEVICE_POLICY_SERVICE); - mUm = (UserManager) getActivity().getSystemService(Context.USER_SERVICE); - return inflater.inflate(R.layout.device_admin_settings, container, false); - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - setHasOptionsMenu(true); - Utils.forceCustomPadding(getListView(), true /* additive padding */); - getActivity().setTitle(R.string.manage_device_admin); - } - - @Override - public void onResume() { - super.onResume(); - final Activity activity = getActivity(); - mVisibilityLoggerMixin.onResume(); - IntentFilter filter = new IntentFilter(); - filter.addAction(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED); - activity.registerReceiverAsUser( - mBroadcastReceiver, UserHandle.ALL, filter, null, null); - - final ComponentName deviceOwnerComponent = mDPM.getDeviceOwnerComponentOnAnyUser(); - mDeviceOwnerPkg = - deviceOwnerComponent != null ? deviceOwnerComponent.getPackageName() : null; - mProfileOwnerComponents.clear(); - final List<UserHandle> profiles = mUm.getUserProfiles(); - final int profilesSize = profiles.size(); - for (int i = 0; i < profilesSize; ++i) { - final int profileId = profiles.get(i).getIdentifier(); - mProfileOwnerComponents.put(profileId, mDPM.getProfileOwnerAsUser(profileId)); - } - updateList(); - } - - @Override - public void onPause() { - final Activity activity = getActivity(); - activity.unregisterReceiver(mBroadcastReceiver); - mVisibilityLoggerMixin.onPause(); - super.onPause(); - } - - /** - * Update the internal collection of available admins for all profiles associated with the - * current user. - */ - void updateList() { - mAdmins.clear(); - - final List<UserHandle> profiles = mUm.getUserProfiles(); - final int profilesSize = profiles.size(); - for (int i = 0; i < profilesSize; ++i) { - final int profileId = profiles.get(i).getIdentifier(); - updateAvailableAdminsForProfile(profileId); - } - Collections.sort(mAdmins); - - getListView().setAdapter(new PolicyListAdapter()); - } - - @Override - public void onListItemClick(ListView l, View v, int position, long id) { - Object o = l.getAdapter().getItem(position); - DeviceAdminInfo dpi = (DeviceAdminInfo) o; - final UserHandle user = new UserHandle(getUserId(dpi)); - final Activity activity = getActivity(); - Intent intent = new Intent(activity, DeviceAdminAdd.class); - intent.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, dpi.getComponent()); - activity.startActivityAsUser(intent, user); - } - - static class ViewHolder { - ImageView icon; - TextView name; - Switch checkbox; - TextView description; - } - - class PolicyListAdapter extends BaseAdapter { - final LayoutInflater mInflater; - - PolicyListAdapter() { - mInflater = (LayoutInflater) - getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE); - } - - @Override - public boolean hasStableIds() { - return false; - } - - @Override - public int getCount() { - return mAdmins.size(); - } - - /** - * The item for the given position in the list. - * - * @return DeviceAdminInfo object for actual device admins. - */ - @Override - public Object getItem(int position) { - return ((DeviceAdminListItem) (mAdmins.get(position))).info; - } - - @Override - public long getItemId(int position) { - return position; - } - - @Override - public boolean areAllItemsEnabled() { - return false; - } - - /** - * See {@link #getItemViewType} for the view types. - */ - @Override - public int getViewTypeCount() { - return 1; - } - - /** - * Returns 0 for all types. - */ - @Override - public int getItemViewType(int position) { - return 0; - } - - @Override - public boolean isEnabled(int position) { - Object o = getItem(position); - return isEnabled(o); - } - - private boolean isEnabled(Object o) { - DeviceAdminInfo info = (DeviceAdminInfo) o; - // Disable item if admin is being removed - if (isRemovingAdmin(info)) { - return false; - } - return true; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - Object o = getItem(position); - if (convertView == null) { - convertView = newDeviceAdminView(parent); - } - bindView(convertView, (DeviceAdminInfo) o); - return convertView; - } - - private View newDeviceAdminView(ViewGroup parent) { - View v = mInflater.inflate(R.layout.device_admin_item, parent, false); - ViewHolder h = new ViewHolder(); - h.icon = v.findViewById(R.id.icon); - h.name = v.findViewById(R.id.name); - h.checkbox = v.findViewById(R.id.checkbox); - h.description = v.findViewById(R.id.description); - v.setTag(h); - return v; - } - - private void bindView(View view, DeviceAdminInfo item) { - final Activity activity = getActivity(); - ViewHolder vh = (ViewHolder) view.getTag(); - Drawable activityIcon = item.loadIcon(activity.getPackageManager()); - Drawable badgedIcon = activity.getPackageManager().getUserBadgedIcon( - activityIcon, new UserHandle(getUserId(item))); - vh.icon.setImageDrawable(badgedIcon); - vh.name.setText(item.loadLabel(activity.getPackageManager())); - vh.checkbox.setChecked(isActiveAdmin(item)); - final boolean enabled = isEnabled(item); - try { - vh.description.setText(item.loadDescription(activity.getPackageManager())); - } catch (Resources.NotFoundException e) { - } - vh.checkbox.setEnabled(enabled); - vh.name.setEnabled(enabled); - vh.description.setEnabled(enabled); - vh.icon.setEnabled(enabled); - } - } - - private boolean isDeviceOwner(DeviceAdminInfo item) { - return getUserId(item) == UserHandle.myUserId() - && item.getPackageName().equals(mDeviceOwnerPkg); - } - - private boolean isProfileOwner(DeviceAdminInfo item) { - ComponentName profileOwner = mProfileOwnerComponents.get(getUserId(item)); - return item.getComponent().equals(profileOwner); - } - - private boolean isActiveAdmin(DeviceAdminInfo item) { - return mDPM.isAdminActiveAsUser(item.getComponent(), getUserId(item)); - } - - private boolean isRemovingAdmin(DeviceAdminInfo item) { - return mDPM.isRemovingAdmin(item.getComponent(), getUserId(item)); - } - - /** - * Add device admins to the internal collection that belong to a profile. - * - * @param profileId the profile identifier. - */ - private void updateAvailableAdminsForProfile(final int profileId) { - // We are adding the union of two sets 'A' and 'B' of device admins to mAvailableAdmins. - // Set 'A' is the set of active admins for the profile whereas set 'B' is the set of - // listeners to DeviceAdminReceiver.ACTION_DEVICE_ADMIN_ENABLED for the profile. - - // Add all of set 'A' to mAvailableAdmins. - List<ComponentName> activeAdminsListForProfile = mDPM.getActiveAdminsAsUser(profileId); - addActiveAdminsForProfile(activeAdminsListForProfile, profileId); - - // Collect set 'B' and add B-A to mAvailableAdmins. - addDeviceAdminBroadcastReceiversForProfile(activeAdminsListForProfile, profileId); - } - - /** - * Add a profile's device admins that are receivers of - * {@code DeviceAdminReceiver.ACTION_DEVICE_ADMIN_ENABLED} to the internal collection if they - * haven't been added yet. - * - * @param alreadyAddedComponents the set of active admin component names. Receivers of - * {@code DeviceAdminReceiver.ACTION_DEVICE_ADMIN_ENABLED} whose component is in this - * set are not added to the internal collection again. - * @param profileId the identifier of the profile - */ - private void addDeviceAdminBroadcastReceiversForProfile( - Collection<ComponentName> alreadyAddedComponents, final int profileId) { - final PackageManager pm = getActivity().getPackageManager(); - List<ResolveInfo> enabledForProfile = pm.queryBroadcastReceiversAsUser( - new Intent(DeviceAdminReceiver.ACTION_DEVICE_ADMIN_ENABLED), - PackageManager.GET_META_DATA | PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS, - profileId); - if (enabledForProfile == null) { - enabledForProfile = Collections.emptyList(); - } - final int n = enabledForProfile.size(); - for (int i = 0; i < n; ++i) { - ResolveInfo resolveInfo = enabledForProfile.get(i); - ComponentName riComponentName = - new ComponentName(resolveInfo.activityInfo.packageName, - resolveInfo.activityInfo.name); - if (alreadyAddedComponents == null - || !alreadyAddedComponents.contains(riComponentName)) { - DeviceAdminInfo deviceAdminInfo = createDeviceAdminInfo(resolveInfo.activityInfo); - // add only visible ones (note: active admins are added regardless of visibility) - if (deviceAdminInfo != null && deviceAdminInfo.isVisible()) { - if (!deviceAdminInfo.getActivityInfo().applicationInfo.isInternal()) { - continue; - } - DeviceAdminListItem item = new DeviceAdminListItem(); - item.info = deviceAdminInfo; - item.name = deviceAdminInfo.loadLabel(pm).toString(); - // Active ones already added. - item.active = false; - mAdmins.add(item); - } - } - } - } - - /** - * Add a {@link DeviceAdminInfo} object to the internal collection of available admins for all - * active admin components associated with a profile. - * - * @param profileId a profile identifier. - */ - private void addActiveAdminsForProfile(final List<ComponentName> activeAdmins, - final int profileId) { - if (activeAdmins != null) { - final PackageManager packageManager = getActivity().getPackageManager(); - final IPackageManager iPackageManager = AppGlobals.getPackageManager(); - final int n = activeAdmins.size(); - for (int i = 0; i < n; ++i) { - final ComponentName activeAdmin = activeAdmins.get(i); - final ActivityInfo ai; - try { - ai = iPackageManager.getReceiverInfo(activeAdmin, - PackageManager.GET_META_DATA | - PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS | - PackageManager.MATCH_DIRECT_BOOT_UNAWARE | - PackageManager.MATCH_DIRECT_BOOT_AWARE, profileId); - } catch (RemoteException e) { - Log.w(TAG, "Unable to load component: " + activeAdmin); - continue; - } - final DeviceAdminInfo deviceAdminInfo = createDeviceAdminInfo(ai); - if (deviceAdminInfo == null) { - continue; - } - // Don't do the applicationInfo.isInternal() check here; if an active - // admin is already on SD card, just show it. - final DeviceAdminListItem item = new DeviceAdminListItem(); - item.info = deviceAdminInfo; - item.name = deviceAdminInfo.loadLabel(packageManager).toString(); - item.active = true; - mAdmins.add(item); - } - } - } - - /** - * Creates a device admin info object for the resolved intent that points to the component of - * the device admin. - * - * @param ai ActivityInfo for the admin component. - * @return new {@link DeviceAdminInfo} object or null if there was an error. - */ - private DeviceAdminInfo createDeviceAdminInfo(ActivityInfo ai) { - try { - return new DeviceAdminInfo(getActivity(), ai); - } catch (XmlPullParserException|IOException e) { - Log.w(TAG, "Skipping " + ai, e); - } - return null; - } - - /** - * Extracts the user id from a device admin info object. - * @param adminInfo the device administrator info. - * @return identifier of the user associated with the device admin. - */ - private int getUserId(DeviceAdminInfo adminInfo) { - return UserHandle.getUserId(adminInfo.getActivityInfo().applicationInfo.uid); - } -} diff --git a/src/com/android/settings/DisplaySettings.java b/src/com/android/settings/DisplaySettings.java index fef56ad582..eb77d4a349 100644 --- a/src/com/android/settings/DisplaySettings.java +++ b/src/com/android/settings/DisplaySettings.java @@ -16,16 +16,15 @@ package com.android.settings; +import android.app.settings.SettingsEnums; import android.content.Context; +import android.os.Bundle; import android.provider.SearchIndexableResource; -import com.android.internal.hardware.AmbientDisplayConfiguration; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.dashboard.DashboardFragment; -import com.android.settings.display.AmbientDisplayPreferenceController; import com.android.settings.display.BrightnessLevelPreferenceController; import com.android.settings.display.CameraGesturePreferenceController; -import com.android.settings.display.ColorModePreferenceController; +import com.android.settings.display.DarkUIPreferenceController; import com.android.settings.display.LiftToWakePreferenceController; import com.android.settings.display.NightDisplayPreferenceController; import com.android.settings.display.NightModePreferenceController; @@ -35,28 +34,24 @@ import com.android.settings.display.TapToWakePreferenceController; import com.android.settings.display.ThemePreferenceController; import com.android.settings.display.TimeoutPreferenceController; import com.android.settings.display.VrDisplayPreferenceController; -import com.android.settings.display.WallpaperPreferenceController; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.List; +@SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC) public class DisplaySettings extends DashboardFragment { private static final String TAG = "DisplaySettings"; - public static final String KEY_DISPLAY_SIZE = "display_settings_screen_zoom"; - private static final String KEY_SCREEN_TIMEOUT = "screen_timeout"; - private static final String KEY_AMBIENT_DISPLAY = "ambient_display"; - private static final String KEY_AUTO_BRIGHTNESS = "auto_brightness_entry"; - private static final String KEY_NIGHT_DISPLAY = "night_display"; @Override public int getMetricsCategory() { - return MetricsEvent.DISPLAY; + return SettingsEnums.DISPLAY; } @Override @@ -70,8 +65,14 @@ public class DisplaySettings extends DashboardFragment { } @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + use(DarkUIPreferenceController.class).setParentFragment(this); + } + + @Override protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { - return buildPreferenceControllers(context, getLifecycle()); + return buildPreferenceControllers(context, getSettingsLifecycle()); } @Override @@ -87,18 +88,12 @@ public class DisplaySettings extends DashboardFragment { controllers.add(new NightDisplayPreferenceController(context)); controllers.add(new NightModePreferenceController(context)); controllers.add(new ScreenSaverPreferenceController(context)); - controllers.add(new AmbientDisplayPreferenceController( - context, - new AmbientDisplayConfiguration(context), - KEY_AMBIENT_DISPLAY)); controllers.add(new TapToWakePreferenceController(context)); controllers.add(new TimeoutPreferenceController(context, KEY_SCREEN_TIMEOUT)); controllers.add(new VrDisplayPreferenceController(context)); controllers.add(new ShowOperatorNamePreferenceController(context)); - controllers.add(new WallpaperPreferenceController(context)); controllers.add(new ThemePreferenceController(context)); controllers.add(new BrightnessLevelPreferenceController(context, lifecycle)); - controllers.add(new ColorModePreferenceController(context)); return controllers; } @@ -116,16 +111,6 @@ public class DisplaySettings extends DashboardFragment { } @Override - public List<String> getNonIndexableKeys(Context context) { - List<String> keys = super.getNonIndexableKeys(context); - keys.add(KEY_DISPLAY_SIZE); - keys.add(WallpaperPreferenceController.KEY_WALLPAPER); - keys.add(KEY_NIGHT_DISPLAY); - keys.add(KEY_AUTO_BRIGHTNESS); - return keys; - } - - @Override public List<AbstractPreferenceController> createPreferenceControllers( Context context) { return buildPreferenceControllers(context, null); diff --git a/src/com/android/settings/EditPinPreference.java b/src/com/android/settings/EditPinPreference.java index 4efed4a30d..611f520b4a 100644 --- a/src/com/android/settings/EditPinPreference.java +++ b/src/com/android/settings/EditPinPreference.java @@ -23,19 +23,19 @@ import android.util.AttributeSet; import android.view.View; import android.widget.EditText; -import com.android.settingslib.CustomEditTextPreference; +import com.android.settingslib.CustomEditTextPreferenceCompat; /** * TODO: Add a soft dialpad for PIN entry. */ -class EditPinPreference extends CustomEditTextPreference { +class EditPinPreference extends CustomEditTextPreferenceCompat { interface OnPinEnteredListener { void onPinEntered(EditPinPreference preference, boolean positiveResult); } - + private OnPinEnteredListener mPinListener; - + public EditPinPreference(Context context, AttributeSet attrs) { super(context, attrs); } @@ -43,7 +43,7 @@ class EditPinPreference extends CustomEditTextPreference { public EditPinPreference(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } - + public void setOnPinEnteredListener(OnPinEnteredListener listener) { mPinListener = listener; } diff --git a/src/com/android/settings/EncryptionInterstitial.java b/src/com/android/settings/EncryptionInterstitial.java index f0115b8ef3..c132c89dc3 100644 --- a/src/com/android/settings/EncryptionInterstitial.java +++ b/src/com/android/settings/EncryptionInterstitial.java @@ -18,9 +18,9 @@ package com.android.settings; import android.accessibilityservice.AccessibilityServiceInfo; import android.app.Activity; -import android.app.AlertDialog; import android.app.Dialog; import android.app.admin.DevicePolicyManager; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -34,11 +34,15 @@ import android.view.accessibility.AccessibilityManager; import android.widget.LinearLayout; import android.widget.TextView; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.appcompat.app.AlertDialog; + import com.android.settings.core.InstrumentedFragment; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; import com.android.settings.password.ChooseLockSettingsHelper; -import com.android.setupwizardlib.GlifLayout; + +import com.google.android.setupcompat.template.FooterBarMixin; +import com.google.android.setupcompat.template.FooterButton; +import com.google.android.setupdesign.GlifLayout; import java.util.List; @@ -80,22 +84,18 @@ public class EncryptionInterstitial extends SettingsActivity { @Override protected void onCreate(Bundle savedInstance) { super.onCreate(savedInstance); - LinearLayout layout = (LinearLayout) findViewById(R.id.content_parent); - layout.setFitsSystemWindows(false); + findViewById(R.id.content_parent).setFitsSystemWindows(false); } - public static class EncryptionInterstitialFragment extends InstrumentedFragment - implements View.OnClickListener { + public static class EncryptionInterstitialFragment extends InstrumentedFragment { - private View mRequirePasswordToDecrypt; - private View mDontRequirePasswordToDecrypt; private boolean mPasswordRequired; private Intent mUnlockMethodIntent; private int mRequestedPasswordQuality; @Override public int getMetricsCategory() { - return MetricsEvent.ENCRYPTION; + return SettingsEnums.ENCRYPTION; } @Override @@ -108,10 +108,10 @@ public class EncryptionInterstitial extends SettingsActivity { public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - mRequirePasswordToDecrypt = view.findViewById(R.id.encrypt_require_password); - mDontRequirePasswordToDecrypt = view.findViewById(R.id.encrypt_dont_require_password); - boolean forFingerprint = getActivity().getIntent().getBooleanExtra( + final boolean forFingerprint = getActivity().getIntent().getBooleanExtra( ChooseLockSettingsHelper.EXTRA_KEY_FOR_FINGERPRINT, false); + final boolean forFace = getActivity().getIntent() + .getBooleanExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FACE, false); Intent intent = getActivity().getIntent(); mRequestedPasswordQuality = intent.getIntExtra(EXTRA_PASSWORD_QUALITY, 0); mUnlockMethodIntent = intent.getParcelableExtra(EXTRA_UNLOCK_METHOD_INTENT); @@ -120,31 +120,53 @@ public class EncryptionInterstitial extends SettingsActivity { case DevicePolicyManager.PASSWORD_QUALITY_SOMETHING: msgId = forFingerprint ? R.string.encryption_interstitial_message_pattern_for_fingerprint : + forFace ? + R.string.encryption_interstitial_message_pattern_for_face : R.string.encryption_interstitial_message_pattern; break; case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC: case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX: msgId = forFingerprint ? R.string.encryption_interstitial_message_pin_for_fingerprint : + forFace ? + R.string.encryption_interstitial_message_pin_for_face : R.string.encryption_interstitial_message_pin; break; default: msgId = forFingerprint ? R.string.encryption_interstitial_message_password_for_fingerprint : + forFace ? + R.string.encryption_interstitial_message_password_for_face : R.string.encryption_interstitial_message_password; break; } - TextView message = (TextView) getActivity().findViewById(R.id.encryption_message); + TextView message = (TextView) getActivity().findViewById(R.id.sud_layout_description); message.setText(msgId); - mRequirePasswordToDecrypt.setOnClickListener(this); - mDontRequirePasswordToDecrypt.setOnClickListener(this); - setRequirePasswordState(getActivity().getIntent().getBooleanExtra( EXTRA_REQUIRE_PASSWORD, true)); GlifLayout layout = (GlifLayout) view; layout.setHeaderText(getActivity().getTitle()); + + final FooterBarMixin mixin = layout.getMixin(FooterBarMixin.class); + mixin.setSecondaryButton( + new FooterButton.Builder(getContext()) + .setText(R.string.encryption_interstitial_no) + .setListener(this::onNoButtonClicked) + .setButtonType(FooterButton.ButtonType.SKIP) + .setTheme(R.style.SudGlifButton_Secondary) + .build() + ); + + mixin.setPrimaryButton( + new FooterButton.Builder(getContext()) + .setText(R.string.encryption_interstitial_yes) + .setListener(this::onYesButtonClicked) + .setButtonType(FooterButton.ButtonType.NEXT) + .setTheme(R.style.SudGlifButton_Primary) + .build() + ); } protected void startLockIntent() { @@ -166,26 +188,25 @@ public class EncryptionInterstitial extends SettingsActivity { } } - @Override - public void onClick(View view) { - if (view == mRequirePasswordToDecrypt) { - final boolean accEn = AccessibilityManager.getInstance(getActivity()).isEnabled(); - if (accEn && !mPasswordRequired) { - setRequirePasswordState(false); // clear the UI state - AccessibilityWarningDialogFragment.newInstance(mRequestedPasswordQuality) - .show( - getChildFragmentManager(), - AccessibilityWarningDialogFragment.TAG); - } else { - setRequirePasswordState(true); - startLockIntent(); - } + private void onYesButtonClicked(View view) { + final boolean accEn = AccessibilityManager.getInstance(getActivity()).isEnabled(); + if (accEn && !mPasswordRequired) { + setRequirePasswordState(false); // clear the UI state + AccessibilityWarningDialogFragment.newInstance(mRequestedPasswordQuality) + .show( + getChildFragmentManager(), + AccessibilityWarningDialogFragment.TAG); } else { - setRequirePasswordState(false); + setRequirePasswordState(true); startLockIntent(); } } + private void onNoButtonClicked(View view) { + setRequirePasswordState(false); + startLockIntent(); + } + private void setRequirePasswordState(boolean required) { mPasswordRequired = required; } @@ -259,7 +280,7 @@ public class EncryptionInterstitial extends SettingsActivity { @Override public int getMetricsCategory() { - return MetricsEvent.DIALOG_ENCRYPTION_INTERSTITIAL_ACCESSIBILITY; + return SettingsEnums.DIALOG_ENCRYPTION_INTERSTITIAL_ACCESSIBILITY; } @Override diff --git a/src/com/android/settings/FallbackHome.java b/src/com/android/settings/FallbackHome.java index 5f7b639236..e3944a65c6 100644 --- a/src/com/android/settings/FallbackHome.java +++ b/src/com/android/settings/FallbackHome.java @@ -17,24 +17,25 @@ package com.android.settings; import android.app.Activity; -import android.app.ProgressDialog; +import android.app.WallpaperColors; +import android.app.WallpaperManager; +import android.app.WallpaperManager.OnColorsChangedListener; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.Message; -import android.os.UserHandle; import android.os.PowerManager; import android.os.SystemClock; +import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; import android.util.Log; import android.view.View; -import android.view.WindowManager; import android.view.WindowManager.LayoutParams; import android.view.animation.AnimationUtils; @@ -45,6 +46,7 @@ public class FallbackHome extends Activity { private static final int PROGRESS_TIMEOUT = 2000; private boolean mProvisioned; + private WallpaperManager mWallManager; private final Runnable mProgressTimeoutRunnable = () -> { View v = getLayoutInflater().inflate( @@ -60,6 +62,18 @@ public class FallbackHome extends Activity { getWindow().addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON); }; + private final OnColorsChangedListener mColorsChangedListener = new OnColorsChangedListener() { + @Override + public void onColorsChanged(WallpaperColors colors, int which) { + if (colors != null) { + final View decorView = getWindow().getDecorView(); + decorView.setSystemUiVisibility( + updateVisibilityFlagsFromColors(colors, decorView.getSystemUiVisibility())); + mWallManager.removeOnColorsChangedListener(this); + } + } + }; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -68,14 +82,23 @@ public class FallbackHome extends Activity { // we don't flash the wallpaper before SUW mProvisioned = Settings.Global.getInt(getContentResolver(), Settings.Global.DEVICE_PROVISIONED, 0) != 0; + final int flags; if (!mProvisioned) { setTheme(R.style.FallbackHome_SetupWizard); - getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN - | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + flags = View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + } else { + flags = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; + } + + mWallManager = getSystemService(WallpaperManager.class); + if (mWallManager == null) { + Log.w(TAG, "Wallpaper manager isn't ready, can't listen to color changes!"); } else { - getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); + loadWallpaperColors(flags); } + getWindow().getDecorView().setSystemUiVisibility(flags); registerReceiver(mReceiver, new IntentFilter(Intent.ACTION_USER_UNLOCKED)); maybeFinish(); @@ -98,6 +121,9 @@ public class FallbackHome extends Activity { protected void onDestroy() { super.onDestroy(); unregisterReceiver(mReceiver); + if (mWallManager != null) { + mWallManager.removeOnColorsChangedListener(mColorsChangedListener); + } } private BroadcastReceiver mReceiver = new BroadcastReceiver() { @@ -107,6 +133,33 @@ public class FallbackHome extends Activity { } }; + private void loadWallpaperColors(int flags) { + final AsyncTask loadWallpaperColorsTask = new AsyncTask<Object, Void, Integer>() { + @Override + protected Integer doInBackground(Object... params) { + final WallpaperColors colors = + mWallManager.getWallpaperColors(WallpaperManager.FLAG_SYSTEM); + + // Use a listener to wait for colors if not ready yet. + if (colors == null) { + mWallManager.addOnColorsChangedListener(mColorsChangedListener, + null /* handler */); + return null; + } + return updateVisibilityFlagsFromColors(colors, flags); + } + + @Override + protected void onPostExecute(Integer flagsToUpdate) { + if (flagsToUpdate == null) { + return; + } + getWindow().getDecorView().setSystemUiVisibility(flagsToUpdate); + } + }; + loadWallpaperColorsTask.execute(); + } + private void maybeFinish() { if (getSystemService(UserManager.class).isUserUnlocked()) { final Intent homeIntent = new Intent(Intent.ACTION_MAIN) @@ -130,6 +183,17 @@ public class FallbackHome extends Activity { } } + // Set the system ui flags to light status bar if the wallpaper supports dark text to match + // current system ui color tints. + private int updateVisibilityFlagsFromColors(WallpaperColors colors, int flags) { + if ((colors.getColorHints() & WallpaperColors.HINT_SUPPORTS_DARK_TEXT) != 0) { + return flags | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; + } + return flags & ~(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) + & ~(View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR); + } + private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { diff --git a/src/com/android/settings/IccLockSettings.java b/src/com/android/settings/IccLockSettings.java index cb196d0a8c..605f483c72 100644 --- a/src/com/android/settings/IccLockSettings.java +++ b/src/com/android/settings/IccLockSettings.java @@ -16,6 +16,7 @@ package com.android.settings; +import android.app.settings.SettingsEnums; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -27,8 +28,6 @@ import android.os.AsyncResult; import android.os.Bundle; import android.os.Handler; import android.os.Message; -import androidx.preference.SwitchPreference; -import androidx.preference.Preference; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; @@ -48,7 +47,10 @@ import android.widget.TabHost.TabSpec; import android.widget.TabWidget; import android.widget.TextView; import android.widget.Toast; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; + +import androidx.preference.Preference; +import androidx.preference.SwitchPreference; + import com.android.internal.telephony.CommandException; import com.android.internal.telephony.Phone; import com.android.internal.telephony.PhoneFactory; @@ -66,7 +68,7 @@ import com.android.internal.telephony.TelephonyIntents; public class IccLockSettings extends SettingsPreferenceFragment implements EditPinPreference.OnPinEnteredListener { private static final String TAG = "IccLockSettings"; - private static final boolean DBG = true; + private static final boolean DBG = false; private static final int OFF_MODE = 0; // State when enabling/disabling ICC lock @@ -277,7 +279,7 @@ public class IccLockSettings extends SettingsPreferenceFragment @Override public int getMetricsCategory() { - return MetricsEvent.ICC_LOCK; + return SettingsEnums.ICC_LOCK; } @Override diff --git a/src/com/android/settings/LegalSettings.java b/src/com/android/settings/LegalSettings.java index 2bdfa3b23f..e6f0c2c073 100644 --- a/src/com/android/settings/LegalSettings.java +++ b/src/com/android/settings/LegalSettings.java @@ -16,113 +16,47 @@ package com.android.settings; -import android.app.Activity; +import android.app.settings.SettingsEnums; import android.content.Context; -import android.content.Intent; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.os.Bundle; import android.provider.SearchIndexableResource; -import androidx.annotation.VisibleForTesting; -import androidx.preference.PreferenceGroup; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.settings.R; +import com.android.settings.dashboard.DashboardFragment; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; +import com.android.settingslib.search.SearchIndexable; import java.util.Arrays; import java.util.List; -public class LegalSettings extends SettingsPreferenceFragment implements Indexable { +@SearchIndexable +public class LegalSettings extends DashboardFragment { - private static final String KEY_TERMS = "terms"; - private static final String KEY_LICENSE = "license"; - private static final String KEY_COPYRIGHT = "copyright"; - private static final String KEY_WEBVIEW_LICENSE = "webview_license"; - @VisibleForTesting static final String KEY_WALLPAPER_ATTRIBUTIONS = "wallpaper_attributions"; - - public void onCreate(Bundle icicle) { - super.onCreate(icicle); - addPreferencesFromResource(R.xml.about_legal); - - final Activity act = getActivity(); - // These are contained in the "container" preference group - PreferenceGroup parentPreference = getPreferenceScreen(); - Utils.updatePreferenceToSpecificActivityOrRemove(act, parentPreference, KEY_TERMS, - Utils.UPDATE_PREFERENCE_FLAG_SET_TITLE_TO_MATCHING_ACTIVITY); - Utils.updatePreferenceToSpecificActivityOrRemove(act, parentPreference, KEY_LICENSE, - Utils.UPDATE_PREFERENCE_FLAG_SET_TITLE_TO_MATCHING_ACTIVITY); - Utils.updatePreferenceToSpecificActivityOrRemove(act, parentPreference, KEY_COPYRIGHT, - Utils.UPDATE_PREFERENCE_FLAG_SET_TITLE_TO_MATCHING_ACTIVITY); - Utils.updatePreferenceToSpecificActivityOrRemove(act, parentPreference, KEY_WEBVIEW_LICENSE, - Utils.UPDATE_PREFERENCE_FLAG_SET_TITLE_TO_MATCHING_ACTIVITY); - - checkWallpaperAttributionAvailability(act); - } + private static final String TAG = "LegalSettings"; @Override public int getMetricsCategory() { - return MetricsEvent.ABOUT_LEGAL_SETTINGS; + return SettingsEnums.ABOUT_LEGAL_SETTINGS; } - @VisibleForTesting - void checkWallpaperAttributionAvailability(Context context) { - if (!context.getResources().getBoolean( - R.bool.config_show_wallpaper_attribution)) { - removePreference(KEY_WALLPAPER_ATTRIBUTIONS); - } + @Override + protected String getLogTag() { + return TAG; } - public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = - new BaseSearchIndexProvider() { - - @Override - public List<SearchIndexableResource> getXmlResourcesToIndex( - Context context, boolean enabled) { - final SearchIndexableResource sir = new SearchIndexableResource(context); - sir.xmlResId = R.xml.about_legal; - return Arrays.asList(sir); - } - - @Override - public List<String> getNonIndexableKeys(Context context) { - final List<String> keys = super.getNonIndexableKeys(context); - if (!checkIntentAction(context, "android.settings.TERMS")) { - keys.add(KEY_TERMS); - } - if (!checkIntentAction(context, "android.settings.LICENSE")) { - keys.add(KEY_LICENSE); - } - if (!checkIntentAction(context, "android.settings.COPYRIGHT")) { - keys.add(KEY_COPYRIGHT); - } - if (!checkIntentAction(context, "android.settings.WEBVIEW_LICENSE")) { - keys.add(KEY_WEBVIEW_LICENSE); - } - keys.add(KEY_WALLPAPER_ATTRIBUTIONS); - return keys; - } - - private boolean checkIntentAction(Context context, String action) { - final Intent intent = new Intent(action); + @Override + protected int getPreferenceScreenResId() { + return R.xml.about_legal; + } - // Find the activity that is in the system image - final PackageManager pm = context.getPackageManager(); - final List<ResolveInfo> list = pm.queryIntentActivities(intent, 0); - final int listSize = list.size(); + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { - for (int i = 0; i < listSize; i++) { - ResolveInfo resolveInfo = list.get(i); - if ((resolveInfo.activityInfo.applicationInfo.flags & - ApplicationInfo.FLAG_SYSTEM) != 0) { - return true; - } + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex( + Context context, boolean enabled) { + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.about_legal; + return Arrays.asList(sir); } - - return false; - } - }; - + }; } diff --git a/src/com/android/settings/ManualDisplayActivity.java b/src/com/android/settings/ManualDisplayActivity.java index 8effc7b9ff..a669c67811 100644 --- a/src/com/android/settings/ManualDisplayActivity.java +++ b/src/com/android/settings/ManualDisplayActivity.java @@ -22,7 +22,6 @@ import android.content.Intent; import android.content.res.Resources; import android.net.Uri; import android.os.Bundle; -import android.text.TextUtils; import android.util.Log; import android.widget.Toast; diff --git a/src/com/android/settings/MasterClear.java b/src/com/android/settings/MasterClear.java index 182081c633..0df39842f9 100644 --- a/src/com/android/settings/MasterClear.java +++ b/src/com/android/settings/MasterClear.java @@ -21,8 +21,9 @@ import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; import android.accounts.Account; import android.accounts.AccountManager; import android.accounts.AuthenticatorDescription; -import android.annotation.Nullable; +import android.app.ActionBar; import android.app.Activity; +import android.app.settings.SettingsEnums; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; @@ -31,6 +32,7 @@ import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.UserInfo; import android.content.res.Resources; +import android.graphics.Color; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Environment; @@ -38,7 +40,6 @@ import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; -import androidx.annotation.VisibleForTesting; import android.sysprop.VoldProperties; import android.telephony.euicc.EuiccManager; import android.text.TextUtils; @@ -55,13 +56,19 @@ import android.widget.LinearLayout; import android.widget.ScrollView; import android.widget.TextView; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.annotation.VisibleForTesting; + import com.android.settings.core.InstrumentedFragment; import com.android.settings.core.SubSettingLauncher; import com.android.settings.enterprise.ActionDisabledByAdminDialogHelper; import com.android.settings.password.ChooseLockSettingsHelper; import com.android.settings.password.ConfirmLockPattern; -import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; + +import com.google.android.setupcompat.template.FooterBarMixin; +import com.google.android.setupcompat.template.FooterButton; +import com.google.android.setupcompat.template.FooterButton.ButtonType; +import com.google.android.setupdesign.GlifLayout; import java.util.List; @@ -78,8 +85,10 @@ import java.util.List; public class MasterClear extends InstrumentedFragment implements OnGlobalLayoutListener { private static final String TAG = "MasterClear"; - @VisibleForTesting static final int KEYGUARD_REQUEST = 55; - @VisibleForTesting static final int CREDENTIAL_CONFIRM_REQUEST = 56; + @VisibleForTesting + static final int KEYGUARD_REQUEST = 55; + @VisibleForTesting + static final int CREDENTIAL_CONFIRM_REQUEST = 56; private static final String KEY_SHOW_ESIM_RESET_CHECKBOX = "masterclear.allow_retain_esim_profiles_after_fdr"; @@ -88,27 +97,42 @@ public class MasterClear extends InstrumentedFragment implements OnGlobalLayoutL static final String ERASE_ESIMS_EXTRA = "erase_esim"; private View mContentView; - @VisibleForTesting Button mInitiateButton; + @VisibleForTesting + FooterButton mInitiateButton; private View mExternalStorageContainer; - @VisibleForTesting CheckBox mExternalStorage; - private View mEsimStorageContainer; - @VisibleForTesting CheckBox mEsimStorage; - @VisibleForTesting ScrollView mScrollView; + @VisibleForTesting + CheckBox mExternalStorage; + @VisibleForTesting + View mEsimStorageContainer; + @VisibleForTesting + CheckBox mEsimStorage; + @VisibleForTesting + ScrollView mScrollView; @Override public void onGlobalLayout() { mInitiateButton.setEnabled(hasReachedBottom(mScrollView)); } - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - getActivity().setTitle(R.string.master_clear_short_title); + private void setUpActionBarAndTitle() { + final Activity activity = getActivity(); + if (activity == null) { + Log.e(TAG, "No activity attached, skipping setUpActionBarAndTitle"); + return; + } + final ActionBar actionBar = activity.getActionBar(); + if (actionBar == null) { + Log.e(TAG, "No actionbar, skipping setUpActionBarAndTitle"); + return; + } + actionBar.hide(); + activity.getWindow().setStatusBarColor(Color.TRANSPARENT); } /** * Keyguard validation is run using the standard {@link ConfirmLockPattern} * component as a subactivity + * * @param request the request code to be returned once confirmation finishes * @return true if confirmation launched */ @@ -162,7 +186,7 @@ public class MasterClear extends InstrumentedFragment implements OnGlobalLayoutL new SubSettingLauncher(getContext()) .setDestination(MasterClearConfirm.class.getName()) .setArguments(args) - .setTitle(R.string.master_clear_confirm_title) + .setTitleRes(R.string.master_clear_confirm_title) .setSourceMetricsCategory(getMetricsCategory()) .launch(); } @@ -188,8 +212,8 @@ public class MasterClear extends InstrumentedFragment implements OnGlobalLayoutL Account[] accounts = am.getAccountsByType(accountType); if (accounts != null && accounts.length > 0) { final Intent requestAccountConfirmation = new Intent() - .setPackage(packageName) - .setComponent(new ComponentName(packageName, className)); + .setPackage(packageName) + .setComponent(new ComponentName(packageName, className)); // Check to make sure that the intent is supported. final PackageManager pm = context.getPackageManager(); final ResolveInfo resolution = pm.resolveActivity(requestAccountConfirmation, 0); @@ -258,8 +282,9 @@ public class MasterClear extends InstrumentedFragment implements OnGlobalLayoutL */ @VisibleForTesting void establishInitialState() { - mInitiateButton = mContentView.findViewById(R.id.initiate_master_clear); - mInitiateButton.setOnClickListener(mInitiateListener); + setUpActionBarAndTitle(); + setUpInitiateButton(); + mExternalStorageContainer = mContentView.findViewById(R.id.erase_external_container); mExternalStorage = mContentView.findViewById(R.id.erase_external); mEsimStorageContainer = mContentView.findViewById(R.id.erase_esim_container); @@ -302,8 +327,6 @@ public class MasterClear extends InstrumentedFragment implements OnGlobalLayoutL if (showWipeEuicc()) { if (showWipeEuiccCheckbox()) { - TextView title = mContentView.findViewById(R.id.erase_esim_title); - title.setText(R.string.erase_esim_storage); mEsimStorageContainer.setVisibility(View.VISIBLE); mEsimStorageContainer.setOnClickListener(new View.OnClickListener() { @Override @@ -333,7 +356,7 @@ public class MasterClear extends InstrumentedFragment implements OnGlobalLayoutL mScrollView.setOnScrollChangeListener(new OnScrollChangeListener() { @Override public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, - int oldScrollY) { + int oldScrollY) { if (v instanceof ScrollView && hasReachedBottom((ScrollView) v)) { mInitiateButton.setEnabled(true); mScrollView.setOnScrollChangeListener(null); @@ -359,8 +382,8 @@ public class MasterClear extends InstrumentedFragment implements OnGlobalLayoutL } ContentResolver cr = context.getContentResolver(); return Settings.Global.getInt(cr, Settings.Global.EUICC_PROVISIONED, 0) != 0 - || Settings.Global.getInt( - cr, Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0; + || Settings.Global.getInt( + cr, Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0; } @VisibleForTesting @@ -387,21 +410,39 @@ public class MasterClear extends InstrumentedFragment implements OnGlobalLayoutL return diff <= 0; } + private void setUpInitiateButton() { + if (mInitiateButton != null) { + return; + } + + final GlifLayout layout = mContentView.findViewById(R.id.setup_wizard_layout); + final FooterBarMixin mixin = layout.getMixin(FooterBarMixin.class); + mixin.setPrimaryButton( + new FooterButton.Builder(getActivity()) + .setText(R.string.master_clear_button_text) + .setListener(mInitiateListener) + .setButtonType(ButtonType.OTHER) + .setTheme(R.style.SudGlifButton_Primary) + .build() + ); + mInitiateButton = mixin.getPrimaryButton(); + } + private void getContentDescription(View v, StringBuffer description) { - if (v.getVisibility() != View.VISIBLE) { - return; - } - if (v instanceof ViewGroup) { - ViewGroup vGroup = (ViewGroup) v; - for (int i = 0; i < vGroup.getChildCount(); i++) { - View nextChild = vGroup.getChildAt(i); - getContentDescription(nextChild, description); - } - } else if (v instanceof TextView) { - TextView vText = (TextView) v; - description.append(vText.getText()); - description.append(","); // Allow Talkback to pause between sections. - } + if (v.getVisibility() != View.VISIBLE) { + return; + } + if (v instanceof ViewGroup) { + ViewGroup vGroup = (ViewGroup) v; + for (int i = 0; i < vGroup.getChildCount(); i++) { + View nextChild = vGroup.getChildAt(i); + getContentDescription(nextChild, description); + } + } else if (v instanceof TextView) { + TextView vText = (TextView) v; + description.append(vText.getText()); + description.append(","); // Allow Talkback to pause between sections. + } } private boolean isExtStorageEncrypted() { @@ -411,7 +452,7 @@ public class MasterClear extends InstrumentedFragment implements OnGlobalLayoutL private void loadAccountList(final UserManager um) { View accountsLabel = mContentView.findViewById(R.id.accounts_label); - LinearLayout contents = (LinearLayout)mContentView.findViewById(R.id.accounts); + LinearLayout contents = (LinearLayout) mContentView.findViewById(R.id.accounts); contents.removeAllViews(); Context context = getActivity(); @@ -420,7 +461,7 @@ public class MasterClear extends InstrumentedFragment implements OnGlobalLayoutL AccountManager mgr = AccountManager.get(context); - LayoutInflater inflater = (LayoutInflater)context.getSystemService( + LayoutInflater inflater = (LayoutInflater) context.getSystemService( Context.LAYOUT_INFLATER_SERVICE); int accountsCount = 0; @@ -499,11 +540,12 @@ public class MasterClear extends InstrumentedFragment implements OnGlobalLayoutL public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final Context context = getContext(); - final EnforcedAdmin admin = RestrictedLockUtils.checkIfRestrictionEnforced(context, + final EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(context, UserManager.DISALLOW_FACTORY_RESET, UserHandle.myUserId()); final UserManager um = UserManager.get(context); - final boolean disallow = !um.isAdminUser() || RestrictedLockUtils.hasBaseUserRestriction( - context, UserManager.DISALLOW_FACTORY_RESET, UserHandle.myUserId()); + final boolean disallow = !um.isAdminUser() || RestrictedLockUtilsInternal + .hasBaseUserRestriction(context, UserManager.DISALLOW_FACTORY_RESET, + UserHandle.myUserId()); if (disallow && !Utils.isDemoUser(context)) { return inflater.inflate(R.layout.master_clear_disallowed_screen, null); } else if (admin != null) { @@ -522,6 +564,6 @@ public class MasterClear extends InstrumentedFragment implements OnGlobalLayoutL @Override public int getMetricsCategory() { - return MetricsEvent.MASTER_CLEAR; + return SettingsEnums.MASTER_CLEAR; } } diff --git a/src/com/android/settings/MasterClearConfirm.java b/src/com/android/settings/MasterClearConfirm.java index a92c8f85c7..a8c4341afd 100644 --- a/src/com/android/settings/MasterClearConfirm.java +++ b/src/com/android/settings/MasterClearConfirm.java @@ -16,28 +16,40 @@ package com.android.settings; + +import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; + +import android.app.ActionBar; +import android.app.Activity; import android.app.ProgressDialog; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; +import android.graphics.Color; import android.os.AsyncTask; import android.os.Bundle; import android.os.UserHandle; import android.os.UserManager; import android.service.oemlock.OemLockManager; import android.service.persistentdata.PersistentDataBlockManager; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.TextView; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.annotation.VisibleForTesting; + import com.android.settings.core.InstrumentedFragment; import com.android.settings.enterprise.ActionDisabledByAdminDialogHelper; -import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; -import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; +import com.google.android.setupcompat.template.FooterBarMixin; +import com.google.android.setupcompat.template.FooterButton; +import com.google.android.setupcompat.template.FooterButton.ButtonType; +import com.google.android.setupdesign.GlifLayout; /** * Confirm and execute a reset of the device to a clean "just out of the box" @@ -50,10 +62,11 @@ import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; * This is the confirmation screen. */ public class MasterClearConfirm extends InstrumentedFragment { + private final static String TAG = "MasterClearConfirm"; - private View mContentView; + @VisibleForTesting View mContentView; private boolean mEraseSdCard; - private boolean mEraseEsims; + @VisibleForTesting boolean mEraseEsims; /** * The user has gone through the multiple confirmation, so now we go ahead @@ -102,9 +115,11 @@ public class MasterClearConfirm extends InstrumentedFragment { mProgressDialog.show(); // need to prevent orientation changes as we're about to go into - // a long IO request, so we won't be able to access inflate resources on flash + // a long IO request, so we won't be able to access inflate resources on + // flash mOldOrientation = getActivity().getRequestedOrientation(); - getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LOCKED); + getActivity().setRequestedOrientation( + ActivityInfo.SCREEN_ORIENTATION_LOCKED); } }.execute(); } else { @@ -139,16 +154,40 @@ public class MasterClearConfirm extends InstrumentedFragment { * Configure the UI for the final confirmation interaction */ private void establishFinalConfirmationState() { - mContentView.findViewById(R.id.execute_master_clear) - .setOnClickListener(mFinalClickListener); + final GlifLayout layout = mContentView.findViewById(R.id.setup_wizard_layout); + + final FooterBarMixin mixin = layout.getMixin(FooterBarMixin.class); + mixin.setPrimaryButton( + new FooterButton.Builder(getActivity()) + .setText(R.string.master_clear_button_text) + .setListener(mFinalClickListener) + .setButtonType(ButtonType.OTHER) + .setTheme(R.style.SudGlifButton_Primary) + .build() + ); + } + + private void setUpActionBarAndTitle() { + final Activity activity = getActivity(); + if (activity == null) { + Log.e(TAG, "No activity attached, skipping setUpActionBarAndTitle"); + return; + } + final ActionBar actionBar = activity.getActionBar(); + if (actionBar == null) { + Log.e(TAG, "No actionbar, skipping setUpActionBarAndTitle"); + return; + } + actionBar.hide(); + activity.getWindow().setStatusBarColor(Color.TRANSPARENT); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - final EnforcedAdmin admin = RestrictedLockUtils.checkIfRestrictionEnforced( + final EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced( getActivity(), UserManager.DISALLOW_FACTORY_RESET, UserHandle.myUserId()); - if (RestrictedLockUtils.hasBaseUserRestriction(getActivity(), + if (RestrictedLockUtilsInternal.hasBaseUserRestriction(getActivity(), UserManager.DISALLOW_FACTORY_RESET, UserHandle.myUserId())) { return inflater.inflate(R.layout.master_clear_disallowed_screen, null); } else if (admin != null) { @@ -159,15 +198,16 @@ public class MasterClearConfirm extends InstrumentedFragment { return new View(getActivity()); } mContentView = inflater.inflate(R.layout.master_clear_confirm, null); + setUpActionBarAndTitle(); establishFinalConfirmationState(); setAccessibilityTitle(); + setSubtitle(); return mContentView; } private void setAccessibilityTitle() { CharSequence currentTitle = getActivity().getTitle(); - TextView confirmationMessage = - (TextView) mContentView.findViewById(R.id.master_clear_confirm); + TextView confirmationMessage = mContentView.findViewById(R.id.sud_layout_description); if (confirmationMessage != null) { String accessibleText = new StringBuilder(currentTitle).append(",").append( confirmationMessage.getText()).toString(); @@ -175,6 +215,14 @@ public class MasterClearConfirm extends InstrumentedFragment { } } + @VisibleForTesting + void setSubtitle() { + if (mEraseEsims) { + ((TextView) mContentView.findViewById(R.id.sud_layout_description)) + .setText(R.string.master_clear_final_desc_esim); + } + } + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -188,6 +236,6 @@ public class MasterClearConfirm extends InstrumentedFragment { @Override public int getMetricsCategory() { - return MetricsEvent.MASTER_CLEAR_CONFIRM; + return SettingsEnums.MASTER_CLEAR_CONFIRM; } } diff --git a/src/com/android/settings/MonitoringCertInfoActivity.java b/src/com/android/settings/MonitoringCertInfoActivity.java index aea2a37eb4..eadebc1f4f 100644 --- a/src/com/android/settings/MonitoringCertInfoActivity.java +++ b/src/com/android/settings/MonitoringCertInfoActivity.java @@ -17,7 +17,6 @@ package com.android.settings; import android.app.Activity; -import android.app.AlertDialog; import android.app.admin.DevicePolicyManager; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; @@ -27,6 +26,8 @@ import android.os.Bundle; import android.os.UserHandle; import android.provider.Settings; +import androidx.appcompat.app.AlertDialog; + import com.android.settingslib.RestrictedLockUtils; /** @@ -45,10 +46,17 @@ public class MonitoringCertInfoActivity extends Activity implements OnClickListe mUserId = getIntent().getIntExtra(Intent.EXTRA_USER_ID, UserHandle.myUserId()); + final UserHandle user; + if (mUserId == UserHandle.USER_NULL) { + user = null; + } else { + user = UserHandle.of(mUserId); + } + DevicePolicyManager dpm = getSystemService(DevicePolicyManager.class); final int numberOfCertificates = getIntent().getIntExtra( Settings.EXTRA_NUMBER_OF_CERTIFICATES, 1); - final int titleId = RestrictedLockUtils.getProfileOrDeviceOwner(this, mUserId) != null + final int titleId = RestrictedLockUtils.getProfileOrDeviceOwner(this, user) != null ? R.plurals.ssl_ca_cert_settings_button // Check certificate : R.plurals.ssl_ca_cert_dialog_title; // Trust or remove certificate final CharSequence title = getResources().getQuantityText(titleId, numberOfCertificates); diff --git a/src/com/android/settings/PrivacySettings.java b/src/com/android/settings/PrivacySettings.java deleted file mode 100644 index 0ae9cb135f..0000000000 --- a/src/com/android/settings/PrivacySettings.java +++ /dev/null @@ -1,248 +0,0 @@ -/* - * Copyright (C) 2009 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.backup.IBackupManager; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.os.Bundle; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.os.UserHandle; -import android.os.UserManager; -import android.provider.SearchIndexableResource; -import android.provider.Settings; -import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; -import androidx.preference.Preference; -import androidx.preference.Preference.OnPreferenceChangeListener; -import androidx.preference.PreferenceScreen; -import android.util.Log; - -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.settings.search.BaseSearchIndexProvider; -import com.android.settings.search.Indexable; -import com.android.settingslib.RestrictedLockUtils; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -/** - * Gesture lock pattern settings. - */ -public class PrivacySettings extends SettingsPreferenceFragment { - - // Vendor specific - private static final String GSETTINGS_PROVIDER = "com.google.settings"; - @VisibleForTesting - static final String BACKUP_DATA = "backup_data"; - @VisibleForTesting - static final String AUTO_RESTORE = "auto_restore"; - @VisibleForTesting - static final String CONFIGURE_ACCOUNT = "configure_account"; - @VisibleForTesting - static final String DATA_MANAGEMENT = "data_management"; - private static final String BACKUP_INACTIVE = "backup_inactive"; - private static final String TAG = "PrivacySettings"; - private IBackupManager mBackupManager; - private Preference mBackup; - private SwitchPreference mAutoRestore; - private Preference mConfigure; - private Preference mManageData; - private boolean mEnabled; - - @Override - public int getMetricsCategory() { - return MetricsEvent.PRIVACY; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - // Don't allow any access if this is not an admin user. - // TODO: backup/restore currently only works with owner user b/22760572 - mEnabled = UserManager.get(getActivity()).isAdminUser(); - if (!mEnabled) { - return; - } - - addPreferencesFromResource(R.xml.privacy_settings); - final PreferenceScreen screen = getPreferenceScreen(); - mBackupManager = IBackupManager.Stub.asInterface( - ServiceManager.getService(Context.BACKUP_SERVICE)); - - setPreferenceReferences(screen); - - Set<String> keysToRemove = new HashSet<>(); - getNonVisibleKeys(getActivity(), keysToRemove); - final int screenPreferenceCount = screen.getPreferenceCount(); - for (int i = screenPreferenceCount - 1; i >= 0; --i) { - Preference preference = screen.getPreference(i); - if (keysToRemove.contains(preference.getKey())) { - screen.removePreference(preference); - } - } - - updateToggles(); - } - - @Override - public void onResume() { - super.onResume(); - - // Refresh UI - if (mEnabled) { - updateToggles(); - } - } - - @VisibleForTesting - void setPreferenceReferences(PreferenceScreen screen) { - mBackup = screen.findPreference(BACKUP_DATA); - - mAutoRestore = (SwitchPreference) screen.findPreference(AUTO_RESTORE); - mAutoRestore.setOnPreferenceChangeListener(preferenceChangeListener); - - mConfigure = screen.findPreference(CONFIGURE_ACCOUNT); - mManageData = screen.findPreference(DATA_MANAGEMENT); - } - - private OnPreferenceChangeListener preferenceChangeListener = new OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - if (!(preference instanceof SwitchPreference)) { - return true; - } - boolean nextValue = (Boolean) newValue; - boolean result = false; - if (preference == mAutoRestore) { - try { - mBackupManager.setAutoRestore(nextValue); - result = true; - } catch (RemoteException e) { - mAutoRestore.setChecked(!nextValue); - } - } - return result; - } - }; - - - /* - * Creates toggles for each backup/reset preference. - */ - private void updateToggles() { - ContentResolver res = getContentResolver(); - - boolean backupEnabled = false; - Intent configIntent = null; - String configSummary = null; - Intent manageIntent = null; - String manageLabel = null; - try { - backupEnabled = mBackupManager.isBackupEnabled(); - String transport = mBackupManager.getCurrentTransport(); - configIntent = validatedActivityIntent( - mBackupManager.getConfigurationIntent(transport), "config"); - configSummary = mBackupManager.getDestinationString(transport); - manageIntent = validatedActivityIntent( - mBackupManager.getDataManagementIntent(transport), "management"); - manageLabel = mBackupManager.getDataManagementLabel(transport); - - mBackup.setSummary(backupEnabled - ? R.string.accessibility_feature_state_on - : R.string.accessibility_feature_state_off); - } catch (RemoteException e) { - // leave it 'false' and disable the UI; there's no backup manager - mBackup.setEnabled(false); - } - - mAutoRestore.setChecked(Settings.Secure.getInt(res, - Settings.Secure.BACKUP_AUTO_RESTORE, 1) == 1); - mAutoRestore.setEnabled(backupEnabled); - - final boolean configureEnabled = (configIntent != null) && backupEnabled; - mConfigure.setEnabled(configureEnabled); - mConfigure.setIntent(configIntent); - setConfigureSummary(configSummary); - - final boolean manageEnabled = (manageIntent != null) && backupEnabled; - if (manageEnabled) { - mManageData.setIntent(manageIntent); - if (manageLabel != null) { - mManageData.setTitle(manageLabel); - } - } else { - // Hide the item if data management intent is not supported by transport. - getPreferenceScreen().removePreference(mManageData); - } - } - - private Intent validatedActivityIntent(Intent intent, String logLabel) { - if (intent != null) { - PackageManager pm = getPackageManager(); - List<ResolveInfo> resolved = pm.queryIntentActivities(intent, 0); - if (resolved == null || resolved.isEmpty()) { - intent = null; - Log.e(TAG, "Backup " + logLabel + " intent " + intent - + " fails to resolve; ignoring"); - } - } - return intent; - } - - private void setConfigureSummary(String summary) { - if (summary != null) { - mConfigure.setSummary(summary); - } else { - mConfigure.setSummary(R.string.backup_configure_account_default_summary); - } - } - - @Override - public int getHelpResource() { - return R.string.help_url_backup_reset; - } - - private static void getNonVisibleKeys(Context context, Collection<String> nonVisibleKeys) { - final IBackupManager backupManager = IBackupManager.Stub.asInterface( - ServiceManager.getService(Context.BACKUP_SERVICE)); - boolean isServiceActive = false; - try { - isServiceActive = backupManager.isBackupServiceActive(UserHandle.myUserId()); - } catch (RemoteException e) { - Log.w(TAG, "Failed querying backup manager service activity status. " + - "Assuming it is inactive."); - } - boolean vendorSpecific = context.getPackageManager(). - resolveContentProvider(GSETTINGS_PROVIDER, 0) == null; - if (vendorSpecific || isServiceActive) { - nonVisibleKeys.add(BACKUP_INACTIVE); - } - if (vendorSpecific || !isServiceActive) { - nonVisibleKeys.add(BACKUP_DATA); - nonVisibleKeys.add(AUTO_RESTORE); - nonVisibleKeys.add(CONFIGURE_ACCOUNT); - } - } -} diff --git a/src/com/android/settings/ProgressCategory.java b/src/com/android/settings/ProgressCategory.java index eeb7bfe033..804d48b30b 100644 --- a/src/com/android/settings/ProgressCategory.java +++ b/src/com/android/settings/ProgressCategory.java @@ -17,11 +17,12 @@ package com.android.settings; import android.content.Context; -import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; import android.util.AttributeSet; import android.view.View; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + /** * A category with a progress spinner */ diff --git a/src/com/android/settings/ProgressCategoryBase.java b/src/com/android/settings/ProgressCategoryBase.java index 89ded4ba01..1b06e07be5 100644 --- a/src/com/android/settings/ProgressCategoryBase.java +++ b/src/com/android/settings/ProgressCategoryBase.java @@ -17,9 +17,10 @@ package com.android.settings; import android.content.Context; -import androidx.preference.PreferenceCategory; import android.util.AttributeSet; +import androidx.preference.PreferenceCategory; + public abstract class ProgressCategoryBase extends PreferenceCategory { public ProgressCategoryBase(Context context) { super(context); diff --git a/src/com/android/settings/ProxySelector.java b/src/com/android/settings/ProxySelector.java index 79767fb7aa..625369e791 100644 --- a/src/com/android/settings/ProxySelector.java +++ b/src/com/android/settings/ProxySelector.java @@ -17,9 +17,9 @@ package com.android.settings; import android.app.Activity; -import android.app.AlertDialog; import android.app.Dialog; import android.app.admin.DevicePolicyManager; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; import android.net.ConnectivityManager; @@ -39,7 +39,8 @@ import android.widget.Button; import android.widget.EditText; import android.widget.TextView; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.appcompat.app.AlertDialog; + import com.android.settings.SettingsPreferenceFragment.SettingsDialogFragment; import com.android.settings.core.InstrumentedFragment; @@ -105,15 +106,15 @@ public class ProxySelector extends InstrumentedFragment implements DialogCreatab @Override public int getDialogMetricsCategory(int dialogId) { - return MetricsEvent.DIALOG_PROXY_SELECTOR_ERROR; + return SettingsEnums.DIALOG_PROXY_SELECTOR_ERROR; } private void showDialog(int dialogId) { if (mDialogFragment != null) { Log.e(TAG, "Old dialog fragment not null!"); } - mDialogFragment = new SettingsDialogFragment(this, dialogId); - mDialogFragment.show(getActivity().getFragmentManager(), Integer.toString(dialogId)); + mDialogFragment = SettingsDialogFragment.newInstance(this, dialogId); + mDialogFragment.show(getActivity().getSupportFragmentManager(), Integer.toString(dialogId)); } private void initView(View view) { @@ -276,6 +277,6 @@ public class ProxySelector extends InstrumentedFragment implements DialogCreatab @Override public int getMetricsCategory() { - return MetricsEvent.PROXY_SELECTOR; + return SettingsEnums.PROXY_SELECTOR; } } diff --git a/src/com/android/settings/RadioInfo.java b/src/com/android/settings/RadioInfo.java index 3c10b8d3cc..5c6fe9cc4c 100644 --- a/src/com/android/settings/RadioInfo.java +++ b/src/com/android/settings/RadioInfo.java @@ -20,11 +20,10 @@ import static android.net.ConnectivityManager.NetworkCallback; import static android.provider.Settings.Global.PREFERRED_NETWORK_MODE; import android.app.Activity; -import android.app.AlertDialog; -import android.app.Dialog; import android.app.QueuedWork; import android.content.ComponentName; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; @@ -41,25 +40,26 @@ import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Message; +import android.os.SystemProperties; import android.provider.Settings; import android.telephony.CarrierConfigManager; +import android.telephony.CellIdentityCdma; +import android.telephony.CellIdentityGsm; +import android.telephony.CellIdentityLte; +import android.telephony.CellIdentityWcdma; import android.telephony.CellInfo; import android.telephony.CellInfoCdma; import android.telephony.CellInfoGsm; import android.telephony.CellInfoLte; import android.telephony.CellInfoWcdma; -import android.telephony.CellIdentityCdma; -import android.telephony.CellIdentityGsm; -import android.telephony.CellIdentityLte; -import android.telephony.CellIdentityWcdma; import android.telephony.CellLocation; import android.telephony.CellSignalStrengthCdma; import android.telephony.CellSignalStrengthGsm; import android.telephony.CellSignalStrengthLte; import android.telephony.CellSignalStrengthWcdma; -import android.telephony.PreciseCallState; import android.telephony.PhoneStateListener; import android.telephony.PhysicalChannelConfig; +import android.telephony.PreciseCallState; import android.telephony.ServiceState; import android.telephony.SignalStrength; import android.telephony.SubscriptionManager; @@ -82,22 +82,21 @@ import android.widget.Spinner; import android.widget.Switch; import android.widget.TextView; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AlertDialog.Builder; + import com.android.ims.ImsConfig; import com.android.ims.ImsException; import com.android.ims.ImsManager; import com.android.internal.telephony.Phone; -import com.android.internal.telephony.PhoneConstants; import com.android.internal.telephony.PhoneFactory; -import com.android.internal.telephony.RILConstants; -import com.android.internal.telephony.TelephonyProperties; import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; -import java.net.UnknownHostException; -import java.util.ArrayList; import java.util.List; +// TODO(b/123598192) consider to move this activity to telephony package. public class RadioInfo extends Activity { private static final String TAG = "RadioInfo"; @@ -132,6 +131,13 @@ public class RadioInfo extends Activity { private static final int CELL_INFO_LIST_RATE_DISABLED = Integer.MAX_VALUE; private static final int CELL_INFO_LIST_RATE_MAX = 0; + private static final String DSDS_MODE_PROPERTY = "ro.boot.hardware.dsds"; + + /** + * A value indicates the device is always on dsds mode. + * @see {@link #DSDS_MODE_PROPERTY} + */ + private static final int ALWAYS_ON_DSDS_MODE = 1; private static final int IMS_VOLTE_PROVISIONED_CONFIG_ID = ImsConfig.ConfigConstants.VLT_SETTING_ENABLED; @@ -221,6 +227,8 @@ public class RadioInfo extends Activity { private Switch imsVtProvisionedSwitch; private Switch imsWfcProvisionedSwitch; private Switch eabProvisionedSwitch; + private Switch cbrsDataSwitch; + private Switch dsdsSwitch; private Spinner preferredNetworkType; private Spinner mSelectPhoneIndex; private Spinner cellInfoRefreshRateSpinner; @@ -493,6 +501,26 @@ public class RadioInfo extends Activity { eabProvisionedSwitch.setVisibility(View.GONE); } + cbrsDataSwitch = (Switch) findViewById(R.id.cbrs_data_switch); + cbrsDataSwitch.setVisibility(isCbrsSupported() ? View.VISIBLE : View.GONE); + + dsdsSwitch = findViewById(R.id.dsds_switch); + if (isDsdsSupported() && !dsdsModeOnly()) { + dsdsSwitch.setVisibility(View.VISIBLE); + dsdsSwitch.setOnClickListener(v -> { + if (mTelephonyManager.doesSwitchMultiSimConfigTriggerReboot()) { + // Undo the click action until user clicks the confirm dialog. + dsdsSwitch.toggle(); + showDsdsChangeDialog(); + } else { + performDsdsSwitch(); + } + }); + dsdsSwitch.setChecked(isDsdsEnabled()); + } else { + dsdsSwitch.setVisibility(View.GONE); + } + radioPowerOnSwitch = (Switch) findViewById(R.id.radio_power); mDownlinkKbps = (TextView) findViewById(R.id.dl_kbps); @@ -579,6 +607,11 @@ public class RadioInfo extends Activity { imsWfcProvisionedSwitch.setOnCheckedChangeListener(mImsWfcCheckedChangeListener); eabProvisionedSwitch.setOnCheckedChangeListener(mEabCheckedChangeListener); + if (isCbrsSupported()) { + cbrsDataSwitch.setChecked(getCbrsDataState()); + cbrsDataSwitch.setOnCheckedChangeListener(mCbrsDataSwitchChangeListener); + } + unregisterPhoneStateListener(); registerPhoneStateListener(); @@ -1616,4 +1649,78 @@ public class RadioInfo extends Activity { } }; + boolean isCbrsSupported() { + return getResources().getBoolean( + com.android.internal.R.bool.config_cbrs_supported); + } + + void updateCbrsDataState(boolean state) { + Log.d(TAG, "setCbrsDataSwitchState() state:" + ((state)? "on":"off")); + if (mTelephonyManager != null) { + QueuedWork.queue(new Runnable() { + public void run() { + mTelephonyManager.setOpportunisticNetworkState(state); + cbrsDataSwitch.setChecked(getCbrsDataState()); + } + }, false); + } + } + + boolean getCbrsDataState() { + boolean state = false; + if (mTelephonyManager != null) { + state = mTelephonyManager.isOpportunisticNetworkEnabled(); + } + Log.d(TAG, "getCbrsDataState() state:" +((state)? "on":"off")); + return state; + } + + OnCheckedChangeListener mCbrsDataSwitchChangeListener = new OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + updateCbrsDataState(isChecked); + } + }; + + private void showDsdsChangeDialog() { + final AlertDialog confirmDialog = new Builder(RadioInfo.this) + .setTitle(R.string.dsds_dialog_title) + .setMessage(R.string.dsds_dialog_message) + .setPositiveButton(R.string.dsds_dialog_confirm, mOnDsdsDialogConfirmedListener) + .setNegativeButton(R.string.dsds_dialog_cancel, mOnDsdsDialogConfirmedListener) + .create(); + confirmDialog.show(); + } + + private static boolean isDsdsSupported() { + return (TelephonyManager.getDefault().isMultiSimSupported() + == TelephonyManager.MULTISIM_ALLOWED); + } + + private static boolean isDsdsEnabled() { + return TelephonyManager.getDefault().getPhoneCount() > 1; + } + + private void performDsdsSwitch() { + mTelephonyManager.switchMultiSimConfig(dsdsSwitch.isChecked() ? 2 : 1); + } + + /** + * @return {@code True} if the device is only supported dsds mode. + */ + private boolean dsdsModeOnly() { + String dsdsMode = SystemProperties.get(DSDS_MODE_PROPERTY); + return !TextUtils.isEmpty(dsdsMode) && Integer.parseInt(dsdsMode) == ALWAYS_ON_DSDS_MODE; + } + + DialogInterface.OnClickListener mOnDsdsDialogConfirmedListener = + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (which == DialogInterface.BUTTON_POSITIVE) { + dsdsSwitch.toggle(); + performDsdsSwitch(); + } + } + }; } diff --git a/src/com/android/settings/RegulatoryInfoDisplayActivity.java b/src/com/android/settings/RegulatoryInfoDisplayActivity.java index 2f35221294..4c7515d10f 100644 --- a/src/com/android/settings/RegulatoryInfoDisplayActivity.java +++ b/src/com/android/settings/RegulatoryInfoDisplayActivity.java @@ -17,7 +17,6 @@ package com.android.settings; import android.app.Activity; -import android.app.AlertDialog; import android.content.DialogInterface; import android.content.res.Resources; import android.graphics.Bitmap; @@ -25,13 +24,15 @@ import android.graphics.BitmapFactory; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.SystemProperties; -import androidx.annotation.VisibleForTesting; import android.text.TextUtils; import android.view.Gravity; import android.view.View; import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; + import java.util.Locale; /** @@ -118,7 +119,8 @@ public class RegulatoryInfoDisplayActivity extends Activity implements } } - private int getResourceId() { + @VisibleForTesting + int getResourceId() { // Use regulatory_info by default. int resId = getResources().getIdentifier( REGULATORY_INFO_RESOURCE, "drawable", getPackageName()); @@ -133,6 +135,18 @@ public class RegulatoryInfoDisplayActivity extends Activity implements resId = id; } } + + // When hardware coo property exists, use regulatory_info_<sku>_<coo> resource if valid. + final String coo = getCoo(); + if (!TextUtils.isEmpty(coo) && !TextUtils.isEmpty(sku)) { + final String regulatory_info_coo_res = + REGULATORY_INFO_RESOURCE + "_" + sku.toLowerCase() + "_" + coo.toLowerCase(); + final int id = getResources().getIdentifier( + regulatory_info_coo_res, "drawable", getPackageName()); + if (id != 0) { + resId = id; + } + } return resId; } @@ -141,13 +155,15 @@ public class RegulatoryInfoDisplayActivity extends Activity implements finish(); // close the activity } - @VisibleForTesting - public static String getSku() { + private String getCoo() { + return SystemProperties.get("ro.boot.hardware.coo", ""); + } + + private String getSku() { return SystemProperties.get("ro.boot.hardware.sku", ""); } - @VisibleForTesting - public static String getRegulatoryInfoImageFileName() { + private String getRegulatoryInfoImageFileName() { final String sku = getSku(); if (TextUtils.isEmpty(sku)) { return DEFAULT_REGULATORY_INFO_FILEPATH; diff --git a/src/com/android/settings/RemoteBugreportActivity.java b/src/com/android/settings/RemoteBugreportActivity.java index 0589a24c50..2c88ec329d 100644 --- a/src/com/android/settings/RemoteBugreportActivity.java +++ b/src/com/android/settings/RemoteBugreportActivity.java @@ -17,16 +17,14 @@ package com.android.settings; import android.annotation.Nullable; import android.app.Activity; -import android.app.AlertDialog; import android.app.admin.DevicePolicyManager; import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; import android.os.UserHandle; import android.util.Log; -import android.widget.LinearLayout; -import com.android.settings.R; +import androidx.appcompat.app.AlertDialog; /** * UI for the remote bugreport dialog. Shows one of 3 possible dialogs: diff --git a/src/com/android/settings/ResetNetwork.java b/src/com/android/settings/ResetNetwork.java index 591ce0ab68..424d976e90 100644 --- a/src/com/android/settings/ResetNetwork.java +++ b/src/com/android/settings/ResetNetwork.java @@ -18,6 +18,7 @@ package com.android.settings; import android.annotation.Nullable; import android.app.Activity; +import android.app.settings.SettingsEnums; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; @@ -41,15 +42,16 @@ import android.widget.CheckBox; import android.widget.Spinner; import android.widget.TextView; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.annotation.VisibleForTesting; + import com.android.internal.telephony.PhoneConstants; import com.android.settings.core.InstrumentedFragment; import com.android.settings.core.SubSettingLauncher; import com.android.settings.enterprise.ActionDisabledByAdminDialogHelper; import com.android.settings.password.ChooseLockSettingsHelper; import com.android.settings.password.ConfirmLockPattern; -import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; +import com.android.settingslib.RestrictedLockUtilsInternal; import java.util.ArrayList; import java.util.List; @@ -74,8 +76,8 @@ public class ResetNetwork extends InstrumentedFragment { private View mContentView; private Spinner mSubscriptionSpinner; private Button mInitiateButton; - private View mEsimContainer; - private CheckBox mEsimCheckbox; + @VisibleForTesting View mEsimContainer; + @VisibleForTesting CheckBox mEsimCheckbox; @Override public void onCreate(@Nullable Bundle savedInstanceState) { @@ -112,18 +114,20 @@ public class ResetNetwork extends InstrumentedFragment { } } - private void showFinalConfirmation() { + @VisibleForTesting + void showFinalConfirmation() { Bundle args = new Bundle(); if (mSubscriptions != null && mSubscriptions.size() > 0) { int selectedIndex = mSubscriptionSpinner.getSelectedItemPosition(); SubscriptionInfo subscription = mSubscriptions.get(selectedIndex); args.putInt(PhoneConstants.SUBSCRIPTION_KEY, subscription.getSubscriptionId()); } - args.putBoolean(MasterClear.ERASE_ESIMS_EXTRA, mEsimCheckbox.isChecked()); + args.putBoolean(MasterClear.ERASE_ESIMS_EXTRA, + mEsimContainer.getVisibility() == View.VISIBLE && mEsimCheckbox.isChecked()); new SubSettingLauncher(getContext()) .setDestination(ResetNetworkConfirm.class.getName()) .setArguments(args) - .setTitle(R.string.reset_network_confirm_title) + .setTitleRes(R.string.reset_network_confirm_title) .setSourceMetricsCategory(getMetricsCategory()) .launch(); } @@ -160,7 +164,8 @@ public class ResetNetwork extends InstrumentedFragment { mEsimContainer = mContentView.findViewById(R.id.erase_esim_container); mEsimCheckbox = mContentView.findViewById(R.id.erase_esim); - mSubscriptions = SubscriptionManager.from(getActivity()).getActiveSubscriptionInfoList(); + mSubscriptions = SubscriptionManager.from(getActivity()) + .getActiveSubscriptionInfoList(true); if (mSubscriptions != null && mSubscriptions.size() > 0) { // Get the default subscription in the order of data, voice, sms, first up. int defaultSubscription = SubscriptionManager.getDefaultDataSubscriptionId(); @@ -212,8 +217,6 @@ public class ResetNetwork extends InstrumentedFragment { mInitiateButton.setOnClickListener(mInitiateListener); if (showEuiccSettings(getContext())) { mEsimContainer.setVisibility(View.VISIBLE); - TextView title = mContentView.findViewById(R.id.erase_esim_title); - title.setText(R.string.reset_esim_title); mEsimContainer.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { @@ -240,9 +243,9 @@ public class ResetNetwork extends InstrumentedFragment { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final UserManager um = UserManager.get(getActivity()); - final EnforcedAdmin admin = RestrictedLockUtils.checkIfRestrictionEnforced( + final EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced( getActivity(), UserManager.DISALLOW_NETWORK_RESET, UserHandle.myUserId()); - if (!um.isAdminUser() || RestrictedLockUtils.hasBaseUserRestriction(getActivity(), + if (!um.isAdminUser() || RestrictedLockUtilsInternal.hasBaseUserRestriction(getActivity(), UserManager.DISALLOW_NETWORK_RESET, UserHandle.myUserId())) { return inflater.inflate(R.layout.network_reset_disallowed_screen, null); } else if (admin != null) { @@ -261,6 +264,6 @@ public class ResetNetwork extends InstrumentedFragment { @Override public int getMetricsCategory() { - return MetricsEvent.RESET_NETWORK; + return SettingsEnums.RESET_NETWORK; } } diff --git a/src/com/android/settings/ResetNetworkConfirm.java b/src/com/android/settings/ResetNetworkConfirm.java index 950a0b08e4..beb0528f6a 100644 --- a/src/com/android/settings/ResetNetworkConfirm.java +++ b/src/com/android/settings/ResetNetworkConfirm.java @@ -18,7 +18,9 @@ package com.android.settings; import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; -import android.app.AlertDialog; +import android.app.Activity; +import android.app.ProgressDialog; +import android.app.settings.SettingsEnums; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothManager; import android.content.ContentResolver; @@ -27,29 +29,30 @@ import android.net.ConnectivityManager; import android.net.NetworkPolicyManager; import android.net.Uri; import android.net.wifi.WifiManager; +import android.net.wifi.p2p.WifiP2pManager; import android.os.AsyncTask; import android.os.Bundle; import android.os.RecoverySystem; import android.os.UserHandle; import android.os.UserManager; -import android.provider.Telephony; -import androidx.annotation.VisibleForTesting; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; +import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; + import com.android.ims.ImsManager; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.telephony.PhoneConstants; import com.android.settings.core.InstrumentedFragment; import com.android.settings.enterprise.ActionDisabledByAdminDialogHelper; import com.android.settings.network.ApnSettings; -import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; /** * Confirm and execute a reset of the network settings to a clean "just out of the box" @@ -63,86 +66,58 @@ import com.android.settingslib.RestrictedLockUtils; */ public class ResetNetworkConfirm extends InstrumentedFragment { - private View mContentView; - private int mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; + @VisibleForTesting View mContentView; @VisibleForTesting boolean mEraseEsim; - @VisibleForTesting EraseEsimAsyncTask mEraseEsimTask; + @VisibleForTesting ResetNetworkTask mResetNetworkTask; + @VisibleForTesting Activity mActivity; + private int mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; + private ProgressDialog mProgressDialog; + private AlertDialog mAlertDialog; /** - * Async task used to erase all the eSIM profiles from the phone. If error happens during + * Async task used to do all reset task. If error happens during * erasing eSIM profiles or timeout, an error msg is shown. */ - private static class EraseEsimAsyncTask extends AsyncTask<Void, Void, Boolean> { + private class ResetNetworkTask extends AsyncTask<Void, Void, Boolean> { private final Context mContext; private final String mPackageName; - EraseEsimAsyncTask(Context context, String packageName) { + ResetNetworkTask(Context context) { mContext = context; - mPackageName = packageName; + mPackageName = context.getPackageName(); } @Override protected Boolean doInBackground(Void... params) { - return RecoverySystem.wipeEuiccData(mContext, mPackageName); - } - - @Override - protected void onPostExecute(Boolean succeeded) { - if (succeeded) { - Toast.makeText(mContext, R.string.reset_network_complete_toast, Toast.LENGTH_SHORT) - .show(); - } else { - new AlertDialog.Builder(mContext) - .setTitle(R.string.reset_esim_error_title) - .setMessage(R.string.reset_esim_error_msg) - .setPositiveButton(android.R.string.ok, null /* listener */) - .show(); - } - } - } - - /** - * The user has gone through the multiple confirmation, so now we go ahead - * and reset the network settings to its factory-default state. - */ - private Button.OnClickListener mFinalClickListener = new Button.OnClickListener() { - - @Override - public void onClick(View v) { - if (Utils.isMonkeyRunning()) { - return; - } - // TODO maybe show a progress screen if this ends up taking a while and won't let user - // go back until the tasks finished. - Context context = getActivity(); - ConnectivityManager connectivityManager = (ConnectivityManager) - context.getSystemService(Context.CONNECTIVITY_SERVICE); + mContext.getSystemService(Context.CONNECTIVITY_SERVICE); if (connectivityManager != null) { connectivityManager.factoryReset(); } WifiManager wifiManager = (WifiManager) - context.getSystemService(Context.WIFI_SERVICE); + mContext.getSystemService(Context.WIFI_SERVICE); if (wifiManager != null) { wifiManager.factoryReset(); } + p2pFactoryReset(mContext); + TelephonyManager telephonyManager = (TelephonyManager) - context.getSystemService(Context.TELEPHONY_SERVICE); + mContext.getSystemService(Context.TELEPHONY_SERVICE); if (telephonyManager != null) { telephonyManager.factoryReset(mSubId); } NetworkPolicyManager policyManager = (NetworkPolicyManager) - context.getSystemService(Context.NETWORK_POLICY_SERVICE); + mContext.getSystemService(Context.NETWORK_POLICY_SERVICE); if (policyManager != null) { String subscriberId = telephonyManager.getSubscriberId(mSubId); policyManager.factoryReset(subscriberId); } BluetoothManager btManager = (BluetoothManager) - context.getSystemService(Context.BLUETOOTH_SERVICE); + mContext.getSystemService(Context.BLUETOOTH_SERVICE); if (btManager != null) { BluetoothAdapter btAdapter = btManager.getAdapter(); if (btAdapter != null) { @@ -150,24 +125,76 @@ public class ResetNetworkConfirm extends InstrumentedFragment { } } - ImsManager.getInstance(context, - SubscriptionManager.getPhoneId(mSubId)).factoryReset(); - restoreDefaultApn(context); - esimFactoryReset(context, context.getPackageName()); + ImsManager.getInstance(mContext, + SubscriptionManager.getPhoneId(mSubId)).factoryReset(); + restoreDefaultApn(mContext); + if (mEraseEsim) { + return RecoverySystem.wipeEuiccData(mContext, mPackageName); + } else { + return true; + } + } + + @Override + protected void onPostExecute(Boolean succeeded) { + mProgressDialog.dismiss(); + if (succeeded) { + Toast.makeText(mContext, R.string.reset_network_complete_toast, Toast.LENGTH_SHORT) + .show(); + } else { + mAlertDialog = new AlertDialog.Builder(mContext) + .setTitle(R.string.reset_esim_error_title) + .setMessage(R.string.reset_esim_error_msg) + .setPositiveButton(android.R.string.ok, null /* listener */) + .show(); + } + } + } + + /** + * The user has gone through the multiple confirmation, so now we go ahead + * and reset the network settings to its factory-default state. + */ + @VisibleForTesting + Button.OnClickListener mFinalClickListener = new Button.OnClickListener() { + + @Override + public void onClick(View v) { + if (Utils.isMonkeyRunning()) { + return; + } + + mProgressDialog = getProgressDialog(mActivity); + mProgressDialog.show(); + + mResetNetworkTask = new ResetNetworkTask(mActivity); + mResetNetworkTask.execute(); } }; @VisibleForTesting - void esimFactoryReset(Context context, String packageName) { - if (mEraseEsim) { - mEraseEsimTask = new EraseEsimAsyncTask(context, packageName); - mEraseEsimTask.execute(); - } else { - Toast.makeText(context, R.string.reset_network_complete_toast, Toast.LENGTH_SHORT) - .show(); + void p2pFactoryReset(Context context) { + WifiP2pManager wifiP2pManager = (WifiP2pManager) + context.getSystemService(Context.WIFI_P2P_SERVICE); + if (wifiP2pManager != null) { + WifiP2pManager.Channel channel = wifiP2pManager.initialize( + context.getApplicationContext(), context.getMainLooper(), + null /* listener */); + if (channel != null) { + wifiP2pManager.factoryReset(channel, null /* listener */); + } } } + private ProgressDialog getProgressDialog(Context context) { + final ProgressDialog progressDialog = new ProgressDialog(context); + progressDialog.setIndeterminate(true); + progressDialog.setCancelable(false); + progressDialog.setMessage( + context.getString(R.string.master_clear_progress_text)); + return progressDialog; + } + /** * Restore APN settings to default. */ @@ -190,23 +217,32 @@ public class ResetNetworkConfirm extends InstrumentedFragment { .setOnClickListener(mFinalClickListener); } + @VisibleForTesting + void setSubtitle() { + if (mEraseEsim) { + ((TextView) mContentView.findViewById(R.id.reset_network_confirm)) + .setText(R.string.reset_network_final_desc_esim); + } + } + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - final EnforcedAdmin admin = RestrictedLockUtils.checkIfRestrictionEnforced( - getActivity(), UserManager.DISALLOW_NETWORK_RESET, UserHandle.myUserId()); - if (RestrictedLockUtils.hasBaseUserRestriction(getActivity(), + final EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced( + mActivity, UserManager.DISALLOW_NETWORK_RESET, UserHandle.myUserId()); + if (RestrictedLockUtilsInternal.hasBaseUserRestriction(mActivity, UserManager.DISALLOW_NETWORK_RESET, UserHandle.myUserId())) { return inflater.inflate(R.layout.network_reset_disallowed_screen, null); } else if (admin != null) { - new ActionDisabledByAdminDialogHelper(getActivity()) + new ActionDisabledByAdminDialogHelper(mActivity) .prepareDialogBuilder(UserManager.DISALLOW_NETWORK_RESET, admin) - .setOnDismissListener(__ -> getActivity().finish()) + .setOnDismissListener(__ -> mActivity.finish()) .show(); - return new View(getContext()); + return new View(mActivity); } mContentView = inflater.inflate(R.layout.reset_network_confirm, null); establishFinalConfirmationState(); + setSubtitle(); return mContentView; } @@ -220,19 +256,27 @@ public class ResetNetworkConfirm extends InstrumentedFragment { SubscriptionManager.INVALID_SUBSCRIPTION_ID); mEraseEsim = args.getBoolean(MasterClear.ERASE_ESIMS_EXTRA); } + + mActivity = getActivity(); } @Override public void onDestroy() { - if (mEraseEsimTask != null) { - mEraseEsimTask.cancel(true /* mayInterruptIfRunning */); - mEraseEsimTask = null; + if (mResetNetworkTask != null) { + mResetNetworkTask.cancel(true /* mayInterruptIfRunning */); + mResetNetworkTask = null; + } + if (mProgressDialog != null) { + mProgressDialog.dismiss(); + } + if (mAlertDialog != null) { + mAlertDialog.dismiss(); } super.onDestroy(); } @Override public int getMetricsCategory() { - return MetricsEvent.RESET_NETWORK_CONFIRM; + return SettingsEnums.RESET_NETWORK_CONFIRM; } } diff --git a/src/com/android/settings/RestrictedCheckBox.java b/src/com/android/settings/RestrictedCheckBox.java index 476df29bf0..828c4434d9 100644 --- a/src/com/android/settings/RestrictedCheckBox.java +++ b/src/com/android/settings/RestrictedCheckBox.java @@ -22,10 +22,9 @@ import android.content.Context; import android.graphics.PorterDuff; import android.util.AttributeSet; import android.widget.CheckBox; -import android.widget.RadioButton; -import android.widget.TextView; import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; /** * A checkbox that can be restricted by device policy, in which case it shows a dialog explaining @@ -59,7 +58,8 @@ public class RestrictedCheckBox extends CheckBox { mEnforcedAdmin = admin; if (mDisabledByAdmin != disabled) { mDisabledByAdmin = disabled; - RestrictedLockUtils.setTextViewAsDisabledByAdmin(mContext, this, mDisabledByAdmin); + RestrictedLockUtilsInternal.setTextViewAsDisabledByAdmin(mContext, this, + mDisabledByAdmin); if (mDisabledByAdmin) { getButtonDrawable().setColorFilter(mContext.getColor(R.color.disabled_text_color), PorterDuff.Mode.MULTIPLY); diff --git a/src/com/android/settings/RestrictedListPreference.java b/src/com/android/settings/RestrictedListPreference.java index 4cfb2cf151..bd3cd17432 100644 --- a/src/com/android/settings/RestrictedListPreference.java +++ b/src/com/android/settings/RestrictedListPreference.java @@ -16,18 +16,14 @@ package com.android.settings; -import android.app.ActivityManager; -import android.app.AlertDialog; +import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; + import android.app.KeyguardManager; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; -import android.os.RemoteException; import android.os.UserManager; -import androidx.annotation.VisibleForTesting; -import androidx.preference.ListPreferenceDialogFragment; -import androidx.preference.PreferenceViewHolder; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; @@ -38,15 +34,17 @@ import android.widget.ImageView; import android.widget.ListAdapter; import android.widget.ListView; -import com.android.settings.Utils; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AlertDialog.Builder; +import androidx.preference.ListPreferenceDialogFragmentCompat; +import androidx.preference.PreferenceViewHolder; + import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedPreferenceHelper; import java.util.ArrayList; import java.util.List; -import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; - public class RestrictedListPreference extends CustomListPreference { private final RestrictedPreferenceHelper mHelper; private final List<RestrictedItem> mRestrictedItems = new ArrayList<>(); @@ -159,8 +157,8 @@ public class RestrictedListPreference extends CustomListPreference { return null; } - protected ListAdapter createListAdapter() { - return new RestrictedArrayAdapter(getContext(), getEntries(), + protected ListAdapter createListAdapter(Context context) { + return new RestrictedArrayAdapter(context, getEntries(), getSelectedValuePos()); } @@ -172,12 +170,11 @@ public class RestrictedListPreference extends CustomListPreference { } @Override - protected void onPrepareDialogBuilder(AlertDialog.Builder builder, + protected void onPrepareDialogBuilder(Builder builder, DialogInterface.OnClickListener listener) { - builder.setAdapter(createListAdapter(), listener); + builder.setAdapter(createListAdapter(builder.getContext()), listener); } - public class RestrictedArrayAdapter extends ArrayAdapter<CharSequence> { private final int mSelectedIndex; public RestrictedArrayAdapter(Context context, CharSequence[] objects, int selectedIndex) { @@ -222,8 +219,8 @@ public class RestrictedListPreference extends CustomListPreference { CustomListPreference.CustomListPreferenceDialogFragment { private int mLastCheckedPosition = AdapterView.INVALID_POSITION; - public static ListPreferenceDialogFragment newInstance(String key) { - final ListPreferenceDialogFragment fragment + public static ListPreferenceDialogFragmentCompat newInstance(String key) { + final ListPreferenceDialogFragmentCompat fragment = new RestrictedListPreferenceDialogFragment(); final Bundle b = new Bundle(1); b.putString(ARG_KEY, key); diff --git a/src/com/android/settings/RestrictedRadioButton.java b/src/com/android/settings/RestrictedRadioButton.java index 6ff66ba50f..20c28723e4 100644 --- a/src/com/android/settings/RestrictedRadioButton.java +++ b/src/com/android/settings/RestrictedRadioButton.java @@ -16,17 +16,16 @@ package com.android.settings; +import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; + import android.content.Context; import android.graphics.PorterDuff; import android.util.AttributeSet; import android.widget.RadioButton; import android.widget.TextView; -import java.util.List; - import com.android.settingslib.RestrictedLockUtils; - -import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; +import com.android.settingslib.RestrictedLockUtilsInternal; public class RestrictedRadioButton extends RadioButton { private Context mContext; @@ -65,7 +64,7 @@ public class RestrictedRadioButton extends RadioButton { mEnforcedAdmin = admin; if (mDisabledByAdmin != disabled) { mDisabledByAdmin = disabled; - RestrictedLockUtils.setTextViewAsDisabledByAdmin(mContext, + RestrictedLockUtilsInternal.setTextViewAsDisabledByAdmin(mContext, (TextView) this, mDisabledByAdmin); if (mDisabledByAdmin) { getButtonDrawable().setColorFilter(mContext.getColor(R.color.disabled_text_color), diff --git a/src/com/android/settings/RestrictedSettingsFragment.java b/src/com/android/settings/RestrictedSettingsFragment.java index 3392d8c3bc..6b538b81f2 100644 --- a/src/com/android/settings/RestrictedSettingsFragment.java +++ b/src/com/android/settings/RestrictedSettingsFragment.java @@ -16,8 +16,9 @@ package com.android.settings; +import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; + import android.app.Activity; -import android.app.AlertDialog; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -27,15 +28,15 @@ import android.os.Bundle; import android.os.PersistableBundle; import android.os.UserHandle; import android.os.UserManager; -import androidx.annotation.VisibleForTesting; import android.view.View; import android.widget.TextView; +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; + import com.android.settings.dashboard.RestrictedDashboardFragment; import com.android.settings.enterprise.ActionDisabledByAdminDialogHelper; -import com.android.settingslib.RestrictedLockUtils; - -import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; +import com.android.settingslib.RestrictedLockUtilsInternal; /** * Base class for settings screens that should be pin protected when in restricted mode or @@ -218,10 +219,10 @@ public abstract class RestrictedSettingsFragment extends SettingsPreferenceFragm } public EnforcedAdmin getRestrictionEnforcedAdmin() { - mEnforcedAdmin = RestrictedLockUtils.checkIfRestrictionEnforced(getActivity(), + mEnforcedAdmin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(getActivity(), mRestrictionKey, UserHandle.myUserId()); - if (mEnforcedAdmin != null && mEnforcedAdmin.userId == UserHandle.USER_NULL) { - mEnforcedAdmin.userId = UserHandle.myUserId(); + if (mEnforcedAdmin != null && mEnforcedAdmin.user == null) { + mEnforcedAdmin.user = UserHandle.of(UserHandle.myUserId()); } return mEnforcedAdmin; } diff --git a/src/com/android/settings/RingtonePreference.java b/src/com/android/settings/RingtonePreference.java index 98f0579d66..8f9c618d5e 100644 --- a/src/com/android/settings/RingtonePreference.java +++ b/src/com/android/settings/RingtonePreference.java @@ -24,11 +24,12 @@ import android.media.RingtoneManager; import android.net.Uri; import android.os.UserHandle; import android.provider.Settings.System; -import androidx.preference.Preference; -import androidx.preference.PreferenceManager; import android.text.TextUtils; import android.util.AttributeSet; +import androidx.preference.Preference; +import androidx.preference.PreferenceManager; + /** * A {@link Preference} that allows the user to choose a ringtone from those on the device. * The chosen ringtone's URI will be persisted as a string. diff --git a/src/com/android/settings/SeekBarDialogPreference.java b/src/com/android/settings/SeekBarDialogPreference.java index 507c551caa..d0c8134176 100644 --- a/src/com/android/settings/SeekBarDialogPreference.java +++ b/src/com/android/settings/SeekBarDialogPreference.java @@ -23,13 +23,13 @@ import android.view.View; import android.widget.ImageView; import android.widget.SeekBar; -import com.android.settingslib.CustomDialogPreference; +import com.android.settingslib.CustomDialogPreferenceCompat; /** * Based on frameworks/base/core/java/android/preference/SeekBarDialogPreference.java * except uses support lib preferences. */ -public class SeekBarDialogPreference extends CustomDialogPreference { +public class SeekBarDialogPreference extends CustomDialogPreferenceCompat { private final Drawable mMyIcon; public SeekBarDialogPreference(Context context, AttributeSet attrs) { diff --git a/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java index 903e49e798..ab59da4600 100644 --- a/src/com/android/settings/Settings.java +++ b/src/com/android/settings/Settings.java @@ -30,10 +30,14 @@ public class Settings extends SettingsActivity { */ public static class AssistGestureSettingsActivity extends SettingsActivity { /* empty */} public static class BluetoothSettingsActivity extends SettingsActivity { /* empty */ } + public static class CreateShortcutActivity extends SettingsActivity { /* empty */ } + public static class FaceSettingsActivity extends SettingsActivity { /* empty */ } + public static class FingerprintSettingsActivity extends SettingsActivity { /* empty */ } public static class SimSettingsActivity extends SettingsActivity { /* empty */ } public static class TetherSettingsActivity extends SettingsActivity { /* empty */ } public static class WifiTetherSettingsActivity extends SettingsActivity { /* empty */ } public static class VpnSettingsActivity extends SettingsActivity { /* empty */ } + public static class DataSaverSummaryActivity extends SettingsActivity{ /* empty */ } public static class DateTimeSettingsActivity extends SettingsActivity { /* empty */ } public static class PrivateVolumeForgetActivity extends SettingsActivity { /* empty */ } public static class PrivateVolumeSettingsActivity extends SettingsActivity { /* empty */ } @@ -51,8 +55,8 @@ public class Settings extends SettingsActivity { public static class DisplaySettingsActivity extends SettingsActivity { /* empty */ } public static class NightDisplaySettingsActivity extends SettingsActivity { /* empty */ } public static class NightDisplaySuggestionActivity extends NightDisplaySettingsActivity { /* empty */ } - public static class DeviceInfoSettingsActivity extends SettingsActivity { /* empty */ } public static class MyDeviceInfoActivity extends SettingsActivity { /* empty */ } + public static class ModuleLicensesActivity extends SettingsActivity { /* empty */ } public static class ApplicationSettingsActivity extends SettingsActivity { /* empty */ } public static class ManageApplicationsActivity extends SettingsActivity { /* empty */ } public static class ManageAssistActivity extends SettingsActivity { /* empty */ } @@ -61,24 +65,27 @@ public class Settings extends SettingsActivity { public static class StorageUseActivity extends SettingsActivity { /* empty */ } public static class DevelopmentSettingsDashboardActivity extends SettingsActivity { /* empty */ } public static class AccessibilitySettingsActivity extends SettingsActivity { /* empty */ } + public static class AccessibilityDetailsSettingsActivity extends SettingsActivity { /* empty */ } public static class CaptioningSettingsActivity extends SettingsActivity { /* empty */ } public static class AccessibilityInversionSettingsActivity extends SettingsActivity { /* empty */ } public static class AccessibilityContrastSettingsActivity extends SettingsActivity { /* empty */ } public static class AccessibilityDaltonizerSettingsActivity extends SettingsActivity { /* empty */ } public static class SecurityDashboardActivity extends SettingsActivity { /* empty */ } public static class UsageAccessSettingsActivity extends SettingsActivity { /* empty */ } + public static class AppUsageAccessSettingsActivity extends SettingsActivity { /* empty */ } public static class LocationSettingsActivity extends SettingsActivity { /* empty */ } public static class ScanningSettingsActivity extends SettingsActivity { /* empty */ } + public static class PrivacyDashboardActivity extends SettingsActivity { /* empty */ } public static class PrivacySettingsActivity extends SettingsActivity { /* empty */ } public static class FactoryResetActivity extends SettingsActivity { /* empty */ } public static class RunningServicesActivity extends SettingsActivity { /* empty */ } public static class BatterySaverSettingsActivity extends SettingsActivity { /* empty */ } + public static class BatterySaverScheduleSettingsActivity extends SettingsActivity { /* empty */ } public static class AccountSyncSettingsActivity extends SettingsActivity { /* empty */ } public static class AccountSyncSettingsInAddAccountActivity extends SettingsActivity { /* empty */ } public static class CryptKeeperSettingsActivity extends SettingsActivity { /* empty */ } public static class DeviceAdminSettingsActivity extends SettingsActivity { /* empty */ } public static class DataUsageSummaryActivity extends SettingsActivity { /* empty */ } - public static class DataUsageSummaryLegacyActivity extends SettingsActivity { /* empty */ } public static class MobileDataUsageListActivity extends SettingsActivity { /* empty */ } public static class ConfigureWifiSettingsActivity extends SettingsActivity { /* empty */ } public static class SavedAccessPointsSettingsActivity extends SettingsActivity { /* empty */ } @@ -93,6 +100,7 @@ public class Settings extends SettingsActivity { public static class PictureInPictureSettingsActivity extends SettingsActivity { /* empty */ } public static class AppPictureInPictureSettingsActivity extends SettingsActivity { /* empty */ } public static class ZenAccessSettingsActivity extends SettingsActivity { /* empty */ } + public static class ZenAccessDetailSettingsActivity extends SettingsActivity {} public static class ConditionProviderSettingsActivity extends SettingsActivity { /* empty */ } public static class UsbSettingsActivity extends SettingsActivity { /* empty */ } public static class UsbDetailsActivity extends SettingsActivity { /* empty */ } @@ -108,6 +116,8 @@ public class Settings extends SettingsActivity { public static class ZenModeEventRuleSettingsActivity extends SettingsActivity { /* empty */ } public static class SoundSettingsActivity extends SettingsActivity { /* empty */ } public static class ConfigureNotificationSettingsActivity extends SettingsActivity { /* empty */ } + public static class AppBubbleNotificationSettingsActivity extends SettingsActivity { /* empty */ } + public static class NotificationAssistantSettingsActivity extends SettingsActivity{ /* empty */ } public static class NotificationAppListActivity extends SettingsActivity { /* empty */ } public static class AppNotificationSettingsActivity extends SettingsActivity { /* empty */ } public static class ChannelNotificationSettingsActivity extends SettingsActivity { /* empty */ } @@ -119,9 +129,7 @@ public class Settings extends SettingsActivity { public static class PhotosStorageActivity extends SettingsActivity { /* empty */ } - public static class DirectoryAccessSettingsActivity extends SettingsActivity { /* empty */ } - public static class TopLevelSettings extends SettingsActivity { /* empty */ } public static class ApnSettingsActivity extends SettingsActivity { /* empty */ } public static class WifiCallingSettingsActivity extends SettingsActivity { /* empty */ } public static class MemorySettingsActivity extends SettingsActivity { /* empty */ } @@ -131,7 +139,6 @@ public class Settings extends SettingsActivity { public static class ChangeWifiStateActivity extends SettingsActivity { /* empty */ } public static class AppDrawOverlaySettingsActivity extends SettingsActivity { /* empty */ } public static class AppWriteSettingsActivity extends SettingsActivity { /* empty */ } - public static class AdvancedAppsActivity extends SettingsActivity { /* empty */ } public static class ManageExternalSourcesActivity extends SettingsActivity {/* empty */ } public static class ManageAppExternalSourcesActivity extends SettingsActivity { /* empty */ } @@ -156,7 +163,10 @@ public class Settings extends SettingsActivity { } public static class WebViewAppPickerActivity extends SettingsActivity { /* empty */ } public static class AdvancedConnectedDeviceActivity extends SettingsActivity { /* empty */ } + public static class BluetoothDeviceDetailActivity extends SettingsActivity { /* empty */ } public static class WifiCallingDisclaimerActivity extends SettingsActivity { /* empty */ } + public static class MobileNetworkListActivity extends SettingsActivity {} + public static class GlobalActionsPanelSettingsActivity extends SettingsActivity {} // Top level categories for new IA public static class NetworkDashboardActivity extends SettingsActivity {} diff --git a/src/com/android/settings/SettingsActivity.java b/src/com/android/settings/SettingsActivity.java index 644bcee52b..3b01b327ea 100644 --- a/src/com/android/settings/SettingsActivity.java +++ b/src/com/android/settings/SettingsActivity.java @@ -16,13 +16,8 @@ package com.android.settings; -import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO; - import android.app.ActionBar; import android.app.ActivityManager; -import android.app.Fragment; -import android.app.FragmentManager; -import android.app.FragmentTransaction; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; @@ -32,54 +27,53 @@ import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.drawable.Drawable; +import android.content.res.Resources.Theme; import android.os.AsyncTask; import android.os.Bundle; import android.os.UserHandle; import android.os.UserManager; -import androidx.annotation.VisibleForTesting; -import androidx.preference.PreferenceFragment; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import androidx.preference.Preference; -import androidx.preference.PreferenceManager; import android.text.TextUtils; -import android.transition.TransitionManager; -import android.util.FeatureFlagUtils; import android.util.Log; import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; import android.widget.Button; -import android.widget.Toolbar; + +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import androidx.preference.Preference; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceManager; import com.android.internal.util.ArrayUtils; import com.android.settings.Settings.WifiSettingsActivity; import com.android.settings.applications.manageapplications.ManageApplications; -import com.android.settings.backup.BackupSettingsActivity; -import com.android.settings.core.FeatureFlags; +import com.android.settings.backup.UserBackupSettingsActivity; +import com.android.settings.core.OnActivityResultListener; +import com.android.settings.core.SettingsBaseActivity; import com.android.settings.core.SubSettingLauncher; import com.android.settings.core.gateway.SettingsGateway; import com.android.settings.dashboard.DashboardFeatureProvider; -import com.android.settings.dashboard.DashboardSummary; +import com.android.settings.homepage.TopLevelSettings; import com.android.settings.overlay.FeatureFactory; -import com.android.settings.search.DeviceIndexFeatureProvider; import com.android.settings.wfd.WifiDisplaySettings; import com.android.settings.widget.SwitchBar; import com.android.settingslib.core.instrumentation.Instrumentable; import com.android.settingslib.core.instrumentation.SharedPreferencesLogger; import com.android.settingslib.development.DevelopmentSettingsEnabler; import com.android.settingslib.drawer.DashboardCategory; -import com.android.settingslib.drawer.SettingsDrawerActivity; -import com.android.settingslib.utils.ThreadUtils; + +import com.google.android.setupcompat.util.WizardManagerHelper; import java.util.ArrayList; import java.util.List; -public class SettingsActivity extends SettingsDrawerActivity + +public class SettingsActivity extends SettingsBaseActivity implements PreferenceManager.OnPreferenceTreeClickListener, - PreferenceFragment.OnPreferenceStartFragmentCallback, + PreferenceFragmentCompat.OnPreferenceStartFragmentCallback, ButtonBarHandler, FragmentManager.OnBackStackChangedListener { private static final String LOG_TAG = "SettingsActivity"; @@ -109,8 +103,6 @@ public class SettingsActivity extends SettingsDrawerActivity */ public static final String EXTRA_FRAGMENT_ARG_KEY = ":settings:fragment_args_key"; - public static final String BACK_STACK_PREFS = ":settings:prefs"; - // extras that allow any preference activity to be launched as part of a wizard // show Back and Next buttons? takes boolean parameter @@ -138,15 +130,10 @@ public class SettingsActivity extends SettingsDrawerActivity ":settings:show_fragment_title_res_package_name"; public static final String EXTRA_SHOW_FRAGMENT_TITLE_RESID = ":settings:show_fragment_title_resid"; - public static final String EXTRA_SHOW_FRAGMENT_AS_SHORTCUT = - ":settings:show_fragment_as_shortcut"; public static final String EXTRA_SHOW_FRAGMENT_AS_SUBSETTING = ":settings:show_fragment_as_subsetting"; - @Deprecated - public static final String EXTRA_HIDE_DRAWER = ":settings:hide_drawer"; - public static final String META_DATA_KEY_FRAGMENT_CLASS = "com.android.settings.FRAGMENT_CLASS"; @@ -179,10 +166,6 @@ public class SettingsActivity extends SettingsDrawerActivity private Button mNextButton; - private boolean mIsShowingDashboard; - - private ViewGroup mContent; - // Categories private ArrayList<DashboardCategory> mCategories = new ArrayList<>(); @@ -193,14 +176,14 @@ public class SettingsActivity extends SettingsDrawerActivity } @Override - public boolean onPreferenceStartFragment(PreferenceFragment caller, Preference pref) { + public boolean onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref) { new SubSettingLauncher(this) .setDestination(pref.getFragment()) .setArguments(pref.getExtras()) .setSourceMetricsCategory(caller instanceof Instrumentable ? ((Instrumentable) caller).getMetricsCategory() : Instrumentable.METRICS_CATEGORY_UNKNOWN) - .setTitle(-1) + .setTitleRes(-1) .launch(); return true; } @@ -251,11 +234,6 @@ public class SettingsActivity extends SettingsDrawerActivity // Getting Intent properties can only be done after the super.onCreate(...) final String initialFragmentName = intent.getStringExtra(EXTRA_SHOW_FRAGMENT); - final ComponentName cn = intent.getComponent(); - final String className = cn.getClassName(); - - mIsShowingDashboard = className.equals(Settings.class.getName()); - // This is a "Sub Settings" when: // - this is a real SubSettings // - or :settings:show_fragment_as_subsetting is passed to the Intent @@ -263,17 +241,16 @@ public class SettingsActivity extends SettingsDrawerActivity intent.getBooleanExtra(EXTRA_SHOW_FRAGMENT_AS_SUBSETTING, false); // If this is a sub settings, then apply the SubSettings Theme for the ActionBar content - // insets - if (isSubSettings) { + // insets. + // If this is in setup flow, don't apply theme. Because light theme needs to be applied + // in SettingsBaseActivity#onCreate(). + if (isSubSettings && !WizardManagerHelper.isAnySetupWizard(getIntent())) { setTheme(R.style.Theme_SubSettings); } - setContentView(mIsShowingDashboard ? - R.layout.settings_main_dashboard : R.layout.settings_main_prefs); - - mContent = findViewById(R.id.main_content); + setContentView(R.layout.settings_main_prefs); - getFragmentManager().addOnBackStackChangedListener(this); + getSupportFragmentManager().addOnBackStackChangedListener(this); if (savedState != null) { // We are restarting from a previous saved state; used that to initialize, instead @@ -288,34 +265,16 @@ public class SettingsActivity extends SettingsDrawerActivity setTitleFromBackStack(); } } else { - launchSettingFragment(initialFragmentName, isSubSettings, intent); + launchSettingFragment(initialFragmentName, intent); } final boolean deviceProvisioned = Utils.isDeviceProvisioned(this); - if (mIsShowingDashboard) { - findViewById(R.id.search_bar).setVisibility( - deviceProvisioned ? View.VISIBLE : View.INVISIBLE); - findViewById(R.id.action_bar).setVisibility(View.GONE); - final Toolbar toolbar = findViewById(R.id.search_action_bar); - FeatureFactory.getFactory(this).getSearchFeatureProvider() - .initSearchToolbar(this, toolbar); - setActionBar(toolbar); - - // Please forgive me for what I am about to do. - // - // Need to make the navigation icon non-clickable so that the entire card is clickable - // and goes to the search UI. Also set the background to null so there's no ripple. - View navView = toolbar.getNavigationView(); - navView.setClickable(false); - navView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); - navView.setBackground(null); - } - ActionBar actionBar = getActionBar(); + final ActionBar actionBar = getActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(deviceProvisioned); actionBar.setHomeButtonEnabled(deviceProvisioned); - actionBar.setDisplayShowTitleEnabled(!mIsShowingDashboard); + actionBar.setDisplayShowTitleEnabled(true); } mSwitchBar = findViewById(R.id.switch_bar); if (mSwitchBar != null) { @@ -329,26 +288,20 @@ public class SettingsActivity extends SettingsDrawerActivity if (buttonBar != null) { buttonBar.setVisibility(View.VISIBLE); - Button backButton = (Button) findViewById(R.id.back_button); - backButton.setOnClickListener(new OnClickListener() { - public void onClick(View v) { - setResult(RESULT_CANCELED, null); - finish(); - } + Button backButton = findViewById(R.id.back_button); + backButton.setOnClickListener(v -> { + setResult(RESULT_CANCELED, null); + finish(); }); - Button skipButton = (Button) findViewById(R.id.skip_button); - skipButton.setOnClickListener(new OnClickListener() { - public void onClick(View v) { - setResult(RESULT_OK, null); - finish(); - } + Button skipButton = findViewById(R.id.skip_button); + skipButton.setOnClickListener(v -> { + setResult(RESULT_OK, null); + finish(); }); - mNextButton = (Button) findViewById(R.id.next_button); - mNextButton.setOnClickListener(new OnClickListener() { - public void onClick(View v) { - setResult(RESULT_OK, null); - finish(); - } + mNextButton = findViewById(R.id.next_button); + mNextButton.setOnClickListener(v -> { + setResult(RESULT_OK, null); + finish(); }); // set our various button parameters @@ -379,20 +332,38 @@ public class SettingsActivity extends SettingsDrawerActivity } } + @Override + protected void onApplyThemeResource(Theme theme, int resid, boolean first) { + theme.applyStyle(R.style.SetupWizardPartnerResource, true); + super.onApplyThemeResource(theme, resid, first); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + final List<Fragment> fragments = getSupportFragmentManager().getFragments(); + if (fragments != null) { + for (Fragment fragment : fragments) { + if (fragment instanceof OnActivityResultListener) { + fragment.onActivityResult(requestCode, resultCode, data); + } + } + } + } + @VisibleForTesting - void launchSettingFragment(String initialFragmentName, boolean isSubSettings, Intent intent) { - if (!mIsShowingDashboard && initialFragmentName != null) { + void launchSettingFragment(String initialFragmentName, Intent intent) { + if (initialFragmentName != null) { setTitleFromIntent(intent); Bundle initialArguments = intent.getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS); - switchToFragment(initialFragmentName, initialArguments, true, false, - mInitialTitleResId, mInitialTitle, false); + switchToFragment(initialFragmentName, initialArguments, true, + mInitialTitleResId, mInitialTitle); } else { // Show search icon as up affordance if we are displaying the main Dashboard mInitialTitleResId = R.string.dashboard_title; - - switchToFragment(DashboardSummary.class.getName(), null /* args */, false, false, - mInitialTitleResId, mInitialTitle, false); + switchToFragment(TopLevelSettings.class.getName(), null /* args */, false, + mInitialTitleResId, mInitialTitle); } } @@ -434,7 +405,7 @@ public class SettingsActivity extends SettingsDrawerActivity } private void setTitleFromBackStack() { - final int count = getFragmentManager().getBackStackEntryCount(); + final int count = getSupportFragmentManager().getBackStackEntryCount(); if (count == 0) { if (mInitialTitleResId > 0) { @@ -445,7 +416,8 @@ public class SettingsActivity extends SettingsDrawerActivity return; } - FragmentManager.BackStackEntry bse = getFragmentManager().getBackStackEntryAt(count - 1); + FragmentManager.BackStackEntry bse = getSupportFragmentManager(). + getBackStackEntryAt(count - 1); setTitleFromBackStackEntry(bse); } @@ -494,7 +466,6 @@ public class SettingsActivity extends SettingsDrawerActivity registerReceiver(mBatteryInfoReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); updateTilesList(); - updateDeviceIndex(); } @Override @@ -507,8 +478,7 @@ public class SettingsActivity extends SettingsDrawerActivity @Override public void setTaskDescription(ActivityManager.TaskDescription taskDescription) { - final Bitmap icon = getBitmapFromXmlResource(R.drawable.ic_launcher_settings); - taskDescription.setIcon(icon); + taskDescription.setIcon(R.drawable.ic_launcher_settings); super.setTaskDescription(taskDescription); } @@ -579,28 +549,22 @@ public class SettingsActivity extends SettingsDrawerActivity * Switch to a specific Fragment with taking care of validation, Title and BackStack */ private Fragment switchToFragment(String fragmentName, Bundle args, boolean validate, - boolean addToBackStack, int titleResId, CharSequence title, boolean withTransition) { + int titleResId, CharSequence title) { Log.d(LOG_TAG, "Switching to fragment " + fragmentName); if (validate && !isValidFragment(fragmentName)) { throw new IllegalArgumentException("Invalid fragment for this activity: " + fragmentName); } Fragment f = Fragment.instantiate(this, fragmentName, args); - FragmentTransaction transaction = getFragmentManager().beginTransaction(); + FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); transaction.replace(R.id.main_content, f); - if (withTransition) { - TransitionManager.beginDelayedTransition(mContent); - } - if (addToBackStack) { - transaction.addToBackStack(SettingsActivity.BACK_STACK_PREFS); - } if (titleResId > 0) { transaction.setBreadCrumbTitle(titleResId); } else if (title != null) { transaction.setBreadCrumbTitle(title); } transaction.commitAllowingStateLoss(); - getFragmentManager().executePendingTransactions(); + getSupportFragmentManager().executePendingTransactions(); Log.d(LOG_TAG, "Executed frag manager pendingTransactions"); return f; } @@ -608,7 +572,7 @@ public class SettingsActivity extends SettingsDrawerActivity private void updateTilesList() { // Generally the items that are will be changing from these updates will // not be in the top list of tiles, so run it in the background and the - // SettingsDrawerActivity will pick up on the updates automatically. + // SettingsBaseActivity will pick up on the updates automatically. AsyncTask.execute(new Runnable() { @Override public void run() { @@ -617,19 +581,10 @@ public class SettingsActivity extends SettingsDrawerActivity }); } - private void updateDeviceIndex() { - DeviceIndexFeatureProvider indexProvider = FeatureFactory.getFactory( - this).getDeviceIndexFeatureProvider(); - - ThreadUtils.postOnBackgroundThread( - () -> indexProvider.updateIndex(SettingsActivity.this, false /* force */)); - } - private void doUpdateTilesList() { PackageManager pm = getPackageManager(); final UserManager um = UserManager.get(this); final boolean isAdmin = um.isAdminUser(); - final FeatureFactory featureFactory = FeatureFactory.getFactory(this); boolean somethingChanged = false; final String packageName = getPackageName(); final StringBuilder changedList = new StringBuilder(); @@ -642,7 +597,6 @@ public class SettingsActivity extends SettingsDrawerActivity pm.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH), isAdmin) || somethingChanged; - // Enable DataUsageSummaryActivity if the data plan feature flag is turned on otherwise // enable DataPlanUsageSummaryActivity. somethingChanged = setTileEnabled(changedList, @@ -657,25 +611,12 @@ public class SettingsActivity extends SettingsDrawerActivity isAdmin) || somethingChanged; somethingChanged = setTileEnabled(changedList, new ComponentName(packageName, - Settings.SimSettingsActivity.class.getName()), - Utils.showSimCardTile(this), isAdmin) - || somethingChanged; - - somethingChanged = setTileEnabled(changedList, new ComponentName(packageName, Settings.PowerUsageSummaryActivity.class.getName()), mBatteryPresent, isAdmin) || somethingChanged; - final boolean isDataUsageSettingsV2Enabled = - FeatureFlagUtils.isEnabled(this, FeatureFlags.DATA_USAGE_SETTINGS_V2); - // Enable new data usage page if v2 enabled somethingChanged = setTileEnabled(changedList, new ComponentName(packageName, Settings.DataUsageSummaryActivity.class.getName()), - Utils.isBandwidthControlEnabled() && isDataUsageSettingsV2Enabled, isAdmin) - || somethingChanged; - // Enable legacy data usage page if v2 disabled - somethingChanged = setTileEnabled(changedList, new ComponentName(packageName, - Settings.DataUsageSummaryLegacyActivity.class.getName()), - Utils.isBandwidthControlEnabled() && !isDataUsageSettingsV2Enabled, isAdmin) + Utils.isBandwidthControlEnabled(), isAdmin) || somethingChanged; somethingChanged = setTileEnabled(changedList, new ComponentName(packageName, @@ -684,27 +625,15 @@ public class SettingsActivity extends SettingsDrawerActivity && !Utils.isMonkeyRunning(), isAdmin) || somethingChanged; - somethingChanged = setTileEnabled(changedList, new ComponentName(packageName, - Settings.NetworkDashboardActivity.class.getName()), - !UserManager.isDeviceInDemoMode(this), isAdmin) - || somethingChanged; - - somethingChanged = setTileEnabled(changedList, new ComponentName(packageName, - Settings.DateTimeSettingsActivity.class.getName()), - !UserManager.isDeviceInDemoMode(this), isAdmin) - || somethingChanged; - final boolean showDev = DevelopmentSettingsEnabler.isDevelopmentSettingsEnabled(this) && !Utils.isMonkeyRunning(); - final boolean isAdminOrDemo = um.isAdminUser() || um.isDemoUser(); somethingChanged = setTileEnabled(changedList, new ComponentName(packageName, Settings.DevelopmentSettingsDashboardActivity.class.getName()), - showDev, isAdminOrDemo) + showDev, isAdmin) || somethingChanged; - // Enable/disable backup settings depending on whether the user is admin. somethingChanged = setTileEnabled(changedList, new ComponentName(packageName, - BackupSettingsActivity.class.getName()), true, isAdmin) + UserBackupSettingsActivity.class.getName()), true, isAdmin) || somethingChanged; somethingChanged = setTileEnabled(changedList, new ComponentName(packageName, @@ -712,19 +641,6 @@ public class SettingsActivity extends SettingsDrawerActivity WifiDisplaySettings.isAvailable(this), isAdmin) || somethingChanged; - // Enable/disable the Me Card page. - final boolean aboutPhoneV2Enabled = featureFactory - .getAccountFeatureProvider() - .isAboutPhoneV2Enabled(this); - somethingChanged = setTileEnabled(changedList, new ComponentName(packageName, - Settings.MyDeviceInfoActivity.class.getName()), - aboutPhoneV2Enabled, isAdmin) - || somethingChanged; - somethingChanged = setTileEnabled(changedList, new ComponentName(packageName, - Settings.DeviceInfoSettingsActivity.class.getName()), - !aboutPhoneV2Enabled, isAdmin) - || somethingChanged; - if (UserHandle.MU_ENABLED && !isAdmin) { // When on restricted users, disable all extra categories (but only the settings ones). @@ -733,12 +649,11 @@ public class SettingsActivity extends SettingsDrawerActivity for (DashboardCategory category : categories) { final int tileCount = category.getTilesCount(); for (int i = 0; i < tileCount; i++) { - final ComponentName component = category.getTile(i).intent.getComponent(); + final ComponentName component = category.getTile(i) + .getIntent().getComponent(); final String name = component.getClassName(); final boolean isEnabledForRestricted = ArrayUtils.contains( - SettingsGateway.SETTINGS_FOR_RESTRICTED, name) || (isAdminOrDemo - && Settings.DevelopmentSettingsDashboardActivity.class.getName() - .equals(name)); + SettingsGateway.SETTINGS_FOR_RESTRICTED, name); if (packageName.equals(component.getPackageName()) && !isEnabledForRestricted) { somethingChanged = @@ -797,17 +712,4 @@ public class SettingsActivity extends SettingsDrawerActivity public Button getNextButton() { return mNextButton; } - - @VisibleForTesting - Bitmap getBitmapFromXmlResource(int drawableRes) { - Drawable drawable = getResources().getDrawable(drawableRes, getTheme()); - Canvas canvas = new Canvas(); - Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), - drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); - canvas.setBitmap(bitmap); - drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); - drawable.draw(canvas); - - return bitmap; - } } diff --git a/src/com/android/settings/SettingsDumpService.java b/src/com/android/settings/SettingsDumpService.java index 07ea73e57e..a57e9832d0 100644 --- a/src/com/android/settings/SettingsDumpService.java +++ b/src/com/android/settings/SettingsDumpService.java @@ -30,7 +30,8 @@ import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; -import com.android.internal.annotations.VisibleForTesting; +import androidx.annotation.VisibleForTesting; + import com.android.settings.applications.ProcStatsData; import com.android.settings.fuelgauge.batterytip.AnomalyConfigJobService; import com.android.settingslib.net.DataUsageController; diff --git a/src/com/android/settings/SettingsInitialize.java b/src/com/android/settings/SettingsInitialize.java index 7272399247..515703358e 100644 --- a/src/com/android/settings/SettingsInitialize.java +++ b/src/com/android/settings/SettingsInitialize.java @@ -16,6 +16,13 @@ package com.android.settings; +import static android.content.pm.PackageManager.GET_ACTIVITIES; +import static android.content.pm.PackageManager.GET_META_DATA; +import static android.content.pm.PackageManager.GET_RESOLVED_FILTER; +import static android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS; + +import static com.android.settings.Utils.SETTINGS_PACKAGE_NAME; + import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; @@ -27,18 +34,14 @@ import android.content.pm.ShortcutManager; import android.content.pm.UserInfo; import android.os.UserHandle; import android.os.UserManager; -import androidx.annotation.VisibleForTesting; import android.util.Log; -import java.util.ArrayList; -import java.util.List; +import androidx.annotation.VisibleForTesting; -import static android.content.pm.PackageManager.GET_ACTIVITIES; -import static android.content.pm.PackageManager.GET_META_DATA; -import static android.content.pm.PackageManager.GET_RESOLVED_FILTER; -import static android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS; +import com.android.settings.Settings.CreateShortcutActivity; -import com.android.settings.shortcut.CreateShortcut; +import java.util.ArrayList; +import java.util.List; /** * Listens to {@link Intent.ACTION_PRE_BOOT_COMPLETED} and {@link Intent.ACTION_USER_INITIALIZED} @@ -50,14 +53,13 @@ public class SettingsInitialize extends BroadcastReceiver { private static final String TAG = "Settings"; private static final String PRIMARY_PROFILE_SETTING = "com.android.settings.PRIMARY_PROFILE_CONTROLLED"; - private static final String SETTINGS_PACKAGE = "com.android.settings"; private static final String WEBVIEW_IMPLEMENTATION_ACTIVITY = ".WebViewImplementation"; @Override public void onReceive(Context context, Intent broadcast) { final UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE); UserInfo userInfo = um.getUserInfo(UserHandle.myUserId()); - final PackageManager pm = context.getPackageManager(); + final PackageManager pm = context.getPackageManager(); managedProfileSetup(context, pm, broadcast, userInfo); webviewSettingSetup(context, pm, userInfo); refreshExistingShortcuts(context); @@ -90,7 +92,7 @@ public class SettingsInitialize extends BroadcastReceiver { PRIMARY_PROFILE_SETTING); if (shouldForward) { pm.addCrossProfileIntentFilter(info.filter, userInfo.id, - userInfo.profileGroupId, PackageManager.SKIP_CURRENT_PROFILE); + userInfo.profileGroupId, PackageManager.SKIP_CURRENT_PROFILE); } } } @@ -100,7 +102,8 @@ public class SettingsInitialize extends BroadcastReceiver { pm.setComponentEnabledSetting(settingsComponentName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); // Disable shortcut picker. - ComponentName shortcutComponentName = new ComponentName(context, CreateShortcut.class); + ComponentName shortcutComponentName = new ComponentName( + context, CreateShortcutActivity.class); pm.setComponentEnabledSetting(shortcutComponentName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); } @@ -111,7 +114,8 @@ public class SettingsInitialize extends BroadcastReceiver { return; } ComponentName settingsComponentName = - new ComponentName(SETTINGS_PACKAGE, SETTINGS_PACKAGE + WEBVIEW_IMPLEMENTATION_ACTIVITY); + new ComponentName(SETTINGS_PACKAGE_NAME, + SETTINGS_PACKAGE_NAME + WEBVIEW_IMPLEMENTATION_ACTIVITY); pm.setComponentEnabledSetting(settingsComponentName, userInfo.isAdmin() ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : @@ -126,6 +130,9 @@ public class SettingsInitialize extends BroadcastReceiver { final List<ShortcutInfo> pinnedShortcuts = shortcutManager.getPinnedShortcuts(); final List<ShortcutInfo> updates = new ArrayList<>(); for (ShortcutInfo info : pinnedShortcuts) { + if (info.isImmutable()) { + continue; + } final Intent shortcutIntent = info.getIntent(); shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); final ShortcutInfo updatedInfo = new ShortcutInfo.Builder(context, info.getId()) diff --git a/src/com/android/settings/SettingsLicenseActivity.java b/src/com/android/settings/SettingsLicenseActivity.java index 300644ccb7..6def6faebd 100644 --- a/src/com/android/settings/SettingsLicenseActivity.java +++ b/src/com/android/settings/SettingsLicenseActivity.java @@ -16,29 +16,29 @@ package com.android.settings; -import android.app.Activity; -import android.app.LoaderManager; import android.content.ActivityNotFoundException; import android.content.ContentResolver; import android.content.Intent; -import android.content.Loader; import android.net.Uri; import android.os.Bundle; -import androidx.annotation.VisibleForTesting; -import androidx.core.content.FileProvider; -import android.text.TextUtils; import android.util.Log; import android.widget.Toast; +import androidx.annotation.VisibleForTesting; +import androidx.core.content.FileProvider; +import androidx.fragment.app.FragmentActivity; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.Loader; + import com.android.settings.users.RestrictedProfileSettings; -import com.android.settingslib.license.LicenseHtmlLoader; +import com.android.settingslib.license.LicenseHtmlLoaderCompat; import java.io.File; /** * The "dialog" that shows from "License" in the Settings app. */ -public class SettingsLicenseActivity extends Activity implements +public class SettingsLicenseActivity extends FragmentActivity implements LoaderManager.LoaderCallbacks<File> { private static final String TAG = "SettingsLicenseActivity"; @@ -60,7 +60,7 @@ public class SettingsLicenseActivity extends Activity implements @Override public Loader<File> onCreateLoader(int id, Bundle args) { - return new LicenseHtmlLoader(this); + return new LicenseHtmlLoaderCompat(this); } @Override @@ -73,7 +73,7 @@ public class SettingsLicenseActivity extends Activity implements } private void showHtmlFromDefaultXmlFiles() { - getLoaderManager().initLoader(LOADER_ID_LICENSE_HTML_LOADER, Bundle.EMPTY, this); + getSupportLoaderManager().initLoader(LOADER_ID_LICENSE_HTML_LOADER, Bundle.EMPTY, this); } @VisibleForTesting diff --git a/src/com/android/settings/SettingsPreferenceFragment.java b/src/com/android/settings/SettingsPreferenceFragment.java index 4a8a6e228b..6b29b2e1e4 100644 --- a/src/com/android/settings/SettingsPreferenceFragment.java +++ b/src/com/android/settings/SettingsPreferenceFragment.java @@ -18,21 +18,12 @@ package com.android.settings; import android.app.Activity; import android.app.Dialog; -import android.app.DialogFragment; -import android.app.Fragment; import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; import android.os.Bundle; -import androidx.annotation.VisibleForTesting; -import androidx.annotation.XmlRes; -import androidx.preference.Preference; -import androidx.preference.PreferenceGroup; -import androidx.preference.PreferenceScreen; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; import android.text.TextUtils; import android.util.ArrayMap; import android.util.Log; @@ -41,18 +32,29 @@ import android.view.View; import android.view.ViewGroup; import android.widget.Button; -import com.android.settings.applications.LayoutPreference; +import androidx.annotation.VisibleForTesting; +import androidx.annotation.XmlRes; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; +import androidx.preference.Preference; +import androidx.preference.PreferenceGroup; +import androidx.preference.PreferenceScreen; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + import com.android.settings.core.InstrumentedPreferenceFragment; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; +import com.android.settings.search.Indexable; import com.android.settings.search.actionbar.SearchMenuController; import com.android.settings.support.actionbar.HelpMenuController; import com.android.settings.support.actionbar.HelpResourceProvider; import com.android.settings.widget.HighlightablePreferenceGroupAdapter; import com.android.settings.widget.LoadingViewController; -import com.android.settingslib.CustomDialogPreference; -import com.android.settingslib.CustomEditTextPreference; +import com.android.settingslib.CustomDialogPreferenceCompat; +import com.android.settingslib.CustomEditTextPreferenceCompat; import com.android.settingslib.core.instrumentation.Instrumentable; -import com.android.settingslib.widget.FooterPreferenceMixin; +import com.android.settingslib.widget.FooterPreferenceMixinCompat; +import com.android.settingslib.widget.LayoutPreference; import java.util.UUID; @@ -60,14 +62,14 @@ import java.util.UUID; * Base class for Settings fragments, with some helper functions and dialog management. */ public abstract class SettingsPreferenceFragment extends InstrumentedPreferenceFragment - implements DialogCreatable, HelpResourceProvider { + implements DialogCreatable, HelpResourceProvider, Indexable { private static final String TAG = "SettingsPreference"; private static final String SAVE_HIGHLIGHTED_KEY = "android:preference_highlighted"; - protected final FooterPreferenceMixin mFooterPreferenceMixin = - new FooterPreferenceMixin(this, getLifecycle()); + protected final FooterPreferenceMixinCompat mFooterPreferenceMixin = + new FooterPreferenceMixinCompat(this, getSettingsLifecycle()); private static final int ORDER_FIRST = -1; @@ -153,13 +155,14 @@ public abstract class SettingsPreferenceFragment extends InstrumentedPreferenceF checkAvailablePrefs(getPreferenceScreen()); } - private void checkAvailablePrefs(PreferenceGroup preferenceGroup) { + @VisibleForTesting + void checkAvailablePrefs(PreferenceGroup preferenceGroup) { if (preferenceGroup == null) return; for (int i = 0; i < preferenceGroup.getPreferenceCount(); i++) { Preference pref = preferenceGroup.getPreference(i); if (pref instanceof SelfAvailablePreference && !((SelfAvailablePreference) pref).isAvailable(getContext())) { - preferenceGroup.removePreference(pref); + pref.setVisible(false); } else if (pref instanceof PreferenceGroup) { checkAvailablePrefs((PreferenceGroup) pref); } @@ -452,7 +455,7 @@ public abstract class SettingsPreferenceFragment extends InstrumentedPreferenceF if (mDialogFragment != null) { Log.e(TAG, "Old dialog fragment not null!"); } - mDialogFragment = new SettingsDialogFragment(this, dialogId); + mDialogFragment = SettingsDialogFragment.newInstance(this, dialogId); mDialogFragment.show(getChildFragmentManager(), Integer.toString(dialogId)); } @@ -515,11 +518,11 @@ public abstract class SettingsPreferenceFragment extends InstrumentedPreferenceF } else if (preference instanceof CustomListPreference) { f = CustomListPreference.CustomListPreferenceDialogFragment .newInstance(preference.getKey()); - } else if (preference instanceof CustomDialogPreference) { - f = CustomDialogPreference.CustomPreferenceDialogFragment + } else if (preference instanceof CustomDialogPreferenceCompat) { + f = CustomDialogPreferenceCompat.CustomPreferenceDialogFragment .newInstance(preference.getKey()); - } else if (preference instanceof CustomEditTextPreference) { - f = CustomEditTextPreference.CustomPreferenceDialogFragment + } else if (preference instanceof CustomEditTextPreferenceCompat) { + f = CustomEditTextPreferenceCompat.CustomPreferenceDialogFragment .newInstance(preference.getKey()); } else { super.onDisplayPreferenceDialog(preference); @@ -539,22 +542,26 @@ public abstract class SettingsPreferenceFragment extends InstrumentedPreferenceF private DialogInterface.OnCancelListener mOnCancelListener; private DialogInterface.OnDismissListener mOnDismissListener; - public SettingsDialogFragment(DialogCreatable fragment, int dialogId) { - super(fragment, dialogId); + public static SettingsDialogFragment newInstance(DialogCreatable fragment, int dialogId) { if (!(fragment instanceof Fragment)) { throw new IllegalArgumentException("fragment argument must be an instance of " + Fragment.class.getName()); } - mParentFragment = (Fragment) fragment; - } + final SettingsDialogFragment settingsDialogFragment = new SettingsDialogFragment(); + settingsDialogFragment.setParentFragment(fragment); + settingsDialogFragment.setDialogId(dialogId); + + return settingsDialogFragment; + } @Override public int getMetricsCategory() { - if (mDialogCreatable == null) { + if (mParentFragment == null) { return Instrumentable.METRICS_CATEGORY_UNKNOWN; } - final int metricsCategory = mDialogCreatable.getDialogMetricsCategory(mDialogId); + final int metricsCategory = + ((DialogCreatable) mParentFragment).getDialogMetricsCategory(mDialogId); if (metricsCategory <= 0) { throw new IllegalStateException("Dialog must provide a metrics category"); } @@ -637,6 +644,14 @@ public abstract class SettingsPreferenceFragment extends InstrumentedPreferenceF } } } + + private void setParentFragment(DialogCreatable fragment) { + mParentFragment = (Fragment) fragment; + } + + private void setDialogId(int dialogId) { + mDialogId = dialogId; + } } protected boolean hasNextButton() { diff --git a/src/com/android/settings/SettingsTutorialDialogWrapperActivity.java b/src/com/android/settings/SettingsTutorialDialogWrapperActivity.java new file mode 100644 index 0000000000..50b966fb21 --- /dev/null +++ b/src/com/android/settings/SettingsTutorialDialogWrapperActivity.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2019 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.os.Bundle; + +import com.android.settings.accessibility.AccessibilityGestureNavigationTutorial; +import com.android.settings.R; + +/** + * This activity is to create the tutorial dialog in gesture navigation settings since we couldn't + * use the dialog utils because SystemNavigationGestureSettings extends RadioButtonPickerFragment, + * not SettingsPreferenceFragment. + */ +public class SettingsTutorialDialogWrapperActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + showDialog(); + } + + private void showDialog() { + AccessibilityGestureNavigationTutorial + .showGestureNavigationSettingsTutorialDialog(this, dialog -> finish()); + } +} diff --git a/src/com/android/settings/SetupEncryptionInterstitial.java b/src/com/android/settings/SetupEncryptionInterstitial.java index 989cc2b12c..d9e265f991 100644 --- a/src/com/android/settings/SetupEncryptionInterstitial.java +++ b/src/com/android/settings/SetupEncryptionInterstitial.java @@ -18,8 +18,6 @@ package com.android.settings; import android.content.Context; import android.content.Intent; -import android.os.Bundle; -import android.widget.LinearLayout; /** * Setup Wizard's version of EncryptionInterstitial screen. It inherits the logic and basic @@ -53,13 +51,6 @@ public class SetupEncryptionInterstitial extends EncryptionInterstitial { return SetupEncryptionInterstitialFragment.class.getName().equals(fragmentName); } - @Override - protected void onCreate(Bundle savedInstance) { - super.onCreate(savedInstance); - LinearLayout layout = (LinearLayout) findViewById(R.id.content_parent); - layout.setFitsSystemWindows(false); - } - public static class SetupEncryptionInterstitialFragment extends EncryptionInterstitialFragment { } } diff --git a/src/com/android/settings/SetupRedactionInterstitial.java b/src/com/android/settings/SetupRedactionInterstitial.java index ac1db15bdc..90f6c21dab 100644 --- a/src/com/android/settings/SetupRedactionInterstitial.java +++ b/src/com/android/settings/SetupRedactionInterstitial.java @@ -20,7 +20,6 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; -import android.content.res.Resources; import com.android.settings.notification.RedactionInterstitial; diff --git a/src/com/android/settings/SetupWizardUtils.java b/src/com/android/settings/SetupWizardUtils.java index e449d52813..bce6f3f291 100644 --- a/src/com/android/settings/SetupWizardUtils.java +++ b/src/com/android/settings/SetupWizardUtils.java @@ -16,62 +16,94 @@ package com.android.settings; +import static com.google.android.setupcompat.util.WizardManagerHelper.EXTRA_IS_FIRST_RUN; +import static com.google.android.setupcompat.util.WizardManagerHelper.EXTRA_IS_SETUP_FLOW; + import android.content.Intent; -import android.os.SystemProperties; -import androidx.annotation.VisibleForTesting; +import android.os.Bundle; +import android.sysprop.SetupWizardProperties; -import com.android.setupwizardlib.util.WizardManagerHelper; +import com.google.android.setupcompat.util.WizardManagerHelper; +import com.google.android.setupdesign.util.ThemeHelper; -public class SetupWizardUtils { +import java.util.Arrays; - @VisibleForTesting - static final String SYSTEM_PROP_SETUPWIZARD_THEME = "setupwizard.theme"; - public static int getTheme(Intent intent) { +public class SetupWizardUtils { + + public static String getThemeString(Intent intent) { String theme = intent.getStringExtra(WizardManagerHelper.EXTRA_THEME); if (theme == null) { - theme = SystemProperties.get(SYSTEM_PROP_SETUPWIZARD_THEME); + theme = SetupWizardProperties.theme().orElse(""); } + return theme; + } + + public static int getTheme(Intent intent) { + String theme = getThemeString(intent); + // TODO(yukl): Move to ThemeResolver and add any additional required attributes in + // onApplyThemeResource using Theme overlays if (theme != null) { - switch (theme) { - case WizardManagerHelper.THEME_GLIF_V3_LIGHT: - return R.style.GlifV3Theme_Light; - case WizardManagerHelper.THEME_GLIF_V3: - return R.style.GlifV3Theme; - case WizardManagerHelper.THEME_GLIF_V2_LIGHT: - return R.style.GlifV2Theme_Light; - case WizardManagerHelper.THEME_GLIF_V2: - return R.style.GlifV2Theme; - case WizardManagerHelper.THEME_GLIF_LIGHT: - return R.style.GlifTheme_Light; - case WizardManagerHelper.THEME_GLIF: - return R.style.GlifTheme; + if (WizardManagerHelper.isAnySetupWizard(intent)) { + switch (theme) { + case ThemeHelper.THEME_GLIF_V3_LIGHT: + return R.style.GlifV3Theme_Light; + case ThemeHelper.THEME_GLIF_V3: + return R.style.GlifV3Theme; + case ThemeHelper.THEME_GLIF_V2_LIGHT: + return R.style.GlifV2Theme_Light; + case ThemeHelper.THEME_GLIF_V2: + return R.style.GlifV2Theme; + case ThemeHelper.THEME_GLIF_LIGHT: + return R.style.GlifTheme_Light; + case ThemeHelper.THEME_GLIF: + return R.style.GlifTheme; + } + } else { + switch (theme) { + case ThemeHelper.THEME_GLIF_V3_LIGHT: + case ThemeHelper.THEME_GLIF_V3: + return R.style.GlifV3Theme; + case ThemeHelper.THEME_GLIF_V2_LIGHT: + case ThemeHelper.THEME_GLIF_V2: + return R.style.GlifV2Theme; + case ThemeHelper.THEME_GLIF_LIGHT: + case ThemeHelper.THEME_GLIF: + return R.style.GlifTheme; + } } } - return R.style.GlifTheme_Light; + return R.style.GlifTheme; } public static int getTransparentTheme(Intent intent) { final int suwTheme = getTheme(intent); - int wifiDialogTheme = R.style.GlifV2Theme_Light_Transparent; + int transparentTheme = R.style.GlifV2Theme_Light_Transparent; if (suwTheme == R.style.GlifV3Theme) { - wifiDialogTheme = R.style.GlifV3Theme_Transparent; + transparentTheme = R.style.GlifV3Theme_Transparent; } else if (suwTheme == R.style.GlifV3Theme_Light) { - wifiDialogTheme = R.style.GlifV3Theme_Light_Transparent; + transparentTheme = R.style.GlifV3Theme_Light_Transparent; } else if (suwTheme == R.style.GlifV2Theme) { - wifiDialogTheme = R.style.GlifV2Theme_Transparent; + transparentTheme = R.style.GlifV2Theme_Transparent; } else if (suwTheme == R.style.GlifTheme_Light) { - wifiDialogTheme = R.style.SetupWizardTheme_Light_Transparent; + transparentTheme = R.style.SetupWizardTheme_Light_Transparent; } else if (suwTheme == R.style.GlifTheme) { - wifiDialogTheme = R.style.SetupWizardTheme_Transparent; + transparentTheme = R.style.SetupWizardTheme_Transparent; } - return wifiDialogTheme; + return transparentTheme; } public static void copySetupExtras(Intent fromIntent, Intent toIntent) { - toIntent.putExtra(WizardManagerHelper.EXTRA_THEME, - fromIntent.getStringExtra(WizardManagerHelper.EXTRA_THEME)); - toIntent.putExtra(WizardManagerHelper.EXTRA_USE_IMMERSIVE_MODE, - fromIntent.getBooleanExtra(WizardManagerHelper.EXTRA_USE_IMMERSIVE_MODE, false)); + WizardManagerHelper.copyWizardManagerExtras(fromIntent, toIntent); + } + + public static Bundle copyLifecycleExtra(Bundle srcBundle, Bundle dstBundle) { + for (String key : + Arrays.asList( + EXTRA_IS_FIRST_RUN, + EXTRA_IS_SETUP_FLOW)) { + dstBundle.putBoolean(key, srcBundle.getBoolean(key, false)); + } + return dstBundle; } } diff --git a/src/com/android/settings/SmsDefaultDialog.java b/src/com/android/settings/SmsDefaultDialog.java deleted file mode 100644 index e8efa98adf..0000000000 --- a/src/com/android/settings/SmsDefaultDialog.java +++ /dev/null @@ -1,289 +0,0 @@ -/* - * 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; - -import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; - -import android.content.ComponentName; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.provider.Telephony.Sms.Intents; -import android.telephony.TelephonyManager; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.Window; -import android.view.WindowManager; -import android.widget.BaseAdapter; -import android.widget.ImageView; -import android.widget.TextView; - -import com.android.internal.app.AlertActivity; -import com.android.internal.app.AlertController; -import com.android.internal.telephony.SmsApplication; -import com.android.internal.telephony.SmsApplication.SmsApplicationData; - -import java.util.ArrayList; -import java.util.List; - -public final class SmsDefaultDialog extends AlertActivity implements - DialogInterface.OnClickListener { - private SmsApplicationData mNewSmsApplicationData; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - Intent intent = getIntent(); - String packageName = intent.getStringExtra(Intents.EXTRA_PACKAGE_NAME); - - setResult(RESULT_CANCELED); - if (!buildDialog(packageName)) { - finish(); - } - } - - @Override - protected void onStart() { - super.onStart(); - getWindow().addPrivateFlags(PRIVATE_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); - android.util.EventLog.writeEvent(0x534e4554, "120484087", -1, ""); - } - - @Override - protected void onStop() { - super.onStop(); - final Window window = getWindow(); - final WindowManager.LayoutParams attrs = window.getAttributes(); - attrs.privateFlags &= ~PRIVATE_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; - window.setAttributes(attrs); - } - - @Override - public void onClick(DialogInterface dialog, int which) { - switch (which) { - case BUTTON_POSITIVE: - SmsApplication.setDefaultApplication(mNewSmsApplicationData.mPackageName, this); - setResult(RESULT_OK); - break; - case BUTTON_NEGATIVE: - break; - default: - if (which >= 0) { - AppListAdapter adapter = (AppListAdapter) mAlertParams.mAdapter; - if (!adapter.isSelected(which)) { - String packageName = adapter.getPackageName(which); - if (!TextUtils.isEmpty(packageName)) { - SmsApplication.setDefaultApplication(packageName, this); - setResult(RESULT_OK); - } - } - } - break; - } - } - - private boolean buildDialog(String packageName) { - TelephonyManager tm = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); - if (!tm.isSmsCapable()) { - // No phone, no SMS - return false; - } - final AlertController.AlertParams p = mAlertParams; - p.mTitle = getString(R.string.sms_change_default_dialog_title); - mNewSmsApplicationData = SmsApplication.getSmsApplicationData(packageName, this); - if (mNewSmsApplicationData != null) { - // New default SMS app specified, change to that directly after the confirmation - // dialog. - SmsApplicationData oldSmsApplicationData = null; - ComponentName oldSmsComponent = SmsApplication.getDefaultSmsApplication(this, true); - if (oldSmsComponent != null) { - oldSmsApplicationData = SmsApplication.getSmsApplicationData( - oldSmsComponent.getPackageName(), this); - if (oldSmsApplicationData.mPackageName.equals( - mNewSmsApplicationData.mPackageName)) { - return false; - } - } - - // Compose dialog; get - if (oldSmsApplicationData != null) { - p.mMessage = getString(R.string.sms_change_default_dialog_text, - mNewSmsApplicationData.getApplicationName(this), - oldSmsApplicationData.getApplicationName(this)); - } else { - p.mMessage = getString(R.string.sms_change_default_no_previous_dialog_text, - mNewSmsApplicationData.getApplicationName(this)); - } - p.mPositiveButtonText = getString(R.string.yes); - p.mNegativeButtonText = getString(R.string.no); - p.mPositiveButtonListener = this; - p.mNegativeButtonListener = this; - } else { - // No new default SMS app specified, show a list of all SMS apps and let user to pick - p.mAdapter = new AppListAdapter(); - p.mOnClickListener = this; - p.mNegativeButtonText = getString(R.string.cancel); - p.mNegativeButtonListener = this; - if (p.mAdapter.isEmpty()) { - // If there is nothing to choose from, don't build the dialog. - return false; - } - } - setupAlert(); - - return true; - } - - /** - * The list of SMS apps with label, icon. Current default SMS app is marked as "default". - */ - private class AppListAdapter extends BaseAdapter { - /** - * SMS app item in the list - */ - private class Item { - final String label; // app label - final Drawable icon; // app icon - final String packgeName; // full app package name - - public Item(String label, Drawable icon, String packageName) { - this.label = label; - this.icon = icon; - this.packgeName = packageName; - } - } - - // The list - private final List<Item> mItems; - // The index of selected - private final int mSelectedIndex; - - public AppListAdapter() { - mItems = getItems(); - int selected = getSelectedIndex(); - // Move selected up to the top so it is easy to find - if (selected > 0) { - Item item = mItems.remove(selected); - mItems.add(0, item); - selected = 0; - } - mSelectedIndex = selected; - } - - @Override - public int getCount() { - return mItems != null ? mItems.size() : 0; - } - - @Override - public Object getItem(int position) { - return mItems != null && position < mItems.size() ? mItems.get(position) : null; - } - - @Override - public long getItemId(int position) { - return position; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - Item item = ((Item) getItem(position)); - LayoutInflater inflater = getLayoutInflater(); - View view = inflater.inflate(R.layout.app_preference_item, parent, false); - TextView textView = (TextView) view.findViewById(android.R.id.title); - textView.setText(item.label); - if (position == mSelectedIndex) { - view.findViewById(R.id.default_label).setVisibility(View.VISIBLE); - } else { - view.findViewById(R.id.default_label).setVisibility(View.GONE); - } - ImageView imageView = (ImageView) view.findViewById(android.R.id.icon); - imageView.setImageDrawable(item.icon); - return view; - } - - /** - * Get the selected package name by - * - * @param position the index of the item in the list - * @return the package name of selected item - */ - public String getPackageName(int position) { - Item item = (Item) getItem(position); - if (item != null) { - return item.packgeName; - } - return null; - } - - /** - * Check if an item at a position is already selected - * - * @param position the index of the item in the list - * @return true if the item at the position is already selected, false otherwise - */ - public boolean isSelected(int position) { - return position == mSelectedIndex; - } - - // Get the list items by looking for SMS apps - private List<Item> getItems() { - PackageManager pm = getPackageManager(); - List<Item> items = new ArrayList<>(); - for (SmsApplication.SmsApplicationData app : - SmsApplication.getApplicationCollection(SmsDefaultDialog.this)) { - try { - String packageName = app.mPackageName; - ApplicationInfo appInfo = pm.getApplicationInfo(packageName, 0/*flags*/); - if (appInfo != null) { - items.add(new Item( - appInfo.loadLabel(pm).toString(), - appInfo.loadIcon(pm), - packageName)); - } - } catch (PackageManager.NameNotFoundException e) { - // Ignore package can't be found - } - } - return items; - } - - // Get the selected item index by looking for the current default SMS app - private int getSelectedIndex() { - ComponentName appName = SmsApplication.getDefaultSmsApplication( - SmsDefaultDialog.this, true); - if (appName != null) { - String defaultSmsAppPackageName = appName.getPackageName(); - if (!TextUtils.isEmpty(defaultSmsAppPackageName)) { - for (int i = 0; i < mItems.size(); i++) { - if (TextUtils.equals(mItems.get(i).packgeName, defaultSmsAppPackageName)) { - return i; - } - } - } - } - return -1; - } - } -} diff --git a/src/com/android/settings/SummaryPreference.java b/src/com/android/settings/SummaryPreference.java index 8823027864..ced5cd7562 100644 --- a/src/com/android/settings/SummaryPreference.java +++ b/src/com/android/settings/SummaryPreference.java @@ -15,14 +15,15 @@ package com.android.settings; import android.content.Context; -import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; import android.text.TextUtils; import android.util.AttributeSet; import android.view.View; import android.widget.ProgressBar; import android.widget.TextView; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + /** * Provides a summary of a setting page in a preference. Such as memory or data usage. */ diff --git a/src/com/android/settings/TestingSettings.java b/src/com/android/settings/TestingSettings.java index b630685cf5..c1df70520d 100644 --- a/src/com/android/settings/TestingSettings.java +++ b/src/com/android/settings/TestingSettings.java @@ -16,11 +16,11 @@ package com.android.settings; +import android.app.settings.SettingsEnums; import android.os.Bundle; import android.os.UserManager; -import androidx.preference.PreferenceScreen; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.preference.PreferenceScreen; public class TestingSettings extends SettingsPreferenceFragment { @@ -40,6 +40,6 @@ public class TestingSettings extends SettingsPreferenceFragment { @Override public int getMetricsCategory() { - return MetricsEvent.TESTING; + return SettingsEnums.TESTING; } } diff --git a/src/com/android/settings/TestingSettingsBroadcastReceiver.java b/src/com/android/settings/TestingSettingsBroadcastReceiver.java index a66f208996..2172cd07d6 100644 --- a/src/com/android/settings/TestingSettingsBroadcastReceiver.java +++ b/src/com/android/settings/TestingSettingsBroadcastReceiver.java @@ -1,11 +1,11 @@ package com.android.settings; +import static com.android.internal.telephony.TelephonyIntents.SECRET_CODE_ACTION; + import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import static com.android.internal.telephony.TelephonyIntents.SECRET_CODE_ACTION; - import com.android.settings.Settings.TestingSettingsActivity; diff --git a/src/com/android/settings/TetherSettings.java b/src/com/android/settings/TetherSettings.java index e7dc952309..20b6e2f7ef 100644 --- a/src/com/android/settings/TetherSettings.java +++ b/src/com/android/settings/TetherSettings.java @@ -20,6 +20,7 @@ import static android.net.ConnectivityManager.TETHERING_BLUETOOTH; import static android.net.ConnectivityManager.TETHERING_USB; import android.app.Activity; +import android.app.settings.SettingsEnums; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothPan; import android.bluetooth.BluetoothProfile; @@ -34,27 +35,41 @@ import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.UserManager; -import androidx.preference.SwitchPreference; +import android.provider.SearchIndexableResource; + +import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; +import androidx.preference.SwitchPreference; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.datausage.DataSaverBackend; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; import com.android.settings.wifi.tether.WifiTetherPreferenceController; import com.android.settingslib.TetherUtil; +import com.android.settingslib.search.SearchIndexable; import java.lang.ref.WeakReference; import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.concurrent.atomic.AtomicReference; /* * Displays preferences for Tethering. */ +@SearchIndexable public class TetherSettings extends RestrictedSettingsFragment implements DataSaverBackend.Listener { - private static final String USB_TETHER_SETTINGS = "usb_tether_settings"; - private static final String ENABLE_BLUETOOTH_TETHERING = "enable_bluetooth_tethering"; - private static final String DATA_SAVER_FOOTER = "disabled_on_data_saver"; + @VisibleForTesting + static final String KEY_TETHER_PREFS_SCREEN = "tether_prefs_screen"; + @VisibleForTesting + static final String KEY_WIFI_TETHER = "wifi_tether"; + @VisibleForTesting + static final String KEY_USB_TETHER_SETTINGS = "usb_tether_settings"; + @VisibleForTesting + static final String KEY_ENABLE_BLUETOOTH_TETHERING = "enable_bluetooth_tethering"; + private static final String KEY_DATA_SAVER_FOOTER = "disabled_on_data_saver"; private static final String TAG = "TetheringSettings"; @@ -86,7 +101,7 @@ public class TetherSettings extends RestrictedSettingsFragment @Override public int getMetricsCategory() { - return MetricsEvent.TETHER; + return SettingsEnums.TETHER; } public TetherSettings() { @@ -97,7 +112,7 @@ public class TetherSettings extends RestrictedSettingsFragment public void onAttach(Context context) { super.onAttach(context); mWifiTetherPreferenceController = - new WifiTetherPreferenceController(context, getLifecycle()); + new WifiTetherPreferenceController(context, getSettingsLifecycle()); } @Override @@ -110,7 +125,7 @@ public class TetherSettings extends RestrictedSettingsFragment mDataSaverBackend = new DataSaverBackend(getContext()); mDataSaverEnabled = mDataSaverBackend.isDataSaverEnabled(); - mDataSaverFooter = findPreference(DATA_SAVER_FOOTER); + mDataSaverFooter = findPreference(KEY_DATA_SAVER_FOOTER); setIfOnlyAvailableForAdmins(true); if (isUiRestricted()) { @@ -126,8 +141,8 @@ public class TetherSettings extends RestrictedSettingsFragment BluetoothProfile.PAN); } - mUsbTether = (SwitchPreference) findPreference(USB_TETHER_SETTINGS); - mBluetoothTether = (SwitchPreference) findPreference(ENABLE_BLUETOOTH_TETHERING); + mUsbTether = (SwitchPreference) findPreference(KEY_USB_TETHER_SETTINGS); + mBluetoothTether = (SwitchPreference) findPreference(KEY_ENABLE_BLUETOOTH_TETHERING); mDataSaverBackend.addListener(this); @@ -408,9 +423,6 @@ public class TetherSettings extends RestrictedSettingsFragment startTethering(TETHERING_BLUETOOTH); } else { mCm.stopTethering(TETHERING_BLUETOOTH); - // No ACTION_TETHER_STATE_CHANGED is fired or bluetooth unless a device is - // connected. Need to update state manually. - updateState(); } } @@ -432,6 +444,42 @@ public class TetherSettings extends RestrictedSettingsFragment } }; + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex( + Context context, boolean enabled) { + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.tether_prefs; + return Arrays.asList(sir); + } + + @Override + public List<String> getNonIndexableKeys(Context context) { + final List<String> keys = super.getNonIndexableKeys(context); + final ConnectivityManager cm = + context.getSystemService(ConnectivityManager.class); + + if (!TetherUtil.isTetherAvailable(context)) { + keys.add(KEY_TETHER_PREFS_SCREEN); + keys.add(KEY_WIFI_TETHER); + } + + final boolean usbAvailable = + cm.getTetherableUsbRegexs().length != 0; + if (!usbAvailable || Utils.isMonkeyRunning()) { + keys.add(KEY_USB_TETHER_SETTINGS); + } + + final boolean bluetoothAvailable = + cm.getTetherableBluetoothRegexs().length != 0; + if (!bluetoothAvailable) { + keys.add(KEY_ENABLE_BLUETOOTH_TETHERING); + } + return keys; + } + }; + private static final class OnStartTetheringCallback extends ConnectivityManager.OnStartTetheringCallback { final WeakReference<TetherSettings> mTetherSettings; diff --git a/src/com/android/settings/TrustedCredentialsDialogBuilder.java b/src/com/android/settings/TrustedCredentialsDialogBuilder.java index 03414bdc2e..806da92099 100644 --- a/src/com/android/settings/TrustedCredentialsDialogBuilder.java +++ b/src/com/android/settings/TrustedCredentialsDialogBuilder.java @@ -17,7 +17,6 @@ package com.android.settings; import android.annotation.NonNull; import android.app.Activity; -import android.app.AlertDialog; import android.app.admin.DevicePolicyManager; import android.content.DialogInterface; import android.content.pm.UserInfo; @@ -32,6 +31,8 @@ import android.widget.Button; import android.widget.LinearLayout; import android.widget.Spinner; +import androidx.appcompat.app.AlertDialog; + import com.android.internal.widget.LockPatternUtils; import com.android.settings.TrustedCredentialsSettings.CertHolder; import com.android.settingslib.RestrictedLockUtils; @@ -228,7 +229,7 @@ class TrustedCredentialsDialogBuilder extends AlertDialog.Builder { && !mDpm.isCaCertApproved(certHolder.getAlias(), certHolder.getUserId()); final boolean isProfileOrDeviceOwner = RestrictedLockUtils.getProfileOrDeviceOwner( - mActivity, certHolder.getUserId()) != null; + mActivity, UserHandle.of(certHolder.getUserId())) != null; // Show trust button only when it requires consumer user (non-PO/DO) to approve CharSequence displayText = mActivity.getText(!isProfileOrDeviceOwner && mNeedsApproval diff --git a/src/com/android/settings/TrustedCredentialsSettings.java b/src/com/android/settings/TrustedCredentialsSettings.java index 86340be98f..ae25ba939f 100644 --- a/src/com/android/settings/TrustedCredentialsSettings.java +++ b/src/com/android/settings/TrustedCredentialsSettings.java @@ -24,6 +24,7 @@ import android.annotation.UiThread; import android.app.Activity; import android.app.KeyguardManager; import android.app.admin.DevicePolicyManager; +import android.app.settings.SettingsEnums; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; @@ -42,9 +43,9 @@ import android.os.UserManager; import android.security.IKeyChainService; import android.security.KeyChain; import android.security.KeyChain.KeyChainConnection; +import android.util.ArraySet; import android.util.Log; import android.util.SparseArray; -import android.util.ArraySet; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -63,7 +64,6 @@ import android.widget.TextView; import com.android.internal.annotations.GuardedBy; import com.android.internal.app.UnlaunchableAppActivity; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.widget.LockPatternUtils; import com.android.settings.core.InstrumentedFragment; @@ -93,7 +93,7 @@ public class TrustedCredentialsSettings extends InstrumentedFragment @Override public int getMetricsCategory() { - return MetricsEvent.TRUSTED_CREDENTIALS; + return SettingsEnums.TRUSTED_CREDENTIALS; } private enum Tab { diff --git a/src/com/android/settings/UserCredentialsSettings.java b/src/com/android/settings/UserCredentialsSettings.java index 571a32c41e..d322819ba9 100644 --- a/src/com/android/settings/UserCredentialsSettings.java +++ b/src/com/android/settings/UserCredentialsSettings.java @@ -18,10 +18,8 @@ package com.android.settings; import android.annotation.LayoutRes; import android.annotation.Nullable; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.DialogFragment; -import android.app.Fragment; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.DialogInterface; import android.os.AsyncTask; @@ -39,7 +37,6 @@ import android.security.KeyChain.KeyChainConnection; import android.security.KeyStore; import android.security.keymaster.KeyCharacteristics; import android.security.keymaster.KeymasterDefs; -import androidx.recyclerview.widget.RecyclerView; import android.util.Log; import android.util.SparseArray; import android.view.LayoutInflater; @@ -47,11 +44,17 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.RecyclerView; + import com.android.internal.widget.LockPatternUtils; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; +import com.android.settingslib.RestrictedLockUtilsInternal; + import java.security.UnrecoverableKeyException; import java.util.ArrayList; import java.util.EnumSet; @@ -65,7 +68,7 @@ public class UserCredentialsSettings extends SettingsPreferenceFragment @Override public int getMetricsCategory() { - return MetricsEvent.USER_CREDENTIALS; + return SettingsEnums.USER_CREDENTIALS; } @Override @@ -135,11 +138,12 @@ public class UserCredentialsSettings extends SettingsPreferenceFragment final String restriction = UserManager.DISALLOW_CONFIG_CREDENTIALS; final int myUserId = UserHandle.myUserId(); - if (!RestrictedLockUtils.hasBaseUserRestriction(getContext(), restriction, myUserId)) { + if (!RestrictedLockUtilsInternal.hasBaseUserRestriction(getContext(), restriction, + myUserId)) { DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { - final EnforcedAdmin admin = RestrictedLockUtils.checkIfRestrictionEnforced( - getContext(), restriction, myUserId); + final EnforcedAdmin admin = RestrictedLockUtilsInternal + .checkIfRestrictionEnforced(getContext(), restriction, myUserId); if (admin != null) { RestrictedLockUtils.sendShowAdminSupportDetailsIntent(getContext(), admin); @@ -150,25 +154,30 @@ public class UserCredentialsSettings extends SettingsPreferenceFragment dialog.dismiss(); } }; - if (item.isSystem()) { - // TODO: a safe means of clearing wifi certificates. Configs refer to aliases - // directly so deleting certs will break dependent access points. - builder.setNegativeButton(R.string.trusted_credentials_remove_label, listener); - } + // TODO: b/127865361 + // a safe means of clearing wifi certificates. Configs refer to aliases + // directly so deleting certs will break dependent access points. + // However, Wi-Fi used to remove this certificate from storage if the network + // was removed, regardless if it is used in more than one network. + // It has been decided to allow removing certificates from this menu, as we + // assume that the user who manually adds certificates must have a way to + // manually remove them. + builder.setNegativeButton(R.string.trusted_credentials_remove_label, listener); } return builder.create(); } @Override public int getMetricsCategory() { - return MetricsEvent.DIALOG_USER_CREDENTIAL; + return SettingsEnums.DIALOG_USER_CREDENTIAL; } /** * Deletes all certificates and keys under a given alias. * * If the {@link Credential} is for a system alias, all active grants to the alias will be - * removed using {@link KeyChain}. + * removed using {@link KeyChain}. If the {@link Credential} is for Wi-Fi alias, all + * credentials and keys will be removed using {@link KeyStore}. */ private class RemoveCredentialsTask extends AsyncTask<Credential, Void, Credential[]> { private Context context; @@ -184,14 +193,32 @@ public class UserCredentialsSettings extends SettingsPreferenceFragment for (final Credential credential : credentials) { if (credential.isSystem()) { removeGrantsAndDelete(credential); - continue; + } else { + deleteWifiCredential(credential); } - throw new UnsupportedOperationException( - "Not implemented for wifi certificates. This should not be reachable."); } return credentials; } + private void deleteWifiCredential(final Credential credential) { + final KeyStore keyStore = KeyStore.getInstance(); + final EnumSet<Credential.Type> storedTypes = credential.getStoredTypes(); + + // Remove all Wi-Fi credentials + if (storedTypes.contains(Credential.Type.USER_KEY)) { + keyStore.delete(Credentials.USER_PRIVATE_KEY + credential.getAlias(), + Process.WIFI_UID); + } + if (storedTypes.contains(Credential.Type.USER_CERTIFICATE)) { + keyStore.delete(Credentials.USER_CERTIFICATE + credential.getAlias(), + Process.WIFI_UID); + } + if (storedTypes.contains(Credential.Type.CA_CERTIFICATE)) { + keyStore.delete(Credentials.CA_CERTIFICATE + credential.getAlias(), + Process.WIFI_UID); + } + } + private void removeGrantsAndDelete(final Credential credential) { final KeyChainConnection conn; try { @@ -484,5 +511,11 @@ public class UserCredentialsSettings extends SettingsPreferenceFragment public boolean isSystem() { return UserHandle.getAppId(uid) == Process.SYSTEM_UID; } + + public String getAlias() { return alias; } + + public EnumSet<Type> getStoredTypes() { + return storedTypes; + } } } diff --git a/src/com/android/settings/Utils.java b/src/com/android/settings/Utils.java index acc9a7646f..c4b1400f5c 100644 --- a/src/com/android/settings/Utils.java +++ b/src/com/android/settings/Utils.java @@ -22,9 +22,10 @@ import static android.text.format.DateUtils.FORMAT_ABBREV_MONTH; import static android.text.format.DateUtils.FORMAT_SHOW_DATE; import android.annotation.Nullable; +import android.app.ActionBar; +import android.app.Activity; import android.app.ActivityManager; import android.app.AppGlobals; -import android.app.Fragment; import android.app.IActivityManager; import android.app.KeyguardManager; import android.app.admin.DevicePolicyManager; @@ -49,12 +50,14 @@ import android.graphics.Canvas; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.VectorDrawable; +import android.hardware.face.FaceManager; import android.hardware.fingerprint.FingerprintManager; import android.net.ConnectivityManager; import android.net.LinkProperties; import android.net.Network; import android.net.wifi.WifiManager; import android.os.BatteryManager; +import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.os.INetworkManagementService; @@ -71,9 +74,6 @@ import android.provider.ContactsContract.Data; import android.provider.ContactsContract.Profile; import android.provider.ContactsContract.RawContacts; import android.provider.Settings; -import androidx.annotation.StringRes; -import androidx.preference.Preference; -import androidx.preference.PreferenceGroup; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; import android.text.Spannable; @@ -91,10 +91,20 @@ import android.widget.EditText; import android.widget.ListView; import android.widget.TabWidget; +import androidx.annotation.StringRes; +import androidx.core.graphics.drawable.IconCompat; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.Lifecycle; +import androidx.preference.Preference; +import androidx.preference.PreferenceGroup; + import com.android.internal.app.UnlaunchableAppActivity; import com.android.internal.util.ArrayUtils; import com.android.internal.widget.LockPatternUtils; +import com.android.settings.core.FeatureFlags; +import com.android.settings.development.featureflags.FeatureFlagPersistent; import com.android.settings.password.ChooseLockSettingsHelper; +import com.android.settingslib.widget.ActionBarShadowController; import java.net.InetAddress; import java.util.Iterator; @@ -110,20 +120,17 @@ public final class Utils extends com.android.settingslib.Utils { */ public static final int UPDATE_PREFERENCE_FLAG_SET_TITLE_TO_MATCHING_ACTIVITY = 1; - /** - * Color spectrum to use to indicate badness. 0 is completely transparent (no data), - * 1 is most bad (red), the last value is least bad (green). - */ - public static final int[] BADNESS_COLORS = new int[] { - 0x00000000, 0xffc43828, 0xffe54918, 0xfff47b00, - 0xfffabf2c, 0xff679e37, 0xff0a7f42 - }; - - private static final String SETTINGS_PACKAGE_NAME = "com.android.settings"; + public static final String SETTINGS_PACKAGE_NAME = "com.android.settings"; public static final String OS_PKG = "os"; /** + * Whether to disable the new device identifier access restrictions. + */ + public static final String PROPERTY_DEVICE_IDENTIFIER_ACCESS_RESTRICTIONS_DISABLED = + "device_identifier_access_restrictions_disabled"; + + /** * Finds a matching activity for a preference's intent. If a matching * activity is not found, it will remove the preference. * @@ -276,27 +283,21 @@ public final class Utils extends com.android.settingslib.Utils { final boolean movePadding = list.getScrollBarStyle() == View.SCROLLBARS_OUTSIDE_OVERLAY; if (movePadding) { final Resources res = list.getResources(); - final int paddingSide = res.getDimensionPixelSize(R.dimen.settings_side_margin); final int paddingBottom = res.getDimensionPixelSize( com.android.internal.R.dimen.preference_fragment_padding_bottom); if (parent instanceof PreferenceFrameLayout) { ((PreferenceFrameLayout.LayoutParams) child.getLayoutParams()).removeBorders = true; - - final int effectivePaddingSide = ignoreSidePadding ? 0 : paddingSide; - list.setPaddingRelative(effectivePaddingSide, 0, effectivePaddingSide, paddingBottom); - } else { - list.setPaddingRelative(paddingSide, 0, paddingSide, paddingBottom); } + list.setPaddingRelative(0 /* start */, 0 /* top */, 0 /* end */, paddingBottom); } } public static void forceCustomPadding(View view, boolean additive) { final Resources res = view.getResources(); - final int paddingSide = res.getDimensionPixelSize(R.dimen.settings_side_margin); - final int paddingStart = paddingSide + (additive ? view.getPaddingStart() : 0); - final int paddingEnd = paddingSide + (additive ? view.getPaddingEnd() : 0); + final int paddingStart = additive ? view.getPaddingStart() : 0; + final int paddingEnd = additive ? view.getPaddingEnd() : 0; final int paddingBottom = res.getDimensionPixelSize( com.android.internal.R.dimen.preference_fragment_padding_bottom); @@ -388,9 +389,7 @@ public final class Utils extends com.android.settingslib.Utils { */ public static UserHandle getManagedProfile(UserManager userManager) { List<UserHandle> userProfiles = userManager.getUserProfiles(); - final int count = userProfiles.size(); - for (int i = 0; i < count; i++) { - final UserHandle profile = userProfiles.get(i); + for (UserHandle profile : userProfiles) { if (profile.getIdentifier() == userManager.getUserHandle()) { continue; } @@ -526,6 +525,9 @@ public final class Utils extends com.android.settingslib.Utils { * TODO: See bug 16533525. */ public static boolean showSimCardTile(Context context) { + if (FeatureFlagPersistent.isEnabled(context, FeatureFlags.NETWORK_INTERNET_V2)) { + return false; + } final TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); @@ -813,6 +815,19 @@ public final class Utils extends com.android.settingslib.Utils { return fingerprintManager != null && fingerprintManager.isHardwareDetected(); } + public static FaceManager getFaceManagerOrNull(Context context) { + if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_FACE)) { + return (FaceManager) context.getSystemService(Context.FACE_SERVICE); + } else { + return null; + } + } + + public static boolean hasFaceHardware(Context context) { + FaceManager faceManager = getFaceManagerOrNull(context); + return faceManager != null && faceManager.isHardwareDetected(); + } + /** * Launches an intent which may optionally have a user id defined. * @param fragment Fragment to use to launch the activity. @@ -946,15 +961,40 @@ public final class Utils extends com.android.settingslib.Utils { bitmap = Bitmap.createScaledBitmap(((BitmapDrawable) original).getBitmap(), width, height, false); } else { - bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - final Canvas canvas = new Canvas(bitmap); - original.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); - original.draw(canvas); + bitmap = createBitmap(original, width, height); } return new BitmapDrawable(null, bitmap); } /** + * Create an Icon pointing to a drawable. + */ + public static IconCompat createIconWithDrawable(Drawable drawable) { + Bitmap bitmap; + if (drawable instanceof BitmapDrawable) { + bitmap = ((BitmapDrawable)drawable).getBitmap(); + } else { + final int width = drawable.getIntrinsicWidth(); + final int height = drawable.getIntrinsicHeight(); + bitmap = createBitmap(drawable, + width > 0 ? width : 1, + height > 0 ? height : 1); + } + return IconCompat.createWithBitmap(bitmap); + } + + /** + * Creates a drawable with specified width and height. + */ + public static Bitmap createBitmap(Drawable drawable, int width, int height) { + final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + return bitmap; + } + + /** * Get the {@link Drawable} that represents the app icon */ public static Drawable getBadgedIcon(IconDrawableFactory iconDrawableFactory, @@ -968,6 +1008,16 @@ public final class Utils extends com.android.settingslib.Utils { } } + /** Returns true if the current package is installed & enabled. */ + public static boolean isPackageEnabled(Context context, String packageName) { + try { + return context.getPackageManager().getApplicationInfo(packageName, 0).enabled; + } catch (Exception e) { + Log.e(TAG, "Error while retrieving application info for package " + packageName, e); + } + return false; + } + /** Get {@link Resources} by subscription id if subscription id is valid. */ public static Resources getResourcesForSubId(Context context, int subId) { if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { @@ -976,4 +1026,38 @@ public final class Utils extends com.android.settingslib.Utils { return context.getResources(); } } + + /** + * Returns true if SYSTEM_ALERT_WINDOW permission is available. + * Starting from Q, SYSTEM_ALERT_WINDOW is disabled on low ram phones. + */ + public static boolean isSystemAlertWindowEnabled(Context context) { + // SYSTEM_ALERT_WINDOW is disabled on on low ram devices starting from Q + ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + return !(am.isLowRamDevice() && (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)); + } + + /** + * Adds a shadow appear/disappear animation to action bar scroll. + * + * <p/> + * This method must be called after {@link Fragment#onCreate(Bundle)}. + */ + public static void setActionBarShadowAnimation(Activity activity, Lifecycle lifecycle, + View scrollView) { + if (activity == null) { + Log.w(TAG, "No activity, cannot style actionbar."); + return; + } + final ActionBar actionBar = activity.getActionBar(); + if (actionBar == null) { + Log.w(TAG, "No actionbar, cannot style actionbar."); + return; + } + actionBar.setElevation(0); + + if (lifecycle != null && scrollView != null) { + ActionBarShadowController.attachToView(activity, lifecycle, scrollView); + } + } } diff --git a/src/com/android/settings/accessibility/AccessibilityControlTimeoutPreferenceFragment.java b/src/com/android/settings/accessibility/AccessibilityControlTimeoutPreferenceFragment.java new file mode 100644 index 0000000000..df950e8faa --- /dev/null +++ b/src/com/android/settings/accessibility/AccessibilityControlTimeoutPreferenceFragment.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2018 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.accessibility; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.res.Resources; +import android.provider.SearchIndexableResource; + +import androidx.preference.Preference; + +import com.android.settings.R; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.search.SearchIndexable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@SearchIndexable +public final class AccessibilityControlTimeoutPreferenceFragment extends DashboardFragment + implements AccessibilityTimeoutController.OnChangeListener { + + static final String TAG = "AccessibilityControlTimeoutPreferenceFragment"; + private static final List<AbstractPreferenceController> sControllers = new ArrayList<>(); + + @Override + public void onCheckedChanged(Preference preference) { + for (AbstractPreferenceController controller : sControllers) { + controller.updateState(preference); + } + } + + @Override + public void onResume() { + super.onResume(); + + for (AbstractPreferenceController controller : + buildPreferenceControllers(getPrefContext(), getSettingsLifecycle())) { + ((AccessibilityTimeoutController)controller).setOnChangeListener(this); + } + } + + @Override + public void onPause() { + super.onPause(); + + for (AbstractPreferenceController controller : + buildPreferenceControllers(getPrefContext(), getSettingsLifecycle())) { + ((AccessibilityTimeoutController)controller).setOnChangeListener(null); + } + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.ACCESSIBILITY; + } + + @Override + protected String getLogTag() { + return TAG; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.accessibility_control_timeout_settings; + } + + @Override + protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { + return buildPreferenceControllers(context, getSettingsLifecycle()); + } + + private static List<AbstractPreferenceController> buildPreferenceControllers(Context context, + Lifecycle lifecycle) { + if (sControllers.size() == 0) { + Resources resources = context.getResources(); + + String[] timeoutKeys = resources.getStringArray( + R.array.accessibility_timeout_control_selector_keys); + + for (int i=0; i < timeoutKeys.length; i++) { + sControllers.add(new AccessibilityTimeoutController( + context, lifecycle, timeoutKeys[i])); + } + } + return sControllers; + } + + public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, + boolean enabled) { + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.accessibility_control_timeout_settings; + return Arrays.asList(sir); + } + + @Override + public List<String> getNonIndexableKeys(Context context) { + final List<String> keys = super.getNonIndexableKeys(context); + return keys; + } + + @Override + public List<AbstractPreferenceController> createPreferenceControllers( + Context context) { + return buildPreferenceControllers(context, null); + } + }; +} diff --git a/src/com/android/settings/accessibility/AccessibilityDetailsSettingsFragment.java b/src/com/android/settings/accessibility/AccessibilityDetailsSettingsFragment.java new file mode 100644 index 0000000000..dc07ef0137 --- /dev/null +++ b/src/com/android/settings/accessibility/AccessibilityDetailsSettingsFragment.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2019 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.accessibility; + +import android.accessibilityservice.AccessibilityServiceInfo; +import android.app.Activity; +import android.app.admin.DevicePolicyManager; +import android.app.settings.SettingsEnums; +import android.content.ComponentName; +import android.content.Intent; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.os.Bundle; +import android.os.UserHandle; +import android.text.TextUtils; +import android.util.Log; +import android.view.accessibility.AccessibilityManager; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.settings.R; +import com.android.settings.core.InstrumentedFragment; +import com.android.settings.core.SubSettingLauncher; +import com.android.settingslib.accessibility.AccessibilityUtils; + +import java.util.List; +import java.util.Set; + +public class AccessibilityDetailsSettingsFragment extends InstrumentedFragment { + + private final static String TAG = "A11yDetailsSettings"; + + @Override + public int getMetricsCategory() { + return SettingsEnums.ACCESSIBILITY_DETAILS_SETTINGS; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // In case the Intent doesn't have component name, go to a11y services list. + final String extraComponentName = getActivity().getIntent().getStringExtra( + Intent.EXTRA_COMPONENT_NAME); + if (extraComponentName == null) { + Log.w(TAG, "Open accessibility services list due to no component name."); + openAccessibilitySettingsAndFinish(); + return; + } + + // In case the A11yServiceInfo doesn't exist, go to ally services list. + final ComponentName componentName = ComponentName.unflattenFromString(extraComponentName); + final AccessibilityServiceInfo info = getAccessibilityServiceInfo(componentName); + if (info == null) { + Log.w(TAG, "Open accessibility services list due to invalid component name."); + openAccessibilitySettingsAndFinish(); + return; + } + + // In case this accessibility service isn't permitted, go to a11y services list. + if (!isServiceAllowed(componentName.getPackageName())) { + Log.w(TAG, + "Open accessibility services list due to target accessibility service is " + + "prohibited by Device Admin."); + openAccessibilitySettingsAndFinish(); + return; + } + + openAccessibilityDetailsSettingsAndFinish(buildArguments(info)); + } + + @VisibleForTesting + void openAccessibilitySettingsAndFinish() { + new SubSettingLauncher(getActivity()) + .setDestination(AccessibilitySettings.class.getName()) + .setSourceMetricsCategory(getMetricsCategory()) + .launch(); + finish(); + } + + @VisibleForTesting + void openAccessibilityDetailsSettingsAndFinish(Bundle arguments) { + new SubSettingLauncher(getActivity()) + .setDestination(ToggleAccessibilityServicePreferenceFragment.class.getName()) + .setSourceMetricsCategory(getMetricsCategory()) + .setArguments(arguments) + .launch(); + finish(); + } + + @VisibleForTesting + boolean isServiceAllowed(String packageName) { + final DevicePolicyManager dpm = getContext().getSystemService(DevicePolicyManager.class); + final List<String> permittedServices = dpm.getPermittedAccessibilityServices( + UserHandle.myUserId()); + return (permittedServices == null || permittedServices.contains(packageName)); + } + + private AccessibilityServiceInfo getAccessibilityServiceInfo(ComponentName componentName) { + if (componentName == null) { + return null; + } + + final List<AccessibilityServiceInfo> serviceInfos = AccessibilityManager.getInstance( + getActivity()).getInstalledAccessibilityServiceList(); + final int serviceInfoCount = serviceInfos.size(); + for (int i = 0; i < serviceInfoCount; i++) { + AccessibilityServiceInfo serviceInfo = serviceInfos.get(i); + ResolveInfo resolveInfo = serviceInfo.getResolveInfo(); + if (componentName.getPackageName().equals(resolveInfo.serviceInfo.packageName) + && componentName.getClassName().equals(resolveInfo.serviceInfo.name)) { + return serviceInfo; + } + } + return null; + } + + private Bundle buildArguments(AccessibilityServiceInfo info) { + final ResolveInfo resolveInfo = info.getResolveInfo(); + final String title = resolveInfo.loadLabel(getActivity().getPackageManager()).toString(); + final ServiceInfo serviceInfo = resolveInfo.serviceInfo; + final String packageName = serviceInfo.packageName; + final ComponentName componentName = new ComponentName(packageName, serviceInfo.name); + + final List<AccessibilityServiceInfo> enabledServiceInfos = AccessibilityManager.getInstance( + getActivity()).getEnabledAccessibilityServiceList( + AccessibilityServiceInfo.FEEDBACK_ALL_MASK); + final Set<ComponentName> enabledServices = + AccessibilityUtils.getEnabledServicesFromSettings(getActivity()); + final boolean serviceEnabled = enabledServices.contains(componentName); + String description = info.loadDescription(getActivity().getPackageManager()); + if (TextUtils.isEmpty(description)) { + description = getString(R.string.accessibility_service_default_description); + } + + if (serviceEnabled && AccessibilityUtils.hasServiceCrashed( + packageName, serviceInfo.name, enabledServiceInfos)) { + // Update the summaries for services that have crashed. + description = getString(R.string.accessibility_description_state_stopped); + } + + final Bundle extras = new Bundle(); + extras.putString(AccessibilitySettings.EXTRA_PREFERENCE_KEY, + componentName.flattenToString()); + extras.putBoolean(AccessibilitySettings.EXTRA_CHECKED, serviceEnabled); + extras.putString(AccessibilitySettings.EXTRA_TITLE, title); + extras.putParcelable(AccessibilitySettings.EXTRA_RESOLVE_INFO, resolveInfo); + extras.putString(AccessibilitySettings.EXTRA_SUMMARY, description); + + final String settingsClassName = info.getSettingsActivityName(); + if (!TextUtils.isEmpty(settingsClassName)) { + extras.putString(AccessibilitySettings.EXTRA_SETTINGS_TITLE, + getString(R.string.accessibility_menu_item_settings)); + extras.putString(AccessibilitySettings.EXTRA_SETTINGS_COMPONENT_NAME, + new ComponentName(packageName, settingsClassName).flattenToString()); + } + extras.putParcelable(AccessibilitySettings.EXTRA_COMPONENT_NAME, componentName); + + return extras; + } + + private void finish() { + Activity activity = getActivity(); + if (activity == null) { + return; + } + activity.finish(); + } +} diff --git a/src/com/android/settings/accessibility/AccessibilityGestureNavigationTutorial.java b/src/com/android/settings/accessibility/AccessibilityGestureNavigationTutorial.java new file mode 100644 index 0000000000..e7b2b829b5 --- /dev/null +++ b/src/com/android/settings/accessibility/AccessibilityGestureNavigationTutorial.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2019 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.accessibility; + +import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; + +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.style.ImageSpan; +import android.view.LayoutInflater; +import android.view.TextureView; +import android.view.View; +import android.view.Window; +import android.view.accessibility.AccessibilityManager; +import android.widget.TextView; + +import androidx.annotation.ColorInt; +import androidx.annotation.IntDef; +import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; + +import com.android.settings.R; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Utility class for creating the dialog that guides users for gesture navigation for + * accessibility services. + */ +public class AccessibilityGestureNavigationTutorial { + + /** IntDef enum for dialog type. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + DialogType.LAUNCH_SERVICE_BY_ACCESSIBILITY_BUTTON, + DialogType.LAUNCH_SERVICE_BY_GESTURE_NAVIGATION, + DialogType.GESTURE_NAVIGATION_SETTINGS, + }) + + private @interface DialogType { + int LAUNCH_SERVICE_BY_ACCESSIBILITY_BUTTON = 0; + int LAUNCH_SERVICE_BY_GESTURE_NAVIGATION = 1; + int GESTURE_NAVIGATION_SETTINGS = 2; + } + + private static final DialogInterface.OnClickListener mOnClickListener = + (DialogInterface dialog, int which) -> dialog.dismiss(); + + public static void showGestureNavigationSettingsTutorialDialog(Context context, + DialogInterface.OnDismissListener dismissListener) { + final AlertDialog alertDialog = new AlertDialog.Builder(context) + .setView(createTutorialDialogContentView(context, + DialogType.GESTURE_NAVIGATION_SETTINGS)) + .setNegativeButton(R.string.accessibility_tutorial_dialog_button, mOnClickListener) + .setOnDismissListener(dismissListener) + .create(); + + alertDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + alertDialog.setCanceledOnTouchOutside(false); + alertDialog.show(); + } + + static AlertDialog showAccessibilityButtonTutorialDialog(Context context) { + final AlertDialog alertDialog = createDialog(context, + DialogType.LAUNCH_SERVICE_BY_ACCESSIBILITY_BUTTON); + + if (!isGestureNavigateEnabled(context)) { + updateMessageWithIcon(context, alertDialog); + } + + return alertDialog; + } + + static AlertDialog showGestureNavigationTutorialDialog(Context context) { + return createDialog(context, DialogType.LAUNCH_SERVICE_BY_GESTURE_NAVIGATION); + } + + /** + * Get a content View for a dialog to confirm that they want to enable a service. + * + * @param context A valid context + * @param dialogType The type of tutorial dialog + * @return A content view suitable for viewing + */ + private static View createTutorialDialogContentView(Context context, int dialogType) { + final LayoutInflater inflater = (LayoutInflater) context.getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + + View content = null; + + switch (dialogType) { + case DialogType.LAUNCH_SERVICE_BY_ACCESSIBILITY_BUTTON: + content = inflater.inflate( + R.layout.tutorial_dialog_launch_service_by_accessibility_button, null); + break; + case DialogType.LAUNCH_SERVICE_BY_GESTURE_NAVIGATION: + content = inflater.inflate( + R.layout.tutorial_dialog_launch_service_by_gesture_navigation, null); + final TextureView gestureTutorialVideo = content.findViewById( + R.id.gesture_tutorial_video); + final TextView gestureTutorialMessage = content.findViewById( + R.id.gesture_tutorial_message); + VideoPlayer.create(context, isTouchExploreOn(context) + ? R.raw.illustration_accessibility_gesture_three_finger + : R.raw.illustration_accessibility_gesture_two_finger, + gestureTutorialVideo); + gestureTutorialMessage.setText(isTouchExploreOn(context) + ? R.string.accessibility_tutorial_dialog_message_gesture_with_talkback + : R.string.accessibility_tutorial_dialog_message_gesture_without_talkback); + break; + case DialogType.GESTURE_NAVIGATION_SETTINGS: + content = inflater.inflate( + R.layout.tutorial_dialog_launch_by_gesture_navigation_settings, null); + final TextureView gestureSettingsTutorialVideo = content.findViewById( + R.id.gesture_tutorial_video); + final TextView gestureSettingsTutorialMessage = content.findViewById( + R.id.gesture_tutorial_message); + VideoPlayer.create(context, isTouchExploreOn(context) + ? R.raw.illustration_accessibility_gesture_three_finger + : R.raw.illustration_accessibility_gesture_two_finger, + gestureSettingsTutorialVideo); + gestureSettingsTutorialMessage.setText(isTouchExploreOn(context) + ? + R.string.accessibility_tutorial_dialog_message_gesture_settings_with_talkback + : R.string.accessibility_tutorial_dialog_message_gesture_settings_without_talkback); + break; + } + + return content; + } + + private static AlertDialog createDialog(Context context, int dialogType) { + final AlertDialog alertDialog = new AlertDialog.Builder(context) + .setView(createTutorialDialogContentView(context, dialogType)) + .setNegativeButton(R.string.accessibility_tutorial_dialog_button, mOnClickListener) + .create(); + + alertDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + alertDialog.setCanceledOnTouchOutside(false); + alertDialog.show(); + + return alertDialog; + } + + private static void updateMessageWithIcon(Context context, AlertDialog alertDialog) { + final TextView gestureTutorialMessage = alertDialog.findViewById( + R.id.button_tutorial_message); + + // Get the textView line height to update [icon] size. Must be called after show() + final int lineHeight = gestureTutorialMessage.getLineHeight(); + gestureTutorialMessage.setText(getMessageStringWithIcon(context, lineHeight)); + } + + private static SpannableString getMessageStringWithIcon(Context context, int lineHeight) { + final String messageString = context + .getString(R.string.accessibility_tutorial_dialog_message_button); + final SpannableString spannableMessage = SpannableString.valueOf(messageString); + + // Icon + final int indexIconStart = messageString.indexOf("%s"); + final int indexIconEnd = indexIconStart + 2; + final Drawable icon = context.getDrawable(R.drawable.ic_accessibility_new); + icon.setTint(getThemeAttrColor(context, android.R.attr.textColorPrimary)); + icon.setBounds(0, 0, lineHeight, lineHeight); + spannableMessage.setSpan( + new ImageSpan(icon), indexIconStart, indexIconEnd, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + + return spannableMessage; + } + + /** Returns the color associated with the specified attribute in the context's theme. */ + @ColorInt + private static int getThemeAttrColor(final Context context, final int attributeColor) { + final int colorResId = getAttrResourceId(context, attributeColor); + return ContextCompat.getColor(context, colorResId); + } + + /** Returns the identifier of the resolved resource assigned to the given attribute. */ + private static int getAttrResourceId(final Context context, final int attributeColor) { + final int[] attrs = {attributeColor}; + final TypedArray typedArray = context.obtainStyledAttributes(attrs); + final int colorResId = typedArray.getResourceId(0, 0); + typedArray.recycle(); + return colorResId; + } + + private static boolean isGestureNavigateEnabled(Context context) { + return context.getResources().getInteger( + com.android.internal.R.integer.config_navBarInteractionMode) + == NAV_BAR_MODE_GESTURAL; + } + + private static boolean isTouchExploreOn(Context context) { + return ((AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE)) + .isTouchExplorationEnabled(); + } +} diff --git a/src/com/android/settings/accessibility/AccessibilityHearingAidPreferenceController.java b/src/com/android/settings/accessibility/AccessibilityHearingAidPreferenceController.java new file mode 100644 index 0000000000..641d8caf56 --- /dev/null +++ b/src/com/android/settings/accessibility/AccessibilityHearingAidPreferenceController.java @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2018 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.accessibility; + +import android.app.settings.SettingsEnums; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothHearingAid; +import android.bluetooth.BluetoothProfile; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; +import androidx.fragment.app.FragmentManager; +import androidx.lifecycle.Lifecycle.Event; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.OnLifecycleEvent; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.bluetooth.BluetoothDeviceDetailsFragment; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.core.SubSettingLauncher; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.LocalBluetoothManager; + +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; + +/** + * Controller that shows and updates the bluetooth device name + */ +public class AccessibilityHearingAidPreferenceController extends BasePreferenceController + implements LifecycleObserver { + private static final String TAG = "AccessibilityHearingAidPreferenceController"; + private Preference mHearingAidPreference; + + private final BroadcastReceiver mHearingAidChangedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED.equals(intent.getAction())) { + final int state = intent.getIntExtra(BluetoothHearingAid.EXTRA_STATE, + BluetoothHearingAid.STATE_DISCONNECTED); + if (state == BluetoothHearingAid.STATE_CONNECTED) { + updateState(mHearingAidPreference); + } else { + mHearingAidPreference + .setSummary(R.string.accessibility_hearingaid_not_connected_summary); + } + } else if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(intent.getAction())) { + final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, + BluetoothAdapter.ERROR); + if (state != BluetoothAdapter.STATE_ON) { + mHearingAidPreference + .setSummary(R.string.accessibility_hearingaid_not_connected_summary); + } + } + } + }; + + private final LocalBluetoothManager mLocalBluetoothManager; + private final BluetoothAdapter mBluetoothAdapter; + //cache value of supporting hearing aid or not + private boolean mHearingAidProfileSupported; + private FragmentManager mFragmentManager; + + public AccessibilityHearingAidPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + mLocalBluetoothManager = getLocalBluetoothManager(); + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + mHearingAidProfileSupported = isHearingAidProfileSupported(); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mHearingAidPreference = screen.findPreference(getPreferenceKey()); + } + + @Override + public int getAvailabilityStatus() { + return mHearingAidProfileSupported ? AVAILABLE : UNSUPPORTED_ON_DEVICE; + } + + @OnLifecycleEvent(Event.ON_RESUME) + public void onResume() { + if (mHearingAidProfileSupported) { + IntentFilter filter = new IntentFilter(); + filter.addAction(BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED); + filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); + mContext.registerReceiver(mHearingAidChangedReceiver, filter); + } + } + + @OnLifecycleEvent(Event.ON_PAUSE) + public void onPause() { + if (mHearingAidProfileSupported) { + mContext.unregisterReceiver(mHearingAidChangedReceiver); + } + } + + @Override + public boolean handlePreferenceTreeClick(Preference preference) { + if (TextUtils.equals(preference.getKey(), getPreferenceKey())){ + final CachedBluetoothDevice device = getConnectedHearingAidDevice(); + if (device == null) { + launchHearingAidInstructionDialog(); + } else { + launchBluetoothDeviceDetailSetting(device); + } + return true; + } + return false; + } + + @Override + public CharSequence getSummary() { + final CachedBluetoothDevice device = getConnectedHearingAidDevice(); + if (device == null) { + return mContext.getText(R.string.accessibility_hearingaid_not_connected_summary); + } + return device.getName(); + } + + public void setFragmentManager(FragmentManager fragmentManager) { + mFragmentManager = fragmentManager; + } + + @VisibleForTesting + CachedBluetoothDevice getConnectedHearingAidDevice() { + if (!mHearingAidProfileSupported) { + return null; + } + if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) { + return null; + } + final List<BluetoothDevice> deviceList = mLocalBluetoothManager.getProfileManager() + .getHearingAidProfile().getConnectedDevices(); + final Iterator it = deviceList.iterator(); + while (it.hasNext()) { + BluetoothDevice obj = (BluetoothDevice)it.next(); + if (!mLocalBluetoothManager.getCachedDeviceManager().isSubDevice(obj)) { + return mLocalBluetoothManager.getCachedDeviceManager().findDevice(obj); + } + } + return null; + } + + private boolean isHearingAidProfileSupported() { + if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) { + return false; + } + final List<Integer> supportedList = mBluetoothAdapter.getSupportedProfiles(); + if (supportedList.contains(BluetoothProfile.HEARING_AID)) { + return true; + } + return false; + } + + private LocalBluetoothManager getLocalBluetoothManager() { + final FutureTask<LocalBluetoothManager> localBtManagerFutureTask = new FutureTask<>( + // Avoid StrictMode ThreadPolicy violation + () -> com.android.settings.bluetooth.Utils.getLocalBtManager(mContext)); + try { + localBtManagerFutureTask.run(); + return localBtManagerFutureTask.get(); + } catch (InterruptedException | ExecutionException e) { + Log.w(TAG, "Error getting LocalBluetoothManager.", e); + return null; + } + } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + void setPreference(Preference preference) { + mHearingAidPreference = preference; + } + + @VisibleForTesting + void launchBluetoothDeviceDetailSetting(final CachedBluetoothDevice device) { + if (device == null) { + return; + } + final Bundle args = new Bundle(); + args.putString(BluetoothDeviceDetailsFragment.KEY_DEVICE_ADDRESS, + device.getDevice().getAddress()); + + new SubSettingLauncher(mContext) + .setDestination(BluetoothDeviceDetailsFragment.class.getName()) + .setArguments(args) + .setTitleRes(R.string.device_details_title) + .setSourceMetricsCategory(SettingsEnums.ACCESSIBILITY) + .launch(); + } + + @VisibleForTesting + void launchHearingAidInstructionDialog() { + HearingAidDialogFragment fragment = HearingAidDialogFragment.newInstance(); + fragment.show(mFragmentManager, HearingAidDialogFragment.class.toString()); + } +}
\ No newline at end of file diff --git a/src/com/android/settings/accessibility/AccessibilityServiceWarning.java b/src/com/android/settings/accessibility/AccessibilityServiceWarning.java index c1c8a64c50..40ac6412d0 100644 --- a/src/com/android/settings/accessibility/AccessibilityServiceWarning.java +++ b/src/com/android/settings/accessibility/AccessibilityServiceWarning.java @@ -16,12 +16,13 @@ package com.android.settings.accessibility; +import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; + import android.accessibilityservice.AccessibilityServiceInfo; import android.app.Activity; -import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; -import android.content.DialogInterface; +import android.graphics.drawable.Drawable; import android.os.storage.StorageManager; import android.text.BidiFormatter; import android.view.LayoutInflater; @@ -29,57 +30,62 @@ import android.view.MotionEvent; import android.view.View; import android.view.Window; import android.view.WindowManager; +import android.widget.Button; import android.widget.ImageView; -import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; +import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; + import com.android.settings.R; -import java.util.List; import java.util.Locale; -import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; - /** - * Utility class for creating the dialog that asks users for explicit permission to grant - * all of the requested capabilities to an accessibility service before the service is enabled + * Utility class for creating the dialog that asks users for explicit permission for an + * accessibility service to access user data before the service is enabled */ public class AccessibilityServiceWarning { + private static final View.OnTouchListener filterTouchListener = (View v, MotionEvent event) -> { + // Filter obscured touches by consuming them. + if (((event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) + || ((event.getFlags() & MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED) != 0)) { + if (event.getAction() == MotionEvent.ACTION_UP) { + Toast.makeText(v.getContext(), R.string.touch_filtered_warning, + Toast.LENGTH_SHORT).show(); + } + return true; + } + return false; + }; + public static Dialog createCapabilitiesDialog(Activity parentActivity, - AccessibilityServiceInfo info, DialogInterface.OnClickListener listener) { + AccessibilityServiceInfo info, View.OnClickListener listener) { final AlertDialog ad = new AlertDialog.Builder(parentActivity) - .setTitle(parentActivity.getString(R.string.enable_service_title, - getServiceName(parentActivity, info))) - .setView(createEnableDialogContentView(parentActivity, info)) - .setPositiveButton(android.R.string.ok, listener) - .setNegativeButton(android.R.string.cancel, listener) + .setView(createEnableDialogContentView(parentActivity, info, listener)) .create(); - final View.OnTouchListener filterTouchListener = (View v, MotionEvent event) -> { - // Filter obscured touches by consuming them. - if (((event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) - || ((event.getFlags() & MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED) != 0)) { - if (event.getAction() == MotionEvent.ACTION_UP) { - Toast.makeText(v.getContext(), R.string.touch_filtered_warning, - Toast.LENGTH_SHORT).show(); - } - return true; - } - return false; - }; - Window window = ad.getWindow(); WindowManager.LayoutParams params = window.getAttributes(); - params.privateFlags |= PRIVATE_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; + params.privateFlags |= SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; window.setAttributes(params); ad.create(); - ad.getButton(AlertDialog.BUTTON_POSITIVE).setOnTouchListener(filterTouchListener); ad.setCanceledOnTouchOutside(true); return ad; } + public static Dialog createDisableDialog(Activity parentActivity, + AccessibilityServiceInfo info, View.OnClickListener listener) { + final AlertDialog ad = new AlertDialog.Builder(parentActivity) + .setView(createDisableDialogContentView(parentActivity, info, listener)) + .setCancelable(true) + .create(); + + return ad; + } + /** * Return whether the device is encrypted with legacy full disk encryption. Newer devices * should be using File Based Encryption. @@ -98,7 +104,7 @@ public class AccessibilityServiceWarning { * @return A content view suitable for viewing */ private static View createEnableDialogContentView(Context context, - AccessibilityServiceInfo info) { + AccessibilityServiceInfo info, View.OnClickListener listener) { LayoutInflater inflater = (LayoutInflater) context.getSystemService( Context.LAYOUT_INFLATER_SERVICE); @@ -116,60 +122,55 @@ public class AccessibilityServiceWarning { encryptionWarningView.setVisibility(View.GONE); } - TextView capabilitiesHeaderView = (TextView) content.findViewById( - R.id.capabilities_header); - capabilitiesHeaderView.setText(context.getString(R.string.capabilities_list_title, - getServiceName(context, info))); - - LinearLayout capabilitiesView = (LinearLayout) content.findViewById(R.id.capabilities); - - // This capability is implicit for all services. - View capabilityView = inflater.inflate( - com.android.internal.R.layout.app_permission_item_old, null); - - ImageView imageView = (ImageView) capabilityView.findViewById( - com.android.internal.R.id.perm_icon); - imageView.setImageDrawable(context.getDrawable( - com.android.internal.R.drawable.ic_text_dot)); - - TextView labelView = (TextView) capabilityView.findViewById( - com.android.internal.R.id.permission_group); - labelView.setText(context.getString( - R.string.capability_title_receiveAccessibilityEvents)); - - TextView descriptionView = (TextView) capabilityView.findViewById( - com.android.internal.R.id.permission_list); - descriptionView.setText( - context.getString(R.string.capability_desc_receiveAccessibilityEvents)); + final Drawable icon; + if (info.getResolveInfo().getIconResource() == 0) { + icon = ContextCompat.getDrawable(context, R.drawable.ic_accessibility_generic); + } else { + icon = info.getResolveInfo().loadIcon(context.getPackageManager()); + } - List<AccessibilityServiceInfo.CapabilityInfo> capabilities = - info.getCapabilityInfos(context); + ImageView permissionDialogIcon = content.findViewById( + R.id.permissionDialog_icon); + permissionDialogIcon.setImageDrawable(icon); - capabilitiesView.addView(capabilityView); + TextView permissionDialogTitle = content.findViewById(R.id.permissionDialog_title); + permissionDialogTitle.setText(context.getString(R.string.enable_service_title, + getServiceName(context, info))); - // Service-specific capabilities. - final int capabilityCount = capabilities.size(); - for (int i = 0; i < capabilityCount; i++) { - AccessibilityServiceInfo.CapabilityInfo capability = capabilities.get(i); + Button permissionAllowButton = content.findViewById( + R.id.permission_enable_allow_button); + Button permissionDenyButton = content.findViewById( + R.id.permission_enable_deny_button); + permissionAllowButton.setOnClickListener(listener); + permissionAllowButton.setOnTouchListener(filterTouchListener); + permissionDenyButton.setOnClickListener(listener); - capabilityView = inflater.inflate( - com.android.internal.R.layout.app_permission_item_old, null); + return content; + } - imageView = (ImageView) capabilityView.findViewById( - com.android.internal.R.id.perm_icon); - imageView.setImageDrawable(context.getDrawable( - com.android.internal.R.drawable.ic_text_dot)); + private static View createDisableDialogContentView(Context context, + AccessibilityServiceInfo info, View.OnClickListener listener) { + LayoutInflater inflater = (LayoutInflater) context.getSystemService( + Context.LAYOUT_INFLATER_SERVICE); - labelView = (TextView) capabilityView.findViewById( - com.android.internal.R.id.permission_group); - labelView.setText(context.getString(capability.titleResId)); + View content = inflater.inflate(R.layout.disable_accessibility_service_dialog_content, + null); - descriptionView = (TextView) capabilityView.findViewById( - com.android.internal.R.id.permission_list); - descriptionView.setText(context.getString(capability.descResId)); + TextView permissionDialogTitle = content.findViewById(R.id.permissionDialog_disable_title); + permissionDialogTitle.setText(context.getString(R.string.disable_service_title, + getServiceName(context, info))); + TextView permissionDialogMessage = content + .findViewById(R.id.permissionDialog_disable_message); + permissionDialogMessage.setText(context.getString(R.string.disable_service_message, + context.getString(R.string.accessibility_dialog_button_stop), + getServiceName(context, info))); - capabilitiesView.addView(capabilityView); - } + Button permissionAllowButton = content.findViewById( + R.id.permission_disable_stop_button); + Button permissionDenyButton = content.findViewById( + R.id.permission_disable_cancel_button); + permissionAllowButton.setOnClickListener(listener); + permissionDenyButton.setOnClickListener(listener); return content; } diff --git a/src/com/android/settings/accessibility/AccessibilitySettings.java b/src/com/android/settings/accessibility/AccessibilitySettings.java index d9479bbd74..cb98892e85 100644 --- a/src/com/android/settings/accessibility/AccessibilitySettings.java +++ b/src/com/android/settings/accessibility/AccessibilitySettings.java @@ -18,48 +18,58 @@ package com.android.settings.accessibility; import static android.os.Vibrator.VibrationIntensity; +import static com.android.settingslib.TwoTargetPreference.ICON_SIZE_MEDIUM; + import android.accessibilityservice.AccessibilityServiceInfo; import android.app.admin.DevicePolicyManager; +import android.app.settings.SettingsEnums; import android.content.ComponentName; +import android.content.ContentResolver; import android.content.Context; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.res.Resources; import android.graphics.drawable.Drawable; +import android.hardware.display.ColorDisplayManager; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.UserHandle; import android.os.Vibrator; +import android.provider.DeviceConfig; import android.provider.SearchIndexableResource; import android.provider.Settings; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.accessibility.AccessibilityManager; + import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; import androidx.core.content.ContextCompat; import androidx.preference.ListPreference; import androidx.preference.Preference; import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceScreen; -import android.text.TextUtils; -import android.util.ArrayMap; -import android.view.KeyCharacterMap; -import android.view.KeyEvent; -import android.view.accessibility.AccessibilityManager; +import androidx.preference.SwitchPreference; import com.android.internal.accessibility.AccessibilityShortcutController; import com.android.internal.content.PackageMonitor; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.view.RotationPolicy; import com.android.internal.view.RotationPolicy.RotationPolicyListener; import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; import com.android.settings.Utils; +import com.android.settings.display.DarkUIPreferenceController; +import com.android.settings.display.ToggleFontSizePreferenceFragment; import com.android.settings.search.BaseSearchIndexProvider; -import com.android.settings.search.Indexable; -import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; +import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.RestrictedPreference; import com.android.settingslib.accessibility.AccessibilityUtils; +import com.android.settingslib.search.SearchIndexable; + +import com.google.common.primitives.Ints; import java.util.ArrayList; import java.util.Collection; @@ -71,8 +81,9 @@ import java.util.Set; /** * Activity with the accessibility settings. */ +@SearchIndexable public class AccessibilitySettings extends SettingsPreferenceFragment implements - Preference.OnPreferenceChangeListener, Indexable { + Preference.OnPreferenceChangeListener { // Index of the first preference in a preference category. private static final int FIRST_PREFERENCE_IN_CATEGORY_INDEX = -1; @@ -108,20 +119,27 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements "select_long_press_timeout_preference"; private static final String ACCESSIBILITY_SHORTCUT_PREFERENCE = "accessibility_shortcut_preference"; + private static final String HEARING_AID_PREFERENCE = + "hearing_aid_preference"; private static final String CAPTIONING_PREFERENCE_SCREEN = "captioning_preference_screen"; private static final String DISPLAY_MAGNIFICATION_PREFERENCE_SCREEN = "magnification_preference_screen"; private static final String FONT_SIZE_PREFERENCE_SCREEN = "font_size_preference_screen"; - private static final String TTS_SETTINGS_PREFERENCE = - "tts_settings_preference"; private static final String AUTOCLICK_PREFERENCE_SCREEN = - "autoclick_preference_screen"; + "autoclick_preference"; private static final String VIBRATION_PREFERENCE_SCREEN = "vibration_preference_screen"; private static final String DISPLAY_DALTONIZER_PREFERENCE_SCREEN = - "daltonizer_preference_screen"; + "daltonizer_preference"; + private static final String ACCESSIBILITY_CONTROL_TIMEOUT_PREFERENCE = + "accessibility_control_timeout_preference_fragment"; + private static final String DARK_UI_MODE_PREFERENCE = + "dark_ui_mode_accessibility"; + private static final String LIVE_CAPTION_PREFERENCE_KEY = + "live_caption"; + // Extras passed to sub-fragments. static final String EXTRA_PREFERENCE_KEY = "preference_key"; @@ -151,6 +169,8 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements private static final String ANIMATION_ON_VALUE = "1"; private static final String ANIMATION_OFF_VALUE = "0"; + static final String RAMPING_RINGER_ENABLED = "ramping_ringer_enabled"; + private final Map<String, String> mLongPressTimeoutValueToTitleMap = new HashMap<>(); private final Handler mHandler = new Handler(); @@ -219,8 +239,15 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements private Preference mAutoclickPreferenceScreen; private Preference mAccessibilityShortcutPreferenceScreen; private Preference mDisplayDaltonizerPreferenceScreen; + private Preference mHearingAidPreference; private Preference mVibrationPreferenceScreen; + private Preference mLiveCaptionPreference; private SwitchPreference mToggleInversionPreference; + private ColorInversionPreferenceController mInversionPreferenceController; + private AccessibilityHearingAidPreferenceController mHearingAidPreferenceController; + private SwitchPreference mDarkUIModePreference; + private DarkUIPreferenceController mDarkUIPreferenceController; + private LiveCaptionPreferenceController mLiveCaptionPreferenceController; private int mLongPressTimeoutDefault; @@ -255,7 +282,7 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements @Override public int getMetricsCategory() { - return MetricsEvent.ACCESSIBILITY; + return SettingsEnums.ACCESSIBILITY; } @Override @@ -273,6 +300,18 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements } @Override + public void onAttach(Context context) { + super.onAttach(context); + mHearingAidPreferenceController = new AccessibilityHearingAidPreferenceController + (context, HEARING_AID_PREFERENCE); + mHearingAidPreferenceController.setFragmentManager(getFragmentManager()); + getLifecycle().addObserver(mHearingAidPreferenceController); + + mLiveCaptionPreferenceController = new LiveCaptionPreferenceController(context, + LIVE_CAPTION_PREFERENCE_KEY); + } + + @Override public void onResume() { super.onResume(); updateAllPreferences(); @@ -301,9 +340,6 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements if (mSelectLongPressTimeoutPreference == preference) { handleLongPressTimeoutPreferenceChange((String) newValue); return true; - } else if (mToggleInversionPreference == preference) { - handleToggleInversionPreferenceChange((Boolean) newValue); - return true; } return false; } @@ -315,11 +351,6 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements mLongPressTimeoutValueToTitleMap.get(stringValue)); } - private void handleToggleInversionPreferenceChange(boolean checked) { - Settings.Secure.putInt(getContentResolver(), - Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED, (checked ? 1 : 0)); - } - @Override public boolean onPreferenceTreeClick(Preference preference) { if (mToggleHighTextContrastPreference == preference) { @@ -340,6 +371,8 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements } else if (mToggleMasterMonoPreference == preference) { handleToggleMasterMonoPreferenceClick(); return true; + } else if (mHearingAidPreferenceController.handlePreferenceTreeClick(preference)) { + return true; } return super.onPreferenceTreeClick(preference); } @@ -359,6 +392,15 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements : stateSummaryCombo; } + @VisibleForTesting + static boolean isRampingRingerEnabled(final Context context) { + return (Settings.Global.getInt( + context.getContentResolver(), + Settings.Global.APPLY_RAMPING_RINGER, 0) == 1) + && DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_TELEPHONY, RAMPING_RINGER_ENABLED, false); + } + private void handleToggleTextContrastPreferenceClick() { Settings.Secure.putInt(getContentResolver(), Settings.Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED, @@ -409,7 +451,9 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements // Display inversion. mToggleInversionPreference = (SwitchPreference) findPreference(TOGGLE_INVERSION_PREFERENCE); - mToggleInversionPreference.setOnPreferenceChangeListener(this); + mInversionPreferenceController = + new ColorInversionPreferenceController(getContext(), TOGGLE_INVERSION_PREFERENCE); + mInversionPreferenceController.displayPreference(getPreferenceScreen()); // Power button ends calls. mTogglePowerButtonEndsCallPreference = @@ -455,9 +499,17 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements } } + // Hearing Aid. + mHearingAidPreference = findPreference(HEARING_AID_PREFERENCE); + mHearingAidPreferenceController.displayPreference(getPreferenceScreen()); + // Captioning. mCaptioningPreferenceScreen = findPreference(CAPTIONING_PREFERENCE_SCREEN); + // Live caption + mLiveCaptionPreference = findPreference(LIVE_CAPTION_PREFERENCE_KEY); + mLiveCaptionPreferenceController.displayPreference(getPreferenceScreen()); + // Display magnification. mDisplayMagnificationPreferenceScreen = findPreference( DISPLAY_MAGNIFICATION_PREFERENCE_SCREEN); @@ -477,6 +529,13 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements // Vibrations. mVibrationPreferenceScreen = findPreference(VIBRATION_PREFERENCE_SCREEN); + + // Dark Mode. + mDarkUIModePreference = findPreference(DARK_UI_MODE_PREFERENCE); + mDarkUIPreferenceController = new DarkUIPreferenceController(getContext(), + DARK_UI_MODE_PREFERENCE); + mDarkUIPreferenceController.setParentFragment(this); + mDarkUIPreferenceController.displayPreference(getPreferenceScreen()); } private void updateAllPreferences() { @@ -535,7 +594,7 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements Drawable icon; if (resolveInfo.getIconResource() == 0) { - icon = ContextCompat.getDrawable(getContext(), R.mipmap.ic_accessibility_generic); + icon = ContextCompat.getDrawable(getContext(), R.drawable.ic_accessibility_generic); } else { icon = resolveInfo.loadIcon(getPackageManager()); } @@ -547,6 +606,7 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements preference.setKey(componentName.flattenToString()); preference.setTitle(title); + preference.setIconSize(ICON_SIZE_MEDIUM); Utils.setSafeIcon(preference, icon); final boolean serviceEnabled = enabledServices.contains(componentName); String description = info.loadDescription(getPackageManager()); @@ -570,7 +630,7 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements permittedServices == null || permittedServices.contains(packageName); if (!serviceAllowed && !serviceEnabled) { final EnforcedAdmin admin = - RestrictedLockUtils.checkIfAccessibilityServiceDisallowed( + RestrictedLockUtilsInternal.checkIfAccessibilityServiceDisallowed( getActivity(), packageName, UserHandle.myUserId()); if (admin != null) { preference.setDisabledByAdmin(admin); @@ -610,6 +670,16 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements mServicePreferenceToPreferenceCategoryMap.put(preference, prefCategory); } + // Update the order of all the category according to the order defined in xml file. + updateCategoryOrderFromArray(CATEGORY_SCREEN_READER, + R.array.config_order_screen_reader_services); + updateCategoryOrderFromArray(CATEGORY_AUDIO_AND_CAPTIONS, + R.array.config_order_audio_and_caption_services); + updateCategoryOrderFromArray(CATEGORY_INTERACTION_CONTROL, + R.array.config_order_interaction_control_services); + updateCategoryOrderFromArray(CATEGORY_DISPLAY, + R.array.config_order_display_services); + // If the user has not installed any additional services, hide the category. if (downloadedServicesCategory.getPreferenceCount() == 0) { final PreferenceScreen screen = getPreferenceScreen(); @@ -626,18 +696,47 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements } } + /** + * Update the order of perferences in the category by matching their preference + * key with the string array of preference order which is defined in the xml. + * + * @param categoryKey The key of the category need to update the order + * @param key The key of the string array which defines the order of category + */ + private void updateCategoryOrderFromArray(String categoryKey, int key) { + String[] services = getResources().getStringArray(key); + PreferenceCategory category = mCategoryToPrefCategoryMap.get(categoryKey); + int preferenceCount = category.getPreferenceCount(); + int serviceLength = services.length; + for (int preferenceIndex = 0; preferenceIndex < preferenceCount; preferenceIndex++) { + for (int serviceIndex = 0; serviceIndex < serviceLength; serviceIndex++) { + if (category.getPreference(preferenceIndex).getKey() + .equals(services[serviceIndex])) { + category.getPreference(preferenceIndex).setOrder(serviceIndex); + break; + } + } + } + } + protected void updateSystemPreferences() { // Move color inversion and color correction preferences to Display category if device // supports HWC hardware-accelerated color transform. - if (isColorTransformAccelerated(getContext())) { + if (ColorDisplayManager.isColorTransformAccelerated(getContext())) { PreferenceCategory experimentalCategory = mCategoryToPrefCategoryMap.get(CATEGORY_EXPERIMENTAL); PreferenceCategory displayCategory = mCategoryToPrefCategoryMap.get(CATEGORY_DISPLAY); experimentalCategory.removePreference(mToggleInversionPreference); experimentalCategory.removePreference(mDisplayDaltonizerPreferenceScreen); - mToggleInversionPreference.setOrder(mToggleLargePointerIconPreference.getOrder()); - mDisplayDaltonizerPreferenceScreen.setOrder(mToggleInversionPreference.getOrder()); + mDisplayDaltonizerPreferenceScreen.setOrder( + mDisplayMagnificationPreferenceScreen.getOrder() + 1); + mToggleInversionPreference.setOrder( + mDisplayDaltonizerPreferenceScreen.getOrder() + 1); + mToggleLargePointerIconPreference.setOrder( + mToggleInversionPreference.getOrder() + 1); + mToggleDisableAnimationsPreference.setOrder( + mToggleLargePointerIconPreference.getOrder() + 1); mToggleInversionPreference.setSummary(R.string.summary_empty); displayCategory.addPreference(mToggleInversionPreference); displayCategory.addPreference(mDisplayDaltonizerPreferenceScreen); @@ -649,8 +748,10 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements Settings.Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED, 0) == 1); // If the quick setting is enabled, the preference MUST be enabled. - mToggleInversionPreference.setChecked(Settings.Secure.getInt(getContentResolver(), - Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED, 0) == 1); + mInversionPreferenceController.updateState(mToggleInversionPreference); + + // Dark Mode + mDarkUIPreferenceController.updateState(mDarkUIModePreference); // Power button ends calls. if (KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_POWER) @@ -684,6 +785,10 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements updateVibrationSummary(mVibrationPreferenceScreen); + mHearingAidPreferenceController.updateState(mHearingAidPreference); + + mLiveCaptionPreferenceController.updateState(mLiveCaptionPreference); + updateFeatureSummary(Settings.Secure.ACCESSIBILITY_CAPTIONING_ENABLED, mCaptioningPreferenceScreen); updateFeatureSummary(Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED, @@ -696,6 +801,22 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements updateAutoclickSummary(mAutoclickPreferenceScreen); updateAccessibilityShortcut(mAccessibilityShortcutPreferenceScreen); + + updateAccessibilityTimeoutSummary(getContentResolver(), + findPreference(ACCESSIBILITY_CONTROL_TIMEOUT_PREFERENCE)); + } + + void updateAccessibilityTimeoutSummary(ContentResolver resolver, Preference pref) { + String[] timeoutSummarys = getResources().getStringArray( + R.array.accessibility_timeout_summaries); + int[] timeoutValues = getResources().getIntArray( + R.array.accessibility_timeout_selector_values); + + int timeoutValue = AccessibilityTimeoutController.getSecureAccessibilityTimeoutValue( + resolver, AccessibilityTimeoutController.CONTROL_TIMEOUT_SETTINGS_SECURE); + + int idx = Ints.indexOf(timeoutValues, timeoutValue); + pref.setSummary(timeoutSummarys[idx == -1 ? 0 : idx]); } private void updateMagnificationSummary(Preference pref) { @@ -748,20 +869,35 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements pref.setSummary(entries[index]); } - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + @VisibleForTesting void updateVibrationSummary(Preference pref) { final Context context = getContext(); final Vibrator vibrator = context.getSystemService(Vibrator.class); - final int ringIntensity = Settings.System.getInt(context.getContentResolver(), - Settings.System.NOTIFICATION_VIBRATION_INTENSITY, - vibrator.getDefaultNotificationVibrationIntensity()); + int ringIntensity = Settings.System.getInt(context.getContentResolver(), + Settings.System.RING_VIBRATION_INTENSITY, + vibrator.getDefaultRingVibrationIntensity()); + if (Settings.System.getInt(context.getContentResolver(), + Settings.System.VIBRATE_WHEN_RINGING, 0) == 0 && !isRampingRingerEnabled(context)) { + ringIntensity = Vibrator.VIBRATION_INTENSITY_OFF; + } CharSequence ringIntensityString = VibrationIntensityPreferenceController.getIntensityString(context, ringIntensity); - final int touchIntensity = Settings.System.getInt(context.getContentResolver(), + int notificationIntensity = Settings.System.getInt(context.getContentResolver(), + Settings.System.NOTIFICATION_VIBRATION_INTENSITY, + vibrator.getDefaultNotificationVibrationIntensity()); + CharSequence notificationIntensityString = + VibrationIntensityPreferenceController.getIntensityString(context, + notificationIntensity); + + int touchIntensity = Settings.System.getInt(context.getContentResolver(), Settings.System.HAPTIC_FEEDBACK_INTENSITY, vibrator.getDefaultHapticFeedbackIntensity()); + if (Settings.System.getInt(context.getContentResolver(), + Settings.System.HAPTIC_FEEDBACK_ENABLED, 0) == 0) { + touchIntensity = Vibrator.VIBRATION_INTENSITY_OFF; + } CharSequence touchIntensityString = VibrationIntensityPreferenceController.getIntensityString(context, touchIntensity); @@ -769,12 +905,14 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements mVibrationPreferenceScreen = findPreference(VIBRATION_PREFERENCE_SCREEN); } - if (ringIntensity == touchIntensity) { + if (ringIntensity == touchIntensity && ringIntensity == notificationIntensity) { mVibrationPreferenceScreen.setSummary(ringIntensityString); } else { mVibrationPreferenceScreen.setSummary( getString(R.string.accessibility_vibration_summary, - ringIntensityString, touchIntensityString)); + ringIntensityString, + notificationIntensityString, + touchIntensityString)); } } @@ -864,8 +1002,6 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = new BaseSearchIndexProvider() { - public static final String KEY_DISPLAY_SIZE = "accessibility_settings_screen_zoom"; - @Override public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, boolean enabled) { @@ -875,18 +1011,5 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements indexables.add(indexable); return indexables; } - - @Override - public List<String> getNonIndexableKeys(Context context) { - List<String> keys = super.getNonIndexableKeys(context); - // Duplicates in Display - keys.add(FONT_SIZE_PREFERENCE_SCREEN); - keys.add(KEY_DISPLAY_SIZE); - - // Duplicates in Language & Input - keys.add(TTS_SETTINGS_PREFERENCE); - - return keys; - } }; } diff --git a/src/com/android/settings/accessibility/AccessibilitySettingsForSetupWizard.java b/src/com/android/settings/accessibility/AccessibilitySettingsForSetupWizard.java index 3500ba29d5..16f5fcdd24 100644 --- a/src/com/android/settings/accessibility/AccessibilitySettingsForSetupWizard.java +++ b/src/com/android/settings/accessibility/AccessibilitySettingsForSetupWizard.java @@ -17,18 +17,25 @@ package com.android.settings.accessibility; import android.accessibilityservice.AccessibilityServiceInfo; +import android.app.settings.SettingsEnums; import android.content.ComponentName; import android.content.Context; import android.content.pm.ServiceInfo; import android.os.Bundle; -import androidx.preference.Preference; import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; import android.view.accessibility.AccessibilityManager; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.preference.Preference; +import androidx.recyclerview.widget.RecyclerView; + import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; +import com.google.android.setupdesign.GlifPreferenceLayout; + import java.util.List; /** @@ -59,7 +66,24 @@ public class AccessibilitySettingsForSetupWizard extends SettingsPreferenceFragm @Override public int getMetricsCategory() { - return MetricsEvent.SUW_ACCESSIBILITY; + return SettingsEnums.SUW_ACCESSIBILITY; + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + GlifPreferenceLayout layout = (GlifPreferenceLayout) view; + layout.setDividerInsets(Integer.MAX_VALUE, 0); + + layout.setHeaderText(R.string.vision_settings_title); + } + + @Override + public RecyclerView onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent, + Bundle savedInstanceState) { + GlifPreferenceLayout layout = (GlifPreferenceLayout) parent; + return layout.onCreateRecyclerView(inflater, parent, savedInstanceState); } @Override diff --git a/src/com/android/settings/accessibility/AccessibilitySettingsForSetupWizardActivity.java b/src/com/android/settings/accessibility/AccessibilitySettingsForSetupWizardActivity.java index 37b1018ffe..0ed1644f52 100644 --- a/src/com/android/settings/accessibility/AccessibilitySettingsForSetupWizardActivity.java +++ b/src/com/android/settings/accessibility/AccessibilitySettingsForSetupWizardActivity.java @@ -16,29 +16,34 @@ package com.android.settings.accessibility; +import android.content.ComponentName; import android.os.Bundle; -import androidx.preference.PreferenceFragment; -import androidx.preference.Preference; +import android.util.Log; import android.view.Menu; import android.view.accessibility.AccessibilityEvent; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceFragmentCompat; + import com.android.settings.SettingsActivity; +import com.android.settings.SetupWizardUtils; import com.android.settings.core.SubSettingLauncher; +import com.android.settings.display.FontSizePreferenceFragmentForSetupWizard; import com.android.settings.search.actionbar.SearchMenuController; import com.android.settings.support.actionbar.HelpResourceProvider; import com.android.settingslib.core.instrumentation.Instrumentable; +import com.google.android.setupcompat.util.WizardManagerHelper; + public class AccessibilitySettingsForSetupWizardActivity extends SettingsActivity { + private static final String LOG_TAG = "A11ySettingsForSUW"; private static final String SAVE_KEY_TITLE = "activity_title"; - @Override - protected void onCreate(Bundle savedState) { - super.onCreate(savedState); - - // Finish configuring the content view. - getActionBar().setDisplayHomeAsUpEnabled(true); - } + @VisibleForTesting + static final String CLASS_NAME_FONT_SIZE_SETTINGS_FOR_SUW = + "com.android.settings.FontSizeSettingsForSetupWizardActivity"; @Override protected void onSaveInstanceState(Bundle savedState) { @@ -70,7 +75,7 @@ public class AccessibilitySettingsForSetupWizardActivity extends SettingsActivit } @Override - public boolean onPreferenceStartFragment(PreferenceFragment caller, Preference pref) { + public boolean onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref) { Bundle args = pref.getExtras(); if (args == null) { args = new Bundle(); @@ -86,4 +91,33 @@ public class AccessibilitySettingsForSetupWizardActivity extends SettingsActivit .launch(); return true; } + + @Override + protected void onCreate(Bundle savedState) { + super.onCreate(savedState); + + tryLaunchFontSizeSettings(); + } + + @VisibleForTesting + void tryLaunchFontSizeSettings() { + if (WizardManagerHelper.isAnySetupWizard(getIntent()) + && new ComponentName(getPackageName(), + CLASS_NAME_FONT_SIZE_SETTINGS_FOR_SUW).equals( + getIntent().getComponent())) { + final Bundle args = new Bundle(); + args.putInt(HelpResourceProvider.HELP_URI_RESOURCE_KEY, 0); + args.putBoolean(SearchMenuController.NEED_SEARCH_ICON_IN_ACTION_BAR, false); + final SubSettingLauncher subSettingLauncher = new SubSettingLauncher(this) + .setDestination(FontSizePreferenceFragmentForSetupWizard.class.getName()) + .setArguments(args) + .setSourceMetricsCategory(Instrumentable.METRICS_CATEGORY_UNKNOWN) + .setExtras(SetupWizardUtils.copyLifecycleExtra(getIntent().getExtras(), + new Bundle())); + + Log.d(LOG_TAG, "Launch font size settings"); + subSettingLauncher.launch(); + finish(); + } + } } diff --git a/src/com/android/settings/accessibility/AccessibilityShortcutPreferenceFragment.java b/src/com/android/settings/accessibility/AccessibilityShortcutPreferenceFragment.java index e9321c67b4..de65324f38 100644 --- a/src/com/android/settings/accessibility/AccessibilityShortcutPreferenceFragment.java +++ b/src/com/android/settings/accessibility/AccessibilityShortcutPreferenceFragment.java @@ -17,6 +17,7 @@ package com.android.settings.accessibility; import android.accessibilityservice.AccessibilityServiceInfo; import android.annotation.Nullable; +import android.app.settings.SettingsEnums; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; @@ -25,21 +26,23 @@ import android.os.Bundle; import android.os.Handler; import android.os.UserHandle; import android.provider.Settings; -import androidx.preference.SwitchPreference; -import androidx.preference.Preference; import android.view.accessibility.AccessibilityManager; import android.widget.Switch; +import androidx.preference.Preference; +import androidx.preference.SwitchPreference; + import com.android.internal.accessibility.AccessibilityShortcutController; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; import com.android.settingslib.accessibility.AccessibilityUtils; +import com.android.settingslib.search.SearchIndexable; /** * Settings page for accessibility shortcut */ +@SearchIndexable public class AccessibilityShortcutPreferenceFragment extends ToggleFeaturePreferenceFragment implements Indexable { @@ -57,7 +60,7 @@ public class AccessibilityShortcutPreferenceFragment extends ToggleFeaturePrefer @Override public int getMetricsCategory() { - return MetricsEvent.ACCESSIBILITY_TOGGLE_GLOBAL_GESTURE; + return SettingsEnums.ACCESSIBILITY_TOGGLE_GLOBAL_GESTURE; } @Override diff --git a/src/com/android/settings/accessibility/AccessibilityTimeoutController.java b/src/com/android/settings/accessibility/AccessibilityTimeoutController.java new file mode 100644 index 0000000000..057013c223 --- /dev/null +++ b/src/com/android/settings/accessibility/AccessibilityTimeoutController.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2018 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.accessibility; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.Resources; +import android.provider.Settings; + +import androidx.lifecycle.LifecycleObserver; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settings.widget.RadioButtonPreference; +import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.core.lifecycle.Lifecycle; + +import com.google.common.primitives.Ints; + +import java.util.HashMap; +import java.util.Map; + +/** + * Controller class that control accessibility time out settings. + */ +public class AccessibilityTimeoutController extends AbstractPreferenceController implements + LifecycleObserver, RadioButtonPreference.OnClickListener, PreferenceControllerMixin { + static final String CONTENT_TIMEOUT_SETTINGS_SECURE = + Settings.Secure.ACCESSIBILITY_NON_INTERACTIVE_UI_TIMEOUT_MS; + static final String CONTROL_TIMEOUT_SETTINGS_SECURE = + Settings.Secure.ACCESSIBILITY_INTERACTIVE_UI_TIMEOUT_MS; + + // pair the preference key and timeout value. + private final Map<String, Integer> mAccessibilityTimeoutKeyToValueMap = new HashMap<>(); + + // RadioButtonPreference key, each preference represent a timeout value. + private final String mPreferenceKey; + private final ContentResolver mContentResolver; + private final Resources mResources; + private OnChangeListener mOnChangeListener; + private RadioButtonPreference mPreference; + private int mAccessibilityUiTimeoutValue; + + public AccessibilityTimeoutController(Context context, Lifecycle lifecycle, + String preferenceKey) { + super(context); + + mContentResolver = context.getContentResolver(); + mResources = context.getResources(); + + if (lifecycle != null) { + lifecycle.addObserver(this); + } + mPreferenceKey = preferenceKey; + } + + protected static int getSecureAccessibilityTimeoutValue(ContentResolver resolver, String name) { + String timeOutSec = Settings.Secure.getString(resolver, name); + if (timeOutSec == null) { + return 0; + } + Integer timeOutValue = Ints.tryParse(timeOutSec); + return timeOutValue == null ? 0 : timeOutValue; + } + + public void setOnChangeListener(OnChangeListener listener) { + mOnChangeListener = listener; + } + + private Map<String, Integer> getTimeoutValueToKeyMap() { + if (mAccessibilityTimeoutKeyToValueMap.size() == 0) { + + String[] timeoutKeys = mResources.getStringArray( + R.array.accessibility_timeout_control_selector_keys); + + int[] timeoutValues = mResources.getIntArray( + R.array.accessibility_timeout_selector_values); + + final int timeoutValueCount = timeoutValues.length; + for (int i = 0; i < timeoutValueCount; i++) { + mAccessibilityTimeoutKeyToValueMap.put(timeoutKeys[i], timeoutValues[i]); + } + } + return mAccessibilityTimeoutKeyToValueMap; + } + + private void putSecureString(String name, String value) { + Settings.Secure.putString(mContentResolver, name, value); + } + + private void handlePreferenceChange(String value) { + // save value to both content and control timeout setting. + putSecureString(Settings.Secure.ACCESSIBILITY_NON_INTERACTIVE_UI_TIMEOUT_MS, value); + putSecureString(Settings.Secure.ACCESSIBILITY_INTERACTIVE_UI_TIMEOUT_MS, value); + } + + @Override + public boolean isAvailable() { + return true; + } + + @Override + public String getPreferenceKey() { + return mPreferenceKey; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + + mPreference = (RadioButtonPreference) + screen.findPreference(getPreferenceKey()); + mPreference.setOnClickListener(this); + } + + @Override + public void onRadioButtonClicked(RadioButtonPreference preference) { + int value = getTimeoutValueToKeyMap().get(mPreferenceKey); + handlePreferenceChange(String.valueOf(value)); + if (mOnChangeListener != null) { + mOnChangeListener.onCheckedChanged(mPreference); + } + } + + private int getAccessibilityTimeoutValue() { + // get accessibility control timeout value + int timeoutValue = getSecureAccessibilityTimeoutValue(mContentResolver, + CONTROL_TIMEOUT_SETTINGS_SECURE); + return timeoutValue; + } + + protected void updatePreferenceCheckedState(int value) { + if (mAccessibilityUiTimeoutValue == value) { + mPreference.setChecked(true); + } + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + + mAccessibilityUiTimeoutValue = getAccessibilityTimeoutValue(); + + // reset RadioButton + mPreference.setChecked(false); + int preferenceValue = getTimeoutValueToKeyMap().get(mPreference.getKey()); + updatePreferenceCheckedState(preferenceValue); + } + + /** + * Listener interface handles checked event. + */ + public interface OnChangeListener { + /** + * A hook that is called when preference checked. + */ + void onCheckedChanged(Preference preference); + } +} diff --git a/src/com/android/settings/accessibility/BalanceSeekBar.java b/src/com/android/settings/accessibility/BalanceSeekBar.java new file mode 100644 index 0000000000..b108e18d2f --- /dev/null +++ b/src/com/android/settings/accessibility/BalanceSeekBar.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2019 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.accessibility; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.AttributeSet; +import android.widget.SeekBar; + +import androidx.annotation.VisibleForTesting; + +import com.android.settings.R; + +/** + * A custom seekbar for the balance setting. + * + * Adds a center line indicator between left and right, which snaps to if close. + * Updates Settings.System for balance on progress changed. + */ +public class BalanceSeekBar extends SeekBar { + private final Context mContext; + private final Object mListenerLock = new Object(); + private OnSeekBarChangeListener mOnSeekBarChangeListener; + private final OnSeekBarChangeListener mProxySeekBarListener = new OnSeekBarChangeListener() { + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + synchronized (mListenerLock) { + if (mOnSeekBarChangeListener != null) { + mOnSeekBarChangeListener.onStopTrackingTouch(seekBar); + } + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + synchronized (mListenerLock) { + if (mOnSeekBarChangeListener != null) { + mOnSeekBarChangeListener.onStartTrackingTouch(seekBar); + } + } + } + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (fromUser) { + // Snap to centre when within the specified threshold + if (progress != mCenter + && progress > mCenter - mSnapThreshold + && progress < mCenter + mSnapThreshold) { + progress = mCenter; + seekBar.setProgress(progress); // direct update (fromUser becomes false) + } + final float balance = (progress - mCenter) * 0.01f; + Settings.System.putFloatForUser(mContext.getContentResolver(), + Settings.System.MASTER_BALANCE, balance, UserHandle.USER_CURRENT); + } + // If fromUser is false, the call is a set from the framework on creation or on + // internal update. The progress may be zero, ignore (don't change system settings). + + // after adjusting the seekbar, notify downstream listener. + // note that progress may have been adjusted in the code above to mCenter. + synchronized (mListenerLock) { + if (mOnSeekBarChangeListener != null) { + mOnSeekBarChangeListener.onProgressChanged(seekBar, progress, fromUser); + } + } + } + }; + + // Percentage of max to be used as a snap to threshold + @VisibleForTesting + static final float SNAP_TO_PERCENTAGE = 0.03f; + private final Paint mCenterMarkerPaint; + private final Rect mCenterMarkerRect; + // changed in setMax() + private float mSnapThreshold; + private int mCenter; + + public BalanceSeekBar(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.seekBarStyle); + } + + public BalanceSeekBar(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0 /* defStyleRes */); + } + + public BalanceSeekBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + mContext = context; + Resources res = getResources(); + mCenterMarkerRect = new Rect(0 /* left */, 0 /* top */, + res.getDimensionPixelSize(R.dimen.balance_seekbar_center_marker_width), + res.getDimensionPixelSize(R.dimen.balance_seekbar_center_marker_height)); + mCenterMarkerPaint = new Paint(); + // TODO use a more suitable colour? + mCenterMarkerPaint.setColor(Color.BLACK); + mCenterMarkerPaint.setStyle(Paint.Style.FILL); + // Remove the progress colour + setProgressTintList(ColorStateList.valueOf(Color.TRANSPARENT)); + + super.setOnSeekBarChangeListener(mProxySeekBarListener); + } + + @Override + public void setOnSeekBarChangeListener(OnSeekBarChangeListener listener) { + synchronized (mListenerLock) { + mOnSeekBarChangeListener = listener; + } + } + + // Note: the superclass AbsSeekBar.setMax is synchronized. + @Override + public synchronized void setMax(int max) { + super.setMax(max); + // update snap to threshold + mCenter = max / 2; + mSnapThreshold = max * SNAP_TO_PERCENTAGE; + } + + // Note: the superclass AbsSeekBar.onDraw is synchronized. + @Override + protected synchronized void onDraw(Canvas canvas) { + // Draw a vertical line at 50% that represents centred balance + int seekBarCenter = (canvas.getHeight() - getPaddingBottom()) / 2; + canvas.save(); + canvas.translate((canvas.getWidth() - mCenterMarkerRect.right) / 2, + seekBarCenter - (mCenterMarkerRect.bottom / 2)); + canvas.drawRect(mCenterMarkerRect, mCenterMarkerPaint); + canvas.restore(); + super.onDraw(canvas); + } + + @VisibleForTesting + OnSeekBarChangeListener getProxySeekBarListener() { + return mProxySeekBarListener; + } +} + diff --git a/src/com/android/settings/accessibility/BalanceSeekBarPreference.java b/src/com/android/settings/accessibility/BalanceSeekBarPreference.java new file mode 100644 index 0000000000..b03c8abd31 --- /dev/null +++ b/src/com/android/settings/accessibility/BalanceSeekBarPreference.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2019 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.accessibility; + +import android.content.Context; +import android.media.AudioSystem; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageView; +import android.widget.SeekBar; + +import androidx.core.content.res.TypedArrayUtils; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + +import com.android.settings.R; +import com.android.settings.widget.SeekBarPreference; + +/** A slider preference that directly controls audio balance **/ +public class BalanceSeekBarPreference extends SeekBarPreference { + private static final int BALANCE_CENTER_VALUE = 100; + private static final int BALANCE_MAX_VALUE = 200; + + private final Context mContext; + private BalanceSeekBar mSeekBar; + private ImageView mIconView; + + public BalanceSeekBarPreference(Context context, AttributeSet attrs) { + super(context, attrs, TypedArrayUtils.getAttr(context, + R.attr.preferenceStyle, + android.R.attr.preferenceStyle)); + mContext = context; + setLayoutResource(R.layout.preference_balance_slider); + } + + @Override + public void onBindViewHolder(PreferenceViewHolder view) { + super.onBindViewHolder(view); + mSeekBar = (BalanceSeekBar) view.findViewById(com.android.internal.R.id.seekbar); + mIconView = (ImageView) view.findViewById(com.android.internal.R.id.icon); + init(); + } + + private void init() { + if (mSeekBar == null) { + return; + } + final float balance = Settings.System.getFloatForUser( + mContext.getContentResolver(), Settings.System.MASTER_BALANCE, + 0.f /* default */, UserHandle.USER_CURRENT); + // Rescale balance to range 0-BALANCE_MAX_VALUE centered at BALANCE_MAX_VALUE / 2. + mSeekBar.setMax(BALANCE_MAX_VALUE); + mSeekBar.setProgress((int) (balance * 100.f) + BALANCE_CENTER_VALUE); + mSeekBar.setEnabled(isEnabled()); + } +} diff --git a/src/com/android/settings/accessibility/CaptionPropertiesFragment.java b/src/com/android/settings/accessibility/CaptionPropertiesFragment.java index a5e02c5cc5..bddca9cead 100644 --- a/src/com/android/settings/accessibility/CaptionPropertiesFragment.java +++ b/src/com/android/settings/accessibility/CaptionPropertiesFragment.java @@ -16,26 +16,22 @@ package com.android.settings.accessibility; +import android.app.settings.SettingsEnums; import android.content.ContentResolver; import android.content.Context; import android.content.res.Resources; import android.graphics.Color; import android.os.Bundle; -import android.preference.PreferenceFrameLayout; import android.provider.Settings; +import android.view.View; +import android.view.accessibility.CaptioningManager; +import android.view.accessibility.CaptioningManager.CaptionStyle; + import androidx.preference.ListPreference; import androidx.preference.Preference; import androidx.preference.Preference.OnPreferenceChangeListener; import androidx.preference.PreferenceCategory; -import android.view.LayoutInflater; -import android.view.View; -import android.view.View.OnLayoutChangeListener; -import android.view.ViewGroup; -import android.view.ViewGroup.LayoutParams; -import android.view.accessibility.CaptioningManager; -import android.view.accessibility.CaptioningManager.CaptionStyle; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.widget.SubtitleView; import com.android.settings.R; import com.android.settings.SettingsActivity; @@ -45,6 +41,7 @@ import com.android.settings.widget.SwitchBar; import com.android.settings.widget.ToggleSwitch; import com.android.settings.widget.ToggleSwitch.OnBeforeCheckedChangeListener; import com.android.settingslib.accessibility.AccessibilityUtils; +import com.android.settingslib.widget.LayoutPreference; import java.util.Locale; @@ -53,6 +50,7 @@ import java.util.Locale; */ public class CaptionPropertiesFragment extends SettingsPreferenceFragment implements OnPreferenceChangeListener, OnValueChangedListener { + private static final String PREF_CAPTION_PREVIEW = "caption_preview"; private static final String PREF_BACKGROUND_COLOR = "captioning_background_color"; private static final String PREF_BACKGROUND_OPACITY = "captioning_background_opacity"; private static final String PREF_FOREGROUND_COLOR = "captioning_foreground_color"; @@ -98,7 +96,7 @@ public class CaptionPropertiesFragment extends SettingsPreferenceFragment @Override public int getMetricsCategory() { - return MetricsEvent.ACCESSIBILITY_CAPTION_PROPERTIES; + return SettingsEnums.ACCESSIBILITY_CAPTION_PROPERTIES; } @Override @@ -115,43 +113,6 @@ public class CaptionPropertiesFragment extends SettingsPreferenceFragment } @Override - public View onCreateView( - LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - final View rootView = inflater.inflate(R.layout.captioning_preview, container, false); - - // We have to do this now because PreferenceFrameLayout looks at it - // only when the view is added. - if (container instanceof PreferenceFrameLayout) { - ((PreferenceFrameLayout.LayoutParams) rootView.getLayoutParams()).removeBorders = true; - } - - final View content = super.onCreateView(inflater, container, savedInstanceState); - ((ViewGroup) rootView.findViewById(R.id.properties_fragment)).addView( - content, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); - - return rootView; - } - - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - final boolean enabled = mCaptioningManager.isEnabled(); - mPreviewText = (SubtitleView) view.findViewById(R.id.preview_text); - mPreviewText.setVisibility(enabled ? View.VISIBLE : View.INVISIBLE); - - mPreviewWindow = view.findViewById(R.id.preview_window); - mPreviewViewport = view.findViewById(R.id.preview_viewport); - mPreviewViewport.addOnLayoutChangeListener(new OnLayoutChangeListener() { - @Override - public void onLayoutChange(View v, int left, int top, int right, int bottom, - int oldLeft, int oldTop, int oldRight, int oldBottom) { - refreshPreviewText(); - } - }); - } - - @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); @@ -262,6 +223,19 @@ public class CaptionPropertiesFragment extends SettingsPreferenceFragment } private void initializeAllPreferences() { + final LayoutPreference captionPreview = findPreference(PREF_CAPTION_PREVIEW); + + final boolean enabled = mCaptioningManager.isEnabled(); + mPreviewText = captionPreview.findViewById(R.id.preview_text); + mPreviewText.setVisibility(enabled ? View.VISIBLE : View.INVISIBLE); + + mPreviewWindow = captionPreview.findViewById(R.id.preview_window); + + mPreviewViewport = captionPreview.findViewById(R.id.preview_viewport); + mPreviewViewport.addOnLayoutChangeListener( + (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) + -> refreshPreviewText()); + mLocale = (LocalePreference) findPreference(PREF_LOCALE); mFontSize = (ListPreference) findPreference(PREF_FONT_SIZE); @@ -369,9 +343,9 @@ public class CaptionPropertiesFragment extends SettingsPreferenceFragment /** * Unpack the specified color value and update the preferences. * - * @param color color preference + * @param color color preference * @param opacity opacity preference - * @param value packed value + * @param value packed value */ private void parseColorOpacity(ColorPreference color, ColorPreference opacity, int value) { final int colorValue; diff --git a/src/com/android/settings/accessibility/ColorInversionPreferenceController.java b/src/com/android/settings/accessibility/ColorInversionPreferenceController.java new file mode 100644 index 0000000000..4e4c17b17a --- /dev/null +++ b/src/com/android/settings/accessibility/ColorInversionPreferenceController.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2018 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.accessibility; + +import android.content.Context; +import android.provider.Settings; + +import androidx.annotation.VisibleForTesting; + +import com.android.settings.core.TogglePreferenceController; + +public class ColorInversionPreferenceController extends TogglePreferenceController { + @VisibleForTesting + static final int ON = 1; + @VisibleForTesting + static final int OFF = 0; + + public ColorInversionPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public boolean isChecked() { + return Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED, OFF) == ON; + } + + @Override + public boolean setChecked(boolean isChecked) { + return Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED, (isChecked ? ON : OFF)); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } +} diff --git a/src/com/android/settings/accessibility/ColorPreference.java b/src/com/android/settings/accessibility/ColorPreference.java index 7a7b7fc645..e360a9ebce 100644 --- a/src/com/android/settings/accessibility/ColorPreference.java +++ b/src/com/android/settings/accessibility/ColorPreference.java @@ -20,13 +20,14 @@ import android.content.Context; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; -import androidx.preference.PreferenceViewHolder; import android.text.TextUtils; import android.util.AttributeSet; import android.view.View; import android.widget.ImageView; import android.widget.TextView; +import androidx.preference.PreferenceViewHolder; + import com.android.settings.R; /** diff --git a/src/com/android/settings/accessibility/DividerAllowedBelowPreference.java b/src/com/android/settings/accessibility/DividerAllowedBelowPreference.java index 53a2dda5b2..1be281f614 100644 --- a/src/com/android/settings/accessibility/DividerAllowedBelowPreference.java +++ b/src/com/android/settings/accessibility/DividerAllowedBelowPreference.java @@ -17,9 +17,10 @@ package com.android.settings.accessibility; import android.content.Context; +import android.util.AttributeSet; + import androidx.preference.Preference; import androidx.preference.PreferenceViewHolder; -import android.util.AttributeSet; /* * Preference that always has a divider below. Used for SUW Accessibility Settings Summary text. diff --git a/src/com/android/settings/accessibility/HapticFeedbackIntensityPreferenceController.java b/src/com/android/settings/accessibility/HapticFeedbackIntensityPreferenceController.java index 090c3de1e9..a2142a2379 100644 --- a/src/com/android/settings/accessibility/HapticFeedbackIntensityPreferenceController.java +++ b/src/com/android/settings/accessibility/HapticFeedbackIntensityPreferenceController.java @@ -18,6 +18,7 @@ package com.android.settings.accessibility; import android.content.Context; import android.provider.Settings; + import androidx.annotation.VisibleForTesting; public class HapticFeedbackIntensityPreferenceController @@ -27,7 +28,8 @@ public class HapticFeedbackIntensityPreferenceController static final String PREF_KEY = "touch_vibration_preference_screen"; public HapticFeedbackIntensityPreferenceController(Context context) { - super(context, PREF_KEY, Settings.System.HAPTIC_FEEDBACK_INTENSITY); + super(context, PREF_KEY, Settings.System.HAPTIC_FEEDBACK_INTENSITY, + Settings.System.HAPTIC_FEEDBACK_ENABLED); } @Override diff --git a/src/com/android/settings/accessibility/HearingAidDialogFragment.java b/src/com/android/settings/accessibility/HearingAidDialogFragment.java new file mode 100644 index 0000000000..67b9d39cff --- /dev/null +++ b/src/com/android/settings/accessibility/HearingAidDialogFragment.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2018 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.accessibility; + +import android.app.Dialog; +import android.app.settings.SettingsEnums; +import android.content.DialogInterface; +import android.os.Bundle; + +import androidx.appcompat.app.AlertDialog; + +import com.android.settings.R; +import com.android.settings.bluetooth.BluetoothPairingDetail; +import com.android.settings.core.SubSettingLauncher; +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; + +public class HearingAidDialogFragment extends InstrumentedDialogFragment { + public static HearingAidDialogFragment newInstance() { + HearingAidDialogFragment frag = new HearingAidDialogFragment(); + return frag; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + return new AlertDialog.Builder(getActivity()) + .setTitle(R.string.accessibility_hearingaid_pair_instructions_first_message) + .setMessage(R.string.accessibility_hearingaid_pair_instructions_second_message) + .setPositiveButton(R.string.accessibility_hearingaid_instruction_continue_button, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + launchBluetoothAddDeviceSetting(); + } + }) + .setNegativeButton(android.R.string.cancel, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { } + }) + .create(); + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.DIALOG_ACCESSIBILITY_HEARINGAID; + } + + private void launchBluetoothAddDeviceSetting() { + new SubSettingLauncher(getActivity()) + .setDestination(BluetoothPairingDetail.class.getName()) + .setSourceMetricsCategory(SettingsEnums.ACCESSIBILITY) + .launch(); + } +} diff --git a/src/com/android/settings/accessibility/ListDialogPreference.java b/src/com/android/settings/accessibility/ListDialogPreference.java index cfb77bb3ea..7abc450f81 100644 --- a/src/com/android/settings/accessibility/ListDialogPreference.java +++ b/src/com/android/settings/accessibility/ListDialogPreference.java @@ -16,7 +16,6 @@ package com.android.settings.accessibility; -import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; @@ -31,12 +30,14 @@ import android.widget.AbsListView; import android.widget.AdapterView; import android.widget.BaseAdapter; -import com.android.settingslib.CustomDialogPreference; +import androidx.appcompat.app.AlertDialog.Builder; + +import com.android.settingslib.CustomDialogPreferenceCompat; /** * Abstract dialog preference that displays a set of values and optional titles. */ -public abstract class ListDialogPreference extends CustomDialogPreference { +public abstract class ListDialogPreference extends CustomDialogPreferenceCompat { private CharSequence[] mEntryTitles; private int[] mEntryValues; @@ -138,7 +139,7 @@ public abstract class ListDialogPreference extends CustomDialogPreference { } @Override - protected void onPrepareDialogBuilder(AlertDialog.Builder builder, + protected void onPrepareDialogBuilder(Builder builder, DialogInterface.OnClickListener listener) { super.onPrepareDialogBuilder(builder, listener); diff --git a/src/com/android/settings/accessibility/LiveCaptionPreferenceController.java b/src/com/android/settings/accessibility/LiveCaptionPreferenceController.java new file mode 100644 index 0000000000..2d12f67fd7 --- /dev/null +++ b/src/com/android/settings/accessibility/LiveCaptionPreferenceController.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2019 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.accessibility; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; + +import com.android.settings.core.BasePreferenceController; + +import java.util.List; + +public class LiveCaptionPreferenceController extends BasePreferenceController { + + @VisibleForTesting + static final Intent LIVE_CAPTION_INTENT = new Intent( + "com.android.settings.action.live_caption"); + + private final PackageManager mPackageManager; + + public LiveCaptionPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + mPackageManager = context.getPackageManager(); + } + + @Override + public int getAvailabilityStatus() { + final List<ResolveInfo> resolved = + mPackageManager.queryIntentActivities(LIVE_CAPTION_INTENT, 0 /* flags */); + return resolved != null && !resolved.isEmpty() + ? AVAILABLE + : UNSUPPORTED_ON_DEVICE; + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + preference.setIntent(LIVE_CAPTION_INTENT); + } +}
\ No newline at end of file diff --git a/src/com/android/settings/accessibility/LocalePreference.java b/src/com/android/settings/accessibility/LocalePreference.java index 4d3497d73f..a3723fd4ea 100644 --- a/src/com/android/settings/accessibility/LocalePreference.java +++ b/src/com/android/settings/accessibility/LocalePreference.java @@ -17,9 +17,10 @@ package com.android.settings.accessibility; import android.content.Context; -import androidx.preference.ListPreference; import android.util.AttributeSet; +import androidx.preference.ListPreference; + import com.android.internal.app.LocalePicker; import com.android.settings.R; diff --git a/src/com/android/settings/accessibility/MagnificationGesturesPreferenceController.java b/src/com/android/settings/accessibility/MagnificationGesturesPreferenceController.java index 16c354c301..f613c69e1b 100644 --- a/src/com/android/settings/accessibility/MagnificationGesturesPreferenceController.java +++ b/src/com/android/settings/accessibility/MagnificationGesturesPreferenceController.java @@ -16,9 +16,10 @@ package com.android.settings.accessibility; import android.content.Context; import android.os.Bundle; import android.provider.Settings; -import androidx.preference.Preference; import android.text.TextUtils; +import androidx.preference.Preference; + import com.android.settings.R; import com.android.settings.core.TogglePreferenceController; diff --git a/src/com/android/settings/accessibility/MagnificationNavbarPreferenceController.java b/src/com/android/settings/accessibility/MagnificationNavbarPreferenceController.java index ff12e27ff6..5af5d93056 100644 --- a/src/com/android/settings/accessibility/MagnificationNavbarPreferenceController.java +++ b/src/com/android/settings/accessibility/MagnificationNavbarPreferenceController.java @@ -16,9 +16,10 @@ package com.android.settings.accessibility; import android.content.Context; import android.os.Bundle; import android.provider.Settings; -import androidx.preference.Preference; import android.text.TextUtils; +import androidx.preference.Preference; + import com.android.settings.R; import com.android.settings.core.TogglePreferenceController; diff --git a/src/com/android/settings/accessibility/MagnificationPreferenceFragment.java b/src/com/android/settings/accessibility/MagnificationPreferenceFragment.java index e6875e479b..c3cb34fbcb 100644 --- a/src/com/android/settings/accessibility/MagnificationPreferenceFragment.java +++ b/src/com/android/settings/accessibility/MagnificationPreferenceFragment.java @@ -17,6 +17,7 @@ package com.android.settings.accessibility; import android.accessibilityservice.AccessibilityServiceInfo; +import android.app.settings.SettingsEnums; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; @@ -24,22 +25,24 @@ import android.content.res.Resources; import android.os.Bundle; import android.provider.SearchIndexableResource; import android.provider.Settings; -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; import android.text.TextUtils; import android.view.accessibility.AccessibilityManager; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; + import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; import com.android.settings.search.actionbar.SearchMenuController; import com.android.settings.support.actionbar.HelpResourceProvider; +import com.android.settingslib.search.SearchIndexable; import java.util.Arrays; import java.util.List; +@SearchIndexable public final class MagnificationPreferenceFragment extends DashboardFragment { @VisibleForTesting static final int ON = 1; @@ -59,7 +62,7 @@ public final class MagnificationPreferenceFragment extends DashboardFragment { @Override public int getMetricsCategory() { - return MetricsEvent.ACCESSIBILITY_SCREEN_MAGNIFICATION_SETTINGS; + return SettingsEnums.ACCESSIBILITY_SCREEN_MAGNIFICATION_SETTINGS; } @Override @@ -169,12 +172,5 @@ public final class MagnificationPreferenceFragment extends DashboardFragment { protected boolean isPageSearchEnabled(Context context) { return isApplicable(context.getResources()); } - - @Override - public List<String> getNonIndexableKeys(Context context) { - List<String> keys = super.getNonIndexableKeys(context); - keys.add(PREFERENCE_TITLE_KEY); - return keys; - } }; } diff --git a/src/com/android/settings/accessibility/NotificationVibrationIntensityPreferenceController.java b/src/com/android/settings/accessibility/NotificationVibrationIntensityPreferenceController.java index 362b59cffc..4ace4c6cdf 100644 --- a/src/com/android/settings/accessibility/NotificationVibrationIntensityPreferenceController.java +++ b/src/com/android/settings/accessibility/NotificationVibrationIntensityPreferenceController.java @@ -18,6 +18,7 @@ package com.android.settings.accessibility; import android.content.Context; import android.provider.Settings; + import androidx.annotation.VisibleForTesting; public class NotificationVibrationIntensityPreferenceController @@ -27,7 +28,7 @@ public class NotificationVibrationIntensityPreferenceController static final String PREF_KEY = "notification_vibration_preference_screen"; public NotificationVibrationIntensityPreferenceController(Context context) { - super(context, PREF_KEY, Settings.System.NOTIFICATION_VIBRATION_INTENSITY); + super(context, PREF_KEY, Settings.System.NOTIFICATION_VIBRATION_INTENSITY, ""); } @Override diff --git a/src/com/android/settings/accessibility/NotificationVibrationPreferenceFragment.java b/src/com/android/settings/accessibility/NotificationVibrationPreferenceFragment.java index 5f43c2d346..3ca197ffb1 100644 --- a/src/com/android/settings/accessibility/NotificationVibrationPreferenceFragment.java +++ b/src/com/android/settings/accessibility/NotificationVibrationPreferenceFragment.java @@ -15,12 +15,11 @@ */ package com.android.settings.accessibility; +import android.app.settings.SettingsEnums; import android.media.AudioAttributes; import android.os.Vibrator; -import android.os.VibrationEffect; import android.provider.Settings; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; /** @@ -29,7 +28,7 @@ import com.android.settings.R; public class NotificationVibrationPreferenceFragment extends VibrationPreferenceFragment { @Override public int getMetricsCategory() { - return MetricsEvent.ACCESSIBILITY_VIBRATION_NOTIFICATION; + return SettingsEnums.ACCESSIBILITY_VIBRATION_NOTIFICATION; } @Override @@ -46,6 +45,11 @@ public class NotificationVibrationPreferenceFragment extends VibrationPreference } @Override + protected String getVibrationEnabledSetting() { + return ""; + } + + @Override protected int getPreviewVibrationAudioAttributesUsage() { return AudioAttributes.USAGE_NOTIFICATION; } diff --git a/src/com/android/settings/accessibility/RingVibrationIntensityPreferenceController.java b/src/com/android/settings/accessibility/RingVibrationIntensityPreferenceController.java new file mode 100644 index 0000000000..4dee34824a --- /dev/null +++ b/src/com/android/settings/accessibility/RingVibrationIntensityPreferenceController.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2018 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.accessibility; + +import android.content.Context; +import android.provider.Settings; + +import androidx.annotation.VisibleForTesting; + +public class RingVibrationIntensityPreferenceController + extends VibrationIntensityPreferenceController { + + @VisibleForTesting + static final String PREF_KEY = "ring_vibration_preference_screen"; + + public RingVibrationIntensityPreferenceController(Context context) { + super(context, PREF_KEY, Settings.System.RING_VIBRATION_INTENSITY, + Settings.System.VIBRATE_WHEN_RINGING, /* supportRampingRinger= */ true); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } + + @Override + protected int getDefaultIntensity() { + return mVibrator.getDefaultRingVibrationIntensity(); + } +} diff --git a/src/com/android/settings/accessibility/RingVibrationPreferenceFragment.java b/src/com/android/settings/accessibility/RingVibrationPreferenceFragment.java new file mode 100644 index 0000000000..babfb9a200 --- /dev/null +++ b/src/com/android/settings/accessibility/RingVibrationPreferenceFragment.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2018 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.accessibility; + +import android.app.settings.SettingsEnums; +import android.media.AudioAttributes; +import android.os.Vibrator; +import android.provider.Settings; + +import com.android.settings.R; + +/** + * Fragment for picking accessibility shortcut service + */ +public class RingVibrationPreferenceFragment extends VibrationPreferenceFragment { + @Override + public int getMetricsCategory() { + return SettingsEnums.ACCESSIBILITY_VIBRATION_RING; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.accessibility_ring_vibration_settings; + } + + /** + * Get the setting string of the vibration intensity setting this preference is dealing with. + */ + @Override + protected String getVibrationIntensitySetting() { + return Settings.System.RING_VIBRATION_INTENSITY; + } + + @Override + protected String getVibrationEnabledSetting() { + if (AccessibilitySettings.isRampingRingerEnabled(getContext())) { + return Settings.Global.APPLY_RAMPING_RINGER; + } else { + return Settings.System.VIBRATE_WHEN_RINGING; + } + } + + @Override + protected int getPreviewVibrationAudioAttributesUsage() { + return AudioAttributes.USAGE_NOTIFICATION_RINGTONE; + } + + @Override + protected int getDefaultVibrationIntensity() { + Vibrator vibrator = getContext().getSystemService(Vibrator.class); + return vibrator.getDefaultRingVibrationIntensity(); + } +} diff --git a/src/com/android/settings/accessibility/ShortcutServicePickerFragment.java b/src/com/android/settings/accessibility/ShortcutServicePickerFragment.java index a8a960f767..dcf1f1cbf8 100644 --- a/src/com/android/settings/accessibility/ShortcutServicePickerFragment.java +++ b/src/com/android/settings/accessibility/ShortcutServicePickerFragment.java @@ -15,17 +15,14 @@ */ package com.android.settings.accessibility; -import static android.content.DialogInterface.BUTTON_POSITIVE; import static com.android.internal.accessibility.AccessibilityShortcutController.COLOR_INVERSION_COMPONENT_NAME; import static com.android.internal.accessibility.AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME; import android.accessibilityservice.AccessibilityServiceInfo; -import android.app.Activity; import android.app.Dialog; -import android.app.Fragment; +import android.app.settings.SettingsEnums; import android.content.ComponentName; import android.content.Context; -import android.content.DialogInterface; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; @@ -37,9 +34,12 @@ import android.os.UserHandle; import android.provider.Settings; import android.text.TextUtils; import android.view.accessibility.AccessibilityManager; +import android.view.View; + +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; import com.android.internal.accessibility.AccessibilityShortcutController; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.accessibility.AccessibilityShortcutController.ToggleableFrameworkFeatureInfo; import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; @@ -47,7 +47,6 @@ import com.android.settings.widget.RadioButtonPickerFragment; import com.android.settings.widget.RadioButtonPreference; import com.android.settingslib.accessibility.AccessibilityUtils; import com.android.settingslib.widget.CandidateInfo; -import com.android.settingslib.wrapper.PackageManagerWrapper; import java.util.ArrayList; import java.util.List; @@ -60,7 +59,7 @@ public class ShortcutServicePickerFragment extends RadioButtonPickerFragment { @Override public int getMetricsCategory() { - return MetricsEvent.ACCESSIBILITY_TOGGLE_GLOBAL_GESTURE; + return SettingsEnums.ACCESSIBILITY_TOGGLE_GLOBAL_GESTURE; } @Override @@ -132,10 +131,11 @@ public class ShortcutServicePickerFragment extends RadioButtonPickerFragment { // This is a framework feature. It doesn't need to be confirmed. onRadioButtonConfirmed(selectedKey); } else { - final Activity activity = getActivity(); + final FragmentActivity activity = getActivity(); if (activity != null) { ConfirmationDialogFragment.newInstance(this, selectedKey) - .show(activity.getFragmentManager(), ConfirmationDialogFragment.TAG); + .show(activity.getSupportFragmentManager(), + ConfirmationDialogFragment.TAG); } } } @@ -146,7 +146,7 @@ public class ShortcutServicePickerFragment extends RadioButtonPickerFragment { } public static class ConfirmationDialogFragment extends InstrumentedDialogFragment - implements DialogInterface.OnClickListener { + implements View.OnClickListener { private static final String EXTRA_KEY = "extra_key"; private static final String TAG = "ConfirmationDialogFragment"; private IBinder mToken; @@ -164,7 +164,7 @@ public class ShortcutServicePickerFragment extends RadioButtonPickerFragment { @Override public int getMetricsCategory() { - return MetricsEvent.ACCESSIBILITY_TOGGLE_GLOBAL_GESTURE; + return SettingsEnums.ACCESSIBILITY_TOGGLE_GLOBAL_GESTURE; } @Override @@ -180,13 +180,15 @@ public class ShortcutServicePickerFragment extends RadioButtonPickerFragment { } @Override - public void onClick(DialogInterface dialog, int which) { + public void onClick(View view) { final Fragment fragment = getTargetFragment(); - if ((which == BUTTON_POSITIVE) && (fragment instanceof ShortcutServicePickerFragment)) { + if ((view.getId() == R.id.permission_enable_allow_button) + && (fragment instanceof ShortcutServicePickerFragment)) { final Bundle bundle = getArguments(); ((ShortcutServicePickerFragment) fragment).onServiceConfirmed( bundle.getString(EXTRA_KEY)); } + dismiss(); } } @@ -229,10 +231,9 @@ public class ShortcutServicePickerFragment extends RadioButtonPickerFragment { @Override public CharSequence loadLabel() { - final PackageManagerWrapper pmw = - new PackageManagerWrapper(getContext().getPackageManager()); + final PackageManager pmw = getContext().getPackageManager(); final CharSequence label = - mServiceInfo.getResolveInfo().serviceInfo.loadLabel(pmw.getPackageManager()); + mServiceInfo.getResolveInfo().serviceInfo.loadLabel(pmw); if (label != null) { return label; } @@ -242,7 +243,7 @@ public class ShortcutServicePickerFragment extends RadioButtonPickerFragment { try { final ApplicationInfo appInfo = pmw.getApplicationInfoAsUser( componentName.getPackageName(), 0, UserHandle.myUserId()); - return appInfo.loadLabel(pmw.getPackageManager()); + return appInfo.loadLabel(pmw); } catch (PackageManager.NameNotFoundException e) { return null; } @@ -254,7 +255,7 @@ public class ShortcutServicePickerFragment extends RadioButtonPickerFragment { public Drawable loadIcon() { final ResolveInfo resolveInfo = mServiceInfo.getResolveInfo(); return (resolveInfo.getIconResource() == 0) - ? getContext().getDrawable(R.mipmap.ic_accessibility_generic) + ? getContext().getDrawable(R.drawable.ic_accessibility_generic) : resolveInfo.loadIcon(getContext().getPackageManager()); } diff --git a/src/com/android/settings/accessibility/ToggleAccessibilityServicePreferenceFragment.java b/src/com/android/settings/accessibility/ToggleAccessibilityServicePreferenceFragment.java index 53f7fb51cc..ec8df88d97 100644 --- a/src/com/android/settings/accessibility/ToggleAccessibilityServicePreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleAccessibilityServicePreferenceFragment.java @@ -16,29 +16,30 @@ package com.android.settings.accessibility; +import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; + import android.accessibilityservice.AccessibilityServiceInfo; import android.app.Activity; -import android.app.AlertDialog; import android.app.Dialog; import android.app.admin.DevicePolicyManager; +import android.app.settings.SettingsEnums; import android.content.ComponentName; -import android.content.DialogInterface; +import android.content.Context; import android.content.Intent; import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; import android.net.Uri; -import android.os.Binder; import android.os.Bundle; import android.os.Handler; -import android.os.IBinder; import android.os.UserHandle; import android.os.storage.StorageManager; import android.provider.Settings; import android.text.TextUtils; import android.view.Menu; import android.view.MenuInflater; +import android.view.View; import android.view.accessibility.AccessibilityManager; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.widget.LockPatternUtils; import com.android.settings.R; import com.android.settings.password.ConfirmDeviceCredentialActivity; @@ -49,10 +50,11 @@ import com.android.settingslib.accessibility.AccessibilityUtils; import java.util.List; public class ToggleAccessibilityServicePreferenceFragment - extends ToggleFeaturePreferenceFragment implements DialogInterface.OnClickListener { + extends ToggleFeaturePreferenceFragment implements View.OnClickListener { private static final int DIALOG_ID_ENABLE_WARNING = 1; private static final int DIALOG_ID_DISABLE_WARNING = 2; + private static final int DIALOG_ID_LAUNCH_ACCESSIBILITY_TUTORIAL = 3; public static final int ACTIVITY_REQUEST_CONFIRM_CREDENTIAL_FOR_WEAKER_ENCRYPTION = 1; @@ -60,7 +62,7 @@ public class ToggleAccessibilityServicePreferenceFragment private final SettingsContentObserver mSettingsContentObserver = new SettingsContentObserver(new Handler()) { - @Override + @Override public void onChange(boolean selfChange, Uri uri) { updateSwitchBarToggleSwitch(); } @@ -68,11 +70,11 @@ public class ToggleAccessibilityServicePreferenceFragment private ComponentName mComponentName; - private int mShownDialogId; + private Dialog mDialog; @Override public int getMetricsCategory() { - return MetricsEvent.ACCESSIBILITY_SERVICE; + return SettingsEnums.ACCESSIBILITY_SERVICE; } @Override @@ -130,43 +132,46 @@ public class ToggleAccessibilityServicePreferenceFragment public Dialog onCreateDialog(int dialogId) { switch (dialogId) { case DIALOG_ID_ENABLE_WARNING: { - mShownDialogId = DIALOG_ID_ENABLE_WARNING; final AccessibilityServiceInfo info = getAccessibilityServiceInfo(); if (info == null) { return null; } - - return AccessibilityServiceWarning + mDialog = AccessibilityServiceWarning .createCapabilitiesDialog(getActivity(), info, this); + break; } case DIALOG_ID_DISABLE_WARNING: { - mShownDialogId = DIALOG_ID_DISABLE_WARNING; AccessibilityServiceInfo info = getAccessibilityServiceInfo(); if (info == null) { return null; } - return new AlertDialog.Builder(getActivity()) - .setTitle(getString(R.string.disable_service_title, - info.getResolveInfo().loadLabel(getPackageManager()))) - .setMessage(getString(R.string.disable_service_message, - info.getResolveInfo().loadLabel(getPackageManager()))) - .setCancelable(true) - .setPositiveButton(android.R.string.ok, this) - .setNegativeButton(android.R.string.cancel, this) - .create(); + mDialog = AccessibilityServiceWarning + .createDisableDialog(getActivity(), info, this); + break; + } + case DIALOG_ID_LAUNCH_ACCESSIBILITY_TUTORIAL: { + if (isGestureNavigateEnabled()) { + mDialog = AccessibilityGestureNavigationTutorial + .showGestureNavigationTutorialDialog(getActivity()); + } else { + mDialog = AccessibilityGestureNavigationTutorial + .showAccessibilityButtonTutorialDialog(getActivity()); + } + break; } default: { throw new IllegalArgumentException(); } } + return mDialog; } @Override public int getDialogMetricsCategory(int dialogId) { if (dialogId == DIALOG_ID_ENABLE_WARNING) { - return MetricsEvent.DIALOG_ACCESSIBILITY_SERVICE_ENABLE; + return SettingsEnums.DIALOG_ACCESSIBILITY_SERVICE_ENABLE; } else { - return MetricsEvent.DIALOG_ACCESSIBILITY_SERVICE_DISABLE; + return SettingsEnums.DIALOG_ACCESSIBILITY_SERVICE_DISABLE; } } @@ -206,30 +211,53 @@ public class ToggleAccessibilityServicePreferenceFragment } @Override - public void onClick(DialogInterface dialog, int which) { - final boolean checked; - switch (which) { - case DialogInterface.BUTTON_POSITIVE: - if (mShownDialogId == DIALOG_ID_ENABLE_WARNING) { - if (isFullDiskEncrypted()) { - String title = createConfirmCredentialReasonMessage(); - Intent intent = ConfirmDeviceCredentialActivity.createIntent(title, null); - startActivityForResult(intent, - ACTIVITY_REQUEST_CONFIRM_CREDENTIAL_FOR_WEAKER_ENCRYPTION); - } else { - handleConfirmServiceEnabled(true); - } - } else { - handleConfirmServiceEnabled(false); + public void onClick(View view) { + if (view.getId() == R.id.permission_enable_allow_button) { + if (isFullDiskEncrypted()) { + String title = createConfirmCredentialReasonMessage(); + Intent intent = ConfirmDeviceCredentialActivity.createIntent(title, null); + startActivityForResult(intent, + ACTIVITY_REQUEST_CONFIRM_CREDENTIAL_FOR_WEAKER_ENCRYPTION); + } else { + handleConfirmServiceEnabled(true); + if (isServiceSupportAccessibilityButton()) { + showDialog(DIALOG_ID_LAUNCH_ACCESSIBILITY_TUTORIAL); } - break; - case DialogInterface.BUTTON_NEGATIVE: - checked = (mShownDialogId == DIALOG_ID_DISABLE_WARNING); - handleConfirmServiceEnabled(checked); - break; - default: - throw new IllegalArgumentException(); + } + } else if (view.getId() == R.id.permission_enable_deny_button) { + handleConfirmServiceEnabled(false); + } else if (view.getId() == R.id.permission_disable_stop_button) { + handleConfirmServiceEnabled(false); + } else if (view.getId() == R.id.permission_disable_cancel_button) { + handleConfirmServiceEnabled(true); + } else { + throw new IllegalArgumentException(); } + mDialog.dismiss(); + } + + private boolean isGestureNavigateEnabled() { + return getContext().getResources().getInteger( + com.android.internal.R.integer.config_navBarInteractionMode) + == NAV_BAR_MODE_GESTURAL; + } + + private boolean isServiceSupportAccessibilityButton() { + final AccessibilityManager ams = (AccessibilityManager) getContext().getSystemService( + Context.ACCESSIBILITY_SERVICE); + final List<AccessibilityServiceInfo> services = ams.getInstalledAccessibilityServiceList(); + + for (AccessibilityServiceInfo info : services) { + if ((info.flags & AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0) { + ServiceInfo serviceInfo = info.getResolveInfo().serviceInfo; + if (serviceInfo != null && TextUtils.equals(serviceInfo.name, + getAccessibilityServiceInfo().getResolveInfo().serviceInfo.name)) { + return true; + } + } + } + + return false; } private void handleConfirmServiceEnabled(boolean confirmed) { @@ -243,11 +271,13 @@ public class ToggleAccessibilityServicePreferenceFragment switch (mLockPatternUtils.getKeyguardStoredPasswordQuality(UserHandle.myUserId())) { case DevicePolicyManager.PASSWORD_QUALITY_SOMETHING: { resId = R.string.enable_service_pattern_reason; - } break; + } + break; case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC: case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX: { resId = R.string.enable_service_pin_reason; - } break; + } + break; } return getString(resId, getAccessibilityServiceInfo().getResolveInfo() .loadLabel(getPackageManager())); @@ -257,7 +287,7 @@ public class ToggleAccessibilityServicePreferenceFragment protected void onInstallSwitchBarToggleSwitch() { super.onInstallSwitchBarToggleSwitch(); mToggleSwitch.setOnBeforeCheckedChangeListener(new OnBeforeCheckedChangeListener() { - @Override + @Override public boolean onBeforeCheckedChanged(ToggleSwitch toggleSwitch, boolean checked) { if (checked) { mSwitchBar.setCheckedInternal(false); diff --git a/src/com/android/settings/accessibility/ToggleAutoclickPreferenceFragment.java b/src/com/android/settings/accessibility/ToggleAutoclickPreferenceFragment.java index 3f0fe0ce3c..8e12339f79 100644 --- a/src/com/android/settings/accessibility/ToggleAutoclickPreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleAutoclickPreferenceFragment.java @@ -16,22 +16,32 @@ package com.android.settings.accessibility; +import android.app.settings.SettingsEnums; +import android.content.Context; import android.content.res.Resources; import android.os.Bundle; +import android.provider.SearchIndexableResource; import android.provider.Settings; -import androidx.preference.Preference; import android.view.accessibility.AccessibilityManager; import android.widget.Switch; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.preference.Preference; + import com.android.settings.R; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; import com.android.settings.widget.SeekBarPreference; import com.android.settings.widget.SwitchBar; +import com.android.settingslib.search.SearchIndexable; + +import java.util.ArrayList; +import java.util.List; /** * Fragment for preference screen for settings related to Automatically click after mouse stops * feature. */ +@SearchIndexable public class ToggleAutoclickPreferenceFragment extends ToggleFeaturePreferenceFragment implements SwitchBar.OnSwitchChangeListener, Preference.OnPreferenceChangeListener { @@ -99,7 +109,7 @@ public class ToggleAutoclickPreferenceFragment extends ToggleFeaturePreferenceFr @Override public int getMetricsCategory() { - return MetricsEvent.ACCESSIBILITY_TOGGLE_AUTOCLICK; + return SettingsEnums.ACCESSIBILITY_TOGGLE_AUTOCLICK; } @Override @@ -177,4 +187,18 @@ public class ToggleAutoclickPreferenceFragment extends ToggleFeaturePreferenceFr private int delayToSeekBarProgress(int delay) { return (delay - MIN_AUTOCLICK_DELAY) / AUTOCLICK_DELAY_STEP; } + + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, + boolean enabled) { + final ArrayList<SearchIndexableResource> result = new ArrayList<>(); + + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.accessibility_autoclick_settings; + result.add(sir); + return result; + } + }; } diff --git a/src/com/android/settings/accessibility/ToggleDaltonizerPreferenceFragment.java b/src/com/android/settings/accessibility/ToggleDaltonizerPreferenceFragment.java index a38fd27537..892489974a 100644 --- a/src/com/android/settings/accessibility/ToggleDaltonizerPreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleDaltonizerPreferenceFragment.java @@ -16,17 +16,28 @@ package com.android.settings.accessibility; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.hardware.display.ColorDisplayManager; import android.os.Bundle; +import android.provider.SearchIndexableResource; import android.provider.Settings; -import androidx.preference.ListPreference; -import androidx.preference.Preference; import android.view.accessibility.AccessibilityManager; import android.widget.Switch; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.preference.ListPreference; +import androidx.preference.Preference; + import com.android.settings.R; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; import com.android.settings.widget.SwitchBar; +import com.android.settingslib.search.SearchIndexable; +import java.util.ArrayList; +import java.util.List; + +@SearchIndexable public class ToggleDaltonizerPreferenceFragment extends ToggleFeaturePreferenceFragment implements Preference.OnPreferenceChangeListener, SwitchBar.OnSwitchChangeListener { private static final String ENABLED = Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED; @@ -37,7 +48,7 @@ public class ToggleDaltonizerPreferenceFragment extends ToggleFeaturePreferenceF @Override public int getMetricsCategory() { - return MetricsEvent.ACCESSIBILITY_TOGGLE_DALTONIZER; + return SettingsEnums.ACCESSIBILITY_TOGGLE_DALTONIZER; } @Override @@ -51,7 +62,7 @@ public class ToggleDaltonizerPreferenceFragment extends ToggleFeaturePreferenceF mType = (ListPreference) findPreference("type"); - if (!AccessibilitySettings.isColorTransformAccelerated(getActivity())) { + if (!ColorDisplayManager.isColorTransformAccelerated(getActivity())) { mFooterPreferenceMixin.createFooterPreference().setTitle( R.string.accessibility_display_daltonizer_preference_subtitle); } @@ -116,4 +127,19 @@ public class ToggleDaltonizerPreferenceFragment extends ToggleFeaturePreferenceF public void onSwitchChanged(Switch switchView, boolean isChecked) { onPreferenceToggled(mPreferenceKey, isChecked); } + + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, + boolean enabled) { + final ArrayList<SearchIndexableResource> result = new ArrayList<>(); + + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.accessibility_daltonizer_settings; + result.add(sir); + return result; + } + }; + } diff --git a/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java b/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java index 94a26b9d5a..fba5ddb311 100644 --- a/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java @@ -19,9 +19,10 @@ package com.android.settings.accessibility; import android.content.Intent; import android.content.pm.ResolveInfo; import android.os.Bundle; +import android.view.View; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.view.View; import com.android.settings.R; import com.android.settings.SettingsActivity; diff --git a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java index 3e5af894a8..ec3ebcc1a6 100644 --- a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java @@ -16,6 +16,10 @@ package com.android.settings.accessibility; +import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; + +import android.app.Dialog; +import android.app.settings.SettingsEnums; import android.content.ContentResolver; import android.content.Context; import android.content.res.Resources; @@ -25,9 +29,8 @@ import android.media.MediaPlayer; import android.media.MediaPlayer.OnPreparedListener; import android.net.Uri; import android.os.Bundle; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; -import androidx.preference.PreferenceViewHolder; +import android.provider.Settings; +import android.text.TextUtils; import android.view.Display; import android.view.ViewTreeObserver.OnGlobalLayoutListener; import android.view.WindowManager; @@ -36,13 +39,20 @@ import android.widget.RelativeLayout.LayoutParams; import android.widget.Switch; import android.widget.VideoView; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; +import androidx.preference.PreferenceViewHolder; + import com.android.settings.R; import com.android.settings.widget.SwitchBar; public class ToggleScreenMagnificationPreferenceFragment extends ToggleFeaturePreferenceFragment implements SwitchBar.OnSwitchChangeListener { + private static final int DIALOG_ID_GESTURE_NAVIGATION_TUTORIAL = 1; + + private Dialog mDialog; + protected class VideoPreference extends Preference { private ImageView mVideoBackgroundView; private OnGlobalLayoutListener mLayoutListener; @@ -161,9 +171,29 @@ public class ToggleScreenMagnificationPreferenceFragment extends } @Override + public Dialog onCreateDialog(int dialogId) { + if (dialogId == DIALOG_ID_GESTURE_NAVIGATION_TUTORIAL) { + if (isGestureNavigateEnabled()) { + mDialog = AccessibilityGestureNavigationTutorial + .showGestureNavigationTutorialDialog(getActivity()); + } else { + mDialog = AccessibilityGestureNavigationTutorial + .showAccessibilityButtonTutorialDialog(getActivity()); + } + } + + return mDialog; + } + + @Override public int getMetricsCategory() { // TODO: Distinguish between magnification modes - return MetricsEvent.ACCESSIBILITY_TOGGLE_SCREEN_MAGNIFICATION; + return SettingsEnums.ACCESSIBILITY_TOGGLE_SCREEN_MAGNIFICATION; + } + + @Override + public int getDialogMetricsCategory(int dialogId) { + return SettingsEnums.ACCESSIBILITY_TOGGLE_SCREEN_MAGNIFICATION; } @Override @@ -173,6 +203,11 @@ public class ToggleScreenMagnificationPreferenceFragment extends @Override protected void onPreferenceToggled(String preferenceKey, boolean enabled) { + if (enabled && TextUtils.equals( + Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED, + preferenceKey)) { + showDialog(DIALOG_ID_GESTURE_NAVIGATION_TUTORIAL); + } MagnificationPreferenceFragment.setChecked(getContentResolver(), preferenceKey, enabled); updateConfigurationWarningIfNeeded(); } @@ -223,6 +258,12 @@ public class ToggleScreenMagnificationPreferenceFragment extends } } + private boolean isGestureNavigateEnabled() { + return getContext().getResources().getInteger( + com.android.internal.R.integer.config_navBarInteractionMode) + == NAV_BAR_MODE_GESTURAL; + } + private void updateConfigurationWarningIfNeeded() { final CharSequence warningMessage = MagnificationPreferenceFragment.getConfigurationWarningStringForSecureSettingsKey( diff --git a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentForSetupWizard.java b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentForSetupWizard.java index 4338172867..5fe62a7e02 100644 --- a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentForSetupWizard.java +++ b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentForSetupWizard.java @@ -16,17 +16,15 @@ package com.android.settings.accessibility; +import android.app.settings.SettingsEnums; import android.os.Bundle; -import com.android.internal.logging.MetricsLogger; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; - public class ToggleScreenMagnificationPreferenceFragmentForSetupWizard extends ToggleScreenMagnificationPreferenceFragment { @Override public int getMetricsCategory() { - return MetricsEvent.SUW_ACCESSIBILITY_TOGGLE_SCREEN_MAGNIFICATION; + return SettingsEnums.SUW_ACCESSIBILITY_TOGGLE_SCREEN_MAGNIFICATION; } @Override @@ -37,7 +35,7 @@ public class ToggleScreenMagnificationPreferenceFragmentForSetupWizard if (mToggleSwitch.isChecked() != args.getBoolean(AccessibilitySettings.EXTRA_CHECKED)) { // TODO: Distinguish between magnification modes mMetricsFeatureProvider.action(getContext(), - MetricsEvent.SUW_ACCESSIBILITY_TOGGLE_SCREEN_MAGNIFICATION, + SettingsEnums.SUW_ACCESSIBILITY_TOGGLE_SCREEN_MAGNIFICATION, mToggleSwitch.isChecked()); } } diff --git a/src/com/android/settings/accessibility/ToggleScreenReaderPreferenceFragmentForSetupWizard.java b/src/com/android/settings/accessibility/ToggleScreenReaderPreferenceFragmentForSetupWizard.java index 9bd2598285..fc68e031e0 100644 --- a/src/com/android/settings/accessibility/ToggleScreenReaderPreferenceFragmentForSetupWizard.java +++ b/src/com/android/settings/accessibility/ToggleScreenReaderPreferenceFragmentForSetupWizard.java @@ -16,10 +16,9 @@ package com.android.settings.accessibility; +import android.app.settings.SettingsEnums; import android.os.Bundle; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; - public class ToggleScreenReaderPreferenceFragmentForSetupWizard extends ToggleAccessibilityServicePreferenceFragment { @@ -33,7 +32,7 @@ public class ToggleScreenReaderPreferenceFragmentForSetupWizard @Override public int getMetricsCategory() { - return MetricsEvent.SUW_ACCESSIBILITY_TOGGLE_SCREEN_READER; + return SettingsEnums.SUW_ACCESSIBILITY_TOGGLE_SCREEN_READER; } @Override @@ -41,7 +40,7 @@ public class ToggleScreenReaderPreferenceFragmentForSetupWizard // Log the final choice in value if it's different from the previous value. if (mToggleSwitch.isChecked() != mToggleSwitchWasInitiallyChecked) { mMetricsFeatureProvider.action(getContext(), - MetricsEvent.SUW_ACCESSIBILITY_TOGGLE_SCREEN_READER, mToggleSwitch.isChecked()); + SettingsEnums.SUW_ACCESSIBILITY_TOGGLE_SCREEN_READER, mToggleSwitch.isChecked()); } super.onStop(); diff --git a/src/com/android/settings/accessibility/ToggleSelectToSpeakPreferenceFragmentForSetupWizard.java b/src/com/android/settings/accessibility/ToggleSelectToSpeakPreferenceFragmentForSetupWizard.java index 2c9b58c662..be9e8d43cb 100644 --- a/src/com/android/settings/accessibility/ToggleSelectToSpeakPreferenceFragmentForSetupWizard.java +++ b/src/com/android/settings/accessibility/ToggleSelectToSpeakPreferenceFragmentForSetupWizard.java @@ -16,10 +16,9 @@ package com.android.settings.accessibility; +import android.app.settings.SettingsEnums; import android.os.Bundle; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; - public class ToggleSelectToSpeakPreferenceFragmentForSetupWizard extends ToggleAccessibilityServicePreferenceFragment { @@ -33,7 +32,7 @@ public class ToggleSelectToSpeakPreferenceFragmentForSetupWizard @Override public int getMetricsCategory() { - return MetricsEvent.SUW_ACCESSIBILITY_TOGGLE_SCREEN_READER; + return SettingsEnums.SUW_ACCESSIBILITY_TOGGLE_SCREEN_READER; } @Override @@ -41,7 +40,7 @@ public class ToggleSelectToSpeakPreferenceFragmentForSetupWizard // Log the final choice in value if it's different from the previous value. if (mToggleSwitch.isChecked() != mToggleSwitchWasInitiallyChecked) { mMetricsFeatureProvider.action(getContext(), - MetricsEvent.SUW_ACCESSIBILITY_TOGGLE_SELECT_TO_SPEAK, + SettingsEnums.SUW_ACCESSIBILITY_TOGGLE_SELECT_TO_SPEAK, mToggleSwitch.isChecked()); } diff --git a/src/com/android/settings/accessibility/TopLevelAccessibilityPreferenceController.java b/src/com/android/settings/accessibility/TopLevelAccessibilityPreferenceController.java new file mode 100644 index 0000000000..41040a0d8e --- /dev/null +++ b/src/com/android/settings/accessibility/TopLevelAccessibilityPreferenceController.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2019 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.accessibility; + +import android.content.Context; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; + +public class TopLevelAccessibilityPreferenceController extends BasePreferenceController { + + public TopLevelAccessibilityPreferenceController(Context context, + String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public int getAvailabilityStatus() { + return mContext.getResources().getBoolean(R.bool.config_show_top_level_accessibility) + ? AVAILABLE_UNSEARCHABLE + : UNSUPPORTED_ON_DEVICE; + } +} + diff --git a/src/com/android/settings/accessibility/TouchVibrationPreferenceFragment.java b/src/com/android/settings/accessibility/TouchVibrationPreferenceFragment.java index 1d2012452a..f6bbbf3f12 100644 --- a/src/com/android/settings/accessibility/TouchVibrationPreferenceFragment.java +++ b/src/com/android/settings/accessibility/TouchVibrationPreferenceFragment.java @@ -15,13 +15,11 @@ */ package com.android.settings.accessibility; -import android.graphics.drawable.Drawable; +import android.app.settings.SettingsEnums; import android.media.AudioAttributes; import android.os.Vibrator; -import android.os.VibrationEffect; import android.provider.Settings; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; /** @@ -30,7 +28,7 @@ import com.android.settings.R; public class TouchVibrationPreferenceFragment extends VibrationPreferenceFragment { @Override public int getMetricsCategory() { - return MetricsEvent.ACCESSIBILITY_VIBRATION_TOUCH; + return SettingsEnums.ACCESSIBILITY_VIBRATION_TOUCH; } @Override @@ -47,6 +45,11 @@ public class TouchVibrationPreferenceFragment extends VibrationPreferenceFragmen } @Override + protected String getVibrationEnabledSetting() { + return Settings.System.HAPTIC_FEEDBACK_ENABLED; + } + + @Override protected int getDefaultVibrationIntensity() { Vibrator vibrator = getContext().getSystemService(Vibrator.class); return vibrator.getDefaultHapticFeedbackIntensity(); @@ -56,13 +59,4 @@ public class TouchVibrationPreferenceFragment extends VibrationPreferenceFragmen protected int getPreviewVibrationAudioAttributesUsage() { return AudioAttributes.USAGE_ASSISTANCE_SONIFICATION; } - - @Override - public void onVibrationIntensitySelected(int intensity) { - // We want to keep HAPTIC_FEEDBACK_ENABLED consistent with this setting since some - // applications check it directly before triggering their own haptic feedback. - final boolean hapticFeedbackEnabled = !(intensity == Vibrator.VIBRATION_INTENSITY_OFF); - Settings.System.putInt(getContext().getContentResolver(), - Settings.System.HAPTIC_FEEDBACK_ENABLED, hapticFeedbackEnabled ? 1 : 0); - } } diff --git a/src/com/android/settings/accessibility/VibrationIntensityPreferenceController.java b/src/com/android/settings/accessibility/VibrationIntensityPreferenceController.java index ef7753513a..9d7117640c 100644 --- a/src/com/android/settings/accessibility/VibrationIntensityPreferenceController.java +++ b/src/com/android/settings/accessibility/VibrationIntensityPreferenceController.java @@ -23,6 +23,7 @@ import android.os.Handler; import android.os.Looper; import android.os.Vibrator; import android.provider.Settings; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; @@ -38,14 +39,18 @@ public abstract class VibrationIntensityPreferenceController extends BasePrefere protected final Vibrator mVibrator; private final SettingObserver mSettingsContentObserver; private final String mSettingKey; + private final String mEnabledKey; + private final boolean mSupportRampingRinger; private Preference mPreference; public VibrationIntensityPreferenceController(Context context, String prefkey, - String settingKey) { + String settingKey, String enabledKey, boolean supportRampingRinger) { super(context, prefkey); mVibrator = mContext.getSystemService(Vibrator.class); mSettingKey = settingKey; + mEnabledKey = enabledKey; + mSupportRampingRinger= supportRampingRinger; mSettingsContentObserver = new SettingObserver(settingKey) { @Override public void onChange(boolean selfChange, Uri uri) { @@ -54,6 +59,11 @@ public abstract class VibrationIntensityPreferenceController extends BasePrefere }; } + public VibrationIntensityPreferenceController(Context context, String prefkey, + String settingKey, String enabledKey) { + this(context, prefkey, settingKey, enabledKey, /* supportRampingRinger= */ false); + } + @Override public void onStart() { mContext.getContentResolver().registerContentObserver( @@ -77,8 +87,11 @@ public abstract class VibrationIntensityPreferenceController extends BasePrefere public CharSequence getSummary() { final int intensity = Settings.System.getInt(mContext.getContentResolver(), mSettingKey, getDefaultIntensity()); - return getIntensityString(mContext, intensity); - } + final boolean enabled = (Settings.System.getInt(mContext.getContentResolver(), + mEnabledKey, 1) == 1) || + (mSupportRampingRinger && AccessibilitySettings.isRampingRingerEnabled(mContext)); + return getIntensityString(mContext, enabled ? intensity : Vibrator.VIBRATION_INTENSITY_OFF); + } public static CharSequence getIntensityString(Context context, int intensity) { final boolean supportsMultipleIntensities = context.getResources().getBoolean( diff --git a/src/com/android/settings/accessibility/VibrationPreferenceFragment.java b/src/com/android/settings/accessibility/VibrationPreferenceFragment.java index 99557836aa..1803a8bafe 100644 --- a/src/com/android/settings/accessibility/VibrationPreferenceFragment.java +++ b/src/com/android/settings/accessibility/VibrationPreferenceFragment.java @@ -17,7 +17,6 @@ package com.android.settings.accessibility; import static android.os.Vibrator.VibrationIntensity; -import androidx.annotation.VisibleForTesting; import android.content.Context; import android.database.ContentObserver; import android.graphics.drawable.Drawable; @@ -27,11 +26,12 @@ import android.os.Handler; import android.os.VibrationEffect; import android.os.Vibrator; import android.provider.Settings; +import android.text.TextUtils; import android.util.ArrayMap; import android.util.Log; -import com.android.internal.accessibility.AccessibilityShortcutController; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.annotation.VisibleForTesting; + import com.android.settings.R; import com.android.settings.widget.RadioButtonPickerFragment; import com.android.settingslib.widget.CandidateInfo; @@ -106,6 +106,54 @@ public abstract class VibrationPreferenceFragment extends RadioButtonPickerFragm } } + private boolean hasVibrationEnabledSetting() { + return !TextUtils.isEmpty(getVibrationEnabledSetting()); + } + + private void updateSettings(VibrationIntensityCandidateInfo candidate) { + boolean vibrationEnabled = candidate.getIntensity() != Vibrator.VIBRATION_INTENSITY_OFF; + if (hasVibrationEnabledSetting()) { + // Update vibration enabled setting + final String vibrationEnabledSetting = getVibrationEnabledSetting(); + final boolean wasEnabled = TextUtils.equals( + vibrationEnabledSetting, Settings.Global.APPLY_RAMPING_RINGER) + ? true + : (Settings.System.getInt( + getContext().getContentResolver(), vibrationEnabledSetting, 1) == 1); + if (vibrationEnabled != wasEnabled) { + if (vibrationEnabledSetting.equals(Settings.Global.APPLY_RAMPING_RINGER)) { + Settings.Global.putInt(getContext().getContentResolver(), + vibrationEnabledSetting, 0); + } else { + Settings.System.putInt(getContext().getContentResolver(), + vibrationEnabledSetting, vibrationEnabled ? 1 : 0); + } + + int previousIntensity = Settings.System.getInt(getContext().getContentResolver(), + getVibrationIntensitySetting(), 0); + if (vibrationEnabled && previousIntensity == candidate.getIntensity()) { + // We can't play preview effect here for all cases because that causes a data + // race (VibratorService may access intensity settings before these settings + // are updated). But we can't just play it in intensity settings update + // observer, because the intensity settings are not changed if we turn the + // vibration off, then on. + // + // In this case we sould play the preview here. + // To be refactored in b/132952771 + playVibrationPreview(); + } + } + } + // There are two conditions that need to change the intensity. + // First: Vibration is enabled and we are changing its strength. + // Second: There is no setting to enable this vibration, change the intensity directly. + if (vibrationEnabled || !hasVibrationEnabledSetting()) { + // Update vibration intensity setting + Settings.System.putInt(getContext().getContentResolver(), + getVibrationIntensitySetting(), candidate.getIntensity()); + } + } + @Override public void onDetach() { super.onDetach(); @@ -118,6 +166,11 @@ public abstract class VibrationPreferenceFragment extends RadioButtonPickerFragm protected abstract String getVibrationIntensitySetting(); /** + * Get the setting string of the vibration enabledness setting this preference is dealing with. + */ + protected abstract String getVibrationEnabledSetting(); + + /** * Get the default intensity for the desired setting. */ protected abstract int getDefaultVibrationIntensity(); @@ -155,8 +208,17 @@ public abstract class VibrationPreferenceFragment extends RadioButtonPickerFragm @Override protected String getDefaultKey() { - final int vibrationIntensity = Settings.System.getInt(getContext().getContentResolver(), + int vibrationIntensity = Settings.System.getInt(getContext().getContentResolver(), getVibrationIntensitySetting(), getDefaultVibrationIntensity()); + final String vibrationEnabledSetting = getVibrationEnabledSetting(); + final boolean vibrationEnabled = TextUtils.equals( + vibrationEnabledSetting, Settings.Global.APPLY_RAMPING_RINGER) + ? true + : (Settings.System.getInt( + getContext().getContentResolver(), vibrationEnabledSetting, 1) == 1); + if (!vibrationEnabled) { + vibrationIntensity = Vibrator.VIBRATION_INTENSITY_OFF; + } for (VibrationIntensityCandidateInfo candidate : mCandidates.values()) { final boolean matchesIntensity = candidate.getIntensity() == vibrationIntensity; final boolean matchesOn = candidate.getKey().equals(KEY_INTENSITY_ON) @@ -175,8 +237,7 @@ public abstract class VibrationPreferenceFragment extends RadioButtonPickerFragm Log.e(TAG, "Tried to set unknown intensity (key=" + key + ")!"); return false; } - Settings.System.putInt(getContext().getContentResolver(), - getVibrationIntensitySetting(), candidate.getIntensity()); + updateSettings(candidate); onVibrationIntensitySelected(candidate.getIntensity()); return true; } diff --git a/src/com/android/settings/accessibility/VibrationSettings.java b/src/com/android/settings/accessibility/VibrationSettings.java index 83a5af6ac8..9d31220120 100644 --- a/src/com/android/settings/accessibility/VibrationSettings.java +++ b/src/com/android/settings/accessibility/VibrationSettings.java @@ -16,13 +16,14 @@ package com.android.settings.accessibility; +import android.app.settings.SettingsEnums; import android.content.Context; import android.provider.SearchIndexableResource; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.List; @@ -30,13 +31,14 @@ import java.util.List; /** * Accessibility settings for the vibration. */ +@SearchIndexable public class VibrationSettings extends DashboardFragment { private static final String TAG = "VibrationSettings"; @Override public int getMetricsCategory() { - return MetricsEvent.ACCESSIBILITY_VIBRATION; + return SettingsEnums.ACCESSIBILITY_VIBRATION; } @Override diff --git a/src/com/android/settings/accessibility/VideoPlayer.java b/src/com/android/settings/accessibility/VideoPlayer.java new file mode 100644 index 0000000000..8f94b768f6 --- /dev/null +++ b/src/com/android/settings/accessibility/VideoPlayer.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2019 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.accessibility; + +import android.content.Context; +import android.graphics.SurfaceTexture; +import android.media.MediaPlayer; +import android.view.Surface; +import android.view.TextureView; +import android.view.TextureView.SurfaceTextureListener; + +import androidx.annotation.GuardedBy; +import androidx.annotation.RawRes; + +/** + * Plays the video by {@link MediaPlayer} on {@link TextureView}, calls {@link #create(Context, int, + * TextureView)} to setup the listener for TextureView and start to play the video. Once this player + * is no longer used, call {@link #release()} so that MediaPlayer object can be released. + */ +public class VideoPlayer implements SurfaceTextureListener { + private final Context context; + private final Object mediaPlayerLock = new Object(); + // Media player object can't be used after it has been released, so it will be set to null. But + // VideoPlayer is asynchronized, media player object might be paused or resumed again before + // released media player is set to null. Therefore, lock mediaPlayer and mediaPlayerState by + // mediaPlayerLock keep their states consistent. + @GuardedBy("mediaPlayerLock") + private MediaPlayer mediaPlayer; + @GuardedBy("mediaPlayerLock") + private State mediaPlayerState = State.NONE; + @RawRes + private final int videoRes; + private Surface animationSurface; + + + /** + * Creates a {@link MediaPlayer} for a given resource id and starts playback when the surface + * for + * a given {@link TextureView} is ready. + */ + public static VideoPlayer create(Context context, @RawRes int videoRes, + TextureView textureView) { + return new VideoPlayer(context, videoRes, textureView); + } + + private VideoPlayer(Context context, @RawRes int videoRes, TextureView textureView) { + this.context = context; + this.videoRes = videoRes; + textureView.setSurfaceTextureListener(this); + } + + public void pause() { + synchronized (mediaPlayerLock) { + if (mediaPlayerState == State.STARTED) { + mediaPlayerState = State.PAUSED; + mediaPlayer.pause(); + } + } + } + + public void resume() { + synchronized (mediaPlayerLock) { + if (mediaPlayerState == State.PAUSED) { + mediaPlayer.start(); + mediaPlayerState = State.STARTED; + } + } + } + + /** Release media player when it's no longer needed. */ + public void release() { + synchronized (mediaPlayerLock) { + if (mediaPlayerState != State.NONE && mediaPlayerState != State.END) { + mediaPlayerState = State.END; + mediaPlayer.release(); + mediaPlayer = null; + } + } + if (animationSurface != null) { + animationSurface.release(); + animationSurface = null; + } + } + + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { + animationSurface = new Surface(surface); + synchronized (mediaPlayerLock) { + mediaPlayer = MediaPlayer.create(context, videoRes); + mediaPlayerState = State.PREPARED; + mediaPlayer.setSurface(animationSurface); + mediaPlayer.setLooping(true); + mediaPlayer.start(); + mediaPlayerState = State.STARTED; + } + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { + release(); + return false; + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surface) { + } + + /** + * The state of MediaPlayer object. Refer to + * https://developer.android.com/reference/android/media/MediaPlayer#StateDiagram. + */ + public enum State { + /** MediaPlayer objects has not be created. */ + NONE, + /** MediaPlayer objects is created by create() method. */ + PREPARED, + /** MediaPlayer is started. It can be paused by pause() method. */ + STARTED, + /** MediaPlayer object is paused. Calling start() to resume it. */ + PAUSED, + /** + * MediaPlayer object is stopped and cannot be started until calling prepare() or + * prepareAsync() + * methods. + */ + STOPPED, + /** MediaPlayer object is released. It cannot be used again. */ + END + } +} + diff --git a/src/com/android/settings/accounts/AccountDashboardFragment.java b/src/com/android/settings/accounts/AccountDashboardFragment.java index b97694031e..515008af59 100644 --- a/src/com/android/settings/accounts/AccountDashboardFragment.java +++ b/src/com/android/settings/accounts/AccountDashboardFragment.java @@ -17,28 +17,25 @@ package com.android.settings.accounts; import static android.provider.Settings.EXTRA_AUTHORITIES; -import android.app.Activity; +import android.app.settings.SettingsEnums; import android.content.Context; -import android.os.UserHandle; import android.provider.SearchIndexableResource; -import android.text.BidiFormatter; -import android.text.TextUtils; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; +import com.android.settings.SettingsPreferenceFragment; import com.android.settings.dashboard.DashboardFragment; -import com.android.settings.dashboard.SummaryLoader; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.users.AutoSyncDataPreferenceController; import com.android.settings.users.AutoSyncPersonalDataPreferenceController; import com.android.settings.users.AutoSyncWorkDataPreferenceController; -import com.android.settingslib.accounts.AuthenticatorHelper; import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +@SearchIndexable public class AccountDashboardFragment extends DashboardFragment { private static final String TAG = "AccountDashboardFrag"; @@ -46,7 +43,7 @@ public class AccountDashboardFragment extends DashboardFragment { @Override public int getMetricsCategory() { - return MetricsEvent.ACCOUNT; + return SettingsEnums.ACCOUNT; } @Override @@ -66,72 +63,26 @@ public class AccountDashboardFragment extends DashboardFragment { @Override protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { - final List<AbstractPreferenceController> controllers = new ArrayList<>(); final String[] authorities = getIntent().getStringArrayExtra(EXTRA_AUTHORITIES); - final AccountPreferenceController accountPrefController = - new AccountPreferenceController(context, this, authorities); - getLifecycle().addObserver(accountPrefController); - controllers.add(accountPrefController); - controllers.add(new AutoSyncDataPreferenceController(context, this /*parent */)); - controllers.add(new AutoSyncPersonalDataPreferenceController(context, this /*parent */)); - controllers.add(new AutoSyncWorkDataPreferenceController(context, this /* parent */)); - return controllers; + return buildPreferenceControllers(context, this /* parent */, authorities); } - private static class SummaryProvider implements SummaryLoader.SummaryProvider { - - private final Context mContext; - private final SummaryLoader mSummaryLoader; - - public SummaryProvider(Context context, SummaryLoader summaryLoader) { - mContext = context; - mSummaryLoader = summaryLoader; - } + private static List<AbstractPreferenceController> buildPreferenceControllers(Context context, + SettingsPreferenceFragment parent, String[] authorities) { + final List<AbstractPreferenceController> controllers = new ArrayList<>(); - @Override - public void setListening(boolean listening) { - if (listening) { - final AuthenticatorHelper authHelper = new AuthenticatorHelper(mContext, - UserHandle.of(UserHandle.myUserId()), null /* OnAccountsUpdateListener */); - final String[] types = authHelper.getEnabledAccountTypes(); - - final BidiFormatter bidiFormatter = BidiFormatter.getInstance(); - - CharSequence summary = null; - if (types == null || types.length == 0) { - summary = mContext.getString(R.string.account_dashboard_default_summary); - } else { - // Show up to 3 account types, ignore any null value - int accountToAdd = Math.min(3, types.length); - - for (int i = 0; i < types.length && accountToAdd > 0; i++) { - final CharSequence label = authHelper.getLabelForType(mContext, types[i]); - if (TextUtils.isEmpty(label)) { - continue; - } - if (summary == null) { - summary = bidiFormatter.unicodeWrap(label); - } else { - summary = mContext.getString(R.string.join_many_items_middle, summary, - bidiFormatter.unicodeWrap(label)); - } - accountToAdd--; - } - } - mSummaryLoader.setSummary(this, summary); - } + final AccountPreferenceController accountPrefController = + new AccountPreferenceController(context, parent, authorities); + if (parent != null) { + parent.getSettingsLifecycle().addObserver(accountPrefController); } + controllers.add(accountPrefController); + controllers.add(new AutoSyncDataPreferenceController(context, parent)); + controllers.add(new AutoSyncPersonalDataPreferenceController(context, parent)); + controllers.add(new AutoSyncWorkDataPreferenceController(context, parent)); + return controllers; } - public static final SummaryLoader.SummaryProviderFactory SUMMARY_PROVIDER_FACTORY - = new SummaryLoader.SummaryProviderFactory() { - @Override - public SummaryLoader.SummaryProvider createSummaryProvider(Activity activity, - SummaryLoader summaryLoader) { - return new SummaryProvider(activity, summaryLoader); - } - }; - public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = new BaseSearchIndexProvider() { @Override @@ -141,5 +92,12 @@ public class AccountDashboardFragment extends DashboardFragment { sir.xmlResId = R.xml.accounts_dashboard_settings; return Arrays.asList(sir); } + + @Override + public List<AbstractPreferenceController> createPreferenceControllers( + Context context) { + return buildPreferenceControllers( + context, null /* parent */, null /* authorities*/); + } }; }
\ No newline at end of file diff --git a/src/com/android/settings/accounts/AccountDetailDashboardFragment.java b/src/com/android/settings/accounts/AccountDetailDashboardFragment.java index 241f1f3e04..1485500931 100644 --- a/src/com/android/settings/accounts/AccountDetailDashboardFragment.java +++ b/src/com/android/settings/accounts/AccountDetailDashboardFragment.java @@ -15,16 +15,21 @@ */ package com.android.settings.accounts; +import static android.content.Intent.EXTRA_USER; + import android.accounts.Account; +import android.accounts.AccountManager; import android.app.Activity; +import android.app.settings.SettingsEnums; import android.content.Context; +import android.content.Intent; import android.os.Bundle; import android.os.UserHandle; import android.os.UserManager; + import androidx.annotation.VisibleForTesting; import androidx.preference.PreferenceScreen; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.Utils; import com.android.settings.dashboard.DashboardFragment; @@ -54,6 +59,8 @@ public class AccountDetailDashboardFragment extends DashboardFragment { String mAccountType; private AccountSyncPreferenceController mAccountSynController; private RemoveAccountPreferenceController mRemoveAccountController; + @VisibleForTesting + UserHandle mUserHandle; @Override public void onCreate(Bundle icicle) { @@ -61,7 +68,7 @@ public class AccountDetailDashboardFragment extends DashboardFragment { getPreferenceManager().setPreferenceComparisonCallback(null); Bundle args = getArguments(); final Activity activity = getActivity(); - UserHandle userHandle = Utils.getSecureTargetUser(activity.getActivityToken(), + mUserHandle = Utils.getSecureTargetUser(activity.getActivityToken(), (UserManager) getSystemService(Context.USER_SERVICE), args, activity.getIntent().getExtras()); if (args != null) { @@ -75,8 +82,8 @@ public class AccountDetailDashboardFragment extends DashboardFragment { mAccountType = args.getString(KEY_ACCOUNT_TYPE); } } - mAccountSynController.init(mAccount, userHandle); - mRemoveAccountController.init(mAccount, userHandle); + mAccountSynController.init(mAccount, mUserHandle); + mRemoveAccountController.init(mAccount, mUserHandle); } @Override @@ -88,9 +95,30 @@ public class AccountDetailDashboardFragment extends DashboardFragment { updateUi(); } + @VisibleForTesting + void finishIfAccountMissing() { + final Context context = getContext(); + final UserManager um = context.getSystemService(UserManager.class); + final AccountManager accountManager = context.getSystemService(AccountManager.class); + for (UserHandle userHandle : um.getUserProfiles()) { + for (Account account : accountManager.getAccountsAsUser(userHandle.getIdentifier())) { + if (account.equals(mAccount)) { + return; + } + } + } + finish(); + } + + @Override + public void onResume() { + super.onResume(); + finishIfAccountMissing(); + } + @Override public int getMetricsCategory() { - return MetricsEvent.ACCOUNT; + return SettingsEnums.ACCOUNT; } @Override @@ -116,22 +144,27 @@ public class AccountDetailDashboardFragment extends DashboardFragment { mRemoveAccountController = new RemoveAccountPreferenceController(context, this); controllers.add(mRemoveAccountController); controllers.add(new AccountHeaderPreferenceController( - context, getLifecycle(), getActivity(), this /* host */, getArguments())); + context, getSettingsLifecycle(), getActivity(), this /* host */, getArguments())); return controllers; } @Override protected boolean displayTile(Tile tile) { + if (!super.displayTile(tile)) { + return false; + } if (mAccountType == null) { return false; } - final Bundle metadata = tile.metaData; + final Bundle metadata = tile.getMetaData(); if (metadata == null) { return false; } final boolean display = mAccountType.equals(metadata.getString(METADATA_IA_ACCOUNT)); - if (display && tile.intent != null) { - tile.intent.putExtra(EXTRA_ACCOUNT_NAME, mAccount.name); + if (display) { + final Intent intent = tile.getIntent(); + intent.putExtra(EXTRA_ACCOUNT_NAME, mAccount.name); + intent.putExtra(EXTRA_USER, mUserHandle); } return display; } @@ -153,4 +186,4 @@ public class AccountDetailDashboardFragment extends DashboardFragment { accountTypePreferenceLoader.updatePreferenceIntents(prefs, mAccountType, mAccount); } } -}
\ No newline at end of file +} diff --git a/src/com/android/settings/accounts/AccountFeatureProvider.java b/src/com/android/settings/accounts/AccountFeatureProvider.java index ecde8fed14..9829ca64b4 100644 --- a/src/com/android/settings/accounts/AccountFeatureProvider.java +++ b/src/com/android/settings/accounts/AccountFeatureProvider.java @@ -18,17 +18,8 @@ package com.android.settings.accounts; import android.accounts.Account; import android.content.Context; -import android.util.FeatureFlagUtils; - -import com.android.settings.core.FeatureFlags; public interface AccountFeatureProvider { String getAccountType(); Account[] getAccounts(Context context); - /** - * Checks whether or not to display the new About Phone page. - */ - default boolean isAboutPhoneV2Enabled(Context context) { - return FeatureFlagUtils.isEnabled(context, FeatureFlags.ABOUT_PHONE_V2); - } } diff --git a/src/com/android/settings/accounts/AccountHeaderPreferenceController.java b/src/com/android/settings/accounts/AccountHeaderPreferenceController.java index 78713469c0..11fcaffb70 100644 --- a/src/com/android/settings/accounts/AccountHeaderPreferenceController.java +++ b/src/com/android/settings/accounts/AccountHeaderPreferenceController.java @@ -24,11 +24,11 @@ import android.app.Activity; import android.content.Context; import android.os.Bundle; import android.os.UserHandle; -import androidx.preference.PreferenceFragment; + +import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceScreen; import com.android.settings.R; -import com.android.settings.applications.LayoutPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.widget.EntityHeaderController; import com.android.settingslib.accounts.AuthenticatorHelper; @@ -36,6 +36,7 @@ import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnResume; +import com.android.settingslib.widget.LayoutPreference; public class AccountHeaderPreferenceController extends AbstractPreferenceController implements PreferenceControllerMixin, LifecycleObserver, OnResume { @@ -43,14 +44,14 @@ public class AccountHeaderPreferenceController extends AbstractPreferenceControl private static final String KEY_ACCOUNT_HEADER = "account_header"; private final Activity mActivity; - private final PreferenceFragment mHost; + private final PreferenceFragmentCompat mHost; private final Account mAccount; private final UserHandle mUserHandle; private LayoutPreference mHeaderPreference; public AccountHeaderPreferenceController(Context context, Lifecycle lifecycle, - Activity activity, PreferenceFragment host, Bundle args) { + Activity activity, PreferenceFragmentCompat host, Bundle args) { super(context); mActivity = activity; mHost = host; @@ -83,7 +84,7 @@ public class AccountHeaderPreferenceController extends AbstractPreferenceControl @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - mHeaderPreference = (LayoutPreference) screen.findPreference(KEY_ACCOUNT_HEADER); + mHeaderPreference = screen.findPreference(KEY_ACCOUNT_HEADER); } @Override diff --git a/src/com/android/settings/accounts/AccountPreference.java b/src/com/android/settings/accounts/AccountPreference.java index ad7a4b380e..eea3113125 100644 --- a/src/com/android/settings/accounts/AccountPreference.java +++ b/src/com/android/settings/accounts/AccountPreference.java @@ -19,11 +19,12 @@ package com.android.settings.accounts; import android.accounts.Account; import android.content.Context; import android.graphics.drawable.Drawable; -import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; import android.util.Log; import android.widget.ImageView; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + import com.android.settings.R; import java.util.ArrayList; diff --git a/src/com/android/settings/accounts/AccountPreferenceBase.java b/src/com/android/settings/accounts/AccountPreferenceBase.java index 54ddf64eaa..bdf9c98a05 100644 --- a/src/com/android/settings/accounts/AccountPreferenceBase.java +++ b/src/com/android/settings/accounts/AccountPreferenceBase.java @@ -33,8 +33,6 @@ import com.android.settings.Utils; import com.android.settingslib.accounts.AuthenticatorHelper; import com.android.settingslib.utils.ThreadUtils; -import java.util.Date; - abstract class AccountPreferenceBase extends SettingsPreferenceFragment implements AuthenticatorHelper.OnAccountsUpdateListener { @@ -129,9 +127,4 @@ abstract class AccountPreferenceBase extends SettingsPreferenceFragment protected CharSequence getLabelForType(final String accountType) { return mAuthenticatorHelper.getLabelForType(getActivity(), accountType); } - - protected String formatSyncDate(Date date) { - // TODO: Switch to using DateUtils.formatDateTime - return mDateFormat.format(date) + " " + mTimeFormat.format(date); - } } diff --git a/src/com/android/settings/accounts/AccountPreferenceController.java b/src/com/android/settings/accounts/AccountPreferenceController.java index fad2f13faf..1309cc2bb5 100644 --- a/src/com/android/settings/accounts/AccountPreferenceController.java +++ b/src/com/android/settings/accounts/AccountPreferenceController.java @@ -36,16 +36,17 @@ import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.UserHandle; import android.os.UserManager; -import androidx.preference.Preference; -import androidx.preference.Preference.OnPreferenceClickListener; -import androidx.preference.PreferenceGroup; -import androidx.preference.PreferenceScreen; import android.text.BidiFormatter; import android.util.ArrayMap; import android.util.Log; import android.util.SparseArray; -import com.android.internal.annotations.VisibleForTesting; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.Preference.OnPreferenceClickListener; +import androidx.preference.PreferenceGroup; +import androidx.preference.PreferenceScreen; + import com.android.settings.AccessiblePreferenceCategory; import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; @@ -191,12 +192,10 @@ public class AccountPreferenceController extends AbstractPreferenceController data.screenTitle = screenTitle; rawData.add(data); } - { - SearchIndexableRaw data = new SearchIndexableRaw(mContext); - data.title = res.getString(R.string.managed_profile_settings_title); - data.screenTitle = screenTitle; - rawData.add(data); - } + SearchIndexableRaw data = new SearchIndexableRaw(mContext); + data.title = res.getString(R.string.managed_profile_settings_title); + data.screenTitle = screenTitle; + rawData.add(data); } } } @@ -250,7 +249,7 @@ public class AccountPreferenceController extends AbstractPreferenceController new SubSettingLauncher(mContext) .setSourceMetricsCategory(mParent.getMetricsCategory()) .setDestination(ManagedProfileSettings.class.getName()) - .setTitle(R.string.managed_profile_settings_title) + .setTitleRes(R.string.managed_profile_settings_title) .setArguments(arguments) .launch(); @@ -299,6 +298,7 @@ public class AccountPreferenceController extends AbstractPreferenceController final ProfileData data = mProfiles.get(userInfo.id); if (data != null) { data.pendingRemoval = false; + data.userInfo = userInfo; if (userInfo.isEnabled()) { // recreate the authentication helper to refresh the list of enabled accounts data.authenticatorHelper = @@ -351,7 +351,7 @@ public class AccountPreferenceController extends AbstractPreferenceController RestrictedPreference preference = new RestrictedPreference(mParent.getPreferenceManager().getContext()); preference.setTitle(R.string.add_account_label); - preference.setIcon(R.drawable.ic_menu_add); + preference.setIcon(R.drawable.ic_add_24dp); preference.setOnPreferenceClickListener(this); preference.setOrder(ORDER_NEXT_TO_NEXT_TO_LAST); return preference; diff --git a/src/com/android/settings/accounts/AccountRestrictionHelper.java b/src/com/android/settings/accounts/AccountRestrictionHelper.java index 43c56ba9c4..05b27aa672 100644 --- a/src/com/android/settings/accounts/AccountRestrictionHelper.java +++ b/src/com/android/settings/accounts/AccountRestrictionHelper.java @@ -17,9 +17,11 @@ package com.android.settings.accounts; import android.annotation.UserIdInt; import android.content.Context; + import com.android.settings.AccessiblePreferenceCategory; -import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.RestrictedPreference; + import java.util.ArrayList; public class AccountRestrictionHelper { @@ -49,7 +51,8 @@ public class AccountRestrictionHelper { } public boolean hasBaseUserRestriction(String userRestriction, @UserIdInt int userId) { - return RestrictedLockUtils.hasBaseUserRestriction(mContext, userRestriction, userId); + return RestrictedLockUtilsInternal.hasBaseUserRestriction(mContext, userRestriction, + userId); } public AccessiblePreferenceCategory createAccessiblePreferenceCategory(Context context) { diff --git a/src/com/android/settings/accounts/AccountSyncPreferenceController.java b/src/com/android/settings/accounts/AccountSyncPreferenceController.java index a0c30ac9c7..cea8843973 100644 --- a/src/com/android/settings/accounts/AccountSyncPreferenceController.java +++ b/src/com/android/settings/accounts/AccountSyncPreferenceController.java @@ -19,16 +19,17 @@ package com.android.settings.accounts; import static android.content.Intent.EXTRA_USER; import android.accounts.Account; +import android.app.settings.SettingsEnums; import android.content.ContentResolver; import android.content.Context; import android.content.SyncAdapterType; import android.os.Bundle; import android.os.UserHandle; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.core.SubSettingLauncher; @@ -65,8 +66,8 @@ public class AccountSyncPreferenceController extends AbstractPreferenceControlle new SubSettingLauncher(mContext) .setDestination(AccountSyncSettings.class.getName()) .setArguments(args) - .setSourceMetricsCategory( MetricsProto.MetricsEvent.ACCOUNT) - .setTitle( R.string.account_sync_title) + .setSourceMetricsCategory( SettingsEnums.ACCOUNT) + .setTitleRes( R.string.account_sync_title) .launch(); return true; diff --git a/src/com/android/settings/accounts/AccountSyncSettings.java b/src/com/android/settings/accounts/AccountSyncSettings.java index 11867c2b10..ec74cd842b 100644 --- a/src/com/android/settings/accounts/AccountSyncSettings.java +++ b/src/com/android/settings/accounts/AccountSyncSettings.java @@ -19,8 +19,8 @@ package com.android.settings.accounts; import android.accounts.Account; import android.accounts.AccountManager; import android.app.Activity; -import android.app.AlertDialog; import android.app.Dialog; +import android.app.settings.SettingsEnums; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; @@ -35,21 +35,19 @@ import android.os.Binder; import android.os.Bundle; import android.os.UserHandle; import android.os.UserManager; -import androidx.preference.Preference; import android.text.TextUtils; +import android.text.format.DateUtils; import android.util.Log; -import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.appcompat.app.AlertDialog; +import androidx.preference.Preference; + import com.android.settings.R; import com.android.settings.Utils; +import com.android.settings.widget.EntityHeaderController; import com.google.android.collect.Lists; @@ -64,10 +62,6 @@ public class AccountSyncSettings extends AccountPreferenceBase { private static final int MENU_SYNC_CANCEL_ID = Menu.FIRST + 1; private static final int CANT_DO_ONETIME_SYNC_DIALOG = 102; - private TextView mUserId; - private TextView mProviderId; - private ImageView mProviderIcon; - private TextView mErrorInfoView; private Account mAccount; private ArrayList<SyncAdapterType> mInvisibleAdapters = Lists.newArrayList(); @@ -86,14 +80,14 @@ public class AccountSyncSettings extends AccountPreferenceBase { @Override public int getMetricsCategory() { - return MetricsEvent.ACCOUNTS_ACCOUNT_SYNC; + return SettingsEnums.ACCOUNTS_ACCOUNT_SYNC; } @Override public int getDialogMetricsCategory(int dialogId) { switch (dialogId) { case CANT_DO_ONETIME_SYNC_DIALOG: - return MetricsEvent.DIALOG_ACCOUNT_SYNC_CANNOT_ONETIME_SYNC; + return SettingsEnums.DIALOG_ACCOUNT_SYNC_CANNOT_ONETIME_SYNC; default: return 0; } @@ -102,36 +96,9 @@ public class AccountSyncSettings extends AccountPreferenceBase { @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); - setPreferenceScreen(null); addPreferencesFromResource(R.xml.account_sync_settings); getPreferenceScreen().setOrderingAsAdded(false); setAccessibilityTitle(); - - setHasOptionsMenu(true); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - final View view = inflater.inflate(R.layout.account_sync_screen, container, false); - - final ViewGroup prefs_container = view.findViewById(R.id.prefs_container); - Utils.prepareCustomPreferencesList(container, view, prefs_container, false); - View prefs = super.onCreateView(inflater, prefs_container, savedInstanceState); - prefs_container.addView(prefs); - - initializeUi(view); - - return view; - } - - protected void initializeUi(final View rootView) { - mErrorInfoView = (TextView) rootView.findViewById(R.id.sync_settings_error_info); - mErrorInfoView.setVisibility(View.GONE); - - mUserId = (TextView) rootView.findViewById(R.id.user_id); - mProviderId = (TextView) rootView.findViewById(R.id.provider_id); - mProviderIcon = (ImageView) rootView.findViewById(R.id.provider_icon); } @Override @@ -144,7 +111,7 @@ public class AccountSyncSettings extends AccountPreferenceBase { finish(); return; } - mAccount = (Account) arguments.getParcelable(ACCOUNT_KEY); + mAccount = arguments.getParcelable(ACCOUNT_KEY); if (!accountExists(mAccount)) { Log.e(TAG, "Account provided does not exist: " + mAccount); finish(); @@ -153,8 +120,16 @@ public class AccountSyncSettings extends AccountPreferenceBase { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Got account: " + mAccount); } - mUserId.setText(mAccount.name); - mProviderId.setText(mAccount.type); + final Activity activity = getActivity(); + final Preference pref = EntityHeaderController + .newInstance(activity, this, null /* header */) + .setRecyclerView(getListView(), getSettingsLifecycle()) + .setIcon(getDrawableForType(mAccount.type)) + .setLabel(mAccount.name) + .setSummary(getLabelForType(mAccount.type)) + .done(activity, getPrefContext()); + pref.setOrder(0); + getPreferenceScreen().addPreference(pref); } private void setAccessibilityTitle() { @@ -171,7 +146,6 @@ public class AccountSyncSettings extends AccountPreferenceBase { @Override public void onResume() { - removePreference("dummy"); mAuthenticatorHelper.listenToAccountUpdates(); updateAuthDescriptions(); onAccountsUpdate(Binder.getCallingUserHandle()); @@ -275,10 +249,13 @@ public class AccountSyncSettings extends AccountPreferenceBase { } if (preference instanceof SyncStateSwitchPreference) { SyncStateSwitchPreference syncPref = (SyncStateSwitchPreference) preference; - String authority = syncPref.getAuthority(); - Account account = syncPref.getAccount(); + final String authority = syncPref.getAuthority(); + if (TextUtils.isEmpty(authority)) { + return false; + } + final Account account = syncPref.getAccount(); final int userId = mUserHandle.getIdentifier(); - String packageName = syncPref.getPackageName(); + final String packageName = syncPref.getPackageName(); boolean syncAutomatically = ContentResolver.getSyncAutomaticallyAsUser(account, authority, userId); @@ -462,7 +439,7 @@ public class AccountSyncSettings extends AccountPreferenceBase { syncPref.setSummary(R.string.sync_in_progress); } else if (successEndTime != 0) { date.setTime(successEndTime); - final String timeString = formatSyncDate(date); + final String timeString = formatSyncDate(getContext(), date); syncPref.setSummary(getResources().getString(R.string.last_synced, timeString)); } else { syncPref.setSummary(""); @@ -480,7 +457,10 @@ public class AccountSyncSettings extends AccountPreferenceBase { syncPref.setOneTimeSyncMode(oneTimeSyncMode); syncPref.setChecked(oneTimeSyncMode || syncEnabled); } - mErrorInfoView.setVisibility(syncIsFailing ? View.VISIBLE : View.GONE); + if (syncIsFailing) { + mFooterPreferenceMixin.createFooterPreference() + .setTitle(R.string.sync_is_failing); + } } @Override @@ -535,7 +515,9 @@ public class AccountSyncSettings extends AccountPreferenceBase { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "looking for sync adapters that match account " + mAccount); } + cacheRemoveAllPrefs(getPreferenceScreen()); + getCachedPreference(EntityHeaderController.PREF_KEY_APP_HEADER); for (int j = 0, m = authorities.size(); j < m; j++) { final SyncAdapterType syncAdapter = authorities.get(j); // We could check services here.... @@ -559,20 +541,15 @@ public class AccountSyncSettings extends AccountPreferenceBase { removeCachedPrefs(getPreferenceScreen()); } - /** - * Updates the titlebar with an icon for the provider type. - */ - @Override - protected void onAuthDescriptionsUpdated() { - super.onAuthDescriptionsUpdated(); - if (mAccount != null) { - mProviderIcon.setImageDrawable(getDrawableForType(mAccount.type)); - mProviderId.setText(getLabelForType(mAccount.type)); - } - } - @Override public int getHelpResource() { return R.string.help_url_accounts; } + + private static String formatSyncDate(Context context, Date date) { + return DateUtils.formatDateTime(context, date.getTime(), + DateUtils.FORMAT_SHOW_DATE + | DateUtils.FORMAT_SHOW_YEAR + | DateUtils.FORMAT_SHOW_TIME); + } } diff --git a/src/com/android/settings/accounts/AccountTypePreference.java b/src/com/android/settings/accounts/AccountTypePreference.java index 66f8888ecf..c82a5990d5 100644 --- a/src/com/android/settings/accounts/AccountTypePreference.java +++ b/src/com/android/settings/accounts/AccountTypePreference.java @@ -24,12 +24,13 @@ import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.UserHandle; import android.os.UserManager; + import androidx.preference.Preference; import androidx.preference.Preference.OnPreferenceClickListener; import com.android.settings.Utils; import com.android.settings.core.SubSettingLauncher; -import com.android.settings.widget.AppPreference; +import com.android.settingslib.widget.apppreference.AppPreference; public class AccountTypePreference extends AppPreference implements OnPreferenceClickListener { /** @@ -107,7 +108,7 @@ public class AccountTypePreference extends AppPreference implements OnPreference new SubSettingLauncher(getContext()) .setDestination(mFragment) .setArguments(mFragmentArguments) - .setTitle(mTitleResPackageName, mTitleResId) + .setTitleRes(mTitleResPackageName, mTitleResId) .setSourceMetricsCategory(mMetricsCategory) .launch(); return true; diff --git a/src/com/android/settings/accounts/AccountTypePreferenceLoader.java b/src/com/android/settings/accounts/AccountTypePreferenceLoader.java index e811388066..d32b63013f 100644 --- a/src/com/android/settings/accounts/AccountTypePreferenceLoader.java +++ b/src/com/android/settings/accounts/AccountTypePreferenceLoader.java @@ -29,13 +29,14 @@ import android.content.pm.ResolveInfo; import android.content.res.Resources; import android.content.res.Resources.Theme; import android.os.UserHandle; -import androidx.preference.PreferenceFragment; +import android.text.TextUtils; +import android.util.Log; + import androidx.preference.Preference; import androidx.preference.Preference.OnPreferenceClickListener; +import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceGroup; import androidx.preference.PreferenceScreen; -import android.text.TextUtils; -import android.util.Log; import com.android.settings.R; import com.android.settings.core.SubSettingLauncher; @@ -60,9 +61,9 @@ public class AccountTypePreferenceLoader { private AuthenticatorHelper mAuthenticatorHelper; private UserHandle mUserHandle; - private PreferenceFragment mFragment; + private PreferenceFragmentCompat mFragment; - public AccountTypePreferenceLoader(PreferenceFragment fragment, + public AccountTypePreferenceLoader(PreferenceFragmentCompat fragment, AuthenticatorHelper authenticatorHelper, UserHandle userHandle) { mFragment = fragment; mAuthenticatorHelper = authenticatorHelper; @@ -235,7 +236,7 @@ public class AccountTypePreferenceLoader { ? ((Instrumentable) mFragment).getMetricsCategory() : Instrumentable.METRICS_CATEGORY_UNKNOWN; new SubSettingLauncher(preference.getContext()) - .setTitle(mTitleRes) + .setTitleRes(mTitleRes) .setDestination(mClass) .setSourceMetricsCategory(metricsCategory) .launch(); diff --git a/src/com/android/settings/accounts/AvatarViewMixin.java b/src/com/android/settings/accounts/AvatarViewMixin.java new file mode 100644 index 0000000000..9e762c7257 --- /dev/null +++ b/src/com/android/settings/accounts/AvatarViewMixin.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2018 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.accounts; + +import android.accounts.Account; +import android.app.ActivityManager; +import android.app.settings.SettingsEnums; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.widget.ImageView; + +import androidx.annotation.VisibleForTesting; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.OnLifecycleEvent; + +import com.android.settings.R; +import com.android.settings.homepage.SettingsHomepageActivity; +import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; +import com.android.settingslib.utils.ThreadUtils; + +import java.net.URISyntaxException; +import java.util.List; + +/** + * Avatar related work to the onStart method of registered observable classes + * in {@link SettingsHomepageActivity}. + */ +public class AvatarViewMixin implements LifecycleObserver { + private static final String TAG = "AvatarViewMixin"; + + @VisibleForTesting + static final Intent INTENT_GET_ACCOUNT_DATA = + new Intent("android.content.action.SETTINGS_ACCOUNT_DATA"); + + private static final String METHOD_GET_ACCOUNT_AVATAR = "getAccountAvatar"; + private static final String KEY_AVATAR_BITMAP = "account_avatar"; + private static final String KEY_ACCOUNT_NAME = "account_name"; + private static final String EXTRA_ACCOUNT_NAME = "extra.accountName"; + + private final Context mContext; + private final ImageView mAvatarView; + private final MutableLiveData<Bitmap> mAvatarImage; + private final ActivityManager mActivityManager; + + private String mAccountName; + + public AvatarViewMixin(SettingsHomepageActivity activity, ImageView avatarView) { + mContext = activity.getApplicationContext(); + mActivityManager = mContext.getSystemService(ActivityManager.class); + mAvatarView = avatarView; + mAvatarView.setOnClickListener(v -> { + Intent intent; + try { + final String uri = mContext.getResources().getString( + R.string.config_account_intent_uri); + intent = Intent.parseUri(uri, Intent.URI_INTENT_SCHEME); + } catch (URISyntaxException e) { + Log.w(TAG, "Error parsing avatar mixin intent, skipping", e); + return; + } + + if (!TextUtils.isEmpty(mAccountName)) { + intent.putExtra(EXTRA_ACCOUNT_NAME, mAccountName); + } + + final List<ResolveInfo> matchedIntents = + mContext.getPackageManager().queryIntentActivities(intent, + PackageManager.MATCH_SYSTEM_ONLY); + if (matchedIntents.isEmpty()) { + Log.w(TAG, "Cannot find any matching action VIEW_ACCOUNT intent."); + return; + } + + final MetricsFeatureProvider metricsFeatureProvider = FeatureFactory.getFactory( + mContext).getMetricsFeatureProvider(); + metricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, + SettingsEnums.CLICK_ACCOUNT_AVATAR, SettingsEnums.SETTINGS_HOMEPAGE, + null /* key */, Integer.MIN_VALUE /* value */); + + // Here may have two different UI while start the activity. + // It will display adding account UI when device has no any account. + // It will display account information page when intent added the specified account. + activity.startActivity(intent); + }); + + mAvatarImage = new MutableLiveData<>(); + mAvatarImage.observe(activity, bitmap -> { + avatarView.setImageBitmap(bitmap); + }); + } + + @OnLifecycleEvent(Lifecycle.Event.ON_START) + public void onStart() { + if (!mContext.getResources().getBoolean(R.bool.config_show_avatar_in_homepage)) { + Log.d(TAG, "Feature disabled by config. Skipping"); + return; + } + if (mActivityManager.isLowRamDevice()) { + Log.d(TAG, "Feature disabled on low ram device. Skipping"); + return; + } + if (hasAccount()) { + loadAccount(); + } else { + mAvatarView.setImageResource(R.drawable.ic_account_circle_24dp); + } + } + + @VisibleForTesting + boolean hasAccount() { + final Account accounts[] = FeatureFactory.getFactory( + mContext).getAccountFeatureProvider().getAccounts(mContext); + return (accounts != null) && (accounts.length > 0); + } + + private void loadAccount() { + final String authority = queryProviderAuthority(); + if (TextUtils.isEmpty(authority)) { + return; + } + + ThreadUtils.postOnBackgroundThread(() -> { + final Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) + .authority(authority) + .build(); + final Bundle bundle = mContext.getContentResolver().call(uri, + METHOD_GET_ACCOUNT_AVATAR, null /* arg */, null /* extras */); + final Bitmap bitmap = bundle.getParcelable(KEY_AVATAR_BITMAP); + mAccountName = bundle.getString(KEY_ACCOUNT_NAME, "" /* defaultValue */); + mAvatarImage.postValue(bitmap); + }); + } + + @VisibleForTesting + String queryProviderAuthority() { + final List<ResolveInfo> providers = + mContext.getPackageManager().queryIntentContentProviders(INTENT_GET_ACCOUNT_DATA, + PackageManager.MATCH_SYSTEM_ONLY); + if (providers.size() == 1) { + return providers.get(0).providerInfo.authority; + } else { + Log.w(TAG, "The size of the provider is " + providers.size()); + return null; + } + } +} diff --git a/src/com/android/settings/accounts/ChooseAccountFragment.java b/src/com/android/settings/accounts/ChooseAccountFragment.java new file mode 100644 index 0000000000..0d7956a3d3 --- /dev/null +++ b/src/com/android/settings/accounts/ChooseAccountFragment.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2018 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.accounts; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.os.UserHandle; +import android.os.UserManager; +import android.provider.SearchIndexableResource; + +import com.android.settings.R; +import com.android.settings.Utils; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; +import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.search.SearchIndexable; + +import java.util.ArrayList; +import java.util.List; + +/** + * Activity asking a user to select an account to be set up. + */ +@SearchIndexable +public class ChooseAccountFragment extends DashboardFragment { + + private static final String TAG = "ChooseAccountFragment"; + + @Override + public int getMetricsCategory() { + return SettingsEnums.ACCOUNTS_CHOOSE_ACCOUNT_ACTIVITY; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + + final String[] authorities = getIntent().getStringArrayExtra( + AccountPreferenceBase.AUTHORITIES_FILTER_KEY); + final String[] accountTypesFilter = getIntent().getStringArrayExtra( + AccountPreferenceBase.ACCOUNT_TYPES_FILTER_KEY); + final UserManager userManager = UserManager.get(getContext()); + final UserHandle userHandle = Utils.getSecureTargetUser(getActivity().getActivityToken(), + userManager, null /* arguments */, getIntent().getExtras()); + + use(ChooseAccountPreferenceController.class).initialize(authorities, accountTypesFilter, + userHandle, getActivity()); + use(EnterpriseDisclosurePreferenceController.class).setFooterPreferenceMixin( + mFooterPreferenceMixin); + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.add_account_settings; + } + + @Override + protected String getLogTag() { + return TAG; + } + + @Override + protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { + return buildControllers(context); + } + + private static List<AbstractPreferenceController> buildControllers(Context context) { + final List<AbstractPreferenceController> controllers = new ArrayList<>(); + controllers.add(new EnterpriseDisclosurePreferenceController(context)); + return controllers; + } + + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, + boolean enabled) { + final ArrayList<SearchIndexableResource> result = new ArrayList<>(); + + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.add_account_settings; + result.add(sir); + return result; + } + + @Override + public List<AbstractPreferenceController> createPreferenceControllers( + Context context) { + return buildControllers(context); + } + }; +} diff --git a/src/com/android/settings/accounts/ChooseAccountActivity.java b/src/com/android/settings/accounts/ChooseAccountPreferenceController.java index 35f51afa80..a217f017f9 100644 --- a/src/com/android/settings/accounts/ChooseAccountActivity.java +++ b/src/com/android/settings/accounts/ChooseAccountPreferenceController.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010 The Android Open Source Project + * Copyright (C) 2018 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. @@ -30,24 +30,16 @@ import android.content.SyncAdapterType; import android.content.pm.PackageManager; import android.content.res.Resources; import android.graphics.drawable.Drawable; -import android.os.Bundle; import android.os.UserHandle; -import android.os.UserManager; -import androidx.preference.Preference; -import androidx.preference.PreferenceGroup; import android.util.Log; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.internal.util.CharSequences; -import com.android.settings.R; -import com.android.settings.SettingsPreferenceFragment; -import com.android.settings.Utils; -import com.android.settings.enterprise.EnterprisePrivacyFeatureProvider; -import com.android.settings.overlay.FeatureFactory; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.core.BasePreferenceController; import com.android.settingslib.RestrictedLockUtils; -import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; -import com.android.settingslib.widget.FooterPreference; -import com.android.settingslib.widget.FooterPreferenceMixin; +import com.android.settingslib.RestrictedLockUtilsInternal; import com.google.android.collect.Maps; @@ -55,81 +47,75 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.Set; /** - * Activity asking a user to select an account to be set up. - * * An extra {@link UserHandle} can be specified in the intent as {@link EXTRA_USER}, if the user for * which the action needs to be performed is different to the one the Settings App will run in. */ -public class ChooseAccountActivity extends SettingsPreferenceFragment { +public class ChooseAccountPreferenceController extends BasePreferenceController { - private static final String TAG = "ChooseAccountActivity"; + private static final String TAG = "ChooseAccountPrefCtrler"; - private EnterprisePrivacyFeatureProvider mFeatureProvider; - private FooterPreference mEnterpriseDisclosurePreference = null; + private final List<ProviderEntry> mProviderList; + private final Map<String, AuthenticatorDescription> mTypeToAuthDescription; private String[] mAuthorities; - private PreferenceGroup mAddAccountGroup; - private final ArrayList<ProviderEntry> mProviderList = new ArrayList<ProviderEntry>(); - public HashSet<String> mAccountTypesFilter; + private Set<String> mAccountTypesFilter; private AuthenticatorDescription[] mAuthDescs; - private HashMap<String, ArrayList<String>> mAccountTypeToAuthorities = null; - private Map<String, AuthenticatorDescription> mTypeToAuthDescription - = new HashMap<String, AuthenticatorDescription>(); + private Map<String, List<String>> mAccountTypeToAuthorities; // The UserHandle of the user we are choosing an account for private UserHandle mUserHandle; - private UserManager mUm; + private Activity mActivity; + private PreferenceScreen mScreen; - private static class ProviderEntry implements Comparable<ProviderEntry> { - private final CharSequence name; - private final String type; - ProviderEntry(CharSequence providerName, String accountType) { - name = providerName; - type = accountType; - } + public ChooseAccountPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); - public int compareTo(ProviderEntry another) { - if (name == null) { - return -1; - } - if (another.name == null) { - return +1; + mProviderList = new ArrayList<>(); + mTypeToAuthDescription = new HashMap<>(); + } + + public void initialize(String[] authorities, String[] accountTypesFilter, UserHandle userHandle, + Activity activity) { + mActivity = activity; + mAuthorities = authorities; + mUserHandle = userHandle; + + if (accountTypesFilter != null) { + mAccountTypesFilter = new HashSet<>(); + for (String accountType : accountTypesFilter) { + mAccountTypesFilter.add(accountType); } - return CharSequences.compareToIgnoreCase(name, another.name); } } @Override - public int getMetricsCategory() { - return MetricsEvent.ACCOUNTS_CHOOSE_ACCOUNT_ACTIVITY; + public int getAvailabilityStatus() { + return AVAILABLE; } @Override - public void onCreate(Bundle icicle) { - super.onCreate(icicle); + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mScreen = screen; + updateAuthDescriptions(); + } - final Activity activity = getActivity(); - mFeatureProvider = FeatureFactory.getFactory(activity) - .getEnterprisePrivacyFeatureProvider(activity); + @Override + public boolean handlePreferenceTreeClick(Preference preference) { + if (!(preference instanceof ProviderPreference)) { + return false; + } - addPreferencesFromResource(R.xml.add_account_settings); - mAuthorities = getIntent().getStringArrayExtra( - AccountPreferenceBase.AUTHORITIES_FILTER_KEY); - String[] accountTypesFilter = getIntent().getStringArrayExtra( - AccountPreferenceBase.ACCOUNT_TYPES_FILTER_KEY); - if (accountTypesFilter != null) { - mAccountTypesFilter = new HashSet<String>(); - for (String accountType : accountTypesFilter) { - mAccountTypesFilter.add(accountType); - } + ProviderPreference pref = (ProviderPreference) preference; + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Attempting to add account of type " + pref.getAccountType()); } - mAddAccountGroup = getPreferenceScreen(); - mUm = UserManager.get(getContext()); - mUserHandle = Utils.getSecureTargetUser(getActivity().getActivityToken(), mUm, - null /* arguments */, getIntent().getExtras()); - updateAuthDescriptions(); + finishWithAccountType(pref.getAccountType()); + return true; } /** @@ -137,7 +123,7 @@ public class ChooseAccountActivity extends SettingsPreferenceFragment { * and update any UI that depends on AuthenticatorDescriptions in onAuthDescriptionsUpdated(). */ private void updateAuthDescriptions() { - mAuthDescs = AccountManager.get(getContext()).getAuthenticatorTypesAsUser( + mAuthDescs = AccountManager.get(mContext).getAuthenticatorTypesAsUser( mUserHandle.getIdentifier()); for (int i = 0; i < mAuthDescs.length; i++) { mTypeToAuthDescription.put(mAuthDescs[i].type, mAuthDescs[i]); @@ -148,12 +134,12 @@ public class ChooseAccountActivity extends SettingsPreferenceFragment { private void onAuthDescriptionsUpdated() { // Create list of providers to show on preference screen for (int i = 0; i < mAuthDescs.length; i++) { - String accountType = mAuthDescs[i].type; - CharSequence providerName = getLabelForType(accountType); + final String accountType = mAuthDescs[i].type; + final CharSequence providerName = getLabelForType(accountType); // Skip preferences for authorities not specified. If no authorities specified, // then include them all. - ArrayList<String> accountAuths = getAuthoritiesForAccountType(accountType); + final List<String> accountAuths = getAuthoritiesForAccountType(accountType); boolean addAccountPref = true; if (mAuthorities != null && mAuthorities.length > 0 && accountAuths != null) { addAccountPref = false; @@ -169,38 +155,39 @@ public class ChooseAccountActivity extends SettingsPreferenceFragment { addAccountPref = false; } if (addAccountPref) { - mProviderList.add(new ProviderEntry(providerName, accountType)); + mProviderList.add( + new ProviderEntry(providerName, accountType)); } else { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Skipped pref " + providerName + ": has no authority we need"); } } } - - final Context context = getPreferenceScreen().getContext(); + final Context context = mScreen.getContext(); if (mProviderList.size() == 1) { // There's only one provider that matches. If it is disabled by admin show the // support dialog otherwise run it. - EnforcedAdmin admin = RestrictedLockUtils.checkIfAccountManagementDisabled( - context, mProviderList.get(0).type, mUserHandle.getIdentifier()); + final RestrictedLockUtils.EnforcedAdmin admin = + RestrictedLockUtilsInternal.checkIfAccountManagementDisabled( + context, mProviderList.get(0).getType(), mUserHandle.getIdentifier()); if (admin != null) { - setResult(RESULT_CANCELED, RestrictedLockUtils.getShowAdminSupportDetailsIntent( - context, admin)); - finish(); + mActivity.setResult(RESULT_CANCELED, + RestrictedLockUtils.getShowAdminSupportDetailsIntent( + context, admin)); + mActivity.finish(); } else { - finishWithAccountType(mProviderList.get(0).type); + finishWithAccountType(mProviderList.get(0).getType()); } } else if (mProviderList.size() > 0) { Collections.sort(mProviderList); - mAddAccountGroup.removeAll(); for (ProviderEntry pref : mProviderList) { - Drawable drawable = getDrawableForType(pref.type); - ProviderPreference p = new ProviderPreference(getPreferenceScreen().getContext(), - pref.type, drawable, pref.name); + final Drawable drawable = getDrawableForType(pref.getType()); + final ProviderPreference p = new ProviderPreference(context, + pref.getType(), drawable, pref.getName()); + p.setKey(pref.getType().toString()); p.checkAccountManagementAndSetDisabled(mUserHandle.getIdentifier()); - mAddAccountGroup.addPreference(p); + mScreen.addPreference(p); } - addEnterpriseDisclosure(); } else { if (Log.isLoggable(TAG, Log.VERBOSE)) { final StringBuilder auths = new StringBuilder(); @@ -210,38 +197,25 @@ public class ChooseAccountActivity extends SettingsPreferenceFragment { } Log.v(TAG, "No providers found for authorities: " + auths); } - setResult(RESULT_CANCELED); - finish(); + mActivity.setResult(RESULT_CANCELED); + mActivity.finish(); } } - private void addEnterpriseDisclosure() { - final CharSequence disclosure = mFeatureProvider.getDeviceOwnerDisclosure(); - if (disclosure == null) { - return; - } - if (mEnterpriseDisclosurePreference == null) { - mEnterpriseDisclosurePreference = mFooterPreferenceMixin.createFooterPreference(); - mEnterpriseDisclosurePreference.setSelectable(false); - } - mEnterpriseDisclosurePreference.setTitle(disclosure); - mAddAccountGroup.addPreference(mEnterpriseDisclosurePreference); - } - - public ArrayList<String> getAuthoritiesForAccountType(String type) { + private List<String> getAuthoritiesForAccountType(String type) { if (mAccountTypeToAuthorities == null) { mAccountTypeToAuthorities = Maps.newHashMap(); - SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypesAsUser( + final SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypesAsUser( mUserHandle.getIdentifier()); for (int i = 0, n = syncAdapters.length; i < n; i++) { final SyncAdapterType sa = syncAdapters[i]; - ArrayList<String> authorities = mAccountTypeToAuthorities.get(sa.accountType); + List<String> authorities = mAccountTypeToAuthorities.get(sa.accountType); if (authorities == null) { - authorities = new ArrayList<String>(); + authorities = new ArrayList<>(); mAccountTypeToAuthorities.put(sa.accountType, authorities); } if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.d(TAG, "added authority " + sa.authority + " to accountType " + Log.v(TAG, "added authority " + sa.authority + " to accountType " + sa.accountType); } authorities.add(sa.authority); @@ -252,18 +226,20 @@ public class ChooseAccountActivity extends SettingsPreferenceFragment { /** * Gets an icon associated with a particular account type. If none found, return null. + * * @param accountType the type of account * @return a drawable for the icon or a default icon returned by * {@link PackageManager#getDefaultActivityIcon} if one cannot be found. */ - protected Drawable getDrawableForType(final String accountType) { + @VisibleForTesting + Drawable getDrawableForType(final String accountType) { Drawable icon = null; if (mTypeToAuthDescription.containsKey(accountType)) { try { - AuthenticatorDescription desc = mTypeToAuthDescription.get(accountType); - Context authContext = getActivity() + final AuthenticatorDescription desc = mTypeToAuthDescription.get(accountType); + final Context authContext = mActivity .createPackageContextAsUser(desc.packageName, 0, mUserHandle); - icon = getPackageManager().getUserBadgedIcon( + icon = mContext.getPackageManager().getUserBadgedIcon( authContext.getDrawable(desc.iconId), mUserHandle); } catch (PackageManager.NameNotFoundException e) { Log.w(TAG, "No icon name for account type " + accountType); @@ -274,21 +250,23 @@ public class ChooseAccountActivity extends SettingsPreferenceFragment { if (icon != null) { return icon; } else { - return getPackageManager().getDefaultActivityIcon(); + return mContext.getPackageManager().getDefaultActivityIcon(); } } /** * Gets the label associated with a particular account type. If none found, return null. + * * @param accountType the type of account * @return a CharSequence for the label or null if one cannot be found. */ - protected CharSequence getLabelForType(final String accountType) { + @VisibleForTesting + CharSequence getLabelForType(final String accountType) { CharSequence label = null; if (mTypeToAuthDescription.containsKey(accountType)) { try { - AuthenticatorDescription desc = mTypeToAuthDescription.get(accountType); - Context authContext = getActivity() + final AuthenticatorDescription desc = mTypeToAuthDescription.get(accountType); + final Context authContext = mActivity .createPackageContextAsUser(desc.packageName, 0, mUserHandle); label = authContext.getResources().getText(desc.labelId); } catch (PackageManager.NameNotFoundException e) { @@ -300,23 +278,11 @@ public class ChooseAccountActivity extends SettingsPreferenceFragment { return label; } - @Override - public boolean onPreferenceTreeClick(Preference preference) { - if (preference instanceof ProviderPreference) { - ProviderPreference pref = (ProviderPreference) preference; - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "Attempting to add account of type " + pref.getAccountType()); - } - finishWithAccountType(pref.getAccountType()); - } - return true; - } - private void finishWithAccountType(String accountType) { Intent intent = new Intent(); intent.putExtra(AddAccountSettings.EXTRA_SELECTED_ACCOUNT, accountType); intent.putExtra(EXTRA_USER, mUserHandle); - setResult(RESULT_OK, intent); - finish(); + mActivity.setResult(RESULT_OK, intent); + mActivity.finish(); } } diff --git a/src/com/android/settings/accounts/ContactSearchPreferenceController.java b/src/com/android/settings/accounts/ContactSearchPreferenceController.java new file mode 100644 index 0000000000..8b94ba1f85 --- /dev/null +++ b/src/com/android/settings/accounts/ContactSearchPreferenceController.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2018 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.accounts; + +import static android.provider.Settings.Secure.MANAGED_PROFILE_CONTACT_REMOTE_SEARCH; + +import android.content.Context; +import android.os.UserHandle; +import android.provider.Settings; + +import androidx.preference.Preference; + +import com.android.settings.core.BasePreferenceController; +import com.android.settings.slices.SliceData; +import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; +import com.android.settingslib.RestrictedSwitchPreference; + +public class ContactSearchPreferenceController extends BasePreferenceController implements + Preference.OnPreferenceChangeListener { + + private UserHandle mManagedUser; + + public ContactSearchPreferenceController(Context context, String key) { + super(context, key); + } + + public void setManagedUser(UserHandle managedUser) { + mManagedUser = managedUser; + } + + @Override + public int getAvailabilityStatus() { + return (mManagedUser != null) ? AVAILABLE : DISABLED_FOR_USER; + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + if (preference instanceof RestrictedSwitchPreference) { + final RestrictedSwitchPreference pref = (RestrictedSwitchPreference) preference; + pref.setChecked(isChecked()); + if (mManagedUser != null) { + final RestrictedLockUtils.EnforcedAdmin enforcedAdmin = + RestrictedLockUtilsInternal.checkIfRemoteContactSearchDisallowed( + mContext, mManagedUser.getIdentifier()); + pref.setDisabledByAdmin(enforcedAdmin); + } + } + } + + private boolean isChecked() { + if (mManagedUser == null) { + return false; + } + return 0 != Settings.Secure.getIntForUser(mContext.getContentResolver(), + MANAGED_PROFILE_CONTACT_REMOTE_SEARCH, 0, mManagedUser.getIdentifier()); + } + + private boolean setChecked(boolean isChecked) { + if (mManagedUser != null) { + final int value = isChecked ? 1 : 0; + Settings.Secure.putIntForUser(mContext.getContentResolver(), + MANAGED_PROFILE_CONTACT_REMOTE_SEARCH, value, mManagedUser.getIdentifier()); + } + return true; + } + + @Override + public final boolean onPreferenceChange(Preference preference, Object newValue) { + return setChecked((boolean) newValue); + } + + @Override + @SliceData.SliceType + public int getSliceType() { + return SliceData.SliceType.SWITCH; + } +}
\ No newline at end of file diff --git a/src/com/android/settings/accounts/CrossProfileCalendarDisabledPreferenceController.java b/src/com/android/settings/accounts/CrossProfileCalendarDisabledPreferenceController.java new file mode 100644 index 0000000000..c087982379 --- /dev/null +++ b/src/com/android/settings/accounts/CrossProfileCalendarDisabledPreferenceController.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2019 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.accounts; +import static com.android.settings.accounts.CrossProfileCalendarPreferenceController.isCrossProfileCalendarDisallowedByAdmin; + +import android.content.Context; +import android.os.UserHandle; + +import com.android.settings.core.BasePreferenceController; + +public class CrossProfileCalendarDisabledPreferenceController extends BasePreferenceController { + private UserHandle mManagedUser; + + public void setManagedUser(UserHandle managedUser) { + mManagedUser = managedUser; + } + + public CrossProfileCalendarDisabledPreferenceController(Context context, + String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public int getAvailabilityStatus() { + if (mManagedUser != null + && isCrossProfileCalendarDisallowedByAdmin( + mContext, mManagedUser.getIdentifier())) { + return AVAILABLE; + } + + return DISABLED_FOR_USER; + } +} diff --git a/src/com/android/settings/accounts/CrossProfileCalendarPreferenceController.java b/src/com/android/settings/accounts/CrossProfileCalendarPreferenceController.java new file mode 100644 index 0000000000..863e790719 --- /dev/null +++ b/src/com/android/settings/accounts/CrossProfileCalendarPreferenceController.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2018 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.accounts; + +import static android.provider.Settings.Secure.CROSS_PROFILE_CALENDAR_ENABLED; + +import android.app.admin.DevicePolicyManager; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.Log; + +import com.android.settings.core.TogglePreferenceController; + +import java.util.Set; + +public class CrossProfileCalendarPreferenceController extends TogglePreferenceController { + + private static final String TAG = "CrossProfileCalendarPreferenceController"; + + private UserHandle mManagedUser; + + public CrossProfileCalendarPreferenceController(Context context, String key) { + super(context, key); + } + + public void setManagedUser(UserHandle managedUser) { + mManagedUser = managedUser; + } + + @Override + public int getAvailabilityStatus() { + if (mManagedUser != null + && !isCrossProfileCalendarDisallowedByAdmin( + mContext, mManagedUser.getIdentifier())) { + return AVAILABLE; + } + + return DISABLED_FOR_USER; + } + + @Override + public boolean isChecked() { + if (mManagedUser == null) { + return false; + } + return Settings.Secure.getIntForUser(mContext.getContentResolver(), + CROSS_PROFILE_CALENDAR_ENABLED, /* default= */ 0, + mManagedUser.getIdentifier()) == 1; + } + + @Override + public boolean setChecked(boolean isChecked) { + if (mManagedUser == null) { + return false; + } + final int value = isChecked ? 1 : 0; + return Settings.Secure.putIntForUser(mContext.getContentResolver(), + CROSS_PROFILE_CALENDAR_ENABLED, value, mManagedUser.getIdentifier()); + } + + static boolean isCrossProfileCalendarDisallowedByAdmin(Context context, int userId) { + final Context managedProfileContext = createPackageContextAsUser(context, userId); + final DevicePolicyManager dpm = managedProfileContext.getSystemService( + DevicePolicyManager.class); + if (dpm == null) { + return true; + } + final Set<String> packages = dpm.getCrossProfileCalendarPackages(); + return packages != null && packages.isEmpty(); + } + + private static Context createPackageContextAsUser(Context context, int userId) { + try { + return context.createPackageContextAsUser( + context.getPackageName(), 0 /* flags */, UserHandle.of(userId)); + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "Failed to create user context", e); + } + return null; + } +}
\ No newline at end of file diff --git a/src/com/android/settings/accounts/EmergencyInfoPreferenceController.java b/src/com/android/settings/accounts/EmergencyInfoPreferenceController.java index 95bc8e7d3c..5be829b837 100644 --- a/src/com/android/settings/accounts/EmergencyInfoPreferenceController.java +++ b/src/com/android/settings/accounts/EmergencyInfoPreferenceController.java @@ -22,24 +22,24 @@ import android.content.pm.UserInfo; import android.content.res.Resources; import android.os.UserHandle; import android.os.UserManager; +import android.text.TextUtils; + import androidx.preference.Preference; import com.android.settings.R; -import com.android.settings.core.PreferenceControllerMixin; +import com.android.settings.core.BasePreferenceController; import com.android.settings.search.SearchIndexableRaw; -import com.android.settingslib.core.AbstractPreferenceController; import java.util.List; -public class EmergencyInfoPreferenceController extends AbstractPreferenceController - implements PreferenceControllerMixin { +public class EmergencyInfoPreferenceController extends BasePreferenceController { - private static final String KEY_EMERGENCY_INFO = "emergency_info"; - private static final String ACTION_EDIT_EMERGENCY_INFO = "android.settings.EDIT_EMERGENCY_INFO"; - private static final String PACKAGE_NAME_EMERGENCY = "com.android.emergency"; + public static String getIntentAction(Context context) { + return context.getResources().getString(R.string.config_emergency_intent_action); + } - public EmergencyInfoPreferenceController(Context context) { - super(context); + public EmergencyInfoPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); } @Override @@ -53,16 +53,17 @@ public class EmergencyInfoPreferenceController extends AbstractPreferenceControl } } + @Override public void updateState(Preference preference) { UserInfo info = mContext.getSystemService(UserManager.class).getUserInfo( - UserHandle.myUserId()); + UserHandle.myUserId()); preference.setSummary(mContext.getString(R.string.emergency_info_summary, info.name)); } @Override public boolean handlePreferenceTreeClick(Preference preference) { - if (KEY_EMERGENCY_INFO.equals(preference.getKey())) { - Intent intent = new Intent(ACTION_EDIT_EMERGENCY_INFO); + if (TextUtils.equals(getPreferenceKey(), preference.getKey())) { + Intent intent = new Intent(getIntentAction(mContext)); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); mContext.startActivity(intent); return true; @@ -71,14 +72,19 @@ public class EmergencyInfoPreferenceController extends AbstractPreferenceControl } @Override - public boolean isAvailable() { - Intent intent = new Intent(ACTION_EDIT_EMERGENCY_INFO).setPackage(PACKAGE_NAME_EMERGENCY); - List<ResolveInfo> infos = mContext.getPackageManager().queryIntentActivities(intent, 0); - return infos != null && !infos.isEmpty(); + public int getAvailabilityStatus() { + if (!mContext.getResources().getBoolean(R.bool.config_show_emergency_info_in_device_info)) { + return UNSUPPORTED_ON_DEVICE; + } + final Intent intent = new Intent(getIntentAction(mContext)).setPackage( + getPackageName(mContext)); + final List<ResolveInfo> infos = mContext.getPackageManager().queryIntentActivities(intent, + 0); + return infos != null && !infos.isEmpty() + ? AVAILABLE : UNSUPPORTED_ON_DEVICE; } - @Override - public String getPreferenceKey() { - return KEY_EMERGENCY_INFO; + private static String getPackageName(Context context) { + return context.getResources().getString(R.string.config_emergency_package_name); } } diff --git a/src/com/android/settings/accounts/EnterpriseDisclosurePreferenceController.java b/src/com/android/settings/accounts/EnterpriseDisclosurePreferenceController.java new file mode 100644 index 0000000000..7a6e5fa253 --- /dev/null +++ b/src/com/android/settings/accounts/EnterpriseDisclosurePreferenceController.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2018 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.accounts; + +import android.content.Context; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.PreferenceScreen; + +import com.android.settings.core.BasePreferenceController; +import com.android.settings.enterprise.EnterprisePrivacyFeatureProvider; +import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.widget.FooterPreference; +import com.android.settingslib.widget.FooterPreferenceMixinCompat; + +public class EnterpriseDisclosurePreferenceController extends BasePreferenceController { + + private final EnterprisePrivacyFeatureProvider mFeatureProvider; + private FooterPreferenceMixinCompat mFooterPreferenceMixin; + private PreferenceScreen mScreen; + + public EnterpriseDisclosurePreferenceController(Context context) { + // Preference key doesn't matter as we are creating the preference in code. + super(context, "add_account_enterprise_disclosure_footer"); + + mFeatureProvider = FeatureFactory.getFactory(mContext) + .getEnterprisePrivacyFeatureProvider(mContext); + } + + public void setFooterPreferenceMixin(FooterPreferenceMixinCompat footerPreferenceMixin) { + mFooterPreferenceMixin = footerPreferenceMixin; + } + + @Override + public int getAvailabilityStatus() { + if (getDisclosure() == null) { + return UNSUPPORTED_ON_DEVICE; + } + return AVAILABLE; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mScreen = screen; + addEnterpriseDisclosure(); + } + + @VisibleForTesting + CharSequence getDisclosure() { + return mFeatureProvider.getDeviceOwnerDisclosure(); + } + + private void addEnterpriseDisclosure() { + final CharSequence disclosure = getDisclosure(); + if (disclosure == null) { + return; + } + final FooterPreference enterpriseDisclosurePreference = + mFooterPreferenceMixin.createFooterPreference(); + enterpriseDisclosurePreference.setSelectable(false); + enterpriseDisclosurePreference.setTitle(disclosure); + mScreen.addPreference(enterpriseDisclosurePreference); + } +} diff --git a/src/com/android/settings/accounts/ManagedProfileSettings.java b/src/com/android/settings/accounts/ManagedProfileSettings.java index 4227e6dec7..1f18d07ec7 100644 --- a/src/com/android/settings/accounts/ManagedProfileSettings.java +++ b/src/com/android/settings/accounts/ManagedProfileSettings.java @@ -16,76 +16,85 @@ package com.android.settings.accounts; +import android.app.admin.DevicePolicyManager; +import android.app.settings.SettingsEnums; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.PackageManager; import android.os.Bundle; import android.os.UserHandle; import android.os.UserManager; -import android.provider.Settings; -import androidx.preference.SwitchPreference; -import androidx.preference.Preference; +import android.provider.SearchIndexableResource; import android.util.Log; -import com.android.internal.logging.nano.MetricsProto; +import androidx.preference.Preference; +import androidx.preference.PreferenceGroup; +import androidx.preference.PreferenceManager; + import com.android.settings.R; -import com.android.settings.SettingsPreferenceFragment; import com.android.settings.Utils; -import com.android.settingslib.RestrictedLockUtils; -import com.android.settingslib.RestrictedSwitchPreference; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; +import com.android.settingslib.search.SearchIndexable; -import static android.provider.Settings.Secure.MANAGED_PROFILE_CONTACT_REMOTE_SEARCH; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; /** * Setting page for managed profile. * FIXME: It currently assumes there is only one managed profile. */ -public class ManagedProfileSettings extends SettingsPreferenceFragment - implements Preference.OnPreferenceChangeListener { - - private SwitchPreference mWorkModePreference; - private RestrictedSwitchPreference mContactPrefrence; +@SearchIndexable +public class ManagedProfileSettings extends DashboardFragment { private UserManager mUserManager; private UserHandle mManagedUser; - private Context mContext; private ManagedProfileBroadcastReceiver mManagedProfileBroadcastReceiver; - private static final String KEY_WORK_MODE = "work_mode"; - private static final String KEY_CONTACT = "contacts_search"; - private static final String TAG = "ManagedProfileSettings"; @Override - public void onCreate(Bundle icicle) { - super.onCreate(icicle); - addPreferencesFromResource(R.xml.managed_profile_settings); - mWorkModePreference = (SwitchPreference) findPreference(KEY_WORK_MODE); - mWorkModePreference.setOnPreferenceChangeListener(this); - mContactPrefrence = (RestrictedSwitchPreference) findPreference(KEY_CONTACT); - mContactPrefrence.setOnPreferenceChangeListener(this); - mContext = getActivity().getApplicationContext(); + protected String getLogTag() { + return TAG; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.managed_profile_settings; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); mUserManager = (UserManager) getSystemService(Context.USER_SERVICE); mManagedUser = getManagedUserFromArgument(); if (mManagedUser == null) { getActivity().finish(); } - mManagedProfileBroadcastReceiver = new ManagedProfileBroadcastReceiver(); - mManagedProfileBroadcastReceiver.register(getActivity()); + use(WorkModePreferenceController.class).setManagedUser(mManagedUser); + use(ContactSearchPreferenceController.class).setManagedUser(mManagedUser); + use(CrossProfileCalendarPreferenceController.class).setManagedUser(mManagedUser); + use(CrossProfileCalendarDisabledPreferenceController.class).setManagedUser(mManagedUser); } @Override - public void onResume() { - super.onResume(); - loadDataAndPopulateUi(); + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + mManagedProfileBroadcastReceiver = new ManagedProfileBroadcastReceiver(); + mManagedProfileBroadcastReceiver.register(getActivity()); } @Override public void onDestroy() { super.onDestroy(); - mManagedProfileBroadcastReceiver.unregister(getActivity()); + if (mManagedProfileBroadcastReceiver != null) { + mManagedProfileBroadcastReceiver.unregister(getActivity()); + } } private UserHandle getManagedUserFromArgument() { @@ -102,59 +111,38 @@ public class ManagedProfileSettings extends SettingsPreferenceFragment return Utils.getManagedProfile(mUserManager); } - private void loadDataAndPopulateUi() { - if (mWorkModePreference != null) { - updateWorkModePreference(); - } - - if (mContactPrefrence != null) { - int value = Settings.Secure.getIntForUser(getContentResolver(), - MANAGED_PROFILE_CONTACT_REMOTE_SEARCH, 0, mManagedUser.getIdentifier()); - mContactPrefrence.setChecked(value != 0); - RestrictedLockUtils.EnforcedAdmin enforcedAdmin = - RestrictedLockUtils.checkIfRemoteContactSearchDisallowed( - mContext, mManagedUser.getIdentifier()); - mContactPrefrence.setDisabledByAdmin(enforcedAdmin); - } - } - @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.ACCOUNTS_WORK_PROFILE_SETTINGS; + return SettingsEnums.ACCOUNTS_WORK_PROFILE_SETTINGS; } - private void updateWorkModePreference() { - boolean isWorkModeOn = !mUserManager.isQuietModeEnabled(mManagedUser); - mWorkModePreference.setChecked(isWorkModeOn); - mWorkModePreference.setSummary(isWorkModeOn - ? R.string.work_mode_on_summary - : R.string.work_mode_off_summary); - } - - - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - if (preference == mWorkModePreference) { - boolean quietModeEnabled = !(boolean) newValue; - mUserManager.requestQuietModeEnabled(quietModeEnabled, mManagedUser); - return true; - } - if (preference == mContactPrefrence) { - int value = ((boolean) newValue == true) ? 1 : 0; - Settings.Secure.putIntForUser(getContentResolver(), - MANAGED_PROFILE_CONTACT_REMOTE_SEARCH, value, mManagedUser.getIdentifier()); - return true; - } - return false; - } + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, + boolean enabled) { + final ArrayList<SearchIndexableResource> result = new ArrayList<>(); + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.managed_profile_settings; + result.add(sir); + return result; + } + @Override + protected boolean isPageSearchEnabled(Context context) { + return false; + } + }; private class ManagedProfileBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { + if (intent == null) { + return; + } final String action = intent.getAction(); Log.v(TAG, "Received broadcast: " + action); - if (action.equals(Intent.ACTION_MANAGED_PROFILE_REMOVED)) { + if (Intent.ACTION_MANAGED_PROFILE_REMOVED.equals(action)) { if (intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL) == mManagedUser.getIdentifier()) { getActivity().finish(); @@ -162,23 +150,12 @@ public class ManagedProfileSettings extends SettingsPreferenceFragment return; } - if (action.equals(Intent.ACTION_MANAGED_PROFILE_AVAILABLE) - || action.equals(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)) { - if (intent.getIntExtra(Intent.EXTRA_USER_HANDLE, - UserHandle.USER_NULL) == mManagedUser.getIdentifier()) { - updateWorkModePreference(); - } - return; - } Log.w(TAG, "Cannot handle received broadcast: " + intent.getAction()); } - public void register(Context context) { IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED); - intentFilter.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE); - intentFilter.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE); context.registerReceiver(this, intentFilter); } @@ -186,5 +163,4 @@ public class ManagedProfileSettings extends SettingsPreferenceFragment context.unregisterReceiver(this); } } - } diff --git a/src/com/android/settings/accounts/ProviderEntry.java b/src/com/android/settings/accounts/ProviderEntry.java new file mode 100644 index 0000000000..cf55e1423f --- /dev/null +++ b/src/com/android/settings/accounts/ProviderEntry.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2018 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.accounts; + +import com.android.internal.util.CharSequences; + +public class ProviderEntry implements Comparable<ProviderEntry> { + private final CharSequence name; + private final String type; + + ProviderEntry(CharSequence providerName, String accountType) { + name = providerName; + type = accountType; + } + + public int compareTo(ProviderEntry another) { + if (name == null) { + return -1; + } + if (another.name == null) { + return +1; + } + return CharSequences.compareToIgnoreCase(name, another.name); + } + + public CharSequence getName() { + return name; + } + + public String getType() { + return type; + } +}
\ No newline at end of file diff --git a/src/com/android/settings/accounts/ProviderPreference.java b/src/com/android/settings/accounts/ProviderPreference.java index 1143f8da40..b39f56192e 100644 --- a/src/com/android/settings/accounts/ProviderPreference.java +++ b/src/com/android/settings/accounts/ProviderPreference.java @@ -21,7 +21,7 @@ import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; import android.content.Context; import android.graphics.drawable.Drawable; -import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.RestrictedPreference; /** @@ -47,7 +47,7 @@ public class ProviderPreference extends RestrictedPreference { } public void checkAccountManagementAndSetDisabled(int userId) { - EnforcedAdmin admin = RestrictedLockUtils.checkIfAccountManagementDisabled( + EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfAccountManagementDisabled( getContext(), getAccountType(), userId); setDisabledByAdmin(admin); } diff --git a/src/com/android/settings/accounts/RemoveAccountPreferenceController.java b/src/com/android/settings/accounts/RemoveAccountPreferenceController.java index ecb849bb8a..5c6e6bb4e7 100644 --- a/src/com/android/settings/accounts/RemoveAccountPreferenceController.java +++ b/src/com/android/settings/accounts/RemoveAccountPreferenceController.java @@ -17,33 +17,34 @@ package com.android.settings.accounts; import android.accounts.Account; import android.accounts.AccountManager; -import android.accounts.AccountManagerCallback; -import android.accounts.AccountManagerFuture; import android.accounts.AuthenticatorException; import android.accounts.OperationCanceledException; import android.app.Activity; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.Fragment; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; import android.os.UserHandle; import android.os.UserManager; -import androidx.preference.PreferenceScreen; +import android.util.Log; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; -import com.android.internal.logging.nano.MetricsProto; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; +import androidx.preference.PreferenceScreen; + import com.android.settings.R; -import com.android.settings.applications.LayoutPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; +import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.widget.LayoutPreference; import java.io.IOException; @@ -64,8 +65,7 @@ public class RemoveAccountPreferenceController extends AbstractPreferenceControl @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - final LayoutPreference removeAccountPreference = - (LayoutPreference) screen.findPreference(KEY_REMOVE_ACCOUNT); + final LayoutPreference removeAccountPreference = screen.findPreference(KEY_REMOVE_ACCOUNT); Button removeAccountButton = (Button) removeAccountPreference.findViewById(R.id.button); removeAccountButton.setOnClickListener(this); } @@ -83,8 +83,8 @@ public class RemoveAccountPreferenceController extends AbstractPreferenceControl @Override public void onClick(View v) { if (mUserHandle != null) { - final EnforcedAdmin admin = RestrictedLockUtils.checkIfRestrictionEnforced(mContext, - UserManager.DISALLOW_MODIFY_ACCOUNTS, mUserHandle.getIdentifier()); + final EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced( + mContext, UserManager.DISALLOW_MODIFY_ACCOUNTS, mUserHandle.getIdentifier()); if (admin != null) { RestrictedLockUtils.sendShowAdminSupportDetailsIntent(mContext, admin); return; @@ -145,39 +145,34 @@ public class RemoveAccountPreferenceController extends AbstractPreferenceControl @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DIALOG_ACCOUNT_SYNC_REMOVE; + return SettingsEnums.DIALOG_ACCOUNT_SYNC_REMOVE; } @Override public void onClick(DialogInterface dialog, int which) { Activity activity = getTargetFragment().getActivity(); AccountManager.get(activity).removeAccountAsUser(mAccount, activity, - new AccountManagerCallback<Bundle>() { - @Override - public void run(AccountManagerFuture<Bundle> future) { - // If already out of this screen, don't proceed. - if (!getTargetFragment().isResumed()) { - return; - } - boolean failed = true; - try { - if (future.getResult() - .getBoolean(AccountManager.KEY_BOOLEAN_RESULT)) { - failed = false; - } - } catch (OperationCanceledException e) { - // handled below - } catch (IOException e) { - // handled below - } catch (AuthenticatorException e) { - // handled below - } - final Activity activity = getTargetFragment().getActivity(); - if (failed && activity != null && !activity.isFinishing()) { - RemoveAccountFailureDialog.show(getTargetFragment()); - } else { - activity.finish(); + future -> { + final Activity targetActivity = getTargetFragment().getActivity(); + if (targetActivity == null || targetActivity.isFinishing()) { + Log.w(TAG, "Activity is no longer alive, skipping results"); + return; + } + boolean failed = true; + try { + if (future.getResult() + .getBoolean(AccountManager.KEY_BOOLEAN_RESULT)) { + failed = false; } + } catch (OperationCanceledException + | IOException + | AuthenticatorException e) { + // handled below + } + if (failed) { + RemoveAccountFailureDialog.show(getTargetFragment()); + } else { + targetActivity.finish(); } }, null, mUserHandle); } @@ -196,7 +191,11 @@ public class RemoveAccountPreferenceController extends AbstractPreferenceControl } final RemoveAccountFailureDialog dialog = new RemoveAccountFailureDialog(); dialog.setTargetFragment(parent, 0); - dialog.show(parent.getFragmentManager(), FAILED_REMOVAL_DIALOG); + try { + dialog.show(parent.getFragmentManager(), FAILED_REMOVAL_DIALOG); + } catch (IllegalStateException e) { + Log.w(TAG, "Can't show RemoveAccountFailureDialog. " + e.getMessage()); + } } @Override @@ -212,7 +211,7 @@ public class RemoveAccountPreferenceController extends AbstractPreferenceControl @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DIALOG_ACCOUNT_SYNC_FAILED_REMOVAL; + return SettingsEnums.DIALOG_ACCOUNT_SYNC_FAILED_REMOVAL; } } diff --git a/src/com/android/settings/accounts/RemoveUserFragment.java b/src/com/android/settings/accounts/RemoveUserFragment.java index 0fcf64fa8c..0524b6317e 100644 --- a/src/com/android/settings/accounts/RemoveUserFragment.java +++ b/src/com/android/settings/accounts/RemoveUserFragment.java @@ -17,12 +17,12 @@ package com.android.settings.accounts; import android.app.Dialog; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; import android.os.UserManager; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; import com.android.settings.users.UserDialogs; @@ -53,6 +53,6 @@ public class RemoveUserFragment extends InstrumentedDialogFragment { @Override public int getMetricsCategory() { - return MetricsEvent.DIALOG_REMOVE_USER; + return SettingsEnums.DIALOG_REMOVE_USER; } } diff --git a/src/com/android/settings/accounts/SyncStateSwitchPreference.java b/src/com/android/settings/accounts/SyncStateSwitchPreference.java index b65b183811..9c7f739a68 100644 --- a/src/com/android/settings/accounts/SyncStateSwitchPreference.java +++ b/src/com/android/settings/accounts/SyncStateSwitchPreference.java @@ -19,14 +19,15 @@ package com.android.settings.accounts; import android.accounts.Account; import android.app.ActivityManager; import android.content.Context; -import androidx.preference.SwitchPreference; -import androidx.preference.PreferenceViewHolder; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.widget.TextView; +import androidx.preference.PreferenceViewHolder; +import androidx.preference.SwitchPreference; + import com.android.settings.R; import com.android.settingslib.widget.AnimatedImageView; diff --git a/src/com/android/settings/accounts/TopLevelAccountEntryPreferenceController.java b/src/com/android/settings/accounts/TopLevelAccountEntryPreferenceController.java new file mode 100644 index 0000000000..a8d93d589d --- /dev/null +++ b/src/com/android/settings/accounts/TopLevelAccountEntryPreferenceController.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2018 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.accounts; + +import android.content.Context; +import android.icu.text.ListFormatter; +import android.os.UserHandle; +import android.text.BidiFormatter; +import android.text.TextUtils; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; +import com.android.settingslib.accounts.AuthenticatorHelper; + +import java.util.ArrayList; +import java.util.List; + +public class TopLevelAccountEntryPreferenceController extends BasePreferenceController { + public TopLevelAccountEntryPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE_UNSEARCHABLE; + } + + @Override + public CharSequence getSummary() { + final AuthenticatorHelper authHelper = new AuthenticatorHelper(mContext, + UserHandle.of(UserHandle.myUserId()), null /* OnAccountsUpdateListener */); + final String[] types = authHelper.getEnabledAccountTypes(); + final BidiFormatter bidiFormatter = BidiFormatter.getInstance(); + final List<CharSequence> summaries = new ArrayList<>(); + + if (types == null || types.length == 0) { + summaries.add(mContext.getString(R.string.account_dashboard_default_summary)); + } else { + // Show up to 3 account types, ignore any null value + int accountToAdd = Math.min(3, types.length); + + for (int i = 0; i < types.length && accountToAdd > 0; i++) { + final CharSequence label = authHelper.getLabelForType(mContext, types[i]); + if (TextUtils.isEmpty(label)) { + continue; + } + + summaries.add(bidiFormatter.unicodeWrap(label)); + accountToAdd--; + } + } + return ListFormatter.getInstance().format(summaries); + } +} diff --git a/src/com/android/settings/accounts/WorkModePreferenceController.java b/src/com/android/settings/accounts/WorkModePreferenceController.java new file mode 100644 index 0000000000..f3a6ed2fa8 --- /dev/null +++ b/src/com/android/settings/accounts/WorkModePreferenceController.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2018 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.accounts; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.UserHandle; +import android.os.UserManager; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; +import androidx.preference.TwoStatePreference; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.slices.SliceData; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.core.lifecycle.events.OnStop; + +public class WorkModePreferenceController extends BasePreferenceController implements + Preference.OnPreferenceChangeListener, LifecycleObserver, OnStart, OnStop { + + private static final String TAG = "WorkModeController"; + + private UserManager mUserManager; + private UserHandle mManagedUser; + + private Preference mPreference; + private IntentFilter mIntentFilter; + + public WorkModePreferenceController(Context context, String key) { + super(context, key); + mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE); + mIntentFilter = new IntentFilter(); + mIntentFilter.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE); + mIntentFilter.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE); + } + + public void setManagedUser(UserHandle managedUser) { + mManagedUser = managedUser; + } + + @Override + public void onStart() { + mContext.registerReceiver(mReceiver, mIntentFilter); + } + + @Override + public void onStop() { + mContext.unregisterReceiver(mReceiver); + } + + @Override + public int getAvailabilityStatus() { + return (mManagedUser != null) ? AVAILABLE : DISABLED_FOR_USER; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + } + + @Override + public CharSequence getSummary() { + return mContext.getText(isChecked() + ? R.string.work_mode_on_summary + : R.string.work_mode_off_summary); + } + + private boolean isChecked() { + boolean isWorkModeOn = false; + if (mUserManager != null && mManagedUser != null) { + isWorkModeOn = !mUserManager.isQuietModeEnabled(mManagedUser); + } + return isWorkModeOn; + } + + private boolean setChecked(boolean isChecked) { + if (mUserManager != null && mManagedUser != null) { + final boolean quietModeEnabled = !isChecked; + mUserManager.requestQuietModeEnabled(quietModeEnabled, mManagedUser); + } + return true; + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + if (preference instanceof TwoStatePreference) { + ((TwoStatePreference) preference).setChecked(isChecked()); + } + } + + @Override + public final boolean onPreferenceChange(Preference preference, Object newValue) { + return setChecked((boolean) newValue); + } + + /** + * Receiver that listens to {@link Intent#ACTION_MANAGED_PROFILE_AVAILABLE} and + * {@link Intent#ACTION_MANAGED_PROFILE_UNAVAILABLE}, and updates the work mode + */ + @VisibleForTesting + final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent == null) { + return; + } + final String action = intent.getAction(); + Log.v(TAG, "Received broadcast: " + action); + + if (Intent.ACTION_MANAGED_PROFILE_AVAILABLE.equals(action) + || Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE.equals(action)) { + if (intent.getIntExtra(Intent.EXTRA_USER_HANDLE, + UserHandle.USER_NULL) == mManagedUser.getIdentifier()) { + updateState(mPreference); + } + return; + } + Log.w(TAG, "Cannot handle received broadcast: " + intent.getAction()); + } + }; + + @Override + @SliceData.SliceType + public int getSliceType() { + return SliceData.SliceType.SWITCH; + } +}
\ No newline at end of file diff --git a/src/com/android/settings/applications/AllAppsInfoPreferenceController.java b/src/com/android/settings/applications/AllAppsInfoPreferenceController.java new file mode 100644 index 0000000000..325b25a361 --- /dev/null +++ b/src/com/android/settings/applications/AllAppsInfoPreferenceController.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2019 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.applications; + +import android.app.usage.UsageStats; +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; + +import java.util.List; + +public class AllAppsInfoPreferenceController extends BasePreferenceController + implements RecentAppStatsMixin.RecentAppStatsListener { + + @VisibleForTesting + Preference mPreference; + + public AllAppsInfoPreferenceController(Context context, String key) { + super(context, key); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + // In most cases, device has recently opened apps. So, we hide it by default. + mPreference.setVisible(false); + } + + @Override + public void onReloadDataCompleted(@NonNull List<UsageStats> recentApps) { + // If device has recently opened apps, we don't show all apps preference. + if (!recentApps.isEmpty()) { + mPreference.setVisible(false); + return; + } + + mPreference.setVisible(true); + // Show total number of installed apps as See all's summary. + new InstalledAppCounter(mContext, InstalledAppCounter.IGNORE_INSTALL_REASON, + mContext.getPackageManager()) { + @Override + protected void onCountComplete(int num) { + mPreference.setSummary(mContext.getString(R.string.apps_summary, num)); + } + }.execute(); + } +} diff --git a/src/com/android/settings/applications/AppAndNotificationDashboardFragment.java b/src/com/android/settings/applications/AppAndNotificationDashboardFragment.java index eb74fb1386..5e57908035 100644 --- a/src/com/android/settings/applications/AppAndNotificationDashboardFragment.java +++ b/src/com/android/settings/applications/AppAndNotificationDashboardFragment.java @@ -16,30 +16,43 @@ package com.android.settings.applications; -import android.app.Activity; -import android.app.Application; -import android.app.Fragment; +import android.app.settings.SettingsEnums; +import android.app.usage.UsageStats; import android.content.Context; +import android.os.Bundle; import android.provider.SearchIndexableResource; +import android.view.View; + +import androidx.annotation.NonNull; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; +import com.android.settings.Utils; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.notification.EmergencyBroadcastPreferenceController; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.search.SearchIndexable; +import com.android.settingslib.widget.AppEntitiesHeaderController; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -public class AppAndNotificationDashboardFragment extends DashboardFragment { +@SearchIndexable +public class AppAndNotificationDashboardFragment extends DashboardFragment + implements RecentAppStatsMixin.RecentAppStatsListener { private static final String TAG = "AppAndNotifDashboard"; + private View mProgressHeader; + private View mProgressAnimation; + private RecentAppStatsMixin mRecentAppStatsMixin; + private RecentAppsPreferenceController mRecentAppsPreferenceController; + private AllAppsInfoPreferenceController mAllAppsInfoPreferenceController; + @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.SETTINGS_APP_NOTIF_CATEGORY; + return SettingsEnums.SETTINGS_APP_NOTIF_CATEGORY; } @Override @@ -58,24 +71,63 @@ public class AppAndNotificationDashboardFragment extends DashboardFragment { } @Override + public void onAttach(Context context) { + super.onAttach(context); + + use(SpecialAppAccessPreferenceController.class).setSession(getSettingsLifecycle()); + + mRecentAppStatsMixin = new RecentAppStatsMixin(context, + AppEntitiesHeaderController.MAXIMUM_APPS); + getSettingsLifecycle().addObserver(mRecentAppStatsMixin); + mRecentAppStatsMixin.addListener(this); + + mRecentAppsPreferenceController = use(RecentAppsPreferenceController.class); + mRecentAppsPreferenceController.setFragment(this /* fragment */); + mRecentAppStatsMixin.addListener(mRecentAppsPreferenceController); + + mAllAppsInfoPreferenceController = use(AllAppsInfoPreferenceController.class); + mRecentAppStatsMixin.addListener(mAllAppsInfoPreferenceController); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + mProgressHeader = setPinnedHeaderView(R.layout.progress_header); + mProgressAnimation = mProgressHeader.findViewById(R.id.progress_bar_animation); + setLoadingEnabled(false); + } + + @Override + public void onStart() { + super.onStart(); + setLoadingEnabled(true); + } + + @Override + public void onReloadDataCompleted(@NonNull List<UsageStats> recentApps) { + setLoadingEnabled(false); + if (!recentApps.isEmpty()) { + Utils.setActionBarShadowAnimation(getActivity(), getSettingsLifecycle(), + getListView()); + } + } + + @Override protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { - final Activity activity = getActivity(); - final Application app; - if (activity != null) { - app = activity.getApplication(); - } else { - app = null; + return buildPreferenceControllers(context); + } + + private void setLoadingEnabled(boolean enabled) { + if (mProgressHeader != null && mProgressAnimation != null) { + mProgressHeader.setVisibility(enabled ? View.VISIBLE : View.INVISIBLE); + mProgressAnimation.setVisibility(enabled ? View.VISIBLE : View.INVISIBLE); } - return buildPreferenceControllers(context, app, this); } - private static List<AbstractPreferenceController> buildPreferenceControllers(Context context, - Application app, Fragment host) { + private static List<AbstractPreferenceController> buildPreferenceControllers(Context context) { final List<AbstractPreferenceController> controllers = new ArrayList<>(); controllers.add(new EmergencyBroadcastPreferenceController(context, "app_and_notif_cell_broadcast_settings")); - controllers.add(new SpecialAppAccessPreferenceController(context)); - controllers.add(new RecentAppsPreferenceController(context, app, host)); return controllers; } @@ -92,15 +144,7 @@ public class AppAndNotificationDashboardFragment extends DashboardFragment { @Override public List<AbstractPreferenceController> createPreferenceControllers( Context context) { - return buildPreferenceControllers(context, null, null /* host */); - } - - @Override - public List<String> getNonIndexableKeys(Context context) { - List<String> keys = super.getNonIndexableKeys(context); - keys.add((new SpecialAppAccessPreferenceController(context)) - .getPreferenceKey()); - return keys; + return buildPreferenceControllers(context); } }; } diff --git a/src/com/android/settings/applications/AppCounter.java b/src/com/android/settings/applications/AppCounter.java index a02ecfa70f..ce2be84343 100644 --- a/src/com/android/settings/applications/AppCounter.java +++ b/src/com/android/settings/applications/AppCounter.java @@ -22,16 +22,14 @@ import android.os.AsyncTask; import android.os.UserHandle; import android.os.UserManager; -import com.android.settingslib.wrapper.PackageManagerWrapper; - import java.util.List; public abstract class AppCounter extends AsyncTask<Void, Void, Integer> { - protected final PackageManagerWrapper mPm; + protected final PackageManager mPm; protected final UserManager mUm; - public AppCounter(Context context, PackageManagerWrapper packageManager) { + public AppCounter(Context context, PackageManager packageManager) { mPm = packageManager; mUm = (UserManager) context.getSystemService(Context.USER_SERVICE); } diff --git a/src/com/android/settings/applications/AppInfoBase.java b/src/com/android/settings/applications/AppInfoBase.java index f8ed315bfd..71043400ff 100644 --- a/src/com/android/settings/applications/AppInfoBase.java +++ b/src/com/android/settings/applications/AppInfoBase.java @@ -19,11 +19,9 @@ package com.android.settings.applications; import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; import android.app.Activity; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.DialogFragment; -import android.app.Fragment; import android.app.admin.DevicePolicyManager; +import android.app.settings.SettingsEnums; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -40,14 +38,17 @@ import android.os.UserManager; import android.text.TextUtils; import android.util.Log; -import com.android.internal.logging.nano.MetricsProto; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; + import com.android.settings.SettingsActivity; import com.android.settings.SettingsPreferenceFragment; import com.android.settings.applications.manageapplications.ManageApplications; import com.android.settings.core.SubSettingLauncher; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; import com.android.settings.overlay.FeatureFactory; -import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.applications.ApplicationsState; import com.android.settingslib.applications.ApplicationsState.AppEntry; @@ -59,8 +60,7 @@ public abstract class AppInfoBase extends SettingsPreferenceFragment public static final String ARG_PACKAGE_NAME = "package"; public static final String ARG_PACKAGE_UID = "uid"; - protected static final String TAG = AppInfoBase.class.getSimpleName(); - protected static final boolean localLOGV = false; + private static final String TAG = "AppInfoBase"; protected EnforcedAdmin mAppsControlDisallowedAdmin; protected boolean mAppsControlDisallowedBySystem; @@ -92,7 +92,7 @@ public abstract class AppInfoBase extends SettingsPreferenceFragment mApplicationFeatureProvider = FeatureFactory.getFactory(activity) .getApplicationFeatureProvider(activity); mState = ApplicationsState.getInstance(activity.getApplication()); - mSession = mState.newSession(this, getLifecycle()); + mSession = mState.newSession(this, getSettingsLifecycle()); mDpm = (DevicePolicyManager) activity.getSystemService(Context.DEVICE_POLICY_SERVICE); mUserManager = (UserManager) activity.getSystemService(Context.USER_SERVICE); mPm = activity.getPackageManager(); @@ -106,13 +106,13 @@ public abstract class AppInfoBase extends SettingsPreferenceFragment @Override public void onResume() { super.onResume(); - mAppsControlDisallowedAdmin = RestrictedLockUtils.checkIfRestrictionEnforced(getActivity(), - UserManager.DISALLOW_APPS_CONTROL, mUserId); - mAppsControlDisallowedBySystem = RestrictedLockUtils.hasBaseUserRestriction(getActivity(), - UserManager.DISALLOW_APPS_CONTROL, mUserId); + mAppsControlDisallowedAdmin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced( + getActivity(), UserManager.DISALLOW_APPS_CONTROL, mUserId); + mAppsControlDisallowedBySystem = RestrictedLockUtilsInternal.hasBaseUserRestriction( + getActivity(), UserManager.DISALLOW_APPS_CONTROL, mUserId); if (!refreshUi()) { - setIntentAndFinish(true, true); + setIntentAndFinish(true /* appChanged */); } } @@ -158,8 +158,8 @@ public abstract class AppInfoBase extends SettingsPreferenceFragment return mPackageName; } - protected void setIntentAndFinish(boolean finish, boolean appChanged) { - if (localLOGV) Log.i(TAG, "appChanged=" + appChanged); + protected void setIntentAndFinish(boolean appChanged) { + Log.i(TAG, "appChanged=" + appChanged); Intent intent = new Intent(); intent.putExtra(ManageApplications.APP_CHG, appChanged); SettingsActivity sa = (SettingsActivity) getActivity(); @@ -215,7 +215,7 @@ public abstract class AppInfoBase extends SettingsPreferenceFragment @Override public void onPackageListChanged() { if (!refreshUi()) { - setIntentAndFinish(true, true); + setIntentAndFinish(true /* appChanged */); } } @@ -228,7 +228,7 @@ public abstract class AppInfoBase extends SettingsPreferenceFragment new SubSettingLauncher(source.getContext()) .setDestination(fragment.getName()) .setSourceMetricsCategory(sourceMetricsCategory) - .setTitle(titleRes) + .setTitleRes(titleRes) .setArguments(args) .setUserHandle(new UserHandle(UserHandle.getUserId(uid))) .setResultListener(source, request) @@ -241,7 +241,7 @@ public abstract class AppInfoBase extends SettingsPreferenceFragment @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DIALOG_APP_INFO_ACTION; + return SettingsEnums.DIALOG_APP_INFO_ACTION; } @Override diff --git a/src/com/android/settings/applications/AppInfoWithHeader.java b/src/com/android/settings/applications/AppInfoWithHeader.java index dd71087a2a..9e3842e578 100644 --- a/src/com/android/settings/applications/AppInfoWithHeader.java +++ b/src/com/android/settings/applications/AppInfoWithHeader.java @@ -20,15 +20,18 @@ import static com.android.settings.widget.EntityHeaderController.ActionType; import android.app.Activity; import android.os.Bundle; -import androidx.preference.Preference; import android.util.IconDrawableFactory; import android.util.Log; +import androidx.preference.Preference; + import com.android.settings.widget.EntityHeaderController; import com.android.settingslib.applications.AppUtils; public abstract class AppInfoWithHeader extends AppInfoBase { + private static final String TAG = "AppInfoWithHeader"; + private boolean mCreated; @Override @@ -43,7 +46,7 @@ public abstract class AppInfoWithHeader extends AppInfoBase { final Activity activity = getActivity(); final Preference pref = EntityHeaderController .newInstance(activity, this, null /* header */) - .setRecyclerView(getListView(), getLifecycle()) + .setRecyclerView(getListView(), getSettingsLifecycle()) .setIcon(IconDrawableFactory.newInstance(getContext()) .getBadgedIcon(mPackageInfo.applicationInfo)) .setLabel(mPackageInfo.applicationInfo.loadLabel(mPm)) diff --git a/src/com/android/settings/applications/AppLaunchSettings.java b/src/com/android/settings/applications/AppLaunchSettings.java index 7d422b1503..c61f5d126a 100644 --- a/src/com/android/settings/applications/AppLaunchSettings.java +++ b/src/com/android/settings/applications/AppLaunchSettings.java @@ -16,7 +16,11 @@ package com.android.settings.applications; -import android.app.AlertDialog; +import static android.content.pm.PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS; +import static android.content.pm.PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS_ASK; +import static android.content.pm.PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_NEVER; + +import android.app.settings.SettingsEnums; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ApplicationInfo; @@ -26,25 +30,21 @@ import android.content.pm.ResolveInfo; import android.net.Uri; import android.os.Bundle; import android.os.UserHandle; -import androidx.preference.DropDownPreference; -import androidx.preference.Preference; -import androidx.preference.Preference.OnPreferenceChangeListener; import android.util.ArraySet; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.appcompat.app.AlertDialog; +import androidx.preference.DropDownPreference; +import androidx.preference.Preference; +import androidx.preference.Preference.OnPreferenceChangeListener; + import com.android.settings.R; import com.android.settings.Utils; import java.util.List; -import static android.content.pm.PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS; -import static android.content.pm.PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS_ASK; -import static android.content.pm.PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_NEVER; -import static android.content.pm.PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED; - public class AppLaunchSettings extends AppInfoWithHeader implements OnClickListener, Preference.OnPreferenceChangeListener { private static final String TAG = "AppLaunchSettings"; @@ -123,6 +123,8 @@ public class AppLaunchSettings extends AppInfoWithHeader implements OnClickListe // * always // * ask // * never + // + // Make sure to update linkStateToIndex() if this presentation order is changed. mAppLinkState.setEntries(new CharSequence[] { getString(R.string.app_link_open_always), getString(R.string.app_link_open_ask), @@ -140,10 +142,7 @@ public class AppLaunchSettings extends AppInfoWithHeader implements OnClickListe // purposes of the UI (and does the right thing around pending domain // verifications that might arrive after the user chooses 'ask' in this UI). final int state = mPm.getIntentVerificationStatusAsUser(mPackageName, UserHandle.myUserId()); - mAppLinkState.setValue( - Integer.toString((state == INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED) - ? INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS_ASK - : state)); + mAppLinkState.setValueIndex(linkStateToIndex(state)); // Set the callback only after setting the initial selected item mAppLinkState.setOnPreferenceChangeListener(new OnPreferenceChangeListener() { @@ -156,6 +155,17 @@ public class AppLaunchSettings extends AppInfoWithHeader implements OnClickListe } } + private int linkStateToIndex(final int state) { + switch (state) { + case INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS: + return 0; // Always + case INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_NEVER: + return 2; // Never + default: + return 1; // Ask + } + } + private boolean updateAppLinkState(final int newState) { if (mIsBrowser) { // We shouldn't get into this state, but if we do make sure @@ -213,6 +223,6 @@ public class AppLaunchSettings extends AppInfoWithHeader implements OnClickListe @Override public int getMetricsCategory() { - return MetricsEvent.APPLICATIONS_APP_LAUNCH; + return SettingsEnums.APPLICATIONS_APP_LAUNCH; } } diff --git a/src/com/android/settings/applications/AppLister.java b/src/com/android/settings/applications/AppLister.java index f1a3be91db..4ecb627b1c 100644 --- a/src/com/android/settings/applications/AppLister.java +++ b/src/com/android/settings/applications/AppLister.java @@ -23,8 +23,6 @@ import android.os.AsyncTask; import android.os.UserHandle; import android.os.UserManager; -import com.android.settingslib.wrapper.PackageManagerWrapper; - import java.util.ArrayList; import java.util.List; @@ -35,10 +33,10 @@ import java.util.List; * of just counting them. */ public abstract class AppLister extends AsyncTask<Void, Void, List<UserAppInfo>> { - protected final PackageManagerWrapper mPm; + protected final PackageManager mPm; protected final UserManager mUm; - public AppLister(PackageManagerWrapper packageManager, UserManager userManager) { + public AppLister(PackageManager packageManager, UserManager userManager) { mPm = packageManager; mUm = userManager; } diff --git a/src/com/android/settings/applications/AppPermissions.java b/src/com/android/settings/applications/AppPermissions.java deleted file mode 100644 index 6299921e51..0000000000 --- a/src/com/android/settings/applications/AppPermissions.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * 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.applications; - -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.pm.PermissionInfo; -import android.os.Build; -import android.util.ArrayMap; -import android.util.Log; - -import java.util.ArrayList; -import java.util.List; - -/** - * Based off from - * packages/apps/PackageInstaller/src/com/android/packageinstaller/permission/AppPermissions.java - * Except we only care about the number rather than the details. - */ -public final class AppPermissions { - private static final String TAG = "AppPermissions"; - - private final ArrayMap<String, PermissionGroup> mGroups = new ArrayMap<>(); - private final Context mContext; - private final PackageInfo mPackageInfo; - - public AppPermissions(Context context, String packageName) { - mContext = context; - mPackageInfo = getPackageInfo(packageName); - refresh(); - } - - private PackageInfo getPackageInfo(String packageName) { - try { - return mContext.getPackageManager().getPackageInfo(packageName, - PackageManager.GET_PERMISSIONS); - } catch (NameNotFoundException e) { - Log.e(TAG, "Unable to find " + packageName, e); - return null; - } - } - - public void refresh() { - if (mPackageInfo != null) { - loadPermissionGroups(); - } - } - - public int getPermissionCount() { - return mGroups.size(); - } - - public int getGrantedPermissionsCount() { - int ct = 0; - for (int i = 0; i < mGroups.size(); i++) { - if (mGroups.valueAt(i).areRuntimePermissionsGranted()) { - ct++; - } - } - return ct; - } - - private void loadPermissionGroups() { - mGroups.clear(); - if (mPackageInfo.requestedPermissions == null) { - return; - } - - final boolean appSupportsRuntimePermissions = appSupportsRuntime( - mPackageInfo.applicationInfo); - - for (int i = 0; i < mPackageInfo.requestedPermissions.length; i++) { - String requestedPerm = mPackageInfo.requestedPermissions[i]; - - final PermissionInfo permInfo; - try { - permInfo = mContext.getPackageManager().getPermissionInfo(requestedPerm, 0); - } catch (NameNotFoundException e) { - Log.w(TAG, "Unknown permission: " + requestedPerm); - continue; - } - - String permName = permInfo.name; - String groupName = permInfo.group != null ? permInfo.group : permName; - - PermissionGroup group = mGroups.get(groupName); - if (group == null) { - group = new PermissionGroup(); - mGroups.put(groupName, group); - } - - final boolean runtime = appSupportsRuntimePermissions - && permInfo.protectionLevel == PermissionInfo.PROTECTION_DANGEROUS; - final boolean granted = (mPackageInfo.requestedPermissionsFlags[i] - & PackageInfo.REQUESTED_PERMISSION_GRANTED) != 0; - - Permission permission = new Permission(runtime, granted); - group.addPermission(permission, permName); - } - // Only care about runtime perms for now. - for (int i = mGroups.size() - 1; i >= 0; i--) { - if (!mGroups.valueAt(i).mHasRuntimePermissions) { - mGroups.removeAt(i); - } - } - } - - public static boolean appSupportsRuntime(ApplicationInfo info) { - return info.targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1; - } - - private static final class PermissionGroup { - private final ArrayMap<String, Permission> mPermissions = new ArrayMap<>(); - private boolean mHasRuntimePermissions; - - public boolean hasRuntimePermissions() { - return mHasRuntimePermissions; - } - - public boolean areRuntimePermissionsGranted() { - final int permissionCount = mPermissions.size(); - for (int i = 0; i < permissionCount; i++) { - Permission permission = mPermissions.valueAt(i); - if (permission.runtime && !permission.granted) { - return false; - } - } - return true; - } - - public List<Permission> getPermissions() { - return new ArrayList<>(mPermissions.values()); - } - - void addPermission(Permission permission, String permName) { - mPermissions.put(permName, permission); - if (permission.runtime) { - mHasRuntimePermissions = true; - } - } - } - - private static final class Permission { - private final boolean runtime; - private boolean granted; - - public Permission(boolean runtime, boolean granted) { - this.runtime = runtime; - this.granted = granted; - } - } -} diff --git a/src/com/android/settings/applications/AppPermissionsPreferenceController.java b/src/com/android/settings/applications/AppPermissionsPreferenceController.java index 206ef33912..59341d5447 100644 --- a/src/com/android/settings/applications/AppPermissionsPreferenceController.java +++ b/src/com/android/settings/applications/AppPermissionsPreferenceController.java @@ -16,38 +16,50 @@ package com.android.settings.applications; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.pm.PermissionGroupInfo; -import android.content.pm.PermissionInfo; -import android.text.TextUtils; +import android.icu.text.ListFormatter; import android.util.ArraySet; -import android.util.Log; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; import com.android.settings.R; import com.android.settings.core.BasePreferenceController; +import com.android.settingslib.applications.PermissionsSummaryHelper; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; public class AppPermissionsPreferenceController extends BasePreferenceController { private static final String TAG = "AppPermissionPrefCtrl"; - private static final String KEY_APP_PERMISSION_GROUPS = "manage_perms"; - private static final String[] PERMISSION_GROUPS = new String[] { - "android.permission-group.LOCATION", - "android.permission-group.MICROPHONE", - "android.permission-group.CAMERA", - "android.permission-group.SMS", - "android.permission-group.CONTACTS", - "android.permission-group.PHONE"}; + private static int NUM_PACKAGE_TO_CHECK = 3; - private static final int NUM_PERMISSION_TO_USE = 3; + @VisibleForTesting + static int NUM_PERMISSIONS_TO_SHOW = 3; private final PackageManager mPackageManager; + private final Set<CharSequence> mPermissionGroups; + + private final PermissionsSummaryHelper.PermissionsResultCallback mPermissionsCallback = + new PermissionsSummaryHelper.PermissionsResultCallback() { + @Override + public void onPermissionSummaryResult(int standardGrantedPermissionCount, + int requestedPermissionCount, int additionalGrantedPermissionCount, + List<CharSequence> grantedGroupLabels) { + updateSummary(grantedGroupLabels); + } + }; + + @VisibleForTesting + int mNumPackageChecked; - public AppPermissionsPreferenceController(Context context) { - super(context, KEY_APP_PERMISSION_GROUPS); + private Preference mPreference; + + public AppPermissionsPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); mPackageManager = context.getPackageManager(); + mPermissionGroups = new ArraySet<>(); } @Override @@ -55,78 +67,45 @@ public class AppPermissionsPreferenceController extends BasePreferenceController return AVAILABLE; } - /* - Summary text looks like: Apps using Permission1, Permission2, Permission3 - The 3 permissions are the first three from the list which any app has granted: - Location, Microphone, Camera, Sms, Contacts, and Phone - */ @Override - public CharSequence getSummary() { - final Set<String> permissions = getAllPermissionsInGroups(); - Set<String> grantedPermissionGroups = getGrantedPermissionGroups(permissions); - CharSequence summary = null; - int count = 0; - for (String group : PERMISSION_GROUPS) { - if (!grantedPermissionGroups.contains(group)) { - continue; - } - summary = concatSummaryText(summary, group); - if (++count >= NUM_PERMISSION_TO_USE) { - break; - } - } - return count > 0 ? mContext.getString(R.string.app_permissions_summary, summary) : null; + public void updateState(Preference preference) { + mPreference = preference; + mNumPackageChecked = 0; + queryPermissionSummary(); } - private Set<String> getGrantedPermissionGroups(Set<String> permissions) { - ArraySet<String> grantedPermissionGroups = new ArraySet<>(); - List<PackageInfo> installedPackages = + @VisibleForTesting + void queryPermissionSummary() { + final List<PackageInfo> installedPackages = mPackageManager.getInstalledPackages(PackageManager.GET_PERMISSIONS); - for (PackageInfo installedPackage : installedPackages) { - if (installedPackage.permissions == null) { - continue; - } - for (PermissionInfo permissionInfo : installedPackage.permissions) { - if (permissions.contains(permissionInfo.name) - && !grantedPermissionGroups.contains(permissionInfo.group)) { - grantedPermissionGroups.add(permissionInfo.group); - } - } - } - return grantedPermissionGroups; - } + // Here we only get the first three apps and check their permissions. + final List<PackageInfo> packagesWithPermission = installedPackages.stream() + .filter(pInfo -> pInfo.permissions != null) + .limit(NUM_PACKAGE_TO_CHECK) + .collect(Collectors.toList()); - private CharSequence concatSummaryText(CharSequence currentSummary, String permission) { - final String label = getPermissionGroupLabel(permission).toString().toLowerCase(); - if (TextUtils.isEmpty(currentSummary)) { - return label; + for (PackageInfo installedPackage : packagesWithPermission) { + PermissionsSummaryHelper.getPermissionSummary(mContext, + installedPackage.packageName, mPermissionsCallback); } - return mContext.getString(R.string.join_many_items_middle, currentSummary, label); } - private CharSequence getPermissionGroupLabel(String group) { - try { - final PermissionGroupInfo groupInfo = mPackageManager.getPermissionGroupInfo(group, 0); - return groupInfo.loadLabel(mPackageManager); - } catch (NameNotFoundException e) { - Log.e(TAG, "Error getting permissions label.", e); - } - return group; - } + @VisibleForTesting + void updateSummary(List<CharSequence> grantedGroupLabels) { + mPermissionGroups.addAll(grantedGroupLabels); + mNumPackageChecked++; - private Set<String> getAllPermissionsInGroups() { - ArraySet<String> result = new ArraySet<>(); - for (String group : PERMISSION_GROUPS) { - try { - final List<PermissionInfo> permissions = - mPackageManager.queryPermissionsByGroup(group, 0); - for (PermissionInfo permissionInfo : permissions) { - result.add(permissionInfo.name); - } - } catch (NameNotFoundException e) { - Log.e(TAG, "Error getting permissions in group " + group, e); - } + if (mNumPackageChecked < NUM_PACKAGE_TO_CHECK) { + return; } - return result; + + final List<CharSequence> permissionsToShow = mPermissionGroups.stream() + .limit(NUM_PERMISSIONS_TO_SHOW) + .collect(Collectors.toList()); + final CharSequence summary = !permissionsToShow.isEmpty() + ? mContext.getString(R.string.app_permissions_summary, + ListFormatter.getInstance().format(permissionsToShow).toLowerCase()) + : null; + mPreference.setSummary(summary); } -} +}
\ No newline at end of file diff --git a/src/com/android/settings/applications/AppStateAppOpsBridge.java b/src/com/android/settings/applications/AppStateAppOpsBridge.java index f8dd59ce8e..0e3ee2d811 100755 --- a/src/com/android/settings/applications/AppStateAppOpsBridge.java +++ b/src/com/android/settings/applications/AppStateAppOpsBridge.java @@ -25,11 +25,12 @@ import android.content.pm.PackageManager; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; -import androidx.annotation.VisibleForTesting; import android.util.ArrayMap; import android.util.Log; import android.util.SparseArray; +import androidx.annotation.VisibleForTesting; + import com.android.settingslib.applications.ApplicationsState; import com.android.settingslib.applications.ApplicationsState.AppEntry; diff --git a/src/com/android/settings/applications/AppStateBaseBridge.java b/src/com/android/settings/applications/AppStateBaseBridge.java index 2329b4469e..1a39483af9 100644 --- a/src/com/android/settings/applications/AppStateBaseBridge.java +++ b/src/com/android/settings/applications/AppStateBaseBridge.java @@ -45,7 +45,7 @@ public abstract class AppStateBaseBridge implements ApplicationsState.Callbacks // the same time as us as well. mHandler = new BackgroundHandler(mAppState != null ? mAppState.getBackgroundLooper() : Looper.getMainLooper()); - mMainHandler = new MainHandler(); + mMainHandler = new MainHandler(Looper.getMainLooper()); } public void resume() { @@ -106,11 +106,16 @@ public abstract class AppStateBaseBridge implements ApplicationsState.Callbacks } protected abstract void loadAllExtraInfo(); + protected abstract void updateExtraInfo(AppEntry app, String pkg, int uid); private class MainHandler extends Handler { private static final int MSG_INFO_UPDATED = 1; + public MainHandler(Looper looper) { + super(looper); + } + @Override public void handleMessage(Message msg) { switch (msg.what) { diff --git a/src/com/android/settings/applications/AppStateDirectoryAccessBridge.java b/src/com/android/settings/applications/AppStateDirectoryAccessBridge.java deleted file mode 100644 index 1c2a0af8a7..0000000000 --- a/src/com/android/settings/applications/AppStateDirectoryAccessBridge.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (C) 2018 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.applications; - -import static android.os.storage.StorageVolume.ScopedAccessProviderContract.AUTHORITY; -import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PACKAGES; -import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PACKAGES_COLUMNS; -import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PACKAGES_COL_PACKAGE; - -import android.content.ContentResolver; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.util.ArraySet; -import android.util.Log; - -import com.android.settingslib.applications.ApplicationsState; -import com.android.settingslib.applications.ApplicationsState.AppEntry; -import com.android.settingslib.applications.ApplicationsState.AppFilter; - -import java.util.Set; - -// TODO(b/72055774): add unit tests -public class AppStateDirectoryAccessBridge extends AppStateBaseBridge { - - private static final String TAG = "DirectoryAccessBridge"; - - // TODO(b/72055774): set to false once feature is ready (or use Log.isLoggable) - static final boolean DEBUG = true; - static final boolean VERBOSE = true; - - public AppStateDirectoryAccessBridge(ApplicationsState appState, Callback callback) { - super(appState, callback); - } - - @Override - protected void loadAllExtraInfo() { } - - @Override - protected void updateExtraInfo(AppEntry app, String pkg, int uid) { } - - public static final AppFilter FILTER_APP_HAS_DIRECTORY_ACCESS = new AppFilter() { - - private Set<String> mPackages; - - @Override - public void init() { - throw new UnsupportedOperationException("Need to call constructor that takes context"); - } - - @Override - public void init(Context context) { - mPackages = null; - final Uri providerUri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) - .authority(AUTHORITY).appendPath(TABLE_PACKAGES).appendPath("*") - .build(); - try (Cursor cursor = context.getContentResolver().query(providerUri, - TABLE_PACKAGES_COLUMNS, null, null)) { - if (cursor == null) { - Log.w(TAG, "Didn't get cursor for " + providerUri); - return; - } - final int count = cursor.getCount(); - if (count == 0) { - if (DEBUG) { - Log.d(TAG, "No packages anymore (was " + mPackages + ")"); - } - return; - } - mPackages = new ArraySet<>(count); - while (cursor.moveToNext()) { - mPackages.add(cursor.getString(TABLE_PACKAGES_COL_PACKAGE)); - } - if (DEBUG) { - Log.d(TAG, "init(): " + mPackages); - } - } - } - - - @Override - public boolean filterApp(AppEntry info) { - return mPackages != null && mPackages.contains(info.info.packageName); - } - }; -} diff --git a/src/com/android/settings/applications/AppStateInstallAppsBridge.java b/src/com/android/settings/applications/AppStateInstallAppsBridge.java index 5b9ded648c..8a3e5a220d 100644 --- a/src/com/android/settings/applications/AppStateInstallAppsBridge.java +++ b/src/com/android/settings/applications/AppStateInstallAppsBridge.java @@ -25,7 +25,6 @@ import android.os.RemoteException; import android.util.Log; import com.android.internal.util.ArrayUtils; -import com.android.settings.R; import com.android.settingslib.applications.ApplicationsState; import com.android.settingslib.applications.ApplicationsState.AppEntry; import com.android.settingslib.applications.ApplicationsState.AppFilter; @@ -94,8 +93,6 @@ public class AppStateInstallAppsBridge extends AppStateBaseBridge { final InstallAppsState appState = new InstallAppsState(); appState.permissionRequested = hasRequestedAppOpPermission( Manifest.permission.REQUEST_INSTALL_PACKAGES, packageName); - appState.permissionGranted = hasPermission(Manifest.permission.REQUEST_INSTALL_PACKAGES, - uid); appState.appOpMode = getAppOpMode(AppOpsManager.OP_REQUEST_INSTALL_PACKAGES, uid, packageName); return appState; @@ -106,7 +103,6 @@ public class AppStateInstallAppsBridge extends AppStateBaseBridge { */ public static class InstallAppsState { boolean permissionRequested; - boolean permissionGranted; int appOpMode; public InstallAppsState() { @@ -114,11 +110,7 @@ public class AppStateInstallAppsBridge extends AppStateBaseBridge { } public boolean canInstallApps() { - if (appOpMode == AppOpsManager.MODE_DEFAULT) { - return permissionGranted; - } else { - return appOpMode == AppOpsManager.MODE_ALLOWED; - } + return appOpMode == AppOpsManager.MODE_ALLOWED; } public boolean isPotentialAppSource() { @@ -127,8 +119,8 @@ public class AppStateInstallAppsBridge extends AppStateBaseBridge { @Override public String toString() { - StringBuilder sb = new StringBuilder("[permissionGranted: " + permissionGranted); - sb.append(", permissionRequested: " + permissionRequested); + StringBuilder sb = new StringBuilder(); + sb.append("[permissionRequested: " + permissionRequested); sb.append(", appOpMode: " + appOpMode); sb.append("]"); return sb.toString(); diff --git a/src/com/android/settings/applications/AppStateNotificationBridge.java b/src/com/android/settings/applications/AppStateNotificationBridge.java index 6cf64c3e57..d179642a63 100644 --- a/src/com/android/settings/applications/AppStateNotificationBridge.java +++ b/src/com/android/settings/applications/AppStateNotificationBridge.java @@ -17,13 +17,13 @@ package com.android.settings.applications; import android.app.usage.IUsageStatsManager; import android.app.usage.UsageEvents; -import android.app.usage.UsageStatsManager; import android.content.Context; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; import android.text.format.DateUtils; import android.util.ArrayMap; +import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.widget.Switch; @@ -47,6 +47,8 @@ import java.util.Map; */ public class AppStateNotificationBridge extends AppStateBaseBridge { + private final String TAG = "AppStateNotificationBridge"; + private final boolean DEBUG = false; private final Context mContext; private IUsageStatsManager mUsageStatsManager; protected List<Integer> mUserIds; @@ -71,7 +73,12 @@ public class AppStateNotificationBridge extends AppStateBaseBridge { @Override protected void loadAllExtraInfo() { ArrayList<AppEntry> apps = mAppSession.getAllApps(); - if (apps == null) return; + if (apps == null) { + if (DEBUG) { + Log.d(TAG, "No apps. No extra info loaded"); + } + return; + } final Map<String, NotificationsSentState> map = getAggregatedUsageEvents(); for (AppEntry entry : apps) { @@ -93,18 +100,22 @@ public class AppStateNotificationBridge extends AppStateBaseBridge { } public static CharSequence getSummary(Context context, NotificationsSentState state, - boolean sortByRecency) { - if (sortByRecency) { + int sortOrder) { + if (sortOrder == R.id.sort_order_recent_notification) { if (state.lastSent == 0) { return context.getString(R.string.notifications_sent_never); } return StringUtil.formatRelativeTime( context, System.currentTimeMillis() - state.lastSent, true); - } else { - if (state.avgSentWeekly > 0) { - return context.getString(R.string.notifications_sent_weekly, state.avgSentWeekly); + } else if (sortOrder == R.id.sort_order_frequent_notification) { + if (state.avgSentDaily > 0) { + return context.getResources().getQuantityString( + R.plurals.notifications_sent_daily, state.avgSentDaily, state.avgSentDaily); } - return context.getString(R.string.notifications_sent_daily, state.avgSentDaily); + return context.getResources().getQuantityString(R.plurals.notifications_sent_weekly, + state.avgSentWeekly, state.avgSentWeekly); + } else { + return ""; } } @@ -260,6 +271,21 @@ public class AppStateNotificationBridge extends AppStateBaseBridge { } }; + public static final AppFilter FILTER_APP_NOTIFICATION_BLOCKED = new AppFilter() { + @Override + public void init() { + } + + @Override + public boolean filterApp(AppEntry info) { + NotificationsSentState state = getNotificationsSentState(info); + if (state != null) { + return state.blocked; + } + return false; + } + }; + public static final Comparator<AppEntry> RECENT_NOTIFICATION_COMPARATOR = new Comparator<AppEntry>() { @Override diff --git a/src/com/android/settings/applications/AppStateSmsPremBridge.java b/src/com/android/settings/applications/AppStateSmsPremBridge.java index 0fa7e50ff6..5a79a62bb9 100644 --- a/src/com/android/settings/applications/AppStateSmsPremBridge.java +++ b/src/com/android/settings/applications/AppStateSmsPremBridge.java @@ -16,6 +16,7 @@ package com.android.settings.applications; import android.content.Context; import android.os.RemoteException; import android.os.ServiceManager; + import com.android.internal.telephony.ISms; import com.android.internal.telephony.SmsUsageMonitor; import com.android.settingslib.applications.ApplicationsState; diff --git a/src/com/android/settings/applications/AppStorageSettings.java b/src/com/android/settings/applications/AppStorageSettings.java index 8289114352..f07c66cd8a 100644 --- a/src/com/android/settings/applications/AppStorageSettings.java +++ b/src/com/android/settings/applications/AppStorageSettings.java @@ -18,28 +18,18 @@ package com.android.settings.applications; import static android.content.pm.ApplicationInfo.FLAG_ALLOW_CLEAR_USER_DATA; import static android.content.pm.ApplicationInfo.FLAG_SYSTEM; -import static android.os.storage.StorageVolume.ScopedAccessProviderContract.AUTHORITY; -import static android.os.storage.StorageVolume.ScopedAccessProviderContract.COL_GRANTED; -import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS; - -import static com.android.settings.applications.AppStateDirectoryAccessBridge.DEBUG; import android.app.ActivityManager; -import android.app.AlertDialog; import android.app.AppGlobals; import android.app.GrantedUriPermission; -import android.app.LoaderManager; -import android.content.ContentResolver; -import android.content.ContentValues; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; -import android.content.Loader; import android.content.pm.ApplicationInfo; import android.content.pm.IPackageDataObserver; import android.content.pm.PackageManager; import android.content.pm.ProviderInfo; -import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Message; @@ -47,24 +37,28 @@ import android.os.RemoteException; import android.os.UserHandle; import android.os.storage.StorageManager; import android.os.storage.VolumeInfo; -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; -import androidx.preference.PreferenceCategory; import android.util.Log; import android.util.MutableInt; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.Loader; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; + import com.android.settings.R; import com.android.settings.Utils; import com.android.settings.deviceinfo.StorageWizardMoveConfirm; -import com.android.settings.widget.ActionButtonPreference; import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.applications.ApplicationsState.Callbacks; import com.android.settingslib.applications.StorageStatsSource; import com.android.settingslib.applications.StorageStatsSource.AppStorageStats; +import com.android.settingslib.widget.ActionButtonsPreference; +import com.android.settingslib.widget.LayoutPreference; import java.util.Collections; import java.util.List; @@ -113,7 +107,7 @@ public class AppStorageSettings extends AppInfoWithHeader // Views related to cache info @VisibleForTesting - ActionButtonPreference mButtonsPref; + ActionButtonsPreference mButtonsPref; private Preference mStorageUsed; private Button mChangeStorageButton; @@ -174,10 +168,7 @@ public class AppStorageSettings extends AppInfoWithHeader .setComputingString(R.string.computing_size) .setErrorString(R.string.invalid_size_value) .build(); - mButtonsPref = ((ActionButtonPreference) findPreference(KEY_HEADER_BUTTONS)) - .setButton1Positive(false) - .setButton2Positive(false); - + mButtonsPref = ((ActionButtonsPreference) findPreference(KEY_HEADER_BUTTONS)); mStorageUsed = findPreference(KEY_STORAGE_USED); mChangeStorageButton = (Button) ((LayoutPreference) findPreference(KEY_CHANGE_STORAGE)) .findViewById(R.id.button); @@ -185,7 +176,9 @@ public class AppStorageSettings extends AppInfoWithHeader mChangeStorageButton.setOnClickListener(this); // Cache section - mButtonsPref.setButton2Text(R.string.clear_cache_btn_text); + mButtonsPref + .setButton2Text(R.string.clear_cache_btn_text) + .setButton2Icon(R.drawable.ic_settings_delete); // URI permissions section mUri = (PreferenceCategory) findPreference(KEY_URI_CATEGORY); @@ -205,7 +198,7 @@ public class AppStorageSettings extends AppInfoWithHeader mClearCacheObserver = new ClearCacheObserver(); } mMetricsFeatureProvider.action(getContext(), - MetricsEvent.ACTION_SETTINGS_CLEAR_APP_CACHE); + SettingsEnums.ACTION_SETTINGS_CLEAR_APP_CACHE); mPm.deleteApplicationCacheFiles(mPackageName, mClearCacheObserver); } @@ -311,16 +304,20 @@ public class AppStorageSettings extends AppInfoWithHeader || !isManageSpaceActivityAvailable) { mButtonsPref .setButton1Text(R.string.clear_user_data_text) + .setButton1Icon(R.drawable.ic_settings_delete) .setButton1Enabled(false); mCanClearData = false; } else { if (appHasSpaceManagementUI) { mButtonsPref.setButton1Text(R.string.manage_space_text); } else { - mButtonsPref.setButton1Text(R.string.clear_user_data_text); + mButtonsPref + .setButton1Text(R.string.clear_user_data_text) + .setButton1Icon(R.drawable.ic_settings_delete); } mButtonsPref .setButton1Text(R.string.clear_user_data_text) + .setButton1Icon(R.drawable.ic_settings_delete) .setButton1OnClickListener(v -> handleClearDataClick()); } @@ -364,7 +361,7 @@ public class AppStorageSettings extends AppInfoWithHeader * button for a system package */ private void initiateClearUserData() { - mMetricsFeatureProvider.action(getContext(), MetricsEvent.ACTION_SETTINGS_CLEAR_APP_DATA); + mMetricsFeatureProvider.action(getContext(), SettingsEnums.ACTION_SETTINGS_CLEAR_APP_DATA); mButtonsPref.setButton1Enabled(false); // Invoke uninstall or clear user data based on sysPackage String packageName = mAppEntry.info.packageName; @@ -391,7 +388,9 @@ public class AppStorageSettings extends AppInfoWithHeader private void processClearMsg(Message msg) { int result = msg.arg1; String packageName = mAppEntry.info.packageName; - mButtonsPref.setButton1Text(R.string.clear_user_data_text); + mButtonsPref + .setButton1Text(R.string.clear_user_data_text) + .setButton1Icon(R.drawable.ic_settings_delete); if (result == OP_SUCCESSFUL) { Log.i(TAG, "Cleared user data for package : " + packageName); updateSize(); @@ -422,6 +421,10 @@ public class AppStorageSettings extends AppInfoWithHeader for (GrantedUriPermission perm : perms) { String authority = perm.uri.getAuthority(); ProviderInfo provider = pm.resolveContentProvider(authority, 0); + if (provider == null) { + continue; + } + CharSequence app = provider.applicationInfo.loadLabel(pm); MutableInt count = uriCounters.get(app); if (count == null) { @@ -464,17 +467,6 @@ public class AppStorageSettings extends AppInfoWithHeader Context.ACTIVITY_SERVICE); am.clearGrantedUriPermissions(packageName); - - // Also update the Scoped Directory Access UI permissions - final Uri providerUri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) - .authority(AUTHORITY).appendPath(TABLE_PERMISSIONS).appendPath("*") - .build(); - Log.v(TAG, "Asking " + providerUri + " to delete permissions for " + packageName); - final int deleted = context.getContentResolver().delete(providerUri, null, new String[] { - packageName - }); - Log.d(TAG, "Deleted " + deleted + " entries for package " + packageName); - // Update UI refreshGrantedUriPermissions(); } @@ -513,7 +505,7 @@ public class AppStorageSettings extends AppInfoWithHeader public void onClick(DialogInterface dialog, int which) { mButtonsPref.setButton1Enabled(false); //force to recompute changed value - setIntentAndFinish(false, false); + setIntentAndFinish(false /* appChanged */); } }) .create(); @@ -614,7 +606,7 @@ public class AppStorageSettings extends AppInfoWithHeader @Override public int getMetricsCategory() { - return MetricsEvent.APPLICATIONS_APP_STORAGE; + return SettingsEnums.APPLICATIONS_APP_STORAGE; } class ClearCacheObserver extends IPackageDataObserver.Stub { diff --git a/src/com/android/settings/applications/AppStorageSizesController.java b/src/com/android/settings/applications/AppStorageSizesController.java index 6999a1581e..43734b272c 100644 --- a/src/com/android/settings/applications/AppStorageSizesController.java +++ b/src/com/android/settings/applications/AppStorageSizesController.java @@ -17,10 +17,11 @@ package com.android.settings.applications; import android.content.Context; +import android.text.format.Formatter; + import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.preference.Preference; -import android.text.format.Formatter; import com.android.internal.util.Preconditions; import com.android.settingslib.applications.StorageStatsSource; diff --git a/src/com/android/settings/applications/AppWithAdminGrantedPermissionsCounter.java b/src/com/android/settings/applications/AppWithAdminGrantedPermissionsCounter.java index 9bb7a4bf65..18a873acb3 100644 --- a/src/com/android/settings/applications/AppWithAdminGrantedPermissionsCounter.java +++ b/src/com/android/settings/applications/AppWithAdminGrantedPermissionsCounter.java @@ -23,8 +23,6 @@ import android.os.Build; import android.os.RemoteException; import android.os.UserHandle; -import com.android.settingslib.wrapper.PackageManagerWrapper; - /** * Counts installed apps across all users that have been granted one or more specific permissions by * the admin. @@ -36,7 +34,7 @@ public abstract class AppWithAdminGrantedPermissionsCounter extends AppCounter { private final DevicePolicyManager mDevicePolicyManager; public AppWithAdminGrantedPermissionsCounter(Context context, String[] permissions, - PackageManagerWrapper packageManager, IPackageManager packageManagerService, + PackageManager packageManager, IPackageManager packageManagerService, DevicePolicyManager devicePolicyManager) { super(context, packageManager); mPermissions = permissions; @@ -51,7 +49,7 @@ public abstract class AppWithAdminGrantedPermissionsCounter extends AppCounter { } public static boolean includeInCount(String[] permissions, - DevicePolicyManager devicePolicyManager, PackageManagerWrapper packageManager, + DevicePolicyManager devicePolicyManager, PackageManager packageManager, IPackageManager packageManagerService, ApplicationInfo info) { if (info.targetSdkVersion >= Build.VERSION_CODES.M) { // The app uses run-time permissions. Check whether one or more of the permissions were diff --git a/src/com/android/settings/applications/AppWithAdminGrantedPermissionsLister.java b/src/com/android/settings/applications/AppWithAdminGrantedPermissionsLister.java index 1308620848..ed3f985830 100644 --- a/src/com/android/settings/applications/AppWithAdminGrantedPermissionsLister.java +++ b/src/com/android/settings/applications/AppWithAdminGrantedPermissionsLister.java @@ -19,10 +19,9 @@ package com.android.settings.applications; import android.app.admin.DevicePolicyManager; import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; +import android.content.pm.PackageManager; import android.os.UserManager; -import com.android.settingslib.wrapper.PackageManagerWrapper; - /** * Lists installed apps across all users that have been granted one or more specific permissions by * the admin. @@ -33,7 +32,7 @@ public abstract class AppWithAdminGrantedPermissionsLister extends AppLister { private final DevicePolicyManager mDevicePolicyManager; public AppWithAdminGrantedPermissionsLister(String[] permissions, - PackageManagerWrapper packageManager, IPackageManager packageManagerService, + PackageManager packageManager, IPackageManager packageManagerService, DevicePolicyManager devicePolicyManager, UserManager userManager) { super(packageManager, userManager); mPermissions = permissions; diff --git a/src/com/android/settings/applications/ApplicationFeatureProvider.java b/src/com/android/settings/applications/ApplicationFeatureProvider.java index 3ffacb0bc1..e9f877e79b 100644 --- a/src/com/android/settings/applications/ApplicationFeatureProvider.java +++ b/src/com/android/settings/applications/ApplicationFeatureProvider.java @@ -82,6 +82,14 @@ public interface ApplicationFeatureProvider { Set<String> getKeepEnabledPackages(); /** + * Returns a user readable text explaining how much time user has spent in an app at a + * pre-specified duration. + */ + default CharSequence getTimeSpentInApp(String packageName) { + return null; + } + + /** * Callback that receives the number of packages installed on the device. */ interface NumberOfAppsCallback { diff --git a/src/com/android/settings/applications/ApplicationFeatureProviderImpl.java b/src/com/android/settings/applications/ApplicationFeatureProviderImpl.java index 3a88337fd2..1fd79971c8 100644 --- a/src/com/android/settings/applications/ApplicationFeatureProviderImpl.java +++ b/src/com/android/settings/applications/ApplicationFeatureProviderImpl.java @@ -25,6 +25,7 @@ import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.UserInfo; +import android.location.LocationManager; import android.os.RemoteException; import android.os.UserManager; import android.telecom.DefaultDialerManager; @@ -32,7 +33,7 @@ import android.text.TextUtils; import android.util.ArraySet; import com.android.internal.telephony.SmsApplication; -import com.android.settingslib.wrapper.PackageManagerWrapper; +import com.android.settings.R; import java.util.ArrayList; import java.util.List; @@ -40,13 +41,13 @@ import java.util.Set; public class ApplicationFeatureProviderImpl implements ApplicationFeatureProvider { - private final Context mContext; - private final PackageManagerWrapper mPm; + protected final Context mContext; + private final PackageManager mPm; private final IPackageManager mPms; private final DevicePolicyManager mDpm; private final UserManager mUm; - public ApplicationFeatureProviderImpl(Context context, PackageManagerWrapper pm, + public ApplicationFeatureProviderImpl(Context context, PackageManager pm, IPackageManager pms, DevicePolicyManager dpm) { mContext = context.getApplicationContext(); mPm = pm; @@ -139,6 +140,28 @@ public class ApplicationFeatureProviderImpl implements ApplicationFeatureProvide if (defaultSms != null) { keepEnabledPackages.add(defaultSms.getPackageName()); } + + keepEnabledPackages.addAll(getEnabledPackageWhitelist()); + + final LocationManager locationManager = + (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE); + final String locationHistoryPackage = locationManager.getExtraLocationControllerPackage(); + if (locationHistoryPackage != null) { + keepEnabledPackages.add(locationHistoryPackage); + } + return keepEnabledPackages; + } + + private Set<String> getEnabledPackageWhitelist() { + final Set<String> keepEnabledPackages = new ArraySet<>(); + + // Keep Settings intelligence enabled, otherwise search feature will be disabled. + keepEnabledPackages.add( + mContext.getString(R.string.config_settingsintelligence_package_name)); + + // Keep Package Installer enabled. + keepEnabledPackages.add(mContext.getString(R.string.config_package_installer_package_name)); + return keepEnabledPackages; } @@ -147,7 +170,7 @@ public class ApplicationFeatureProviderImpl implements ApplicationFeatureProvide private NumberOfAppsCallback mCallback; CurrentUserAndManagedProfilePolicyInstalledAppCounter(Context context, - PackageManagerWrapper packageManager, NumberOfAppsCallback callback) { + PackageManager packageManager, NumberOfAppsCallback callback) { super(context, PackageManager.INSTALL_REASON_POLICY, packageManager); mCallback = callback; } @@ -163,7 +186,7 @@ public class ApplicationFeatureProviderImpl implements ApplicationFeatureProvide private NumberOfAppsCallback mCallback; CurrentUserAndManagedProfileAppWithAdminGrantedPermissionsCounter(Context context, - String[] permissions, PackageManagerWrapper packageManager, + String[] permissions, PackageManager packageManager, IPackageManager packageManagerService, DevicePolicyManager devicePolicyManager, NumberOfAppsCallback callback) { super(context, permissions, packageManager, packageManagerService, devicePolicyManager); @@ -179,7 +202,7 @@ public class ApplicationFeatureProviderImpl implements ApplicationFeatureProvide private static class CurrentUserPolicyInstalledAppLister extends InstalledAppLister { private ListOfAppsCallback mCallback; - CurrentUserPolicyInstalledAppLister(PackageManagerWrapper packageManager, + CurrentUserPolicyInstalledAppLister(PackageManager packageManager, UserManager userManager, ListOfAppsCallback callback) { super(packageManager, userManager); mCallback = callback; @@ -196,7 +219,7 @@ public class ApplicationFeatureProviderImpl implements ApplicationFeatureProvide private ListOfAppsCallback mCallback; CurrentUserAppWithAdminGrantedPermissionsLister(String[] permissions, - PackageManagerWrapper packageManager, IPackageManager packageManagerService, + PackageManager packageManager, IPackageManager packageManagerService, DevicePolicyManager devicePolicyManager, UserManager userManager, ListOfAppsCallback callback) { super(permissions, packageManager, packageManagerService, devicePolicyManager, @@ -209,5 +232,4 @@ public class ApplicationFeatureProviderImpl implements ApplicationFeatureProvide mCallback.onListOfAppsResult(list); } } - } diff --git a/src/com/android/settings/applications/ClearDefaultsPreference.java b/src/com/android/settings/applications/ClearDefaultsPreference.java index 26eb0844ce..768fb4ea99 100644 --- a/src/com/android/settings/applications/ClearDefaultsPreference.java +++ b/src/com/android/settings/applications/ClearDefaultsPreference.java @@ -24,9 +24,6 @@ import android.os.IBinder; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; -import androidx.core.content.res.TypedArrayUtils; -import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; import android.text.SpannableString; import android.text.TextUtils; import android.text.style.BulletSpan; @@ -36,6 +33,10 @@ import android.view.View; import android.widget.Button; import android.widget.TextView; +import androidx.core.content.res.TypedArrayUtils; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + import com.android.settings.R; import com.android.settingslib.applications.AppUtils; import com.android.settingslib.applications.ApplicationsState; diff --git a/src/com/android/settings/applications/ConfirmConvertToFbe.java b/src/com/android/settings/applications/ConfirmConvertToFbe.java index 1a4421e230..35ddc6b3ca 100644 --- a/src/com/android/settings/applications/ConfirmConvertToFbe.java +++ b/src/com/android/settings/applications/ConfirmConvertToFbe.java @@ -15,7 +15,7 @@ */ package com.android.settings.applications; -import android.app.Fragment; +import android.app.settings.SettingsEnums; import android.content.Intent; import android.os.Bundle; import android.view.LayoutInflater; @@ -23,7 +23,6 @@ import android.view.View; import android.view.ViewGroup; import android.widget.Button; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; @@ -51,6 +50,6 @@ public class ConfirmConvertToFbe extends SettingsPreferenceFragment { @Override public int getMetricsCategory() { - return MetricsEvent.CONVERT_FBE_CONFIRM; + return SettingsEnums.CONVERT_FBE_CONFIRM; } } diff --git a/src/com/android/settings/applications/ConvertToFbe.java b/src/com/android/settings/applications/ConvertToFbe.java index 70ee415eaa..2a6bdaf2c4 100644 --- a/src/com/android/settings/applications/ConvertToFbe.java +++ b/src/com/android/settings/applications/ConvertToFbe.java @@ -17,6 +17,7 @@ package com.android.settings.applications; import android.annotation.Nullable; import android.app.Activity; +import android.app.settings.SettingsEnums; import android.content.Intent; import android.content.res.Resources; import android.os.Bundle; @@ -25,7 +26,6 @@ import android.view.View; import android.view.ViewGroup; import android.widget.Button; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.core.InstrumentedFragment; import com.android.settings.core.SubSettingLauncher; @@ -83,13 +83,13 @@ public class ConvertToFbe extends InstrumentedFragment { private void convert() { new SubSettingLauncher(getContext()) .setDestination(ConfirmConvertToFbe.class.getName()) - .setTitle(R.string.convert_to_file_encryption) + .setTitleRes(R.string.convert_to_file_encryption) .setSourceMetricsCategory(getMetricsCategory()) .launch(); } @Override public int getMetricsCategory() { - return MetricsEvent.CONVERT_FBE; + return SettingsEnums.CONVERT_FBE; } } diff --git a/src/com/android/settings/applications/DefaultAppSettings.java b/src/com/android/settings/applications/DefaultAppSettings.java deleted file mode 100644 index 7fc405f3d3..0000000000 --- a/src/com/android/settings/applications/DefaultAppSettings.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright (C) 2017 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.applications; - -import android.app.Activity; -import android.content.Context; -import android.provider.SearchIndexableResource; -import android.text.TextUtils; - -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.settings.R; -import com.android.settings.applications.assist.DefaultAssistPreferenceController; -import com.android.settings.applications.defaultapps.DefaultBrowserPreferenceController; -import com.android.settings.applications.defaultapps.DefaultEmergencyPreferenceController; -import com.android.settings.applications.defaultapps.DefaultHomePreferenceController; -import com.android.settings.applications.defaultapps.DefaultPaymentSettingsPreferenceController; -import com.android.settings.applications.defaultapps.DefaultPhonePreferenceController; -import com.android.settings.applications.defaultapps.DefaultSmsPreferenceController; -import com.android.settings.applications.defaultapps.DefaultWorkBrowserPreferenceController; -import com.android.settings.applications.defaultapps.DefaultWorkPhonePreferenceController; -import com.android.settings.dashboard.DashboardFragment; -import com.android.settings.dashboard.SummaryLoader; -import com.android.settings.search.BaseSearchIndexProvider; -import com.android.settings.search.Indexable; -import com.android.settings.widget.PreferenceCategoryController; -import com.android.settingslib.core.AbstractPreferenceController; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -public class DefaultAppSettings extends DashboardFragment { - - static final String TAG = "DefaultAppSettings"; - - private static final String KEY_DEFAULT_WORK_CATEGORY = "work_app_defaults"; - private static final String KEY_ASSIST_VOICE_INPUT = "assist_and_voice_input"; - - @Override - protected String getLogTag() { - return TAG; - } - - @Override - protected int getPreferenceScreenResId() { - return R.xml.app_default_settings; - } - - @Override - protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { - return buildPreferenceControllers(context); - } - - @Override - public int getMetricsCategory() { - return MetricsEvent.APPLICATIONS_ADVANCED; - } - - private static List<AbstractPreferenceController> buildPreferenceControllers(Context context) { - final List<AbstractPreferenceController> controllers = new ArrayList<>(); - final List<AbstractPreferenceController> workControllers = new ArrayList<>(); - workControllers.add(new DefaultWorkPhonePreferenceController(context)); - workControllers.add(new DefaultWorkBrowserPreferenceController(context)); - controllers.addAll(workControllers); - controllers.add(new PreferenceCategoryController( - context, KEY_DEFAULT_WORK_CATEGORY).setChildren(workControllers)); - controllers.add(new DefaultAssistPreferenceController(context, KEY_ASSIST_VOICE_INPUT, - false /* showSetting */)); - controllers.add(new DefaultBrowserPreferenceController(context)); - controllers.add(new DefaultPhonePreferenceController(context)); - controllers.add(new DefaultSmsPreferenceController(context)); - controllers.add(new DefaultEmergencyPreferenceController(context)); - controllers.add(new DefaultHomePreferenceController(context)); - controllers.add(new DefaultPaymentSettingsPreferenceController(context)); - return controllers; - } - - public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = - new BaseSearchIndexProvider() { - @Override - public List<SearchIndexableResource> getXmlResourcesToIndex( - Context context, boolean enabled) { - final SearchIndexableResource sir = new SearchIndexableResource(context); - sir.xmlResId = R.xml.app_default_settings; - return Arrays.asList(sir); - } - - @Override - public List<String> getNonIndexableKeys(Context context) { - List<String> keys = super.getNonIndexableKeys(context); - keys.add(KEY_ASSIST_VOICE_INPUT); - // TODO (b/38230148) Remove these keys when we can differentiate work results - keys.add(DefaultWorkPhonePreferenceController.KEY); - keys.add(DefaultWorkBrowserPreferenceController.KEY); - return keys; - } - - @Override - public List<AbstractPreferenceController> createPreferenceControllers( - Context context) { - return buildPreferenceControllers(context); - } - }; - - static class SummaryProvider implements SummaryLoader.SummaryProvider { - - private final Context mContext; - private final SummaryLoader mSummaryLoader; - private final DefaultSmsPreferenceController mDefaultSmsPreferenceController; - private final DefaultBrowserPreferenceController mDefaultBrowserPreferenceController; - private final DefaultPhonePreferenceController mDefaultPhonePreferenceController; - - public SummaryProvider(Context context, SummaryLoader summaryLoader) { - mContext = context; - mSummaryLoader = summaryLoader; - mDefaultSmsPreferenceController = new DefaultSmsPreferenceController(mContext); - mDefaultBrowserPreferenceController = new DefaultBrowserPreferenceController(mContext); - mDefaultPhonePreferenceController = new DefaultPhonePreferenceController(mContext); - } - - @Override - public void setListening(boolean listening) { - if (!listening) { - return; - } - CharSequence summary = concatSummaryText( - mDefaultBrowserPreferenceController.getDefaultAppLabel(), - mDefaultPhonePreferenceController.getDefaultAppLabel()); - summary = concatSummaryText(summary, - mDefaultSmsPreferenceController.getDefaultAppLabel()); - if (!TextUtils.isEmpty(summary)) { - mSummaryLoader.setSummary(this, summary); - } - } - - private CharSequence concatSummaryText(CharSequence summary1, CharSequence summary2) { - if (TextUtils.isEmpty(summary1)) { - return summary2; - } - if (TextUtils.isEmpty(summary2)) { - return summary1; - } - return mContext.getString(R.string.join_many_items_middle, summary1, summary2); - } - } - - public static final SummaryLoader.SummaryProviderFactory SUMMARY_PROVIDER_FACTORY = - new SummaryLoader.SummaryProviderFactory() { - @Override - public SummaryLoader.SummaryProvider createSummaryProvider(Activity activity, - SummaryLoader summaryLoader) { - return new DefaultAppSettings.SummaryProvider(activity, summaryLoader); - } - }; -} diff --git a/src/com/android/settings/applications/DefaultAppsPreferenceController.java b/src/com/android/settings/applications/DefaultAppsPreferenceController.java new file mode 100644 index 0000000000..4d4165b8f1 --- /dev/null +++ b/src/com/android/settings/applications/DefaultAppsPreferenceController.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2019 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.applications; + +import android.app.role.RoleManager; +import android.content.Context; +import android.content.pm.PackageManager; +import android.icu.text.ListFormatter; +import android.text.TextUtils; + +import com.android.settings.core.BasePreferenceController; +import com.android.settingslib.applications.AppUtils; + +import java.util.ArrayList; +import java.util.List; + +public class DefaultAppsPreferenceController extends BasePreferenceController { + + private final PackageManager mPackageManager; + private final RoleManager mRoleManager; + + public DefaultAppsPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + + mPackageManager = context.getPackageManager(); + mRoleManager = context.getSystemService(RoleManager.class); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } + + @Override + public CharSequence getSummary() { + final List<CharSequence> defaultAppLabels = new ArrayList<>(); + final CharSequence defaultBrowserLabel = getDefaultAppLabel(RoleManager.ROLE_BROWSER); + if(!TextUtils.isEmpty(defaultBrowserLabel)) { + defaultAppLabels.add(defaultBrowserLabel); + } + final CharSequence defaultPhoneLabel = getDefaultAppLabel(RoleManager.ROLE_DIALER); + if(!TextUtils.isEmpty(defaultPhoneLabel)) { + defaultAppLabels.add(defaultPhoneLabel); + } + final CharSequence defaultSmsLabel = getDefaultAppLabel(RoleManager.ROLE_SMS); + if(!TextUtils.isEmpty(defaultSmsLabel)) { + defaultAppLabels.add(defaultSmsLabel); + } + if (defaultAppLabels.isEmpty()) { + return null; + } + return ListFormatter.getInstance().format(defaultAppLabels); + } + + private CharSequence getDefaultAppLabel(String roleName) { + final List<String> packageNames = mRoleManager.getRoleHolders(roleName); + if (packageNames.isEmpty()) { + return null; + } + final String packageName = packageNames.get(0); + return AppUtils.getApplicationLabel(mPackageManager, packageName); + } +} diff --git a/src/com/android/settings/applications/DirectoryAccessDetails.java b/src/com/android/settings/applications/DirectoryAccessDetails.java deleted file mode 100644 index 8a8fa98b65..0000000000 --- a/src/com/android/settings/applications/DirectoryAccessDetails.java +++ /dev/null @@ -1,327 +0,0 @@ -/* - * Copyright (C) 2018 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.applications; - -import static android.os.storage.StorageVolume.ScopedAccessProviderContract.AUTHORITY; -import static android.os.storage.StorageVolume.ScopedAccessProviderContract.COL_DIRECTORY; -import static android.os.storage.StorageVolume.ScopedAccessProviderContract.COL_GRANTED; -import static android.os.storage.StorageVolume.ScopedAccessProviderContract.COL_PACKAGE; -import static android.os.storage.StorageVolume.ScopedAccessProviderContract.COL_VOLUME_UUID; -import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS; -import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COLUMNS; -import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COL_DIRECTORY; -import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COL_GRANTED; -import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COL_PACKAGE; -import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COL_VOLUME_UUID; - -import static com.android.settings.applications.AppStateDirectoryAccessBridge.DEBUG; -import static com.android.settings.applications.AppStateDirectoryAccessBridge.VERBOSE; - -import android.annotation.Nullable; -import android.app.Activity; -import android.app.AlertDialog; -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.os.Bundle; -import android.os.storage.StorageManager; -import android.os.storage.VolumeInfo; -import androidx.preference.SwitchPreference; -import androidx.preference.Preference; -import androidx.preference.PreferenceGroupAdapter; -import androidx.preference.Preference.OnPreferenceChangeListener; -import androidx.preference.Preference.OnPreferenceClickListener; -import androidx.preference.PreferenceCategory; -import android.text.TextUtils; -import androidx.preference.PreferenceManager; -import androidx.preference.PreferenceScreen; -import android.util.ArrayMap; -import android.util.ArraySet; -import android.util.IconDrawableFactory; -import android.util.Log; -import android.util.Pair; - -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.settings.R; -import com.android.settings.widget.EntityHeaderController; -import com.android.settings.widget.EntityHeaderController.ActionType; -import com.android.settingslib.applications.AppUtils; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * Detailed settings for an app's directory access permissions (A.K.A Scoped Directory Access). - * - * <p>Currently, it shows the entry for which the user denied access with the "Do not ask again" - * flag checked on: the user than can use the settings toggle to reset that deniel. - * - * <p>This fragments dynamically lists all such permissions, starting with one preference per - * directory in the primary storage, then adding additional entries for the external volumes (one - * entry for the whole volume). - */ -// TODO(b/72055774): add unit tests -public class DirectoryAccessDetails extends AppInfoBase { - - @SuppressWarnings("hiding") - private static final String TAG = "DirectoryAccessDetails"; - - private boolean mCreated; - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - if (mCreated) { - Log.w(TAG, "onActivityCreated(): ignoring duplicate call"); - return; - } - mCreated = true; - if (mPackageInfo == null) { - Log.w(TAG, "onActivityCreated(): no package info"); - return; - } - final Activity activity = getActivity(); - final Preference pref = EntityHeaderController - .newInstance(activity, this, /* header= */ null ) - .setRecyclerView(getListView(), getLifecycle()) - .setIcon(IconDrawableFactory.newInstance(getPrefContext()) - .getBadgedIcon(mPackageInfo.applicationInfo)) - .setLabel(mPackageInfo.applicationInfo.loadLabel(mPm)) - .setIsInstantApp(AppUtils.isInstant(mPackageInfo.applicationInfo)) - .setPackageName(mPackageName) - .setUid(mPackageInfo.applicationInfo.uid) - .setHasAppInfoLink(false) - .setButtonActions(ActionType.ACTION_NONE, ActionType.ACTION_NONE) - .done(activity, getPrefContext()); - getPreferenceScreen().addPreference(pref); - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - addPreferencesFromResource(R.xml.directory_access_details); - - } - - @Override - protected boolean refreshUi() { - final Context context = getPrefContext(); - final PreferenceScreen prefsGroup = getPreferenceScreen(); - prefsGroup.removeAll(); - - final Map<String, ExternalVolume> externalVolumes = new HashMap<>(); - - final Uri providerUri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) - .authority(AUTHORITY).appendPath(TABLE_PERMISSIONS).appendPath("*") - .build(); - // Query provider for entries. - try (Cursor cursor = context.getContentResolver().query(providerUri, - TABLE_PERMISSIONS_COLUMNS, null, new String[] { mPackageName }, null)) { - if (cursor == null) { - Log.w(TAG, "Didn't get cursor for " + mPackageName); - return true; - } - final int count = cursor.getCount(); - if (count == 0) { - // This setting screen should not be reached if there was no permission, so just - // ignore it - Log.w(TAG, "No permissions for " + mPackageName); - return true; - } - - while (cursor.moveToNext()) { - final String pkg = cursor.getString(TABLE_PERMISSIONS_COL_PACKAGE); - final String uuid = cursor.getString(TABLE_PERMISSIONS_COL_VOLUME_UUID); - final String dir = cursor.getString(TABLE_PERMISSIONS_COL_DIRECTORY); - final boolean granted = cursor.getInt(TABLE_PERMISSIONS_COL_GRANTED) == 1; - if (VERBOSE) { - Log.v(TAG, "Pkg:" + pkg + " uuid: " + uuid + " dir: " + dir - + " granted:" + granted); - } - - if (!mPackageName.equals(pkg)) { - // Sanity check, shouldn't happen - Log.w(TAG, "Ignoring " + uuid + "/" + dir + " due to package mismatch: " - + "expected " + mPackageName + ", got " + pkg); - continue; - } - - if (uuid == null) { - if (dir == null) { - // Sanity check, shouldn't happen - Log.wtf(TAG, "Ignoring permission on primary storage root"); - } else { - // Primary storage entry: add right away - prefsGroup.addPreference(newPreference(context, dir, providerUri, - /* uuid= */ null, dir, granted, /* children= */ null)); - } - } else { - // External volume entry: save it for later. - ExternalVolume externalVolume = externalVolumes.get(uuid); - if (externalVolume == null) { - externalVolume = new ExternalVolume(uuid); - externalVolumes.put(uuid, externalVolume); - } - if (dir == null) { - // Whole volume - externalVolume.granted = granted; - } else { - // Directory only - externalVolume.children.add(new Pair<>(dir, granted)); - } - } - } - } - - if (VERBOSE) { - Log.v(TAG, "external volumes: " + externalVolumes); - } - - if (externalVolumes.isEmpty()) { - // We're done! - return true; - } - - // Add entries from external volumes - - // Query StorageManager to get the user-friendly volume names. - final StorageManager sm = context.getSystemService(StorageManager.class); - final List<VolumeInfo> volumes = sm.getVolumes(); - if (volumes.isEmpty()) { - Log.w(TAG, "StorageManager returned no secondary volumes"); - return true; - } - final Map<String, String> volumeNames = new HashMap<>(volumes.size()); - for (VolumeInfo volume : volumes) { - final String uuid = volume.getFsUuid(); - if (uuid == null) continue; // Primary storage; not used. - - String name = sm.getBestVolumeDescription(volume); - if (name == null) { - Log.w(TAG, "No description for " + volume + "; using uuid instead: " + uuid); - name = uuid; - } - volumeNames.put(uuid, name); - } - if (VERBOSE) { - Log.v(TAG, "UUID -> name mapping: " + volumeNames); - } - - for (ExternalVolume volume : externalVolumes.values()) { - final String volumeName = volumeNames.get(volume.uuid); - if (volumeName == null) { - Log.w(TAG, "Ignoring entry for invalid UUID: " + volume.uuid); - continue; - } - // First add the pref for the whole volume... - final PreferenceCategory category = new PreferenceCategory(context); - prefsGroup.addPreference(category); - final Set<SwitchPreference> children = new HashSet<>(volume.children.size()); - category.addPreference(newPreference(context, volumeName, providerUri, volume.uuid, - /* dir= */ null, volume.granted, children)); - - // ... then the children prefs - volume.children.forEach((pair) -> { - final String dir = pair.first; - final String name = context.getResources() - .getString(R.string.directory_on_volume, volumeName, dir); - final SwitchPreference childPref = - newPreference(context, name, providerUri, volume.uuid, dir, pair.second, - /* children= */ null); - category.addPreference(childPref); - children.add(childPref); - }); - } - return true; - } - - private SwitchPreference newPreference(Context context, String title, Uri providerUri, - String uuid, String dir, boolean granted, @Nullable Set<SwitchPreference> children) { - final SwitchPreference pref = new SwitchPreference(context); - pref.setKey(String.format("%s:%s", uuid, dir)); - pref.setTitle(title); - pref.setChecked(granted); - pref.setOnPreferenceChangeListener((unused, value) -> { - if (!Boolean.class.isInstance(value)) { - // Sanity check - Log.wtf(TAG, "Invalid value from switch: " + value); - return true; - } - final boolean newValue = ((Boolean) value).booleanValue(); - - resetDoNotAskAgain(context, newValue, providerUri, uuid, dir); - if (children != null) { - // When parent is granted, children should be hidden; and vice versa - final boolean newChildValue = !newValue; - for (SwitchPreference child : children) { - child.setVisible(newChildValue); - } - } - return true; - }); - return pref; - } - - private void resetDoNotAskAgain(Context context, boolean newValue, Uri providerUri, - @Nullable String uuid, @Nullable String directory) { - if (DEBUG) { - Log.d(TAG, "Asking " + providerUri + " to update " + uuid + "/" + directory + " to " - + newValue); - } - final ContentValues values = new ContentValues(1); - values.put(COL_GRANTED, newValue); - final int updated = context.getContentResolver().update(providerUri, values, - null, new String[] { mPackageName, uuid, directory }); - if (DEBUG) { - Log.d(TAG, "Updated " + updated + " entries for " + uuid + "/" + directory); - } - } - - @Override - protected AlertDialog createDialog(int id, int errorCode) { - return null; - } - - @Override - public int getMetricsCategory() { - return MetricsEvent.APPLICATIONS_DIRECTORY_ACCESS_DETAIL; - } - - private static class ExternalVolume { - final String uuid; - final List<Pair<String, Boolean>> children = new ArrayList<>(); - boolean granted; - - ExternalVolume(String uuid) { - this.uuid = uuid; - } - - @Override - public String toString() { - return "ExternalVolume: [uuid=" + uuid + ", granted=" + granted + - ", children=" + children + "]"; - } - } -} diff --git a/src/com/android/settings/applications/FetchPackageStorageAsyncLoader.java b/src/com/android/settings/applications/FetchPackageStorageAsyncLoader.java index 9ff96c1f0b..f3ad32655c 100644 --- a/src/com/android/settings/applications/FetchPackageStorageAsyncLoader.java +++ b/src/com/android/settings/applications/FetchPackageStorageAsyncLoader.java @@ -26,14 +26,14 @@ import android.util.Log; import com.android.internal.util.Preconditions; import com.android.settingslib.applications.StorageStatsSource; import com.android.settingslib.applications.StorageStatsSource.AppStorageStats; -import com.android.settingslib.utils.AsyncLoader; +import com.android.settingslib.utils.AsyncLoaderCompat; import java.io.IOException; /** * Fetches the storage stats using the StorageStatsManager for a given package and user tuple. */ -public class FetchPackageStorageAsyncLoader extends AsyncLoader<AppStorageStats> { +public class FetchPackageStorageAsyncLoader extends AsyncLoaderCompat<AppStorageStats> { private static final String TAG = "FetchPackageStorage"; private final StorageStatsSource mSource; private final ApplicationInfo mInfo; diff --git a/src/com/android/settings/applications/InstalledAppCounter.java b/src/com/android/settings/applications/InstalledAppCounter.java index 26372ee577..aeac26e4e7 100644 --- a/src/com/android/settings/applications/InstalledAppCounter.java +++ b/src/com/android/settings/applications/InstalledAppCounter.java @@ -17,12 +17,10 @@ package com.android.settings.applications; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; -import android.content.pm.ResolveInfo; import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.os.UserHandle; -import com.android.settingslib.wrapper.PackageManagerWrapper; - import java.util.List; public abstract class InstalledAppCounter extends AppCounter { @@ -35,7 +33,7 @@ public abstract class InstalledAppCounter extends AppCounter { private final int mInstallReason; public InstalledAppCounter(Context context, int installReason, - PackageManagerWrapper packageManager) { + PackageManager packageManager) { super(context, packageManager); mInstallReason = installReason; } @@ -45,7 +43,7 @@ public abstract class InstalledAppCounter extends AppCounter { return includeInCount(mInstallReason, mPm, info); } - public static boolean includeInCount(int installReason, PackageManagerWrapper pm, + public static boolean includeInCount(int installReason, PackageManager pm, ApplicationInfo info) { final int userId = UserHandle.getUserId(info.uid); if (installReason != IGNORE_INSTALL_REASON diff --git a/src/com/android/settings/applications/InstalledAppLister.java b/src/com/android/settings/applications/InstalledAppLister.java index 3312d3ebce..547822627f 100644 --- a/src/com/android/settings/applications/InstalledAppLister.java +++ b/src/com/android/settings/applications/InstalledAppLister.java @@ -20,11 +20,9 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.os.UserManager; -import com.android.settingslib.wrapper.PackageManagerWrapper; - public abstract class InstalledAppLister extends AppLister { - public InstalledAppLister(PackageManagerWrapper packageManager, UserManager userManager) { + public InstalledAppLister(PackageManager packageManager, UserManager userManager) { super(packageManager, userManager); } diff --git a/src/com/android/settings/applications/InstalledAppOpenByDefaultPage.java b/src/com/android/settings/applications/InstalledAppOpenByDefaultActivity.java index 40eef25840..cd30d792ef 100644 --- a/src/com/android/settings/applications/InstalledAppOpenByDefaultPage.java +++ b/src/com/android/settings/applications/InstalledAppOpenByDefaultActivity.java @@ -20,7 +20,7 @@ import android.content.Intent; import com.android.settings.SettingsActivity; -public class InstalledAppOpenByDefaultPage extends SettingsActivity { +public class InstalledAppOpenByDefaultActivity extends SettingsActivity { @Override public Intent getIntent() { diff --git a/src/com/android/settings/applications/LayoutPreference.java b/src/com/android/settings/applications/LayoutPreference.java deleted file mode 100644 index f80100f9d2..0000000000 --- a/src/com/android/settings/applications/LayoutPreference.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * 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.applications; - -import android.content.Context; -import android.content.res.TypedArray; -import androidx.annotation.VisibleForTesting; -import androidx.core.content.res.TypedArrayUtils; -import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.FrameLayout; - -import com.android.settings.R; -import com.android.settings.Utils; - -public class LayoutPreference extends Preference { - - private final View.OnClickListener mClickListener = v -> performClick(v); - private boolean mAllowDividerAbove; - private boolean mAllowDividerBelow; - - @VisibleForTesting - View mRootView; - - public LayoutPreference(Context context, AttributeSet attrs) { - super(context, attrs); - init(context, attrs, 0 /* defStyleAttr */); - } - - public LayoutPreference(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - init(context, attrs, defStyleAttr); - } - - public LayoutPreference(Context context, int resource) { - this(context, LayoutInflater.from(context).inflate(resource, null, false)); - } - - public LayoutPreference(Context context, View view) { - super(context); - setView(view); - } - - private void init(Context context, AttributeSet attrs, int defStyleAttr) { - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Preference); - mAllowDividerAbove = TypedArrayUtils.getBoolean(a, R.styleable.Preference_allowDividerAbove, - R.styleable.Preference_allowDividerAbove, false); - mAllowDividerBelow = TypedArrayUtils.getBoolean(a, R.styleable.Preference_allowDividerBelow, - R.styleable.Preference_allowDividerBelow, false); - a.recycle(); - - a = context.obtainStyledAttributes( - attrs, com.android.internal.R.styleable.Preference, defStyleAttr, 0); - int layoutResource = a.getResourceId(com.android.internal.R.styleable.Preference_layout, - 0); - if (layoutResource == 0) { - throw new IllegalArgumentException("LayoutPreference requires a layout to be defined"); - } - a.recycle(); - - // Need to create view now so that findViewById can be called immediately. - final View view = LayoutInflater.from(getContext()) - .inflate(layoutResource, null, false); - setView(view); - } - - private void setView(View view) { - setLayoutResource(R.layout.layout_preference_frame); - final ViewGroup allDetails = view.findViewById(R.id.all_details); - if (allDetails != null) { - Utils.forceCustomPadding(allDetails, true /* additive padding */); - } - mRootView = view; - setShouldDisableView(false); - } - - @Override - public void onBindViewHolder(PreferenceViewHolder holder) { - holder.itemView.setOnClickListener(mClickListener); - - final boolean selectable = isSelectable(); - holder.itemView.setFocusable(selectable); - holder.itemView.setClickable(selectable); - holder.setDividerAllowedAbove(mAllowDividerAbove); - holder.setDividerAllowedBelow(mAllowDividerBelow); - - FrameLayout layout = (FrameLayout) holder.itemView; - layout.removeAllViews(); - ViewGroup parent = (ViewGroup) mRootView.getParent(); - if (parent != null) { - parent.removeView(mRootView); - } - layout.addView(mRootView); - } - - public <T extends View> T findViewById(int id) { - return mRootView.findViewById(id); - } - -}
\ No newline at end of file diff --git a/src/com/android/settings/applications/ManageDomainUrls.java b/src/com/android/settings/applications/ManageDomainUrls.java deleted file mode 100644 index 1b9dbaf64c..0000000000 --- a/src/com/android/settings/applications/ManageDomainUrls.java +++ /dev/null @@ -1,293 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file - * except in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the - * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package com.android.settings.applications; - -import android.app.Application; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.os.Bundle; -import android.os.UserHandle; -import android.provider.Settings; -import android.provider.Settings.Global; -import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; -import androidx.preference.Preference; -import androidx.preference.Preference.OnPreferenceChangeListener; -import androidx.preference.Preference.OnPreferenceClickListener; -import androidx.preference.PreferenceCategory; -import androidx.preference.PreferenceGroup; -import androidx.preference.PreferenceViewHolder; -import android.util.ArraySet; -import android.view.View; - -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.settings.R; -import com.android.settings.SettingsPreferenceFragment; -import com.android.settings.Utils; -import com.android.settings.widget.AppPreference; -import com.android.settingslib.applications.ApplicationsState; -import com.android.settingslib.applications.ApplicationsState.AppEntry; - -import java.util.ArrayList; - -/** - * Activity to manage how Android handles URL resolution. Includes both per-app - * handling as well as system handling for Web Actions. - */ -public class ManageDomainUrls extends SettingsPreferenceFragment - implements ApplicationsState.Callbacks, OnPreferenceChangeListener, - OnPreferenceClickListener { - - // constant value that can be used to check return code from sub activity. - private static final int INSTALLED_APP_DETAILS = 1; - - private ApplicationsState mApplicationsState; - private ApplicationsState.Session mSession; - private PreferenceGroup mDomainAppList; - private SwitchPreference mWebAction; - private Preference mInstantAppAccountPreference; - - @Override - public void onCreate(Bundle icicle) { - super.onCreate(icicle); - setAnimationAllowed(true); - mApplicationsState = ApplicationsState.getInstance( - (Application) getContext().getApplicationContext()); - mSession = mApplicationsState.newSession(this, getLifecycle()); - setHasOptionsMenu(true); - } - - @Override - protected int getPreferenceScreenResId() { - return R.xml.manage_domain_url_settings; - } - - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - } - - @Override - public void onRunningStateChanged(boolean running) { - } - - @Override - public void onPackageListChanged() { - } - - @Override - public void onRebuildComplete(ArrayList<AppEntry> apps) { - if (getContext() == null) { - return; - } - - final boolean disableWebActions = Global.getInt(getContext().getContentResolver(), - Global.ENABLE_EPHEMERAL_FEATURE, 1) == 0; - if (disableWebActions) { - mDomainAppList = getPreferenceScreen(); - } else { - final PreferenceGroup preferenceScreen = getPreferenceScreen(); - if (preferenceScreen.getPreferenceCount() == 0) { - // add preferences - final PreferenceCategory webActionCategory = - new PreferenceCategory(getPrefContext()); - webActionCategory.setTitle(R.string.web_action_section_title); - preferenceScreen.addPreference(webActionCategory); - - // toggle to enable / disable Web Actions [aka Instant Apps] - mWebAction = new SwitchPreference(getPrefContext()); - mWebAction.setTitle(R.string.web_action_enable_title); - mWebAction.setSummary(R.string.web_action_enable_summary); - mWebAction.setChecked(Settings.Secure.getInt(getContentResolver(), - Settings.Secure.INSTANT_APPS_ENABLED, 1) != 0); - mWebAction.setOnPreferenceChangeListener(this); - webActionCategory.addPreference(mWebAction); - - // Determine whether we should show the instant apps account chooser setting - ComponentName instantAppSettingsComponent = getActivity().getPackageManager() - .getInstantAppResolverSettingsComponent(); - Intent instantAppSettingsIntent = null; - if (instantAppSettingsComponent != null) { - instantAppSettingsIntent = - new Intent().setComponent(instantAppSettingsComponent); - } - if (instantAppSettingsIntent != null) { - final Intent launchIntent = instantAppSettingsIntent; - // TODO: Make this button actually launch the account chooser. - mInstantAppAccountPreference = new Preference(getPrefContext()); - mInstantAppAccountPreference.setTitle(R.string.instant_apps_settings); - mInstantAppAccountPreference.setOnPreferenceClickListener(pref -> { - startActivity(launchIntent); - return true; - }); - webActionCategory.addPreference(mInstantAppAccountPreference); - } - - // list to manage link handling per app - mDomainAppList = new PreferenceCategory(getPrefContext()); - mDomainAppList.setTitle(R.string.domain_url_section_title); - preferenceScreen.addPreference(mDomainAppList); - } - } - rebuildAppList(mDomainAppList, apps); - } - - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - if (preference == mWebAction) { - boolean checked = (boolean) newValue; - Settings.Secure.putInt( - getContentResolver(), - Settings.Secure.INSTANT_APPS_ENABLED, checked ? 1 : 0); - return true; - } - return false; - } - - private void rebuild() { - final ArrayList<AppEntry> apps = mSession.rebuild( - ApplicationsState.FILTER_WITH_DOMAIN_URLS, ApplicationsState.ALPHA_COMPARATOR); - if (apps != null) { - onRebuildComplete(apps); - } - } - - private void rebuildAppList(PreferenceGroup group, ArrayList<AppEntry> apps) { - cacheRemoveAllPrefs(group); - final int N = apps.size(); - for (int i = 0; i < N; i++) { - AppEntry entry = apps.get(i); - String key = entry.info.packageName + "|" + entry.info.uid; - DomainAppPreference preference = (DomainAppPreference) getCachedPreference(key); - if (preference == null) { - preference = new DomainAppPreference(getPrefContext(), mApplicationsState, entry); - preference.setKey(key); - preference.setOnPreferenceClickListener(this); - group.addPreference(preference); - } else { - preference.reuse(); - } - preference.setOrder(i); - } - removeCachedPrefs(group); - } - - @Override - public void onPackageIconChanged() { - } - - @Override - public void onPackageSizeChanged(String packageName) { - } - - @Override - public void onAllSizesComputed() { - } - - @Override - public void onLauncherInfoChanged() { - } - - @Override - public void onLoadEntriesCompleted() { - rebuild(); - } - - @Override - public int getMetricsCategory() { - return MetricsEvent.MANAGE_DOMAIN_URLS; - } - - @Override - public boolean onPreferenceClick(Preference preference) { - if (preference.getClass() == DomainAppPreference.class) { - ApplicationsState.AppEntry entry = ((DomainAppPreference) preference).mEntry; - AppInfoBase.startAppInfoFragment(AppLaunchSettings.class, R.string.auto_launch_label, - entry.info.packageName, entry.info.uid, this, - INSTALLED_APP_DETAILS, getMetricsCategory()); - return true; - } - return false; - } - - @VisibleForTesting - static class DomainAppPreference extends AppPreference { - private final AppEntry mEntry; - private final PackageManager mPm; - private final ApplicationsState mApplicationsState; - - public DomainAppPreference(final Context context, ApplicationsState applicationsState, - AppEntry entry) { - super(context); - mApplicationsState = applicationsState; - mPm = context.getPackageManager(); - mEntry = entry; - mEntry.ensureLabel(getContext()); - setState(); - if (mEntry.icon != null) { - setIcon(mEntry.icon); - } - } - - private void setState() { - setTitle(mEntry.label); - setSummary(getDomainsSummary(mEntry.info.packageName)); - } - - public void reuse() { - setState(); - notifyChanged(); - } - - @Override - public void onBindViewHolder(PreferenceViewHolder holder) { - if (mEntry.icon == null) { - holder.itemView.post(new Runnable() { - @Override - public void run() { - // Ensure we have an icon before binding. - mApplicationsState.ensureIcon(mEntry); - // This might trigger us to bind again, but it gives an easy way to only - // load the icon once its needed, so its probably worth it. - setIcon(mEntry.icon); - } - }); - } - super.onBindViewHolder(holder); - } - - private CharSequence getDomainsSummary(String packageName) { - // If the user has explicitly said "no" for this package, that's the - // string we should show. - int domainStatus = - mPm.getIntentVerificationStatusAsUser(packageName, UserHandle.myUserId()); - if (domainStatus == PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_NEVER) { - return getContext().getString(R.string.domain_urls_summary_none); - } - // Otherwise, ask package manager for the domains for this package, - // and show the first one (or none if there aren't any). - ArraySet<String> result = Utils.getHandledDomains(mPm, packageName); - if (result.size() == 0) { - return getContext().getString(R.string.domain_urls_summary_none); - } else if (result.size() == 1) { - return getContext().getString(R.string.domain_urls_summary_one, result.valueAt(0)); - } else { - return getContext().getString(R.string.domain_urls_summary_some, result.valueAt(0)); - } - } - } -} diff --git a/src/com/android/settings/applications/ProcStatsPackageEntry.java b/src/com/android/settings/applications/ProcStatsPackageEntry.java index 88d5bd645e..0c4f9be82b 100644 --- a/src/com/android/settings/applications/ProcStatsPackageEntry.java +++ b/src/com/android/settings/applications/ProcStatsPackageEntry.java @@ -21,8 +21,8 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.os.Parcel; import android.os.Parcelable; - import android.util.Log; + import com.android.settings.R; import com.android.settings.Utils; diff --git a/src/com/android/settings/applications/ProcessStatsBase.java b/src/com/android/settings/applications/ProcessStatsBase.java index 67a324bb1e..67fc4c1eb3 100644 --- a/src/com/android/settings/applications/ProcessStatsBase.java +++ b/src/com/android/settings/applications/ProcessStatsBase.java @@ -30,6 +30,7 @@ import com.android.settings.SettingsPreferenceFragment; import com.android.settings.applications.ProcStatsData.MemInfo; import com.android.settings.core.SubSettingLauncher; import com.android.settingslib.core.instrumentation.Instrumentable; +import com.android.settingslib.widget.settingsspinner.SettingsSpinnerAdapter; public abstract class ProcessStatsBase extends SettingsPreferenceFragment implements OnItemSelectedListener { @@ -104,9 +105,8 @@ public abstract class ProcessStatsBase extends SettingsPreferenceFragment super.onViewCreated(view, savedInstanceState); mSpinnerHeader = (ViewGroup) setPinnedHeaderView(R.layout.apps_filter_spinner); mFilterSpinner = (Spinner) mSpinnerHeader.findViewById(R.id.filter_spinner); - mFilterAdapter = new ArrayAdapter<String>(mFilterSpinner.getContext(), - R.layout.filter_spinner_item); - mFilterAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + mFilterAdapter = new SettingsSpinnerAdapter<String>(mFilterSpinner.getContext()); + for (int i = 0; i < NUM_DURATIONS; i++) { mFilterAdapter.add(getString(sDurationLabels[i])); } @@ -141,7 +141,7 @@ public abstract class ProcessStatsBase extends SettingsPreferenceFragment args.putDouble(ProcessStatsDetail.EXTRA_TOTAL_SCALE, memInfo.totalScale); new SubSettingLauncher(activity) .setDestination(ProcessStatsDetail.class.getName()) - .setTitle(R.string.memory_usage) + .setTitleRes(R.string.memory_usage) .setArguments(args) .setSourceMetricsCategory(Instrumentable.METRICS_CATEGORY_UNKNOWN) .launch(); diff --git a/src/com/android/settings/applications/ProcessStatsDetail.java b/src/com/android/settings/applications/ProcessStatsDetail.java index 75609528d8..266c195130 100644 --- a/src/com/android/settings/applications/ProcessStatsDetail.java +++ b/src/com/android/settings/applications/ProcessStatsDetail.java @@ -16,11 +16,13 @@ package com.android.settings.applications; +import static com.android.settings.widget.EntityHeaderController.ActionType; + import android.app.Activity; import android.app.ActivityManager; import android.app.ActivityManager.RunningServiceInfo; -import android.app.AlertDialog; import android.app.admin.DevicePolicyManager; +import android.app.settings.SettingsEnums; import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; @@ -33,8 +35,6 @@ import android.graphics.drawable.ColorDrawable; import android.os.Bundle; import android.os.Process; import android.os.UserHandle; -import androidx.preference.Preference; -import androidx.preference.PreferenceCategory; import android.text.format.Formatter; import android.util.ArrayMap; import android.util.IconDrawableFactory; @@ -44,7 +44,10 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.appcompat.app.AlertDialog; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; + import com.android.settings.CancellablePreference; import com.android.settings.CancellablePreference.OnCancelListener; import com.android.settings.R; @@ -59,8 +62,6 @@ import java.util.Comparator; import java.util.HashMap; import java.util.List; -import static com.android.settings.widget.EntityHeaderController.ActionType; - public class ProcessStatsDetail extends SettingsPreferenceFragment { private static final String TAG = "ProcessStatsDetail"; @@ -128,7 +129,7 @@ public class ProcessStatsDetail extends SettingsPreferenceFragment { final Activity activity = getActivity(); final Preference pref = EntityHeaderController .newInstance(activity, this, null /* appHeader */) - .setRecyclerView(getListView(), getLifecycle()) + .setRecyclerView(getListView(), getSettingsLifecycle()) .setIcon(mApp.mUiTargetApp != null ? IconDrawableFactory.newInstance(activity).getBadgedIcon(mApp.mUiTargetApp) : new ColorDrawable(0)) @@ -145,7 +146,7 @@ public class ProcessStatsDetail extends SettingsPreferenceFragment { @Override public int getMetricsCategory() { - return MetricsEvent.APPLICATIONS_PROCESS_STATS_DETAIL; + return SettingsEnums.APPLICATIONS_PROCESS_STATS_DETAIL; } @Override diff --git a/src/com/android/settings/applications/ProcessStatsPreference.java b/src/com/android/settings/applications/ProcessStatsPreference.java index 034a68dd69..4249381732 100644 --- a/src/com/android/settings/applications/ProcessStatsPreference.java +++ b/src/com/android/settings/applications/ProcessStatsPreference.java @@ -20,9 +20,9 @@ import android.content.Context; import android.content.pm.PackageManager; import android.text.TextUtils; import android.text.format.Formatter; - import android.util.Log; -import com.android.settings.widget.AppPreference; + +import com.android.settingslib.widget.apppreference.AppPreference; public class ProcessStatsPreference extends AppPreference { static final String TAG = "ProcessStatsPreference"; diff --git a/src/com/android/settings/applications/ProcessStatsSummary.java b/src/com/android/settings/applications/ProcessStatsSummary.java index 72572db83a..e5760651b6 100644 --- a/src/com/android/settings/applications/ProcessStatsSummary.java +++ b/src/com/android/settings/applications/ProcessStatsSummary.java @@ -16,14 +16,15 @@ package com.android.settings.applications; import android.app.Activity; +import android.app.settings.SettingsEnums; import android.content.Context; import android.os.Bundle; -import androidx.preference.Preference; -import androidx.preference.Preference.OnPreferenceClickListener; import android.text.format.Formatter; import android.text.format.Formatter.BytesResult; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.preference.Preference; +import androidx.preference.Preference.OnPreferenceClickListener; + import com.android.settings.R; import com.android.settings.SummaryPreference; import com.android.settings.Utils; @@ -101,7 +102,7 @@ public class ProcessStatsSummary extends ProcessStatsBase implements OnPreferenc @Override public int getMetricsCategory() { - return MetricsEvent.PROCESS_STATS_SUMMARY; + return SettingsEnums.PROCESS_STATS_SUMMARY; } @Override @@ -118,7 +119,7 @@ public class ProcessStatsSummary extends ProcessStatsBase implements OnPreferenc mStatsManager.xferStats(); new SubSettingLauncher(getContext()) .setDestination(ProcessStatsUi.class.getName()) - .setTitle(R.string.memory_usage_apps) + .setTitleRes(R.string.memory_usage_apps) .setArguments(args) .setSourceMetricsCategory(getMetricsCategory()) .launch(); diff --git a/src/com/android/settings/applications/ProcessStatsUi.java b/src/com/android/settings/applications/ProcessStatsUi.java index 42475d295f..f49d638723 100644 --- a/src/com/android/settings/applications/ProcessStatsUi.java +++ b/src/com/android/settings/applications/ProcessStatsUi.java @@ -16,19 +16,20 @@ package com.android.settings.applications; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.pm.PackageManager; import android.os.Bundle; -import androidx.preference.Preference; -import androidx.preference.PreferenceGroup; import android.util.Log; import android.util.TimeUtils; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import androidx.preference.Preference; +import androidx.preference.PreferenceGroup; + import com.android.internal.app.procstats.ProcessStats; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.applications.ProcStatsData.MemInfo; @@ -92,7 +93,7 @@ public class ProcessStatsUi extends ProcessStatsBase { @Override public int getMetricsCategory() { - return MetricsEvent.APPLICATIONS_PROCESS_STATS_UI; + return SettingsEnums.APPLICATIONS_PROCESS_STATS_UI; } @Override diff --git a/src/com/android/settings/applications/RecentAppStatsMixin.java b/src/com/android/settings/applications/RecentAppStatsMixin.java new file mode 100644 index 0000000000..4bf3864262 --- /dev/null +++ b/src/com/android/settings/applications/RecentAppStatsMixin.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2019 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.applications; + +import static com.android.settings.Utils.SETTINGS_PACKAGE_NAME; + +import android.app.Application; +import android.app.usage.UsageStats; +import android.app.usage.UsageStatsManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.PowerManager; +import android.os.UserHandle; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + +import com.android.settingslib.applications.AppUtils; +import com.android.settingslib.applications.ApplicationsState; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.utils.ThreadUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Set; + + +public class RecentAppStatsMixin implements Comparator<UsageStats>, LifecycleObserver, OnStart { + + private static final String TAG = "RecentAppStatsMixin"; + private static final Set<String> SKIP_SYSTEM_PACKAGES = new ArraySet<>(); + + @VisibleForTesting + final List<UsageStats> mRecentApps; + private final int mUserId; + private final int mMaximumApps; + private final Context mContext; + private final PackageManager mPm; + private final PowerManager mPowerManager;; + private final UsageStatsManager mUsageStatsManager; + private final ApplicationsState mApplicationsState; + private final List<RecentAppStatsListener> mAppStatsListeners; + private Calendar mCalendar; + + static { + SKIP_SYSTEM_PACKAGES.addAll(Arrays.asList( + "android", + "com.android.phone", + SETTINGS_PACKAGE_NAME, + "com.android.systemui", + "com.android.providers.calendar", + "com.android.providers.media" + )); + } + + public RecentAppStatsMixin(Context context, int maximumApps) { + mContext = context; + mMaximumApps = maximumApps; + mUserId = UserHandle.myUserId(); + mPm = mContext.getPackageManager(); + mPowerManager = mContext.getSystemService(PowerManager.class); + mUsageStatsManager = mContext.getSystemService(UsageStatsManager.class); + mApplicationsState = ApplicationsState.getInstance( + (Application) mContext.getApplicationContext()); + mRecentApps = new ArrayList<>(); + mAppStatsListeners = new ArrayList<>(); + } + + @Override + public void onStart() { + ThreadUtils.postOnBackgroundThread(() -> { + loadDisplayableRecentApps(mMaximumApps); + for (RecentAppStatsListener listener : mAppStatsListeners) { + ThreadUtils.postOnMainThread(() -> listener.onReloadDataCompleted(mRecentApps)); + } + }); + } + + @Override + public final int compare(UsageStats a, UsageStats b) { + // return by descending order + return Long.compare(b.getLastTimeUsed(), a.getLastTimeUsed()); + } + + public void addListener(@NonNull RecentAppStatsListener listener) { + mAppStatsListeners.add(listener); + } + + @VisibleForTesting + void loadDisplayableRecentApps(int number) { + mRecentApps.clear(); + mCalendar = Calendar.getInstance(); + mCalendar.add(Calendar.DAY_OF_YEAR, -1); + final List<UsageStats> mStats = mPowerManager.isPowerSaveMode() + ? new ArrayList<>() + : mUsageStatsManager.queryUsageStats( + UsageStatsManager.INTERVAL_BEST, mCalendar.getTimeInMillis(), + System.currentTimeMillis()); + + final Map<String, UsageStats> map = new ArrayMap<>(); + final int statCount = mStats.size(); + for (int i = 0; i < statCount; i++) { + final UsageStats pkgStats = mStats.get(i); + if (!shouldIncludePkgInRecents(pkgStats)) { + continue; + } + final String pkgName = pkgStats.getPackageName(); + final UsageStats existingStats = map.get(pkgName); + if (existingStats == null) { + map.put(pkgName, pkgStats); + } else { + existingStats.add(pkgStats); + } + } + final List<UsageStats> packageStats = new ArrayList<>(); + packageStats.addAll(map.values()); + Collections.sort(packageStats, this /* comparator */); + int count = 0; + for (UsageStats stat : packageStats) { + final ApplicationsState.AppEntry appEntry = mApplicationsState.getEntry( + stat.getPackageName(), mUserId); + if (appEntry == null) { + continue; + } + mRecentApps.add(stat); + count++; + if (count >= number) { + break; + } + } + } + + /** + * Whether or not the app should be included in recent list. + */ + private boolean shouldIncludePkgInRecents(UsageStats stat) { + final String pkgName = stat.getPackageName(); + if (stat.getLastTimeUsed() < mCalendar.getTimeInMillis()) { + Log.d(TAG, "Invalid timestamp (usage time is more than 24 hours ago), skipping " + + pkgName); + return false; + } + + if (SKIP_SYSTEM_PACKAGES.contains(pkgName)) { + Log.d(TAG, "System package, skipping " + pkgName); + return false; + } + if (AppUtils.isHiddenSystemModule(mContext, pkgName)) { + return false; + } + final Intent launchIntent = new Intent().addCategory(Intent.CATEGORY_LAUNCHER) + .setPackage(pkgName); + + if (mPm.resolveActivity(launchIntent, 0) == null) { + // Not visible on launcher -> likely not a user visible app, skip if non-instant. + final ApplicationsState.AppEntry appEntry = + mApplicationsState.getEntry(pkgName, mUserId); + if (appEntry == null || appEntry.info == null || !AppUtils.isInstant(appEntry.info)) { + Log.d(TAG, "Not a user visible or instant app, skipping " + pkgName); + return false; + } + } + return true; + } + + public interface RecentAppStatsListener { + + void onReloadDataCompleted(List<UsageStats> recentApps); + } +} diff --git a/src/com/android/settings/applications/RecentAppsPreferenceController.java b/src/com/android/settings/applications/RecentAppsPreferenceController.java index 92d8af1f09..4f5ec01fc7 100644 --- a/src/com/android/settings/applications/RecentAppsPreferenceController.java +++ b/src/com/android/settings/applications/RecentAppsPreferenceController.java @@ -16,311 +16,156 @@ package com.android.settings.applications; -import static com.android.internal.logging.nano.MetricsProto.MetricsEvent - .SETTINGS_APP_NOTIF_CATEGORY; - import android.app.Application; -import android.app.Fragment; +import android.app.settings.SettingsEnums; import android.app.usage.UsageStats; -import android.app.usage.UsageStatsManager; import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; +import android.icu.text.RelativeDateTimeFormatter; import android.os.UserHandle; +import android.util.IconDrawableFactory; +import android.util.Log; +import android.view.View; + +import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; +import androidx.fragment.app.Fragment; import androidx.preference.Preference; -import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceScreen; -import android.text.TextUtils; -import android.util.ArrayMap; -import android.util.ArraySet; -import android.util.IconDrawableFactory; -import android.util.Log; import com.android.settings.R; import com.android.settings.applications.appinfo.AppInfoDashboardFragment; -import com.android.settings.core.PreferenceControllerMixin; -import com.android.settings.widget.AppPreference; -import com.android.settingslib.applications.AppUtils; +import com.android.settings.applications.manageapplications.ManageApplications; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.core.SubSettingLauncher; import com.android.settingslib.applications.ApplicationsState; -import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.utils.StringUtil; -import com.android.settingslib.wrapper.PackageManagerWrapper; +import com.android.settingslib.widget.AppEntitiesHeaderController; +import com.android.settingslib.widget.AppEntityInfo; +import com.android.settingslib.widget.LayoutPreference; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Calendar; -import java.util.Collections; -import java.util.Comparator; import java.util.List; -import java.util.Map; -import java.util.Set; /** - * This controller displays a list of recently used apps and a "See all" button. If there is - * no recently used app, "See all" will be displayed as "App info". + * This controller displays up to three recently used apps. + * If there is no recently used app, we only show up an "App Info" preference. */ -public class RecentAppsPreferenceController extends AbstractPreferenceController - implements PreferenceControllerMixin, Comparator<UsageStats> { +public class RecentAppsPreferenceController extends BasePreferenceController + implements RecentAppStatsMixin.RecentAppStatsListener { - private static final String TAG = "RecentAppsCtrl"; - private static final String KEY_PREF_CATEGORY = "recent_apps_category"; @VisibleForTesting - static final String KEY_DIVIDER = "all_app_info_divider"; + static final String KEY_DIVIDER = "recent_apps_divider"; + @VisibleForTesting - static final String KEY_SEE_ALL = "all_app_info"; - private static final int SHOW_RECENT_APP_COUNT = 5; - private static final Set<String> SKIP_SYSTEM_PACKAGES = new ArraySet<>(); + AppEntitiesHeaderController mAppEntitiesController; + @VisibleForTesting + LayoutPreference mRecentAppsPreference; + @VisibleForTesting + Preference mDivider; - private final Fragment mHost; - private final PackageManager mPm; - private final UsageStatsManager mUsageStatsManager; private final ApplicationsState mApplicationsState; private final int mUserId; private final IconDrawableFactory mIconDrawableFactory; - private Calendar mCal; - private List<UsageStats> mStats; - - private PreferenceCategory mCategory; - private Preference mSeeAllPref; - private Preference mDivider; - private boolean mHasRecentApps; + private Fragment mHost; + private List<UsageStats> mRecentApps; - static { - SKIP_SYSTEM_PACKAGES.addAll(Arrays.asList( - "android", - "com.android.phone", - "com.android.settings", - "com.android.systemui", - "com.android.providers.calendar", - "com.android.providers.media" - )); - } - - public RecentAppsPreferenceController(Context context, Application app, Fragment host) { - this(context, app == null ? null : ApplicationsState.getInstance(app), host); - } - - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - RecentAppsPreferenceController(Context context, ApplicationsState appState, Fragment host) { - super(context); - mIconDrawableFactory = IconDrawableFactory.newInstance(context); + public RecentAppsPreferenceController(Context context, String key) { + super(context, key); + mApplicationsState = ApplicationsState.getInstance( + (Application) mContext.getApplicationContext()); mUserId = UserHandle.myUserId(); - mPm = context.getPackageManager(); - mHost = host; - mUsageStatsManager = - (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE); - mApplicationsState = appState; - } - - @Override - public boolean isAvailable() { - return true; + mIconDrawableFactory = IconDrawableFactory.newInstance(mContext); } - @Override - public String getPreferenceKey() { - return KEY_PREF_CATEGORY; + public void setFragment(Fragment fragment) { + mHost = fragment; } @Override - public void updateNonIndexableKeys(List<String> keys) { - PreferenceControllerMixin.super.updateNonIndexableKeys(keys); - // Don't index category name into search. It's not actionable. - keys.add(KEY_PREF_CATEGORY); - keys.add(KEY_DIVIDER); + public int getAvailabilityStatus() { + return AVAILABLE_UNSEARCHABLE; } @Override public void displayPreference(PreferenceScreen screen) { - mCategory = (PreferenceCategory) screen.findPreference(getPreferenceKey()); - mSeeAllPref = screen.findPreference(KEY_SEE_ALL); - mDivider = screen.findPreference(KEY_DIVIDER); super.displayPreference(screen); - refreshUi(mCategory.getContext()); + + mDivider = screen.findPreference(KEY_DIVIDER); + mRecentAppsPreference = screen.findPreference(getPreferenceKey()); + final View view = mRecentAppsPreference.findViewById(R.id.app_entities_header); + mAppEntitiesController = AppEntitiesHeaderController.newInstance(mContext, view) + .setHeaderTitleRes(R.string.recent_app_category_title) + .setHeaderDetailsClickListener((View v) -> { + new SubSettingLauncher(mContext) + .setDestination(ManageApplications.class.getName()) + .setArguments(null /* arguments */) + .setTitleRes(R.string.application_info_label) + .setSourceMetricsCategory(SettingsEnums.SETTINGS_APP_NOTIF_CATEGORY) + .launch(); + }); } @Override - public void updateState(Preference preference) { - super.updateState(preference); - refreshUi(mCategory.getContext()); + public void onReloadDataCompleted(@NonNull List<UsageStats> recentApps) { + mRecentApps = recentApps; + refreshUi(); // Show total number of installed apps as See all's summary. new InstalledAppCounter(mContext, InstalledAppCounter.IGNORE_INSTALL_REASON, - new PackageManagerWrapper(mContext.getPackageManager())) { + mContext.getPackageManager()) { @Override protected void onCountComplete(int num) { - if (mHasRecentApps) { - mSeeAllPref.setTitle(mContext.getString(R.string.see_all_apps_title, num)); - } else { - mSeeAllPref.setSummary(mContext.getString(R.string.apps_summary, num)); - } + mAppEntitiesController.setHeaderDetails( + mContext.getString(R.string.see_all_apps_title, num)); + mAppEntitiesController.apply(); } }.execute(); - - } - - @Override - public final int compare(UsageStats a, UsageStats b) { - // return by descending order - return Long.compare(b.getLastTimeUsed(), a.getLastTimeUsed()); } - @VisibleForTesting - void refreshUi(Context prefContext) { - reloadData(); - final List<UsageStats> recentApps = getDisplayableRecentAppList(); - if (recentApps != null && !recentApps.isEmpty()) { - mHasRecentApps = true; - displayRecentApps(prefContext, recentApps); + private void refreshUi() { + if (!mRecentApps.isEmpty()) { + displayRecentApps(); + mRecentAppsPreference.setVisible(true); + mDivider.setVisible(true); } else { - mHasRecentApps = false; - displayOnlyAppInfo(); + mDivider.setVisible(false); + mRecentAppsPreference.setVisible(false); } } - @VisibleForTesting - void reloadData() { - mCal = Calendar.getInstance(); - mCal.add(Calendar.DAY_OF_YEAR, -1); - mStats = mUsageStatsManager.queryUsageStats( - UsageStatsManager.INTERVAL_BEST, mCal.getTimeInMillis(), - System.currentTimeMillis()); - } + private void displayRecentApps() { + int showAppsCount = 0; - private void displayOnlyAppInfo() { - mCategory.setTitle(null); - mDivider.setVisible(false); - mSeeAllPref.setTitle(R.string.applications_settings); - mSeeAllPref.setIcon(null); - int prefCount = mCategory.getPreferenceCount(); - for (int i = prefCount - 1; i >= 0; i--) { - final Preference pref = mCategory.getPreference(i); - if (!TextUtils.equals(pref.getKey(), KEY_SEE_ALL)) { - mCategory.removePreference(pref); + for (UsageStats stat : mRecentApps) { + final AppEntityInfo appEntityInfoInfo = createAppEntity(stat); + if (appEntityInfoInfo != null) { + mAppEntitiesController.setAppEntity(showAppsCount++, appEntityInfoInfo); } - } - } - - private void displayRecentApps(Context prefContext, List<UsageStats> recentApps) { - mCategory.setTitle(R.string.recent_app_category_title); - mDivider.setVisible(true); - mSeeAllPref.setSummary(null); - mSeeAllPref.setIcon(R.drawable.ic_chevron_right_24dp); - // Rebind prefs/avoid adding new prefs if possible. Adding/removing prefs causes jank. - // Build a cached preference pool - final Map<String, Preference> appPreferences = new ArrayMap<>(); - int prefCount = mCategory.getPreferenceCount(); - for (int i = 0; i < prefCount; i++) { - final Preference pref = mCategory.getPreference(i); - final String key = pref.getKey(); - if (!TextUtils.equals(key, KEY_SEE_ALL)) { - appPreferences.put(key, pref); - } - } - final int recentAppsCount = recentApps.size(); - for (int i = 0; i < recentAppsCount; i++) { - final UsageStats stat = recentApps.get(i); - // Bind recent apps to existing prefs if possible, or create a new pref. - final String pkgName = stat.getPackageName(); - final ApplicationsState.AppEntry appEntry = - mApplicationsState.getEntry(pkgName, mUserId); - if (appEntry == null) { - continue; - } - - boolean rebindPref = true; - Preference pref = appPreferences.remove(pkgName); - if (pref == null) { - pref = new AppPreference(prefContext); - rebindPref = false; - } - pref.setKey(pkgName); - pref.setTitle(appEntry.label); - pref.setIcon(mIconDrawableFactory.getBadgedIcon(appEntry.info)); - pref.setSummary(StringUtil.formatRelativeTime(mContext, - System.currentTimeMillis() - stat.getLastTimeUsed(), false)); - pref.setOrder(i); - pref.setOnPreferenceClickListener(preference -> { - AppInfoBase.startAppInfoFragment(AppInfoDashboardFragment.class, - R.string.application_info_label, pkgName, appEntry.info.uid, mHost, - 1001 /*RequestCode*/, SETTINGS_APP_NOTIF_CATEGORY); - return true; - }); - if (!rebindPref) { - mCategory.addPreference(pref); - } - } - // Remove unused prefs from pref cache pool - for (Preference unusedPrefs : appPreferences.values()) { - mCategory.removePreference(unusedPrefs); - } - } - - private List<UsageStats> getDisplayableRecentAppList() { - final List<UsageStats> recentApps = new ArrayList<>(); - final Map<String, UsageStats> map = new ArrayMap<>(); - final int statCount = mStats.size(); - for (int i = 0; i < statCount; i++) { - final UsageStats pkgStats = mStats.get(i); - if (!shouldIncludePkgInRecents(pkgStats)) { - continue; - } - final String pkgName = pkgStats.getPackageName(); - final UsageStats existingStats = map.get(pkgName); - if (existingStats == null) { - map.put(pkgName, pkgStats); - } else { - existingStats.add(pkgStats); - } - } - final List<UsageStats> packageStats = new ArrayList<>(); - packageStats.addAll(map.values()); - Collections.sort(packageStats, this /* comparator */); - int count = 0; - for (UsageStats stat : packageStats) { - final ApplicationsState.AppEntry appEntry = mApplicationsState.getEntry( - stat.getPackageName(), mUserId); - if (appEntry == null) { - continue; - } - recentApps.add(stat); - count++; - if (count >= SHOW_RECENT_APP_COUNT) { + if (showAppsCount == AppEntitiesHeaderController.MAXIMUM_APPS) { break; } } - return recentApps; } - - /** - * Whether or not the app should be included in recent list. - */ - private boolean shouldIncludePkgInRecents(UsageStats stat) { + private AppEntityInfo createAppEntity(UsageStats stat) { final String pkgName = stat.getPackageName(); - if (stat.getLastTimeUsed() < mCal.getTimeInMillis()) { - Log.d(TAG, "Invalid timestamp, skipping " + pkgName); - return false; - } - - if (SKIP_SYSTEM_PACKAGES.contains(pkgName)) { - Log.d(TAG, "System package, skipping " + pkgName); - return false; + final ApplicationsState.AppEntry appEntry = + mApplicationsState.getEntry(pkgName, mUserId); + if (appEntry == null) { + return null; } - final Intent launchIntent = new Intent().addCategory(Intent.CATEGORY_LAUNCHER) - .setPackage(pkgName); - if (mPm.resolveActivity(launchIntent, 0) == null) { - // Not visible on launcher -> likely not a user visible app, skip if non-instant. - final ApplicationsState.AppEntry appEntry = - mApplicationsState.getEntry(pkgName, mUserId); - if (appEntry == null || appEntry.info == null || !AppUtils.isInstant(appEntry.info)) { - Log.d(TAG, "Not a user visible or instant app, skipping " + pkgName); - return false; - } - } - return true; + return new AppEntityInfo.Builder() + .setIcon(mIconDrawableFactory.getBadgedIcon(appEntry.info)) + .setTitle(appEntry.label) + .setSummary(StringUtil.formatRelativeTime(mContext, + System.currentTimeMillis() - stat.getLastTimeUsed(), false, + RelativeDateTimeFormatter.Style.SHORT)) + .setOnClickListener(v -> + AppInfoBase.startAppInfoFragment(AppInfoDashboardFragment.class, + R.string.application_info_label, pkgName, appEntry.info.uid, + mHost, 1001 /*RequestCode*/, + SettingsEnums.SETTINGS_APP_NOTIF_CATEGORY)) + .build(); } } diff --git a/src/com/android/settings/applications/RunningProcessesView.java b/src/com/android/settings/applications/RunningProcessesView.java index d714c5f8fc..5491ad14c6 100644 --- a/src/com/android/settings/applications/RunningProcessesView.java +++ b/src/com/android/settings/applications/RunningProcessesView.java @@ -416,7 +416,7 @@ public class RunningProcessesView extends FrameLayout new SubSettingLauncher(getContext()) .setDestination(RunningServiceDetails.class.getName()) .setArguments(args) - .setTitle(R.string.runningservicedetails_settings_title) + .setTitleRes(R.string.runningservicedetails_settings_title) .setSourceMetricsCategory(mOwner.getMetricsCategory()) .launch(); } @@ -452,8 +452,7 @@ public class RunningProcessesView extends FrameLayout final Context context = getContext(); mColorBar.setProgressTintList( ColorStateList.valueOf(context.getColor(R.color.running_processes_system_ram))); - mColorBar.setSecondaryProgressTintList( - ColorStateList.valueOf(Utils.getColorAccent(context))); + mColorBar.setSecondaryProgressTintList(Utils.getColorAccent(context)); mColorBar.setSecondaryProgressTintMode(PorterDuff.Mode.SRC); mColorBar.setProgressBackgroundTintList( ColorStateList.valueOf(context.getColor(R.color.running_processes_free_ram))); diff --git a/src/com/android/settings/applications/RunningServiceDetails.java b/src/com/android/settings/applications/RunningServiceDetails.java index 770b1d6e4a..6165fb43b8 100644 --- a/src/com/android/settings/applications/RunningServiceDetails.java +++ b/src/com/android/settings/applications/RunningServiceDetails.java @@ -2,11 +2,10 @@ package com.android.settings.applications; import android.app.Activity; import android.app.ActivityManager; -import android.app.AlertDialog; import android.app.ApplicationErrorReport; import android.app.Dialog; -import android.app.DialogFragment; import android.app.PendingIntent; +import android.app.settings.SettingsEnums; import android.content.ActivityNotFoundException; import android.content.ComponentName; import android.content.Context; @@ -31,7 +30,9 @@ import android.view.ViewGroup; import android.widget.Button; import android.widget.TextView; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; + import com.android.settings.R; import com.android.settings.Utils; import com.android.settings.core.InstrumentedFragment; @@ -534,7 +535,7 @@ public class RunningServiceDetails extends InstrumentedFragment @Override public int getMetricsCategory() { - return MetricsEvent.RUNNING_SERVICE_DETAILS; + return SettingsEnums.RUNNING_SERVICE_DETAILS; } @Override @@ -607,7 +608,7 @@ public class RunningServiceDetails extends InstrumentedFragment @Override public int getMetricsCategory() { - return MetricsEvent.DIALOG_RUNNIGN_SERVICE; + return SettingsEnums.DIALOG_RUNNIGN_SERVICE; } } diff --git a/src/com/android/settings/applications/RunningServices.java b/src/com/android/settings/applications/RunningServices.java index bf48492102..4d13241126 100644 --- a/src/com/android/settings/applications/RunningServices.java +++ b/src/com/android/settings/applications/RunningServices.java @@ -15,6 +15,7 @@ */ package com.android.settings.applications; +import android.app.settings.SettingsEnums; import android.os.Bundle; import android.view.LayoutInflater; import android.view.Menu; @@ -23,7 +24,6 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; import com.android.settings.widget.LoadingViewController; @@ -110,7 +110,7 @@ public class RunningServices extends SettingsPreferenceFragment { @Override public int getMetricsCategory() { - return MetricsEvent.RUNNING_SERVICES; + return SettingsEnums.RUNNING_SERVICES; } private final Runnable mRunningProcessesAvail = new Runnable() { diff --git a/src/com/android/settings/applications/SpacePreference.java b/src/com/android/settings/applications/SpacePreference.java index b532896f10..6575ad1dfa 100644 --- a/src/com/android/settings/applications/SpacePreference.java +++ b/src/com/android/settings/applications/SpacePreference.java @@ -17,11 +17,13 @@ package com.android.settings.applications; import android.content.Context; import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.ViewGroup.LayoutParams; + import androidx.core.content.res.TypedArrayUtils; import androidx.preference.Preference; import androidx.preference.PreferenceViewHolder; -import android.util.AttributeSet; -import android.view.ViewGroup.LayoutParams; + import com.android.settings.R; /** diff --git a/src/com/android/settings/applications/SpecialAccessSettings.java b/src/com/android/settings/applications/SpecialAccessSettings.java deleted file mode 100644 index 16fb4057e1..0000000000 --- a/src/com/android/settings/applications/SpecialAccessSettings.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file - * except in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the - * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package com.android.settings.applications; - -import android.app.ActivityManager; -import android.content.Context; -import android.os.Bundle; -import androidx.annotation.NonNull; -import android.provider.SearchIndexableResource; -import androidx.preference.Preference; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.settings.R; -import com.android.settings.dashboard.DashboardFragment; -import com.android.settings.search.BaseSearchIndexProvider; -import com.android.settings.search.Indexable; -import com.android.settingslib.core.AbstractPreferenceController; -import java.util.ArrayList; -import java.util.List; - -public class SpecialAccessSettings extends DashboardFragment { - - private static final String TAG = "SpecialAccessSettings"; - private static final String[] DISABLED_FEATURES_LOW_RAM = - new String[]{"notification_access", "zen_access", "enabled_vr_listeners", - "picture_in_picture"}; - - @Override - protected String getLogTag() { - return TAG; - } - - @Override - protected int getPreferenceScreenResId() { - return R.xml.special_access; - } - - @Override - public void onCreate(Bundle icicle) { - super.onCreate(icicle); - - if (ActivityManager.isLowRamDeviceStatic()) { - for (String disabledFeature : DISABLED_FEATURES_LOW_RAM) { - Preference pref = findPreference(disabledFeature); - if (pref != null) { - removePreference(disabledFeature); - } - } - } - } - - @Override - protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { - return buildPreferenceControllers(context); - } - - private static List<AbstractPreferenceController> buildPreferenceControllers( - @NonNull Context context) { - final List<AbstractPreferenceController> controllers = new ArrayList<>(); - controllers.add(new HighPowerAppsController(context)); - controllers.add(new DeviceAdministratorsController(context)); - controllers.add(new PremiumSmsController(context)); - controllers.add(new DataSaverController(context)); - controllers.add(new EnabledVrListenersController(context)); - return controllers; - } - - @Override - public int getMetricsCategory() { - return MetricsEvent.SPECIAL_ACCESS; - } - - public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = - new BaseSearchIndexProvider() { - @Override - public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, - boolean enabled) { - final ArrayList<SearchIndexableResource> result = new ArrayList<>(); - - final SearchIndexableResource sir = new SearchIndexableResource(context); - sir.xmlResId = R.xml.special_access; - result.add(sir); - return result; - } - - @Override - public List<AbstractPreferenceController> createPreferenceControllers( - Context context) { - return buildPreferenceControllers(context); - } - }; -} diff --git a/src/com/android/settings/applications/SpecialAppAccessPreferenceController.java b/src/com/android/settings/applications/SpecialAppAccessPreferenceController.java index ef2b2118d3..1763d84491 100644 --- a/src/com/android/settings/applications/SpecialAppAccessPreferenceController.java +++ b/src/com/android/settings/applications/SpecialAppAccessPreferenceController.java @@ -13,41 +13,141 @@ */ package com.android.settings.applications; +import android.app.Application; import android.content.Context; + +import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + import com.android.settings.R; -import com.android.settings.core.PreferenceControllerMixin; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.datausage.AppStateDataUsageBridge; +import com.android.settings.datausage.AppStateDataUsageBridge.DataUsageState; import com.android.settings.datausage.DataSaverBackend; -import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.applications.ApplicationsState; +import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnDestroy; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.core.lifecycle.events.OnStop; + +import java.util.ArrayList; + +public class SpecialAppAccessPreferenceController extends BasePreferenceController implements + AppStateBaseBridge.Callback, ApplicationsState.Callbacks, LifecycleObserver, OnStart, + OnStop, OnDestroy { + + @VisibleForTesting + ApplicationsState.Session mSession; + + private final ApplicationsState mApplicationsState; + private final AppStateDataUsageBridge mDataUsageBridge; + private final DataSaverBackend mDataSaverBackend; -public class SpecialAppAccessPreferenceController extends AbstractPreferenceController - implements PreferenceControllerMixin { + private Preference mPreference; + private boolean mExtraLoaded; - private static final String KEY_SPECIAL_ACCESS = "special_access"; - private DataSaverBackend mDataSaverBackend; + public SpecialAppAccessPreferenceController(Context context, String key) { + super(context, key); + mApplicationsState = ApplicationsState.getInstance( + (Application) context.getApplicationContext()); + mDataSaverBackend = new DataSaverBackend(context); + mDataUsageBridge = new AppStateDataUsageBridge(mApplicationsState, this, mDataSaverBackend); + } + + public void setSession(Lifecycle lifecycle) { + mSession = mApplicationsState.newSession(this, lifecycle); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE_UNSEARCHABLE; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + } - public SpecialAppAccessPreferenceController(Context context) { - super(context); + @Override + public void onStart() { + mDataUsageBridge.resume(); } @Override - public boolean isAvailable() { - return true; + public void onStop() { + mDataUsageBridge.pause(); } @Override - public String getPreferenceKey() { - return KEY_SPECIAL_ACCESS; + public void onDestroy() { + mDataUsageBridge.release(); } @Override public void updateState(Preference preference) { - if (mDataSaverBackend == null) { - mDataSaverBackend = new DataSaverBackend(mContext); + updateSummary(); + } + + @Override + public void onExtraInfoUpdated() { + mExtraLoaded = true; + updateSummary(); + } + + private void updateSummary() { + if (!mExtraLoaded || mPreference == null) { + return; } - final int count = mDataSaverBackend.getWhitelistedCount(); - preference.setSummary(mContext.getResources().getQuantityString( - R.plurals.special_access_summary, count, count)); + + final ArrayList<ApplicationsState.AppEntry> allApps = mSession.getAllApps(); + int count = 0; + for (ApplicationsState.AppEntry entry : allApps) { + if (!ApplicationsState.FILTER_DOWNLOADED_AND_LAUNCHER.filterApp(entry)) { + continue; + } + if (entry.extraInfo instanceof DataUsageState + && ((DataUsageState) entry.extraInfo).isDataSaverWhitelisted) { + count++; + } + } + mPreference.setSummary(mContext.getResources().getQuantityString( + R.plurals.special_access_summary, count, count)); + } + + @Override + public void onRunningStateChanged(boolean running) { + } + + @Override + public void onPackageListChanged() { + } + + @Override + public void onRebuildComplete(ArrayList<ApplicationsState.AppEntry> apps) { + } + + @Override + public void onPackageIconChanged() { + } + + @Override + public void onPackageSizeChanged(String packageName) { + } + + @Override + public void onAllSizesComputed() { + } + + @Override + public void onLauncherInfoChanged() { + } + + @Override + public void onLoadEntriesCompleted() { } + } diff --git a/src/com/android/settings/applications/UsageAccessDetails.java b/src/com/android/settings/applications/UsageAccessDetails.java index 1681b1d316..58dff7c2e0 100644 --- a/src/com/android/settings/applications/UsageAccessDetails.java +++ b/src/com/android/settings/applications/UsageAccessDetails.java @@ -15,26 +15,24 @@ */ package com.android.settings.applications; -import android.app.AlertDialog; import android.app.AppOpsManager; import android.app.admin.DevicePolicyManager; -import android.content.ActivityNotFoundException; +import android.app.settings.SettingsEnums; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.os.Bundle; -import android.os.UserHandle; import android.provider.Settings; -import androidx.preference.SwitchPreference; + +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; import androidx.preference.Preference; import androidx.preference.Preference.OnPreferenceChangeListener; import androidx.preference.Preference.OnPreferenceClickListener; -import android.util.Log; +import androidx.preference.SwitchPreference; -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.applications.AppStateUsageBridge.UsageState; import com.android.settings.overlay.FeatureFactory; @@ -114,10 +112,16 @@ public class UsageAccessDetails extends AppInfoWithHeader implements OnPreferenc @VisibleForTesting void logSpecialPermissionChange(boolean newState, String packageName) { - int logCategory = newState ? MetricsEvent.APP_SPECIAL_PERMISSION_USAGE_VIEW_ALLOW - : MetricsEvent.APP_SPECIAL_PERMISSION_USAGE_VIEW_DENY; - FeatureFactory.getFactory(getContext()).getMetricsFeatureProvider().action(getContext(), - logCategory, packageName); + int logCategory = newState ? SettingsEnums.APP_SPECIAL_PERMISSION_USAGE_VIEW_ALLOW + : SettingsEnums.APP_SPECIAL_PERMISSION_USAGE_VIEW_DENY; + final MetricsFeatureProvider metricsFeatureProvider = + FeatureFactory.getFactory(getContext()).getMetricsFeatureProvider(); + metricsFeatureProvider.action( + metricsFeatureProvider.getAttribution(getActivity()), + logCategory, + getMetricsCategory(), + packageName, + 0); } @Override @@ -159,7 +163,7 @@ public class UsageAccessDetails extends AppInfoWithHeader implements OnPreferenc @Override public int getMetricsCategory() { - return MetricsEvent.APPLICATIONS_USAGE_ACCESS_DETAIL; + return SettingsEnums.APPLICATIONS_USAGE_ACCESS_DETAIL; } } diff --git a/src/com/android/settings/applications/appinfo/AppActionButtonPreferenceController.java b/src/com/android/settings/applications/appinfo/AppActionButtonPreferenceController.java deleted file mode 100644 index 79c22248a6..0000000000 --- a/src/com/android/settings/applications/appinfo/AppActionButtonPreferenceController.java +++ /dev/null @@ -1,321 +0,0 @@ -/* - * Copyright (C) 2017 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.applications.appinfo; - -import android.app.Activity; -import android.app.ActivityManager; -import android.app.admin.DevicePolicyManager; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.net.Uri; -import android.os.Bundle; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.os.UserHandle; -import android.os.UserManager; -import androidx.annotation.VisibleForTesting; -import androidx.preference.PreferenceScreen; -import android.util.Log; -import android.webkit.IWebViewUpdateService; - -import com.android.settings.R; -import com.android.settings.Utils; -import com.android.settings.applications.ApplicationFeatureProvider; -import com.android.settings.core.BasePreferenceController; -import com.android.settings.overlay.FeatureFactory; -import com.android.settings.widget.ActionButtonPreference; -import com.android.settingslib.RestrictedLockUtils; -import com.android.settingslib.applications.AppUtils; -import com.android.settingslib.applications.ApplicationsState.AppEntry; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; - -public class AppActionButtonPreferenceController extends BasePreferenceController - implements AppInfoDashboardFragment.Callback { - - private static final String TAG = "AppActionButtonControl"; - private static final String KEY_ACTION_BUTTONS = "action_buttons"; - - @VisibleForTesting - ActionButtonPreference mActionButtons; - private final AppInfoDashboardFragment mParent; - private final String mPackageName; - private final HashSet<String> mHomePackages = new HashSet<>(); - private final ApplicationFeatureProvider mApplicationFeatureProvider; - - private int mUserId; - private DevicePolicyManager mDpm; - private UserManager mUserManager; - private PackageManager mPm; - - private final BroadcastReceiver mCheckKillProcessesReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - final boolean enabled = getResultCode() != Activity.RESULT_CANCELED; - Log.d(TAG, "Got broadcast response: Restart status for " - + mParent.getAppEntry().info.packageName + " " + enabled); - updateForceStopButton(enabled); - } - }; - - public AppActionButtonPreferenceController(Context context, AppInfoDashboardFragment parent, - String packageName) { - super(context, KEY_ACTION_BUTTONS); - mParent = parent; - mPackageName = packageName; - mUserId = UserHandle.myUserId(); - mApplicationFeatureProvider = FeatureFactory.getFactory(context) - .getApplicationFeatureProvider(context); - } - - @Override - public int getAvailabilityStatus() { - return AppUtils.isInstant(mParent.getPackageInfo().applicationInfo) - ? DISABLED_FOR_USER : AVAILABLE; - } - - @Override - public void displayPreference(PreferenceScreen screen) { - super.displayPreference(screen); - mActionButtons = ((ActionButtonPreference) screen.findPreference(KEY_ACTION_BUTTONS)) - .setButton2Text(R.string.force_stop) - .setButton2Positive(false) - .setButton2Enabled(false); - } - - @Override - public void refreshUi() { - if (mPm == null) { - mPm = mContext.getPackageManager(); - } - if (mDpm == null) { - mDpm = (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE); - } - if (mUserManager == null) { - mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); - } - final AppEntry appEntry = mParent.getAppEntry(); - final PackageInfo packageInfo = mParent.getPackageInfo(); - - // Get list of "home" apps and trace through any meta-data references - final List<ResolveInfo> homeActivities = new ArrayList<ResolveInfo>(); - mPm.getHomeActivities(homeActivities); - mHomePackages.clear(); - for (int i = 0; i < homeActivities.size(); i++) { - final ResolveInfo ri = homeActivities.get(i); - final String activityPkg = ri.activityInfo.packageName; - mHomePackages.add(activityPkg); - - // Also make sure to include anything proxying for the home app - final Bundle metadata = ri.activityInfo.metaData; - if (metadata != null) { - final String metaPkg = metadata.getString(ActivityManager.META_HOME_ALTERNATE); - if (signaturesMatch(metaPkg, activityPkg)) { - mHomePackages.add(metaPkg); - } - } - } - - checkForceStop(appEntry, packageInfo); - initUninstallButtons(appEntry, packageInfo); - } - - @VisibleForTesting - void initUninstallButtons(AppEntry appEntry, PackageInfo packageInfo) { - final boolean isBundled = (appEntry.info.flags & ApplicationInfo.FLAG_SYSTEM) != 0; - boolean enabled; - if (isBundled) { - enabled = handleDisableable(appEntry, packageInfo); - } else { - enabled = initUninstallButtonForUserApp(); - } - // If this is a device admin, it can't be uninstalled or disabled. - // We do this here so the text of the button is still set correctly. - if (isBundled && mDpm.packageHasActiveAdmins(packageInfo.packageName)) { - enabled = false; - } - - // We don't allow uninstalling DO/PO on *any* users, because if it's a system app, - // "uninstall" is actually "downgrade to the system version + disable", and "downgrade" - // will clear data on all users. - if (Utils.isProfileOrDeviceOwner(mUserManager, mDpm, packageInfo.packageName)) { - enabled = false; - } - - // Don't allow uninstalling the device provisioning package. - if (Utils.isDeviceProvisioningPackage(mContext.getResources(), appEntry.info.packageName)) { - enabled = false; - } - - // If the uninstall intent is already queued, disable the uninstall button - if (mDpm.isUninstallInQueue(mPackageName)) { - enabled = false; - } - - // Home apps need special handling. Bundled ones we don't risk downgrading - // because that can interfere with home-key resolution. Furthermore, we - // can't allow uninstallation of the only home app, and we don't want to - // allow uninstallation of an explicitly preferred one -- the user can go - // to Home settings and pick a different one, after which we'll permit - // uninstallation of the now-not-default one. - if (enabled && mHomePackages.contains(packageInfo.packageName)) { - if (isBundled) { - enabled = false; - } else { - ArrayList<ResolveInfo> homeActivities = new ArrayList<ResolveInfo>(); - ComponentName currentDefaultHome = mPm.getHomeActivities(homeActivities); - if (currentDefaultHome == null) { - // No preferred default, so permit uninstall only when - // there is more than one candidate - enabled = (mHomePackages.size() > 1); - } else { - // There is an explicit default home app -- forbid uninstall of - // that one, but permit it for installed-but-inactive ones. - enabled = !packageInfo.packageName.equals(currentDefaultHome.getPackageName()); - } - } - } - - if (RestrictedLockUtils.hasBaseUserRestriction( - mContext, UserManager.DISALLOW_APPS_CONTROL, mUserId)) { - enabled = false; - } - - try { - final IWebViewUpdateService webviewUpdateService = - IWebViewUpdateService.Stub.asInterface( - ServiceManager.getService("webviewupdate")); - if (webviewUpdateService.isFallbackPackage(appEntry.info.packageName)) { - enabled = false; - } - } catch (RemoteException e) { - throw new RuntimeException(e); - } - - mActionButtons.setButton1Enabled(enabled); - if (enabled) { - // Register listener - mActionButtons.setButton1OnClickListener(v -> mParent.handleUninstallButtonClick()); - } - } - - @VisibleForTesting - boolean initUninstallButtonForUserApp() { - boolean enabled = true; - final PackageInfo packageInfo = mParent.getPackageInfo(); - if ((packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_INSTALLED) == 0 - && mUserManager.getUsers().size() >= 2) { - // When we have multiple users, there is a separate menu - // to uninstall for all users. - enabled = false; - } else if (AppUtils.isInstant(packageInfo.applicationInfo)) { - enabled = false; - mActionButtons.setButton1Visible(false); - } - mActionButtons.setButton1Text(R.string.uninstall_text).setButton1Positive(false); - return enabled; - } - - @VisibleForTesting - boolean handleDisableable(AppEntry appEntry, PackageInfo packageInfo) { - boolean disableable = false; - // Try to prevent the user from bricking their phone - // by not allowing disabling of apps signed with the - // system cert and any launcher app in the system. - if (mHomePackages.contains(appEntry.info.packageName) - || Utils.isSystemPackage(mContext.getResources(), mPm, packageInfo)) { - // Disable button for core system applications. - mActionButtons - .setButton1Text(R.string.disable_text) - .setButton1Positive(false); - } else if (appEntry.info.enabled && appEntry.info.enabledSetting - != PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED) { - mActionButtons - .setButton1Text(R.string.disable_text) - .setButton1Positive(false); - disableable = !mApplicationFeatureProvider.getKeepEnabledPackages() - .contains(appEntry.info.packageName); - } else { - mActionButtons - .setButton1Text(R.string.enable_text) - .setButton1Positive(true); - disableable = true; - } - - return disableable; - } - - private void updateForceStopButton(boolean enabled) { - final boolean disallowedBySystem = RestrictedLockUtils.hasBaseUserRestriction( - mContext, UserManager.DISALLOW_APPS_CONTROL, mUserId); - mActionButtons - .setButton2Enabled(disallowedBySystem ? false : enabled) - .setButton2OnClickListener( - disallowedBySystem ? null : v -> mParent.handleForceStopButtonClick()); - } - - void checkForceStop(AppEntry appEntry, PackageInfo packageInfo) { - if (mDpm.packageHasActiveAdmins(packageInfo.packageName)) { - // User can't force stop device admin. - Log.w(TAG, "User can't force stop device admin"); - updateForceStopButton(false); - } else if (mPm.isPackageStateProtected(packageInfo.packageName, - UserHandle.getUserId(appEntry.info.uid))) { - Log.w(TAG, "User can't force stop protected packages"); - updateForceStopButton(false); - } else if (AppUtils.isInstant(packageInfo.applicationInfo)) { - updateForceStopButton(false); - mActionButtons.setButton2Visible(false); - } else if ((appEntry.info.flags & ApplicationInfo.FLAG_STOPPED) == 0) { - // If the app isn't explicitly stopped, then always show the - // force stop button. - Log.w(TAG, "App is not explicitly stopped"); - updateForceStopButton(true); - } else { - final Intent intent = new Intent(Intent.ACTION_QUERY_PACKAGE_RESTART, - Uri.fromParts("package", appEntry.info.packageName, null)); - intent.putExtra(Intent.EXTRA_PACKAGES, new String[] {appEntry.info.packageName}); - intent.putExtra(Intent.EXTRA_UID, appEntry.info.uid); - intent.putExtra(Intent.EXTRA_USER_HANDLE, UserHandle.getUserId(appEntry.info.uid)); - Log.d(TAG, "Sending broadcast to query restart status for " - + appEntry.info.packageName); - mContext.sendOrderedBroadcastAsUser(intent, UserHandle.CURRENT, null, - mCheckKillProcessesReceiver, null, Activity.RESULT_CANCELED, null, null); - } - } - - private boolean signaturesMatch(String pkg1, String pkg2) { - if (pkg1 != null && pkg2 != null) { - try { - return mPm.checkSignatures(pkg1, pkg2) >= PackageManager.SIGNATURE_MATCH; - } catch (Exception e) { - // e.g. named alternate package not found during lookup; - // this is an expected case sometimes - } - } - return false; - } - -} diff --git a/src/com/android/settings/applications/appinfo/AppBatteryPreferenceController.java b/src/com/android/settings/applications/appinfo/AppBatteryPreferenceController.java index 55a6c9ff4f..6e4818ab92 100644 --- a/src/com/android/settings/applications/appinfo/AppBatteryPreferenceController.java +++ b/src/com/android/settings/applications/appinfo/AppBatteryPreferenceController.java @@ -16,21 +16,21 @@ package com.android.settings.applications.appinfo; -import android.app.LoaderManager; import android.content.Context; -import android.content.Loader; import android.content.pm.PackageInfo; import android.os.BatteryStats; import android.os.Bundle; import android.os.UserManager; + import androidx.annotation.VisibleForTesting; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.Loader; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; import com.android.internal.os.BatterySipper; import com.android.internal.os.BatteryStatsHelper; import com.android.settings.R; -import com.android.settings.SettingsActivity; import com.android.settings.Utils; import com.android.settings.core.BasePreferenceController; import com.android.settings.fuelgauge.AdvancedPowerUsageDetail; @@ -98,13 +98,11 @@ public class AppBatteryPreferenceController extends BasePreferenceController (UserManager) mContext.getSystemService(Context.USER_SERVICE); final BatteryEntry entry = new BatteryEntry(mContext, null, userManager, mSipper); entry.defaultPackageName = mPackageName; - AdvancedPowerUsageDetail.startBatteryDetailPage( - (SettingsActivity) mParent.getActivity(), mParent, mBatteryHelper, - BatteryStats.STATS_SINCE_CHARGED, entry, mBatteryPercent, - null /* mAnomalies */); + AdvancedPowerUsageDetail.startBatteryDetailPage(mParent.getActivity(), mParent, + mBatteryHelper, BatteryStats.STATS_SINCE_CHARGED, entry, mBatteryPercent); } else { - AdvancedPowerUsageDetail.startBatteryDetailPage( - (SettingsActivity) mParent.getActivity(), mParent, mPackageName); + AdvancedPowerUsageDetail.startBatteryDetailPage(mParent.getActivity(), mParent, + mPackageName); } return true; } diff --git a/src/com/android/settings/fuelgauge/AppButtonsPreferenceController.java b/src/com/android/settings/applications/appinfo/AppButtonsPreferenceController.java index a2ba9a9618..e15b0e3e4b 100644 --- a/src/com/android/settings/fuelgauge/AppButtonsPreferenceController.java +++ b/src/com/android/settings/applications/appinfo/AppButtonsPreferenceController.java @@ -14,17 +14,19 @@ * limitations under the License. */ -package com.android.settings.fuelgauge; +package com.android.settings.applications.appinfo; import android.app.Activity; import android.app.ActivityManager; -import android.app.Fragment; import android.app.admin.DevicePolicyManager; +import android.app.settings.SettingsEnums; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.om.OverlayManager; +import android.content.om.OverlayInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; @@ -33,34 +35,34 @@ import android.content.res.Resources; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; -import android.os.RemoteException; -import android.os.ServiceManager; import android.os.UserHandle; import android.os.UserManager; -import androidx.annotation.VisibleForTesting; -import androidx.preference.PreferenceScreen; import android.util.Log; import android.view.View; -import android.webkit.IWebViewUpdateService; -import com.android.internal.logging.nano.MetricsProto; -import com.android.settings.DeviceAdminAdd; +import androidx.annotation.VisibleForTesting; +import androidx.fragment.app.Fragment; +import androidx.preference.PreferenceScreen; + import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.Utils; import com.android.settings.applications.ApplicationFeatureProvider; +import com.android.settings.applications.specialaccess.deviceadmin.DeviceAdminAdd; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.core.InstrumentedPreferenceFragment; import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.overlay.FeatureFactory; -import com.android.settings.widget.ActionButtonPreference; import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.applications.AppUtils; import com.android.settingslib.applications.ApplicationsState; -import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnDestroy; import com.android.settingslib.core.lifecycle.events.OnResume; +import com.android.settingslib.widget.ActionButtonsPreference; import java.util.ArrayList; import java.util.HashSet; @@ -74,8 +76,7 @@ import java.util.List; * An easy way to handle them is to delegate them to {@link #handleDialogClick(int)} and * {@link #handleActivityResult(int, int, Intent)} in this controller. */ -//TODO(80312809): Merge this class into {@link AppActionButtonPreferenceController} -public class AppButtonsPreferenceController extends AbstractPreferenceController implements +public class AppButtonsPreferenceController extends BasePreferenceController implements PreferenceControllerMixin, LifecycleObserver, OnResume, OnDestroy, ApplicationsState.Callbacks { public static final String APP_CHG = "chg"; @@ -97,19 +98,21 @@ public class AppButtonsPreferenceController extends AbstractPreferenceController @VisibleForTesting boolean mDisableAfterUninstall = false; @VisibleForTesting - ActionButtonPreference mButtonsPref; + ActionButtonsPreference mButtonsPref; + private final int mUserId; private final int mRequestUninstall; private final int mRequestRemoveDeviceAdmin; private final DevicePolicyManager mDpm; private final UserManager mUserManager; + private final OverlayManager mOverlayManager; private final PackageManager mPm; private final SettingsActivity mActivity; - private final Fragment mFragment; + private final InstrumentedPreferenceFragment mFragment; private final MetricsFeatureProvider mMetricsFeatureProvider; private final ApplicationFeatureProvider mApplicationFeatureProvider; - private final int mUserId; + private Intent mAppLaunchIntent; private ApplicationsState.Session mSession; private RestrictedLockUtils.EnforcedAdmin mAppsControlDisallowedAdmin; @@ -118,11 +121,11 @@ public class AppButtonsPreferenceController extends AbstractPreferenceController private boolean mFinishing = false; private boolean mAppsControlDisallowedBySystem; - public AppButtonsPreferenceController(SettingsActivity activity, Fragment fragment, + public AppButtonsPreferenceController(SettingsActivity activity, + InstrumentedPreferenceFragment fragment, Lifecycle lifecycle, String packageName, ApplicationsState state, - DevicePolicyManager dpm, UserManager userManager, - PackageManager packageManager, int requestUninstall, int requestRemoveDeviceAdmin) { - super(activity); + int requestUninstall, int requestRemoveDeviceAdmin) { + super(activity, KEY_ACTION_BUTTONS); if (!(fragment instanceof ButtonActionDialogFragment.AppButtonsDialogListener)) { throw new IllegalArgumentException( @@ -133,15 +136,17 @@ public class AppButtonsPreferenceController extends AbstractPreferenceController mMetricsFeatureProvider = factory.getMetricsFeatureProvider(); mApplicationFeatureProvider = factory.getApplicationFeatureProvider(activity); mState = state; - mDpm = dpm; - mUserManager = userManager; - mPm = packageManager; + mDpm = (DevicePolicyManager) activity.getSystemService(Context.DEVICE_POLICY_SERVICE); + mUserManager = (UserManager) activity.getSystemService(Context.USER_SERVICE); + mPm = activity.getPackageManager(); + mOverlayManager = activity.getSystemService(OverlayManager.class); mPackageName = packageName; mActivity = activity; mFragment = fragment; mUserId = UserHandle.myUserId(); mRequestUninstall = requestUninstall; mRequestRemoveDeviceAdmin = requestRemoveDeviceAdmin; + mAppLaunchIntent = mPm.getLaunchIntentForPackage(mPackageName); if (packageName != null) { mAppEntry = mState.getEntry(packageName, mUserId); @@ -153,23 +158,27 @@ public class AppButtonsPreferenceController extends AbstractPreferenceController } @Override - public boolean isAvailable() { + public int getAvailabilityStatus() { // TODO(b/37313605): Re-enable once this controller supports instant apps - return mAppEntry != null && !AppUtils.isInstant(mAppEntry.info); + return isInstantApp() || isSystemModule() ? DISABLED_FOR_USER : AVAILABLE; } @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); if (isAvailable()) { - mButtonsPref = ((ActionButtonPreference) screen.findPreference(KEY_ACTION_BUTTONS)) - .setButton1Text(R.string.uninstall_text) - .setButton2Text(R.string.force_stop) - .setButton1OnClickListener(new UninstallAndDisableButtonListener()) - .setButton2OnClickListener(new ForceStopButtonListener()) - .setButton1Positive(false) - .setButton2Positive(false) - .setButton2Enabled(false); + mButtonsPref = ((ActionButtonsPreference) screen.findPreference( + KEY_ACTION_BUTTONS)) + .setButton1Text(R.string.launch_instant_app) + .setButton1Icon(R.drawable.ic_settings_open) + .setButton1OnClickListener(v -> launchApplication()) + .setButton2Text(R.string.uninstall_text) + .setButton2Icon(R.drawable.ic_settings_delete) + .setButton2OnClickListener(new UninstallAndDisableButtonListener()) + .setButton3Text(R.string.force_stop) + .setButton3Icon(R.drawable.ic_settings_force_stop) + .setButton3OnClickListener(new ForceStopButtonListener()) + .setButton3Enabled(false); } } @@ -181,10 +190,10 @@ public class AppButtonsPreferenceController extends AbstractPreferenceController @Override public void onResume() { if (isAvailable() && !mFinishing) { - mAppsControlDisallowedBySystem = RestrictedLockUtils.hasBaseUserRestriction(mActivity, - UserManager.DISALLOW_APPS_CONTROL, mUserId); - mAppsControlDisallowedAdmin = RestrictedLockUtils.checkIfRestrictionEnforced(mActivity, - UserManager.DISALLOW_APPS_CONTROL, mUserId); + mAppsControlDisallowedBySystem = RestrictedLockUtilsInternal.hasBaseUserRestriction( + mActivity, UserManager.DISALLOW_APPS_CONTROL, mUserId); + mAppsControlDisallowedAdmin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced( + mActivity, UserManager.DISALLOW_APPS_CONTROL, mUserId); if (!refreshUi()) { setIntentAndFinish(true); @@ -209,15 +218,16 @@ public class AppButtonsPreferenceController extends AbstractPreferenceController uninstallDaIntent.putExtra(DeviceAdminAdd.EXTRA_DEVICE_ADMIN_PACKAGE_NAME, packageName); mMetricsFeatureProvider.action(mActivity, - MetricsProto.MetricsEvent.ACTION_SETTINGS_UNINSTALL_DEVICE_ADMIN); + SettingsEnums.ACTION_SETTINGS_UNINSTALL_DEVICE_ADMIN); mFragment.startActivityForResult(uninstallDaIntent, mRequestRemoveDeviceAdmin); return; } RestrictedLockUtils.EnforcedAdmin admin = - RestrictedLockUtils.checkIfUninstallBlocked(mActivity, + RestrictedLockUtilsInternal.checkIfUninstallBlocked(mActivity, packageName, mUserId); boolean uninstallBlockedBySystem = mAppsControlDisallowedBySystem || - RestrictedLockUtils.hasBaseUserRestriction(mActivity, packageName, mUserId); + RestrictedLockUtilsInternal.hasBaseUserRestriction(mActivity, packageName, + mUserId); if (admin != null && !uninstallBlockedBySystem) { RestrictedLockUtils.sendShowAdminSupportDetailsIntent(mActivity, admin); } else if ((mAppEntry.info.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { @@ -234,8 +244,8 @@ public class AppButtonsPreferenceController extends AbstractPreferenceController mMetricsFeatureProvider.action( mActivity, mAppEntry.info.enabled - ? MetricsProto.MetricsEvent.ACTION_SETTINGS_DISABLE_APP - : MetricsProto.MetricsEvent.ACTION_SETTINGS_ENABLE_APP); + ? SettingsEnums.ACTION_SETTINGS_DISABLE_APP + : SettingsEnums.ACTION_SETTINGS_ENABLE_APP); AsyncTask.execute(new DisableChangerRunnable(mPm, mAppEntry.info.packageName, PackageManager.COMPONENT_ENABLED_STATE_DEFAULT)); } @@ -278,13 +288,13 @@ public class AppButtonsPreferenceController extends AbstractPreferenceController switch (id) { case ButtonActionDialogFragment.DialogType.DISABLE: mMetricsFeatureProvider.action(mActivity, - MetricsProto.MetricsEvent.ACTION_SETTINGS_DISABLE_APP); + SettingsEnums.ACTION_SETTINGS_DISABLE_APP); AsyncTask.execute(new DisableChangerRunnable(mPm, mAppEntry.info.packageName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER)); break; case ButtonActionDialogFragment.DialogType.SPECIAL_DISABLE: mMetricsFeatureProvider.action(mActivity, - MetricsProto.MetricsEvent.ACTION_SETTINGS_DISABLE_APP); + SettingsEnums.ACTION_SETTINGS_DISABLE_APP); uninstallPkg(mAppEntry.info.packageName, false, true); break; case ButtonActionDialogFragment.DialogType.FORCE_STOP: @@ -357,6 +367,12 @@ public class AppButtonsPreferenceController extends AbstractPreferenceController } @VisibleForTesting + void updateOpenButton() { + mAppLaunchIntent = mPm.getLaunchIntentForPackage(mPackageName); + mButtonsPref.setButton1Visible(mAppLaunchIntent != null); + } + + @VisibleForTesting void updateUninstallButton() { final boolean isBundled = (mAppEntry.info.flags & ApplicationInfo.FLAG_SYSTEM) != 0; boolean enabled = true; @@ -422,11 +438,29 @@ public class AppButtonsPreferenceController extends AbstractPreferenceController enabled = false; } - if (isFallbackPackage(mAppEntry.info.packageName)) { - enabled = false; + // Resource overlays can be uninstalled iff they are public + // (installed on /data) and disabled. ("Enabled" means they + // are in use by resource management.) If they are + // system/vendor, they can never be uninstalled. :-( + if (mAppEntry.info.isResourceOverlay()) { + if (isBundled) { + enabled = false; + } else { + String pkgName = mAppEntry.info.packageName; + UserHandle user = UserHandle.getUserHandleForUid(mAppEntry.info.uid); + OverlayInfo overlayInfo = mOverlayManager.getOverlayInfo(pkgName, user); + if (overlayInfo != null && overlayInfo.isEnabled()) { + ApplicationsState.AppEntry targetEntry = + mState.getEntry(overlayInfo.targetPackageName, + UserHandle.getUserId(mAppEntry.info.uid)); + if (targetEntry != null) { + enabled = false; + } + } + } } - mButtonsPref.setButton1Enabled(enabled); + mButtonsPref.setButton2Enabled(enabled); } /** @@ -451,22 +485,6 @@ public class AppButtonsPreferenceController extends AbstractPreferenceController } @VisibleForTesting - boolean isFallbackPackage(String packageName) { - try { - IWebViewUpdateService webviewUpdateService = - IWebViewUpdateService.Stub.asInterface( - ServiceManager.getService("webviewupdate")); - if (webviewUpdateService.isFallbackPackage(packageName)) { - return true; - } - } catch (RemoteException e) { - throw new RuntimeException(e); - } - - return false; - } - - @VisibleForTesting void updateForceStopButton() { if (mDpm.packageHasActiveAdmins(mPackageInfo.packageName)) { // User can't force stop device admin. @@ -480,7 +498,7 @@ public class AppButtonsPreferenceController extends AbstractPreferenceController } else { Intent intent = new Intent(Intent.ACTION_QUERY_PACKAGE_RESTART, Uri.fromParts("package", mAppEntry.info.packageName, null)); - intent.putExtra(Intent.EXTRA_PACKAGES, new String[] {mAppEntry.info.packageName}); + intent.putExtra(Intent.EXTRA_PACKAGES, new String[]{mAppEntry.info.packageName}); intent.putExtra(Intent.EXTRA_UID, mAppEntry.info.uid); intent.putExtra(Intent.EXTRA_USER_HANDLE, UserHandle.getUserId(mAppEntry.info.uid)); Log.d(TAG, "Sending broadcast to query restart status for " @@ -493,9 +511,9 @@ public class AppButtonsPreferenceController extends AbstractPreferenceController @VisibleForTesting void updateForceStopButtonInner(boolean enabled) { if (mAppsControlDisallowedBySystem) { - mButtonsPref.setButton2Enabled(false); + mButtonsPref.setButton3Enabled(false); } else { - mButtonsPref.setButton2Enabled(enabled); + mButtonsPref.setButton3Enabled(enabled); } } @@ -508,15 +526,19 @@ public class AppButtonsPreferenceController extends AbstractPreferenceController uninstallIntent.putExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, allUsers); mMetricsFeatureProvider.action( - mActivity, MetricsProto.MetricsEvent.ACTION_SETTINGS_UNINSTALL_APP); + mActivity, SettingsEnums.ACTION_SETTINGS_UNINSTALL_APP); mFragment.startActivityForResult(uninstallIntent, mRequestUninstall); mDisableAfterUninstall = andDisable; } @VisibleForTesting void forceStopPackage(String pkgName) { - FeatureFactory.getFactory(mContext).getMetricsFeatureProvider().action(mContext, - MetricsProto.MetricsEvent.ACTION_APP_FORCE_STOP, pkgName); + mMetricsFeatureProvider.action( + mMetricsFeatureProvider.getAttribution(mActivity), + SettingsEnums.ACTION_APP_FORCE_STOP, + mFragment.getMetricsCategory(), + pkgName, + 0); ActivityManager am = (ActivityManager) mActivity.getSystemService( Context.ACTIVITY_SERVICE); Log.d(TAG, "Stopping package " + pkgName); @@ -539,16 +561,16 @@ public class AppButtonsPreferenceController extends AbstractPreferenceController if (mHomePackages.contains(mAppEntry.info.packageName) || isSystemPackage(mActivity.getResources(), mPm, mPackageInfo)) { // Disable button for core system applications. - mButtonsPref.setButton1Text(R.string.disable_text) - .setButton1Positive(false); + mButtonsPref.setButton2Text(R.string.disable_text) + .setButton2Icon(R.drawable.ic_settings_disable); } else if (mAppEntry.info.enabled && !isDisabledUntilUsed()) { - mButtonsPref.setButton1Text(R.string.disable_text) - .setButton1Positive(false); + mButtonsPref.setButton2Text(R.string.disable_text) + .setButton2Icon(R.drawable.ic_settings_disable); disableable = !mApplicationFeatureProvider.getKeepEnabledPackages() .contains(mAppEntry.info.packageName); } else { - mButtonsPref.setButton1Text(R.string.enable_text) - .setButton1Positive(true); + mButtonsPref.setButton2Text(R.string.enable_text) + .setButton2Icon(R.drawable.ic_settings_enable); disableable = true; } @@ -568,7 +590,7 @@ public class AppButtonsPreferenceController extends AbstractPreferenceController private void showDialogInner(@ButtonActionDialogFragment.DialogType int id) { ButtonActionDialogFragment newFragment = ButtonActionDialogFragment.newInstance(id); newFragment.setTargetFragment(mFragment, 0); - newFragment.show(mActivity.getFragmentManager(), "dialog " + id); + newFragment.show(mActivity.getSupportFragmentManager(), "dialog " + id); } /** Returns whether there is only one user on this device, not including the system-only user */ @@ -631,6 +653,7 @@ public class AppButtonsPreferenceController extends AbstractPreferenceController } } + updateOpenButton(); updateUninstallButton(); updateForceStopButton(); @@ -655,6 +678,19 @@ public class AppButtonsPreferenceController extends AbstractPreferenceController mActivity.unregisterReceiver(mPackageRemovedReceiver); } + private void launchApplication() { + if (mAppLaunchIntent != null) { + mContext.startActivityAsUser(mAppLaunchIntent, new UserHandle(mUserId)); + } + } + + private boolean isInstantApp() { + return mAppEntry != null && AppUtils.isInstant(mAppEntry.info); + } + + private boolean isSystemModule() { + return mAppEntry != null && AppUtils.isSystemModule(mContext, mAppEntry.info.packageName); + } /** * Changes the status of disable/enable for a package diff --git a/src/com/android/settings/applications/appinfo/AppDataUsagePreferenceController.java b/src/com/android/settings/applications/appinfo/AppDataUsagePreferenceController.java index c572c3724e..4d19151dbc 100644 --- a/src/com/android/settings/applications/appinfo/AppDataUsagePreferenceController.java +++ b/src/com/android/settings/applications/appinfo/AppDataUsagePreferenceController.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 The Android Open Source Project + * Copyright (C) 2018 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. @@ -16,41 +16,39 @@ package com.android.settings.applications.appinfo; -import android.app.LoaderManager; import android.content.Context; -import android.content.Loader; -import android.net.INetworkStatsService; -import android.net.INetworkStatsSession; import android.net.NetworkTemplate; import android.os.Bundle; -import android.os.RemoteException; -import android.os.ServiceManager; +import android.text.format.DateUtils; +import android.text.format.Formatter; + import androidx.annotation.VisibleForTesting; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.Loader; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.text.format.DateUtils; -import android.text.format.Formatter; import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; import com.android.settings.Utils; import com.android.settings.datausage.AppDataUsage; -import com.android.settings.datausage.DataUsageList; import com.android.settings.datausage.DataUsageUtils; import com.android.settingslib.AppItem; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnPause; import com.android.settingslib.core.lifecycle.events.OnResume; -import com.android.settingslib.net.ChartData; -import com.android.settingslib.net.ChartDataLoader; +import com.android.settingslib.net.NetworkCycleDataForUid; +import com.android.settingslib.net.NetworkCycleDataForUidLoader; + +import java.util.List; public class AppDataUsagePreferenceController extends AppInfoPreferenceControllerBase - implements LoaderManager.LoaderCallbacks<ChartData>, LifecycleObserver, OnResume, OnPause { + implements LoaderManager.LoaderCallbacks<List<NetworkCycleDataForUid>>, LifecycleObserver, + OnResume, OnPause { - private ChartData mChartData; - private INetworkStatsSession mStatsSession; + private List<NetworkCycleDataForUid> mAppUsageData; - public AppDataUsagePreferenceController(Context context,String key) { + public AppDataUsagePreferenceController(Context context, String key) { super(context, key); } @@ -62,15 +60,6 @@ public class AppDataUsagePreferenceController extends AppInfoPreferenceControlle @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - if (isAvailable()) { - final INetworkStatsService statsService = INetworkStatsService.Stub.asInterface( - ServiceManager.getService(Context.NETWORK_STATS_SERVICE)); - try { - mStatsSession = statsService.openSession(); - } catch (RemoteException e) { - throw new RuntimeException(e); - } - } } @Override @@ -80,34 +69,41 @@ public class AppDataUsagePreferenceController extends AppInfoPreferenceControlle @Override public void onResume() { - if (mStatsSession != null) { + if (isAvailable()) { final int uid = mParent.getAppEntry().info.uid; final AppItem app = new AppItem(uid); app.addUid(uid); - mParent.getLoaderManager().restartLoader(mParent.LOADER_CHART_DATA, - ChartDataLoader.buildArgs(getTemplate(mContext), app), + mParent.getLoaderManager().restartLoader(mParent.LOADER_CHART_DATA, null /* args */, this); } } @Override public void onPause() { - mParent.getLoaderManager().destroyLoader(mParent.LOADER_CHART_DATA); + if (isAvailable()) { + mParent.getLoaderManager().destroyLoader(mParent.LOADER_CHART_DATA); + } } @Override - public Loader<ChartData> onCreateLoader(int id, Bundle args) { - return new ChartDataLoader(mContext, mStatsSession, args); + public Loader<List<NetworkCycleDataForUid>> onCreateLoader(int id, Bundle args) { + final NetworkTemplate template = getTemplate(mContext); + return NetworkCycleDataForUidLoader.builder(mContext) + .addUid(mParent.getAppEntry().info.uid) + .setRetrieveDetail(false) + .setNetworkTemplate(template) + .build(); } @Override - public void onLoadFinished(Loader<ChartData> loader, ChartData data) { - mChartData = data; + public void onLoadFinished(Loader<List<NetworkCycleDataForUid>> loader, + List<NetworkCycleDataForUid> data) { + mAppUsageData = data; updateState(mPreference); } @Override - public void onLoaderReset(Loader<ChartData> loader) { + public void onLoaderReset(Loader<List<NetworkCycleDataForUid>> loader) { // Leave last result. } @@ -117,21 +113,29 @@ public class AppDataUsagePreferenceController extends AppInfoPreferenceControlle } private CharSequence getDataSummary() { - if (mChartData != null) { - final long totalBytes = mChartData.detail.getTotalBytes(); + if (mAppUsageData != null) { + long totalBytes = 0; + long startTime = System.currentTimeMillis(); + for (NetworkCycleDataForUid data : mAppUsageData) { + totalBytes += data.getTotalUsage(); + final long cycleStart = data.getStartTime(); + if (cycleStart < startTime) { + startTime = cycleStart; + } + } if (totalBytes == 0) { return mContext.getString(R.string.no_data_usage); } return mContext.getString(R.string.data_summary_format, Formatter.formatFileSize(mContext, totalBytes), - DateUtils.formatDateTime(mContext, mChartData.detail.getStart(), + DateUtils.formatDateTime(mContext, startTime, DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_MONTH)); } return mContext.getString(R.string.computing_size); } private static NetworkTemplate getTemplate(Context context) { - if (DataUsageList.hasReadyMobileRadio(context)) { + if (DataUsageUtils.hasReadyMobileRadio(context)) { return NetworkTemplate.buildTemplateMobileWildcard(); } if (DataUsageUtils.hasWifiRadio(context)) { diff --git a/src/com/android/settings/applications/appinfo/AppHeaderViewPreferenceController.java b/src/com/android/settings/applications/appinfo/AppHeaderViewPreferenceController.java index e37e670820..c9f0e31f17 100644 --- a/src/com/android/settings/applications/appinfo/AppHeaderViewPreferenceController.java +++ b/src/com/android/settings/applications/appinfo/AppHeaderViewPreferenceController.java @@ -19,11 +19,10 @@ package com.android.settings.applications.appinfo; import android.app.Activity; import android.content.Context; import android.content.pm.PackageInfo; + import androidx.preference.PreferenceScreen; import com.android.settings.R; -import com.android.settings.Utils; -import com.android.settings.applications.LayoutPreference; import com.android.settings.core.BasePreferenceController; import com.android.settings.widget.EntityHeaderController; import com.android.settingslib.applications.AppUtils; @@ -31,6 +30,7 @@ import com.android.settingslib.applications.ApplicationsState.AppEntry; import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.widget.LayoutPreference; public class AppHeaderViewPreferenceController extends BasePreferenceController implements AppInfoDashboardFragment.Callback, LifecycleObserver, OnStart { @@ -63,7 +63,7 @@ public class AppHeaderViewPreferenceController extends BasePreferenceController @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - mHeader = (LayoutPreference) screen.findPreference(KEY_HEADER); + mHeader = screen.findPreference(KEY_HEADER); final Activity activity = mParent.getActivity(); mEntityHeaderController = EntityHeaderController .newInstance(activity, mParent, mHeader.findViewById(R.id.entity_header)) @@ -89,12 +89,9 @@ public class AppHeaderViewPreferenceController extends BasePreferenceController private void setAppLabelAndIcon(PackageInfo pkgInfo, AppEntry appEntry) { final Activity activity = mParent.getActivity(); final boolean isInstantApp = AppUtils.isInstant(pkgInfo.applicationInfo); - final CharSequence summary = isInstantApp - ? null : mContext.getString(Utils.getInstallationStatus(appEntry.info)); mEntityHeaderController .setLabel(appEntry) .setIcon(appEntry) - .setSummary(summary) .setIsInstantApp(isInstantApp) .done(activity, false /* rebindActions */); } diff --git a/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java b/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java index d370ce0bce..8274634a01 100755 --- a/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java +++ b/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java @@ -19,14 +19,10 @@ package com.android.settings.applications.appinfo; import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; import android.app.Activity; -import android.app.ActivityManager; -import android.app.AlertDialog; -import android.app.Dialog; -import android.app.DialogFragment; import android.app.admin.DevicePolicyManager; +import android.app.settings.SettingsEnums; import android.content.BroadcastReceiver; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ApplicationInfo; @@ -35,34 +31,31 @@ import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.UserInfo; import android.net.Uri; -import android.os.AsyncTask; import android.os.Bundle; import android.os.UserHandle; import android.os.UserManager; -import androidx.annotation.VisibleForTesting; import android.text.TextUtils; import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.settings.DeviceAdminAdd; +import androidx.annotation.VisibleForTesting; + import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.SettingsPreferenceFragment; import com.android.settings.applications.manageapplications.ManageApplications; +import com.android.settings.applications.specialaccess.pictureinpicture.PictureInPictureDetailPreferenceController; import com.android.settings.core.SubSettingLauncher; -import com.android.settings.core.instrumentation.InstrumentedDialogFragment; import com.android.settings.dashboard.DashboardFragment; -import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.applications.AppUtils; import com.android.settingslib.applications.ApplicationsState; import com.android.settingslib.applications.ApplicationsState.AppEntry; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; -import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -77,7 +70,8 @@ import java.util.List; * uninstall the application. */ public class AppInfoDashboardFragment extends DashboardFragment - implements ApplicationsState.Callbacks { + implements ApplicationsState.Callbacks, + ButtonActionDialogFragment.AppButtonsDialogListener { private static final String TAG = "AppInfoDashboard"; @@ -91,7 +85,7 @@ public class AppInfoDashboardFragment extends DashboardFragment // Result code identifiers @VisibleForTesting static final int REQUEST_UNINSTALL = 0; - private static final int REQUEST_REMOVE_DEVICE_ADMIN = 1; + private static final int REQUEST_REMOVE_DEVICE_ADMIN = 5; static final int SUB_INFO_FRAGMENT = 1; @@ -99,13 +93,6 @@ public class AppInfoDashboardFragment extends DashboardFragment static final int LOADER_STORAGE = 3; static final int LOADER_BATTERY = 4; - // Dialog identifiers used in showDialog - private static final int DLG_BASE = 0; - private static final int DLG_FORCE_STOP = DLG_BASE + 1; - private static final int DLG_DISABLE = DLG_BASE + 2; - private static final int DLG_SPECIAL_DISABLE = DLG_BASE + 3; - static final int DLG_CLEAR_INSTANT_APP = DLG_BASE + 4; - public static final String ARG_PACKAGE_NAME = "package"; public static final String ARG_PACKAGE_UID = "uid"; @@ -125,19 +112,19 @@ public class AppInfoDashboardFragment extends DashboardFragment private UserManager mUserManager; private PackageManager mPm; - private boolean mFinishing; + @VisibleForTesting + boolean mFinishing; private boolean mListeningToPackageRemove; private boolean mInitialized; private boolean mShowUninstalled; private boolean mUpdatedSysApp = false; - private boolean mDisableAfterUninstall; private List<Callback> mCallbacks = new ArrayList<>(); private InstantAppButtonsPreferenceController mInstantAppButtonPreferenceController; - private AppActionButtonPreferenceController mAppActionButtonPreferenceController; + private AppButtonsPreferenceController mAppButtonsPreferenceController; /** * Callback to invoke when app info has been changed. @@ -146,11 +133,6 @@ public class AppInfoDashboardFragment extends DashboardFragment void refreshUi(); } - private boolean isDisabledUntilUsed() { - return mAppEntry.info.enabledSetting - == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED; - } - @Override public void onAttach(Context context) { super.onAttach(context); @@ -206,6 +188,9 @@ public class AppInfoDashboardFragment extends DashboardFragment if (!ensurePackageInfoAvailable(activity)) { return; } + if (!ensureDisplayableModule(activity)) { + return; + } startListeningToPackageRemove(); setHasOptionsMenu(true); @@ -227,17 +212,17 @@ public class AppInfoDashboardFragment extends DashboardFragment @Override public int getMetricsCategory() { - return MetricsEvent.APPLICATIONS_INSTALLED_APP_DETAILS; + return SettingsEnums.APPLICATIONS_INSTALLED_APP_DETAILS; } @Override public void onResume() { super.onResume(); final Activity activity = getActivity(); - mAppsControlDisallowedAdmin = RestrictedLockUtils.checkIfRestrictionEnforced(activity, - UserManager.DISALLOW_APPS_CONTROL, mUserId); - mAppsControlDisallowedBySystem = RestrictedLockUtils.hasBaseUserRestriction(activity, - UserManager.DISALLOW_APPS_CONTROL, mUserId); + mAppsControlDisallowedAdmin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced( + activity, UserManager.DISALLOW_APPS_CONTROL, mUserId); + mAppsControlDisallowedBySystem = RestrictedLockUtilsInternal.hasBaseUserRestriction( + activity, UserManager.DISALLOW_APPS_CONTROL, mUserId); if (!refreshUi()) { setIntentAndFinish(true, true); @@ -262,15 +247,12 @@ public class AppInfoDashboardFragment extends DashboardFragment } final String packageName = getPackageName(); final List<AbstractPreferenceController> controllers = new ArrayList<>(); - final Lifecycle lifecycle = getLifecycle(); + final Lifecycle lifecycle = getSettingsLifecycle(); // The following are controllers for preferences that needs to refresh the preference state // when app state changes. controllers.add( new AppHeaderViewPreferenceController(context, this, packageName, lifecycle)); - mAppActionButtonPreferenceController = - new AppActionButtonPreferenceController(context, this, packageName); - controllers.add(mAppActionButtonPreferenceController); for (AbstractPreferenceController controller : controllers) { mCallbacks.add((Callback) controller); @@ -281,6 +263,10 @@ public class AppInfoDashboardFragment extends DashboardFragment mInstantAppButtonPreferenceController = new InstantAppButtonsPreferenceController(context, this, packageName, lifecycle); controllers.add(mInstantAppButtonPreferenceController); + mAppButtonsPreferenceController = new AppButtonsPreferenceController( + (SettingsActivity) getActivity(), this, lifecycle, packageName, mState, + REQUEST_UNINSTALL, REQUEST_REMOVE_DEVICE_ADMIN); + controllers.add(mAppButtonsPreferenceController); controllers.add(new AppBatteryPreferenceController(context, this, packageName, lifecycle)); controllers.add(new AppMemoryPreferenceController(context, this, lifecycle)); controllers.add(new DefaultHomeShortcutPreferenceController(context, packageName)); @@ -306,7 +292,7 @@ public class AppInfoDashboardFragment extends DashboardFragment mAppEntry = appEntry; } - PackageInfo getPackageInfo() { + public PackageInfo getPackageInfo() { return mPackageInfo; } @@ -336,6 +322,23 @@ public class AppInfoDashboardFragment extends DashboardFragment return true; } + /** + * Ensures the package is displayable as directed by {@link AppUtils#isHiddenSystemModule}. + * If it's not, the fragment will finish. + * + * @return true if package is displayable. + */ + @VisibleForTesting + boolean ensureDisplayableModule(Activity activity) { + if (AppUtils.isHiddenSystemModule(activity.getApplicationContext(), mPackageName)) { + mFinishing = true; + Log.w(TAG, "Package is hidden module, exiting: " + mPackageName); + activity.finishAndRemoveTask(); + return false; + } + return true; + } + @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); @@ -356,10 +359,12 @@ public class AppInfoDashboardFragment extends DashboardFragment final MenuItem uninstallUpdatesItem = menu.findItem(UNINSTALL_UPDATES); final boolean uninstallUpdateDisabled = getContext().getResources().getBoolean( R.bool.config_disable_uninstall_update); - uninstallUpdatesItem.setVisible( - mUpdatedSysApp && !mAppsControlDisallowedBySystem && !uninstallUpdateDisabled); + uninstallUpdatesItem.setVisible(mUserManager.isAdminUser() + && mUpdatedSysApp + && !mAppsControlDisallowedBySystem + && !uninstallUpdateDisabled); if (uninstallUpdatesItem.isVisible()) { - RestrictedLockUtils.setMenuItemAsDisabledByAdmin(getActivity(), + RestrictedLockUtilsInternal.setMenuItemAsDisabledByAdmin(getActivity(), uninstallUpdatesItem, mAppsControlDisallowedAdmin); } } @@ -380,30 +385,19 @@ public class AppInfoDashboardFragment extends DashboardFragment @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); - switch (requestCode) { - case REQUEST_UNINSTALL: - // Refresh option menu - getActivity().invalidateOptionsMenu(); - - if (mDisableAfterUninstall) { - mDisableAfterUninstall = false; - new DisableChanger(this, mAppEntry.info, - PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER) - .execute((Object) null); - } - if (!refreshUi()) { - onPackageRemoved(); - } else { - startListeningToPackageRemove(); - } - break; - case REQUEST_REMOVE_DEVICE_ADMIN: - if (!refreshUi()) { - setIntentAndFinish(true, true); - } else { - startListeningToPackageRemove(); - } - break; + if (requestCode == REQUEST_UNINSTALL) { + // Refresh option menu + getActivity().invalidateOptionsMenu(); + } + if (mAppButtonsPreferenceController != null) { + mAppButtonsPreferenceController.handleActivityResult(requestCode, resultCode, data); + } + } + + @Override + public void handleDialogClick(int id) { + if (mAppButtonsPreferenceController != null) { + mAppButtonsPreferenceController.handleDialogClick(id); } } @@ -448,6 +442,9 @@ public class AppInfoDashboardFragment extends DashboardFragment for (Callback callback : mCallbacks) { callback.refreshUi(); } + if (mAppButtonsPreferenceController.isAvailable()) { + mAppButtonsPreferenceController.refreshUi(); + } if (!mInitialized) { // First time init: are we displaying an uninstalled app? @@ -475,58 +472,6 @@ public class AppInfoDashboardFragment extends DashboardFragment return true; } - @VisibleForTesting - AlertDialog createDialog(int id, int errorCode) { - switch (id) { - case DLG_DISABLE: - return new AlertDialog.Builder(getActivity()) - .setMessage(getActivity().getText(R.string.app_disable_dlg_text)) - .setPositiveButton(R.string.app_disable_dlg_positive, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - // Disable the app - mMetricsFeatureProvider.action(getContext(), - MetricsEvent.ACTION_SETTINGS_DISABLE_APP); - new DisableChanger(AppInfoDashboardFragment.this, - mAppEntry.info, - PackageManager - .COMPONENT_ENABLED_STATE_DISABLED_USER) - .execute((Object) null); - } - }) - .setNegativeButton(R.string.dlg_cancel, null) - .create(); - case DLG_SPECIAL_DISABLE: - return new AlertDialog.Builder(getActivity()) - .setMessage(getActivity().getText(R.string.app_disable_dlg_text)) - .setPositiveButton(R.string.app_disable_dlg_positive, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - // Disable the app and ask for uninstall - mMetricsFeatureProvider.action(getContext(), - MetricsEvent.ACTION_SETTINGS_DISABLE_APP); - uninstallPkg(mAppEntry.info.packageName, - false, true); - } - }) - .setNegativeButton(R.string.dlg_cancel, null) - .create(); - case DLG_FORCE_STOP: - return new AlertDialog.Builder(getActivity()) - .setTitle(getActivity().getText(R.string.force_stop_dlg_title)) - .setMessage(getActivity().getText(R.string.force_stop_dlg_text)) - .setPositiveButton(R.string.dlg_ok, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - // Force stop - forceStopPackage(mAppEntry.info.packageName); - } - }) - .setNegativeButton(R.string.dlg_cancel, null) - .create(); - } - return mInstantAppButtonPreferenceController.createDialog(id); - } - private void uninstallPkg(String packageName, boolean allUsers, boolean andDisable) { stopListeningToPackageRemove(); // Create new intent to launch Uninstaller activity @@ -534,24 +479,8 @@ public class AppInfoDashboardFragment extends DashboardFragment final Intent uninstallIntent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageURI); uninstallIntent.putExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, allUsers); mMetricsFeatureProvider.action( - getContext(), MetricsEvent.ACTION_SETTINGS_UNINSTALL_APP); + getContext(), SettingsEnums.ACTION_SETTINGS_UNINSTALL_APP); startActivityForResult(uninstallIntent, REQUEST_UNINSTALL); - mDisableAfterUninstall = andDisable; - } - - private void forceStopPackage(String pkgName) { - mMetricsFeatureProvider.action(getContext(), MetricsEvent.ACTION_APP_FORCE_STOP, pkgName); - final ActivityManager am = (ActivityManager) getActivity().getSystemService( - Context.ACTIVITY_SERVICE); - Log.d(TAG, "Stopping package " + pkgName); - am.forceStopPackage(pkgName); - final int userId = UserHandle.getUserId(mAppEntry.info.uid); - mState.invalidatePackage(pkgName, userId); - final AppEntry newEnt = mState.getEntry(pkgName, userId); - if (newEnt != null) { - mAppEntry = newEnt; - } - mAppActionButtonPreferenceController.checkForceStop(mAppEntry, mPackageInfo); } public static void startAppInfoFragment(Class<?> fragment, int title, Bundle args, @@ -565,80 +494,12 @@ public class AppInfoDashboardFragment extends DashboardFragment new SubSettingLauncher(caller.getContext()) .setDestination(fragment.getName()) .setArguments(args) - .setTitle(title) + .setTitleRes(title) .setResultListener(caller, SUB_INFO_FRAGMENT) .setSourceMetricsCategory(caller.getMetricsCategory()) .launch(); } - void handleUninstallButtonClick() { - if (mAppEntry == null) { - setIntentAndFinish(true, true); - return; - } - final String packageName = mAppEntry.info.packageName; - if (mDpm.packageHasActiveAdmins(mPackageInfo.packageName)) { - stopListeningToPackageRemove(); - final Activity activity = getActivity(); - final Intent uninstallDAIntent = new Intent(activity, DeviceAdminAdd.class); - uninstallDAIntent.putExtra(DeviceAdminAdd.EXTRA_DEVICE_ADMIN_PACKAGE_NAME, - mPackageName); - mMetricsFeatureProvider.action( - activity, MetricsEvent.ACTION_SETTINGS_UNINSTALL_DEVICE_ADMIN); - activity.startActivityForResult(uninstallDAIntent, REQUEST_REMOVE_DEVICE_ADMIN); - return; - } - final EnforcedAdmin admin = RestrictedLockUtils.checkIfUninstallBlocked(getActivity(), - packageName, mUserId); - final boolean uninstallBlockedBySystem = mAppsControlDisallowedBySystem || - RestrictedLockUtils.hasBaseUserRestriction(getActivity(), packageName, mUserId); - if (admin != null && !uninstallBlockedBySystem) { - RestrictedLockUtils.sendShowAdminSupportDetailsIntent(getActivity(), admin); - } else if ((mAppEntry.info.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { - if (mAppEntry.info.enabled && !isDisabledUntilUsed()) { - // If the system app has an update and this is the only user on the device, - // then offer to downgrade the app, otherwise only offer to disable the - // app for this user. - if (mUpdatedSysApp && isSingleUser()) { - showDialogInner(DLG_SPECIAL_DISABLE, 0); - } else { - showDialogInner(DLG_DISABLE, 0); - } - } else { - mMetricsFeatureProvider.action( - getActivity(), - MetricsEvent.ACTION_SETTINGS_ENABLE_APP); - new DisableChanger(this, mAppEntry.info, - PackageManager.COMPONENT_ENABLED_STATE_ENABLED) - .execute((Object) null); - } - } else if ((mAppEntry.info.flags & ApplicationInfo.FLAG_INSTALLED) == 0) { - uninstallPkg(packageName, true, false); - } else { - uninstallPkg(packageName, false, false); - } - } - - void handleForceStopButtonClick() { - if (mAppEntry == null) { - setIntentAndFinish(true, true); - return; - } - if (mAppsControlDisallowedAdmin != null && !mAppsControlDisallowedBySystem) { - RestrictedLockUtils.sendShowAdminSupportDetailsIntent( - getActivity(), mAppsControlDisallowedAdmin); - } else { - showDialogInner(DLG_FORCE_STOP, 0); - //forceStopPackage(mAppInfo.packageName); - } - } - - /** Returns whether there is only one user on this device, not including the system-only user */ - private boolean isSingleUser() { - final int userCount = mUserManager.getUserCount(); - return userCount == 1 || (mUserManager.isSplitSystemUser() && userCount == 2); - } - private void onPackageRemoved() { getActivity().finishActivity(SUB_INFO_FRAGMENT); getActivity().finishAndRemoveTask(); @@ -665,26 +526,6 @@ public class AppInfoDashboardFragment extends DashboardFragment return count; } - private static class DisableChanger extends AsyncTask<Object, Object, Object> { - final PackageManager mPm; - final WeakReference<AppInfoDashboardFragment> mActivity; - final ApplicationInfo mInfo; - final int mState; - - DisableChanger(AppInfoDashboardFragment activity, ApplicationInfo info, int state) { - mPm = activity.mPm; - mActivity = new WeakReference<AppInfoDashboardFragment>(activity); - mInfo = info; - mState = state; - } - - @Override - protected Object doInBackground(Object... params) { - mPm.setApplicationEnabledSetting(mInfo.packageName, mState, 0); - return null; - } - } - private String getPackageName() { if (mPackageName != null) { return mPackageName; @@ -704,12 +545,12 @@ public class AppInfoDashboardFragment extends DashboardFragment @VisibleForTesting void retrieveAppEntry() { final Activity activity = getActivity(); - if (activity == null) { + if (activity == null || mFinishing) { return; } if (mState == null) { mState = ApplicationsState.getInstance(activity.getApplication()); - mSession = mState.newSession(this, getLifecycle()); + mSession = mState.newSession(this, getSettingsLifecycle()); } mUserId = UserHandle.myUserId(); mAppEntry = mState.getEntry(getPackageName(), UserHandle.myUserId()); @@ -740,12 +581,6 @@ public class AppInfoDashboardFragment extends DashboardFragment mFinishing = true; } - void showDialogInner(int id, int moveErrorCode) { - final DialogFragment newFragment = MyAlertDialogFragment.newInstance(id, moveErrorCode); - newFragment.setTargetFragment(this, 0); - newFragment.show(getFragmentManager(), "dialog " + id); - } - @Override public void onRunningStateChanged(boolean running) { // No op. @@ -783,37 +618,6 @@ public class AppInfoDashboardFragment extends DashboardFragment } } - public static class MyAlertDialogFragment extends InstrumentedDialogFragment { - - private static final String ARG_ID = "id"; - - @Override - public int getMetricsCategory() { - return MetricsEvent.DIALOG_APP_INFO_ACTION; - } - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - final int id = getArguments().getInt(ARG_ID); - final int errorCode = getArguments().getInt("moveError"); - final Dialog dialog = - ((AppInfoDashboardFragment) getTargetFragment()).createDialog(id, errorCode); - if (dialog == null) { - throw new IllegalArgumentException("unknown id " + id); - } - return dialog; - } - - public static MyAlertDialogFragment newInstance(int id, int errorCode) { - final MyAlertDialogFragment dialogFragment = new MyAlertDialogFragment(); - final Bundle args = new Bundle(); - args.putInt(ARG_ID, id); - args.putInt("moveError", errorCode); - dialogFragment.setArguments(args); - return dialogFragment; - } - } - @VisibleForTesting void startListeningToPackageRemove() { if (mListeningToPackageRemove) { @@ -837,10 +641,18 @@ public class AppInfoDashboardFragment extends DashboardFragment final BroadcastReceiver mPackageRemovedReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { + if (mFinishing) { + return; + } + final String packageName = intent.getData().getSchemeSpecificPart(); - if (!mFinishing && (mAppEntry == null || mAppEntry.info == null - || TextUtils.equals(mAppEntry.info.packageName, packageName))) { + if (mAppEntry == null + || mAppEntry.info == null + || TextUtils.equals(mAppEntry.info.packageName, packageName)) { onPackageRemoved(); + } else if (mAppEntry.info.isResourceOverlay() + && TextUtils.equals(mPackageInfo.overlayTarget, packageName)) { + refreshUi(); } } }; diff --git a/src/com/android/settings/applications/appinfo/AppInfoPreferenceControllerBase.java b/src/com/android/settings/applications/appinfo/AppInfoPreferenceControllerBase.java index 3fc629d5a5..c495cbc50c 100644 --- a/src/com/android/settings/applications/appinfo/AppInfoPreferenceControllerBase.java +++ b/src/com/android/settings/applications/appinfo/AppInfoPreferenceControllerBase.java @@ -18,9 +18,10 @@ package com.android.settings.applications.appinfo; import android.content.Context; import android.os.Bundle; +import android.text.TextUtils; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.text.TextUtils; import com.android.settings.SettingsPreferenceFragment; import com.android.settings.core.BasePreferenceController; diff --git a/src/com/android/settings/applications/appinfo/AppInstallerInfoPreferenceController.java b/src/com/android/settings/applications/appinfo/AppInstallerInfoPreferenceController.java index 6a1079c7a7..bf8567684e 100644 --- a/src/com/android/settings/applications/appinfo/AppInstallerInfoPreferenceController.java +++ b/src/com/android/settings/applications/appinfo/AppInstallerInfoPreferenceController.java @@ -19,6 +19,7 @@ package com.android.settings.applications.appinfo; import android.content.Context; import android.content.Intent; import android.os.UserManager; + import androidx.preference.Preference; import com.android.settings.R; diff --git a/src/com/android/settings/applications/appinfo/AppMemoryPreferenceController.java b/src/com/android/settings/applications/appinfo/AppMemoryPreferenceController.java index e5b6eed0f5..19e8ebbb29 100644 --- a/src/com/android/settings/applications/appinfo/AppMemoryPreferenceController.java +++ b/src/com/android/settings/applications/appinfo/AppMemoryPreferenceController.java @@ -20,9 +20,10 @@ import android.app.Activity; import android.content.Context; import android.content.pm.PackageInfo; import android.os.AsyncTask; +import android.text.format.Formatter; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.text.format.Formatter; import com.android.settings.R; import com.android.settings.SettingsActivity; diff --git a/src/com/android/settings/applications/appinfo/AppNotificationPreferenceController.java b/src/com/android/settings/applications/appinfo/AppNotificationPreferenceController.java index d4e7e602b6..07e14d4b1a 100644 --- a/src/com/android/settings/applications/appinfo/AppNotificationPreferenceController.java +++ b/src/com/android/settings/applications/appinfo/AppNotificationPreferenceController.java @@ -20,6 +20,7 @@ import static com.android.settings.SettingsActivity.EXTRA_FRAGMENT_ARG_KEY; import android.content.Context; import android.os.Bundle; + import androidx.preference.Preference; import com.android.settings.R; @@ -84,14 +85,15 @@ public class AppNotificationPreferenceController extends AppInfoPreferenceContro if (appRow.banned) { return context.getText(R.string.notifications_disabled); } else if (appRow.channelCount == 0) { - return context.getText(R.string.notifications_enabled); + return NotificationBackend.getSentSummary(context, appRow.sentByApp, false); } else if (appRow.channelCount == appRow.blockedChannelCount) { return context.getText(R.string.notifications_disabled); } else { if (appRow.blockedChannelCount == 0) { - return context.getText(R.string.notifications_enabled); + return NotificationBackend.getSentSummary(context, appRow.sentByApp, false); } return context.getString(R.string.notifications_enabled_with_info, + NotificationBackend.getSentSummary(context, appRow.sentByApp, false), context.getResources().getQuantityString(R.plurals.notifications_categories_off, appRow.blockedChannelCount, appRow.blockedChannelCount)); } diff --git a/src/com/android/settings/applications/appinfo/AppOpenByDefaultPreferenceController.java b/src/com/android/settings/applications/appinfo/AppOpenByDefaultPreferenceController.java index 7e61385cb7..7f64e3b48b 100644 --- a/src/com/android/settings/applications/appinfo/AppOpenByDefaultPreferenceController.java +++ b/src/com/android/settings/applications/appinfo/AppOpenByDefaultPreferenceController.java @@ -22,6 +22,7 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.hardware.usb.IUsbManager; import android.os.ServiceManager; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; diff --git a/src/com/android/settings/applications/appinfo/AppPermissionPreferenceController.java b/src/com/android/settings/applications/appinfo/AppPermissionPreferenceController.java index c5eed7b4ec..12393ad13f 100644 --- a/src/com/android/settings/applications/appinfo/AppPermissionPreferenceController.java +++ b/src/com/android/settings/applications/appinfo/AppPermissionPreferenceController.java @@ -21,9 +21,10 @@ import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.icu.text.ListFormatter; +import android.util.Log; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; -import android.util.Log; import com.android.settings.R; import com.android.settingslib.applications.PermissionsSummaryHelper; @@ -45,11 +46,8 @@ public class AppPermissionPreferenceController extends AppInfoPreferenceControll public void onPermissionSummaryResult(int standardGrantedPermissionCount, int requestedPermissionCount, int additionalGrantedPermissionCount, List<CharSequence> grantedGroupLabels) { - if (mParent.getActivity() == null) { - return; - } final Resources res = mContext.getResources(); - CharSequence summary = null; + CharSequence summary; if (requestedPermissionCount == 0) { summary = res.getString( diff --git a/src/com/android/settings/applications/appinfo/AppSettingPreferenceController.java b/src/com/android/settings/applications/appinfo/AppSettingPreferenceController.java index 4973ee90bb..cacbffb946 100644 --- a/src/com/android/settings/applications/appinfo/AppSettingPreferenceController.java +++ b/src/com/android/settings/applications/appinfo/AppSettingPreferenceController.java @@ -16,14 +16,14 @@ package com.android.settings.applications.appinfo; -import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_OPEN_APP_SETTING; - +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; import android.content.pm.ResolveInfo; -import androidx.preference.Preference; import android.text.TextUtils; +import androidx.preference.Preference; + import com.android.settings.overlay.FeatureFactory; public class AppSettingPreferenceController extends AppInfoPreferenceControllerBase { @@ -60,8 +60,10 @@ public class AppSettingPreferenceController extends AppInfoPreferenceControllerB return false; } FeatureFactory.getFactory(mContext).getMetricsFeatureProvider() - .actionWithSource(mContext, mParent.getMetricsCategory(), - ACTION_OPEN_APP_SETTING); + .action(SettingsEnums.PAGE_UNKNOWN, + SettingsEnums.ACTION_OPEN_APP_SETTING, + mParent.getMetricsCategory(), + null, 0); mContext.startActivity(intent); return true; } diff --git a/src/com/android/settings/applications/appinfo/AppStoragePreferenceController.java b/src/com/android/settings/applications/appinfo/AppStoragePreferenceController.java index 5754dd2030..d887634a54 100644 --- a/src/com/android/settings/applications/appinfo/AppStoragePreferenceController.java +++ b/src/com/android/settings/applications/appinfo/AppStoragePreferenceController.java @@ -16,15 +16,16 @@ package com.android.settings.applications.appinfo; -import android.app.LoaderManager; import android.content.Context; -import android.content.Loader; import android.content.pm.ApplicationInfo; import android.os.Bundle; import android.os.UserHandle; +import android.text.format.Formatter; + import androidx.annotation.VisibleForTesting; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.Loader; import androidx.preference.Preference; -import android.text.format.Formatter; import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; diff --git a/src/com/android/settings/fuelgauge/ButtonActionDialogFragment.java b/src/com/android/settings/applications/appinfo/ButtonActionDialogFragment.java index f4784a9782..80ce802003 100644 --- a/src/com/android/settings/fuelgauge/ButtonActionDialogFragment.java +++ b/src/com/android/settings/applications/appinfo/ButtonActionDialogFragment.java @@ -1,15 +1,30 @@ -package com.android.settings.fuelgauge; +/* + * Copyright (C) 2018 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.applications.appinfo; -import android.app.Activity; -import android.app.AlertDialog; import android.app.Dialog; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; + import androidx.annotation.IntDef; import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; @@ -26,7 +41,7 @@ public class ButtonActionDialogFragment extends InstrumentedDialogFragment imple /** * Interface to handle the dialog click */ - interface AppButtonsDialogListener { + public interface AppButtonsDialogListener { void handleDialogClick(int type); } @@ -59,7 +74,7 @@ public class ButtonActionDialogFragment extends InstrumentedDialogFragment imple public int getMetricsCategory() { //TODO(35810915): update the metrics label because for now this fragment will be shown // in two screens - return MetricsProto.MetricsEvent.DIALOG_APP_INFO_ACTION; + return SettingsEnums.DIALOG_APP_INFO_ACTION; } @Override diff --git a/src/com/android/settings/applications/appinfo/DefaultAppShortcutPreferenceControllerBase.java b/src/com/android/settings/applications/appinfo/DefaultAppShortcutPreferenceControllerBase.java index 7b45b67de2..0663e2a35b 100644 --- a/src/com/android/settings/applications/appinfo/DefaultAppShortcutPreferenceControllerBase.java +++ b/src/com/android/settings/applications/appinfo/DefaultAppShortcutPreferenceControllerBase.java @@ -14,18 +14,22 @@ package com.android.settings.applications.appinfo; +import android.app.role.RoleControllerManager; +import android.app.role.RoleManager; +import android.app.settings.SettingsEnums; import android.content.Context; -import android.os.Bundle; +import android.content.Intent; import android.os.UserManager; -import androidx.preference.Preference; import android.text.TextUtils; -import com.android.internal.logging.nano.MetricsProto; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.internal.util.CollectionUtils; import com.android.settings.R; -import com.android.settings.SettingsActivity; -import com.android.settings.applications.DefaultAppSettings; import com.android.settings.core.BasePreferenceController; -import com.android.settings.core.SubSettingLauncher; + +import java.util.concurrent.Executor; /* * Abstract base controller for the default app shortcut preferences that launches the default app @@ -33,56 +37,91 @@ import com.android.settings.core.SubSettingLauncher; */ public abstract class DefaultAppShortcutPreferenceControllerBase extends BasePreferenceController { + private final String mRoleName; + protected final String mPackageName; + private final RoleManager mRoleManager; + + private boolean mRoleVisible; + + private boolean mAppQualified; + + private PreferenceScreen mPreferenceScreen; + public DefaultAppShortcutPreferenceControllerBase(Context context, String preferenceKey, - String packageName) { + String roleName, String packageName) { super(context, preferenceKey); + + mRoleName = roleName; mPackageName = packageName; + + mRoleManager = context.getSystemService(RoleManager.class); + + final RoleControllerManager roleControllerManager = + mContext.getSystemService(RoleControllerManager.class); + final Executor executor = mContext.getMainExecutor(); + roleControllerManager.isRoleVisible(mRoleName, executor, visible -> { + mRoleVisible = visible; + refreshAvailability(); + }); + roleControllerManager.isApplicationQualifiedForRole(mRoleName, mPackageName, executor, + qualified -> { + mAppQualified = qualified; + refreshAvailability(); + }); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + + mPreferenceScreen = screen; + } + + private void refreshAvailability() { + if (mPreferenceScreen != null) { + final Preference preference = mPreferenceScreen.findPreference(getPreferenceKey()); + if (preference != null) { + preference.setVisible(isAvailable()); + updateState(preference); + } + } } @Override public int getAvailabilityStatus() { - if (UserManager.get(mContext).isManagedProfile()) { + if (mContext.getSystemService(UserManager.class).isManagedProfile()) { return DISABLED_FOR_USER; } - return hasAppCapability() ? AVAILABLE : UNSUPPORTED_ON_DEVICE; + return mRoleVisible && mAppQualified ? AVAILABLE : UNSUPPORTED_ON_DEVICE; } @Override public CharSequence getSummary() { - int summaryResId = isDefaultApp() ? R.string.yes : R.string.no; + final int summaryResId = isDefaultApp() ? R.string.yes : R.string.no; return mContext.getText(summaryResId); } @Override public boolean handlePreferenceTreeClick(Preference preference) { - if (TextUtils.equals(mPreferenceKey, preference.getKey())) { - final Bundle bundle = new Bundle(); - bundle.putString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, mPreferenceKey); - new SubSettingLauncher(mContext) - .setDestination(DefaultAppSettings.class.getName()) - .setArguments(bundle) - .setTitle(R.string.configure_apps) - .setSourceMetricsCategory(MetricsProto.MetricsEvent.VIEW_UNKNOWN) - .launch(); - return true; + if (!TextUtils.equals(mPreferenceKey, preference.getKey())) { + return false; } - return false; + final Intent intent = new Intent(Intent.ACTION_MANAGE_DEFAULT_APP) + .putExtra(Intent.EXTRA_ROLE_NAME, mRoleName); + mContext.startActivity(intent); + return true; } /** - * Check whether the app has the default app capability - * - * @return true if the app has the default app capability - */ - protected abstract boolean hasAppCapability(); - - /** * Check whether the app is the default app * * @return true if the app is the default app */ - protected abstract boolean isDefaultApp(); - + private boolean isDefaultApp() { + final String packageName = CollectionUtils.firstOrNull(mRoleManager.getRoleHolders( + mRoleName)); + return TextUtils.equals(mPackageName, packageName); + } } diff --git a/src/com/android/settings/applications/appinfo/DefaultBrowserShortcutPreferenceController.java b/src/com/android/settings/applications/appinfo/DefaultBrowserShortcutPreferenceController.java index 64af3c25b7..d554620010 100644 --- a/src/com/android/settings/applications/appinfo/DefaultBrowserShortcutPreferenceController.java +++ b/src/com/android/settings/applications/appinfo/DefaultBrowserShortcutPreferenceController.java @@ -14,10 +14,8 @@ package com.android.settings.applications.appinfo; +import android.app.role.RoleManager; import android.content.Context; -import android.os.UserHandle; - -import com.android.settings.applications.defaultapps.DefaultBrowserPreferenceController; public class DefaultBrowserShortcutPreferenceController extends DefaultAppShortcutPreferenceControllerBase { @@ -25,18 +23,6 @@ public class DefaultBrowserShortcutPreferenceController private static final String KEY = "default_browser"; public DefaultBrowserShortcutPreferenceController(Context context, String packageName) { - super(context, KEY, packageName); - } - - @Override - protected boolean hasAppCapability() { - return DefaultBrowserPreferenceController.hasBrowserPreference(mPackageName, mContext); + super(context, KEY, RoleManager.ROLE_BROWSER, packageName); } - - @Override - protected boolean isDefaultApp() { - return new DefaultBrowserPreferenceController(mContext) - .isBrowserDefault(mPackageName, UserHandle.myUserId()); - } - } diff --git a/src/com/android/settings/applications/appinfo/DefaultEmergencyShortcutPreferenceController.java b/src/com/android/settings/applications/appinfo/DefaultEmergencyShortcutPreferenceController.java index f0c1b8aec1..bd467ac411 100644 --- a/src/com/android/settings/applications/appinfo/DefaultEmergencyShortcutPreferenceController.java +++ b/src/com/android/settings/applications/appinfo/DefaultEmergencyShortcutPreferenceController.java @@ -14,27 +14,15 @@ package com.android.settings.applications.appinfo; +import android.app.role.RoleManager; import android.content.Context; -import com.android.settings.applications.defaultapps.DefaultEmergencyPreferenceController; - public class DefaultEmergencyShortcutPreferenceController extends DefaultAppShortcutPreferenceControllerBase { private static final String KEY = "default_emergency_app"; public DefaultEmergencyShortcutPreferenceController(Context context, String packageName) { - super(context, KEY, packageName); - } - - @Override - protected boolean hasAppCapability() { - return DefaultEmergencyPreferenceController.hasEmergencyPreference(mPackageName, mContext); + super(context, KEY, RoleManager.ROLE_EMERGENCY, packageName); } - - @Override - protected boolean isDefaultApp() { - return DefaultEmergencyPreferenceController.isEmergencyDefault(mPackageName, mContext); - } - } diff --git a/src/com/android/settings/applications/appinfo/DefaultHomeShortcutPreferenceController.java b/src/com/android/settings/applications/appinfo/DefaultHomeShortcutPreferenceController.java index 4ae908326a..beb2d7e5b9 100644 --- a/src/com/android/settings/applications/appinfo/DefaultHomeShortcutPreferenceController.java +++ b/src/com/android/settings/applications/appinfo/DefaultHomeShortcutPreferenceController.java @@ -14,29 +14,15 @@ package com.android.settings.applications.appinfo; +import android.app.role.RoleManager; import android.content.Context; -import com.android.settings.applications.defaultapps.DefaultHomePreferenceController; -import com.android.settingslib.wrapper.PackageManagerWrapper; - public class DefaultHomeShortcutPreferenceController extends DefaultAppShortcutPreferenceControllerBase { private static final String KEY = "default_home"; public DefaultHomeShortcutPreferenceController(Context context, String packageName) { - super(context, KEY, packageName); - } - - @Override - protected boolean hasAppCapability() { - return DefaultHomePreferenceController.hasHomePreference(mPackageName, mContext); + super(context, KEY, RoleManager.ROLE_HOME, packageName); } - - @Override - protected boolean isDefaultApp() { - return DefaultHomePreferenceController.isHomeDefault(mPackageName, - new PackageManagerWrapper(mContext.getPackageManager())); - } - } diff --git a/src/com/android/settings/applications/appinfo/DefaultPhoneShortcutPreferenceController.java b/src/com/android/settings/applications/appinfo/DefaultPhoneShortcutPreferenceController.java index c968d5530c..c19c3676bb 100644 --- a/src/com/android/settings/applications/appinfo/DefaultPhoneShortcutPreferenceController.java +++ b/src/com/android/settings/applications/appinfo/DefaultPhoneShortcutPreferenceController.java @@ -14,27 +14,15 @@ package com.android.settings.applications.appinfo; +import android.app.role.RoleManager; import android.content.Context; -import com.android.settings.applications.defaultapps.DefaultPhonePreferenceController; - public class DefaultPhoneShortcutPreferenceController extends DefaultAppShortcutPreferenceControllerBase { private static final String KEY = "default_phone_app"; public DefaultPhoneShortcutPreferenceController(Context context, String packageName) { - super(context, KEY, packageName); - } - - @Override - protected boolean hasAppCapability() { - return DefaultPhonePreferenceController.hasPhonePreference(mPackageName, mContext); + super(context, KEY, RoleManager.ROLE_DIALER, packageName); } - - @Override - protected boolean isDefaultApp() { - return DefaultPhonePreferenceController.isPhoneDefault(mPackageName, mContext); - } - } diff --git a/src/com/android/settings/applications/appinfo/DefaultSmsShortcutPreferenceController.java b/src/com/android/settings/applications/appinfo/DefaultSmsShortcutPreferenceController.java index cf8b446b58..1b8393e50e 100644 --- a/src/com/android/settings/applications/appinfo/DefaultSmsShortcutPreferenceController.java +++ b/src/com/android/settings/applications/appinfo/DefaultSmsShortcutPreferenceController.java @@ -14,27 +14,15 @@ package com.android.settings.applications.appinfo; +import android.app.role.RoleManager; import android.content.Context; -import com.android.settings.applications.defaultapps.DefaultSmsPreferenceController; - public class DefaultSmsShortcutPreferenceController extends DefaultAppShortcutPreferenceControllerBase { private static final String KEY = "default_sms_app"; public DefaultSmsShortcutPreferenceController(Context context, String packageName) { - super(context, KEY, packageName); - } - - @Override - protected boolean hasAppCapability() { - return DefaultSmsPreferenceController.hasSmsPreference(mPackageName, mContext); + super(context, KEY, RoleManager.ROLE_SMS, packageName); } - - @Override - protected boolean isDefaultApp() { - return DefaultSmsPreferenceController.isSmsDefault(mPackageName, mContext); - } - } diff --git a/src/com/android/settings/applications/appinfo/DrawOverlayDetails.java b/src/com/android/settings/applications/appinfo/DrawOverlayDetails.java index 9efa998727..0f90c69c9a 100644 --- a/src/com/android/settings/applications/appinfo/DrawOverlayDetails.java +++ b/src/com/android/settings/applications/appinfo/DrawOverlayDetails.java @@ -15,35 +15,30 @@ */ package com.android.settings.applications.appinfo; -import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; - -import android.app.AlertDialog; import android.app.AppOpsManager; -import android.content.ActivityNotFoundException; +import android.app.settings.SettingsEnums; import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; import android.os.Bundle; -import android.os.UserHandle; -import android.provider.Settings; -import androidx.preference.SwitchPreference; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; import androidx.preference.Preference; import androidx.preference.Preference.OnPreferenceChangeListener; import androidx.preference.Preference.OnPreferenceClickListener; -import android.util.Log; -import android.view.Window; -import android.view.WindowManager; +import androidx.preference.SwitchPreference; -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; +import com.android.settings.Utils; import com.android.settings.applications.AppInfoWithHeader; import com.android.settings.applications.AppStateAppOpsBridge.PermissionState; import com.android.settings.applications.AppStateOverlayBridge; import com.android.settings.applications.AppStateOverlayBridge.OverlayState; import com.android.settings.overlay.FeatureFactory; import com.android.settingslib.applications.ApplicationsState.AppEntry; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; public class DrawOverlayDetails extends AppInfoWithHeader implements OnPreferenceChangeListener, OnPreferenceClickListener { @@ -51,16 +46,11 @@ public class DrawOverlayDetails extends AppInfoWithHeader implements OnPreferenc private static final String KEY_APP_OPS_SETTINGS_SWITCH = "app_ops_settings_switch"; private static final String LOG_TAG = "DrawOverlayDetails"; - private static final int [] APP_OPS_OP_CODE = { - AppOpsManager.OP_SYSTEM_ALERT_WINDOW - }; - // Use a bridge to get the overlay details but don't initialize it to connect with all state. // TODO: Break out this functionality into its own class. private AppStateOverlayBridge mOverlayBridge; private AppOpsManager mAppOpsManager; private SwitchPreference mSwitchPref; - private Intent mSettingsIntent; private OverlayState mOverlayState; @Override @@ -71,30 +61,29 @@ public class DrawOverlayDetails extends AppInfoWithHeader implements OnPreferenc mOverlayBridge = new AppStateOverlayBridge(context, mState, null); mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); + if (!Utils.isSystemAlertWindowEnabled(context)) { + mPackageInfo = null; + return; + } + // find preferences addPreferencesFromResource(R.xml.draw_overlay_permissions_details); - mSwitchPref = (SwitchPreference) findPreference(KEY_APP_OPS_SETTINGS_SWITCH); + mSwitchPref = findPreference(KEY_APP_OPS_SETTINGS_SWITCH); // install event listeners mSwitchPref.setOnPreferenceChangeListener(this); - - mSettingsIntent = new Intent(Intent.ACTION_MAIN) - .setAction(Settings.ACTION_MANAGE_OVERLAY_PERMISSION); } + // Override here so we don't have an empty screen @Override - public void onResume() { - super.onResume(); - getActivity().getWindow().addPrivateFlags(PRIVATE_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); - } - - @Override - public void onPause() { - super.onPause(); - Window window = getActivity().getWindow(); - WindowManager.LayoutParams attrs = window.getAttributes(); - attrs.privateFlags &= ~PRIVATE_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; - window.setAttributes(attrs); + public View onCreateView(LayoutInflater inflater, + ViewGroup container, + Bundle savedInstanceState) { + // if we don't have a package info, show a page saying this is unsupported + if (mPackageInfo == null) { + return inflater.inflate(R.layout.manage_applications_apps_unsupported, null); + } + return super.onCreateView(inflater, container, savedInstanceState); } @Override @@ -124,19 +113,29 @@ public class DrawOverlayDetails extends AppInfoWithHeader implements OnPreferenc logSpecialPermissionChange(newState, mPackageName); mAppOpsManager.setMode(AppOpsManager.OP_SYSTEM_ALERT_WINDOW, mPackageInfo.applicationInfo.uid, mPackageName, newState - ? AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_ERRORED); + ? AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_ERRORED); } @VisibleForTesting void logSpecialPermissionChange(boolean newState, String packageName) { - int logCategory = newState ? MetricsEvent.APP_SPECIAL_PERMISSION_APPDRAW_ALLOW - : MetricsEvent.APP_SPECIAL_PERMISSION_APPDRAW_DENY; - FeatureFactory.getFactory(getContext()) - .getMetricsFeatureProvider().action(getContext(), logCategory, packageName); + int logCategory = newState ? SettingsEnums.APP_SPECIAL_PERMISSION_APPDRAW_ALLOW + : SettingsEnums.APP_SPECIAL_PERMISSION_APPDRAW_DENY; + final MetricsFeatureProvider metricsFeatureProvider = + FeatureFactory.getFactory(getContext()).getMetricsFeatureProvider(); + metricsFeatureProvider.action( + metricsFeatureProvider.getAttribution(getActivity()), + logCategory, + getMetricsCategory(), + packageName, + 0); } @Override protected boolean refreshUi() { + if (mPackageInfo == null) { + return true; + } + mOverlayState = mOverlayBridge.getOverlayInfo(mPackageName, mPackageInfo.applicationInfo.uid); @@ -145,9 +144,6 @@ public class DrawOverlayDetails extends AppInfoWithHeader implements OnPreferenc // you cannot ask a user to grant you a permission you did not have! mSwitchPref.setEnabled(mOverlayState.permissionDeclared && mOverlayState.controlEnabled); - ResolveInfo resolveInfo = mPm.resolveActivityAsUser(mSettingsIntent, - PackageManager.GET_META_DATA, mUserId); - return true; } @@ -158,7 +154,7 @@ public class DrawOverlayDetails extends AppInfoWithHeader implements OnPreferenc @Override public int getMetricsCategory() { - return MetricsEvent.SYSTEM_ALERT_WINDOW_APPS; + return SettingsEnums.SYSTEM_ALERT_WINDOW_APPS; } public static CharSequence getSummary(Context context, AppEntry entry) { @@ -177,6 +173,7 @@ public class DrawOverlayDetails extends AppInfoWithHeader implements OnPreferenc public static CharSequence getSummary(Context context, OverlayState overlayState) { return context.getString(overlayState.isPermissible() ? - R.string.app_permission_summary_allowed : R.string.app_permission_summary_not_allowed); + R.string.app_permission_summary_allowed + : R.string.app_permission_summary_not_allowed); } } diff --git a/src/com/android/settings/applications/appinfo/ExternalSourceDetailPreferenceController.java b/src/com/android/settings/applications/appinfo/ExternalSourceDetailPreferenceController.java index df8b86298b..ec2508f107 100644 --- a/src/com/android/settings/applications/appinfo/ExternalSourceDetailPreferenceController.java +++ b/src/com/android/settings/applications/appinfo/ExternalSourceDetailPreferenceController.java @@ -19,6 +19,7 @@ package com.android.settings.applications.appinfo; import android.content.Context; import android.content.pm.PackageInfo; import android.os.UserManager; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; diff --git a/src/com/android/settings/applications/appinfo/ExternalSourcesDetails.java b/src/com/android/settings/applications/appinfo/ExternalSourcesDetails.java index 0c06e08177..fe1d81c17c 100644 --- a/src/com/android/settings/applications/appinfo/ExternalSourcesDetails.java +++ b/src/com/android/settings/applications/appinfo/ExternalSourcesDetails.java @@ -18,16 +18,19 @@ package com.android.settings.applications.appinfo; import static android.app.Activity.RESULT_CANCELED; import static android.app.Activity.RESULT_OK; -import android.app.AlertDialog; +import android.app.ActivityManager; import android.app.AppOpsManager; +import android.app.settings.SettingsEnums; import android.content.Context; import android.os.Bundle; import android.os.UserHandle; import android.os.UserManager; + +import androidx.appcompat.app.AlertDialog; import androidx.preference.Preference; import androidx.preference.Preference.OnPreferenceChangeListener; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.internal.annotations.VisibleForTesting; import com.android.settings.R; import com.android.settings.Settings; import com.android.settings.applications.AppInfoWithHeader; @@ -43,6 +46,7 @@ public class ExternalSourcesDetails extends AppInfoWithHeader private AppStateInstallAppsBridge mAppBridge; private AppOpsManager mAppOpsManager; + private ActivityManager mActivityManager; private UserManager mUserManager; private RestrictedSwitchPreference mSwitchPref; private InstallAppsState mInstallAppsState; @@ -54,6 +58,7 @@ public class ExternalSourcesDetails extends AppInfoWithHeader final Context context = getActivity(); mAppBridge = new AppStateInstallAppsBridge(context, mState, null); mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); + mActivityManager = context.getSystemService(ActivityManager.class); mUserManager = UserManager.get(context); addPreferencesFromResource(R.xml.external_sources_details); @@ -79,30 +84,40 @@ public class ExternalSourcesDetails extends AppInfoWithHeader } public static CharSequence getPreferenceSummary(Context context, AppEntry entry) { + final UserHandle userHandle = UserHandle.getUserHandleForUid(entry.info.uid); final UserManager um = UserManager.get(context); final int userRestrictionSource = um.getUserRestrictionSource( - UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES, - UserHandle.getUserHandleForUid(entry.info.uid)); - switch (userRestrictionSource) { - case UserManager.RESTRICTION_SOURCE_DEVICE_OWNER: - case UserManager.RESTRICTION_SOURCE_PROFILE_OWNER: - return context.getString(R.string.disabled_by_admin); - case UserManager.RESTRICTION_SOURCE_SYSTEM: - return context.getString(R.string.disabled); + UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES, userHandle) + | um.getUserRestrictionSource( + UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY, + userHandle); + if ((userRestrictionSource & UserManager.RESTRICTION_SOURCE_SYSTEM) != 0) { + return context.getString(R.string.disabled_by_admin); + } else if (userRestrictionSource != 0) { + return context.getString(R.string.disabled); } - final InstallAppsState appsState = new AppStateInstallAppsBridge(context, null, null) .createInstallAppsStateFor(entry.info.packageName, entry.info.uid); - return context.getString(appsState.canInstallApps() ? R.string.app_permission_summary_allowed : R.string.app_permission_summary_not_allowed); } - private void setCanInstallApps(boolean newState) { + @VisibleForTesting + void setCanInstallApps(boolean newState) { mAppOpsManager.setMode(AppOpsManager.OP_REQUEST_INSTALL_PACKAGES, mPackageInfo.applicationInfo.uid, mPackageName, newState ? AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_ERRORED); + if (!newState) { + killApp(mPackageInfo.applicationInfo.uid); + } + } + + private void killApp(int uid) { + if (UserHandle.isCore(uid)) { + return; + } + mActivityManager.killUid(uid, "User denied OP_REQUEST_INSTALL_PACKAGES"); } @Override @@ -118,6 +133,10 @@ public class ExternalSourcesDetails extends AppInfoWithHeader return true; } mSwitchPref.checkRestrictionAndSetDisabled(UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES); + if (!mSwitchPref.isDisabledByAdmin()) { + mSwitchPref.checkRestrictionAndSetDisabled( + UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY); + } if (mSwitchPref.isDisabledByAdmin()) { return true; } @@ -139,6 +158,6 @@ public class ExternalSourcesDetails extends AppInfoWithHeader @Override public int getMetricsCategory() { - return MetricsEvent.MANAGE_EXTERNAL_SOURCES; + return SettingsEnums.MANAGE_EXTERNAL_SOURCES; } } diff --git a/src/com/android/settings/applications/appinfo/InstantAppButtonDialogFragment.java b/src/com/android/settings/applications/appinfo/InstantAppButtonDialogFragment.java new file mode 100644 index 0000000000..6376d521be --- /dev/null +++ b/src/com/android/settings/applications/appinfo/InstantAppButtonDialogFragment.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2018 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.applications.appinfo; + +import android.app.Dialog; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.DialogInterface; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.os.UserHandle; + +import androidx.appcompat.app.AlertDialog; + +import com.android.settings.R; +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; +import com.android.settings.overlay.FeatureFactory; + +/** + * Fragment to show the dialog for clearing the instant app. + */ +public class InstantAppButtonDialogFragment extends InstrumentedDialogFragment implements + DialogInterface.OnClickListener { + + private static final String ARG_PACKAGE_NAME = "packageName"; + + private String mPackageName; + + public static InstantAppButtonDialogFragment newInstance(String packageName) { + final InstantAppButtonDialogFragment dialogFragment = new InstantAppButtonDialogFragment(); + final Bundle args = new Bundle(1); + args.putString(ARG_PACKAGE_NAME, packageName); + dialogFragment.setArguments(args); + return dialogFragment; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.DIALOG_APP_INFO_ACTION; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Bundle arguments = getArguments(); + mPackageName = arguments.getString(ARG_PACKAGE_NAME); + return createDialog(); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + final Context context = getContext(); + final PackageManager packageManager = context.getPackageManager(); + FeatureFactory.getFactory(context).getMetricsFeatureProvider() + .action(context, SettingsEnums.ACTION_SETTINGS_CLEAR_INSTANT_APP, mPackageName); + packageManager.deletePackageAsUser(mPackageName, null, 0, UserHandle.myUserId()); + } + + private AlertDialog createDialog() { + AlertDialog confirmDialog = new AlertDialog.Builder(getContext()) + .setPositiveButton(R.string.clear_instant_app_data, this) + .setNegativeButton(R.string.cancel, null) + .setTitle(R.string.clear_instant_app_data) + .setMessage(R.string.clear_instant_app_confirmation) + .create(); + return confirmDialog; + } + +} + diff --git a/src/com/android/settings/applications/appinfo/InstantAppButtonsPreferenceController.java b/src/com/android/settings/applications/appinfo/InstantAppButtonsPreferenceController.java index eb5e2ebd92..21e641d779 100644 --- a/src/com/android/settings/applications/appinfo/InstantAppButtonsPreferenceController.java +++ b/src/com/android/settings/applications/appinfo/InstantAppButtonsPreferenceController.java @@ -16,17 +16,12 @@ package com.android.settings.applications.appinfo; -import android.app.AlertDialog; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.net.Uri; import android.os.Bundle; -import android.os.UserHandle; -import androidx.annotation.VisibleForTesting; -import androidx.preference.PreferenceScreen; import android.text.TextUtils; import android.view.Menu; import android.view.MenuInflater; @@ -34,32 +29,30 @@ import android.view.MenuItem; import android.view.View; import android.widget.Button; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.fragment.app.DialogFragment; +import androidx.preference.PreferenceScreen; + import com.android.settings.R; import com.android.settings.applications.AppStoreUtil; -import com.android.settings.applications.LayoutPreference; import com.android.settings.core.BasePreferenceController; -import com.android.settings.overlay.FeatureFactory; import com.android.settingslib.applications.AppUtils; import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnCreateOptionsMenu; import com.android.settingslib.core.lifecycle.events.OnOptionsItemSelected; import com.android.settingslib.core.lifecycle.events.OnPrepareOptionsMenu; -import com.android.settingslib.wrapper.PackageManagerWrapper; +import com.android.settingslib.widget.LayoutPreference; import java.util.List; public class InstantAppButtonsPreferenceController extends BasePreferenceController implements - LifecycleObserver, OnCreateOptionsMenu, OnPrepareOptionsMenu, OnOptionsItemSelected, - DialogInterface.OnClickListener { + LifecycleObserver, OnCreateOptionsMenu, OnPrepareOptionsMenu, OnOptionsItemSelected { private static final String KEY_INSTANT_APP_BUTTONS = "instant_app_buttons"; private static final String META_DATA_DEFAULT_URI = "default-url"; private final AppInfoDashboardFragment mParent; private final String mPackageName; - private final PackageManagerWrapper mPackageManagerWrapper; private String mLaunchUri; private LayoutPreference mPreference; private MenuItem mInstallMenu; @@ -69,7 +62,6 @@ public class InstantAppButtonsPreferenceController extends BasePreferenceControl super(context, KEY_INSTANT_APP_BUTTONS); mParent = parent; mPackageName = packageName; - mPackageManagerWrapper = new PackageManagerWrapper(context.getPackageManager()); mLaunchUri = getDefaultLaunchUri(); if (lifecycle != null) { lifecycle.addObserver(this); @@ -85,7 +77,7 @@ public class InstantAppButtonsPreferenceController extends BasePreferenceControl @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - mPreference = (LayoutPreference) screen.findPreference(KEY_INSTANT_APP_BUTTONS); + mPreference = screen.findPreference(KEY_INSTANT_APP_BUTTONS); initButtons(mPreference.findViewById(R.id.instant_app_button_container)); } @@ -93,7 +85,7 @@ public class InstantAppButtonsPreferenceController extends BasePreferenceControl public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { if (!TextUtils.isEmpty(mLaunchUri)) { menu.add(0, AppInfoDashboardFragment.INSTALL_INSTANT_APP_MENU, 2, R.string.install_text) - .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); + .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); } } @@ -119,27 +111,6 @@ public class InstantAppButtonsPreferenceController extends BasePreferenceControl } } - @Override - public void onClick(DialogInterface dialog, int which) { - FeatureFactory.getFactory(mContext).getMetricsFeatureProvider() - .action(mContext, MetricsEvent.ACTION_SETTINGS_CLEAR_INSTANT_APP, mPackageName); - mPackageManagerWrapper.deletePackageAsUser( - mPackageName, null, 0, UserHandle.myUserId()); - } - - AlertDialog createDialog(int id) { - if (id == AppInfoDashboardFragment.DLG_CLEAR_INSTANT_APP) { - AlertDialog confirmDialog = new AlertDialog.Builder(mContext) - .setPositiveButton(R.string.clear_instant_app_data, this) - .setNegativeButton(R.string.cancel, null) - .setTitle(R.string.clear_instant_app_data) - .setMessage(mContext.getString(R.string.clear_instant_app_confirmation)) - .create(); - return confirmDialog; - } - return null; - } - private void initButtons(View view) { final Button installButton = view.findViewById(R.id.install); final Button clearDataButton = view.findViewById(R.id.clear_data); @@ -161,8 +132,14 @@ public class InstantAppButtonsPreferenceController extends BasePreferenceControl installButton.setEnabled(false); } } - clearDataButton.setOnClickListener( - v -> mParent.showDialogInner(mParent.DLG_CLEAR_INSTANT_APP, 0)); + clearDataButton.setOnClickListener(v -> showDialog()); + } + + private void showDialog() { + final DialogFragment newFragment = + InstantAppButtonDialogFragment.newInstance(mPackageName); + newFragment.setTargetFragment(mParent, 0); + newFragment.show(mParent.getFragmentManager(), KEY_INSTANT_APP_BUTTONS); } private String getDefaultLaunchUri() { @@ -171,7 +148,7 @@ public class InstantAppButtonsPreferenceController extends BasePreferenceControl intent.addCategory(Intent.CATEGORY_LAUNCHER); intent.setPackage(mPackageName); final List<ResolveInfo> infos = manager.queryIntentActivities( - intent, PackageManager.GET_META_DATA | PackageManager.MATCH_INSTANT); + intent, PackageManager.GET_META_DATA | PackageManager.MATCH_INSTANT); for (ResolveInfo info : infos) { final Bundle metaData = info.activityInfo.metaData; if (metaData != null) { diff --git a/src/com/android/settings/applications/appinfo/InstantAppDomainsPreferenceController.java b/src/com/android/settings/applications/appinfo/InstantAppDomainsPreferenceController.java index c0ba33f2d0..cbb805f355 100644 --- a/src/com/android/settings/applications/appinfo/InstantAppDomainsPreferenceController.java +++ b/src/com/android/settings/applications/appinfo/InstantAppDomainsPreferenceController.java @@ -18,6 +18,7 @@ package com.android.settings.applications.appinfo; import android.content.Context; import android.content.pm.PackageManager; + import androidx.preference.Preference; import com.android.settings.Utils; diff --git a/src/com/android/settings/applications/appinfo/TimeSpentInAppPreferenceController.java b/src/com/android/settings/applications/appinfo/TimeSpentInAppPreferenceController.java index 6a39077024..b1bbd06e9d 100644 --- a/src/com/android/settings/applications/appinfo/TimeSpentInAppPreferenceController.java +++ b/src/com/android/settings/applications/appinfo/TimeSpentInAppPreferenceController.java @@ -21,29 +21,34 @@ import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.provider.Settings; +import android.text.TextUtils; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.text.TextUtils; +import com.android.settings.applications.ApplicationFeatureProvider; import com.android.settings.core.BasePreferenceController; +import com.android.settings.overlay.FeatureFactory; import java.util.List; public class TimeSpentInAppPreferenceController extends BasePreferenceController { @VisibleForTesting - static final Intent SEE_TIME_IN_APP_TEMPLATE = - new Intent("com.android.settings.action.TIME_SPENT_IN_APP"); + static final Intent SEE_TIME_IN_APP_TEMPLATE = new Intent(Settings.ACTION_APP_USAGE_SETTINGS); private final PackageManager mPackageManager; - + private final ApplicationFeatureProvider mAppFeatureProvider; private Intent mIntent; private String mPackageName; public TimeSpentInAppPreferenceController(Context context, String preferenceKey) { super(context, preferenceKey); mPackageManager = context.getPackageManager(); + mAppFeatureProvider = FeatureFactory.getFactory(context) + .getApplicationFeatureProvider(context); } public void setPackageName(String packageName) { @@ -79,6 +84,11 @@ public class TimeSpentInAppPreferenceController extends BasePreferenceController } } + @Override + public CharSequence getSummary() { + return mAppFeatureProvider.getTimeSpentInApp(mPackageName); + } + private boolean isSystemApp(ResolveInfo info) { return info != null && info.activityInfo != null diff --git a/src/com/android/settings/applications/appinfo/WriteSettingsDetails.java b/src/com/android/settings/applications/appinfo/WriteSettingsDetails.java index 617154797f..f8c7ac5ad5 100644 --- a/src/com/android/settings/applications/appinfo/WriteSettingsDetails.java +++ b/src/com/android/settings/applications/appinfo/WriteSettingsDetails.java @@ -15,23 +15,21 @@ */ package com.android.settings.applications.appinfo; -import android.app.AlertDialog; import android.app.AppOpsManager; -import android.content.ActivityNotFoundException; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.os.Bundle; -import android.os.UserHandle; import android.provider.Settings; -import androidx.preference.SwitchPreference; + +import androidx.appcompat.app.AlertDialog; import androidx.preference.Preference; import androidx.preference.Preference.OnPreferenceChangeListener; import androidx.preference.Preference.OnPreferenceClickListener; -import android.util.Log; +import androidx.preference.SwitchPreference; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.applications.AppInfoWithHeader; import com.android.settings.applications.AppStateAppOpsBridge.PermissionState; @@ -103,8 +101,8 @@ public class WriteSettingsDetails extends AppInfoWithHeader implements OnPrefere } void logSpecialPermissionChange(boolean newState, String packageName) { - int logCategory = newState ? MetricsEvent.APP_SPECIAL_PERMISSION_SETTINGS_CHANGE_ALLOW - : MetricsEvent.APP_SPECIAL_PERMISSION_SETTINGS_CHANGE_DENY; + int logCategory = newState ? SettingsEnums.APP_SPECIAL_PERMISSION_SETTINGS_CHANGE_ALLOW + : SettingsEnums.APP_SPECIAL_PERMISSION_SETTINGS_CHANGE_DENY; FeatureFactory.getFactory(getContext()).getMetricsFeatureProvider().action(getContext(), logCategory, packageName); } @@ -141,7 +139,7 @@ public class WriteSettingsDetails extends AppInfoWithHeader implements OnPrefere @Override public int getMetricsCategory() { - return MetricsEvent.SYSTEM_ALERT_WINDOW_APPS; + return SettingsEnums.SYSTEM_ALERT_WINDOW_APPS; } public static CharSequence getSummary(Context context, AppEntry entry) { diff --git a/src/com/android/settings/applications/appops/AppOpsCategory.java b/src/com/android/settings/applications/appops/AppOpsCategory.java index b506ce0ad7..57d7dc4c49 100644 --- a/src/com/android/settings/applications/appops/AppOpsCategory.java +++ b/src/com/android/settings/applications/appops/AppOpsCategory.java @@ -17,14 +17,10 @@ package com.android.settings.applications.appops; import android.app.AppOpsManager; -import android.app.ListFragment; -import android.app.LoaderManager; -import android.content.AsyncTaskLoader; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.Loader; import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.content.res.Resources; @@ -38,6 +34,11 @@ import android.widget.ListView; import android.widget.Switch; import android.widget.TextView; +import androidx.fragment.app.ListFragment; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.AsyncTaskLoader; +import androidx.loader.content.Loader; + import com.android.settings.R; import com.android.settings.applications.appops.AppOpsState.AppOpEntry; diff --git a/src/com/android/settings/applications/appops/AppOpsState.java b/src/com/android/settings/applications/appops/AppOpsState.java index 2686b8c144..3c8d647938 100644 --- a/src/com/android/settings/applications/appops/AppOpsState.java +++ b/src/com/android/settings/applications/appops/AppOpsState.java @@ -618,7 +618,7 @@ public class AppOpsState { } AppOpsManager.OpEntry opEntry = new AppOpsManager.OpEntry( - permOps.get(k), AppOpsManager.MODE_ALLOWED, 0, 0, 0, -1, null); + permOps.get(k), AppOpsManager.MODE_ALLOWED); dummyOps.add(opEntry); addOp(entries, pkgOps, appEntry, opEntry, packageName == null, packageName == null ? 0 : opToOrder[opEntry.getOp()]); diff --git a/src/com/android/settings/applications/appops/BackgroundCheckSummary.java b/src/com/android/settings/applications/appops/BackgroundCheckSummary.java index d9db9aa6c2..58f962ab71 100644 --- a/src/com/android/settings/applications/appops/BackgroundCheckSummary.java +++ b/src/com/android/settings/applications/appops/BackgroundCheckSummary.java @@ -17,16 +17,17 @@ package com.android.settings.applications.appops; import android.annotation.Nullable; -import android.app.FragmentTransaction; +import android.app.settings.SettingsEnums; import android.os.Bundle; import android.preference.PreferenceFrameLayout; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.settings.core.InstrumentedPreferenceFragment; +import androidx.fragment.app.FragmentTransaction; + import com.android.settings.R; +import com.android.settings.core.InstrumentedPreferenceFragment; public class BackgroundCheckSummary extends InstrumentedPreferenceFragment { // layout inflater object used to inflate views @@ -34,7 +35,7 @@ public class BackgroundCheckSummary extends InstrumentedPreferenceFragment { @Override public int getMetricsCategory() { - return MetricsEvent.BACKGROUND_CHECK_SUMMARY; + return SettingsEnums.BACKGROUND_CHECK_SUMMARY; } @Override diff --git a/src/com/android/settings/applications/assist/AssistContextPreferenceController.java b/src/com/android/settings/applications/assist/AssistContextPreferenceController.java index 2ac51e7264..3e5c4ec528 100644 --- a/src/com/android/settings/applications/assist/AssistContextPreferenceController.java +++ b/src/com/android/settings/applications/assist/AssistContextPreferenceController.java @@ -20,6 +20,7 @@ import android.content.Context; import android.net.Uri; import android.os.UserHandle; import android.provider.Settings; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; import androidx.preference.TwoStatePreference; diff --git a/src/com/android/settings/applications/assist/AssistFlashScreenPreferenceController.java b/src/com/android/settings/applications/assist/AssistFlashScreenPreferenceController.java index ccdacedc5a..1880accb6d 100644 --- a/src/com/android/settings/applications/assist/AssistFlashScreenPreferenceController.java +++ b/src/com/android/settings/applications/assist/AssistFlashScreenPreferenceController.java @@ -21,11 +21,12 @@ import android.content.Context; import android.net.Uri; import android.os.UserHandle; import android.provider.Settings; + +import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; import androidx.preference.TwoStatePreference; -import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.AssistUtils; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.core.AbstractPreferenceController; diff --git a/src/com/android/settings/applications/assist/AssistScreenshotPreferenceController.java b/src/com/android/settings/applications/assist/AssistScreenshotPreferenceController.java index 59425f392e..59479438c2 100644 --- a/src/com/android/settings/applications/assist/AssistScreenshotPreferenceController.java +++ b/src/com/android/settings/applications/assist/AssistScreenshotPreferenceController.java @@ -20,6 +20,7 @@ import android.content.Context; import android.net.Uri; import android.os.UserHandle; import android.provider.Settings; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; import androidx.preference.TwoStatePreference; diff --git a/src/com/android/settings/applications/assist/AssistSettingObserver.java b/src/com/android/settings/applications/assist/AssistSettingObserver.java index c5d028ec43..f0da694a3a 100644 --- a/src/com/android/settings/applications/assist/AssistSettingObserver.java +++ b/src/com/android/settings/applications/assist/AssistSettingObserver.java @@ -20,6 +20,7 @@ import android.content.ContentResolver; import android.database.ContentObserver; import android.net.Uri; import android.provider.Settings; + import androidx.annotation.MainThread; import com.android.settingslib.utils.ThreadUtils; diff --git a/src/com/android/settings/applications/assist/DefaultAssistPicker.java b/src/com/android/settings/applications/assist/DefaultAssistPicker.java deleted file mode 100644 index d54e6d1a3f..0000000000 --- a/src/com/android/settings/applications/assist/DefaultAssistPicker.java +++ /dev/null @@ -1,239 +0,0 @@ -/* - * Copyright (C) 2017 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.applications.assist; - -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.provider.Settings; -import android.service.voice.VoiceInteractionService; -import android.service.voice.VoiceInteractionServiceInfo; -import android.speech.RecognitionService; -import android.text.TextUtils; -import android.util.Log; - -import com.android.internal.app.AssistUtils; -import com.android.internal.logging.nano.MetricsProto; -import com.android.settings.R; -import com.android.settings.applications.defaultapps.DefaultAppPickerFragment; - -import com.android.settingslib.applications.DefaultAppInfo; -import com.android.settingslib.widget.CandidateInfo; - -import java.util.ArrayList; -import java.util.List; - -public class DefaultAssistPicker extends DefaultAppPickerFragment { - - private static final String TAG = "DefaultAssistPicker"; - private static final Intent ASSIST_SERVICE_PROBE = - new Intent(VoiceInteractionService.SERVICE_INTERFACE); - private static final Intent ASSIST_ACTIVITY_PROBE = - new Intent(Intent.ACTION_ASSIST); - private final List<Info> mAvailableAssistants = new ArrayList<>(); - - private AssistUtils mAssistUtils; - - @Override - public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DEFAULT_ASSIST_PICKER; - } - - @Override - protected boolean shouldShowItemNone() { - return true; - } - - @Override - public void onAttach(Context context) { - super.onAttach(context); - mAssistUtils = new AssistUtils(context); - } - - @Override - protected int getPreferenceScreenResId() { - return R.xml.default_assist_settings; - } - - @Override - protected List<DefaultAppInfo> getCandidates() { - mAvailableAssistants.clear(); - addAssistServices(); - addAssistActivities(); - - final List<String> packages = new ArrayList<>(); - final List<DefaultAppInfo> candidates = new ArrayList<>(); - for (Info info : mAvailableAssistants) { - final String packageName = info.component.getPackageName(); - if (packages.contains(packageName)) { - // A service appears before an activity thus overrides it if from the same package. - continue; - } - packages.add(packageName); - candidates.add(new DefaultAppInfo(getContext(), mPm, mUserId, info.component)); - } - return candidates; - } - - @Override - protected String getDefaultKey() { - final ComponentName cn = getCurrentAssist(); - if (cn != null) { - return new DefaultAppInfo(getContext(), mPm, mUserId, cn).getKey(); - } - return null; - } - - @Override - protected String getConfirmationMessage(CandidateInfo appInfo) { - if (appInfo == null) { - return null; - } - return getContext().getString(R.string.assistant_security_warning, appInfo.loadLabel()); - } - - @Override - protected boolean setDefaultKey(String key) { - if (TextUtils.isEmpty(key)) { - setAssistNone(); - return true; - } - ComponentName cn = ComponentName.unflattenFromString(key); - final Info info = findAssistantByPackageName(cn.getPackageName()); - if (info == null) { - setAssistNone(); - return true; - } - - if (info.isVoiceInteractionService()) { - setAssistService(info); - } else { - setAssistActivity(info); - } - return true; - } - - public ComponentName getCurrentAssist() { - return mAssistUtils.getAssistComponentForUser(mUserId); - } - - private void addAssistServices() { - final PackageManager pm = mPm.getPackageManager(); - final List<ResolveInfo> services = pm.queryIntentServices( - ASSIST_SERVICE_PROBE, PackageManager.GET_META_DATA); - for (ResolveInfo resolveInfo : services) { - VoiceInteractionServiceInfo voiceInteractionServiceInfo = - new VoiceInteractionServiceInfo(pm, resolveInfo.serviceInfo); - if (!voiceInteractionServiceInfo.getSupportsAssist()) { - continue; - } - - mAvailableAssistants.add(new Info( - new ComponentName(resolveInfo.serviceInfo.packageName, - resolveInfo.serviceInfo.name), - voiceInteractionServiceInfo)); - } - } - - private void addAssistActivities() { - final PackageManager pm = mPm.getPackageManager(); - final List<ResolveInfo> activities = pm.queryIntentActivities( - ASSIST_ACTIVITY_PROBE, PackageManager.MATCH_DEFAULT_ONLY); - for (ResolveInfo resolveInfo : activities) { - mAvailableAssistants.add(new Info( - new ComponentName(resolveInfo.activityInfo.packageName, - resolveInfo.activityInfo.name))); - } - } - - private Info findAssistantByPackageName(String packageName) { - for (Info info : mAvailableAssistants) { - if (TextUtils.equals(info.component.getPackageName(), packageName)) { - return info; - } - } - return null; - } - - private void setAssistNone() { - Settings.Secure.putString(getContext().getContentResolver(), - Settings.Secure.ASSISTANT, ""); - Settings.Secure.putString(getContext().getContentResolver(), - Settings.Secure.VOICE_INTERACTION_SERVICE, ""); - Settings.Secure.putString(getContext().getContentResolver(), - Settings.Secure.VOICE_RECOGNITION_SERVICE, getDefaultRecognizer()); - } - - private void setAssistService(Info serviceInfo) { - final String serviceComponentName = serviceInfo.component. - flattenToShortString(); - final String serviceRecognizerName = new ComponentName( - serviceInfo.component.getPackageName(), - serviceInfo.voiceInteractionServiceInfo.getRecognitionService()) - .flattenToShortString(); - - Settings.Secure.putString(getContext().getContentResolver(), - Settings.Secure.ASSISTANT, serviceComponentName); - Settings.Secure.putString(getContext().getContentResolver(), - Settings.Secure.VOICE_INTERACTION_SERVICE, serviceComponentName); - Settings.Secure.putString(getContext().getContentResolver(), - Settings.Secure.VOICE_RECOGNITION_SERVICE, serviceRecognizerName); - } - - private void setAssistActivity(Info activityInfo) { - Settings.Secure.putString(getContext().getContentResolver(), - Settings.Secure.ASSISTANT, activityInfo.component.flattenToShortString()); - Settings.Secure.putString(getContext().getContentResolver(), - Settings.Secure.VOICE_INTERACTION_SERVICE, ""); - Settings.Secure.putString(getContext().getContentResolver(), - Settings.Secure.VOICE_RECOGNITION_SERVICE, getDefaultRecognizer()); - } - - private String getDefaultRecognizer() { - final ResolveInfo resolveInfo = mPm.getPackageManager().resolveService( - new Intent(RecognitionService.SERVICE_INTERFACE), - PackageManager.GET_META_DATA); - if (resolveInfo == null || resolveInfo.serviceInfo == null) { - Log.w(TAG, "Unable to resolve default voice recognition service."); - return ""; - } - - return new ComponentName(resolveInfo.serviceInfo.packageName, - resolveInfo.serviceInfo.name).flattenToShortString(); - } - - static class Info { - public final ComponentName component; - public final VoiceInteractionServiceInfo voiceInteractionServiceInfo; - - Info(ComponentName component) { - this.component = component; - this.voiceInteractionServiceInfo = null; - } - - Info(ComponentName component, VoiceInteractionServiceInfo voiceInteractionServiceInfo) { - this.component = component; - this.voiceInteractionServiceInfo = voiceInteractionServiceInfo; - } - - public boolean isVoiceInteractionService() { - return voiceInteractionServiceInfo != null; - } - } -} diff --git a/src/com/android/settings/applications/assist/DefaultAssistPreferenceController.java b/src/com/android/settings/applications/assist/DefaultAssistPreferenceController.java index 6f44ff4f70..b2e36445a9 100644 --- a/src/com/android/settings/applications/assist/DefaultAssistPreferenceController.java +++ b/src/com/android/settings/applications/assist/DefaultAssistPreferenceController.java @@ -16,14 +16,19 @@ package com.android.settings.applications.assist; +import android.app.role.RoleManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.provider.Settings; import android.service.voice.VoiceInteractionService; import android.service.voice.VoiceInteractionServiceInfo; +import android.text.TextUtils; + import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; import com.android.internal.app.AssistUtils; import com.android.settings.R; @@ -37,6 +42,7 @@ public class DefaultAssistPreferenceController extends DefaultAppPreferenceContr private final AssistUtils mAssistUtils; private final boolean mShowSetting; private final String mPrefKey; + private final Intent mIntent; public DefaultAssistPreferenceController(Context context, String prefKey, boolean showSetting) { @@ -44,6 +50,15 @@ public class DefaultAssistPreferenceController extends DefaultAppPreferenceContr mPrefKey = prefKey; mShowSetting = showSetting; mAssistUtils = new AssistUtils(context); + + final String packageName = mPackageManager.getPermissionControllerPackageName(); + if (packageName != null) { + mIntent = new Intent(Intent.ACTION_MANAGE_DEFAULT_APP) + .setPackage(packageName) + .putExtra(Intent.EXTRA_ROLE_NAME, RoleManager.ROLE_ASSISTANT); + } else { + mIntent = null; + } } @Override @@ -58,13 +73,12 @@ public class DefaultAssistPreferenceController extends DefaultAppPreferenceContr final Intent probe = new Intent(VoiceInteractionService.SERVICE_INTERFACE) .setPackage(cn.getPackageName()); - final PackageManager pm = mPackageManager.getPackageManager(); - final List<ResolveInfo> services = pm.queryIntentServices(probe, PackageManager - .GET_META_DATA); + final List<ResolveInfo> services = mPackageManager.queryIntentServices(probe, + PackageManager.GET_META_DATA); if (services == null || services.isEmpty()) { return null; } - final String activity = getAssistSettingsActivity(cn, services.get(0), pm); + final String activity = getAssistSettingsActivity(cn, services.get(0), mPackageManager); if (activity == null) { return null; } @@ -73,6 +87,17 @@ public class DefaultAssistPreferenceController extends DefaultAppPreferenceContr } @Override + public boolean handlePreferenceTreeClick(Preference preference) { + if (TextUtils.equals(preference.getKey(), "default_assist")) { + if (mIntent != null) { + mContext.startActivity(mIntent); + } + return true; + } + return false; + } + + @Override public boolean isAvailable() { return mContext.getResources().getBoolean(R.bool.config_show_assist_and_voice_input); } diff --git a/src/com/android/settings/applications/assist/DefaultVoiceInputPicker.java b/src/com/android/settings/applications/assist/DefaultVoiceInputPicker.java index 84c1f76819..e5953dbe72 100644 --- a/src/com/android/settings/applications/assist/DefaultVoiceInputPicker.java +++ b/src/com/android/settings/applications/assist/DefaultVoiceInputPicker.java @@ -16,18 +16,18 @@ package com.android.settings.applications.assist; +import android.app.settings.SettingsEnums; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; import android.provider.Settings; import android.text.TextUtils; import com.android.internal.app.AssistUtils; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.applications.defaultapps.DefaultAppPickerFragment; import com.android.settingslib.applications.DefaultAppInfo; -import com.android.settingslib.wrapper.PackageManagerWrapper; import java.util.ArrayList; import java.util.List; @@ -40,7 +40,7 @@ public class DefaultVoiceInputPicker extends DefaultAppPickerFragment { @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DEFAULT_VOICE_INPUT_PICKER; + return SettingsEnums.DEFAULT_VOICE_INPUT_PICKER; } @Override @@ -139,7 +139,7 @@ public class DefaultVoiceInputPicker extends DefaultAppPickerFragment { public VoiceInputHelper.BaseInfo mInfo; - public VoiceInputDefaultAppInfo(Context context, PackageManagerWrapper pm, int userId, + public VoiceInputDefaultAppInfo(Context context, PackageManager pm, int userId, VoiceInputHelper.BaseInfo info, boolean enabled) { super(context, pm, userId, info.componentName, null /* summary */, enabled); mInfo = info; diff --git a/src/com/android/settings/applications/assist/DefaultVoiceInputPreferenceController.java b/src/com/android/settings/applications/assist/DefaultVoiceInputPreferenceController.java index 15433676dd..1f8b9d1a5c 100644 --- a/src/com/android/settings/applications/assist/DefaultVoiceInputPreferenceController.java +++ b/src/com/android/settings/applications/assist/DefaultVoiceInputPreferenceController.java @@ -20,9 +20,10 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.net.Uri; +import android.text.TextUtils; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.text.TextUtils; import com.android.internal.app.AssistUtils; import com.android.settings.applications.defaultapps.DefaultAppPreferenceController; diff --git a/src/com/android/settings/applications/assist/ManageAssist.java b/src/com/android/settings/applications/assist/ManageAssist.java index 82db01ff3d..952a3ad896 100644 --- a/src/com/android/settings/applications/assist/ManageAssist.java +++ b/src/com/android/settings/applications/assist/ManageAssist.java @@ -16,10 +16,10 @@ package com.android.settings.applications.assist; +import android.app.settings.SettingsEnums; import android.content.Context; import android.provider.SearchIndexableResource; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.gestures.AssistGestureSettingsPreferenceController; @@ -27,6 +27,7 @@ import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.Arrays; @@ -35,6 +36,7 @@ import java.util.List; /** * Settings screen to manage everything about assist. */ +@SearchIndexable public class ManageAssist extends DashboardFragment { private static final String TAG = "ManageAssist"; @@ -52,12 +54,12 @@ public class ManageAssist extends DashboardFragment { @Override protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { - return buildPreferenceControllers(context, getLifecycle()); + return buildPreferenceControllers(context, getSettingsLifecycle()); } @Override public int getMetricsCategory() { - return MetricsEvent.APPLICATIONS_MANAGE_ASSIST; + return SettingsEnums.APPLICATIONS_MANAGE_ASSIST; } @Override diff --git a/src/com/android/settings/applications/autofill/AutofillPickerTrampolineActivity.java b/src/com/android/settings/applications/autofill/AutofillPickerTrampolineActivity.java index 1db3c82804..ee58bfea79 100644 --- a/src/com/android/settings/applications/autofill/AutofillPickerTrampolineActivity.java +++ b/src/com/android/settings/applications/autofill/AutofillPickerTrampolineActivity.java @@ -14,6 +14,7 @@ package com.android.settings.applications.autofill; import android.app.Activity; +import android.content.ComponentName; import android.content.Intent; import android.os.Bundle; import android.view.autofill.AutofillManager; @@ -33,19 +34,9 @@ public class AutofillPickerTrampolineActivity extends Activity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - // First check if the current user's service already belongs to the app... - final Intent intent = getIntent(); - final String packageName = intent.getData().getSchemeSpecificPart(); - final String currentService = DefaultAutofillPicker.getDefaultKey(this); - if (currentService != null && currentService.startsWith(packageName)) { - // ...and succeed right away if it does. - setResult(RESULT_OK); - finish(); - return; - } - - // Then check if the Autofill is available for the current user... final AutofillManager afm = getSystemService(AutofillManager.class); + + // First check if the Autofill is available for the current user... if (afm == null || !afm.hasAutofillFeature() || !afm.isAutofillSupported()) { // ... and fail right away if it is not. setResult(RESULT_CANCELED); @@ -53,6 +44,17 @@ public class AutofillPickerTrampolineActivity extends Activity { return; } + // Then check if the current user's service already belongs to the app... + final Intent intent = getIntent(); + final String packageName = intent.getData().getSchemeSpecificPart(); + final ComponentName currentService = afm.getAutofillServiceComponentName(); + if (currentService != null && currentService.getPackageName().equals(packageName)) { + // ...and succeed right away if it does. + setResult(RESULT_OK); + finish(); + return; + } + // Otherwise, go ahead and show the real UI... final Intent newIntent = new Intent(this, AutofillPickerActivity.class) .setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT) diff --git a/src/com/android/settings/applications/defaultapps/AutofillPicker.java b/src/com/android/settings/applications/defaultapps/AutofillPicker.java new file mode 100644 index 0000000000..fb9d6314ac --- /dev/null +++ b/src/com/android/settings/applications/defaultapps/AutofillPicker.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2018 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.applications.defaultapps; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.provider.SearchIndexableResource; + +import com.android.settings.R; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; +import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.search.SearchIndexable; + +import java.util.Arrays; +import java.util.List; + +@SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC) +public class AutofillPicker extends DashboardFragment { + private static final String TAG = "AutofillPicker"; + + @Override + public int getMetricsCategory() { + return SettingsEnums.DEFAULT_AUTOFILL_PICKER; + } + + @Override + protected String getLogTag() { + return TAG; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.default_autofill_picker_settings; + } + + @Override + protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { + return buildPreferenceControllers(context); + } + + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, + boolean enabled) { + SearchIndexableResource searchIndexableResource = + new SearchIndexableResource(context); + searchIndexableResource.xmlResId = R.xml.default_autofill_picker_settings; + return Arrays.asList(searchIndexableResource); + } + + @Override + public List<AbstractPreferenceController> getPreferenceControllers(Context + context) { + return buildPreferenceControllers(context); + } + }; + + private static List<AbstractPreferenceController> buildPreferenceControllers(Context context) { + return Arrays.asList( + new DefaultAutofillPreferenceController(context), + new DefaultWorkAutofillPreferenceController(context)); + } +} diff --git a/src/com/android/settings/applications/defaultapps/DefaultAppPickerFragment.java b/src/com/android/settings/applications/defaultapps/DefaultAppPickerFragment.java index ab81290d96..5d41ba7133 100644 --- a/src/com/android/settings/applications/defaultapps/DefaultAppPickerFragment.java +++ b/src/com/android/settings/applications/defaultapps/DefaultAppPickerFragment.java @@ -16,60 +16,64 @@ package com.android.settings.applications.defaultapps; -import android.app.Activity; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.DialogFragment; -import android.app.Fragment; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.DialogInterface; +import android.content.pm.PackageManager; import android.os.Bundle; import android.text.TextUtils; -import android.util.Pair; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; + import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; +import com.android.settings.fuelgauge.BatteryUtils; import com.android.settings.widget.RadioButtonPickerFragment; import com.android.settings.widget.RadioButtonPreference; import com.android.settingslib.applications.DefaultAppInfo; import com.android.settingslib.widget.CandidateInfo; -import com.android.settingslib.wrapper.PackageManagerWrapper; /** * A generic app picker fragment that shows a list of app as radio button group. */ public abstract class DefaultAppPickerFragment extends RadioButtonPickerFragment { - protected PackageManagerWrapper mPm; + protected PackageManager mPm; + protected BatteryUtils mBatteryUtils; @Override public void onAttach(Context context) { super.onAttach(context); - mPm = new PackageManagerWrapper(context.getPackageManager()); + mPm = context.getPackageManager(); + mBatteryUtils = BatteryUtils.getInstance(context); } @Override public void onRadioButtonClicked(RadioButtonPreference selected) { final String selectedKey = selected.getKey(); final CharSequence confirmationMessage = getConfirmationMessage(getCandidate(selectedKey)); - final Activity activity = getActivity(); + final FragmentActivity activity = getActivity(); if (TextUtils.isEmpty(confirmationMessage)) { super.onRadioButtonClicked(selected); } else if (activity != null) { final DialogFragment fragment = newConfirmationDialogFragment(selectedKey, confirmationMessage); - fragment.show(activity.getFragmentManager(), ConfirmationDialogFragment.TAG); + fragment.show(activity.getSupportFragmentManager(), ConfirmationDialogFragment.TAG); } } @Override protected void onRadioButtonConfirmed(String selectedKey) { - mMetricsFeatureProvider.action(getContext(), - MetricsEvent.ACTION_SETTINGS_UPDATE_DEFAULT_APP, + mMetricsFeatureProvider.action( + mMetricsFeatureProvider.getAttribution(getActivity()), + SettingsEnums.ACTION_SETTINGS_UPDATE_DEFAULT_APP, + getMetricsCategory(), selectedKey, - Pair.create(MetricsEvent.FIELD_CONTEXT, getMetricsCategory())); - + 0 /* value */); super.onRadioButtonConfirmed(selectedKey); } @@ -108,7 +112,7 @@ public abstract class DefaultAppPickerFragment extends RadioButtonPickerFragment @Override public int getMetricsCategory() { - return MetricsEvent.DEFAULT_APP_PICKER_CONFIRMATION_DIALOG; + return SettingsEnums.DEFAULT_APP_PICKER_CONFIRMATION_DIALOG; } /** diff --git a/src/com/android/settings/applications/defaultapps/DefaultAppPreferenceController.java b/src/com/android/settings/applications/defaultapps/DefaultAppPreferenceController.java index ec8111ba90..73d80a3436 100644 --- a/src/com/android/settings/applications/defaultapps/DefaultAppPreferenceController.java +++ b/src/com/android/settings/applications/defaultapps/DefaultAppPreferenceController.java @@ -20,13 +20,15 @@ import static com.android.settingslib.TwoTargetPreference.ICON_SIZE_MEDIUM; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; import android.os.UserHandle; import android.os.UserManager; -import androidx.preference.Preference; import android.text.TextUtils; import android.util.Log; +import androidx.preference.Preference; + import com.android.settings.R; import com.android.settings.Utils; import com.android.settings.core.PreferenceControllerMixin; @@ -34,21 +36,20 @@ import com.android.settings.widget.GearPreference; import com.android.settingslib.TwoTargetPreference; import com.android.settingslib.applications.DefaultAppInfo; import com.android.settingslib.core.AbstractPreferenceController; -import com.android.settingslib.wrapper.PackageManagerWrapper; public abstract class DefaultAppPreferenceController extends AbstractPreferenceController implements PreferenceControllerMixin { private static final String TAG = "DefaultAppPrefControl"; - protected final PackageManagerWrapper mPackageManager; + protected final PackageManager mPackageManager; protected final UserManager mUserManager; protected int mUserId; public DefaultAppPreferenceController(Context context) { super(context); - mPackageManager = new PackageManagerWrapper(context.getPackageManager()); + mPackageManager = context.getPackageManager(); mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE); mUserId = UserHandle.myUserId(); } @@ -81,12 +82,16 @@ public abstract class DefaultAppPreferenceController extends AbstractPreferenceC final Intent settingIntent = getSettingIntent(app); if (settingIntent != null) { ((GearPreference) preference).setOnGearClickListener( - p -> mContext.startActivity(settingIntent)); + p -> startActivity(settingIntent)); } else { ((GearPreference) preference).setOnGearClickListener(null); } } + protected void startActivity(Intent intent) { + mContext.startActivity(intent); + } + protected abstract DefaultAppInfo getDefaultAppInfo(); /** diff --git a/src/com/android/settings/applications/defaultapps/DefaultAutofillPicker.java b/src/com/android/settings/applications/defaultapps/DefaultAutofillPicker.java index 7d9989ff1c..f1e7ac0974 100644 --- a/src/com/android/settings/applications/defaultapps/DefaultAutofillPicker.java +++ b/src/com/android/settings/applications/defaultapps/DefaultAutofillPicker.java @@ -18,6 +18,7 @@ package com.android.settings.applications.defaultapps; import android.Manifest; import android.app.Activity; +import android.app.settings.SettingsEnums; import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; @@ -27,16 +28,17 @@ import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.net.Uri; import android.os.Bundle; +import android.os.UserHandle; import android.provider.Settings; import android.service.autofill.AutofillService; import android.service.autofill.AutofillServiceInfo; -import androidx.preference.Preference; import android.text.Html; import android.text.TextUtils; import android.util.Log; +import androidx.preference.Preference; + import com.android.internal.content.PackageMonitor; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settingslib.applications.DefaultAppInfo; import com.android.settingslib.utils.ThreadUtils; @@ -72,8 +74,10 @@ public class DefaultAutofillPicker extends DefaultAppPickerFragment { activity.setResult(Activity.RESULT_CANCELED); activity.finish(); }; + // If mCancelListener is not null, fragment is started from + // ACTION_REQUEST_SET_AUTOFILL_SERVICE and we should always use the calling uid. + mUserId = UserHandle.myUserId(); } - mSettingsPackageMonitor.register(activity, activity.getMainLooper(), false); update(); } @@ -110,7 +114,7 @@ public class DefaultAutofillPicker extends DefaultAppPickerFragment { @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DEFAULT_AUTOFILL_PICKER; + return SettingsEnums.DEFAULT_AUTOFILL_PICKER; } @Override @@ -158,18 +162,24 @@ public class DefaultAutofillPicker extends DefaultAppPickerFragment { * @return The preference or {@code null} if no service can be added */ private Preference newAddServicePreferenceOrNull() { - final String searchUri = Settings.Secure.getString(getActivity().getContentResolver(), - Settings.Secure.AUTOFILL_SERVICE_SEARCH_URI); + final String searchUri = Settings.Secure.getStringForUser( + getActivity().getContentResolver(), + Settings.Secure.AUTOFILL_SERVICE_SEARCH_URI, + mUserId); if (TextUtils.isEmpty(searchUri)) { return null; } final Intent addNewServiceIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(searchUri)); - Preference preference = new Preference(getPrefContext()); + final Context context = getPrefContext(); + final Preference preference = new Preference(context); + preference.setOnPreferenceClickListener(p -> { + context.startActivityAsUser(addNewServiceIntent, UserHandle.of(mUserId)); + return true; + }); preference.setTitle(R.string.print_menu_item_add_service); - preference.setIcon(R.drawable.ic_menu_add); + preference.setIcon(R.drawable.ic_add_24dp); preference.setOrder(Integer.MAX_VALUE -1); - preference.setIntent(addNewServiceIntent); preference.setPersistent(false); return preference; } @@ -188,8 +198,8 @@ public class DefaultAutofillPicker extends DefaultAppPickerFragment { @Override protected List<DefaultAppInfo> getCandidates() { final List<DefaultAppInfo> candidates = new ArrayList<>(); - final List<ResolveInfo> resolveInfos = mPm.queryIntentServices( - AUTOFILL_PROBE, PackageManager.GET_META_DATA); + final List<ResolveInfo> resolveInfos = mPm.queryIntentServicesAsUser( + AUTOFILL_PROBE, PackageManager.GET_META_DATA, mUserId); final Context context = getContext(); for (ResolveInfo info : resolveInfos) { final String permission = info.serviceInfo.permission; @@ -209,8 +219,9 @@ public class DefaultAutofillPicker extends DefaultAppPickerFragment { return candidates; } - public static String getDefaultKey(Context context) { - String setting = Settings.Secure.getString(context.getContentResolver(), SETTING); + public static String getDefaultKey(Context context, int userId) { + String setting = Settings.Secure.getStringForUser( + context.getContentResolver(), SETTING, userId); if (setting != null) { ComponentName componentName = ComponentName.unflattenFromString(setting); if (componentName != null) { @@ -222,7 +233,7 @@ public class DefaultAutofillPicker extends DefaultAppPickerFragment { @Override protected String getDefaultKey() { - return getDefaultKey(getContext()); + return getDefaultKey(getContext(), mUserId); } @Override @@ -238,7 +249,7 @@ public class DefaultAutofillPicker extends DefaultAppPickerFragment { @Override protected boolean setDefaultKey(String key) { - Settings.Secure.putString(getContext().getContentResolver(), SETTING, key); + Settings.Secure.putStringForUser(getContext().getContentResolver(), SETTING, key, mUserId); // Check if activity was launched from Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE // intent, and set proper result if so... @@ -262,16 +273,19 @@ public class DefaultAutofillPicker extends DefaultAppPickerFragment { private final String mSelectedKey; private final Context mContext; + private final int mUserId; - public AutofillSettingIntentProvider(Context context, String key) { + public AutofillSettingIntentProvider(Context context, int userId, String key) { mSelectedKey = key; mContext = context; + mUserId = userId; } @Override public Intent getIntent() { - final List<ResolveInfo> resolveInfos = mContext.getPackageManager().queryIntentServices( - AUTOFILL_PROBE, PackageManager.GET_META_DATA); + final List<ResolveInfo> resolveInfos = mContext.getPackageManager() + .queryIntentServicesAsUser( + AUTOFILL_PROBE, PackageManager.GET_META_DATA, mUserId); for (ResolveInfo resolveInfo : resolveInfos) { final ServiceInfo serviceInfo = resolveInfo.serviceInfo; diff --git a/src/com/android/settings/applications/defaultapps/DefaultAutofillPreferenceController.java b/src/com/android/settings/applications/defaultapps/DefaultAutofillPreferenceController.java index bab1d167df..d32322b6fd 100644 --- a/src/com/android/settings/applications/defaultapps/DefaultAutofillPreferenceController.java +++ b/src/com/android/settings/applications/defaultapps/DefaultAutofillPreferenceController.java @@ -44,7 +44,7 @@ public class DefaultAutofillPreferenceController extends DefaultAppPreferenceCon @Override public String getPreferenceKey() { - return "default_autofill"; + return "default_autofill_main"; } @Override @@ -54,7 +54,7 @@ public class DefaultAutofillPreferenceController extends DefaultAppPreferenceCon } final DefaultAutofillPicker.AutofillSettingIntentProvider intentProvider = new DefaultAutofillPicker.AutofillSettingIntentProvider( - mContext, info.getKey()); + mContext, mUserId, info.getKey()); return intentProvider.getIntent(); } diff --git a/src/com/android/settings/applications/defaultapps/DefaultBrowserPicker.java b/src/com/android/settings/applications/defaultapps/DefaultBrowserPicker.java deleted file mode 100644 index c243970bad..0000000000 --- a/src/com/android/settings/applications/defaultapps/DefaultBrowserPicker.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (C) 2017 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.applications.defaultapps; - -import android.content.Context; -import android.content.pm.ComponentInfo; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; - -import android.util.ArraySet; -import com.android.internal.logging.nano.MetricsProto; -import com.android.settings.R; - -import com.android.settingslib.applications.DefaultAppInfo; - -import java.util.ArrayList; -import java.util.List; -import java.util.Set; - -/** - * Fragment for choosing default browser. - */ -public class DefaultBrowserPicker extends DefaultAppPickerFragment { - - @Override - protected int getPreferenceScreenResId() { - return R.xml.default_browser_settings; - } - - @Override - public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DEFAULT_BROWSER_PICKER; - } - - @Override - protected String getDefaultKey() { - return mPm.getDefaultBrowserPackageNameAsUser(mUserId); - } - - @Override - protected boolean setDefaultKey(String packageName) { - return mPm.setDefaultBrowserPackageNameAsUser(packageName, mUserId); - } - - @Override - protected List<DefaultAppInfo> getCandidates() { - final List<DefaultAppInfo> candidates = new ArrayList<>(); - final Context context = getContext(); - // Resolve that intent and check that the handleAllWebDataURI boolean is set - final List<ResolveInfo> list = mPm.queryIntentActivitiesAsUser( - DefaultBrowserPreferenceController.BROWSE_PROBE, PackageManager.MATCH_ALL, mUserId); - - final int count = list.size(); - final Set<String> addedPackages = new ArraySet<>(); - for (int i = 0; i < count; i++) { - ResolveInfo info = list.get(i); - if (info.activityInfo == null || !info.handleAllWebDataURI) { - continue; - } - final String packageName = info.activityInfo.packageName; - if (addedPackages.contains(packageName)) { - continue; - } - try { - candidates.add(new DefaultAppInfo(context, mPm, - mPm.getApplicationInfoAsUser(packageName, 0, mUserId))); - addedPackages.add(packageName); - } catch (PackageManager.NameNotFoundException e) { - // Skip unknown packages. - } - } - - return candidates; - } -} diff --git a/src/com/android/settings/applications/defaultapps/DefaultBrowserPreferenceController.java b/src/com/android/settings/applications/defaultapps/DefaultBrowserPreferenceController.java deleted file mode 100644 index 9d72bcf638..0000000000 --- a/src/com/android/settings/applications/defaultapps/DefaultBrowserPreferenceController.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright (C) 2017 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.applications.defaultapps; - -import android.content.Context; -import android.content.Intent; -import android.content.pm.ApplicationInfo; -import android.content.pm.ComponentInfo; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import androidx.preference.Preference; -import android.text.TextUtils; -import android.util.IconDrawableFactory; -import android.util.Log; - -import com.android.settingslib.applications.DefaultAppInfo; - -import java.util.List; - -public class DefaultBrowserPreferenceController extends DefaultAppPreferenceController { - - private static final String TAG = "BrowserPrefCtrl"; - - static final Intent BROWSE_PROBE = new Intent() - .setAction(Intent.ACTION_VIEW) - .addCategory(Intent.CATEGORY_BROWSABLE) - .setData(Uri.parse("http:")); - - public DefaultBrowserPreferenceController(Context context) { - super(context); - } - - @Override - public boolean isAvailable() { - final List<ResolveInfo> candidates = getCandidates(); - return candidates != null && !candidates.isEmpty(); - } - - @Override - public String getPreferenceKey() { - return "default_browser"; - } - - @Override - public void updateState(Preference preference) { - super.updateState(preference); - final CharSequence defaultAppLabel = getDefaultAppLabel(); - if (!TextUtils.isEmpty(defaultAppLabel)) { - preference.setSummary(defaultAppLabel); - } - } - - @Override - protected DefaultAppInfo getDefaultAppInfo() { - try { - final String packageName = mPackageManager.getDefaultBrowserPackageNameAsUser(mUserId); - Log.d(TAG, "Get default browser package: " + packageName); - return new DefaultAppInfo(mContext, mPackageManager, - mPackageManager.getPackageManager().getApplicationInfo(packageName, 0)); - } catch (PackageManager.NameNotFoundException e) { - return null; - } - } - - @Override - public CharSequence getDefaultAppLabel() { - if (!isAvailable()) { - return null; - } - final DefaultAppInfo defaultApp = getDefaultAppInfo(); - final CharSequence defaultAppLabel = defaultApp != null ? defaultApp.loadLabel() : null; - if (!TextUtils.isEmpty(defaultAppLabel)) { - return defaultAppLabel; - } - return getOnlyAppLabel(); - } - - @Override - public Drawable getDefaultAppIcon() { - if (!isAvailable()) { - return null; - } - final DefaultAppInfo defaultApp = getDefaultAppInfo(); - if (defaultApp != null) { - return defaultApp.loadIcon(); - } - return getOnlyAppIcon(); - } - - private List<ResolveInfo> getCandidates() { - return mPackageManager.queryIntentActivitiesAsUser(BROWSE_PROBE, PackageManager.MATCH_ALL, - mUserId); - } - - private String getOnlyAppLabel() { - // Resolve that intent and check that the handleAllWebDataURI boolean is set - final List<ResolveInfo> list = getCandidates(); - if (list != null && list.size() == 1) { - final ResolveInfo info = list.get(0); - final String label = info.loadLabel(mPackageManager.getPackageManager()).toString(); - final ComponentInfo cn = info.getComponentInfo(); - final String packageName = cn == null ? null : cn.packageName; - Log.d(TAG, "Getting label for the only browser app: " + packageName + label); - return label; - } - return null; - } - - private Drawable getOnlyAppIcon() { - final List<ResolveInfo> list = getCandidates(); - if (list != null && list.size() == 1) { - final ResolveInfo info = list.get(0); - final ComponentInfo cn = info.getComponentInfo(); - final String packageName = cn == null ? null : cn.packageName; - if (TextUtils.isEmpty(packageName)) { - return null; - } - final ApplicationInfo appInfo; - try { - appInfo = mPackageManager.getPackageManager().getApplicationInfo(packageName, 0); - } catch (PackageManager.NameNotFoundException e) { - Log.w(TAG, "Error getting app info for " + packageName); - return null; - } - Log.d(TAG, "Getting icon for the only browser app: " + packageName); - final IconDrawableFactory iconFactory = IconDrawableFactory.newInstance(mContext); - return iconFactory.getBadgedIcon(cn, appInfo, mUserId); - } - return null; - } - - /** - * Whether or not the pkg contains browser capability - */ - public static boolean hasBrowserPreference(String pkg, Context context) { - final Intent intent = new Intent(BROWSE_PROBE); - intent.setPackage(pkg); - final List<ResolveInfo> resolveInfos = - context.getPackageManager().queryIntentActivities(intent, 0); - return resolveInfos != null && resolveInfos.size() != 0; - } - - /** - * Whether or not the pkg is the default browser - */ - public boolean isBrowserDefault(String pkg, int userId) { - String defaultPackage = mPackageManager.getDefaultBrowserPackageNameAsUser(userId); - if (defaultPackage != null) { - return defaultPackage.equals(pkg); - } - - final List<ResolveInfo> list = mPackageManager.queryIntentActivitiesAsUser(BROWSE_PROBE, - PackageManager.MATCH_ALL, userId); - // There is only 1 app, it must be the default browser. - return list != null && list.size() == 1; - } -} diff --git a/src/com/android/settings/applications/defaultapps/DefaultEmergencyPicker.java b/src/com/android/settings/applications/defaultapps/DefaultEmergencyPicker.java deleted file mode 100644 index c20c0df490..0000000000 --- a/src/com/android/settings/applications/defaultapps/DefaultEmergencyPicker.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (C) 2017 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.applications.defaultapps; - -import android.content.ContentResolver; -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.provider.Settings; -import android.text.TextUtils; - -import com.android.internal.logging.nano.MetricsProto; -import com.android.settings.R; -import com.android.settings.Utils; -import com.android.settingslib.applications.DefaultAppInfo; -import com.android.settingslib.widget.CandidateInfo; - -import java.util.ArrayList; -import java.util.List; - -public class DefaultEmergencyPicker extends DefaultAppPickerFragment { - - @Override - public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DEFAULT_EMERGENCY_APP_PICKER; - } - - @Override - protected int getPreferenceScreenResId() { - return R.xml.default_emergency_settings; - } - - @Override - protected List<DefaultAppInfo> getCandidates() { - final List<DefaultAppInfo> candidates = new ArrayList<>(); - final List<ResolveInfo> infos = mPm.getPackageManager().queryIntentActivities( - DefaultEmergencyPreferenceController.QUERY_INTENT, 0); - PackageInfo bestMatch = null; - final Context context = getContext(); - for (ResolveInfo info : infos) { - try { - final PackageInfo packageInfo = - mPm.getPackageManager().getPackageInfo(info.activityInfo.packageName, 0); - final ApplicationInfo appInfo = packageInfo.applicationInfo; - candidates.add(new DefaultAppInfo(context, mPm, appInfo)); - // Get earliest installed system app. - if (isSystemApp(appInfo) && (bestMatch == null || - bestMatch.firstInstallTime > packageInfo.firstInstallTime)) { - bestMatch = packageInfo; - } - } catch (PackageManager.NameNotFoundException e) { - // Skip unknown packages. - } - if (bestMatch != null) { - final String defaultKey = getDefaultKey(); - if (TextUtils.isEmpty(defaultKey)) { - setDefaultKey(bestMatch.packageName); - } - } - } - return candidates; - } - - @Override - protected String getConfirmationMessage(CandidateInfo info) { - return Utils.isPackageDirectBootAware(getContext(), info.getKey()) ? null - : getContext().getString(R.string.direct_boot_unaware_dialog_message); - } - - @Override - protected String getDefaultKey() { - return Settings.Secure.getString(getContext().getContentResolver(), - Settings.Secure.EMERGENCY_ASSISTANCE_APPLICATION); - } - - @Override - protected boolean setDefaultKey(String key) { - final ContentResolver contentResolver = getContext().getContentResolver(); - final String previousValue = Settings.Secure.getString(contentResolver, - Settings.Secure.EMERGENCY_ASSISTANCE_APPLICATION); - - if (!TextUtils.isEmpty(key) && !TextUtils.equals(key, previousValue)) { - Settings.Secure.putString(contentResolver, - Settings.Secure.EMERGENCY_ASSISTANCE_APPLICATION, - key); - return true; - } - return false; - } - - private boolean isSystemApp(ApplicationInfo info) { - return info != null && (info.flags & ApplicationInfo.FLAG_SYSTEM) != 0; - } -} diff --git a/src/com/android/settings/applications/defaultapps/DefaultEmergencyPreferenceController.java b/src/com/android/settings/applications/defaultapps/DefaultEmergencyPreferenceController.java deleted file mode 100644 index d9f33204fe..0000000000 --- a/src/com/android/settings/applications/defaultapps/DefaultEmergencyPreferenceController.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (C) 2017 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.applications.defaultapps; - -import android.content.Context; -import android.content.Intent; -import android.content.pm.ResolveInfo; -import android.provider.Settings; -import android.telephony.TelephonyManager; - -import com.android.settingslib.applications.DefaultAppInfo; - -import java.util.List; - -public class DefaultEmergencyPreferenceController extends DefaultAppPreferenceController { - - private static final boolean DEFAULT_EMERGENCY_APP_IS_CONFIGURABLE = false; - - public static final Intent QUERY_INTENT = new Intent( - TelephonyManager.ACTION_EMERGENCY_ASSISTANCE); - - public DefaultEmergencyPreferenceController(Context context) { - super(context); - } - - @Override - public boolean isAvailable() { - return DEFAULT_EMERGENCY_APP_IS_CONFIGURABLE - && isCapable() - && mPackageManager.getPackageManager().resolveActivity(QUERY_INTENT, 0) != null; - } - - @Override - public String getPreferenceKey() { - return "default_emergency_app"; - } - - @Override - protected DefaultAppInfo getDefaultAppInfo() { - return null; - } - - private boolean isCapable() { - return TelephonyManager.EMERGENCY_ASSISTANCE_ENABLED - && mContext.getResources().getBoolean( - com.android.internal.R.bool.config_voice_capable); - } - - public static boolean hasEmergencyPreference(String pkg, Context context) { - Intent i = new Intent(QUERY_INTENT); - i.setPackage(pkg); - final List<ResolveInfo> resolveInfos = - context.getPackageManager().queryIntentActivities(i, 0); - return resolveInfos != null && resolveInfos.size() != 0; - } - - public static boolean isEmergencyDefault(String pkg, Context context) { - String defaultPackage = Settings.Secure.getString(context.getContentResolver(), - Settings.Secure.EMERGENCY_ASSISTANCE_APPLICATION); - return defaultPackage != null && defaultPackage.equals(pkg); - } -} diff --git a/src/com/android/settings/applications/defaultapps/DefaultHomePicker.java b/src/com/android/settings/applications/defaultapps/DefaultHomePicker.java deleted file mode 100644 index 8617e85ab5..0000000000 --- a/src/com/android/settings/applications/defaultapps/DefaultHomePicker.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright (C) 2017 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.applications.defaultapps; - -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.ActivityInfo; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.content.pm.UserInfo; -import android.os.Build; -import android.text.TextUtils; - -import com.android.internal.logging.nano.MetricsProto; -import com.android.settings.R; -import com.android.settingslib.applications.DefaultAppInfo; - -import java.util.ArrayList; -import java.util.List; - -public class DefaultHomePicker extends DefaultAppPickerFragment { - - private String mPackageName; - - @Override - public void onAttach(Context context) { - super.onAttach(context); - mPackageName = context.getPackageName(); - } - - @Override - protected int getPreferenceScreenResId() { - return R.xml.default_home_settings; - } - - @Override - public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DEFAULT_HOME_PICKER; - } - - @Override - protected List<DefaultAppInfo> getCandidates() { - final boolean mustSupportManagedProfile = hasManagedProfile(); - final List<DefaultAppInfo> candidates = new ArrayList<>(); - final List<ResolveInfo> homeActivities = new ArrayList<>(); - final Context context = getContext(); - mPm.getHomeActivities(homeActivities); - - for (ResolveInfo resolveInfo : homeActivities) { - final ActivityInfo info = resolveInfo.activityInfo; - final ComponentName activityName = new ComponentName(info.packageName, info.name); - if (info.packageName.equals(mPackageName)) { - continue; - } - - final String summary; - boolean enabled = true; - if (mustSupportManagedProfile && !launcherHasManagedProfilesFeature(resolveInfo)) { - summary = getContext().getString(R.string.home_work_profile_not_supported); - enabled = false; - } else { - summary = null; - } - final DefaultAppInfo candidate = - new DefaultAppInfo(context, mPm, mUserId, activityName, summary, enabled); - candidates.add(candidate); - } - return candidates; - } - - @Override - protected String getDefaultKey() { - final ArrayList<ResolveInfo> homeActivities = new ArrayList<>(); - final ComponentName currentDefaultHome = mPm.getHomeActivities(homeActivities); - if (currentDefaultHome != null) { - return currentDefaultHome.flattenToString(); - } - return null; - } - - @Override - protected boolean setDefaultKey(String key) { - if (!TextUtils.isEmpty(key)) { - final ComponentName component = ComponentName.unflattenFromString(key); - final List<ResolveInfo> homeActivities = new ArrayList<>(); - mPm.getHomeActivities(homeActivities); - final List<ComponentName> allComponents = new ArrayList<>(); - for (ResolveInfo info : homeActivities) { - final ActivityInfo appInfo = info.activityInfo; - ComponentName activityName = new ComponentName(appInfo.packageName, appInfo.name); - allComponents.add(activityName); - } - mPm.replacePreferredActivity( - DefaultHomePreferenceController.HOME_FILTER, - IntentFilter.MATCH_CATEGORY_EMPTY, - allComponents.toArray(new ComponentName[0]), - component); - - // Launch the new Home app so the change is immediately visible even if - // the Home button is not pressed. - final Context context = getContext(); - Intent i = new Intent(Intent.ACTION_MAIN); - i.addCategory(Intent.CATEGORY_HOME); - i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(i); - return true; - } - return false; - } - - private boolean hasManagedProfile() { - final Context context = getContext(); - List<UserInfo> profiles = mUserManager.getProfiles(context.getUserId()); - for (UserInfo userInfo : profiles) { - if (userInfo.isManagedProfile()) { - return true; - } - } - return false; - } - - private boolean launcherHasManagedProfilesFeature(ResolveInfo resolveInfo) { - try { - ApplicationInfo appInfo = mPm.getPackageManager().getApplicationInfo( - resolveInfo.activityInfo.packageName, 0 /* default flags */); - return versionNumberAtLeastL(appInfo.targetSdkVersion); - } catch (PackageManager.NameNotFoundException e) { - return false; - } - } - - private boolean versionNumberAtLeastL(int versionNumber) { - return versionNumber >= Build.VERSION_CODES.LOLLIPOP; - } -} diff --git a/src/com/android/settings/applications/defaultapps/DefaultHomePreferenceController.java b/src/com/android/settings/applications/defaultapps/DefaultHomePreferenceController.java deleted file mode 100644 index 20cf1523c8..0000000000 --- a/src/com/android/settings/applications/defaultapps/DefaultHomePreferenceController.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright (C) 2017 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.applications.defaultapps; - -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.ActivityInfo; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; - -import com.android.settings.R; -import com.android.settingslib.applications.DefaultAppInfo; -import com.android.settingslib.wrapper.PackageManagerWrapper; - -import java.util.ArrayList; -import java.util.List; - -public class DefaultHomePreferenceController extends DefaultAppPreferenceController { - - static final IntentFilter HOME_FILTER; - - private final String mPackageName; - - static { - HOME_FILTER = new IntentFilter(Intent.ACTION_MAIN); - HOME_FILTER.addCategory(Intent.CATEGORY_HOME); - HOME_FILTER.addCategory(Intent.CATEGORY_DEFAULT); - } - - public DefaultHomePreferenceController(Context context) { - super(context); - mPackageName = mContext.getPackageName(); - } - - @Override - public String getPreferenceKey() { - return "default_home"; - } - - @Override - public boolean isAvailable() { - return mContext.getResources().getBoolean(R.bool.config_show_default_home); - } - - @Override - protected DefaultAppInfo getDefaultAppInfo() { - final ArrayList<ResolveInfo> homeActivities = new ArrayList<>(); - final ComponentName currentDefaultHome = mPackageManager.getHomeActivities(homeActivities); - if (currentDefaultHome != null) { - return new DefaultAppInfo(mContext, mPackageManager, mUserId, currentDefaultHome); - } - final ActivityInfo onlyAppInfo = getOnlyAppInfo(homeActivities); - if (onlyAppInfo != null) { - return new DefaultAppInfo(mContext, mPackageManager, mUserId, - onlyAppInfo.getComponentName()); - } - return null; - } - - private ActivityInfo getOnlyAppInfo(List<ResolveInfo> homeActivities) { - final List<ActivityInfo> appLabels = new ArrayList<>(); - - mPackageManager.getHomeActivities(homeActivities); - for (ResolveInfo candidate : homeActivities) { - final ActivityInfo info = candidate.activityInfo; - if (info.packageName.equals(mPackageName)) { - continue; - } - appLabels.add(info); - } - return appLabels.size() == 1 - ? appLabels.get(0) - : null; - } - - @Override - protected Intent getSettingIntent(DefaultAppInfo info) { - if (info == null) { - return null; - } - final String packageName; - if (info.componentName != null) { - packageName = info.componentName.getPackageName(); - } else if (info.packageItemInfo != null) { - packageName = info.packageItemInfo.packageName; - } else { - return null; - } - - Intent intent = new Intent(Intent.ACTION_APPLICATION_PREFERENCES) - .setPackage(packageName) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - return intent.resolveActivity(mPackageManager.getPackageManager()) != null - ? intent - : null; - } - - public static boolean hasHomePreference(String pkg, Context context) { - ArrayList<ResolveInfo> homeActivities = new ArrayList<>(); - PackageManager pm = context.getPackageManager(); - pm.getHomeActivities(homeActivities); - for (int i = 0; i < homeActivities.size(); i++) { - if (homeActivities.get(i).activityInfo.packageName.equals(pkg)) { - return true; - } - } - return false; - } - - public static boolean isHomeDefault(String pkg, PackageManagerWrapper pm) { - final ArrayList<ResolveInfo> homeActivities = new ArrayList<>(); - ComponentName def = pm.getHomeActivities(homeActivities); - - return def == null || def.getPackageName().equals(pkg); - } -} diff --git a/src/com/android/settings/applications/defaultapps/DefaultPaymentSettingsPreferenceController.java b/src/com/android/settings/applications/defaultapps/DefaultPaymentSettingsPreferenceController.java deleted file mode 100644 index b774602aee..0000000000 --- a/src/com/android/settings/applications/defaultapps/DefaultPaymentSettingsPreferenceController.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (C) 2017 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.applications.defaultapps; - -import android.content.Context; -import android.content.pm.PackageManager; -import android.nfc.NfcAdapter; -import android.os.UserManager; -import androidx.preference.Preference; - -import com.android.settings.R; -import com.android.settings.core.PreferenceControllerMixin; -import com.android.settings.nfc.PaymentBackend; -import com.android.settingslib.core.AbstractPreferenceController; - -public class DefaultPaymentSettingsPreferenceController extends AbstractPreferenceController - implements PreferenceControllerMixin { - - private final NfcAdapter mNfcAdapter; - private final PackageManager mPackageManager; - private final UserManager mUserManager; - private PaymentBackend mPaymentBackend; - - public DefaultPaymentSettingsPreferenceController(Context context) { - super(context); - mPackageManager = context.getPackageManager(); - mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE); - mNfcAdapter = NfcAdapter.getDefaultAdapter(mContext); - } - - @Override - public boolean isAvailable() { - return mPackageManager.hasSystemFeature(PackageManager.FEATURE_NFC) - && mPackageManager.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION) - && mUserManager.isAdminUser() - && mNfcAdapter != null - && mNfcAdapter.isEnabled(); - } - - @Override - public void updateState(Preference preference) { - if (mPaymentBackend == null) { - if (mNfcAdapter != null) { - mPaymentBackend = new PaymentBackend(mContext); - } else { - mPaymentBackend = null; - } - } - if (mPaymentBackend == null) { - return; - } - mPaymentBackend.refresh(); - final PaymentBackend.PaymentAppInfo app = mPaymentBackend.getDefaultApp(); - if (app != null) { - preference.setSummary(app.label); - } else { - preference.setSummary(R.string.app_list_preference_none); - } - } - - @Override - public String getPreferenceKey() { - return "default_payment_app"; - } -} diff --git a/src/com/android/settings/applications/defaultapps/DefaultPhonePicker.java b/src/com/android/settings/applications/defaultapps/DefaultPhonePicker.java deleted file mode 100644 index e462ab88a7..0000000000 --- a/src/com/android/settings/applications/defaultapps/DefaultPhonePicker.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (C) 2017 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.applications.defaultapps; - -import android.content.Context; -import android.content.pm.PackageManager; -import android.telecom.DefaultDialerManager; -import android.telecom.TelecomManager; -import android.text.TextUtils; - -import com.android.internal.logging.nano.MetricsProto; -import com.android.settings.R; -import com.android.settingslib.applications.DefaultAppInfo; - -import java.util.ArrayList; -import java.util.List; - -public class DefaultPhonePicker extends DefaultAppPickerFragment { - - private DefaultKeyUpdater mDefaultKeyUpdater; - - @Override - public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DEFAULT_PHONE_PICKER; - } - - @Override - public void onAttach(Context context) { - super.onAttach(context); - mDefaultKeyUpdater = new DefaultKeyUpdater( - (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE)); - } - - @Override - protected int getPreferenceScreenResId() { - return R.xml.default_phone_settings; - } - - @Override - protected List<DefaultAppInfo> getCandidates() { - final List<DefaultAppInfo> candidates = new ArrayList<>(); - final List<String> dialerPackages = - DefaultDialerManager.getInstalledDialerApplications(getContext(), mUserId); - final Context context = getContext(); - for (String packageName : dialerPackages) { - try { - candidates.add(new DefaultAppInfo(context, mPm, - mPm.getApplicationInfoAsUser(packageName, 0, mUserId))); - } catch (PackageManager.NameNotFoundException e) { - // Skip unknown packages. - } - } - return candidates; - } - - @Override - protected String getDefaultKey() { - return mDefaultKeyUpdater.getDefaultDialerApplication(getContext(), mUserId); - } - - @Override - protected String getSystemDefaultKey() { - return mDefaultKeyUpdater.getSystemDialerPackage(); - } - - @Override - protected boolean setDefaultKey(String key) { - if (!TextUtils.isEmpty(key) && !TextUtils.equals(key, getDefaultKey())) { - return mDefaultKeyUpdater.setDefaultDialerApplication(getContext(), key, mUserId); - } - return false; - } - - /** - * Wrapper class to handle default phone app update. - */ - static class DefaultKeyUpdater { - private final TelecomManager mTelecomManager; - - public DefaultKeyUpdater(TelecomManager telecomManager) { - mTelecomManager = telecomManager; - } - - public String getSystemDialerPackage() { - return mTelecomManager.getSystemDialerPackage(); - } - - public String getDefaultDialerApplication(Context context, int uid) { - return DefaultDialerManager.getDefaultDialerApplication(context, uid); - } - - public boolean setDefaultDialerApplication(Context context, String key, int uid) { - return DefaultDialerManager.setDefaultDialerApplication(context, key, uid); - } - } -} diff --git a/src/com/android/settings/applications/defaultapps/DefaultPhonePreferenceController.java b/src/com/android/settings/applications/defaultapps/DefaultPhonePreferenceController.java deleted file mode 100644 index 37d71923f9..0000000000 --- a/src/com/android/settings/applications/defaultapps/DefaultPhonePreferenceController.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (C) 2017 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.applications.defaultapps; - -import android.content.Context; -import android.content.pm.PackageManager; -import android.os.UserHandle; -import android.os.UserManager; -import android.telecom.DefaultDialerManager; -import android.telephony.TelephonyManager; - -import com.android.settingslib.applications.DefaultAppInfo; - -import java.util.List; - -public class DefaultPhonePreferenceController extends DefaultAppPreferenceController { - - public DefaultPhonePreferenceController(Context context) { - super(context); - } - - @Override - public boolean isAvailable() { - final TelephonyManager tm = - (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); - if (!tm.isVoiceCapable()) { - return false; - } - final UserManager um = (UserManager) mContext.getSystemService(Context.USER_SERVICE); - final boolean hasUserRestriction = - um.hasUserRestriction(UserManager.DISALLOW_OUTGOING_CALLS); - - if (hasUserRestriction) { - return false; - } - final List<String> candidates = getCandidates(); - return candidates != null && !candidates.isEmpty(); - } - - @Override - public String getPreferenceKey() { - return "default_phone_app"; - } - - @Override - protected DefaultAppInfo getDefaultAppInfo() { - try { - return new DefaultAppInfo(mContext, mPackageManager, - mPackageManager.getPackageManager().getApplicationInfo( - DefaultDialerManager.getDefaultDialerApplication(mContext, mUserId), - 0)); - } catch (PackageManager.NameNotFoundException e) { - return null; - } - } - - private List<String> getCandidates() { - return DefaultDialerManager.getInstalledDialerApplications(mContext, mUserId); - } - - public static boolean hasPhonePreference(String pkg, Context context) { - List<String> dialerPackages = - DefaultDialerManager.getInstalledDialerApplications(context, UserHandle.myUserId()); - return dialerPackages.contains(pkg); - } - - public static boolean isPhoneDefault(String pkg, Context context) { - String def = DefaultDialerManager.getDefaultDialerApplication(context, - UserHandle.myUserId()); - return def != null && def.equals(pkg); - } -} diff --git a/src/com/android/settings/applications/defaultapps/DefaultSmsPicker.java b/src/com/android/settings/applications/defaultapps/DefaultSmsPicker.java deleted file mode 100644 index 91f9cafe95..0000000000 --- a/src/com/android/settings/applications/defaultapps/DefaultSmsPicker.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (C) 2017 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.applications.defaultapps; - -import android.content.ComponentName; -import android.content.Context; -import android.content.pm.PackageManager; -import android.text.TextUtils; - -import com.android.internal.logging.nano.MetricsProto; -import com.android.internal.telephony.SmsApplication; -import com.android.settings.R; -import com.android.settings.Utils; -import com.android.settingslib.applications.DefaultAppInfo; -import com.android.settingslib.widget.CandidateInfo; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -public class DefaultSmsPicker extends DefaultAppPickerFragment { - - private DefaultKeyUpdater mDefaultKeyUpdater = new DefaultKeyUpdater(); - - @Override - public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DEFAULT_SMS_PICKER; - } - - @Override - protected int getPreferenceScreenResId() { - return R.xml.default_sms_settings; - } - - @Override - protected List<DefaultAppInfo> getCandidates() { - final Context context = getContext(); - final Collection<SmsApplication.SmsApplicationData> smsApplications = - SmsApplication.getApplicationCollection(context); - final List<DefaultAppInfo> candidates = new ArrayList<>(smsApplications.size()); - - for (SmsApplication.SmsApplicationData smsApplicationData : smsApplications) { - try { - candidates.add(new DefaultAppInfo(context, mPm, - mPm.getApplicationInfoAsUser(smsApplicationData.mPackageName, 0, mUserId))); - } catch (PackageManager.NameNotFoundException e) { - // Skip unknown packages. - } - } - - return candidates; - } - - @Override - protected String getDefaultKey() { - return mDefaultKeyUpdater.getDefaultApplication(getContext()); - } - - @Override - protected boolean setDefaultKey(String key) { - if (!TextUtils.isEmpty(key) && !TextUtils.equals(key, getDefaultKey())) { - mDefaultKeyUpdater.setDefaultApplication(getContext(), key); - return true; - } - return false; - } - - @Override - protected String getConfirmationMessage(CandidateInfo info) { - return Utils.isPackageDirectBootAware(getContext(), info.getKey()) ? null - : getContext().getString(R.string.direct_boot_unaware_dialog_message); - } - - /** - * Wrapper class to handle default phone app update. - */ - static class DefaultKeyUpdater { - - public String getDefaultApplication(Context context) { - final ComponentName appName = SmsApplication.getDefaultSmsApplication(context, true); - if (appName != null) { - return appName.getPackageName(); - } - return null; - } - - public void setDefaultApplication(Context context, String key) { - SmsApplication.setDefaultApplication(key, context); - } - } -} diff --git a/src/com/android/settings/applications/defaultapps/DefaultSmsPreferenceController.java b/src/com/android/settings/applications/defaultapps/DefaultSmsPreferenceController.java deleted file mode 100644 index cb86b2ec95..0000000000 --- a/src/com/android/settings/applications/defaultapps/DefaultSmsPreferenceController.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (C) 2017 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.applications.defaultapps; - -import android.content.ComponentName; -import android.content.Context; -import android.telephony.TelephonyManager; - -import com.android.internal.telephony.SmsApplication; -import com.android.settingslib.applications.DefaultAppInfo; - -import java.util.Collection; - -public class DefaultSmsPreferenceController extends DefaultAppPreferenceController { - - public DefaultSmsPreferenceController(Context context) { - super(context); - } - - @Override - public boolean isAvailable() { - boolean isRestrictedUser = mUserManager.getUserInfo(mUserId).isRestricted(); - TelephonyManager tm = - (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); - return !isRestrictedUser && tm.isSmsCapable(); - } - - @Override - public String getPreferenceKey() { - return "default_sms_app"; - } - - @Override - protected DefaultAppInfo getDefaultAppInfo() { - final ComponentName app = SmsApplication.getDefaultSmsApplication(mContext, true); - if (app != null) { - return new DefaultAppInfo(mContext, mPackageManager, mUserId, app); - } - return null; - } - - public static boolean hasSmsPreference(String pkg, Context context) { - Collection<SmsApplication.SmsApplicationData> smsApplications = - SmsApplication.getApplicationCollection(context); - for (SmsApplication.SmsApplicationData data : smsApplications) { - if (data.mPackageName.equals(pkg)) { - return true; - } - } - return false; - } - - public static boolean isSmsDefault(String pkg, Context context) { - ComponentName appName = SmsApplication.getDefaultSmsApplication(context, true); - return appName != null && appName.getPackageName().equals(pkg); - } -} diff --git a/src/com/android/settings/applications/defaultapps/DefaultWorkAutofillPreferenceController.java b/src/com/android/settings/applications/defaultapps/DefaultWorkAutofillPreferenceController.java new file mode 100644 index 0000000000..47735ac17c --- /dev/null +++ b/src/com/android/settings/applications/defaultapps/DefaultWorkAutofillPreferenceController.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2018 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.applications.defaultapps; + + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.os.UserHandle; +import android.provider.Settings; +import android.text.TextUtils; + +import com.android.settings.Utils; +import com.android.settingslib.applications.DefaultAppInfo; + +public class DefaultWorkAutofillPreferenceController extends DefaultAutofillPreferenceController { + private final UserHandle mUserHandle; + + public DefaultWorkAutofillPreferenceController(Context context) { + super(context); + mUserHandle = Utils.getManagedProfile(mUserManager); + } + + @Override + public boolean isAvailable() { + if (mUserHandle == null) { + return false; + } + return super.isAvailable(); + } + + @Override + public String getPreferenceKey() { + return "default_autofill_work"; + } + + @Override + protected DefaultAppInfo getDefaultAppInfo() { + final String flattenComponent = Settings.Secure.getStringForUser( + mContext.getContentResolver(), + DefaultAutofillPicker.SETTING, + mUserHandle.getIdentifier()); + if (!TextUtils.isEmpty(flattenComponent)) { + DefaultAppInfo appInfo = new DefaultAppInfo( + mContext, + mPackageManager, + mUserHandle.getIdentifier(), + ComponentName.unflattenFromString(flattenComponent)); + return appInfo; + } + return null; + } + + @Override + protected Intent getSettingIntent(DefaultAppInfo info) { + if (info == null) { + return null; + } + final DefaultAutofillPicker.AutofillSettingIntentProvider intentProvider = + new DefaultAutofillPicker.AutofillSettingIntentProvider( + mContext, mUserHandle.getIdentifier(), info.getKey()); + return intentProvider.getIntent(); + } + + @Override + protected void startActivity(Intent intent) { + mContext.startActivityAsUser(intent, mUserHandle); + } +} diff --git a/src/com/android/settings/applications/defaultapps/DefaultWorkBrowserPreferenceController.java b/src/com/android/settings/applications/defaultapps/DefaultWorkBrowserPreferenceController.java deleted file mode 100644 index 46528558fc..0000000000 --- a/src/com/android/settings/applications/defaultapps/DefaultWorkBrowserPreferenceController.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (C) 2017 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.applications.defaultapps; - -import android.content.Context; -import android.os.UserHandle; - -import com.android.settings.Utils; - - -public class DefaultWorkBrowserPreferenceController extends DefaultBrowserPreferenceController { - - public static final String KEY = "work_default_browser"; - private final UserHandle mUserHandle; - - public DefaultWorkBrowserPreferenceController(Context context) { - super(context); - mUserHandle = Utils.getManagedProfile(mUserManager); - if (mUserHandle != null) { - mUserId = mUserHandle.getIdentifier(); - } - } - - @Override - public String getPreferenceKey() { - return KEY; - } - - @Override - public boolean isAvailable() { - if (mUserHandle == null) { - return false; - } - return super.isAvailable(); - } -} diff --git a/src/com/android/settings/applications/defaultapps/DefaultWorkPhonePreferenceController.java b/src/com/android/settings/applications/defaultapps/DefaultWorkPhonePreferenceController.java deleted file mode 100644 index decff06759..0000000000 --- a/src/com/android/settings/applications/defaultapps/DefaultWorkPhonePreferenceController.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2017 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.applications.defaultapps; - -import android.content.Context; -import android.os.UserHandle; - -import com.android.settings.Utils; - -public class DefaultWorkPhonePreferenceController extends DefaultPhonePreferenceController { - - public static final String KEY = "work_default_phone_app"; - private final UserHandle mUserHandle; - - public DefaultWorkPhonePreferenceController(Context context) { - super(context); - mUserHandle = Utils.getManagedProfile(mUserManager); - if (mUserHandle != null) { - mUserId = mUserHandle.getIdentifier(); - } - } - - @Override - public boolean isAvailable() { - if (mUserHandle == null) { - return false; - } - return super.isAvailable(); - } - - @Override - public String getPreferenceKey() { - return KEY; - } -} diff --git a/src/com/android/settings/applications/manageapplications/AppFilterRegistry.java b/src/com/android/settings/applications/manageapplications/AppFilterRegistry.java index ff584b57e3..0d2bcab312 100644 --- a/src/com/android/settings/applications/manageapplications/AppFilterRegistry.java +++ b/src/com/android/settings/applications/manageapplications/AppFilterRegistry.java @@ -23,7 +23,6 @@ import com.android.settings.applications.AppStateInstallAppsBridge; import com.android.settings.applications.AppStateNotificationBridge; import com.android.settings.applications.AppStateOverlayBridge; import com.android.settings.applications.AppStatePowerBridge; -import com.android.settings.applications.AppStateDirectoryAccessBridge; import com.android.settings.applications.AppStateUsageBridge; import com.android.settings.applications.AppStateWriteSettingsBridge; import com.android.settings.wifi.AppStateChangeWifiStateBridge; @@ -49,6 +48,7 @@ public class AppFilterRegistry { FILTER_APPS_WITH_OVERLAY, FILTER_APPS_WRITE_SETTINGS, FILTER_APPS_INSTALL_SOURCES, + FILTER_APPS_BLOCKED, }) @interface FilterType { } @@ -69,16 +69,16 @@ public class AppFilterRegistry { public static final int FILTER_APPS_WITH_OVERLAY = 11; public static final int FILTER_APPS_WRITE_SETTINGS = 12; public static final int FILTER_APPS_INSTALL_SOURCES = 13; - public static final int FILTER_APP_HAS_DIRECTORY_ACCESS = 14; public static final int FILTER_APP_CAN_CHANGE_WIFI_STATE = 15; - // Next id: 16 + public static final int FILTER_APPS_BLOCKED = 16; + // Next id: 17 private static AppFilterRegistry sRegistry; private final AppFilterItem[] mFilters; private AppFilterRegistry() { - mFilters = new AppFilterItem[16]; + mFilters = new AppFilterItem[17]; // High power whitelist, on mFilters[FILTER_APPS_POWER_WHITELIST] = new AppFilterItem( @@ -168,16 +168,16 @@ public class AppFilterRegistry { FILTER_APPS_INSTALL_SOURCES, R.string.filter_install_sources_apps); - // Apps that interacted with directory access permissions (A.K.A. Scoped Directory Access) - mFilters[FILTER_APP_HAS_DIRECTORY_ACCESS] = new AppFilterItem( - AppStateDirectoryAccessBridge.FILTER_APP_HAS_DIRECTORY_ACCESS, - FILTER_APP_HAS_DIRECTORY_ACCESS, - R.string.filter_install_sources_apps); - mFilters[FILTER_APP_CAN_CHANGE_WIFI_STATE] = new AppFilterItem( AppStateChangeWifiStateBridge.FILTER_CHANGE_WIFI_STATE, FILTER_APP_CAN_CHANGE_WIFI_STATE, R.string.filter_write_settings_apps); + + // Blocked Notifications + mFilters[FILTER_APPS_BLOCKED] = new AppFilterItem( + AppStateNotificationBridge.FILTER_APP_NOTIFICATION_BLOCKED, + FILTER_APPS_BLOCKED, + R.string.filter_notif_blocked_apps); } public static AppFilterRegistry getInstance() { @@ -200,8 +200,6 @@ public class AppFilterRegistry { return FILTER_APPS_WRITE_SETTINGS; case ManageApplications.LIST_TYPE_MANAGE_SOURCES: return FILTER_APPS_INSTALL_SOURCES; - case ManageApplications.LIST_TYPE_DIRECTORY_ACCESS: - return FILTER_APP_HAS_DIRECTORY_ACCESS; case ManageApplications.LIST_TYPE_WIFI_ACCESS: return FILTER_APP_CAN_CHANGE_WIFI_STATE; case ManageApplications.LIST_TYPE_NOTIFICATION: diff --git a/src/com/android/settings/applications/manageapplications/ApplicationViewHolder.java b/src/com/android/settings/applications/manageapplications/ApplicationViewHolder.java index a1a4c22a4b..bd08236cb2 100644 --- a/src/com/android/settings/applications/manageapplications/ApplicationViewHolder.java +++ b/src/com/android/settings/applications/manageapplications/ApplicationViewHolder.java @@ -19,10 +19,6 @@ package com.android.settings.applications.manageapplications; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; -import androidx.annotation.StringRes; -import androidx.annotation.VisibleForTesting; -import androidx.recyclerview.widget.RecyclerView; -import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -31,6 +27,10 @@ import android.widget.ImageView; import android.widget.Switch; import android.widget.TextView; +import androidx.annotation.StringRes; +import androidx.annotation.VisibleForTesting; +import androidx.recyclerview.widget.RecyclerView; + import com.android.settings.R; import com.android.settingslib.applications.ApplicationsState; import com.android.settingslib.applications.ApplicationsState.AppEntry; @@ -40,10 +40,6 @@ public class ApplicationViewHolder extends RecyclerView.ViewHolder { private final TextView mAppName; private final ImageView mAppIcon; - private final boolean mKeepStableHeight; - - @VisibleForTesting - View mSummaryContainer; @VisibleForTesting final TextView mSummary; @VisibleForTesting @@ -53,48 +49,46 @@ public class ApplicationViewHolder extends RecyclerView.ViewHolder { @VisibleForTesting final Switch mSwitch; - ApplicationViewHolder(View itemView, boolean keepStableHeight) { + ApplicationViewHolder(View itemView) { super(itemView); mAppName = itemView.findViewById(android.R.id.title); mAppIcon = itemView.findViewById(android.R.id.icon); - mSummaryContainer = itemView.findViewById(R.id.summary_container); mSummary = itemView.findViewById(android.R.id.summary); mDisabled = itemView.findViewById(R.id.appendix); - mKeepStableHeight = keepStableHeight; mSwitch = itemView.findViewById(R.id.switchWidget); mWidgetContainer = itemView.findViewById(android.R.id.widget_frame); } static View newView(ViewGroup parent) { - return newView(parent, false); + return newView(parent, false /* twoTarget */); } static View newView(ViewGroup parent, boolean twoTarget) { ViewGroup view = (ViewGroup) LayoutInflater.from(parent.getContext()) .inflate(R.layout.preference_app, parent, false); + final ViewGroup widgetFrame = view.findViewById(android.R.id.widget_frame); if (twoTarget) { - final ViewGroup widgetFrame = view.findViewById(android.R.id.widget_frame); if (widgetFrame != null) { - LayoutInflater.from(parent.getContext()) - .inflate(R.layout.preference_widget_master_switch, widgetFrame, true); + LayoutInflater.from(parent.getContext()) + .inflate(R.layout.preference_widget_master_switch, widgetFrame, true); - View divider = LayoutInflater.from(parent.getContext()).inflate( - R.layout.preference_two_target_divider, view, false); - // second to last, before widget frame - view.addView(divider, view.getChildCount() - 1); + View divider = LayoutInflater.from(parent.getContext()).inflate( + R.layout.preference_two_target_divider, view, false); + // second to last, before widget frame + view.addView(divider, view.getChildCount() - 1); } + } else if (widgetFrame != null) { + widgetFrame.setVisibility(View.GONE); } return view; } void setSummary(CharSequence summary) { mSummary.setText(summary); - updateSummaryContainer(); } void setSummary(@StringRes int summary) { mSummary.setText(summary); - updateSummaryContainer(); } void setEnabled(boolean isEnabled) { @@ -130,17 +124,6 @@ public class ApplicationViewHolder extends RecyclerView.ViewHolder { } else { mDisabled.setVisibility(View.GONE); } - updateSummaryContainer(); - } - - void updateSummaryContainer() { - if (mKeepStableHeight) { - mSummaryContainer.setVisibility(View.VISIBLE); - return; - } - final boolean hasContent = - !TextUtils.isEmpty(mDisabled.getText()) || !TextUtils.isEmpty(mSummary.getText()); - mSummaryContainer.setVisibility(hasContent ? View.VISIBLE : View.GONE); } void updateSizeText(AppEntry entry, CharSequence invalidSizeStr, int whichSize) { diff --git a/src/com/android/settings/applications/manageapplications/FileViewHolderController.java b/src/com/android/settings/applications/manageapplications/FileViewHolderController.java index 4e6bf26253..ded0286d16 100644 --- a/src/com/android/settings/applications/manageapplications/FileViewHolderController.java +++ b/src/com/android/settings/applications/manageapplications/FileViewHolderController.java @@ -16,7 +16,7 @@ package com.android.settings.applications.manageapplications; -import android.app.Fragment; +import androidx.fragment.app.Fragment; /** * FileViewHolderController handles adapting the AppViewHolder to work as a general purpose diff --git a/src/com/android/settings/applications/manageapplications/ManageApplications.java b/src/com/android/settings/applications/manageapplications/ManageApplications.java index b23a9670e6..490228363b 100644 --- a/src/com/android/settings/applications/manageapplications/ManageApplications.java +++ b/src/com/android/settings/applications/manageapplications/ManageApplications.java @@ -17,32 +17,24 @@ package com.android.settings.applications.manageapplications; import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE; -import static com.android.settings.applications.manageapplications.AppFilterRegistry - .FILTER_APPS_ALL; -import static com.android.settings.applications.manageapplications.AppFilterRegistry - .FILTER_APPS_DISABLED; -import static com.android.settings.applications.manageapplications.AppFilterRegistry - .FILTER_APPS_ENABLED; -import static com.android.settings.applications.manageapplications.AppFilterRegistry - .FILTER_APPS_FREQUENT; -import static com.android.settings.applications.manageapplications.AppFilterRegistry - .FILTER_APPS_INSTANT; -import static com.android.settings.applications.manageapplications.AppFilterRegistry - .FILTER_APPS_PERSONAL; -import static com.android.settings.applications.manageapplications.AppFilterRegistry - .FILTER_APPS_POWER_WHITELIST; -import static com.android.settings.applications.manageapplications.AppFilterRegistry - .FILTER_APPS_POWER_WHITELIST_ALL; -import static com.android.settings.applications.manageapplications.AppFilterRegistry - .FILTER_APPS_RECENT; -import static com.android.settings.applications.manageapplications.AppFilterRegistry - .FILTER_APPS_WORK; + +import static com.android.settings.applications.manageapplications.AppFilterRegistry.FILTER_APPS_ALL; +import static com.android.settings.applications.manageapplications.AppFilterRegistry.FILTER_APPS_BLOCKED; +import static com.android.settings.applications.manageapplications.AppFilterRegistry.FILTER_APPS_DISABLED; +import static com.android.settings.applications.manageapplications.AppFilterRegistry.FILTER_APPS_ENABLED; +import static com.android.settings.applications.manageapplications.AppFilterRegistry.FILTER_APPS_FREQUENT; +import static com.android.settings.applications.manageapplications.AppFilterRegistry.FILTER_APPS_INSTANT; +import static com.android.settings.applications.manageapplications.AppFilterRegistry.FILTER_APPS_PERSONAL; +import static com.android.settings.applications.manageapplications.AppFilterRegistry.FILTER_APPS_POWER_WHITELIST; +import static com.android.settings.applications.manageapplications.AppFilterRegistry.FILTER_APPS_POWER_WHITELIST_ALL; +import static com.android.settings.applications.manageapplications.AppFilterRegistry.FILTER_APPS_RECENT; +import static com.android.settings.applications.manageapplications.AppFilterRegistry.FILTER_APPS_WORK; import android.annotation.Nullable; import android.annotation.StringRes; import android.app.Activity; +import android.app.settings.SettingsEnums; import android.app.usage.IUsageStatsManager; -import android.app.usage.UsageStatsManager; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; @@ -53,12 +45,9 @@ import android.os.ServiceManager; import android.os.UserHandle; import android.os.UserManager; import android.preference.PreferenceFrameLayout; -import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; import android.text.TextUtils; import android.util.ArraySet; +import android.util.IconDrawableFactory; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; @@ -68,11 +57,17 @@ import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.AdapterView.OnItemSelectedListener; -import android.widget.ArrayAdapter; +import android.widget.Filter; import android.widget.FrameLayout; +import android.widget.SearchView; import android.widget.Spinner; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.annotation.WorkerThread; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + import com.android.settings.R; import com.android.settings.Settings; import com.android.settings.Settings.GamesStorageActivity; @@ -84,10 +79,10 @@ import com.android.settings.Settings.StorageUseActivity; import com.android.settings.Settings.UsageAccessSettingsActivity; import com.android.settings.Settings.WriteSettingsActivity; import com.android.settings.SettingsActivity; +import com.android.settings.Utils; import com.android.settings.applications.AppInfoBase; import com.android.settings.applications.AppStateAppOpsBridge.PermissionState; import com.android.settings.applications.AppStateBaseBridge; -import com.android.settings.applications.AppStateDirectoryAccessBridge; import com.android.settings.applications.AppStateInstallAppsBridge; import com.android.settings.applications.AppStateNotificationBridge; import com.android.settings.applications.AppStateNotificationBridge.NotificationsSentState; @@ -97,8 +92,6 @@ import com.android.settings.applications.AppStateUsageBridge; import com.android.settings.applications.AppStateUsageBridge.UsageState; import com.android.settings.applications.AppStateWriteSettingsBridge; import com.android.settings.applications.AppStorageSettings; -import com.android.settings.applications.DefaultAppSettings; -import com.android.settings.applications.DirectoryAccessDetails; import com.android.settings.applications.InstalledAppCounter; import com.android.settings.applications.UsageAccessDetails; import com.android.settings.applications.appinfo.AppInfoDashboardFragment; @@ -124,7 +117,7 @@ import com.android.settingslib.applications.ApplicationsState.VolumeFilter; import com.android.settingslib.applications.StorageStatsSource; import com.android.settingslib.fuelgauge.PowerWhitelistBackend; import com.android.settingslib.utils.ThreadUtils; -import com.android.settingslib.wrapper.PackageManagerWrapper; +import com.android.settingslib.widget.settingsspinner.SettingsSpinnerAdapter; import java.util.ArrayList; import java.util.Arrays; @@ -139,10 +132,10 @@ import java.util.Set; * intent. */ public class ManageApplications extends InstrumentedFragment - implements View.OnClickListener, OnItemSelectedListener { + implements View.OnClickListener, OnItemSelectedListener, SearchView.OnQueryTextListener { static final String TAG = "ManageApplications"; - static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + static final boolean DEBUG = false; // Intent extras. public static final String EXTRA_CLASSNAME = "classname"; @@ -157,6 +150,8 @@ public class ManageApplications extends InstrumentedFragment private static final String EXTRA_SHOW_SYSTEM = "showSystem"; private static final String EXTRA_HAS_ENTRIES = "hasEntries"; private static final String EXTRA_HAS_BRIDGE = "hasBridge"; + private static final String EXTRA_FILTER_TYPE = "filterType"; + private static final String EXTRA_EXPAND_SEARCH_VIEW = "expand_search_view"; // attributes used as keys when passing values to AppInfoDashboardFragment activity public static final String APP_CHG = "chg"; @@ -191,9 +186,8 @@ public class ManageApplications extends InstrumentedFragment private ApplicationsAdapter mApplications; private View mLoadingContainer; - private View mListContainer; - private RecyclerView mRecyclerView; + private SearchView mSearchView; // Size resource used for packages whose size computation failed for some reason CharSequence mInvalidSizeStr; @@ -214,7 +208,6 @@ public class ManageApplications extends InstrumentedFragment public static final int LIST_TYPE_GAMES = 9; public static final int LIST_TYPE_MOVIES = 10; public static final int LIST_TYPE_PHOTOGRAPHY = 11; - public static final int LIST_TYPE_DIRECTORY_ACCESS = 12; public static final int LIST_TYPE_WIFI_ACCESS = 13; // List types that should show instant apps. @@ -222,10 +215,18 @@ public class ManageApplications extends InstrumentedFragment LIST_TYPE_MAIN, LIST_TYPE_STORAGE)); + @VisibleForTesting + View mSpinnerHeader; + @VisibleForTesting + FilterSpinnerAdapter mFilterAdapter; + @VisibleForTesting + RecyclerView mRecyclerView; + // Whether or not search view is expanded. + @VisibleForTesting + boolean mExpandSearch; + private View mRootView; - private View mSpinnerHeader; private Spinner mFilterSpinner; - private FilterSpinnerAdapter mFilterAdapter; private IUsageStatsManager mUsageStatsManager; private UserManager mUserManager; private NotificationBackend mNotificationBackend; @@ -235,6 +236,7 @@ public class ManageApplications extends InstrumentedFragment private boolean mIsWorkOnly; private int mWorkUserId; private View mEmptyView; + private int mFilterType; @Override public void onCreate(Bundle savedInstanceState) { @@ -288,9 +290,6 @@ public class ManageApplications extends InstrumentedFragment mListType = LIST_TYPE_PHOTOGRAPHY; mSortOrder = R.id.sort_order_size; mStorageType = args.getInt(EXTRA_STORAGE_TYPE, STORAGE_TYPE_DEFAULT); - } else if (className.equals(Settings.DirectoryAccessSettingsActivity.class.getName())) { - mListType = LIST_TYPE_DIRECTORY_ACCESS; - screenTitle = R.string.directory_access; } else if (className.equals(Settings.ChangeWifiStateActivity.class.getName())) { mListType = LIST_TYPE_WIFI_ACCESS; screenTitle = R.string.change_wifi_state_title; @@ -312,10 +311,14 @@ public class ManageApplications extends InstrumentedFragment mFilter = appFilterRegistry.get(appFilterRegistry.getDefaultFilterType(mListType)); mIsWorkOnly = args != null ? args.getBoolean(EXTRA_WORK_ONLY) : false; mWorkUserId = args != null ? args.getInt(EXTRA_WORK_ID) : NO_USER_SPECIFIED; + mExpandSearch = activity.getIntent().getBooleanExtra(EXTRA_EXPAND_SEARCH_VIEW, false); if (savedInstanceState != null) { mSortOrder = savedInstanceState.getInt(EXTRA_SORT_ORDER, mSortOrder); mShowSystem = savedInstanceState.getBoolean(EXTRA_SHOW_SYSTEM, mShowSystem); + mFilterType = + savedInstanceState.getInt(EXTRA_FILTER_TYPE, AppFilterRegistry.FILTER_APPS_ALL); + mExpandSearch = savedInstanceState.getBoolean(EXTRA_EXPAND_SEARCH_VIEW); } mInvalidSizeStr = activity.getText(R.string.invalid_size_value); @@ -330,12 +333,19 @@ public class ManageApplications extends InstrumentedFragment @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + if (mListType == LIST_TYPE_OVERLAY && !Utils.isSystemAlertWindowEnabled(getContext())) { + mRootView = inflater.inflate(R.layout.manage_applications_apps_unsupported, null); + setHasOptionsMenu(false); + return mRootView; + } + mRootView = inflater.inflate(R.layout.manage_applications_apps, null); mLoadingContainer = mRootView.findViewById(R.id.loading_container); mListContainer = mRootView.findViewById(R.id.list_container); if (mListContainer != null) { // Create adapter and list view here mEmptyView = mListContainer.findViewById(android.R.id.empty); + mApplications = new ApplicationsAdapter(mApplicationsState, this, mFilter, savedInstanceState); if (savedInstanceState != null) { @@ -363,6 +373,7 @@ public class ManageApplications extends InstrumentedFragment UserHandle.of(userId))); } mRecyclerView = mListContainer.findViewById(R.id.apps_list); + mRecyclerView.setItemAnimator(null); mRecyclerView.setLayoutManager(new LinearLayoutManager( getContext(), RecyclerView.VERTICAL, false /* reverseLayout */)); mRecyclerView.setAdapter(mApplications); @@ -404,6 +415,7 @@ public class ManageApplications extends InstrumentedFragment if (mListType == LIST_TYPE_NOTIFICATION) { mFilterAdapter.enableFilter(FILTER_APPS_RECENT); mFilterAdapter.enableFilter(FILTER_APPS_FREQUENT); + mFilterAdapter.enableFilter(FILTER_APPS_BLOCKED); mFilterAdapter.disableFilter(FILTER_APPS_ALL); } if (mListType == LIST_TYPE_HIGH_POWER) { @@ -447,36 +459,34 @@ public class ManageApplications extends InstrumentedFragment public int getMetricsCategory() { switch (mListType) { case LIST_TYPE_MAIN: - return MetricsEvent.MANAGE_APPLICATIONS; + return SettingsEnums.MANAGE_APPLICATIONS; case LIST_TYPE_NOTIFICATION: - return MetricsEvent.MANAGE_APPLICATIONS_NOTIFICATIONS; + return SettingsEnums.MANAGE_APPLICATIONS_NOTIFICATIONS; case LIST_TYPE_STORAGE: if (mStorageType == STORAGE_TYPE_MUSIC) { - return MetricsEvent.APPLICATIONS_STORAGE_MUSIC; + return SettingsEnums.APPLICATIONS_STORAGE_MUSIC; } - return MetricsEvent.APPLICATIONS_STORAGE_APPS; + return SettingsEnums.APPLICATIONS_STORAGE_APPS; case LIST_TYPE_GAMES: - return MetricsEvent.APPLICATIONS_STORAGE_GAMES; + return SettingsEnums.APPLICATIONS_STORAGE_GAMES; case LIST_TYPE_MOVIES: - return MetricsEvent.APPLICATIONS_STORAGE_MOVIES; + return SettingsEnums.APPLICATIONS_STORAGE_MOVIES; case LIST_TYPE_PHOTOGRAPHY: - return MetricsEvent.APPLICATIONS_STORAGE_PHOTOS; + return SettingsEnums.APPLICATIONS_STORAGE_PHOTOS; case LIST_TYPE_USAGE_ACCESS: - return MetricsEvent.USAGE_ACCESS; + return SettingsEnums.USAGE_ACCESS; case LIST_TYPE_HIGH_POWER: - return MetricsEvent.APPLICATIONS_HIGH_POWER_APPS; + return SettingsEnums.APPLICATIONS_HIGH_POWER_APPS; case LIST_TYPE_OVERLAY: - return MetricsEvent.SYSTEM_ALERT_WINDOW_APPS; + return SettingsEnums.SYSTEM_ALERT_WINDOW_APPS; case LIST_TYPE_WRITE_SETTINGS: - return MetricsEvent.SYSTEM_ALERT_WINDOW_APPS; + return SettingsEnums.SYSTEM_ALERT_WINDOW_APPS; case LIST_TYPE_MANAGE_SOURCES: - return MetricsEvent.MANAGE_EXTERNAL_SOURCES; - case LIST_TYPE_DIRECTORY_ACCESS: - return MetricsEvent.DIRECTORY_ACCESS; + return SettingsEnums.MANAGE_EXTERNAL_SOURCES; case LIST_TYPE_WIFI_ACCESS: - return MetricsEvent.CONFIGURE_WIFI; + return SettingsEnums.CONFIGURE_WIFI; default: - return MetricsEvent.VIEW_UNKNOWN; + return SettingsEnums.PAGE_UNKNOWN; } } @@ -498,6 +508,8 @@ public class ManageApplications extends InstrumentedFragment outState.putBoolean(EXTRA_SHOW_SYSTEM, mShowSystem); outState.putBoolean(EXTRA_HAS_ENTRIES, mApplications.mHasReceivedLoadEntries); outState.putBoolean(EXTRA_HAS_BRIDGE, mApplications.mHasReceivedBridgeCallback); + outState.putBoolean(EXTRA_EXPAND_SEARCH_VIEW, !mSearchView.isIconified()); + outState.putInt(EXTRA_FILTER_TYPE, mFilter.getFilterType()); if (mApplications != null) { mApplications.onSaveInstanceState(outState); } @@ -569,9 +581,6 @@ public class ManageApplications extends InstrumentedFragment case LIST_TYPE_PHOTOGRAPHY: startAppInfoFragment(AppStorageSettings.class, R.string.storage_photos_videos); break; - case LIST_TYPE_DIRECTORY_ACCESS: - startAppInfoFragment(DirectoryAccessDetails.class, R.string.directory_access); - break; case LIST_TYPE_WIFI_ACCESS: startAppInfoFragment(ChangeWifiStateDetails.class, R.string.change_wifi_state_title); @@ -601,6 +610,16 @@ public class ManageApplications extends InstrumentedFragment mOptionsMenu = menu; inflater.inflate(R.menu.manage_apps, menu); + final MenuItem searchMenuItem = menu.findItem(R.id.search_app_list_menu); + if (searchMenuItem != null) { + mSearchView = (SearchView) searchMenuItem.getActionView(); + mSearchView.setQueryHint(getText(R.string.search_settings)); + mSearchView.setOnQueryTextListener(this); + if (mExpandSearch) { + searchMenuItem.expandActionView(); + } + } + updateOptionsMenu(); } @@ -616,12 +635,32 @@ public class ManageApplications extends InstrumentedFragment @StringRes int getHelpResource() { - if (mListType == LIST_TYPE_MAIN) { - return R.string.help_uri_apps; - } else if (mListType == LIST_TYPE_USAGE_ACCESS) { - return R.string.help_url_usage_access; - } else { - return R.string.help_uri_notifications; + switch (mListType) { + case LIST_TYPE_NOTIFICATION: + return R.string.help_uri_notifications; + case LIST_TYPE_USAGE_ACCESS: + return R.string.help_url_usage_access; + case LIST_TYPE_STORAGE: + return R.string.help_uri_apps_storage; + case LIST_TYPE_HIGH_POWER: + return R.string.help_uri_apps_high_power; + case LIST_TYPE_OVERLAY: + return R.string.help_uri_apps_overlay; + case LIST_TYPE_WRITE_SETTINGS: + return R.string.help_uri_apps_write_settings; + case LIST_TYPE_MANAGE_SOURCES: + return R.string.help_uri_apps_manage_sources; + case LIST_TYPE_GAMES: + return R.string.help_uri_apps_overlay; + case LIST_TYPE_MOVIES: + return R.string.help_uri_apps_movies; + case LIST_TYPE_PHOTOGRAPHY: + return R.string.help_uri_apps_photography; + case LIST_TYPE_WIFI_ACCESS: + return R.string.help_uri_apps_wifi_access; + default: + case LIST_TYPE_MAIN: + return R.string.help_uri_apps; } } @@ -651,41 +690,33 @@ public class ManageApplications extends InstrumentedFragment @Override public boolean onOptionsItemSelected(MenuItem item) { int menuId = item.getItemId(); - switch (item.getItemId()) { - case R.id.sort_order_alpha: - case R.id.sort_order_size: - if (mApplications != null) { - mApplications.rebuild(menuId); - } - break; - case R.id.show_system: - case R.id.hide_system: - mShowSystem = !mShowSystem; - mApplications.rebuild(); - break; - case R.id.reset_app_preferences: - mResetAppsHelper.buildResetDialog(); - return true; - case R.id.advanced: - if (mListType == LIST_TYPE_NOTIFICATION) { - new SubSettingLauncher(getContext()) - .setDestination(ConfigureNotificationSettings.class.getName()) - .setTitle(R.string.configure_notification_settings) - .setSourceMetricsCategory(getMetricsCategory()) - .setResultListener(this, ADVANCED_SETTINGS) - .launch(); - } else { - new SubSettingLauncher(getContext()) - .setDestination(DefaultAppSettings.class.getName()) - .setTitle(R.string.configure_apps) - .setSourceMetricsCategory(getMetricsCategory()) - .setResultListener(this, ADVANCED_SETTINGS) - .launch(); - } - return true; - default: - // Handle the home button - return false; + int i = item.getItemId(); + if (i == R.id.sort_order_alpha || i == R.id.sort_order_size) { + if (mApplications != null) { + mApplications.rebuild(menuId); + } + } else if (i == R.id.show_system || i == R.id.hide_system) { + mShowSystem = !mShowSystem; + mApplications.rebuild(); + } else if (i == R.id.reset_app_preferences) { + mResetAppsHelper.buildResetDialog(); + return true; + } else if (i == R.id.advanced) { + if (mListType == LIST_TYPE_NOTIFICATION) { + new SubSettingLauncher(getContext()) + .setDestination(ConfigureNotificationSettings.class.getName()) + .setTitleRes(R.string.configure_notification_settings) + .setSourceMetricsCategory(getMetricsCategory()) + .setResultListener(this, ADVANCED_SETTINGS) + .launch(); + } else { + Intent intent = new Intent( + android.provider.Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS); + startActivityForResult(intent, ADVANCED_SETTINGS); + } + return true; + } else {// Handle the home button + return false; } updateOptionsMenu(); return true; @@ -717,13 +748,26 @@ public class ManageApplications extends InstrumentedFragment mFilter = mFilterAdapter.getFilter(position); mApplications.setFilter(mFilter); - if (DEBUG) Log.d(TAG, "Selecting filter " + mFilter); + if (DEBUG) { + Log.d(TAG, "Selecting filter " + getContext().getText(mFilter.getTitle())); + } } @Override public void onNothingSelected(AdapterView<?> parent) { } + @Override + public boolean onQueryTextSubmit(String query) { + return false; + } + + @Override + public boolean onQueryTextChange(String newText) { + mApplications.filterSearch(newText); + return false; + } + public void updateView() { updateOptionsMenu(); final Activity host = getActivity(); @@ -746,7 +790,7 @@ public class ManageApplications extends InstrumentedFragment } } - static class FilterSpinnerAdapter extends ArrayAdapter<CharSequence> { + static class FilterSpinnerAdapter extends SettingsSpinnerAdapter<CharSequence> { private final ManageApplications mManageApplications; private final Context mContext; @@ -756,10 +800,9 @@ public class ManageApplications extends InstrumentedFragment private final ArrayList<AppFilterItem> mFilterOptions = new ArrayList<>(); public FilterSpinnerAdapter(ManageApplications manageApplications) { - super(manageApplications.getContext(), R.layout.filter_spinner_item); + super(manageApplications.getContext()); mContext = manageApplications.getContext(); mManageApplications = manageApplications; - setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); } public AppFilterItem getFilter(int position) { @@ -780,20 +823,29 @@ public class ManageApplications extends InstrumentedFragment return; } if (DEBUG) { - Log.d(TAG, "Enabling filter " + filter); + Log.d(TAG, "Enabling filter " + mContext.getText(filter.getTitle())); } mFilterOptions.add(filter); Collections.sort(mFilterOptions); - mManageApplications.mSpinnerHeader.setVisibility( - mFilterOptions.size() > 1 ? View.VISIBLE : View.GONE); + updateFilterView(mFilterOptions.size() > 1); notifyDataSetChanged(); if (mFilterOptions.size() == 1) { if (DEBUG) { - Log.d(TAG, "Auto selecting filter " + filter); + Log.d(TAG, "Auto selecting filter " + filter + " " + mContext.getText( + filter.getTitle())); } mManageApplications.mFilterSpinner.setSelection(0); mManageApplications.onItemSelected(null, null, 0, 0); } + if (mFilterOptions.size() > 1) { + final AppFilterItem previousFilter = AppFilterRegistry.getInstance().get( + mManageApplications.mFilterType); + final int index = mFilterOptions.indexOf(previousFilter); + if (index != -1) { + mManageApplications.mFilterSpinner.setSelection(index); + mManageApplications.onItemSelected(null, null, index, 0); + } + } } public void disableFilter(@AppFilterRegistry.FilterType int filterType) { @@ -802,16 +854,17 @@ public class ManageApplications extends InstrumentedFragment return; } if (DEBUG) { - Log.d(TAG, "Disabling filter " + filter); + Log.d(TAG, "Disabling filter " + filter + " " + mContext.getText( + filter.getTitle())); } Collections.sort(mFilterOptions); - mManageApplications.mSpinnerHeader.setVisibility( - mFilterOptions.size() > 1 ? View.VISIBLE : View.GONE); + updateFilterView(mFilterOptions.size() > 1); notifyDataSetChanged(); if (mManageApplications.mFilter == filter) { if (mFilterOptions.size() > 0) { if (DEBUG) { - Log.d(TAG, "Auto selecting filter " + mFilterOptions.get(0)); + Log.d(TAG, "Auto selecting filter " + mFilterOptions.get(0) + + mContext.getText(mFilterOptions.get(0).getTitle())); } mManageApplications.mFilterSpinner.setSelection(0); mManageApplications.onItemSelected(null, null, 0, 0); @@ -828,6 +881,26 @@ public class ManageApplications extends InstrumentedFragment public CharSequence getItem(int position) { return mContext.getText(mFilterOptions.get(position).getTitle()); } + + @VisibleForTesting + void updateFilterView(boolean hasFilter) { + // If we need to add a floating filter in this screen, we should have an extra top + // padding for putting floating filter view. Otherwise, the content of list will be + // overlapped by floating filter. + if (hasFilter) { + mManageApplications.mSpinnerHeader.setVisibility(View.VISIBLE); + mManageApplications.mRecyclerView.setPadding(0 /* left */, + mContext.getResources().getDimensionPixelSize( + R.dimen.app_bar_height) /* top */, + 0 /* right */, + 0 /* bottom */); + } else { + mManageApplications.mSpinnerHeader.setVisibility(View.GONE); + mManageApplications.mRecyclerView.setPadding(0 /* left */, 0 /* top */, + 0 /* right */, + 0 /* bottom */); + } + } } static class ApplicationsAdapter extends RecyclerView.Adapter<ApplicationViewHolder> @@ -843,9 +916,11 @@ public class ManageApplications extends InstrumentedFragment private final Context mContext; private final AppStateBaseBridge mExtraInfoBridge; private final LoadingViewController mLoadingViewController; + private final IconDrawableFactory mIconDrawableFactory; private AppFilterItem mAppFilter; private ArrayList<ApplicationsState.AppEntry> mEntries; + private ArrayList<ApplicationsState.AppEntry> mOriginalEntries; private boolean mResumed; private int mLastSortMode = -1; private int mWhichSize = SIZE_TOTAL; @@ -853,6 +928,8 @@ public class ManageApplications extends InstrumentedFragment private boolean mHasReceivedLoadEntries; private boolean mHasReceivedBridgeCallback; private FileViewHolderController mExtraViewController; + private SearchFilter mSearchFilter; + private PowerWhitelistBackend mBackend; // This is to remember and restore the last scroll position when this // fragment is paused. We need this special handling because app entries are added gradually @@ -875,7 +952,9 @@ public class ManageApplications extends InstrumentedFragment mManageApplications.mListContainer ); mContext = manageApplications.getActivity(); + mIconDrawableFactory = IconDrawableFactory.newInstance(mContext); mAppFilter = appFilter; + mBackend = PowerWhitelistBackend.getInstance(mContext); if (mManageApplications.mListType == LIST_TYPE_NOTIFICATION) { mExtraInfoBridge = new AppStateNotificationBridge(mContext, mState, this, manageApplications.mUsageStatsManager, @@ -891,8 +970,6 @@ public class ManageApplications extends InstrumentedFragment mExtraInfoBridge = new AppStateWriteSettingsBridge(mContext, mState, this); } else if (mManageApplications.mListType == LIST_TYPE_MANAGE_SOURCES) { mExtraInfoBridge = new AppStateInstallAppsBridge(mContext, mState, this); - } else if (mManageApplications.mListType == LIST_TYPE_DIRECTORY_ACCESS) { - mExtraInfoBridge = new AppStateDirectoryAccessBridge(mState, this); } else if (mManageApplications.mListType == LIST_TYPE_WIFI_ACCESS) { mExtraInfoBridge = new AppStateChangeWifiStateBridge(mContext, mState, this); } else { @@ -932,6 +1009,8 @@ public class ManageApplications extends InstrumentedFragment rebuild(R.id.sort_order_frequent_notification); } else if (FILTER_APPS_RECENT == appFilter.getFilterType()) { rebuild(R.id.sort_order_recent_notification); + } else if (FILTER_APPS_BLOCKED == appFilter.getFilterType()) { + rebuild(R.id.sort_order_alpha); } else { rebuild(); } @@ -999,14 +1078,13 @@ public class ManageApplications extends InstrumentedFragment @Override public ApplicationViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View view; + final View view; if (mManageApplications.mListType == LIST_TYPE_NOTIFICATION) { view = ApplicationViewHolder.newView(parent, true /* twoTarget */); } else { view = ApplicationViewHolder.newView(parent, false /* twoTarget */); } - return new ApplicationViewHolder(view, - shouldUseStableItemHeight(mManageApplications.mListType)); + return new ApplicationViewHolder(view); } @Override @@ -1021,6 +1099,12 @@ public class ManageApplications extends InstrumentedFragment if (!mHasReceivedLoadEntries || (mExtraInfoBridge != null && !mHasReceivedBridgeCallback)) { // Don't rebuild the list until all the app entries are loaded. + if (DEBUG) { + Log.d(TAG, "Not rebuilding until all the app entries loaded." + + " !mHasReceivedLoadEntries=" + !mHasReceivedLoadEntries + + " !mExtraInfoBridgeNull=" + (mExtraInfoBridge != null) + + " !mHasReceivedBridgeCallback=" + !mHasReceivedBridgeCallback); + } return; } ApplicationsState.AppFilter filterObj; @@ -1044,29 +1128,24 @@ public class ManageApplications extends InstrumentedFragment ApplicationsState.FILTER_DOWNLOADED_AND_LAUNCHER); } } - switch (mLastSortMode) { - case R.id.sort_order_size: - switch (mWhichSize) { - case SIZE_INTERNAL: - comparatorObj = ApplicationsState.INTERNAL_SIZE_COMPARATOR; - break; - case SIZE_EXTERNAL: - comparatorObj = ApplicationsState.EXTERNAL_SIZE_COMPARATOR; - break; - default: - comparatorObj = ApplicationsState.SIZE_COMPARATOR; - break; - } - break; - case R.id.sort_order_recent_notification: - comparatorObj = AppStateNotificationBridge.RECENT_NOTIFICATION_COMPARATOR; - break; - case R.id.sort_order_frequent_notification: - comparatorObj = AppStateNotificationBridge.FREQUENCY_NOTIFICATION_COMPARATOR; - break; - default: - comparatorObj = ApplicationsState.ALPHA_COMPARATOR; - break; + if (mLastSortMode == R.id.sort_order_size) { + switch (mWhichSize) { + case SIZE_INTERNAL: + comparatorObj = ApplicationsState.INTERNAL_SIZE_COMPARATOR; + break; + case SIZE_EXTERNAL: + comparatorObj = ApplicationsState.EXTERNAL_SIZE_COMPARATOR; + break; + default: + comparatorObj = ApplicationsState.SIZE_COMPARATOR; + break; + } + } else if (mLastSortMode == R.id.sort_order_recent_notification) { + comparatorObj = AppStateNotificationBridge.RECENT_NOTIFICATION_COMPARATOR; + } else if (mLastSortMode == R.id.sort_order_frequent_notification) { + comparatorObj = AppStateNotificationBridge.FREQUENCY_NOTIFICATION_COMPARATOR; + } else { + comparatorObj = ApplicationsState.ALPHA_COMPARATOR; } filterObj = new CompoundFilter(filterObj, ApplicationsState.FILTER_NOT_HIDE); @@ -1081,17 +1160,16 @@ public class ManageApplications extends InstrumentedFragment } @VisibleForTesting - static boolean shouldUseStableItemHeight(int listType) { - switch (listType) { - case LIST_TYPE_NOTIFICATION: - // Most entries in notification type has no summary. Don't use stable height - // so height is short for most entries. - return false; - default: - // Other types have non-empty summary, so keep the height as we expect summary - // to fill in. - return true; + void filterSearch(String query) { + if (mSearchFilter == null) { + mSearchFilter = new SearchFilter(); } + // If we haven't load apps list completely, don't filter anything. + if(mOriginalEntries == null) { + Log.w(TAG, "Apps haven't loaded completely yet, so nothing can be filtered"); + return; + } + mSearchFilter.filter(query); } private static boolean packageNameEquals(PackageItemInfo info1, PackageItemInfo info2) { @@ -1126,12 +1204,16 @@ public class ManageApplications extends InstrumentedFragment @Override public void onRebuildComplete(ArrayList<AppEntry> entries) { + if (DEBUG) { + Log.d(TAG, "onRebuildComplete"); + } final int filterType = mAppFilter.getFilterType(); if (filterType == FILTER_APPS_POWER_WHITELIST || filterType == FILTER_APPS_POWER_WHITELIST_ALL) { entries = removeDuplicateIgnoringUser(entries); } mEntries = entries; + mOriginalEntries = entries; notifyDataSetChanged(); if (getItemCount() == 0) { mManageApplications.mRecyclerView.setVisibility(View.GONE); @@ -1139,6 +1221,14 @@ public class ManageApplications extends InstrumentedFragment } else { mManageApplications.mEmptyView.setVisibility(View.GONE); mManageApplications.mRecyclerView.setVisibility(View.VISIBLE); + + if (mManageApplications.mSearchView != null + && mManageApplications.mSearchView.isVisibleToUser()) { + final CharSequence query = mManageApplications.mSearchView.getQuery(); + if (!TextUtils.isEmpty(query)) { + filterSearch(query.toString()); + } + } } // Restore the last scroll position if the number of entries added so far is bigger than // it. @@ -1276,8 +1366,9 @@ public class ManageApplications extends InstrumentedFragment return true; } ApplicationsState.AppEntry entry = mEntries.get(position); - return !PowerWhitelistBackend.getInstance(mContext) - .isSysWhitelisted(entry.info.packageName); + + return !mBackend.isSysWhitelisted(entry.info.packageName) + && !mBackend.isDefaultActiveApp(entry.info.packageName); } @Override @@ -1290,8 +1381,7 @@ public class ManageApplications extends InstrumentedFragment ApplicationsState.AppEntry entry = mEntries.get(position); synchronized (entry) { holder.setTitle(entry.label); - mState.ensureIcon(entry); - holder.setIcon(entry.icon); + holder.setIcon(mIconDrawableFactory.getBadgedIcon(entry.info)); updateSummary(holder, entry); updateSwitch(holder, entry); holder.updateDisableView(entry.info); @@ -1304,10 +1394,10 @@ public class ManageApplications extends InstrumentedFragment private void updateSummary(ApplicationViewHolder holder, AppEntry entry) { switch (mManageApplications.mListType) { case LIST_TYPE_NOTIFICATION: - if (entry.extraInfo != null) { + if (entry.extraInfo != null + && entry.extraInfo instanceof NotificationsSentState) { holder.setSummary(AppStateNotificationBridge.getSummary(mContext, - (NotificationsSentState) entry.extraInfo, - (mLastSortMode == R.id.sort_order_recent_notification))); + (NotificationsSentState) entry.extraInfo, mLastSortMode)); } else { holder.setSummary(null); } @@ -1334,9 +1424,6 @@ public class ManageApplications extends InstrumentedFragment case LIST_TYPE_MANAGE_SOURCES: holder.setSummary(ExternalSourcesDetails.getPreferenceSummary(mContext, entry)); break; - case LIST_TYPE_DIRECTORY_ACCESS: - holder.setSummary(null); - break; case LIST_TYPE_WIFI_ACCESS: holder.setSummary(ChangeWifiStateDetails.getSummary(mContext, entry)); break; @@ -1353,10 +1440,10 @@ public class ManageApplications extends InstrumentedFragment .getSwitchOnClickListener(entry), AppStateNotificationBridge.enableSwitch(entry), AppStateNotificationBridge.checkSwitch(entry)); - if (entry.extraInfo != null) { + if (entry.extraInfo != null + && entry.extraInfo instanceof NotificationsSentState) { holder.setSummary(AppStateNotificationBridge.getSummary(mContext, - (NotificationsSentState) entry.extraInfo, - (mLastSortMode == R.id.sort_order_recent_notification))); + (NotificationsSentState) entry.extraInfo, mLastSortMode)); } else { holder.setSummary(null); } @@ -1395,6 +1482,38 @@ public class ManageApplications extends InstrumentedFragment } } } + + /** + * An array filter that constrains the content of the array adapter with a substring. + * Item that does not contains the specified substring will be removed from the list.</p> + */ + private class SearchFilter extends Filter { + @WorkerThread + @Override + protected FilterResults performFiltering(CharSequence query) { + final ArrayList<ApplicationsState.AppEntry> matchedEntries; + if (TextUtils.isEmpty(query)) { + matchedEntries = mOriginalEntries; + } else { + matchedEntries = new ArrayList<>(); + for (ApplicationsState.AppEntry entry : mOriginalEntries) { + if (entry.label.toLowerCase().contains(query.toString().toLowerCase())) { + matchedEntries.add(entry); + } + } + } + final FilterResults results = new FilterResults(); + results.values = matchedEntries; + results.count = matchedEntries.size(); + return results; + } + + @Override + protected void publishResults(CharSequence constraint, FilterResults results) { + mEntries = (ArrayList<ApplicationsState.AppEntry>) results.values; + notifyDataSetChanged(); + } + } } private static class SummaryProvider implements SummaryLoader.SummaryProvider { @@ -1411,7 +1530,7 @@ public class ManageApplications extends InstrumentedFragment public void setListening(boolean listening) { if (listening) { new InstalledAppCounter(mContext, InstalledAppCounter.IGNORE_INSTALL_REASON, - new PackageManagerWrapper(mContext.getPackageManager())) { + mContext.getPackageManager()) { @Override protected void onCountComplete(int num) { mLoader.setSummary(SummaryProvider.this, diff --git a/src/com/android/settings/applications/manageapplications/MusicViewHolderController.java b/src/com/android/settings/applications/manageapplications/MusicViewHolderController.java index 2f33c1e336..53c778c7d6 100644 --- a/src/com/android/settings/applications/manageapplications/MusicViewHolderController.java +++ b/src/com/android/settings/applications/manageapplications/MusicViewHolderController.java @@ -16,15 +16,16 @@ package com.android.settings.applications.manageapplications; -import android.app.Fragment; import android.content.Context; import android.content.Intent; import android.os.UserHandle; import android.provider.DocumentsContract; -import androidx.annotation.WorkerThread; import android.text.format.Formatter; import android.util.Log; +import androidx.annotation.WorkerThread; +import androidx.fragment.app.Fragment; + import com.android.settings.R; import com.android.settings.Utils; import com.android.settingslib.applications.StorageStatsSource; diff --git a/src/com/android/settings/applications/manageapplications/PhotosViewHolderController.java b/src/com/android/settings/applications/manageapplications/PhotosViewHolderController.java index 6b7c8d6ce0..c7a1a1cb49 100644 --- a/src/com/android/settings/applications/manageapplications/PhotosViewHolderController.java +++ b/src/com/android/settings/applications/manageapplications/PhotosViewHolderController.java @@ -16,14 +16,15 @@ package com.android.settings.applications.manageapplications; -import android.app.Fragment; import android.content.Context; import android.content.Intent; import android.os.UserHandle; -import androidx.annotation.WorkerThread; import android.text.format.Formatter; import android.util.Log; +import androidx.annotation.WorkerThread; +import androidx.fragment.app.Fragment; + import com.android.settings.R; import com.android.settings.Utils; import com.android.settingslib.applications.StorageStatsSource; diff --git a/src/com/android/settings/applications/manageapplications/ResetAppPrefPreferenceController.java b/src/com/android/settings/applications/manageapplications/ResetAppPrefPreferenceController.java index 3fc3c1dbf4..6ef45935d4 100644 --- a/src/com/android/settings/applications/manageapplications/ResetAppPrefPreferenceController.java +++ b/src/com/android/settings/applications/manageapplications/ResetAppPrefPreferenceController.java @@ -18,9 +18,10 @@ package com.android.settings.applications.manageapplications; import android.content.Context; import android.os.Bundle; -import androidx.preference.Preference; import android.text.TextUtils; +import androidx.preference.Preference; + import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; diff --git a/src/com/android/settings/applications/manageapplications/ResetAppsHelper.java b/src/com/android/settings/applications/manageapplications/ResetAppsHelper.java index 686e027928..92c0958c64 100644 --- a/src/com/android/settings/applications/manageapplications/ResetAppsHelper.java +++ b/src/com/android/settings/applications/manageapplications/ResetAppsHelper.java @@ -19,7 +19,6 @@ import static android.net.NetworkPolicyManager.POLICY_NONE; import static android.net.NetworkPolicyManager.POLICY_REJECT_METERED_BACKGROUND; import android.app.ActivityManager; -import android.app.AlertDialog; import android.app.AppOpsManager; import android.app.INotificationManager; import android.content.Context; @@ -33,7 +32,8 @@ import android.os.Bundle; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; -import android.webkit.IWebViewUpdateService; + +import androidx.appcompat.app.AlertDialog; import com.android.settings.R; @@ -47,7 +47,6 @@ public class ResetAppsHelper implements DialogInterface.OnClickListener, private final PackageManager mPm; private final IPackageManager mIPm; private final INotificationManager mNm; - private final IWebViewUpdateService mWvus; private final NetworkPolicyManager mNpm; private final AppOpsManager mAom; private final Context mContext; @@ -60,7 +59,6 @@ public class ResetAppsHelper implements DialogInterface.OnClickListener, mIPm = IPackageManager.Stub.asInterface(ServiceManager.getService("package")); mNm = INotificationManager.Stub.asInterface( ServiceManager.getService(Context.NOTIFICATION_SERVICE)); - mWvus = IWebViewUpdateService.Stub.asInterface(ServiceManager.getService("webviewupdate")); mNpm = NetworkPolicyManager.from(context); mAom = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); } @@ -116,13 +114,12 @@ public class ResetAppsHelper implements DialogInterface.OnClickListener, for (int i = 0; i < apps.size(); i++) { ApplicationInfo app = apps.get(i); try { - mNm.setNotificationsEnabledForPackage(app.packageName, app.uid, true); + mNm.clearData(app.packageName, app.uid, false); } catch (android.os.RemoteException ex) { } if (!app.enabled) { if (mPm.getApplicationEnabledSetting(app.packageName) - == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER - && !isNonEnableableFallback(app.packageName)) { + == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER) { mPm.setApplicationEnabledSetting(app.packageName, PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, PackageManager.DONT_KILL_APP); @@ -146,12 +143,4 @@ public class ResetAppsHelper implements DialogInterface.OnClickListener, } }); } - - private boolean isNonEnableableFallback(String packageName) { - try { - return mWvus.isFallbackPackage(packageName); - } catch (RemoteException e) { - throw new RuntimeException(e); - } - } } diff --git a/src/com/android/settings/applications/managedomainurls/DomainAppPreference.java b/src/com/android/settings/applications/managedomainurls/DomainAppPreference.java new file mode 100644 index 0000000000..bc7b85555e --- /dev/null +++ b/src/com/android/settings/applications/managedomainurls/DomainAppPreference.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2018 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.applications.managedomainurls; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.UserHandle; +import android.util.ArraySet; +import android.util.IconDrawableFactory; +import android.view.View; + +import androidx.preference.PreferenceViewHolder; + +import com.android.settings.R; +import com.android.settings.Utils; +import com.android.settingslib.applications.ApplicationsState.AppEntry; +import com.android.settingslib.widget.apppreference.AppPreference; + +public class DomainAppPreference extends AppPreference { + + private final AppEntry mEntry; + private final PackageManager mPm; + private final IconDrawableFactory mIconDrawableFactory; + + public DomainAppPreference(final Context context, IconDrawableFactory iconFactory, + AppEntry entry) { + super(context); + mIconDrawableFactory = iconFactory; + mPm = context.getPackageManager(); + mEntry = entry; + mEntry.ensureLabel(getContext()); + + setState(); + } + + public void reuse() { + setState(); + notifyChanged(); + } + + public AppEntry getEntry() { + return mEntry; + } + + private void setState() { + setTitle(mEntry.label); + setIcon(mIconDrawableFactory.getBadgedIcon(mEntry.info)); + setSummary(getDomainsSummary(mEntry.info.packageName)); + } + + private CharSequence getDomainsSummary(String packageName) { + // If the user has explicitly said "no" for this package, that's the + // string we should show. + int domainStatus = + mPm.getIntentVerificationStatusAsUser(packageName, UserHandle.myUserId()); + if (domainStatus == PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_NEVER) { + return getContext().getText(R.string.domain_urls_summary_none); + } + // Otherwise, ask package manager for the domains for this package, + // and show the first one (or none if there aren't any). + final ArraySet<String> result = Utils.getHandledDomains(mPm, packageName); + if (result.isEmpty()) { + return getContext().getText(R.string.domain_urls_summary_none); + } else if (result.size() == 1) { + return getContext().getString(R.string.domain_urls_summary_one, result.valueAt(0)); + } else { + return getContext().getString(R.string.domain_urls_summary_some, result.valueAt(0)); + } + } +} diff --git a/src/com/android/settings/applications/managedomainurls/DomainAppPreferenceController.java b/src/com/android/settings/applications/managedomainurls/DomainAppPreferenceController.java new file mode 100644 index 0000000000..c859db204e --- /dev/null +++ b/src/com/android/settings/applications/managedomainurls/DomainAppPreferenceController.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2018 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.applications.managedomainurls; + +import android.app.Application; +import android.content.Context; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.IconDrawableFactory; + +import androidx.preference.Preference; +import androidx.preference.PreferenceGroup; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.applications.AppInfoBase; +import com.android.settings.applications.AppLaunchSettings; +import com.android.settings.core.BasePreferenceController; +import com.android.settingslib.applications.ApplicationsState; +import com.android.settingslib.applications.ApplicationsState.AppEntry; + +import java.util.ArrayList; +import java.util.Map; + +public class DomainAppPreferenceController extends BasePreferenceController implements + ApplicationsState.Callbacks { + + // constant value that can be used to check return code from sub activity. + private static final int INSTALLED_APP_DETAILS = 1; + + private int mMetricsCategory; + private ApplicationsState mApplicationsState; + private ApplicationsState.Session mSession; + private ManageDomainUrls mFragment; + private PreferenceGroup mDomainAppList; + private Map<String, Preference> mPreferenceCache; + + public DomainAppPreferenceController(Context context, String key) { + super(context, key); + mApplicationsState = ApplicationsState.getInstance( + (Application) mContext.getApplicationContext()); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mDomainAppList = screen.findPreference(getPreferenceKey()); + } + + @Override + public boolean handlePreferenceTreeClick(Preference preference) { + if (preference instanceof DomainAppPreference) { + ApplicationsState.AppEntry entry = ((DomainAppPreference) preference).getEntry(); + AppInfoBase.startAppInfoFragment(AppLaunchSettings.class, R.string.auto_launch_label, + entry.info.packageName, entry.info.uid, mFragment, + INSTALLED_APP_DETAILS, mMetricsCategory); + return true; + } + return false; + } + + public void setFragment(ManageDomainUrls fragment) { + mFragment = fragment; + mMetricsCategory = fragment.getMetricsCategory(); + mSession = mApplicationsState.newSession(this, mFragment.getSettingsLifecycle()); + } + + @Override + public void onRunningStateChanged(boolean running) { + } + + @Override + public void onPackageListChanged() { + } + + @Override + public void onRebuildComplete(ArrayList<AppEntry> apps) { + if (mContext == null) { + return; + } + rebuildAppList(mDomainAppList, apps); + } + + @Override + public void onPackageIconChanged() { + } + + @Override + public void onPackageSizeChanged(String packageName) { + } + + @Override + public void onAllSizesComputed() { + } + + @Override + public void onLauncherInfoChanged() { + } + + @Override + public void onLoadEntriesCompleted() { + rebuild(); + } + + private void cacheAllPrefs(PreferenceGroup group) { + mPreferenceCache = new ArrayMap(); + final int count = group.getPreferenceCount(); + for (int i = 0; i < count; i++) { + Preference p = group.getPreference(i); + if (TextUtils.isEmpty(p.getKey())) { + continue; + } + mPreferenceCache.put(p.getKey(), p); + } + } + + private Preference getCachedPreference(String key) { + return mPreferenceCache != null ? mPreferenceCache.remove(key) : null; + } + + private void removeCachedPrefs(PreferenceGroup group) { + for (Preference p : mPreferenceCache.values()) { + group.removePreference(p); + } + mPreferenceCache = null; + } + + private void rebuild() { + final ArrayList<AppEntry> apps = mSession.rebuild( + ApplicationsState.FILTER_WITH_DOMAIN_URLS, ApplicationsState.ALPHA_COMPARATOR); + if (apps != null) { + onRebuildComplete(apps); + } + } + + private void rebuildAppList(PreferenceGroup group, ArrayList<AppEntry> apps) { + cacheAllPrefs(group); + final int size = apps.size(); + final Context context = group.getContext(); + final IconDrawableFactory iconDrawableFactory = IconDrawableFactory.newInstance(context); + for (int i = 0; i < size; i++) { + final AppEntry entry = apps.get(i); + final String key = entry.info.packageName + "|" + entry.info.uid; + DomainAppPreference preference = (DomainAppPreference) getCachedPreference(key); + if (preference == null) { + preference = new DomainAppPreference(context, iconDrawableFactory, entry); + preference.setKey(key); + group.addPreference(preference); + } else { + preference.reuse(); + } + preference.setOrder(i); + } + removeCachedPrefs(group); + } +} diff --git a/src/com/android/settings/applications/managedomainurls/InstantAppAccountPreferenceController.java b/src/com/android/settings/applications/managedomainurls/InstantAppAccountPreferenceController.java new file mode 100644 index 0000000000..86c88c9f05 --- /dev/null +++ b/src/com/android/settings/applications/managedomainurls/InstantAppAccountPreferenceController.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2018 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.applications.managedomainurls; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; + +import androidx.preference.Preference; + +import com.android.settings.core.BasePreferenceController; + +public class InstantAppAccountPreferenceController extends BasePreferenceController { + + private Intent mLaunchIntent; + + public InstantAppAccountPreferenceController(Context context, String key) { + super(context, key); + initAppSettingsIntent(); + } + + @Override + public int getAvailabilityStatus() { + if (mLaunchIntent == null || WebActionCategoryController.isDisableWebActions(mContext)) { + return UNSUPPORTED_ON_DEVICE; + } else { + return AVAILABLE; + } + } + + @Override + public boolean handlePreferenceTreeClick(Preference preference) { + if (!getPreferenceKey().equals(preference.getKey())) { + return false; + } + // TODO: Make this button actually launch the account chooser. + if (mLaunchIntent != null) { + mContext.startActivity(mLaunchIntent); + } + return true; + } + + private void initAppSettingsIntent() { + // Determine whether we should show the instant apps account chooser setting + ComponentName instantAppSettingsComponent = + mContext.getPackageManager().getInstantAppResolverSettingsComponent(); + Intent instantAppSettingsIntent = null; + if (instantAppSettingsComponent != null) { + instantAppSettingsIntent = + new Intent().setComponent(instantAppSettingsComponent); + } + + if (instantAppSettingsIntent != null) { + mLaunchIntent = instantAppSettingsIntent; + } + } +} diff --git a/src/com/android/settings/applications/managedomainurls/InstantAppWebActionPreferenceController.java b/src/com/android/settings/applications/managedomainurls/InstantAppWebActionPreferenceController.java new file mode 100644 index 0000000000..77abfe7117 --- /dev/null +++ b/src/com/android/settings/applications/managedomainurls/InstantAppWebActionPreferenceController.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2018 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.applications.managedomainurls; + +import android.content.Context; +import android.provider.Settings; + +import com.android.settings.core.TogglePreferenceController; + +public class InstantAppWebActionPreferenceController extends TogglePreferenceController { + + public InstantAppWebActionPreferenceController(Context context, String key) { + super(context, key); + } + + @Override + public int getAvailabilityStatus() { + return WebActionCategoryController.isDisableWebActions(mContext) + ? UNSUPPORTED_ON_DEVICE + : AVAILABLE; + } + + public boolean isChecked() { + return Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.INSTANT_APPS_ENABLED, 1) == 1; + } + + public boolean setChecked(boolean isChecked) { + return Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.INSTANT_APPS_ENABLED, isChecked ? 1 : 0); + } +} diff --git a/src/com/android/settings/display/AmbientDisplaySettings.java b/src/com/android/settings/applications/managedomainurls/ManageDomainUrls.java index 8745e3ff18..010bc94c89 100644 --- a/src/com/android/settings/display/AmbientDisplaySettings.java +++ b/src/com/android/settings/applications/managedomainurls/ManageDomainUrls.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 The Android Open Source Project + * Copyright (C) 2018 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. @@ -14,63 +14,51 @@ * limitations under the License. */ -package com.android.settings.display; +package com.android.settings.applications.managedomainurls; +import static com.android.settingslib.search.SearchIndexable.MOBILE; + +import android.app.settings.SettingsEnums; import android.content.Context; -import android.os.UserHandle; import android.provider.SearchIndexableResource; -import com.android.internal.hardware.AmbientDisplayConfiguration; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; -import com.android.settings.gestures.DoubleTapScreenPreferenceController; -import com.android.settings.gestures.PickupGesturePreferenceController; -import com.android.settings.overlay.FeatureFactory; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; -import com.android.settingslib.core.AbstractPreferenceController; -import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; -import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.List; /** - * Settings screen for Ambient display. + * Activity to manage how Android handles URL resolution. Includes both per-app + * handling as well as system handling for Web Actions. */ -public class AmbientDisplaySettings extends DashboardFragment { - - public static final String KEY_AMBIENT_DISPLAY_ALWAYS_ON = "ambient_display_always_on"; +@SearchIndexable(forTarget = MOBILE) +public class ManageDomainUrls extends DashboardFragment { - private static final String TAG = "AmbientDisplaySettings"; - - private AmbientDisplayConfiguration mConfig; + private static final String TAG = "ManageDomainUrls"; @Override - public void onAttach(Context context) { - super.onAttach(context); - use(AmbientDisplayAlwaysOnPreferenceController.class) - .setConfig(getConfig(context)) - .setCallback(this::updatePreferenceStates); - use(AmbientDisplayNotificationsPreferenceController.class).setConfig(getConfig(context)); - use(DoubleTapScreenPreferenceController.class).setConfig(getConfig(context)); - use(PickupGesturePreferenceController.class).setConfig(getConfig(context)); + protected String getLogTag() { + return TAG; } @Override - protected String getLogTag() { - return TAG; + public void onAttach(Context context) { + super.onAttach(context); + use(DomainAppPreferenceController.class).setFragment(this); } @Override protected int getPreferenceScreenResId() { - return R.xml.ambient_display_settings; + return R.xml.manage_domain_url_settings; } @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.AMBIENT_DISPLAY_SETTINGS; + return SettingsEnums.MANAGE_DOMAIN_URLS; } public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = @@ -81,16 +69,9 @@ public class AmbientDisplaySettings extends DashboardFragment { final ArrayList<SearchIndexableResource> result = new ArrayList<>(); final SearchIndexableResource sir = new SearchIndexableResource(context); - sir.xmlResId = R.xml.ambient_display_settings; + sir.xmlResId = R.xml.manage_domain_url_settings; result.add(sir); return result; } }; - - private AmbientDisplayConfiguration getConfig(Context context) { - if (mConfig == null) { - mConfig = new AmbientDisplayConfiguration(context); - } - return mConfig; - } } diff --git a/src/com/android/settings/applications/managedomainurls/WebActionCategoryController.java b/src/com/android/settings/applications/managedomainurls/WebActionCategoryController.java new file mode 100644 index 0000000000..5aa57db75c --- /dev/null +++ b/src/com/android/settings/applications/managedomainurls/WebActionCategoryController.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2018 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.applications.managedomainurls; + +import android.content.Context; +import android.provider.Settings; + +import com.android.settings.core.BasePreferenceController; + +public class WebActionCategoryController extends BasePreferenceController { + + public WebActionCategoryController(Context context, String key) { + super(context, key); + } + + @Override + public int getAvailabilityStatus() { + return isDisableWebActions(mContext) ? UNSUPPORTED_ON_DEVICE : AVAILABLE; + } + + public static boolean isDisableWebActions(Context context) { + return Settings.Global.getInt(context.getContentResolver(), + Settings.Global.ENABLE_EPHEMERAL_FEATURE, 1) == 0; + } +} diff --git a/src/com/android/settings/applications/DataSaverController.java b/src/com/android/settings/applications/specialaccess/DataSaverController.java index afe7cd64cd..c169d7fc89 100644 --- a/src/com/android/settings/applications/DataSaverController.java +++ b/src/com/android/settings/applications/specialaccess/DataSaverController.java @@ -15,26 +15,23 @@ */ -package com.android.settings.applications; +package com.android.settings.applications.specialaccess; import android.content.Context; -import androidx.annotation.VisibleForTesting; -import com.android.settings.core.BasePreferenceController; import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; public class DataSaverController extends BasePreferenceController { - @VisibleForTesting static final String KEY_DATA_SAVER = "data_saver"; - - public DataSaverController(Context context) { - super(context, KEY_DATA_SAVER); + public DataSaverController(Context context, String key) { + super(context, key); } @AvailabilityStatus public int getAvailabilityStatus() { return mContext.getResources().getBoolean(R.bool.config_show_data_saver) - ? AVAILABLE + ? AVAILABLE_UNSEARCHABLE : UNSUPPORTED_ON_DEVICE; } }
\ No newline at end of file diff --git a/src/com/android/settings/applications/specialaccess/DefaultPaymentSettingsPreferenceController.java b/src/com/android/settings/applications/specialaccess/DefaultPaymentSettingsPreferenceController.java new file mode 100644 index 0000000000..dbdc9fef6b --- /dev/null +++ b/src/com/android/settings/applications/specialaccess/DefaultPaymentSettingsPreferenceController.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2017 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.applications.specialaccess; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.nfc.NfcAdapter; +import android.os.UserManager; + +import com.android.settings.core.BasePreferenceController; + +public class DefaultPaymentSettingsPreferenceController extends BasePreferenceController { + + private final NfcAdapter mNfcAdapter; + private final PackageManager mPackageManager; + private final UserManager mUserManager; + + public DefaultPaymentSettingsPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + + mPackageManager = context.getPackageManager(); + mUserManager = context.getSystemService(UserManager.class); + mNfcAdapter = NfcAdapter.getDefaultAdapter(mContext); + } + + @Override + public int getAvailabilityStatus() { + if (!mPackageManager.hasSystemFeature(PackageManager.FEATURE_NFC) + || !mPackageManager.hasSystemFeature( + PackageManager.FEATURE_NFC_HOST_CARD_EMULATION)) { + return UNSUPPORTED_ON_DEVICE; + } + if (!mUserManager.isAdminUser()) { + return DISABLED_FOR_USER; + } + if (mNfcAdapter == null || !mNfcAdapter.isEnabled()) { + return CONDITIONALLY_UNAVAILABLE; + } + return AVAILABLE; + } +} diff --git a/src/com/android/settings/applications/HighPowerAppsController.java b/src/com/android/settings/applications/specialaccess/HighPowerAppsController.java index 39b8451631..b893b88760 100644 --- a/src/com/android/settings/applications/HighPowerAppsController.java +++ b/src/com/android/settings/applications/specialaccess/HighPowerAppsController.java @@ -14,20 +14,17 @@ * limitations under the License. */ -package com.android.settings.applications; +package com.android.settings.applications.specialaccess; import android.content.Context; -import androidx.annotation.VisibleForTesting; -import com.android.settings.core.BasePreferenceController; import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; public class HighPowerAppsController extends BasePreferenceController { - @VisibleForTesting static final String KEY_HIGH_POWER_APPS = "high_power_apps"; - - public HighPowerAppsController(Context context) { - super(context, KEY_HIGH_POWER_APPS); + public HighPowerAppsController(Context context, String key) { + super(context, key); } @AvailabilityStatus diff --git a/src/com/android/settings/applications/specialaccess/MoreSpecialAccessPreferenceController.java b/src/com/android/settings/applications/specialaccess/MoreSpecialAccessPreferenceController.java new file mode 100644 index 0000000000..10d5c36226 --- /dev/null +++ b/src/com/android/settings/applications/specialaccess/MoreSpecialAccessPreferenceController.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2019 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.applications.specialaccess; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.text.TextUtils; + +import androidx.preference.Preference; + +import com.android.settings.core.BasePreferenceController; + +public class MoreSpecialAccessPreferenceController extends BasePreferenceController { + + private final Intent mIntent; + + public MoreSpecialAccessPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + + final PackageManager packageManager = context.getPackageManager(); + final String packageName = packageManager.getPermissionControllerPackageName(); + if (packageName != null) { + Intent intent = new Intent(Intent.ACTION_MANAGE_SPECIAL_APP_ACCESSES) + .setPackage(packageName); + ResolveInfo resolveInfo = packageManager.resolveActivity(intent, + PackageManager.MATCH_DEFAULT_ONLY); + mIntent = resolveInfo != null ? intent : null; + } else { + mIntent = null; + } + } + + @Override + public int getAvailabilityStatus() { + return mIntent != null ? AVAILABLE_UNSEARCHABLE : UNSUPPORTED_ON_DEVICE; + } + + @Override + public boolean handlePreferenceTreeClick(Preference preference) { + if (TextUtils.equals(preference.getKey(), mPreferenceKey)) { + if (mIntent != null) { + mContext.startActivity(mIntent); + } + return true; + } + return false; + } +} diff --git a/src/com/android/settings/applications/specialaccess/SpecialAccessSettings.java b/src/com/android/settings/applications/specialaccess/SpecialAccessSettings.java new file mode 100644 index 0000000000..4d9a42786a --- /dev/null +++ b/src/com/android/settings/applications/specialaccess/SpecialAccessSettings.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.applications.specialaccess; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.provider.SearchIndexableResource; + +import com.android.settings.R; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; +import com.android.settingslib.search.SearchIndexable; + +import java.util.ArrayList; +import java.util.List; + +@SearchIndexable +public class SpecialAccessSettings extends DashboardFragment { + + private static final String TAG = "SpecialAccessSettings"; + + @Override + protected String getLogTag() { + return TAG; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.special_access; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.SPECIAL_ACCESS; + } + + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, + boolean enabled) { + final ArrayList<SearchIndexableResource> result = new ArrayList<>(); + + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.special_access; + result.add(sir); + return result; + } + }; +} diff --git a/src/com/android/settings/applications/specialaccess/SystemAlertWindowPreferenceController.java b/src/com/android/settings/applications/specialaccess/SystemAlertWindowPreferenceController.java new file mode 100644 index 0000000000..5d9e8b612b --- /dev/null +++ b/src/com/android/settings/applications/specialaccess/SystemAlertWindowPreferenceController.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2018 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.applications.specialaccess; + +import static com.android.settings.Utils.isSystemAlertWindowEnabled; + +import android.app.ActivityManager; +import android.content.Context; +import android.os.Build; + +import com.android.settings.core.BasePreferenceController; + +public class SystemAlertWindowPreferenceController extends BasePreferenceController { + public SystemAlertWindowPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public int getAvailabilityStatus() { + return isSystemAlertWindowEnabled(mContext) + ? AVAILABLE_UNSEARCHABLE : UNSUPPORTED_ON_DEVICE ; + } +} diff --git a/src/com/android/settings/DeviceAdminAdd.java b/src/com/android/settings/applications/specialaccess/deviceadmin/DeviceAdminAdd.java index 72f4aa32ca..8b06975bc0 100644 --- a/src/com/android/settings/DeviceAdminAdd.java +++ b/src/com/android/settings/applications/specialaccess/deviceadmin/DeviceAdminAdd.java @@ -14,16 +14,16 @@ * limitations under the License. */ -package com.android.settings; +package com.android.settings.applications.specialaccess.deviceadmin; import android.app.Activity; import android.app.ActivityManager; -import android.app.AlertDialog; import android.app.AppOpsManager; import android.app.Dialog; import android.app.admin.DeviceAdminInfo; import android.app.admin.DeviceAdminReceiver; import android.app.admin.DevicePolicyManager; +import android.app.settings.SettingsEnums; import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; @@ -46,6 +46,7 @@ import android.os.UserHandle; import android.os.UserManager; import android.text.TextUtils; import android.text.TextUtils.TruncateAt; +import android.text.method.ScrollingMovementMethod; import android.util.EventLog; import android.util.Log; import android.view.Display; @@ -58,12 +59,16 @@ import android.widget.Button; import android.widget.ImageView; import android.widget.TextView; -import com.android.internal.logging.nano.MetricsProto; +import androidx.appcompat.app.AlertDialog; + +import com.android.settings.EventLogTags; +import com.android.settings.R; import com.android.settings.fuelgauge.BatteryUtils; import com.android.settings.overlay.FeatureFactory; import com.android.settings.users.UserDialogs; import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; +import com.android.settingslib.RestrictedLockUtilsInternal; import org.xmlpull.v1.XmlPullParserException; @@ -269,15 +274,63 @@ public class DeviceAdminAdd extends Activity { } } - // If we're trying to add a profile owner and user setup hasn't completed yet, no - // need to prompt for permission. Just add and finish. - if (mAddingProfileOwner && !mDPM.hasUserSetupCompleted()) { - addAndFinish(); - return; - } - mAddMsgText = getIntent().getCharSequenceExtra(DevicePolicyManager.EXTRA_ADD_EXPLANATION); + if (mAddingProfileOwner) { + // If we're trying to add a profile owner and user setup hasn't completed yet, no + // need to prompt for permission. Just add and finish + if (!mDPM.hasUserSetupCompleted()) { + addAndFinish(); + return; + } + + // othewise, only the defined default supervision profile owner can be set after user + // setup. + final String supervisor = getString( + com.android.internal.R.string.config_defaultSupervisionProfileOwnerComponent); + if (supervisor == null) { + Log.w(TAG, "Unable to set profile owner post-setup, no default supervisor" + + "profile owner defined"); + finish(); + return; + } + + final ComponentName supervisorComponent = ComponentName.unflattenFromString( + supervisor); + if (who.compareTo(supervisorComponent) != 0) { + Log.w(TAG, "Unable to set non-default profile owner post-setup " + who); + finish(); + return; + } + + // Build and show the simplified dialog + final Dialog dialog = new AlertDialog.Builder(this) + .setTitle(getText(R.string.profile_owner_add_title_simplified)) + .setView(R.layout.profile_owner_add) + .setPositiveButton(R.string.allow, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + addAndFinish(); + } + }) + .setNegativeButton(R.string.cancel, null) + .setOnDismissListener(new DialogInterface.OnDismissListener() { + public void onDismiss(DialogInterface dialogInterface) { + finish(); + } + }) + .create(); + dialog.show(); + + mActionButton = ((AlertDialog) dialog).getButton(DialogInterface.BUTTON_POSITIVE); + mActionButton.setFilterTouchesWhenObscured(true); + mAddMsg = dialog.findViewById(R.id.add_msg_simplified); + mAddMsg.setMovementMethod(new ScrollingMovementMethod()); + mAddMsg.setText(mAddMsgText); + mAdminWarning = dialog.findViewById(R.id.admin_warning_simplified); + mAdminWarning.setText(getString(R.string.device_admin_warning_simplified, + mProfileOwnerName)); + return; + } setContentView(R.layout.device_admin_add); mAdminIcon = (ImageView)findViewById(R.id.admin_icon); @@ -453,10 +506,7 @@ public class DeviceAdminAdd extends Activity { void unrestrictAppIfPossible(BatteryUtils batteryUtils) { // Unrestrict admin app if it is already been restricted final String packageName = mDeviceAdmin.getComponent().getPackageName(); - final int uid = batteryUtils.getPackageUid(packageName); - if (batteryUtils.isForceAppStandbyEnabled(uid, packageName)) { - batteryUtils.setForceAppStandby(uid, packageName, AppOpsManager.MODE_ALLOWED); - } + batteryUtils.clearForceAppStandby(packageName); } void continueRemoveAction(CharSequence msg) { @@ -486,16 +536,23 @@ public class DeviceAdminAdd extends Activity { } void logSpecialPermissionChange(boolean allow, String packageName) { - int logCategory = allow ? MetricsProto.MetricsEvent.APP_SPECIAL_PERMISSION_ADMIN_ALLOW : - MetricsProto.MetricsEvent.APP_SPECIAL_PERMISSION_ADMIN_DENY; - FeatureFactory.getFactory(this).getMetricsFeatureProvider().action(this, logCategory, packageName); + int logCategory = allow ? SettingsEnums.APP_SPECIAL_PERMISSION_ADMIN_ALLOW : + SettingsEnums.APP_SPECIAL_PERMISSION_ADMIN_DENY; + FeatureFactory.getFactory(this).getMetricsFeatureProvider().action( + SettingsEnums.PAGE_UNKNOWN, + logCategory, + SettingsEnums.PAGE_UNKNOWN, + packageName, + 0); } @Override protected void onResume() { super.onResume(); mActionButton.setEnabled(true); - updateInterface(); + if (!mAddingProfileOwner) { + updateInterface(); + } // As long as we are running, don't let anyone overlay stuff on top of the screen. mAppOps.setUserRestriction(AppOpsManager.OP_SYSTEM_ALERT_WINDOW, true, mToken); mAppOps.setUserRestriction(AppOpsManager.OP_TOAST_WINDOW, true, mToken); @@ -565,9 +622,6 @@ public class DeviceAdminAdd extends Activity { } catch (Resources.NotFoundException e) { mAdminDescription.setVisibility(View.GONE); } - if (mAddingProfileOwner) { - mProfileOwnerWarning.setVisibility(View.VISIBLE); - } if (mAddMsgText != null) { mAddMsg.setText(mAddMsgText); mAddMsg.setVisibility(View.VISIBLE); @@ -628,11 +682,7 @@ public class DeviceAdminAdd extends Activity { addDeviceAdminPolicies(true /* showDescription */); mAdminWarning.setText(getString(R.string.device_admin_warning, mDeviceAdmin.getActivityInfo().applicationInfo.loadLabel(getPackageManager()))); - if (mAddingProfileOwner) { - setTitle(getText(R.string.profile_owner_add_title)); - } else { - setTitle(getText(R.string.add_device_admin_msg)); - } + setTitle(getText(R.string.add_device_admin_msg)); mActionButton.setText(getText(R.string.add_device_admin)); if (isAdminUninstallable()) { mUninstallButton.setVisibility(View.VISIBLE); @@ -645,12 +695,12 @@ public class DeviceAdminAdd extends Activity { private EnforcedAdmin getAdminEnforcingCantRemoveProfile() { // Removing a managed profile is disallowed if DISALLOW_REMOVE_MANAGED_PROFILE // is set in the parent rather than the user itself. - return RestrictedLockUtils.checkIfRestrictionEnforced(this, + return RestrictedLockUtilsInternal.checkIfRestrictionEnforced(this, UserManager.DISALLOW_REMOVE_MANAGED_PROFILE, getParentUserId()); } private boolean hasBaseCantRemoveProfileRestriction() { - return RestrictedLockUtils.hasBaseUserRestriction(this, + return RestrictedLockUtilsInternal.hasBaseUserRestriction(this, UserManager.DISALLOW_REMOVE_MANAGED_PROFILE, getParentUserId()); } diff --git a/src/com/android/settings/applications/specialaccess/deviceadmin/DeviceAdminListItem.java b/src/com/android/settings/applications/specialaccess/deviceadmin/DeviceAdminListItem.java new file mode 100644 index 0000000000..370a4dfe12 --- /dev/null +++ b/src/com/android/settings/applications/specialaccess/deviceadmin/DeviceAdminListItem.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2018 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.applications.specialaccess.deviceadmin; + +import android.app.admin.DeviceAdminInfo; +import android.app.admin.DevicePolicyManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.os.UserHandle; +import android.util.Log; + +class DeviceAdminListItem implements Comparable<DeviceAdminListItem> { + + private static final String TAG = "DeviceAdminListItem"; + + private final UserHandle mUserHandle; + private final String mKey; + private final DeviceAdminInfo mInfo; + private final CharSequence mName; + private final Drawable mIcon; + private final DevicePolicyManager mDPM; + private CharSequence mDescription; + + public DeviceAdminListItem(Context context, DeviceAdminInfo info) { + mInfo = info; + mUserHandle = new UserHandle(getUserIdFromDeviceAdminInfo(mInfo)); + mKey = mUserHandle.getIdentifier() + "@" + mInfo.getComponent().flattenToString(); + mDPM = (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE); + final PackageManager pm = context.getPackageManager(); + mName = mInfo.loadLabel(pm); + try { + mDescription = mInfo.loadDescription(pm); + } catch (Resources.NotFoundException exception) { + Log.w(TAG, "Setting description to null because can't find resource: " + mKey); + } + mIcon = pm.getUserBadgedIcon(mInfo.loadIcon(pm), mUserHandle); + } + + @Override + public int compareTo(DeviceAdminListItem other) { + return this.mName.toString().compareTo(other.mName.toString()); + } + + public String getKey() { + return mKey; + } + + public CharSequence getName() { + return mName; + } + + public CharSequence getDescription() { + return mDescription; + } + + public boolean isActive() { + return mDPM.isAdminActiveAsUser(mInfo.getComponent(), getUserIdFromDeviceAdminInfo(mInfo)); + } + + public Drawable getIcon() { + return mIcon; + } + + public boolean isEnabled() { + return !mDPM.isRemovingAdmin(mInfo.getComponent(), getUserIdFromDeviceAdminInfo(mInfo)); + } + + public UserHandle getUser() { + return new UserHandle(getUserIdFromDeviceAdminInfo(mInfo)); + } + + public Intent getLaunchIntent(Context context) { + return new Intent(context, DeviceAdminAdd.class) + .putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, mInfo.getComponent()); + } + + /** + * Extracts the user id from a device admin info object. + * + * @param adminInfo the device administrator info. + * @return identifier of the user associated with the device admin. + */ + private static int getUserIdFromDeviceAdminInfo(DeviceAdminInfo adminInfo) { + return UserHandle.getUserId(adminInfo.getActivityInfo().applicationInfo.uid); + } +} diff --git a/src/com/android/settings/applications/specialaccess/deviceadmin/DeviceAdminListPreferenceController.java b/src/com/android/settings/applications/specialaccess/deviceadmin/DeviceAdminListPreferenceController.java new file mode 100644 index 0000000000..7b139d9770 --- /dev/null +++ b/src/com/android/settings/applications/specialaccess/deviceadmin/DeviceAdminListPreferenceController.java @@ -0,0 +1,318 @@ +/* + * Copyright (C) 2018 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.applications.specialaccess.deviceadmin; + +import static android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED; + +import android.app.AppGlobals; +import android.app.admin.DeviceAdminInfo; +import android.app.admin.DeviceAdminReceiver; +import android.app.admin.DevicePolicyManager; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ActivityInfo; +import android.content.pm.IPackageManager; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.RemoteException; +import android.os.UserHandle; +import android.os.UserManager; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.Log; +import android.util.SparseArray; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceGroup; +import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreference; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.core.lifecycle.events.OnStop; +import com.android.settingslib.widget.FooterPreference; +import com.android.settingslib.widget.FooterPreferenceMixinCompat; + +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class DeviceAdminListPreferenceController extends BasePreferenceController + implements LifecycleObserver, OnStart, OnStop { + + private static final IntentFilter FILTER = new IntentFilter(); + private static final String TAG = "DeviceAdminListPrefCtrl"; + + private final DevicePolicyManager mDPM; + private final UserManager mUm; + private final PackageManager mPackageManager; + private final IPackageManager mIPackageManager; + /** + * Internal collection of device admin info objects for all profiles associated with the current + * user. + */ + private final ArrayList<DeviceAdminListItem> mAdmins = new ArrayList<>(); + private final SparseArray<ComponentName> mProfileOwnerComponents = new SparseArray<>(); + + private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + // Refresh the list, if state change has been received. It could be that checkboxes + // need to be updated + if (TextUtils.equals(ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED, intent.getAction())) { + updateList(); + } + } + }; + + private PreferenceGroup mPreferenceGroup; + private FooterPreferenceMixinCompat mFooterPreferenceMixin; + + static { + FILTER.addAction(ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED); + } + + public DeviceAdminListPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + mDPM = (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE); + mUm = (UserManager) context.getSystemService(Context.USER_SERVICE); + mPackageManager = mContext.getPackageManager(); + mIPackageManager = AppGlobals.getPackageManager(); + } + + public DeviceAdminListPreferenceController setFooterPreferenceMixin( + FooterPreferenceMixinCompat mixin) { + mFooterPreferenceMixin = mixin; + return this; + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreferenceGroup = screen.findPreference(getPreferenceKey()); + } + + @Override + public void onStart() { + mContext.registerReceiverAsUser( + mBroadcastReceiver, UserHandle.ALL, FILTER, + null /* broadcastPermission */, null /* scheduler */); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + mProfileOwnerComponents.clear(); + final List<UserHandle> profiles = mUm.getUserProfiles(); + final int profilesSize = profiles.size(); + for (int i = 0; i < profilesSize; ++i) { + final int profileId = profiles.get(i).getIdentifier(); + mProfileOwnerComponents.put(profileId, mDPM.getProfileOwnerAsUser(profileId)); + } + updateList(); + } + + @Override + public void onStop() { + mContext.unregisterReceiver(mBroadcastReceiver); + } + + @VisibleForTesting + void updateList() { + refreshData(); + refreshUI(); + } + + private void refreshData() { + mAdmins.clear(); + final List<UserHandle> profiles = mUm.getUserProfiles(); + for (UserHandle profile : profiles) { + final int profileId = profile.getIdentifier(); + updateAvailableAdminsForProfile(profileId); + } + Collections.sort(mAdmins); + } + + private void refreshUI() { + if (mPreferenceGroup == null) { + return; + } + if (mFooterPreferenceMixin != null) { + final FooterPreference footer = mFooterPreferenceMixin.createFooterPreference(); + footer.setTitle(R.string.no_device_admins); + footer.setVisible(mAdmins.isEmpty()); + } + final Map<String, SwitchPreference> preferenceCache = new ArrayMap<>(); + final Context prefContext = mPreferenceGroup.getContext(); + final int childrenCount = mPreferenceGroup.getPreferenceCount(); + for (int i = 0; i < childrenCount; i++) { + final Preference pref = mPreferenceGroup.getPreference(i); + if (!(pref instanceof SwitchPreference)) { + continue; + } + final SwitchPreference appSwitch = (SwitchPreference) pref; + preferenceCache.put(appSwitch.getKey(), appSwitch); + } + for (DeviceAdminListItem item : mAdmins) { + final String key = item.getKey(); + SwitchPreference pref = preferenceCache.remove(key); + if (pref == null) { + pref = new SwitchPreference(prefContext); + mPreferenceGroup.addPreference(pref); + } + bindPreference(item, pref); + } + for (SwitchPreference unusedCacheItem : preferenceCache.values()) { + mPreferenceGroup.removePreference(unusedCacheItem); + } + } + + private void bindPreference(DeviceAdminListItem item, SwitchPreference pref) { + pref.setKey(item.getKey()); + pref.setTitle(item.getName()); + pref.setIcon(item.getIcon()); + pref.setChecked(item.isActive()); + pref.setSummary(item.getDescription()); + pref.setEnabled(item.isEnabled()); + pref.setOnPreferenceClickListener(preference -> { + final UserHandle user = item.getUser(); + mContext.startActivityAsUser(item.getLaunchIntent(mContext), user); + return true; + }); + pref.setOnPreferenceChangeListener((preference, newValue) -> false); + } + + /** + * Add device admins to the internal collection that belong to a profile. + * + * @param profileId the profile identifier. + */ + private void updateAvailableAdminsForProfile(final int profileId) { + // We are adding the union of two sets 'A' and 'B' of device admins to mAvailableAdmins. + // - Set 'A' is the set of active admins for the profile + // - set 'B' is the set of listeners to DeviceAdminReceiver.ACTION_DEVICE_ADMIN_ENABLED for + // the profile. + + // Add all of set 'A' to mAvailableAdmins. + final List<ComponentName> activeAdminsForProfile = mDPM.getActiveAdminsAsUser(profileId); + addActiveAdminsForProfile(activeAdminsForProfile, profileId); + + // Collect set 'B' and add B-A to mAvailableAdmins. + addDeviceAdminBroadcastReceiversForProfile(activeAdminsForProfile, profileId); + } + + /** + * Add a {@link DeviceAdminInfo} object to the internal collection of available admins for all + * active admin components associated with a profile. + */ + private void addActiveAdminsForProfile(List<ComponentName> activeAdmins, int profileId) { + if (activeAdmins == null) { + return; + } + + for (ComponentName activeAdmin : activeAdmins) { + final ActivityInfo ai; + try { + ai = mIPackageManager.getReceiverInfo(activeAdmin, + PackageManager.GET_META_DATA | + PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS | + PackageManager.MATCH_DIRECT_BOOT_UNAWARE | + PackageManager.MATCH_DIRECT_BOOT_AWARE, profileId); + } catch (RemoteException e) { + Log.w(TAG, "Unable to load component: " + activeAdmin); + continue; + } + final DeviceAdminInfo deviceAdminInfo = createDeviceAdminInfo(mContext, ai); + if (deviceAdminInfo == null) { + continue; + } + mAdmins.add(new DeviceAdminListItem(mContext, deviceAdminInfo)); + } + } + + /** + * Add a profile's device admins that are receivers of + * {@code DeviceAdminReceiver.ACTION_DEVICE_ADMIN_ENABLED} to the internal collection if they + * haven't been added yet. + * + * @param alreadyAddedComponents the set of active admin component names. Receivers of + * {@code DeviceAdminReceiver.ACTION_DEVICE_ADMIN_ENABLED} + * whose component is in this + * set are not added to the internal collection again. + * @param profileId the identifier of the profile + */ + private void addDeviceAdminBroadcastReceiversForProfile( + Collection<ComponentName> alreadyAddedComponents, int profileId) { + final List<ResolveInfo> enabledForProfile = mPackageManager.queryBroadcastReceiversAsUser( + new Intent(DeviceAdminReceiver.ACTION_DEVICE_ADMIN_ENABLED), + PackageManager.GET_META_DATA | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS, + profileId); + if (enabledForProfile == null) { + return; + } + for (ResolveInfo resolveInfo : enabledForProfile) { + final ComponentName riComponentName = + new ComponentName(resolveInfo.activityInfo.packageName, + resolveInfo.activityInfo.name); + if (alreadyAddedComponents != null + && alreadyAddedComponents.contains(riComponentName)) { + continue; + } + DeviceAdminInfo deviceAdminInfo = createDeviceAdminInfo( + mContext, resolveInfo.activityInfo); + // add only visible ones (note: active admins are added regardless of visibility) + if (deviceAdminInfo != null && deviceAdminInfo.isVisible()) { + if (!deviceAdminInfo.getActivityInfo().applicationInfo.isInternal()) { + continue; + } + mAdmins.add(new DeviceAdminListItem(mContext, deviceAdminInfo)); + } + } + } + + /** + * Creates a device admin info object for the resolved intent that points to the component of + * the device admin. + * + * @param ai ActivityInfo for the admin component. + * @return new {@link DeviceAdminInfo} object or null if there was an error. + */ + private static DeviceAdminInfo createDeviceAdminInfo(Context context, ActivityInfo ai) { + try { + return new DeviceAdminInfo(context, ai); + } catch (XmlPullParserException | IOException e) { + Log.w(TAG, "Skipping " + ai, e); + } + return null; + } +} diff --git a/src/com/android/settings/applications/specialaccess/deviceadmin/DeviceAdminSettings.java b/src/com/android/settings/applications/specialaccess/deviceadmin/DeviceAdminSettings.java new file mode 100644 index 0000000000..7cbd8c72aa --- /dev/null +++ b/src/com/android/settings/applications/specialaccess/deviceadmin/DeviceAdminSettings.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2010 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.applications.specialaccess.deviceadmin; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.provider.SearchIndexableResource; + +import com.android.settings.R; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; +import com.android.settingslib.search.SearchIndexable; + +import java.util.ArrayList; +import java.util.List; + +@SearchIndexable +public class DeviceAdminSettings extends DashboardFragment { + static final String TAG = "DeviceAdminSettings"; + + public int getMetricsCategory() { + return SettingsEnums.DEVICE_ADMIN_SETTINGS; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + use(DeviceAdminListPreferenceController.class).setFooterPreferenceMixin( + mFooterPreferenceMixin); + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.device_admin_settings; + } + + @Override + protected String getLogTag() { + return TAG; + } + + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, + boolean enabled) { + final ArrayList<SearchIndexableResource> result = new ArrayList<>(); + + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.device_admin_settings; + result.add(sir); + return result; + } + }; +} diff --git a/src/com/android/settings/applications/specialaccess/deviceadmin/ProfileOwnerAdd.java b/src/com/android/settings/applications/specialaccess/deviceadmin/ProfileOwnerAdd.java new file mode 100644 index 0000000000..6841ea491f --- /dev/null +++ b/src/com/android/settings/applications/specialaccess/deviceadmin/ProfileOwnerAdd.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2019 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.applications.specialaccess.deviceadmin; + +import android.os.Bundle; + +/** + * ProfileOwnerAdd uses the DeviceAdminAdd logic to handle SET_PROFILE_OWNER intents + * + * TODO(b/131713071): Move profile owner add logic from DeviceAdminAdd to here + */ +public class ProfileOwnerAdd extends DeviceAdminAdd { + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + } +} diff --git a/src/com/android/settings/applications/specialaccess/notificationaccess/NotificationAccessController.java b/src/com/android/settings/applications/specialaccess/notificationaccess/NotificationAccessController.java new file mode 100644 index 0000000000..f9e8fe3157 --- /dev/null +++ b/src/com/android/settings/applications/specialaccess/notificationaccess/NotificationAccessController.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2018 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.applications.specialaccess.notificationaccess; + +import android.app.ActivityManager; +import android.content.Context; + +import com.android.settings.core.BasePreferenceController; + +public class NotificationAccessController extends BasePreferenceController { + + public NotificationAccessController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public int getAvailabilityStatus() { + return !ActivityManager.isLowRamDeviceStatic() + ? AVAILABLE_UNSEARCHABLE + : UNSUPPORTED_ON_DEVICE; + } +} diff --git a/src/com/android/settings/applications/specialaccess/notificationaccess/NotificationAccessScreenPreferenceController.java b/src/com/android/settings/applications/specialaccess/notificationaccess/NotificationAccessScreenPreferenceController.java new file mode 100644 index 0000000000..b86489dbf7 --- /dev/null +++ b/src/com/android/settings/applications/specialaccess/notificationaccess/NotificationAccessScreenPreferenceController.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2018 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.applications.specialaccess.notificationaccess; + +import android.app.ActivityManager; +import android.content.Context; + +import com.android.settings.core.BasePreferenceController; + +public class NotificationAccessScreenPreferenceController extends BasePreferenceController { + + public NotificationAccessScreenPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public int getAvailabilityStatus() { + return !ActivityManager.isLowRamDeviceStatic() + ? AVAILABLE + : UNSUPPORTED_ON_DEVICE; + } +} diff --git a/src/com/android/settings/applications/specialaccess/pictureinpicture/PictureInPictureController.java b/src/com/android/settings/applications/specialaccess/pictureinpicture/PictureInPictureController.java new file mode 100644 index 0000000000..714b662a7b --- /dev/null +++ b/src/com/android/settings/applications/specialaccess/pictureinpicture/PictureInPictureController.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2018 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.applications.specialaccess.pictureinpicture; + +import android.app.ActivityManager; +import android.content.Context; + +import com.android.settings.core.BasePreferenceController; + +public class PictureInPictureController extends BasePreferenceController { + + public PictureInPictureController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public int getAvailabilityStatus() { + return !ActivityManager.isLowRamDeviceStatic() + ? AVAILABLE_UNSEARCHABLE + : UNSUPPORTED_ON_DEVICE; + } +} diff --git a/src/com/android/settings/applications/appinfo/PictureInPictureDetailPreferenceController.java b/src/com/android/settings/applications/specialaccess/pictureinpicture/PictureInPictureDetailPreferenceController.java index 7b98404e17..1f94ed1452 100644 --- a/src/com/android/settings/applications/appinfo/PictureInPictureDetailPreferenceController.java +++ b/src/com/android/settings/applications/specialaccess/pictureinpicture/PictureInPictureDetailPreferenceController.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 The Android Open Source Project + * Copyright (C) 2018 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. @@ -14,17 +14,19 @@ * limitations under the License. */ -package com.android.settings.applications.appinfo; +package com.android.settings.applications.specialaccess.pictureinpicture; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.UserHandle; +import android.util.Log; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; -import android.util.Log; import com.android.settings.SettingsPreferenceFragment; +import com.android.settings.applications.appinfo.AppInfoPreferenceControllerBase; public class PictureInPictureDetailPreferenceController extends AppInfoPreferenceControllerBase { diff --git a/src/com/android/settings/applications/appinfo/PictureInPictureDetails.java b/src/com/android/settings/applications/specialaccess/pictureinpicture/PictureInPictureDetails.java index 21b224a96a..3dd428b2e1 100644 --- a/src/com/android/settings/applications/appinfo/PictureInPictureDetails.java +++ b/src/com/android/settings/applications/specialaccess/pictureinpicture/PictureInPictureDetails.java @@ -13,25 +13,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.settings.applications.appinfo; +package com.android.settings.applications.specialaccess.pictureinpicture; + +import static android.app.AppOpsManager.MODE_ALLOWED; +import static android.app.AppOpsManager.MODE_ERRORED; +import static android.app.AppOpsManager.OP_PICTURE_IN_PICTURE; -import android.app.AlertDialog; import android.app.AppOpsManager; +import android.app.settings.SettingsEnums; import android.content.Context; import android.os.Bundle; -import androidx.preference.SwitchPreference; + +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; import androidx.preference.Preference; import androidx.preference.Preference.OnPreferenceChangeListener; +import androidx.preference.SwitchPreference; -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.applications.AppInfoWithHeader; import com.android.settings.overlay.FeatureFactory; - -import static android.app.AppOpsManager.MODE_ALLOWED; -import static android.app.AppOpsManager.MODE_ERRORED; -import static android.app.AppOpsManager.OP_PICTURE_IN_PICTURE; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; public class PictureInPictureDetails extends AppInfoWithHeader implements OnPreferenceChangeListener { @@ -82,7 +84,7 @@ public class PictureInPictureDetails extends AppInfoWithHeader @Override public int getMetricsCategory() { - return MetricsEvent.SETTINGS_MANAGE_PICTURE_IN_PICTURE; + return SettingsEnums.SETTINGS_MANAGE_PICTURE_IN_PICTURE; } /** @@ -98,7 +100,7 @@ public class PictureInPictureDetails extends AppInfoWithHeader /** * @return whether the app associated with the given {@param packageName} is allowed to enter - * picture-in-picture. + * picture-in-picture. */ static boolean getEnterPipStateForPackage(Context context, int uid, String packageName) { final AppOpsManager appOps = context.getSystemService(AppOpsManager.class); @@ -107,7 +109,7 @@ public class PictureInPictureDetails extends AppInfoWithHeader /** * @return the summary for the current state of whether the app associated with the given - * {@param packageName} is allowed to enter picture-in-picture. + * {@param packageName} is allowed to enter picture-in-picture. */ public static int getPreferenceSummary(Context context, int uid, String packageName) { final boolean enabled = PictureInPictureDetails.getEnterPipStateForPackage(context, uid, @@ -119,9 +121,15 @@ public class PictureInPictureDetails extends AppInfoWithHeader @VisibleForTesting void logSpecialPermissionChange(boolean newState, String packageName) { int logCategory = newState - ? MetricsEvent.APP_PICTURE_IN_PICTURE_ALLOW - : MetricsEvent.APP_PICTURE_IN_PICTURE_DENY; - FeatureFactory.getFactory(getContext()) - .getMetricsFeatureProvider().action(getContext(), logCategory, packageName); + ? SettingsEnums.APP_PICTURE_IN_PICTURE_ALLOW + : SettingsEnums.APP_PICTURE_IN_PICTURE_DENY; + final MetricsFeatureProvider metricsFeatureProvider = + FeatureFactory.getFactory(getContext()).getMetricsFeatureProvider(); + metricsFeatureProvider.action( + metricsFeatureProvider.getAttribution(getActivity()), + logCategory, + getMetricsCategory(), + packageName, + 0); } } diff --git a/src/com/android/settings/applications/specialaccess/pictureinpicture/PictureInPictureScreenPreferenceController.java b/src/com/android/settings/applications/specialaccess/pictureinpicture/PictureInPictureScreenPreferenceController.java new file mode 100644 index 0000000000..def600848d --- /dev/null +++ b/src/com/android/settings/applications/specialaccess/pictureinpicture/PictureInPictureScreenPreferenceController.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2018 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.applications.specialaccess.pictureinpicture; + +import android.app.ActivityManager; +import android.content.Context; + +import com.android.settings.core.BasePreferenceController; + +public class PictureInPictureScreenPreferenceController extends BasePreferenceController { + + public PictureInPictureScreenPreferenceController(Context context, + String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public int getAvailabilityStatus() { + return !ActivityManager.isLowRamDeviceStatic() + ? AVAILABLE + : UNSUPPORTED_ON_DEVICE; + } +} diff --git a/src/com/android/settings/applications/appinfo/PictureInPictureSettings.java b/src/com/android/settings/applications/specialaccess/pictureinpicture/PictureInPictureSettings.java index 9d7f30a911..fdbe1d8cd7 100644 --- a/src/com/android/settings/applications/appinfo/PictureInPictureSettings.java +++ b/src/com/android/settings/applications/specialaccess/pictureinpicture/PictureInPictureSettings.java @@ -13,11 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.settings.applications.appinfo; +package com.android.settings.applications.specialaccess.pictureinpicture; import static android.content.pm.PackageManager.GET_ACTIVITIES; import android.annotation.Nullable; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; @@ -27,20 +28,23 @@ import android.content.pm.UserInfo; import android.os.Bundle; import android.os.UserHandle; import android.os.UserManager; -import androidx.preference.Preference; -import androidx.preference.Preference.OnPreferenceClickListener; -import androidx.preference.PreferenceScreen; +import android.provider.SearchIndexableResource; import android.util.IconDrawableFactory; import android.util.Pair; import android.view.View; -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.Preference.OnPreferenceClickListener; +import androidx.preference.PreferenceScreen; + import com.android.settings.R; import com.android.settings.applications.AppInfoBase; -import com.android.settings.notification.EmptyTextSettings; -import com.android.settings.widget.AppPreference; -import com.android.settingslib.wrapper.PackageManagerWrapper; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; +import com.android.settings.widget.EmptyTextSettings; +import com.android.settingslib.search.SearchIndexable; +import com.android.settingslib.widget.apppreference.AppPreference; import java.text.Collator; import java.util.ArrayList; @@ -48,11 +52,12 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; +@SearchIndexable public class PictureInPictureSettings extends EmptyTextSettings { - private static final String TAG = PictureInPictureSettings.class.getSimpleName(); @VisibleForTesting static final List<String> IGNORE_PACKAGE_LIST = new ArrayList<>(); + static { IGNORE_PACKAGE_LIST.add("com.android.systemui"); } @@ -72,9 +77,9 @@ public class PictureInPictureSettings extends EmptyTextSettings { public final int compare(Pair<ApplicationInfo, Integer> a, Pair<ApplicationInfo, Integer> b) { - CharSequence sa = a.first.loadLabel(mPm); + CharSequence sa = a.first.loadLabel(mPm); if (sa == null) sa = a.first.name; - CharSequence sb = b.first.loadLabel(mPm); + CharSequence sb = b.first.loadLabel(mPm); if (sb == null) sb = b.first.name; int nameCmp = mCollator.compare(sa.toString(), sb.toString()); if (nameCmp != 0) { @@ -86,13 +91,13 @@ public class PictureInPictureSettings extends EmptyTextSettings { } private Context mContext; - private PackageManagerWrapper mPackageManager; + private PackageManager mPackageManager; private UserManager mUserManager; private IconDrawableFactory mIconDrawableFactory; /** * @return true if the package has any activities that declare that they support - * picture-in-picture. + * picture-in-picture. */ public static boolean checkPackageHasPictureInPictureActivities(String packageName, @@ -118,7 +123,7 @@ public class PictureInPictureSettings extends EmptyTextSettings { // Do nothing } - public PictureInPictureSettings(PackageManagerWrapper pm, UserManager um) { + public PictureInPictureSettings(PackageManager pm, UserManager um) { mPackageManager = pm; mUserManager = um; } @@ -128,7 +133,7 @@ public class PictureInPictureSettings extends EmptyTextSettings { super.onCreate(icicle); mContext = getActivity(); - mPackageManager = new PackageManagerWrapper(mContext.getPackageManager()); + mPackageManager = mContext.getPackageManager(); mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); mIconDrawableFactory = IconDrawableFactory.newInstance(mContext); } @@ -143,10 +148,9 @@ public class PictureInPictureSettings extends EmptyTextSettings { // Fetch the set of applications for each profile which have at least one activity that // declare that they support picture-in-picture - final PackageManager pm = mPackageManager.getPackageManager(); final ArrayList<Pair<ApplicationInfo, Integer>> pipApps = collectPipApps(UserHandle.myUserId()); - Collections.sort(pipApps, new AppComparator(pm)); + Collections.sort(pipApps, new AppComparator(mPackageManager)); // Rebuild the list of prefs final Context prefContext = getPrefContext(); @@ -155,11 +159,11 @@ public class PictureInPictureSettings extends EmptyTextSettings { final int userId = appData.second; final UserHandle user = UserHandle.of(userId); final String packageName = appInfo.packageName; - final CharSequence label = appInfo.loadLabel(pm); + final CharSequence label = appInfo.loadLabel(mPackageManager); final Preference pref = new AppPreference(prefContext); pref.setIcon(mIconDrawableFactory.getBadgedIcon(appInfo, userId)); - pref.setTitle(pm.getUserBadgedLabel(label, user)); + pref.setTitle(mPackageManager.getUserBadgedLabel(label, user)); pref.setSummary(PictureInPictureDetails.getPreferenceSummary(prefContext, appInfo.uid, packageName)); pref.setOnPreferenceClickListener(new OnPreferenceClickListener() { @@ -188,12 +192,12 @@ public class PictureInPictureSettings extends EmptyTextSettings { @Override public int getMetricsCategory() { - return MetricsEvent.SETTINGS_MANAGE_PICTURE_IN_PICTURE; + return SettingsEnums.SETTINGS_MANAGE_PICTURE_IN_PICTURE; } /** * @return the list of applications for the given user and all their profiles that have - * activities which support PiP. + * activities which support PiP. */ ArrayList<Pair<ApplicationInfo, Integer>> collectPipApps(int userId) { final ArrayList<Pair<ApplicationInfo, Integer>> pipApps = new ArrayList<>(); @@ -214,4 +218,18 @@ public class PictureInPictureSettings extends EmptyTextSettings { } return pipApps; } + + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, + boolean enabled) { + final ArrayList<SearchIndexableResource> result = new ArrayList<>(); + + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.picture_in_picture_settings; + result.add(sir); + return result; + } + }; } diff --git a/src/com/android/settings/applications/PremiumSmsAccess.java b/src/com/android/settings/applications/specialaccess/premiumsms/PremiumSmsAccess.java index 2210c0b176..a40ddc9485 100644 --- a/src/com/android/settings/applications/PremiumSmsAccess.java +++ b/src/com/android/settings/applications/specialaccess/premiumsms/PremiumSmsAccess.java @@ -1,47 +1,59 @@ /* * Copyright (C) 2016 The Android Open Source Project * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file - * except in compliance with the License. You may obtain a copy of the License at + * 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. + * 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.applications; +package com.android.settings.applications.specialaccess.premiumsms; import android.annotation.Nullable; import android.app.Application; +import android.app.settings.SettingsEnums; import android.content.Context; import android.os.Bundle; +import android.provider.SearchIndexableResource; +import android.view.View; + +import androidx.annotation.VisibleForTesting; import androidx.preference.DropDownPreference; import androidx.preference.Preference; import androidx.preference.Preference.OnPreferenceChangeListener; import androidx.preference.PreferenceScreen; import androidx.preference.PreferenceViewHolder; -import android.view.View; -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.logging.nano.MetricsProto; import com.android.internal.telephony.SmsUsageMonitor; import com.android.settings.R; import com.android.settings.applications.AppStateBaseBridge.Callback; +import com.android.settings.applications.AppStateSmsPremBridge; import com.android.settings.applications.AppStateSmsPremBridge.SmsState; -import com.android.settings.notification.EmptyTextSettings; import com.android.settings.overlay.FeatureFactory; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; +import com.android.settings.widget.EmptyTextSettings; import com.android.settingslib.applications.ApplicationsState; import com.android.settingslib.applications.ApplicationsState.AppEntry; import com.android.settingslib.applications.ApplicationsState.Callbacks; import com.android.settingslib.applications.ApplicationsState.Session; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; +import com.android.settingslib.search.SearchIndexable; import com.android.settingslib.widget.FooterPreference; import java.util.ArrayList; +import java.util.List; -public class PremiumSmsAccess extends EmptyTextSettings implements Callback, Callbacks, OnPreferenceChangeListener { +@SearchIndexable +public class PremiumSmsAccess extends EmptyTextSettings + implements Callback, Callbacks, OnPreferenceChangeListener { private ApplicationsState mApplicationsState; private AppStateSmsPremBridge mSmsBackend; @@ -52,7 +64,7 @@ public class PremiumSmsAccess extends EmptyTextSettings implements Callback, Cal super.onCreate(icicle); mApplicationsState = ApplicationsState.getInstance((Application) getContext().getApplicationContext()); - mSession = mApplicationsState.newSession(this, getLifecycle()); + mSession = mApplicationsState.newSession(this, getSettingsLifecycle()); mSmsBackend = new AppStateSmsPremBridge(getContext(), mApplicationsState, this); } @@ -87,7 +99,7 @@ public class PremiumSmsAccess extends EmptyTextSettings implements Callback, Cal @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.PREMIUM_SMS_ACCESS; + return SettingsEnums.PREMIUM_SMS_ACCESS; } @Override @@ -104,19 +116,26 @@ public class PremiumSmsAccess extends EmptyTextSettings implements Callback, Cal int category = SmsUsageMonitor.PREMIUM_SMS_PERMISSION_UNKNOWN; switch (smsState) { case SmsUsageMonitor.PREMIUM_SMS_PERMISSION_ASK_USER: - category = MetricsProto.MetricsEvent.APP_SPECIAL_PERMISSION_PREMIUM_SMS_ASK; + category = SettingsEnums.APP_SPECIAL_PERMISSION_PREMIUM_SMS_ASK; break; case SmsUsageMonitor.PREMIUM_SMS_PERMISSION_NEVER_ALLOW: - category = MetricsProto.MetricsEvent.APP_SPECIAL_PERMISSION_PREMIUM_SMS_DENY; + category = SettingsEnums.APP_SPECIAL_PERMISSION_PREMIUM_SMS_DENY; break; case SmsUsageMonitor.PREMIUM_SMS_PERMISSION_ALWAYS_ALLOW: - category = MetricsProto.MetricsEvent. + category = SettingsEnums. APP_SPECIAL_PERMISSION_PREMIUM_SMS_ALWAYS_ALLOW; break; } if (category != SmsUsageMonitor.PREMIUM_SMS_PERMISSION_UNKNOWN) { - FeatureFactory.getFactory(getContext()).getMetricsFeatureProvider().action( - getContext(), category, packageName); + // TODO(117860032): Category is wrong. It should be defined in SettingsEnums. + final MetricsFeatureProvider metricsFeatureProvider = + FeatureFactory.getFactory(getContext()).getMetricsFeatureProvider(); + metricsFeatureProvider.action( + metricsFeatureProvider.getAttribution(getActivity()), + category, + getMetricsCategory(), + packageName, + smsState); } } @@ -203,7 +222,7 @@ public class PremiumSmsAccess extends EmptyTextSettings implements Callback, Cal setIcon(mAppEntry.icon); } setEntries(R.array.security_settings_premium_sms_values); - setEntryValues(new CharSequence[] { + setEntryValues(new CharSequence[]{ String.valueOf(SmsUsageMonitor.PREMIUM_SMS_PERMISSION_ASK_USER), String.valueOf(SmsUsageMonitor.PREMIUM_SMS_PERMISSION_NEVER_ALLOW), String.valueOf(SmsUsageMonitor.PREMIUM_SMS_PERMISSION_ALWAYS_ALLOW), @@ -232,4 +251,18 @@ public class PremiumSmsAccess extends EmptyTextSettings implements Callback, Cal super.onBindViewHolder(holder); } } + + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, + boolean enabled) { + final ArrayList<SearchIndexableResource> result = new ArrayList<>(); + + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.premium_sms_settings; + result.add(sir); + return result; + } + }; } diff --git a/src/com/android/settings/applications/PremiumSmsController.java b/src/com/android/settings/applications/specialaccess/premiumsms/PremiumSmsController.java index eeb5d86fd9..158fe3cfd4 100644 --- a/src/com/android/settings/applications/PremiumSmsController.java +++ b/src/com/android/settings/applications/specialaccess/premiumsms/PremiumSmsController.java @@ -14,26 +14,23 @@ * limitations under the License. */ -package com.android.settings.applications; +package com.android.settings.applications.specialaccess.premiumsms; import android.content.Context; -import androidx.annotation.VisibleForTesting; -import com.android.settings.core.BasePreferenceController; import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; public class PremiumSmsController extends BasePreferenceController { - @VisibleForTesting static final String KEY_PREMIUM_SMS = "premium_sms"; - - public PremiumSmsController(Context context) { - super(context, KEY_PREMIUM_SMS); + public PremiumSmsController(Context context, String key) { + super(context, key); } @AvailabilityStatus public int getAvailabilityStatus() { return mContext.getResources().getBoolean(R.bool.config_show_premium_sms) - ? AVAILABLE + ? AVAILABLE_UNSEARCHABLE : UNSUPPORTED_ON_DEVICE; } }
\ No newline at end of file diff --git a/src/com/android/settings/applications/DeviceAdministratorsController.java b/src/com/android/settings/applications/specialaccess/premiumsms/PremiumSmsScreenPreferenceController.java index ec1d556a61..582e75ea5d 100644 --- a/src/com/android/settings/applications/DeviceAdministratorsController.java +++ b/src/com/android/settings/applications/specialaccess/premiumsms/PremiumSmsScreenPreferenceController.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 The Android Open Source Project + * Copyright (C) 2018 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. @@ -14,26 +14,23 @@ * limitations under the License. */ -package com.android.settings.applications; +package com.android.settings.applications.specialaccess.premiumsms; import android.content.Context; -import androidx.annotation.VisibleForTesting; -import com.android.settings.core.BasePreferenceController; import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; -public class DeviceAdministratorsController extends BasePreferenceController { - - @VisibleForTesting static final String KEY_DEVICE_ADMIN = "device_administrators"; +public class PremiumSmsScreenPreferenceController extends BasePreferenceController { - public DeviceAdministratorsController(Context context) { - super(context, KEY_DEVICE_ADMIN); + public PremiumSmsScreenPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); } @AvailabilityStatus public int getAvailabilityStatus() { - return mContext.getResources().getBoolean(R.bool.config_show_device_administrators) + return mContext.getResources().getBoolean(R.bool.config_show_premium_sms) ? AVAILABLE : UNSUPPORTED_ON_DEVICE; } -}
\ No newline at end of file +} diff --git a/src/com/android/settings/applications/EnabledVrListenersController.java b/src/com/android/settings/applications/specialaccess/vrlistener/EnabledVrListenersController.java index 7b33529d7e..fd59d8d3e3 100644 --- a/src/com/android/settings/applications/EnabledVrListenersController.java +++ b/src/com/android/settings/applications/specialaccess/vrlistener/EnabledVrListenersController.java @@ -14,26 +14,28 @@ * limitations under the License. */ -package com.android.settings.applications; +package com.android.settings.applications.specialaccess.vrlistener; +import android.app.ActivityManager; import android.content.Context; -import androidx.annotation.VisibleForTesting; -import com.android.settings.core.BasePreferenceController; import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; public class EnabledVrListenersController extends BasePreferenceController { - @VisibleForTesting static final String KEY_ENABLED_VR_LISTENERS = "enabled_vr_listeners"; + private final ActivityManager mActivityManager; - public EnabledVrListenersController(Context context) { - super(context, KEY_ENABLED_VR_LISTENERS); + public EnabledVrListenersController(Context context, String key) { + super(context, key); + mActivityManager = mContext.getSystemService(ActivityManager.class); } @AvailabilityStatus public int getAvailabilityStatus() { return mContext.getResources().getBoolean(R.bool.config_show_enabled_vr_listeners) - ? AVAILABLE + && !mActivityManager.isLowRamDevice() + ? AVAILABLE_UNSEARCHABLE : UNSUPPORTED_ON_DEVICE; } }
\ No newline at end of file diff --git a/src/com/android/settings/applications/specialaccess/vrlistener/VrListenerScreenPreferenceController.java b/src/com/android/settings/applications/specialaccess/vrlistener/VrListenerScreenPreferenceController.java new file mode 100644 index 0000000000..655db878f8 --- /dev/null +++ b/src/com/android/settings/applications/specialaccess/vrlistener/VrListenerScreenPreferenceController.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2018 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.applications.specialaccess.vrlistener; + +import android.app.ActivityManager; +import android.content.Context; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; + +public class VrListenerScreenPreferenceController extends BasePreferenceController { + + private final ActivityManager mActivityManager; + + public VrListenerScreenPreferenceController(Context context, String key) { + super(context, key); + mActivityManager = mContext.getSystemService(ActivityManager.class); + } + + @AvailabilityStatus + public int getAvailabilityStatus() { + return mContext.getResources().getBoolean(R.bool.config_show_enabled_vr_listeners) + && !mActivityManager.isLowRamDevice() + ? AVAILABLE + : UNSUPPORTED_ON_DEVICE; + } +}
\ No newline at end of file diff --git a/src/com/android/settings/applications/VrListenerSettings.java b/src/com/android/settings/applications/specialaccess/vrlistener/VrListenerSettings.java index ea88ae4335..fec57c25ac 100644 --- a/src/com/android/settings/applications/VrListenerSettings.java +++ b/src/com/android/settings/applications/specialaccess/vrlistener/VrListenerSettings.java @@ -13,18 +13,29 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.settings.applications; +package com.android.settings.applications.specialaccess.vrlistener; +import android.app.settings.SettingsEnums; import android.content.ComponentName; +import android.content.Context; +import android.provider.SearchIndexableResource; import android.provider.Settings; import android.service.vr.VrListenerService; -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.annotation.VisibleForTesting; + import com.android.settings.R; import com.android.settings.overlay.FeatureFactory; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; import com.android.settings.utils.ManagedServiceSettings; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; +import com.android.settingslib.search.SearchIndexable; + +import java.util.ArrayList; +import java.util.List; +@SearchIndexable public class VrListenerSettings extends ManagedServiceSettings { private static final String TAG = VrListenerSettings.class.getSimpleName(); private static final Config CONFIG = new Config.Builder() @@ -45,7 +56,7 @@ public class VrListenerSettings extends ManagedServiceSettings { @Override public int getMetricsCategory() { - return MetricsEvent.VR_MANAGE_LISTENERS; + return SettingsEnums.VR_MANAGE_LISTENERS; } @Override @@ -61,9 +72,30 @@ public class VrListenerSettings extends ManagedServiceSettings { @VisibleForTesting void logSpecialPermissionChange(boolean enable, String packageName) { - int logCategory = enable ? MetricsEvent.APP_SPECIAL_PERMISSION_VRHELPER_ALLOW - : MetricsEvent.APP_SPECIAL_PERMISSION_VRHELPER_DENY; - FeatureFactory.getFactory(getContext()).getMetricsFeatureProvider().action(getContext(), - logCategory, packageName); + int logCategory = enable ? SettingsEnums.APP_SPECIAL_PERMISSION_VRHELPER_ALLOW + : SettingsEnums.APP_SPECIAL_PERMISSION_VRHELPER_DENY; + final MetricsFeatureProvider metricsFeatureProvider = + FeatureFactory.getFactory(getContext()).getMetricsFeatureProvider(); + metricsFeatureProvider.action( + metricsFeatureProvider.getAttribution(getActivity()), + logCategory, + getMetricsCategory(), + packageName, + 0); } + + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, + boolean enabled) { + final List<SearchIndexableResource> result = new ArrayList<>(); + + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.vr_listeners_settings; + result.add(sir); + return result; + } + }; + } diff --git a/src/com/android/settings/applications/specialaccess/zenaccess/FriendlyWarningDialogFragment.java b/src/com/android/settings/applications/specialaccess/zenaccess/FriendlyWarningDialogFragment.java new file mode 100644 index 0000000000..fc85f7dffd --- /dev/null +++ b/src/com/android/settings/applications/specialaccess/zenaccess/FriendlyWarningDialogFragment.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2019 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.applications.specialaccess.zenaccess; + +import android.app.Dialog; +import android.app.settings.SettingsEnums; +import android.os.Bundle; +import android.text.TextUtils; + +import androidx.appcompat.app.AlertDialog; + +import com.android.settings.R; +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; + +/** + * Warning dialog when revoking zen access warning that zen rule instances will be deleted. + */ +public class FriendlyWarningDialogFragment extends InstrumentedDialogFragment { + static final String KEY_PKG = "p"; + static final String KEY_LABEL = "l"; + + + @Override + public int getMetricsCategory() { + return SettingsEnums.DIALOG_ZEN_ACCESS_REVOKE; + } + + public FriendlyWarningDialogFragment setPkgInfo(String pkg, CharSequence label) { + Bundle args = new Bundle(); + args.putString(KEY_PKG, pkg); + args.putString(KEY_LABEL, TextUtils.isEmpty(label) ? pkg : label.toString()); + setArguments(args); + return this; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + final Bundle args = getArguments(); + final String pkg = args.getString(KEY_PKG); + final String label = args.getString(KEY_LABEL); + + final String title = getResources().getString( + R.string.zen_access_revoke_warning_dialog_title, label); + final String summary = getResources() + .getString(R.string.zen_access_revoke_warning_dialog_summary); + return new AlertDialog.Builder(getContext()) + .setMessage(summary) + .setTitle(title) + .setCancelable(true) + .setPositiveButton(R.string.okay, + (dialog, id) -> { + ZenAccessController.deleteRules(getContext(), pkg); + ZenAccessController.setAccess(getContext(), pkg, false); + }) + .setNegativeButton(R.string.cancel, + (dialog, id) -> { + // pass + }) + .create(); + } +} diff --git a/src/com/android/settings/applications/specialaccess/zenaccess/OWNERS b/src/com/android/settings/applications/specialaccess/zenaccess/OWNERS new file mode 100644 index 0000000000..9b5f41ef32 --- /dev/null +++ b/src/com/android/settings/applications/specialaccess/zenaccess/OWNERS @@ -0,0 +1,2 @@ +beverlyt@google.com +juliacr@google.com
\ No newline at end of file diff --git a/src/com/android/settings/applications/specialaccess/zenaccess/ScaryWarningDialogFragment.java b/src/com/android/settings/applications/specialaccess/zenaccess/ScaryWarningDialogFragment.java new file mode 100644 index 0000000000..69318f8d6a --- /dev/null +++ b/src/com/android/settings/applications/specialaccess/zenaccess/ScaryWarningDialogFragment.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2019 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.applications.specialaccess.zenaccess; + +import android.app.Dialog; +import android.app.settings.SettingsEnums; +import android.os.Bundle; +import android.text.TextUtils; + +import androidx.appcompat.app.AlertDialog; + +import com.android.settings.R; +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; +import com.android.settings.notification.ZenAccessSettings; + +/** + * Warning dialog when allowing zen access warning about the privileges being granted. + */ +public class ScaryWarningDialogFragment extends InstrumentedDialogFragment { + static final String KEY_PKG = "p"; + static final String KEY_LABEL = "l"; + + @Override + public int getMetricsCategory() { + return SettingsEnums.DIALOG_ZEN_ACCESS_GRANT; + } + + public ScaryWarningDialogFragment setPkgInfo(String pkg, CharSequence label) { + Bundle args = new Bundle(); + args.putString(KEY_PKG, pkg); + args.putString(KEY_LABEL, TextUtils.isEmpty(label) ? pkg : label.toString()); + setArguments(args); + return this; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + final Bundle args = getArguments(); + final String pkg = args.getString(KEY_PKG); + final String label = args.getString(KEY_LABEL); + + final String title = getResources().getString(R.string.zen_access_warning_dialog_title, + label); + final String summary = getResources() + .getString(R.string.zen_access_warning_dialog_summary); + return new AlertDialog.Builder(getContext()) + .setMessage(summary) + .setTitle(title) + .setCancelable(true) + .setPositiveButton(R.string.allow, + (dialog, id) -> ZenAccessController.setAccess(getContext(), pkg, true)) + .setNegativeButton(R.string.deny, + (dialog, id) -> { + // pass + }) + .create(); + } +} diff --git a/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessController.java b/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessController.java new file mode 100644 index 0000000000..946599b4aa --- /dev/null +++ b/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessController.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2018 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.applications.specialaccess.zenaccess; + +import android.app.ActivityManager; +import android.app.AppGlobals; +import android.app.NotificationManager; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.ParceledListSlice; +import android.os.AsyncTask; +import android.os.RemoteException; +import android.util.ArraySet; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; + +import com.android.settings.core.BasePreferenceController; +import com.android.settings.overlay.FeatureFactory; + +import java.util.List; +import java.util.Set; + +public class ZenAccessController extends BasePreferenceController { + + private static final String TAG = "ZenAccessController"; + + private final ActivityManager mActivityManager; + + public ZenAccessController(Context context, String preferenceKey) { + super(context, preferenceKey); + mActivityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); + } + + @Override + public int getAvailabilityStatus() { + return isSupported(mActivityManager) + ? AVAILABLE_UNSEARCHABLE + : UNSUPPORTED_ON_DEVICE; + } + + public static boolean isSupported(ActivityManager activityManager) { + return !activityManager.isLowRamDevice(); + } + + public static Set<String> getPackagesRequestingNotificationPolicyAccess() { + final ArraySet<String> requestingPackages = new ArraySet<>(); + try { + final String[] PERM = { + android.Manifest.permission.ACCESS_NOTIFICATION_POLICY + }; + final ParceledListSlice list = AppGlobals.getPackageManager() + .getPackagesHoldingPermissions(PERM, 0 /*flags*/, + ActivityManager.getCurrentUser()); + final List<PackageInfo> pkgs = list.getList(); + if (pkgs != null) { + for (PackageInfo info : pkgs) { + requestingPackages.add(info.packageName); + } + } + } catch (RemoteException e) { + Log.e(TAG, "Cannot reach packagemanager", e); + } + return requestingPackages; + } + + public static Set<String> getAutoApprovedPackages(Context context) { + final Set<String> autoApproved = new ArraySet<>(); + autoApproved.addAll(context.getSystemService(NotificationManager.class) + .getEnabledNotificationListenerPackages()); + return autoApproved; + } + + public static boolean hasAccess(Context context, String pkg) { + return context.getSystemService( + NotificationManager.class).isNotificationPolicyAccessGrantedForPackage(pkg); + } + + public static void setAccess(final Context context, final String pkg, final boolean access) { + logSpecialPermissionChange(access, pkg, context); + AsyncTask.execute(() -> { + final NotificationManager mgr = context.getSystemService(NotificationManager.class); + mgr.setNotificationPolicyAccessGranted(pkg, access); + }); + } + + public static void deleteRules(final Context context, final String pkg) { + AsyncTask.execute(() -> { + final NotificationManager mgr = context.getSystemService(NotificationManager.class); + mgr.removeAutomaticZenRules(pkg); + }); + } + + @VisibleForTesting + static void logSpecialPermissionChange(boolean enable, String packageName, Context context) { + int logCategory = enable ? SettingsEnums.APP_SPECIAL_PERMISSION_DND_ALLOW + : SettingsEnums.APP_SPECIAL_PERMISSION_DND_DENY; + FeatureFactory.getFactory(context).getMetricsFeatureProvider().action(context, + logCategory, packageName); + } +} diff --git a/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessDetails.java b/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessDetails.java new file mode 100644 index 0000000000..a18e7d63ca --- /dev/null +++ b/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessDetails.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2019 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.applications.specialaccess.zenaccess; + +import android.app.ActivityManager; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.os.Bundle; + +import androidx.appcompat.app.AlertDialog; +import androidx.preference.SwitchPreference; + +import com.android.settings.R; +import com.android.settings.applications.AppInfoWithHeader; + +import java.util.Set; + +public class ZenAccessDetails extends AppInfoWithHeader implements + ZenAccessSettingObserverMixin.Listener { + + private static final String SWITCH_PREF_KEY = "zen_access_switch"; + + @Override + public int getMetricsCategory() { + return SettingsEnums.ZEN_ACCESS_DETAIL; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.zen_access_permission_details); + getSettingsLifecycle().addObserver( + new ZenAccessSettingObserverMixin(getContext(), this /* listener */)); + } + + @Override + protected boolean refreshUi() { + final Context context = getContext(); + if (!ZenAccessController.isSupported(context.getSystemService(ActivityManager.class))) { + return false; + } + // If this app didn't declare this permission in their manifest, don't bother showing UI. + final Set<String> needAccessApps = + ZenAccessController.getPackagesRequestingNotificationPolicyAccess(); + if (!needAccessApps.contains(mPackageName)) { + return false; + } + updatePreference(context, findPreference(SWITCH_PREF_KEY)); + return true; + } + + @Override + protected AlertDialog createDialog(int id, int errorCode) { + return null; + } + + public void updatePreference(Context context, SwitchPreference preference) { + final CharSequence label = mPackageInfo.applicationInfo.loadLabel(mPm); + final Set<String> autoApproved = ZenAccessController.getAutoApprovedPackages(context); + if (autoApproved.contains(mPackageName)) { + //Auto approved, user cannot do anything. Hard code summary and disable preference. + preference.setEnabled(false); + preference.setSummary(getString(R.string.zen_access_disabled_package_warning)); + return; + } + preference.setChecked(ZenAccessController.hasAccess(context, mPackageName)); + preference.setOnPreferenceChangeListener((p, newValue) -> { + final boolean access = (Boolean) newValue; + if (access) { + new ScaryWarningDialogFragment() + .setPkgInfo(mPackageName, label) + .show(getFragmentManager(), "dialog"); + } else { + new FriendlyWarningDialogFragment() + .setPkgInfo(mPackageName, label) + .show(getFragmentManager(), "dialog"); + } + return false; + }); + } + + @Override + public void onZenAccessPolicyChanged() { + refreshUi(); + } +} diff --git a/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessSettingObserverMixin.java b/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessSettingObserverMixin.java new file mode 100644 index 0000000000..30507efffa --- /dev/null +++ b/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessSettingObserverMixin.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2019 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.applications.specialaccess.zenaccess; + +import android.app.ActivityManager; +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.provider.Settings; + +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.core.lifecycle.events.OnStop; + +public class ZenAccessSettingObserverMixin extends ContentObserver implements LifecycleObserver, + OnStart, OnStop { + + public interface Listener { + void onZenAccessPolicyChanged(); + } + + private final Context mContext; + private final Listener mListener; + + public ZenAccessSettingObserverMixin(Context context, Listener listener) { + super(new Handler(Looper.getMainLooper())); + mContext = context; + mListener = listener; + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + if (mListener != null) { + mListener.onZenAccessPolicyChanged(); + } + } + + @Override + public void onStart() { + if (!ZenAccessController.isSupported(mContext.getSystemService(ActivityManager.class))) { + return; + } + mContext.getContentResolver().registerContentObserver( + Settings.Secure.getUriFor( + Settings.Secure.ENABLED_NOTIFICATION_POLICY_ACCESS_PACKAGES), + false /* notifyForDescendants */, + this /* observer */); + mContext.getContentResolver().registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.ENABLED_NOTIFICATION_LISTENERS), + false /* notifyForDescendants */, + this /* observer */); + } + + @Override + public void onStop() { + if (!ZenAccessController.isSupported(mContext.getSystemService(ActivityManager.class))) { + return; + } + mContext.getContentResolver().unregisterContentObserver(this /* observer */); + } +} diff --git a/src/com/android/settings/aware/AwareFeatureProvider.java b/src/com/android/settings/aware/AwareFeatureProvider.java new file mode 100644 index 0000000000..bda27c7143 --- /dev/null +++ b/src/com/android/settings/aware/AwareFeatureProvider.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2019 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.aware; + +import android.content.Context; + +import androidx.fragment.app.Fragment; + +public interface AwareFeatureProvider { + /** Returns true if the aware sensor is supported. */ + boolean isSupported(Context context); + + /** Returns true if the aware feature is enabled. */ + boolean isEnabled(Context context); + + /** Show information dialog. */ + void showRestrictionDialog(Fragment parent); +} diff --git a/src/com/android/settings/aware/AwareFeatureProviderImpl.java b/src/com/android/settings/aware/AwareFeatureProviderImpl.java new file mode 100644 index 0000000000..5d16031386 --- /dev/null +++ b/src/com/android/settings/aware/AwareFeatureProviderImpl.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2019 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.aware; + +import android.content.Context; + +import androidx.fragment.app.Fragment; + +public class AwareFeatureProviderImpl implements AwareFeatureProvider { + @Override + public boolean isSupported(Context context) { + return false; + } + + @Override + public boolean isEnabled(Context context) { + return false; + } + + @Override + public void showRestrictionDialog(Fragment parent) { + } +} diff --git a/src/com/android/settings/backup/AutoRestorePreferenceController.java b/src/com/android/settings/backup/AutoRestorePreferenceController.java new file mode 100644 index 0000000000..4dd2eb651d --- /dev/null +++ b/src/com/android/settings/backup/AutoRestorePreferenceController.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2018 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.backup; + +import android.app.backup.IBackupManager; +import android.content.ContentResolver; +import android.content.Context; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.provider.Settings; +import android.util.Log; + +import androidx.preference.Preference; +import androidx.preference.SwitchPreference; + +import com.android.settings.core.TogglePreferenceController; + +public class AutoRestorePreferenceController extends TogglePreferenceController { + private static final String TAG = "AutoRestorePrefCtrler"; + + private PrivacySettingsConfigData mPSCD; + private Preference mPreference; + + public AutoRestorePreferenceController(Context context, String key) { + super(context, key); + mPSCD = PrivacySettingsConfigData.getInstance(); + } + + @Override + public int getAvailabilityStatus() { + if (!PrivacySettingsUtils.isAdminUser(mContext)) { + return DISABLED_FOR_USER; + } + if (PrivacySettingsUtils.isInvisibleKey(mContext, PrivacySettingsUtils.AUTO_RESTORE)) { + return UNSUPPORTED_ON_DEVICE; + } + return AVAILABLE; + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + mPreference = preference; + preference.setEnabled(mPSCD.isBackupEnabled()); + } + + @Override + public boolean isChecked() { + final ContentResolver res = mContext.getContentResolver(); + + return Settings.Secure.getInt(res, + Settings.Secure.BACKUP_AUTO_RESTORE, 1) == 1; + } + + @Override + public boolean setChecked(boolean isChecked) { + final boolean nextValue = isChecked; + boolean result = false; + + final IBackupManager backupManager = IBackupManager.Stub.asInterface( + ServiceManager.getService(Context.BACKUP_SERVICE)); + + try { + backupManager.setAutoRestore(nextValue); + result = true; + } catch (RemoteException e) { + ((SwitchPreference) mPreference).setChecked(!nextValue); + Log.e(TAG, "Error can't set setAutoRestore", e); + } + + return result; + } +}
\ No newline at end of file diff --git a/src/com/android/settings/backup/BackupDataPreferenceController.java b/src/com/android/settings/backup/BackupDataPreferenceController.java new file mode 100644 index 0000000000..25ef58c62c --- /dev/null +++ b/src/com/android/settings/backup/BackupDataPreferenceController.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2018 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.backup; + +import android.content.Context; + +import androidx.preference.Preference; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; + +public class BackupDataPreferenceController extends BasePreferenceController { + private PrivacySettingsConfigData mPSCD; + + public BackupDataPreferenceController(Context context, String key) { + super(context, key); + mPSCD = PrivacySettingsConfigData.getInstance(); + } + + @Override + public int getAvailabilityStatus() { + if (!PrivacySettingsUtils.isAdminUser(mContext)) { + return DISABLED_FOR_USER; + } + if (PrivacySettingsUtils.isInvisibleKey(mContext, PrivacySettingsUtils.BACKUP_DATA)) { + return UNSUPPORTED_ON_DEVICE; + } + return AVAILABLE; + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + if (mPSCD.isBackupGray()) { + preference.setEnabled(false); + } + } + + @Override + public CharSequence getSummary() { + if (!mPSCD.isBackupGray()) { + return mPSCD.isBackupEnabled() + ? mContext.getText(R.string.accessibility_feature_state_on) + : mContext.getText(R.string.accessibility_feature_state_off); + } + return null; + } +}
\ No newline at end of file diff --git a/src/com/android/settings/backup/BackupInactivePreferenceController.java b/src/com/android/settings/backup/BackupInactivePreferenceController.java new file mode 100644 index 0000000000..86e1220442 --- /dev/null +++ b/src/com/android/settings/backup/BackupInactivePreferenceController.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2018 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.backup; + +import android.content.Context; + +import com.android.settings.core.BasePreferenceController; + +public class BackupInactivePreferenceController extends BasePreferenceController { + + public BackupInactivePreferenceController(Context context, String key) { + super(context, key); + } + + @Override + public int getAvailabilityStatus() { + if (!new BackupSettingsHelper(mContext).isBackupServiceActive()) { + return AVAILABLE_UNSEARCHABLE; + } + if (PrivacySettingsUtils.isInvisibleKey(mContext, PrivacySettingsUtils.BACKUP_INACTIVE)) { + return UNSUPPORTED_ON_DEVICE; + } + return AVAILABLE; + } +}
\ No newline at end of file diff --git a/src/com/android/settings/backup/BackupSettingsActivityPreferenceController.java b/src/com/android/settings/backup/BackupSettingsActivityPreferenceController.java deleted file mode 100644 index f0439d6d0a..0000000000 --- a/src/com/android/settings/backup/BackupSettingsActivityPreferenceController.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2017 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.backup; - -import android.app.backup.BackupManager; -import android.content.Context; -import android.os.UserManager; - -import com.android.settings.R; -import com.android.settings.core.BasePreferenceController; - -public class BackupSettingsActivityPreferenceController extends BasePreferenceController { - private static final String TAG = "BackupSettingActivityPC"; - - private static final String KEY_BACKUP_SETTINGS = "backup_settings"; - - private final UserManager mUm; - private final BackupManager mBackupManager; - - public BackupSettingsActivityPreferenceController(Context context) { - super(context, KEY_BACKUP_SETTINGS); - mUm = (UserManager) context.getSystemService(Context.USER_SERVICE); - mBackupManager = new BackupManager(context); - } - - @Override - public int getAvailabilityStatus() { - return mUm.isAdminUser() - ? AVAILABLE - : UNSUPPORTED_ON_DEVICE; - } - - @Override - public CharSequence getSummary() { - final boolean backupEnabled = mBackupManager.isBackupEnabled(); - - return backupEnabled - ? mContext.getText(R.string.accessibility_feature_state_on) - : mContext.getText(R.string.accessibility_feature_state_off); - } -}
\ No newline at end of file diff --git a/src/com/android/settings/backup/BackupSettingsContentProvider.java b/src/com/android/settings/backup/BackupSettingsContentProvider.java new file mode 100644 index 0000000000..5c888ab07c --- /dev/null +++ b/src/com/android/settings/backup/BackupSettingsContentProvider.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2019 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.backup; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.UriMatcher; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; + +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY; + +/** Provider stores and manages user interaction feedback for homepage contextual cards. */ +public class BackupSettingsContentProvider extends ContentProvider { + private static final String AUTHORITY = + "com.android.settings.backup.BackupSettingsContentProvider"; + private static final String SUMMARY = "summary"; + private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH); + static { + URI_MATCHER.addURI(AUTHORITY, SUMMARY, 1); + } + + @Override + public Bundle call(String method, String uri, Bundle extras) { + if (!SUMMARY.equals(method)) { + return null; + } + Bundle bundle = new Bundle(); + bundle.putString(META_DATA_PREFERENCE_SUMMARY, + new BackupSettingsHelper(getContext()).getSummary()); + return bundle; + } + + @Override + public boolean onCreate() { + return true; + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + throw new UnsupportedOperationException(); + } + + @Override + public String getType(Uri uri) { + throw new UnsupportedOperationException(); + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + throw new UnsupportedOperationException(); + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException(); + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException(); + } +} diff --git a/src/com/android/settings/backup/BackupSettingsFragment.java b/src/com/android/settings/backup/BackupSettingsFragment.java index e9bcc6fe9e..f08c8e7516 100644 --- a/src/com/android/settings/backup/BackupSettingsFragment.java +++ b/src/com/android/settings/backup/BackupSettingsFragment.java @@ -16,16 +16,16 @@ package com.android.settings.backup; +import android.app.settings.SettingsEnums; import android.content.Context; import android.os.Bundle; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; -import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.List; @@ -33,6 +33,7 @@ import java.util.List; /** * Fragment showing the items to launch different backup settings screens. */ +@SearchIndexable public class BackupSettingsFragment extends DashboardFragment { private static final String TAG = "BackupSettings"; @@ -67,7 +68,7 @@ public class BackupSettingsFragment extends DashboardFragment { return controllers; } - // The intention is to index {@link BackupSettingsActivity} instead of the fragments, + // The intention is to index {@link UserBackupSettingsActivity} instead of the fragments, // therefore leaving this index provider empty. public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = new BaseSearchIndexProvider() { @@ -75,6 +76,6 @@ public class BackupSettingsFragment extends DashboardFragment { @Override public int getMetricsCategory() { - return MetricsEvent.BACKUP_SETTINGS; + return SettingsEnums.BACKUP_SETTINGS; } } diff --git a/src/com/android/settings/backup/BackupSettingsHelper.java b/src/com/android/settings/backup/BackupSettingsHelper.java index 15f1dab92f..1d3455b149 100644 --- a/src/com/android/settings/backup/BackupSettingsHelper.java +++ b/src/com/android/settings/backup/BackupSettingsHelper.java @@ -24,16 +24,19 @@ import android.content.Intent; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; -import androidx.annotation.VisibleForTesting; +import android.os.UserManager; +import android.text.TextUtils; import android.util.Log; +import androidx.annotation.VisibleForTesting; + import com.android.settings.R; import com.android.settings.Settings.PrivacySettingsActivity; import java.net.URISyntaxException; /** - * Helper class for {@link BackupSettingsActivity} that interacts with {@link IBackupManager}. + * Helper class for {@link UserBackupSettingsActivity} that interacts with {@link IBackupManager}. */ public class BackupSettingsHelper { private static final String TAG = "BackupSettingsHelper"; @@ -48,6 +51,24 @@ public class BackupSettingsHelper { } /** + * If there is only one profile, show whether the backup is on or off. + * Otherwise, show nothing. + */ + String getSummary() { + UserManager userManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); + if (userManager.getUserProfiles().size() == 1) { + try { + int resId = mBackupManager.isBackupEnabled() ? R.string.backup_summary_state_on + : R.string.backup_summary_state_off; + return mContext.getText(resId).toString(); + } catch (RemoteException e) { + Log.e(TAG, "Error getting isBackupEnabled", e); + } + } + return null; + } + + /** * Returns an intent to launch backup settings from backup transport if the intent was provided * by the transport. Otherwise returns the intent to launch the default backup settings screen. * @@ -71,9 +92,9 @@ public class BackupSettingsHelper { * * @return Label for the backup settings item. */ - public String getLabelForBackupSettings() { - String label = getLabelFromBackupTransport(); - if (label == null || label.isEmpty()) { + public CharSequence getLabelForBackupSettings() { + CharSequence label = getLabelFromBackupTransport(); + if (TextUtils.isEmpty(label)) { label = mContext.getString(R.string.privacy_settings_title); } return label; @@ -151,7 +172,6 @@ public class BackupSettingsHelper { } private Intent getIntentForDefaultBackupSettings() { - // Extra needed by {@link SettingsDrawerActivity} to show the back button navigation. return new Intent(mContext, PrivacySettingsActivity.class); } @@ -190,7 +210,7 @@ public class BackupSettingsHelper { } /** Checks if backup service is enabled for this user. */ - private boolean isBackupServiceActive() { + public boolean isBackupServiceActive() { boolean backupOkay; try { backupOkay = mBackupManager.isBackupServiceActive(UserHandle.myUserId()); @@ -203,10 +223,11 @@ public class BackupSettingsHelper { } @VisibleForTesting - String getLabelFromBackupTransport() { + CharSequence getLabelFromBackupTransport() { try { - String label = - mBackupManager.getDataManagementLabel(mBackupManager.getCurrentTransport()); + CharSequence label = + mBackupManager.getDataManagementLabelForUser( + UserHandle.myUserId(), mBackupManager.getCurrentTransport()); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Received the backup settings label from backup transport: " + label); } diff --git a/src/com/android/settings/backup/BackupSettingsPreferenceController.java b/src/com/android/settings/backup/BackupSettingsPreferenceController.java index ec0c011585..4e0e3b4e74 100644 --- a/src/com/android/settings/backup/BackupSettingsPreferenceController.java +++ b/src/com/android/settings/backup/BackupSettingsPreferenceController.java @@ -19,6 +19,7 @@ package com.android.settings.backup; import android.content.Context; import android.content.Intent; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; @@ -30,7 +31,7 @@ public class BackupSettingsPreferenceController extends AbstractPreferenceContro private static final String BACKUP_SETTINGS = "backup_settings"; private static final String MANUFACTURER_SETTINGS = "manufacturer_backup"; private Intent mBackupSettingsIntent; - private String mBackupSettingsTitle; + private CharSequence mBackupSettingsTitle; private String mBackupSettingsSummary; private Intent mManufacturerIntent; private String mManufacturerLabel; diff --git a/src/com/android/settings/backup/ConfigureAccountPreferenceController.java b/src/com/android/settings/backup/ConfigureAccountPreferenceController.java new file mode 100644 index 0000000000..553edc25fa --- /dev/null +++ b/src/com/android/settings/backup/ConfigureAccountPreferenceController.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2018 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.backup; + +import android.content.Context; +import android.content.Intent; + +import androidx.preference.Preference; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; + +public class ConfigureAccountPreferenceController extends BasePreferenceController { + private PrivacySettingsConfigData mPSCD; + + public ConfigureAccountPreferenceController(Context context, String key) { + super(context, key); + mPSCD = PrivacySettingsConfigData.getInstance(); + } + + @Override + public int getAvailabilityStatus() { + if (!PrivacySettingsUtils.isAdminUser(mContext)) { + return DISABLED_FOR_USER; + } + if (PrivacySettingsUtils.isInvisibleKey(mContext, PrivacySettingsUtils.CONFIGURE_ACCOUNT)) { + return UNSUPPORTED_ON_DEVICE; + } + return AVAILABLE; + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + final Intent configIntent = mPSCD.getConfigIntent(); + final boolean configureEnabled = (configIntent != null) && mPSCD.isBackupEnabled(); + preference.setEnabled(configureEnabled); + preference.setIntent(configIntent); + } + + @Override + public CharSequence getSummary() { + final String configSummary = mPSCD.getConfigSummary(); + return configSummary != null + ? configSummary + : mContext.getText(R.string.backup_configure_account_default_summary); + } +}
\ No newline at end of file diff --git a/src/com/android/settings/backup/DataManagementPreferenceController.java b/src/com/android/settings/backup/DataManagementPreferenceController.java new file mode 100644 index 0000000000..34d51ac3cb --- /dev/null +++ b/src/com/android/settings/backup/DataManagementPreferenceController.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2018 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.backup; + +import android.content.Context; + +import androidx.preference.Preference; + +import com.android.settings.core.BasePreferenceController; + +public class DataManagementPreferenceController extends BasePreferenceController { + private PrivacySettingsConfigData mPSCD; + + public DataManagementPreferenceController(Context context, String key) { + super(context, key); + mPSCD = PrivacySettingsConfigData.getInstance(); + } + + @Override + public int getAvailabilityStatus() { + if (!PrivacySettingsUtils.isAdminUser(mContext)) { + return DISABLED_FOR_USER; + } + boolean manageEnabled = (mPSCD.getManageIntent() != null) && mPSCD.isBackupEnabled(); + if (!manageEnabled) { + return UNSUPPORTED_ON_DEVICE; + } + return AVAILABLE; + } + + @Override + public void updateState(Preference preference) { + if (!isAvailable()) { + return; + } + preference.setIntent(mPSCD.getManageIntent()); + final CharSequence manageLabel = mPSCD.getManageLabel(); + if (manageLabel != null) { + preference.setTitle(manageLabel); + } + } +}
\ No newline at end of file diff --git a/src/com/android/settings/backup/OWNERS b/src/com/android/settings/backup/OWNERS index c026a350e7..a7b55fd134 100644 --- a/src/com/android/settings/backup/OWNERS +++ b/src/com/android/settings/backup/OWNERS @@ -1,6 +1,13 @@ -# Default reviewers for this and subdirectories. +# Use this reviewer by default. +br-framework-team+reviews@google.com + +# People who can approve changes for submission. +anniemeng@google.com +nathch@google.com +rthakohov@google.com + +# Others (in case above are not available). bryanmawhinney@google.com -cprins@google.com jorlow@google.com philippov@google.com stefanot@google.com
\ No newline at end of file diff --git a/src/com/android/settings/backup/PrivacySettings.java b/src/com/android/settings/backup/PrivacySettings.java new file mode 100644 index 0000000000..e6d2bd443c --- /dev/null +++ b/src/com/android/settings/backup/PrivacySettings.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2009 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.backup; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.provider.SearchIndexableResource; + +import com.android.settings.R; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settingslib.search.SearchIndexable; + +import java.util.Arrays; +import java.util.List; + +@SearchIndexable +public class PrivacySettings extends DashboardFragment { + private static final String TAG = "PrivacySettings"; + + @Override + public int getMetricsCategory() { + return SettingsEnums.PRIVACY; + } + + @Override + protected String getLogTag() { + return TAG; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.privacy_settings; + } + + @Override + public int getHelpResource() { + return R.string.help_url_backup_reset; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + updatePrivacySettingsConfigData(context); + } + + @Override + protected void updatePreferenceStates() { + updatePrivacySettingsConfigData(getContext()); + super.updatePreferenceStates(); + } + + private void updatePrivacySettingsConfigData(final Context context) { + if (PrivacySettingsUtils.isAdminUser(context)) { + PrivacySettingsUtils.updatePrivacyBuffer(context, + PrivacySettingsConfigData.getInstance()); + } + } + + public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, + boolean enabled) { + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.privacy_settings; + return Arrays.asList(sir); + } + + @Override + protected boolean isPageSearchEnabled(Context context) { + final BackupSettingsHelper backupHelper = new BackupSettingsHelper(context); + return !backupHelper.isBackupProvidedByManufacturer() && + !backupHelper.isIntentProvidedByTransport(); + } + }; +} diff --git a/src/com/android/settings/backup/PrivacySettingsConfigData.java b/src/com/android/settings/backup/PrivacySettingsConfigData.java new file mode 100644 index 0000000000..8bea11b9d5 --- /dev/null +++ b/src/com/android/settings/backup/PrivacySettingsConfigData.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2018 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.backup; + +import android.content.Intent; + +public class PrivacySettingsConfigData { + + private static PrivacySettingsConfigData sInstance; + + private boolean mBackupEnabled; + private boolean mBackupGray; + private Intent mConfigIntent; + private String mConfigSummary; + private Intent mManageIntent; + private CharSequence mManageLabel; + + private PrivacySettingsConfigData() { + mBackupEnabled = false; + mBackupGray = false; + mConfigIntent = null; + mConfigSummary = null; + mManageIntent = null; + mManageLabel = null; + } + + public static PrivacySettingsConfigData getInstance() { + if (sInstance == null) { + sInstance = new PrivacySettingsConfigData(); + } + return sInstance; + } + + public boolean isBackupEnabled() { + return mBackupEnabled; + } + + public void setBackupEnabled(final boolean backupEnabled) { + mBackupEnabled = backupEnabled; + } + + public boolean isBackupGray() { + return mBackupGray; + } + + public void setBackupGray(final boolean backupGray) { + mBackupGray = backupGray; + } + + public Intent getConfigIntent() { + return mConfigIntent; + } + + public void setConfigIntent(final Intent configIntent) { + mConfigIntent = configIntent; + } + + public String getConfigSummary() { + return mConfigSummary; + } + + public void setConfigSummary(final String configSummary) { + mConfigSummary = configSummary; + } + + public Intent getManageIntent() { + return mManageIntent; + } + + public void setManageIntent(final Intent manageIntent) { + mManageIntent = manageIntent; + } + + public CharSequence getManageLabel() { + return mManageLabel; + } + + public void setManageLabel(final CharSequence manageLabel) { + mManageLabel = manageLabel; + } +} diff --git a/src/com/android/settings/backup/PrivacySettingsUtils.java b/src/com/android/settings/backup/PrivacySettingsUtils.java new file mode 100644 index 0000000000..bb1108c56d --- /dev/null +++ b/src/com/android/settings/backup/PrivacySettingsUtils.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2018 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.backup; + +import android.app.backup.IBackupManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.UserHandle; +import android.os.UserManager; +import android.util.Log; + +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +public class PrivacySettingsUtils { + private static final String TAG = "PrivacySettingsUtils"; + private static final String GSETTINGS_PROVIDER = "com.google.settings"; + + static final String BACKUP_DATA = "backup_data"; + static final String AUTO_RESTORE = "auto_restore"; + static final String CONFIGURE_ACCOUNT = "configure_account"; + static final String BACKUP_INACTIVE = "backup_inactive"; + + // Don't allow any access if this is not an admin user. + // TODO: backup/restore currently only works with owner user b/22760572 + static boolean isAdminUser(final Context context) { + return UserManager.get(context).isAdminUser(); + } + + /** + * Send a {@param key} to check its preference will display in PrivacySettings or not. + */ + static boolean isInvisibleKey(final Context context, final String key) { + final Set<String> keysToRemove = getInvisibleKey(context); + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, + "keysToRemove size=" + keysToRemove.size() + " keysToRemove=" + keysToRemove); + } + if (keysToRemove.contains(key)) { + return true; + } + return false; + } + + private static Set<String> getInvisibleKey(final Context context) { + final IBackupManager backupManager = IBackupManager.Stub.asInterface( + ServiceManager.getService(Context.BACKUP_SERVICE)); + boolean isServiceActive = false; + try { + isServiceActive = backupManager.isBackupServiceActive(UserHandle.myUserId()); + } catch (RemoteException e) { + Log.w(TAG, "Failed querying backup manager service activity status. " + + "Assuming it is inactive."); + } + boolean vendorSpecific = context.getPackageManager(). + resolveContentProvider(GSETTINGS_PROVIDER, 0) == null; + final Set<String> inVisibleKeys = new TreeSet<>(); + if (vendorSpecific || isServiceActive) { + inVisibleKeys.add(BACKUP_INACTIVE); + } + if (vendorSpecific || !isServiceActive) { + inVisibleKeys.add(BACKUP_DATA); + inVisibleKeys.add(AUTO_RESTORE); + inVisibleKeys.add(CONFIGURE_ACCOUNT); + } + return inVisibleKeys; + } + + public static void updatePrivacyBuffer(final Context context, PrivacySettingsConfigData data) { + final IBackupManager backupManager = IBackupManager.Stub.asInterface( + ServiceManager.getService(Context.BACKUP_SERVICE)); + + try { + data.setBackupEnabled(backupManager.isBackupEnabled()); + String transport = backupManager.getCurrentTransport(); + data.setConfigIntent(validatedActivityIntent(context, + backupManager.getConfigurationIntent(transport), "config")); + data.setConfigSummary(backupManager.getDestinationString(transport)); + data.setManageIntent(validatedActivityIntent(context, + backupManager.getDataManagementIntent(transport), "management")); + data.setManageLabel( + backupManager.getDataManagementLabelForUser(UserHandle.myUserId(), transport)); + data.setBackupGray(false); + } catch (RemoteException e) { + // leave it 'false' and disable the UI; there's no backup manager + // mBackup.setEnabled(false); + data.setBackupGray(true); + } + } + + private static Intent validatedActivityIntent(final Context context, Intent intent, + String logLabel) { + if (intent != null) { + PackageManager pm = context.getPackageManager(); + List<ResolveInfo> resolved = pm.queryIntentActivities(intent, 0); + if (resolved == null || resolved.isEmpty()) { + intent = null; + Log.e(TAG, "Backup " + logLabel + " intent " + intent + + " fails to resolve; ignoring"); + } + } + return intent; + } +}
\ No newline at end of file diff --git a/src/com/android/settings/backup/SettingsBackupHelper.java b/src/com/android/settings/backup/SettingsBackupHelper.java new file mode 100644 index 0000000000..92612b0548 --- /dev/null +++ b/src/com/android/settings/backup/SettingsBackupHelper.java @@ -0,0 +1,89 @@ +/** + * Copyright (C) 2019 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.backup; + +import android.app.backup.BackupAgentHelper; +import android.app.backup.BackupDataInputStream; +import android.app.backup.BackupDataOutput; +import android.app.backup.BackupHelper; +import android.os.ParcelFileDescriptor; + +import com.android.settings.shortcut.CreateShortcutPreferenceController; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; + +/** + * Backup agent for Settings APK + */ +public class SettingsBackupHelper extends BackupAgentHelper { + + @Override + public void onCreate() { + super.onCreate(); + addHelper("no-op", new NoOpHelper()); + } + + @Override + public void onRestoreFinished() { + super.onRestoreFinished(); + CreateShortcutPreferenceController.updateRestoredShortcuts(this); + } + + /** + * Backup helper which does not do anything. Having at least one helper ensures that the + * transport is not empty and onRestoreFinished is called eventually. + */ + private static class NoOpHelper implements BackupHelper { + + private final int VERSION_CODE = 1; + + @Override + public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data, + ParcelFileDescriptor newState) { + + try (FileOutputStream out = new FileOutputStream(newState.getFileDescriptor())) { + if (getVersionCode(oldState) != VERSION_CODE) { + data.writeEntityHeader("dummy", 1); + data.writeEntityData(new byte[1], 1); + } + + // Write new version code + out.write(VERSION_CODE); + out.flush(); + } catch (IOException e) { } + } + + @Override + public void restoreEntity(BackupDataInputStream data) { } + + @Override + public void writeNewStateDescription(ParcelFileDescriptor newState) { } + + private int getVersionCode(ParcelFileDescriptor state) { + if (state == null) { + return 0; + } + try (FileInputStream in = new FileInputStream(state.getFileDescriptor())) { + return in.read(); + } catch (IOException e) { + return 0; + } + } + } +} diff --git a/src/com/android/settings/backup/ToggleBackupSettingFragment.java b/src/com/android/settings/backup/ToggleBackupSettingFragment.java index d7ff0e6de9..8b3a54a65c 100644 --- a/src/com/android/settings/backup/ToggleBackupSettingFragment.java +++ b/src/com/android/settings/backup/ToggleBackupSettingFragment.java @@ -1,22 +1,23 @@ package com.android.settings.backup; -import android.app.AlertDialog; import android.app.Dialog; import android.app.backup.IBackupManager; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; import android.os.RemoteException; import android.os.ServiceManager; import android.provider.Settings; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; -import androidx.preference.PreferenceViewHolder; import android.util.Log; import android.view.View; import android.widget.TextView; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.appcompat.app.AlertDialog; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; +import androidx.preference.PreferenceViewHolder; + import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.SettingsPreferenceFragment; @@ -193,7 +194,7 @@ public class ToggleBackupSettingFragment extends SettingsPreferenceFragment @Override public int getMetricsCategory() { - return MetricsEvent.PRIVACY; + return SettingsEnums.PRIVACY; } /** diff --git a/src/com/android/settings/backup/BackupSettingsActivity.java b/src/com/android/settings/backup/UserBackupSettingsActivity.java index a16ab7839b..c2bcd07c8f 100644 --- a/src/com/android/settings/backup/BackupSettingsActivity.java +++ b/src/com/android/settings/backup/UserBackupSettingsActivity.java @@ -16,21 +16,21 @@ package com.android.settings.backup; -import android.app.Activity; -import android.app.FragmentManager; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.os.Bundle; -import android.os.UserHandle; -import androidx.annotation.VisibleForTesting; import android.util.Log; -import com.android.settings.R; +import androidx.annotation.VisibleForTesting; +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; +import com.android.settings.R; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; import com.android.settings.search.SearchIndexableRaw; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.List; @@ -39,8 +39,14 @@ import java.util.List; /** * The activity used to launch the configured Backup activity or the preference screen * if the manufacturer provided their backup settings. + * Pre-Q, BackupSettingsActivity was disabled for non-system users. Therefore, for phones which + * upgrade to Q, BackupSettingsActivity is disabled for those users. However, we cannot simply + * enable it in Q since component enable can only be done by the user itself; which is not + * enough in Q we want it to be enabled for all profile users of the user. + * Therefore, as a simple workaround, we use a new class which is enabled by default. */ -public class BackupSettingsActivity extends Activity implements Indexable { +@SearchIndexable +public class UserBackupSettingsActivity extends FragmentActivity implements Indexable { private static final String TAG = "BackupSettingsActivity"; private FragmentManager mFragmentManager; @@ -79,7 +85,7 @@ public class BackupSettingsActivity extends Activity implements Indexable { } // mFragmentManager can be set by {@link #setFragmentManager()} for testing if (mFragmentManager == null) { - mFragmentManager = getFragmentManager(); + mFragmentManager = getSupportFragmentManager(); } mFragmentManager.beginTransaction() .replace(android.R.id.content, new BackupSettingsFragment()) @@ -92,7 +98,7 @@ public class BackupSettingsActivity extends Activity implements Indexable { */ public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = new BaseSearchIndexProvider() { - private static final String BACKUP_SEARCH_INDEX_KEY = "backup"; + private static final String BACKUP_SEARCH_INDEX_KEY = "Backup"; @Override public List<SearchIndexableRaw> getRawDataToIndex(Context context, @@ -106,7 +112,7 @@ public class BackupSettingsActivity extends Activity implements Indexable { data.screenTitle = context.getString(R.string.settings_label); data.keywords = context.getString(R.string.keywords_backup); data.intentTargetPackage = context.getPackageName(); - data.intentTargetClass = BackupSettingsActivity.class.getName(); + data.intentTargetClass = UserBackupSettingsActivity.class.getName(); data.intentAction = Intent.ACTION_MAIN; data.key = BACKUP_SEARCH_INDEX_KEY; result.add(data); @@ -117,16 +123,9 @@ public class BackupSettingsActivity extends Activity implements Indexable { @Override public List<String> getNonIndexableKeys(Context context) { final List<String> keys = super.getNonIndexableKeys(context); - - // For non-primary user, no backup is available, so don't show it in search - // TODO: http://b/22388012 - if (UserHandle.myUserId() != UserHandle.USER_SYSTEM) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Not a system user, not indexing the screen"); - } + if (!new BackupSettingsHelper(context).isBackupServiceActive()) { keys.add(BACKUP_SEARCH_INDEX_KEY); } - return keys; } }; diff --git a/src/com/android/settings/biometrics/BiometricEnrollActivity.java b/src/com/android/settings/biometrics/BiometricEnrollActivity.java new file mode 100644 index 0000000000..64ddf4f8c4 --- /dev/null +++ b/src/com/android/settings/biometrics/BiometricEnrollActivity.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2018 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.biometrics; + +import android.app.settings.SettingsEnums; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.os.UserHandle; + +import com.android.settings.SetupWizardUtils; +import com.android.settings.biometrics.face.FaceEnrollIntroduction; +import com.android.settings.biometrics.fingerprint.FingerprintEnrollFindSensor; +import com.android.settings.biometrics.fingerprint.FingerprintEnrollIntroduction; +import com.android.settings.biometrics.fingerprint.SetupFingerprintEnrollIntroduction; +import com.android.settings.core.InstrumentedActivity; +import com.android.settings.password.ChooseLockSettingsHelper; + +import com.google.android.setupcompat.util.WizardManagerHelper; + +/** + * Trampoline activity launched by the {@code android.settings.BIOMETRIC_ENROLL} action which + * shows the user an appropriate enrollment flow depending on the device's biometric hardware. + * This activity must only allow enrollment of biometrics that can be used by + * {@link android.hardware.biometrics.BiometricPrompt}. + */ +public class BiometricEnrollActivity extends InstrumentedActivity { + + private static final String TAG = "BiometricEnrollActivity"; + + public static final String EXTRA_SKIP_INTRO = "skip_intro"; + + public static final class InternalActivity extends BiometricEnrollActivity {} + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + final PackageManager pm = getApplicationContext().getPackageManager(); + Intent intent = null; + + // This logic may have to be modified on devices with multiple biometrics. + if (pm.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) { + // ChooseLockGeneric can request to start fingerprint enroll bypassing the intro screen. + if (getIntent().getBooleanExtra(EXTRA_SKIP_INTRO, false) + && this instanceof InternalActivity) { + intent = getFingerprintFindSensorIntent(); + } else { + intent = getFingerprintIntroIntent(); + } + } else if (pm.hasSystemFeature(PackageManager.FEATURE_FACE)) { + intent = getFaceIntroIntent(); + } + + if (intent != null) { + intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); + + if (this instanceof InternalActivity) { + // Propagate challenge and user Id from ChooseLockGeneric. + final byte[] token = getIntent() + .getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN); + final int userId = getIntent() + .getIntExtra(Intent.EXTRA_USER_ID, UserHandle.USER_NULL); + + intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token); + intent.putExtra(Intent.EXTRA_USER_ID, userId); + } + + startActivity(intent); + } + finish(); + } + + private Intent getFingerprintFindSensorIntent() { + Intent intent = new Intent(this, FingerprintEnrollFindSensor.class); + SetupWizardUtils.copySetupExtras(getIntent(), intent); + return intent; + } + + private Intent getFingerprintIntroIntent() { + if (WizardManagerHelper.isAnySetupWizard(getIntent())) { + Intent intent = new Intent(this, SetupFingerprintEnrollIntroduction.class); + WizardManagerHelper.copyWizardManagerExtras(getIntent(), intent); + return intent; + } else { + return new Intent(this, FingerprintEnrollIntroduction.class); + } + } + + private Intent getFaceIntroIntent() { + Intent intent = new Intent(this, FaceEnrollIntroduction.class); + WizardManagerHelper.copyWizardManagerExtras(getIntent(), intent); + return intent; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.BIOMETRIC_ENROLL_ACTIVITY; + } +} diff --git a/src/com/android/settings/biometrics/BiometricEnrollBase.java b/src/com/android/settings/biometrics/BiometricEnrollBase.java new file mode 100644 index 0000000000..c3f794f690 --- /dev/null +++ b/src/com/android/settings/biometrics/BiometricEnrollBase.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2018 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.biometrics; + +import static com.android.settings.Utils.SETTINGS_PACKAGE_NAME; + +import android.annotation.Nullable; +import android.content.Intent; +import android.content.res.Resources; +import android.graphics.Color; +import android.os.Bundle; +import android.os.UserHandle; +import android.text.TextUtils; +import android.view.View; +import android.widget.TextView; + +import com.android.settings.R; +import com.android.settings.SetupWizardUtils; +import com.android.settings.biometrics.fingerprint.FingerprintEnrollEnrolling; +import com.android.settings.core.InstrumentedActivity; +import com.android.settings.password.ChooseLockSettingsHelper; + +import com.google.android.setupcompat.template.FooterBarMixin; +import com.google.android.setupcompat.template.FooterButton; +import com.google.android.setupdesign.GlifLayout; + +/** + * Base activity for all biometric enrollment steps. + */ +public abstract class BiometricEnrollBase extends InstrumentedActivity { + + public static final String EXTRA_FROM_SETTINGS_SUMMARY = "from_settings_summary"; + public static final String EXTRA_KEY_LAUNCHED_CONFIRM = "launched_confirm_lock"; + public static final String EXTRA_KEY_REQUIRE_VISION = "accessibility_vision"; + public static final String EXTRA_KEY_REQUIRE_DIVERSITY = "accessibility_diversity"; + + /** + * 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. + */ + public 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. + */ + public 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. + */ + public static final int RESULT_TIMEOUT = RESULT_FIRST_USER + 2; + + public static final int CHOOSE_LOCK_GENERIC_REQUEST = 1; + public static final int BIOMETRIC_FIND_SENSOR_REQUEST = 2; + public static final int LEARN_MORE_REQUEST = 3; + public static final int CONFIRM_REQUEST = 4; + public static final int ENROLL_REQUEST = 5; + + protected boolean mLaunchedConfirmLock; + protected byte[] mToken; + protected int mUserId; + protected boolean mFromSettingsSummary; + protected FooterBarMixin mFooterBarMixin; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mToken = getIntent().getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN); + mFromSettingsSummary = getIntent().getBooleanExtra(EXTRA_FROM_SETTINGS_SUMMARY, false); + if (savedInstanceState != null && mToken == null) { + mLaunchedConfirmLock = savedInstanceState.getBoolean(EXTRA_KEY_LAUNCHED_CONFIRM); + mToken = savedInstanceState.getByteArray( + ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN); + mFromSettingsSummary = + savedInstanceState.getBoolean(EXTRA_FROM_SETTINGS_SUMMARY, false); + } + mUserId = getIntent().getIntExtra(Intent.EXTRA_USER_ID, UserHandle.myUserId()); + } + + @Override + protected void onApplyThemeResource(Resources.Theme theme, int resid, boolean first) { + resid = SetupWizardUtils.getTheme(getIntent()); + theme.applyStyle(R.style.SetupWizardPartnerResource, true); + super.onApplyThemeResource(theme, resid, first); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean(EXTRA_KEY_LAUNCHED_CONFIRM, mLaunchedConfirmLock); + outState.putByteArray(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, mToken); + outState.putBoolean(EXTRA_FROM_SETTINGS_SUMMARY, mFromSettingsSummary); + } + + @Override + protected void onPostCreate(@Nullable Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + initViews(); + } + + protected void initViews() { + getWindow().setStatusBarColor(Color.TRANSPARENT); + } + + protected GlifLayout getLayout() { + return (GlifLayout) findViewById(R.id.setup_wizard_layout); + } + + protected void setHeaderText(int resId, boolean force) { + TextView layoutTitle = getLayout().getHeaderTextView(); + CharSequence previousTitle = layoutTitle.getText(); + CharSequence title = getText(resId); + if (previousTitle != title || force) { + if (!TextUtils.isEmpty(previousTitle)) { + layoutTitle.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); + } + getLayout().setHeaderText(title); + setTitle(title); + } + } + + protected void setHeaderText(int resId) { + setHeaderText(resId, false /* force */); + } + + protected FooterButton getNextButton() { + if (mFooterBarMixin != null) { + return mFooterBarMixin.getPrimaryButton(); + } + return null; + } + + protected void onNextButtonClick(View view) { + } + + protected Intent getFingerprintEnrollingIntent() { + Intent intent = new Intent(); + intent.setClassName(SETTINGS_PACKAGE_NAME, FingerprintEnrollEnrolling.class.getName()); + intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, mToken); + intent.putExtra(EXTRA_FROM_SETTINGS_SUMMARY, mFromSettingsSummary); + if (mUserId != UserHandle.USER_NULL) { + intent.putExtra(Intent.EXTRA_USER_ID, mUserId); + } + return intent; + } + + protected void launchConfirmLock(int titleResId, long challenge) { + ChooseLockSettingsHelper helper = new ChooseLockSettingsHelper(this); + boolean launchedConfirmationActivity; + if (mUserId == UserHandle.USER_NULL) { + launchedConfirmationActivity = helper.launchConfirmationActivity(CONFIRM_REQUEST, + getString(titleResId), + null, null, challenge); + } else { + launchedConfirmationActivity = helper.launchConfirmationActivity(CONFIRM_REQUEST, + getString(titleResId), + null, null, challenge, mUserId); + } + if (!launchedConfirmationActivity) { + // This shouldn't happen, as we should only end up at this step if a lock thingy is + // already set. + finish(); + } else { + mLaunchedConfirmLock = true; + } + } +} diff --git a/src/com/android/settings/biometrics/BiometricEnrollIntroduction.java b/src/com/android/settings/biometrics/BiometricEnrollIntroduction.java new file mode 100644 index 0000000000..d5414c9664 --- /dev/null +++ b/src/com/android/settings/biometrics/BiometricEnrollIntroduction.java @@ -0,0 +1,289 @@ +/* + * Copyright (C) 2018 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.biometrics; + +import android.app.admin.DevicePolicyManager; +import android.content.Intent; +import android.os.Bundle; +import android.os.UserHandle; +import android.os.UserManager; +import android.os.storage.StorageManager; +import android.view.View; +import android.widget.TextView; + +import com.android.internal.widget.LockPatternUtils; +import com.android.settings.R; +import com.android.settings.SetupWizardUtils; +import com.android.settings.password.ChooseLockGeneric; +import com.android.settings.password.ChooseLockGeneric.ChooseLockGenericFragment; +import com.android.settings.password.ChooseLockSettingsHelper; +import com.android.settings.password.SetupChooseLockGeneric; + +import com.google.android.setupcompat.template.FooterButton; +import com.google.android.setupcompat.util.WizardManagerHelper; +import com.google.android.setupdesign.span.LinkSpan; + +/** + * Abstract base class for the intro onboarding activity for biometric enrollment. + */ +public abstract class BiometricEnrollIntroduction extends BiometricEnrollBase + implements LinkSpan.OnClickListener { + + private UserManager mUserManager; + private boolean mHasPassword; + private boolean mBiometricUnlockDisabledByAdmin; + private TextView mErrorText; + + /** + * @return true if the biometric is disabled by a device administrator + */ + protected abstract boolean isDisabledByAdmin(); + + /** + * @return the layout resource + */ + protected abstract int getLayoutResource(); + + /** + * @return the header resource for if the biometric has been disabled by a device administrator + */ + protected abstract int getHeaderResDisabledByAdmin(); + + /** + * @return the default header resource + */ + protected abstract int getHeaderResDefault(); + + /** + * @return the description resource for if the biometric has been disabled by a device admin + */ + protected abstract int getDescriptionResDisabledByAdmin(); + + /** + * @return the cancel button + */ + protected abstract FooterButton getCancelButton(); + + /** + * @return the next button + */ + protected abstract FooterButton getNextButton(); + + /** + * @return the error TextView + */ + protected abstract TextView getErrorTextView(); + + /** + * @return 0 if there are no errors, otherwise returns the resource ID for the error string + * to be displayed. + */ + protected abstract int checkMaxEnrolled(); + + /** + * @return the challenge generated by the biometric hardware + */ + protected abstract long getChallenge(); + + /** + * @return one of the ChooseLockSettingsHelper#EXTRA_KEY_FOR_* constants + */ + protected abstract String getExtraKeyForBiometric(); + + /** + * @return the intent for proceeding to the next step of enrollment. For Fingerprint, this + * should lead to the "Find Sensor" activity. For Face, this should lead to the "Enrolling" + * activity. + */ + protected abstract Intent getEnrollingIntent(); + + /** + * @return the title to be shown on the ConfirmLock screen. + */ + protected abstract int getConfirmLockTitleResId(); + + /** + * @param span + */ + public abstract void onClick(LinkSpan span); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Intent intent = getIntent(); + if (intent.getStringExtra(WizardManagerHelper.EXTRA_THEME) == null) { + // Put the theme in the intent so it gets propagated to other activities in the flow + intent.putExtra( + WizardManagerHelper.EXTRA_THEME, + SetupWizardUtils.getThemeString(intent)); + } + + mBiometricUnlockDisabledByAdmin = isDisabledByAdmin(); + + setContentView(getLayoutResource()); + if (mBiometricUnlockDisabledByAdmin) { + setHeaderText(getHeaderResDisabledByAdmin()); + } else { + setHeaderText(getHeaderResDefault()); + } + + mErrorText = getErrorTextView(); + + mUserManager = UserManager.get(this); + updatePasswordQuality(); + + if (!mHasPassword) { + // No password registered, launch into enrollment wizard. + launchChooseLock(); + } else if (mToken == null) { + // It's possible to have a token but mLaunchedConfirmLock == false, since + // ChooseLockGeneric can pass us a token. + launchConfirmLock(getConfirmLockTitleResId(), getChallenge()); + } + } + + @Override + protected void onResume() { + super.onResume(); + + final int errorMsg = checkMaxEnrolled(); + if (errorMsg == 0) { + mErrorText.setText(null); + mErrorText.setVisibility(View.GONE); + getNextButton().setVisibility(View.VISIBLE); + } else { + mErrorText.setText(errorMsg); + mErrorText.setVisibility(View.VISIBLE); + getNextButton().setText(getResources().getString(R.string.done)); + getNextButton().setVisibility(View.VISIBLE); + } + } + + private void updatePasswordQuality() { + final int passwordQuality = new ChooseLockSettingsHelper(this).utils() + .getActivePasswordQuality(mUserManager.getCredentialOwnerProfile(mUserId)); + mHasPassword = passwordQuality != DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED; + } + + @Override + protected void onNextButtonClick(View view) { + if (checkMaxEnrolled() == 0) { + // Lock thingy is already set up, launch directly to the next page + launchNextEnrollingActivity(mToken); + } else { + setResult(RESULT_FINISHED); + finish(); + } + } + + private void launchChooseLock() { + Intent intent = getChooseLockIntent(); + long challenge = getChallenge(); + intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment.MINIMUM_QUALITY_KEY, + DevicePolicyManager.PASSWORD_QUALITY_SOMETHING); + intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment.HIDE_DISABLED_PREFS, true); + intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_HAS_CHALLENGE, true); + intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE, challenge); + intent.putExtra(getExtraKeyForBiometric(), true); + if (mUserId != UserHandle.USER_NULL) { + intent.putExtra(Intent.EXTRA_USER_ID, mUserId); + } + startActivityForResult(intent, CHOOSE_LOCK_GENERIC_REQUEST); + } + + private void launchNextEnrollingActivity(byte[] token) { + Intent intent = getEnrollingIntent(); + if (token != null) { + intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token); + } + if (mUserId != UserHandle.USER_NULL) { + intent.putExtra(Intent.EXTRA_USER_ID, mUserId); + } + intent.putExtra(EXTRA_FROM_SETTINGS_SUMMARY, mFromSettingsSummary); + startActivityForResult(intent, BIOMETRIC_FIND_SENSOR_REQUEST); + } + + protected Intent getChooseLockIntent() { + if (WizardManagerHelper.isAnySetupWizard(getIntent())) { + // Default to PIN lock in setup wizard + Intent intent = new Intent(this, SetupChooseLockGeneric.class); + if (StorageManager.isFileEncryptedNativeOrEmulated()) { + intent.putExtra( + LockPatternUtils.PASSWORD_TYPE_KEY, + DevicePolicyManager.PASSWORD_QUALITY_NUMERIC); + intent.putExtra(ChooseLockGenericFragment.EXTRA_SHOW_OPTIONS_BUTTON, true); + } + WizardManagerHelper.copyWizardManagerExtras(getIntent(), intent); + return intent; + } else { + return new Intent(this, ChooseLockGeneric.class); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == BIOMETRIC_FIND_SENSOR_REQUEST) { + if (resultCode == RESULT_FINISHED || resultCode == RESULT_SKIP) { + setResult(resultCode, data); + finish(); + return; + } + } else if (requestCode == CHOOSE_LOCK_GENERIC_REQUEST) { + if (resultCode == RESULT_FINISHED) { + updatePasswordQuality(); + mToken = data.getByteArrayExtra( + ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN); + overridePendingTransition(R.anim.sud_slide_next_in, R.anim.sud_slide_next_out); + return; + } else { + setResult(resultCode, data); + finish(); + } + } else if (requestCode == CONFIRM_REQUEST) { + if (resultCode == RESULT_OK && data != null) { + mToken = data.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN); + overridePendingTransition(R.anim.sud_slide_next_in, R.anim.sud_slide_next_out); + } else { + setResult(resultCode, data); + finish(); + } + } else if (requestCode == LEARN_MORE_REQUEST) { + overridePendingTransition(R.anim.sud_slide_back_in, R.anim.sud_slide_back_out); + } + super.onActivityResult(requestCode, resultCode, data); + } + + protected void onCancelButtonClick(View view) { + finish(); + } + + protected void onSkipButtonClick(View view) { + setResult(RESULT_SKIP); + finish(); + } + + @Override + protected void initViews() { + super.initViews(); + + TextView description = (TextView) findViewById(R.id.sud_layout_description); + if (mBiometricUnlockDisabledByAdmin) { + description.setText(getDescriptionResDisabledByAdmin()); + } + } +} diff --git a/src/com/android/settings/fingerprint/FingerprintEnrollSidecar.java b/src/com/android/settings/biometrics/BiometricEnrollSidecar.java index 7f7cb67324..cedbec1a0a 100644 --- a/src/com/android/settings/fingerprint/FingerprintEnrollSidecar.java +++ b/src/com/android/settings/biometrics/BiometricEnrollSidecar.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 The Android Open Source Project + * Copyright (C) 2018 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. @@ -14,41 +14,46 @@ * limitations under the License */ -package com.android.settings.fingerprint; +package com.android.settings.biometrics; import android.annotation.Nullable; import android.app.Activity; import android.content.Intent; -import android.hardware.fingerprint.FingerprintManager; import android.os.Bundle; import android.os.CancellationSignal; import android.os.Handler; import android.os.UserHandle; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.settings.Utils; import com.android.settings.core.InstrumentedFragment; import com.android.settings.password.ChooseLockSettingsHelper; import java.util.ArrayList; /** - * Sidecar fragment to handle the state around fingerprint enrollment. + * Abstract sidecar fragment to handle the state around biometric enrollment. This sidecar manages + * the state of enrollment throughout the activity lifecycle so the app can continue after an + * event like rotation. */ -public class FingerprintEnrollSidecar extends InstrumentedFragment { +public abstract class BiometricEnrollSidecar extends InstrumentedFragment { + + public interface Listener { + void onEnrollmentHelp(int helpMsgId, CharSequence helpString); + void onEnrollmentError(int errMsgId, CharSequence errString); + void onEnrollmentProgressChange(int steps, int remaining); + } private int mEnrollmentSteps = -1; private int mEnrollmentRemaining = 0; private Listener mListener; private boolean mEnrolling; - private CancellationSignal mEnrollmentCancel; private Handler mHandler = new Handler(); - private byte[] mToken; private boolean mDone; - private int mUserId; - private FingerprintManager mFingerprintManager; private ArrayList<QueuedEvent> mQueuedEvents; + protected CancellationSignal mEnrollmentCancel; + protected byte[] mToken; + protected int mUserId; + private abstract class QueuedEvent { public abstract void send(Listener listener); } @@ -77,7 +82,7 @@ public class FingerprintEnrollSidecar extends InstrumentedFragment { @Override public void send(Listener listener) { - listener.onEnrollmentHelp(helpString); + listener.onEnrollmentHelp(helpMsgId, helpString); } } @@ -95,7 +100,14 @@ public class FingerprintEnrollSidecar extends InstrumentedFragment { } } - public FingerprintEnrollSidecar() { + private final Runnable mTimeoutRunnable = new Runnable() { + @Override + public void run() { + cancelEnrollment(); + } + }; + + public BiometricEnrollSidecar() { mQueuedEvents = new ArrayList<>(); } @@ -108,7 +120,6 @@ public class FingerprintEnrollSidecar extends InstrumentedFragment { @Override public void onAttach(Activity activity) { super.onAttach(activity); - mFingerprintManager = Utils.getFingerprintManagerOrNull(activity); mToken = activity.getIntent().getByteArrayExtra( ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN); mUserId = activity.getIntent().getIntExtra(Intent.EXTRA_USER_ID, UserHandle.USER_NULL); @@ -130,19 +141,14 @@ public class FingerprintEnrollSidecar extends InstrumentedFragment { } } - private void startEnrollment() { + protected void startEnrollment() { mHandler.removeCallbacks(mTimeoutRunnable); mEnrollmentSteps = -1; mEnrollmentCancel = new CancellationSignal(); - if (mUserId != UserHandle.USER_NULL) { - mFingerprintManager.setActiveUser(mUserId); - } - mFingerprintManager.enroll(mToken, mEnrollmentCancel, - 0 /* flags */, mUserId, mEnrollmentCallback); mEnrolling = true; } - boolean cancelEnrollment() { + public boolean cancelEnrollment() { mHandler.removeCallbacks(mTimeoutRunnable); if (mEnrolling) { mEnrollmentCancel.cancel(); @@ -153,6 +159,36 @@ public class FingerprintEnrollSidecar extends InstrumentedFragment { return false; } + protected void onEnrollmentProgress(int remaining) { + if (mEnrollmentSteps == -1) { + mEnrollmentSteps = remaining; + } + mEnrollmentRemaining = remaining; + mDone = remaining == 0; + if (mListener != null) { + mListener.onEnrollmentProgressChange(mEnrollmentSteps, remaining); + } else { + mQueuedEvents.add(new QueuedEnrollmentProgress(mEnrollmentSteps, remaining)); + } + } + + protected void onEnrollmentHelp(int helpMsgId, CharSequence helpString) { + if (mListener != null) { + mListener.onEnrollmentHelp(helpMsgId, helpString); + } else { + mQueuedEvents.add(new QueuedEnrollmentHelp(helpMsgId, helpString)); + } + } + + protected void onEnrollmentError(int errMsgId, CharSequence errString) { + if (mListener != null) { + mListener.onEnrollmentError(errMsgId, errString); + } else { + mQueuedEvents.add(new QueuedEnrollmentError(errMsgId, errString)); + } + mEnrolling = false; + } + public void setListener(Listener listener) { mListener = listener; if (mListener != null) { @@ -176,61 +212,6 @@ public class FingerprintEnrollSidecar extends InstrumentedFragment { return mDone; } - private FingerprintManager.EnrollmentCallback mEnrollmentCallback - = new FingerprintManager.EnrollmentCallback() { - - @Override - public void onEnrollmentProgress(int remaining) { - if (mEnrollmentSteps == -1) { - mEnrollmentSteps = remaining; - } - mEnrollmentRemaining = remaining; - mDone = remaining == 0; - if (mListener != null) { - mListener.onEnrollmentProgressChange(mEnrollmentSteps, remaining); - } else { - mQueuedEvents.add(new QueuedEnrollmentProgress(mEnrollmentSteps, remaining)); - } - } - - @Override - public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) { - if (mListener != null) { - mListener.onEnrollmentHelp(helpString); - } else { - mQueuedEvents.add(new QueuedEnrollmentHelp(helpMsgId, helpString)); - } - } - - @Override - public void onEnrollmentError(int errMsgId, CharSequence errString) { - if (mListener != null) { - mListener.onEnrollmentError(errMsgId, errString); - } else { - mQueuedEvents.add(new QueuedEnrollmentError(errMsgId, errString)); - } - mEnrolling = false; - } - }; - - private final Runnable mTimeoutRunnable = new Runnable() { - @Override - public void run() { - cancelEnrollment(); - } - }; - - @Override - public int getMetricsCategory() { - return MetricsEvent.FINGERPRINT_ENROLL_SIDECAR; - } - - public interface Listener { - void onEnrollmentHelp(CharSequence helpString); - void onEnrollmentError(int errMsgId, CharSequence errString); - void onEnrollmentProgressChange(int steps, int remaining); - } - public boolean isEnrolling() { return mEnrolling; } diff --git a/src/com/android/settings/biometrics/BiometricErrorDialog.java b/src/com/android/settings/biometrics/BiometricErrorDialog.java new file mode 100644 index 0000000000..d8cb123d85 --- /dev/null +++ b/src/com/android/settings/biometrics/BiometricErrorDialog.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2018 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.biometrics; + +import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_FINISHED; +import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_TIMEOUT; + +import android.app.Activity; +import android.app.Dialog; +import android.content.DialogInterface; +import android.hardware.biometrics.BiometricConstants; +import android.os.Bundle; + +import androidx.appcompat.app.AlertDialog; + +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; + +/** + * Abstract dialog, shown when an error occurs during biometric enrollment. + */ +public abstract class BiometricErrorDialog extends InstrumentedDialogFragment { + + public static final String KEY_ERROR_MSG = "error_msg"; + public static final String KEY_ERROR_ID = "error_id"; + + public abstract int getTitleResId(); + public abstract int getOkButtonTextResId(); + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + CharSequence errorString = getArguments().getCharSequence(KEY_ERROR_MSG); + final int errMsgId = getArguments().getInt(KEY_ERROR_ID); + + builder.setTitle(getTitleResId()) + .setMessage(errorString) + .setCancelable(false) + .setPositiveButton(getOkButtonTextResId(), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + boolean wasTimeout = + errMsgId == BiometricConstants.BIOMETRIC_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/FingerprintStatusPreferenceController.java b/src/com/android/settings/biometrics/BiometricStatusPreferenceController.java index 519f116a04..62dc1cf1fa 100644 --- a/src/com/android/settings/fingerprint/FingerprintStatusPreferenceController.java +++ b/src/com/android/settings/biometrics/BiometricStatusPreferenceController.java @@ -11,45 +11,66 @@ * 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. + * limitations under the License */ -package com.android.settings.fingerprint; +package com.android.settings.biometrics; + +import static com.android.settings.Utils.SETTINGS_PACKAGE_NAME; +import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_FROM_SETTINGS_SUMMARY; import android.content.Context; import android.content.Intent; -import android.hardware.fingerprint.Fingerprint; -import android.hardware.fingerprint.FingerprintManager; import android.os.UserHandle; import android.os.UserManager; + import androidx.preference.Preference; import com.android.internal.widget.LockPatternUtils; -import com.android.settings.R; import com.android.settings.Utils; import com.android.settings.core.BasePreferenceController; import com.android.settings.overlay.FeatureFactory; -import java.util.List; - -public class FingerprintStatusPreferenceController extends BasePreferenceController { +public abstract class BiometricStatusPreferenceController extends BasePreferenceController { - private static final String KEY_FINGERPRINT_SETTINGS = "fingerprint_settings"; - - protected final FingerprintManager mFingerprintManager; protected final UserManager mUm; protected final LockPatternUtils mLockPatternUtils; - protected final int mUserId = UserHandle.myUserId(); + private final int mUserId = UserHandle.myUserId(); protected final int mProfileChallengeUserId; - public FingerprintStatusPreferenceController(Context context) { - this(context, KEY_FINGERPRINT_SETTINGS); - } + /** + * @return true if the manager is not null and the hardware is detected. + */ + protected abstract boolean isDeviceSupported(); + + /** + * @return true if the user has enrolled biometrics of the subclassed type. + */ + protected abstract boolean hasEnrolledBiometrics(); + + /** + * @return the summary text if biometrics are enrolled. + */ + protected abstract String getSummaryTextEnrolled(); + + /** + * @return the summary text if no biometrics are enrolled. + */ + protected abstract String getSummaryTextNoneEnrolled(); + + /** + * @return the class name for the settings page. + */ + protected abstract String getSettingsClassName(); + + /** + * @return the class name for entry to enrollment. + */ + protected abstract String getEnrollClassName(); - public FingerprintStatusPreferenceController(Context context, String key) { + public BiometricStatusPreferenceController(Context context, String key) { super(context, key); - mFingerprintManager = Utils.getFingerprintManagerOrNull(context); mUm = (UserManager) context.getSystemService(Context.USER_SERVICE); mLockPatternUtils = FeatureFactory.getFactory(context) .getSecurityFeatureProvider() @@ -59,7 +80,7 @@ public class FingerprintStatusPreferenceController extends BasePreferenceControl @Override public int getAvailabilityStatus() { - if (mFingerprintManager == null || !mFingerprintManager.isHardwareDetected()) { + if (!isDeviceSupported()) { return UNSUPPORTED_ON_DEVICE; } if (isUserSupported()) { @@ -80,18 +101,13 @@ public class FingerprintStatusPreferenceController extends BasePreferenceControl preference.setVisible(true); } final int userId = getUserId(); - final List<Fingerprint> items = mFingerprintManager.getEnrolledFingerprints(userId); - final int fingerprintCount = items != null ? items.size() : 0; final String clazz; - if (fingerprintCount > 0) { - preference.setSummary(mContext.getResources().getQuantityString( - R.plurals.security_settings_fingerprint_preference_summary, - fingerprintCount, fingerprintCount)); - clazz = FingerprintSettings.class.getName(); + if (hasEnrolledBiometrics()) { + preference.setSummary(getSummaryTextEnrolled()); + clazz = getSettingsClassName(); } else { - preference.setSummary( - R.string.security_settings_fingerprint_preference_summary_none); - clazz = FingerprintEnrollIntroduction.class.getName(); + preference.setSummary(getSummaryTextNoneEnrolled()); + clazz = getEnrollClassName(); } preference.setOnPreferenceClickListener(target -> { final Context context = target.getContext(); @@ -101,8 +117,9 @@ public class FingerprintStatusPreferenceController extends BasePreferenceControl return false; } Intent intent = new Intent(); - intent.setClassName("com.android.settings", clazz); + intent.setClassName(SETTINGS_PACKAGE_NAME, clazz); intent.putExtra(Intent.EXTRA_USER_ID, userId); + intent.putExtra(EXTRA_FROM_SETTINGS_SUMMARY, true); context.startActivity(intent); return true; }); diff --git a/src/com/android/settings/biometrics/BiometricsEnrollEnrolling.java b/src/com/android/settings/biometrics/BiometricsEnrollEnrolling.java new file mode 100644 index 0000000000..a2460553e2 --- /dev/null +++ b/src/com/android/settings/biometrics/BiometricsEnrollEnrolling.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2018 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.biometrics; + +import android.content.Intent; +import android.os.UserHandle; +import android.view.View; + +import com.android.settings.R; +import com.android.settings.password.ChooseLockSettingsHelper; + +/** + * Abstract base activity which handles the actual enrolling for biometrics. + */ +public abstract class BiometricsEnrollEnrolling extends BiometricEnrollBase + implements BiometricEnrollSidecar.Listener { + + private static final String TAG_SIDECAR = "sidecar"; + + protected BiometricEnrollSidecar mSidecar; + + /** + * @return the intent for the finish activity + */ + protected abstract Intent getFinishIntent(); + + /** + * @return an instance of the biometric enroll sidecar + */ + protected abstract BiometricEnrollSidecar getSidecar(); + + /** + * @return true if enrollment should start automatically. + */ + protected abstract boolean shouldStartAutomatically(); + + @Override + protected void onStart() { + super.onStart(); + if (shouldStartAutomatically()) { + startEnrollment(); + } + } + + @Override + protected void onStop() { + super.onStop(); + if (mSidecar != null) { + mSidecar.setListener(null); + } + + if (!isChangingConfigurations()) { + if (mSidecar != null) { + mSidecar.cancelEnrollment(); + getSupportFragmentManager() + .beginTransaction().remove(mSidecar).commitAllowingStateLoss(); + } + finish(); + } + } + + @Override + public void onBackPressed() { + if (mSidecar != null) { + mSidecar.setListener(null); + mSidecar.cancelEnrollment(); + getSupportFragmentManager() + .beginTransaction().remove(mSidecar).commitAllowingStateLoss(); + mSidecar = null; + } + super.onBackPressed(); + } + + protected void onSkipButtonClick(View view) { + setResult(RESULT_SKIP); + finish(); + } + + public void startEnrollment() { + mSidecar = (BiometricEnrollSidecar) getSupportFragmentManager() + .findFragmentByTag(TAG_SIDECAR); + if (mSidecar == null) { + mSidecar = getSidecar(); + getSupportFragmentManager().beginTransaction().add(mSidecar, TAG_SIDECAR) + .commitAllowingStateLoss(); + } + mSidecar.setListener(this); + } + + protected void launchFinish(byte[] token) { + Intent intent = getFinishIntent(); + intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT + | Intent.FLAG_ACTIVITY_CLEAR_TOP + | Intent.FLAG_ACTIVITY_SINGLE_TOP); + intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token); + intent.putExtra(EXTRA_FROM_SETTINGS_SUMMARY, mFromSettingsSummary); + if (mUserId != UserHandle.USER_NULL) { + intent.putExtra(Intent.EXTRA_USER_ID, mUserId); + } + startActivity(intent); + overridePendingTransition(R.anim.sud_slide_next_in, R.anim.sud_slide_next_out); + finish(); + } + +} diff --git a/src/com/android/settings/fingerprint/OWNERS b/src/com/android/settings/biometrics/OWNERS index 937b3030fd..6332a46482 100644 --- a/src/com/android/settings/fingerprint/OWNERS +++ b/src/com/android/settings/biometrics/OWNERS @@ -1,5 +1,6 @@ # Default reviewers for this and subdirectories. jaggies@google.com +kchyn@google.com yukl@google.com # Emergency approvers in case the above are not available
\ No newline at end of file diff --git a/src/com/android/settings/biometrics/face/AnimationParticle.java b/src/com/android/settings/biometrics/face/AnimationParticle.java new file mode 100644 index 0000000000..a192e9f142 --- /dev/null +++ b/src/com/android/settings/biometrics/face/AnimationParticle.java @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2018 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.biometrics.face; + +import android.animation.ArgbEvaluator; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Rect; +import android.graphics.RectF; +import android.util.Log; + +import com.android.settings.R; + +import java.util.List; + +/** + * Class containing the state for an individual feedback dot / path. The dots are assigned colors + * based on their index. + */ +public class AnimationParticle { + + private static final String TAG = "AnimationParticle"; + + private static final int MIN_STROKE_WIDTH = 10; + private static final int MAX_STROKE_WIDTH = 20; // Be careful that this doesn't get clipped + private static final int FINAL_RING_STROKE_WIDTH = 15; + + private static final float ROTATION_SPEED_NORMAL = 0.8f; // radians per second, 1 = ~57 degrees + private static final float ROTATION_ACCELERATION_SPEED = 2.0f; + private static final float PULSE_SPEED_NORMAL = 1 * 2 * (float) Math.PI; // 1 cycle per second + private static final float RING_SWEEP_GROW_RATE_PRIMARY = 480; // degrees per second + private static final float RING_SWEEP_GROW_RATE_SECONDARY = 240; // degrees per second + private static final float RING_SIZE_FINALIZATION_TIME = 0.1f; // seconds + + private final Rect mBounds; // bounds for the canvas + private final int mBorderWidth; // amount of padding from the edges + private final ArgbEvaluator mEvaluator; + private final int mErrorColor; + private final int mIndex; + private final Listener mListener; + + private final Paint mPaint; + private final int mAssignedColor; + private final float mOffsetTimeSec; // stagger particle size to make a wave effect + + private int mLastAnimationState; + private int mAnimationState; + private float mCurrentSize = MIN_STROKE_WIDTH; + private float mCurrentAngle; // 0 is to the right, in radians + private float mRotationSpeed = ROTATION_SPEED_NORMAL; // speed of dot rotation + private float mSweepAngle = 0; // ring sweep, degrees per second + private float mSweepRate = RING_SWEEP_GROW_RATE_SECONDARY; // acceleration + private float mRingAdjustRate; // rate at which ring should grow/shrink to final size + private float mRingCompletionTime; // time at which ring should be completed + + public interface Listener { + void onRingCompleted(int index); + } + + public AnimationParticle(Context context, Listener listener, Rect bounds, int borderWidth, + int index, int totalParticles, List<Integer> colors) { + mBounds = bounds; + mBorderWidth = borderWidth; + mEvaluator = new ArgbEvaluator(); + mErrorColor = context.getResources() + .getColor(R.color.face_anim_particle_error, context.getTheme()); + mIndex = index; + mListener = listener; + + mCurrentAngle = (float) index / totalParticles * 2 * (float) Math.PI; + mOffsetTimeSec = (float) index / totalParticles + * (1 / ROTATION_SPEED_NORMAL) * 2 * (float) Math.PI; + + mPaint = new Paint(); + mAssignedColor = colors.get(index % colors.size()); + mPaint.setColor(mAssignedColor); + mPaint.setAntiAlias(true); + mPaint.setStrokeWidth(mCurrentSize); + mPaint.setStyle(Paint.Style.FILL); + mPaint.setStrokeCap(Paint.Cap.ROUND); + } + + public void updateState(int animationState) { + if (mAnimationState == animationState) { + Log.w(TAG, "Already in state " + animationState); + return; + } + if (animationState == ParticleCollection.STATE_COMPLETE) { + mPaint.setStyle(Paint.Style.STROKE); + } + mLastAnimationState = mAnimationState; + mAnimationState = animationState; + } + + // There are two types of particles, secondary and primary. Primary particles accelerate faster + // during the "completed" animation. Particles are secondary by default. + public void setAsPrimary() { + mSweepRate = RING_SWEEP_GROW_RATE_PRIMARY; + } + + public void update(long t, long dt) { + if (mAnimationState != ParticleCollection.STATE_COMPLETE) { + updateDot(t, dt); + } else { + updateRing(t, dt); + } + } + + private void updateDot(long t, long dt) { + final float dtSec = 0.001f * dt; + final float tSec = 0.001f * t; + + final float multiplier = mRotationSpeed / ROTATION_SPEED_NORMAL; + + // Calculate rotation speed / angle + if ((mAnimationState == ParticleCollection.STATE_STOPPED_COLORFUL + || mAnimationState == ParticleCollection.STATE_STOPPED_GRAY) + && mRotationSpeed > 0) { + // Linear slow down for now + mRotationSpeed = Math.max(mRotationSpeed - ROTATION_ACCELERATION_SPEED * dtSec, 0); + } else if (mAnimationState == ParticleCollection.STATE_STARTED + && mRotationSpeed < ROTATION_SPEED_NORMAL) { + // Linear speed up for now + mRotationSpeed += ROTATION_ACCELERATION_SPEED * dtSec; + } + + mCurrentAngle += dtSec * mRotationSpeed; + + // Calculate dot / ring size; linearly proportional with rotation speed + mCurrentSize = + (MAX_STROKE_WIDTH - MIN_STROKE_WIDTH) / 2 + * (float) Math.sin(tSec * PULSE_SPEED_NORMAL + mOffsetTimeSec) + + (MAX_STROKE_WIDTH + MIN_STROKE_WIDTH) / 2; + mCurrentSize = (mCurrentSize - MIN_STROKE_WIDTH) * multiplier + MIN_STROKE_WIDTH; + + // Calculate paint color; linearly proportional to rotation speed + int color = mAssignedColor; + if (mAnimationState == ParticleCollection.STATE_STOPPED_GRAY) { + color = (int) mEvaluator.evaluate(1 - multiplier, mAssignedColor, mErrorColor); + } else if (mLastAnimationState == ParticleCollection.STATE_STOPPED_GRAY) { + color = (int) mEvaluator.evaluate(1 - multiplier, mAssignedColor, mErrorColor); + } + + mPaint.setColor(color); + mPaint.setStrokeWidth(mCurrentSize); + } + + private void updateRing(long t, long dt) { + final float dtSec = 0.001f * dt; + final float tSec = 0.001f * t; + + // Store the start time, since we need to guarantee all rings reach final size at same time + // independent of current size. The magic 0 check is safe. + if (mRingAdjustRate == 0) { + mRingAdjustRate = + (FINAL_RING_STROKE_WIDTH - mCurrentSize) / RING_SIZE_FINALIZATION_TIME; + if (mRingCompletionTime == 0) { + mRingCompletionTime = tSec + RING_SIZE_FINALIZATION_TIME; + } + } + + // Accelerate to attack speed.. jk, back to normal speed + if (mRotationSpeed < ROTATION_SPEED_NORMAL) { + mRotationSpeed += ROTATION_ACCELERATION_SPEED * dtSec; + } + + // For arcs, this is the "start" + mCurrentAngle += dtSec * mRotationSpeed; + + // Update the sweep angle until it fills entire circle + if (mSweepAngle < 360) { + final float sweepGrowth = mSweepRate * dtSec; + mSweepAngle = mSweepAngle + sweepGrowth; + mSweepRate = mSweepRate + sweepGrowth; + } + if (mSweepAngle > 360) { + mSweepAngle = 360; + mListener.onRingCompleted(mIndex); + } + + // Animate stroke width to final size. + if (tSec < RING_SIZE_FINALIZATION_TIME) { + mCurrentSize = mCurrentSize + mRingAdjustRate * dtSec; + mPaint.setStrokeWidth(mCurrentSize); + } else { + // There should be small to no discontinuity in this if/else + mCurrentSize = FINAL_RING_STROKE_WIDTH; + mPaint.setStrokeWidth(mCurrentSize); + } + + } + + public void draw(Canvas canvas) { + if (mAnimationState != ParticleCollection.STATE_COMPLETE) { + drawDot(canvas); + } else { + drawRing(canvas); + } + } + + // Draws a dot at the current position on the circumference of the path. + private void drawDot(Canvas canvas) { + final float w = mBounds.right - mBounds.exactCenterX() - mBorderWidth; + final float h = mBounds.bottom - mBounds.exactCenterY() - mBorderWidth; + canvas.drawCircle( + mBounds.exactCenterX() + w * (float) Math.cos(mCurrentAngle), + mBounds.exactCenterY() + h * (float) Math.sin(mCurrentAngle), + mCurrentSize, + mPaint); + } + + private void drawRing(Canvas canvas) { + RectF arc = new RectF( + mBorderWidth, mBorderWidth, + mBounds.width() - mBorderWidth, mBounds.height() - mBorderWidth); + Path path = new Path(); + path.arcTo(arc, (float) Math.toDegrees(mCurrentAngle), mSweepAngle); + canvas.drawPath(path, mPaint); + } +} diff --git a/src/com/android/settings/biometrics/face/FaceEnrollAccessibilityToggle.java b/src/com/android/settings/biometrics/face/FaceEnrollAccessibilityToggle.java new file mode 100644 index 0000000000..dffc67de16 --- /dev/null +++ b/src/com/android/settings/biometrics/face/FaceEnrollAccessibilityToggle.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2018 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.biometrics.face; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.widget.CompoundButton; +import android.widget.LinearLayout; +import android.widget.Switch; +import android.widget.TextView; + +import com.android.settings.R; + +/** + * A layout that contains a start-justified title, and an end-justified switch. + */ +public class FaceEnrollAccessibilityToggle extends LinearLayout { + + private Switch mSwitch; + + public FaceEnrollAccessibilityToggle(Context context) { + this(context, null /* attrs */); + } + + public FaceEnrollAccessibilityToggle(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public FaceEnrollAccessibilityToggle(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + LayoutInflater.from(context).inflate(R.layout.face_enroll_accessibility_toggle, + this, true /* attachToRoot */); + + final TypedArray a = + context.obtainStyledAttributes(attrs, R.styleable.FaceEnrollAccessibilityToggle); + try { + final CharSequence title = + a.getText(R.styleable.FaceEnrollAccessibilityToggle_messageText); + final TextView titleTextView = findViewById(R.id.title); + titleTextView.setText(title); + } finally { + a.recycle(); + } + mSwitch = findViewById(R.id.toggle); + mSwitch.setChecked(false); + } + + public boolean isChecked() { + return mSwitch.isChecked(); + } + + public void setChecked(boolean checked) { + mSwitch.setChecked(checked); + } + + public void setListener(CompoundButton.OnCheckedChangeListener listener) { + mSwitch.setOnCheckedChangeListener(listener); + } + + public Switch getSwitch() { + return mSwitch; + } +} diff --git a/src/com/android/settings/biometrics/face/FaceEnrollAnimationDrawable.java b/src/com/android/settings/biometrics/face/FaceEnrollAnimationDrawable.java new file mode 100644 index 0000000000..5be7c5331d --- /dev/null +++ b/src/com/android/settings/biometrics/face/FaceEnrollAnimationDrawable.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2018 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.biometrics.face; + +import android.animation.TimeAnimator; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; + +import com.android.settings.biometrics.BiometricEnrollSidecar; + +/** + * A drawable containing the circle cutout as well as the animations. + */ +public class FaceEnrollAnimationDrawable extends Drawable + implements BiometricEnrollSidecar.Listener { + + // Tune this parameter so the UI looks nice - and so that we don't have to draw the animations + // outside our bounds. A fraction of each rotating dot should be overlapping the camera preview. + private static final int BORDER_BOUNDS = 20; + + private final Context mContext; + private final ParticleCollection.Listener mListener; + private Rect mBounds; + private final Paint mSquarePaint; + private final Paint mCircleCutoutPaint; + + private ParticleCollection mParticleCollection; + + private TimeAnimator mTimeAnimator; + + private final ParticleCollection.Listener mAnimationListener + = new ParticleCollection.Listener() { + @Override + public void onEnrolled() { + if (mTimeAnimator != null && mTimeAnimator.isStarted()) { + mTimeAnimator.end(); + mListener.onEnrolled(); + } + } + }; + + public FaceEnrollAnimationDrawable(Context context, ParticleCollection.Listener listener) { + mContext = context; + mListener = listener; + + mSquarePaint = new Paint(); + mSquarePaint.setColor(Color.WHITE); + mSquarePaint.setAntiAlias(true); + + mCircleCutoutPaint = new Paint(); + mCircleCutoutPaint.setColor(Color.TRANSPARENT); + mCircleCutoutPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); + mCircleCutoutPaint.setAntiAlias(true); + } + + @Override + public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) { + mParticleCollection.onEnrollmentHelp(helpMsgId, helpString); + } + + @Override + public void onEnrollmentError(int errMsgId, CharSequence errString) { + mParticleCollection.onEnrollmentError(errMsgId, errString); + } + + @Override + public void onEnrollmentProgressChange(int steps, int remaining) { + mParticleCollection.onEnrollmentProgressChange(steps, remaining); + } + + @Override + protected void onBoundsChange(Rect bounds) { + mBounds = bounds; + mParticleCollection = + new ParticleCollection(mContext, mAnimationListener, bounds, BORDER_BOUNDS); + + if (mTimeAnimator == null) { + mTimeAnimator = new TimeAnimator(); + mTimeAnimator.setTimeListener((animation, totalTimeMs, deltaTimeMs) -> { + mParticleCollection.update(totalTimeMs, deltaTimeMs); + FaceEnrollAnimationDrawable.this.invalidateSelf(); + }); + mTimeAnimator.start(); + } + } + + @Override + public void draw(Canvas canvas) { + if (mBounds == null) { + return; + } + canvas.save(); + + // Draw a rectangle covering the whole view + canvas.drawRect(0, 0, mBounds.width(), mBounds.height(), mSquarePaint); + + // Clear a circle in the middle for the camera preview + canvas.drawCircle(mBounds.exactCenterX(), mBounds.exactCenterY(), + mBounds.height() / 2 - BORDER_BOUNDS, mCircleCutoutPaint); + + // Draw the animation + mParticleCollection.draw(canvas); + + canvas.restore(); + } + + @Override + public void setAlpha(int alpha) { + + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } +} diff --git a/src/com/android/settings/biometrics/face/FaceEnrollEducation.java b/src/com/android/settings/biometrics/face/FaceEnrollEducation.java new file mode 100644 index 0000000000..956ba49d6b --- /dev/null +++ b/src/com/android/settings/biometrics/face/FaceEnrollEducation.java @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2018 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.biometrics.face; + +import static android.provider.Settings.Secure.FACE_UNLOCK_EDUCATION_INFO_DISPLAYED; +import static android.security.KeyStore.getApplicationContext; + +import android.app.settings.SettingsEnums; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.hardware.face.FaceManager; +import android.os.Bundle; +import android.os.Handler; +import android.os.UserHandle; +import android.provider.Settings; +import android.text.TextUtils; +import android.view.View; +import android.view.accessibility.AccessibilityManager; +import android.widget.Button; +import android.widget.CompoundButton; +import android.widget.TextView; + +import com.android.settings.R; +import com.android.settings.Utils; +import com.android.settings.biometrics.BiometricEnrollBase; +import com.android.settings.password.ChooseLockSettingsHelper; + +import com.google.android.setupcompat.template.FooterBarMixin; +import com.google.android.setupcompat.template.FooterButton; +import com.google.android.setupcompat.util.WizardManagerHelper; +import com.google.android.setupdesign.view.IllustrationVideoView; + +public class FaceEnrollEducation extends BiometricEnrollBase { + + private static final String TAG = "FaceEducation"; + private static final int ON = 1; + private static final int OFF = 0; + // 8 seconds. + private static final long FACE_ENROLL_EDUCATION_DELAY = 8000; + + private FaceManager mFaceManager; + private FaceEnrollAccessibilityToggle mSwitchDiversity; + + private IllustrationVideoView mIllustrationNormal; + private View mIllustrationAccessibility; + private Handler mHandler; + private Intent mResultIntent; + private TextView mDescriptionText; + + private CompoundButton.OnCheckedChangeListener mSwitchDiversityListener = + new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + int titleRes = isChecked ? + R.string.security_settings_face_enroll_education_title_accessibility + : R.string.security_settings_face_enroll_education_title; + getLayout().setHeaderText(titleRes); + setTitle(titleRes); + + if (isChecked) { + mIllustrationNormal.stop(); + mIllustrationNormal.setVisibility(View.INVISIBLE); + mIllustrationAccessibility.setVisibility(View.VISIBLE); + mDescriptionText.setVisibility(View.INVISIBLE); + } else { + mIllustrationNormal.setVisibility(View.VISIBLE); + mIllustrationNormal.start(); + mIllustrationAccessibility.setVisibility(View.INVISIBLE); + mDescriptionText.setVisibility(View.VISIBLE); + } + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.face_enroll_education); + getLayout().setHeaderText(R.string.security_settings_face_enroll_education_title); + setTitle(R.string.security_settings_face_enroll_education_title); + mHandler = new Handler(); + + mFaceManager = Utils.getFaceManagerOrNull(this); + + mIllustrationNormal = findViewById(R.id.illustration_normal); + mIllustrationAccessibility = findViewById(R.id.illustration_accessibility); + mDescriptionText = findViewById(R.id.sud_layout_description); + + mFooterBarMixin = getLayout().getMixin(FooterBarMixin.class); + mFooterBarMixin.setSecondaryButton( + new FooterButton.Builder(this) + .setText(R.string.security_settings_face_enroll_enrolling_skip) + .setListener(this::onSkipButtonClick) + .setButtonType(FooterButton.ButtonType.SKIP) + .setTheme(R.style.SudGlifButton_Secondary) + .build() + ); + + final FooterButton footerButton = new FooterButton.Builder(this) + .setText(R.string.security_settings_face_enroll_education_start) + .setListener(this::onNextButtonClick) + .setButtonType(FooterButton.ButtonType.NEXT) + .setTheme(R.style.SudGlifButton_Primary) + .build(); + + boolean accessibilityEnabled = false; + final AccessibilityManager accessibilityManager = getApplicationContext().getSystemService( + AccessibilityManager.class); + if (accessibilityManager != null) { + accessibilityEnabled = accessibilityManager.isEnabled(); + } + mFooterBarMixin.setPrimaryButton(footerButton); + final Context context = getApplicationContext(); + final boolean didDisplayEdu = Settings.Secure.getIntForUser(context.getContentResolver(), + FACE_UNLOCK_EDUCATION_INFO_DISPLAYED, OFF, mUserId) == ON; + if (!didDisplayEdu && !accessibilityEnabled) { + Settings.Secure.putIntForUser(context.getContentResolver(), + FACE_UNLOCK_EDUCATION_INFO_DISPLAYED, ON, mUserId); + footerButton.setEnabled(false); + mHandler.postDelayed(() -> { + footerButton.setEnabled(true); + }, FACE_ENROLL_EDUCATION_DELAY); + } + + final Button accessibilityButton = findViewById(R.id.accessibility_button); + accessibilityButton.setOnClickListener(view -> { + footerButton.setEnabled(true); + mSwitchDiversity.setChecked(true); + accessibilityButton.setVisibility(View.GONE); + mSwitchDiversity.setVisibility(View.VISIBLE); + }); + + mSwitchDiversity = findViewById(R.id.toggle_diversity); + mSwitchDiversity.setListener(mSwitchDiversityListener); + + if (accessibilityEnabled) { + accessibilityButton.callOnClick(); + } + } + + @Override + protected void onResume() { + super.onResume(); + mSwitchDiversityListener.onCheckedChanged(mSwitchDiversity.getSwitch(), + mSwitchDiversity.isChecked()); + } + + @Override + protected void onNextButtonClick(View view) { + final Intent intent = new Intent(); + if (mToken != null) { + intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, mToken); + } + if (mUserId != UserHandle.USER_NULL) { + intent.putExtra(Intent.EXTRA_USER_ID, mUserId); + } + final String flattenedString = getString(R.string.config_face_enroll); + if (!TextUtils.isEmpty(flattenedString)) { + ComponentName componentName = ComponentName.unflattenFromString(flattenedString); + intent.setComponent(componentName); + } else { + intent.setClass(this, FaceEnrollEnrolling.class); + } + intent.putExtra(EXTRA_KEY_REQUIRE_DIVERSITY, !mSwitchDiversity.isChecked()); + if (mResultIntent != null) { + intent.putExtras(mResultIntent); + } + WizardManagerHelper.copyWizardManagerExtras(getIntent(), intent); + startActivityForResult(intent, BIOMETRIC_FIND_SENSOR_REQUEST); + } + + protected void onSkipButtonClick(View view) { + setResult(RESULT_SKIP); + finish(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + mResultIntent = data; + if (requestCode == BIOMETRIC_FIND_SENSOR_REQUEST) { + // If the user finished or skipped enrollment, finish this activity + if (resultCode == RESULT_SKIP || resultCode == RESULT_FINISHED) { + setResult(resultCode); + finish(); + } + } + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.FACE_ENROLL_INTRO; + } +} diff --git a/src/com/android/settings/biometrics/face/FaceEnrollEnrolling.java b/src/com/android/settings/biometrics/face/FaceEnrollEnrolling.java new file mode 100644 index 0000000000..ea2fa5c438 --- /dev/null +++ b/src/com/android/settings/biometrics/face/FaceEnrollEnrolling.java @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2018 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.biometrics.face; + +import android.app.settings.SettingsEnums; +import android.content.Intent; +import android.hardware.face.FaceManager; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; +import android.widget.TextView; + +import com.android.settings.R; +import com.android.settings.biometrics.BiometricEnrollBase; +import com.android.settings.biometrics.BiometricEnrollSidecar; +import com.android.settings.biometrics.BiometricErrorDialog; +import com.android.settings.biometrics.BiometricsEnrollEnrolling; + +import com.google.android.setupcompat.template.FooterBarMixin; +import com.google.android.setupcompat.template.FooterButton; + +import java.util.ArrayList; + +public class FaceEnrollEnrolling extends BiometricsEnrollEnrolling { + + private static final String TAG = "FaceEnrollEnrolling"; + private static final boolean DEBUG = false; + private static final String TAG_FACE_PREVIEW = "tag_preview"; + + private TextView mErrorText; + private Interpolator mLinearOutSlowInInterpolator; + private FaceEnrollPreviewFragment mPreviewFragment; + + private ArrayList<Integer> mDisabledFeatures = new ArrayList<>(); + private ParticleCollection.Listener mListener = new ParticleCollection.Listener() { + @Override + public void onEnrolled() { + FaceEnrollEnrolling.this.launchFinish(mToken); + } + }; + + public static class FaceErrorDialog extends BiometricErrorDialog { + static FaceErrorDialog newInstance(CharSequence msg, int msgId) { + FaceErrorDialog dialog = new FaceErrorDialog(); + Bundle args = new Bundle(); + args.putCharSequence(KEY_ERROR_MSG, msg); + args.putInt(KEY_ERROR_ID, msgId); + dialog.setArguments(args); + return dialog; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.DIALOG_FACE_ERROR; + } + + @Override + public int getTitleResId() { + return R.string.security_settings_face_enroll_error_dialog_title; + } + + @Override + public int getOkButtonTextResId() { + return R.string.security_settings_face_enroll_dialog_ok; + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.face_enroll_enrolling); + setHeaderText(R.string.security_settings_face_enroll_repeat_title); + mErrorText = findViewById(R.id.error_text); + mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator( + this, android.R.interpolator.linear_out_slow_in); + + mFooterBarMixin = getLayout().getMixin(FooterBarMixin.class); + mFooterBarMixin.setSecondaryButton( + new FooterButton.Builder(this) + .setText(R.string.security_settings_face_enroll_enrolling_skip) + .setListener(this::onSkipButtonClick) + .setButtonType(FooterButton.ButtonType.SKIP) + .setTheme(R.style.SudGlifButton_Secondary) + .build() + ); + + if (!getIntent().getBooleanExtra(BiometricEnrollBase.EXTRA_KEY_REQUIRE_DIVERSITY, true)) { + mDisabledFeatures.add(FaceManager.FEATURE_REQUIRE_REQUIRE_DIVERSITY); + } + if (!getIntent().getBooleanExtra(BiometricEnrollBase.EXTRA_KEY_REQUIRE_VISION, true)) { + mDisabledFeatures.add(FaceManager.FEATURE_REQUIRE_ATTENTION); + } + + startEnrollment(); + } + + @Override + public void startEnrollment() { + super.startEnrollment(); + mPreviewFragment = (FaceEnrollPreviewFragment) getSupportFragmentManager() + .findFragmentByTag(TAG_FACE_PREVIEW); + if (mPreviewFragment == null) { + mPreviewFragment = new FaceEnrollPreviewFragment(); + getSupportFragmentManager().beginTransaction().add(mPreviewFragment, TAG_FACE_PREVIEW) + .commitAllowingStateLoss(); + } + mPreviewFragment.setListener(mListener); + } + + @Override + protected Intent getFinishIntent() { + return new Intent(this, FaceEnrollFinish.class); + } + + @Override + protected BiometricEnrollSidecar getSidecar() { + final int[] disabledFeatures = new int[mDisabledFeatures.size()]; + for (int i = 0; i < mDisabledFeatures.size(); i++) { + disabledFeatures[i] = mDisabledFeatures.get(i); + } + + return new FaceEnrollSidecar(disabledFeatures); + } + + @Override + protected boolean shouldStartAutomatically() { + return false; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.FACE_ENROLL_ENROLLING; + } + + @Override + public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) { + if (!TextUtils.isEmpty(helpString)) { + showError(helpString); + } + mPreviewFragment.onEnrollmentHelp(helpMsgId, helpString); + } + + @Override + public void onEnrollmentError(int errMsgId, CharSequence errString) { + int msgId; + switch (errMsgId) { + case FaceManager.FACE_ERROR_TIMEOUT: + msgId = R.string.security_settings_face_enroll_error_timeout_dialog_message; + break; + default: + msgId = R.string.security_settings_face_enroll_error_generic_dialog_message; + break; + } + mPreviewFragment.onEnrollmentError(errMsgId, errString); + showErrorDialog(getText(msgId), errMsgId); + } + + @Override + public void onEnrollmentProgressChange(int steps, int remaining) { + if (DEBUG) { + Log.v(TAG, "Steps: " + steps + " Remaining: " + remaining); + } + mPreviewFragment.onEnrollmentProgressChange(steps, remaining); + + // TODO: Update the actual animation + showError("Steps: " + steps + " Remaining: " + remaining); + + // TODO: Have this match any animations that UX comes up with + if (remaining == 0) { + launchFinish(mToken); + } + } + + private void showErrorDialog(CharSequence msg, int msgId) { + BiometricErrorDialog dialog = FaceErrorDialog.newInstance(msg, msgId); + dialog.show(getSupportFragmentManager(), FaceErrorDialog.class.getName()); + } + + private void showError(CharSequence error) { + mErrorText.setText(error); + if (mErrorText.getVisibility() == View.INVISIBLE) { + mErrorText.setVisibility(View.VISIBLE); + mErrorText.setTranslationY(getResources().getDimensionPixelSize( + R.dimen.fingerprint_error_text_appear_distance)); + mErrorText.setAlpha(0f); + mErrorText.animate() + .alpha(1f) + .translationY(0f) + .setDuration(200) + .setInterpolator(mLinearOutSlowInInterpolator) + .start(); + } else { + mErrorText.animate().cancel(); + mErrorText.setAlpha(1f); + mErrorText.setTranslationY(0f); + } + } +} diff --git a/src/com/android/settings/biometrics/face/FaceEnrollFinish.java b/src/com/android/settings/biometrics/face/FaceEnrollFinish.java new file mode 100644 index 0000000000..6e99cdb344 --- /dev/null +++ b/src/com/android/settings/biometrics/face/FaceEnrollFinish.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2018 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.biometrics.face; + +import android.app.settings.SettingsEnums; +import android.os.Bundle; +import android.view.View; + +import com.android.settings.R; +import com.android.settings.biometrics.BiometricEnrollBase; + +import com.google.android.setupcompat.template.FooterBarMixin; +import com.google.android.setupcompat.template.FooterButton; + +/** + * Activity which concludes face enrollment. + */ +public class FaceEnrollFinish extends BiometricEnrollBase { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.face_enroll_finish); + setHeaderText(R.string.security_settings_face_enroll_finish_title); + + mFooterBarMixin = getLayout().getMixin(FooterBarMixin.class); + mFooterBarMixin.setPrimaryButton( + new FooterButton.Builder(this) + .setText(R.string.security_settings_face_enroll_done) + .setListener(this::onNextButtonClick) + .setButtonType(FooterButton.ButtonType.NEXT) + .setTheme(R.style.SudGlifButton_Primary) + .build() + ); + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.FACE_ENROLL_FINISHED; + } + + @Override + public void onNextButtonClick(View view) { + setResult(RESULT_FINISHED); + finish(); + } +} diff --git a/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java b/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java new file mode 100644 index 0000000000..525c1a3ee4 --- /dev/null +++ b/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2018 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.biometrics.face; + +import android.app.admin.DevicePolicyManager; +import android.app.settings.SettingsEnums; +import android.content.Intent; +import android.hardware.face.FaceManager; +import android.os.Bundle; +import android.widget.TextView; + +import com.android.settings.R; +import com.android.settings.Utils; +import com.android.settings.biometrics.BiometricEnrollIntroduction; +import com.android.settings.password.ChooseLockSettingsHelper; +import com.android.settingslib.RestrictedLockUtilsInternal; + +import com.google.android.setupcompat.template.FooterBarMixin; +import com.google.android.setupcompat.template.FooterButton; +import com.google.android.setupcompat.util.WizardManagerHelper; +import com.google.android.setupdesign.span.LinkSpan; + +public class FaceEnrollIntroduction extends BiometricEnrollIntroduction { + + private static final String TAG = "FaceIntro"; + + private FaceManager mFaceManager; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mFaceManager = Utils.getFaceManagerOrNull(this); + + mFooterBarMixin = getLayout().getMixin(FooterBarMixin.class); + if (WizardManagerHelper.isAnySetupWizard(getIntent())) { + mFooterBarMixin.setSecondaryButton( + new FooterButton.Builder(this) + .setText(R.string.skip_label) + .setListener(this::onSkipButtonClick) + .setButtonType(FooterButton.ButtonType.SKIP) + .setTheme(R.style.SudGlifButton_Secondary) + .build() + ); + } else { + mFooterBarMixin.setSecondaryButton( + new FooterButton.Builder(this) + .setText(R.string.security_settings_face_enroll_introduction_cancel) + .setListener(this::onCancelButtonClick) + .setButtonType(FooterButton.ButtonType.CANCEL) + .setTheme(R.style.SudGlifButton_Secondary) + .build() + ); + } + + mFooterBarMixin.setPrimaryButton( + new FooterButton.Builder(this) + .setText(R.string.wizard_next) + .setListener(this::onNextButtonClick) + .setButtonType(FooterButton.ButtonType.NEXT) + .setTheme(R.style.SudGlifButton_Primary) + .build() + ); + } + + @Override + protected boolean isDisabledByAdmin() { + return RestrictedLockUtilsInternal.checkIfKeyguardFeaturesDisabled( + this, DevicePolicyManager.KEYGUARD_DISABLE_FACE, mUserId) != null; + } + + @Override + protected int getLayoutResource() { + return R.layout.face_enroll_introduction; + } + + @Override + protected int getHeaderResDisabledByAdmin() { + return R.string.security_settings_face_enroll_introduction_title_unlock_disabled; + } + + @Override + protected int getHeaderResDefault() { + return R.string.security_settings_face_enroll_introduction_title; + } + + @Override + protected int getDescriptionResDisabledByAdmin() { + return R.string.security_settings_face_enroll_introduction_message_unlock_disabled; + } + + @Override + protected FooterButton getCancelButton() { + if (mFooterBarMixin != null) { + return mFooterBarMixin.getSecondaryButton(); + } + return null; + } + + @Override + protected FooterButton getNextButton() { + if (mFooterBarMixin != null) { + return mFooterBarMixin.getPrimaryButton(); + } + return null; + } + + @Override + protected TextView getErrorTextView() { + return findViewById(R.id.error_text); + } + + @Override + protected int checkMaxEnrolled() { + if (mFaceManager != null) { + final int max = getResources().getInteger( + com.android.internal.R.integer.config_faceMaxTemplatesPerUser); + final int numEnrolledFaces = mFaceManager.getEnrolledFaces(mUserId).size(); + if (numEnrolledFaces >= max) { + return R.string.face_intro_error_max; + } + } else { + return R.string.face_intro_error_unknown; + } + return 0; + } + + @Override + protected long getChallenge() { + mFaceManager = Utils.getFaceManagerOrNull(this); + if (mFaceManager == null) { + return 0; + } + return mFaceManager.generateChallenge(); + } + + @Override + protected String getExtraKeyForBiometric() { + return ChooseLockSettingsHelper.EXTRA_KEY_FOR_FACE; + } + + @Override + protected Intent getEnrollingIntent() { + Intent intent = new Intent(this, FaceEnrollEducation.class); + WizardManagerHelper.copyWizardManagerExtras(getIntent(), intent); + return intent; + } + + @Override + protected int getConfirmLockTitleResId() { + return R.string.security_settings_face_preference_title; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.FACE_ENROLL_INTRO; + } + + @Override + public void onClick(LinkSpan span) { + // TODO(b/110906762) + } +} diff --git a/src/com/android/settings/biometrics/face/FaceEnrollPreviewFragment.java b/src/com/android/settings/biometrics/face/FaceEnrollPreviewFragment.java new file mode 100644 index 0000000000..880671274e --- /dev/null +++ b/src/com/android/settings/biometrics/face/FaceEnrollPreviewFragment.java @@ -0,0 +1,339 @@ +/* + * Copyright (C) 2018 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.biometrics.face; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.graphics.SurfaceTexture; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CameraManager; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.params.StreamConfigurationMap; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.util.Size; +import android.util.TypedValue; +import android.view.Surface; +import android.view.TextureView; +import android.view.View; +import android.widget.ImageView; + +import com.android.settings.R; +import com.android.settings.biometrics.BiometricEnrollSidecar; +import com.android.settings.core.InstrumentedPreferenceFragment; + +import java.util.Arrays; + +/** + * Fragment that contains the logic for showing and controlling the camera preview, circular + * overlay, as well as the enrollment animations. + */ +public class FaceEnrollPreviewFragment extends InstrumentedPreferenceFragment + implements BiometricEnrollSidecar.Listener { + + private static final String TAG = "FaceEnrollPreviewFragment"; + + private static final int MAX_PREVIEW_WIDTH = 1920; + private static final int MAX_PREVIEW_HEIGHT = 1080; + + private Handler mHandler = new Handler(Looper.getMainLooper()); + private CameraManager mCameraManager; + private String mCameraId; + private CameraDevice mCameraDevice; + private CaptureRequest.Builder mPreviewRequestBuilder; + private CameraCaptureSession mCaptureSession; + private CaptureRequest mPreviewRequest; + private Size mPreviewSize; + private ParticleCollection.Listener mListener; + + // View used to contain the circular cutout and enrollment animation drawable + private ImageView mCircleView; + + // Drawable containing the circular cutout and enrollment animations + private FaceEnrollAnimationDrawable mAnimationDrawable; + + // Texture used for showing the camera preview + private FaceSquareTextureView mTextureView; + + // Listener sent to the animation drawable + private final ParticleCollection.Listener mAnimationListener + = new ParticleCollection.Listener() { + @Override + public void onEnrolled() { + mListener.onEnrolled(); + } + }; + + private final TextureView.SurfaceTextureListener mSurfaceTextureListener = + new TextureView.SurfaceTextureListener() { + + @Override + public void onSurfaceTextureAvailable( + SurfaceTexture surfaceTexture, int width, int height) { + openCamera(width, height); + } + + @Override + public void onSurfaceTextureSizeChanged( + SurfaceTexture surfaceTexture, int width, int height) { + // Shouldn't be called, but do this for completeness. + configureTransform(width, height); + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { + return true; + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { + + } + }; + + private final CameraDevice.StateCallback mCameraStateCallback = + new CameraDevice.StateCallback() { + + @Override + public void onOpened(CameraDevice cameraDevice) { + mCameraDevice = cameraDevice; + try { + // Configure the size of default buffer + SurfaceTexture texture = mTextureView.getSurfaceTexture(); + texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight()); + + // This is the output Surface we need to start preview + Surface surface = new Surface(texture); + + // Set up a CaptureRequest.Builder with the output Surface + mPreviewRequestBuilder = + mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); + mPreviewRequestBuilder.addTarget(surface); + + // Create a CameraCaptureSession for camera preview + mCameraDevice.createCaptureSession(Arrays.asList(surface), + new CameraCaptureSession.StateCallback() { + + @Override + public void onConfigured(CameraCaptureSession cameraCaptureSession) { + // The camera is already closed + if (null == mCameraDevice) { + return; + } + // When the session is ready, we start displaying the preview. + mCaptureSession = cameraCaptureSession; + try { + // Auto focus should be continuous for camera preview. + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, + CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); + + // Finally, we start displaying the camera preview. + mPreviewRequest = mPreviewRequestBuilder.build(); + mCaptureSession.setRepeatingRequest(mPreviewRequest, + null /* listener */, mHandler); + } catch (CameraAccessException e) { + Log.e(TAG, "Unable to access camera", e); + } + } + + @Override + public void onConfigureFailed(CameraCaptureSession cameraCaptureSession) { + Log.e(TAG, "Unable to configure camera"); + } + }, null /* handler */); + } catch (CameraAccessException e) { + e.printStackTrace(); + } + } + + @Override + public void onDisconnected(CameraDevice cameraDevice) { + cameraDevice.close(); + mCameraDevice = null; + } + + @Override + public void onError(CameraDevice cameraDevice, int error) { + cameraDevice.close(); + mCameraDevice = null; + } + }; + + @Override + public int getMetricsCategory() { + return SettingsEnums.FACE_ENROLL_PREVIEW; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mTextureView = getActivity().findViewById(R.id.texture_view); + mCircleView = getActivity().findViewById(R.id.circle_view); + + // Must disable hardware acceleration for this view, otherwise transparency breaks + mCircleView.setLayerType(View.LAYER_TYPE_SOFTWARE, null); + + mAnimationDrawable = new FaceEnrollAnimationDrawable(getContext(), mAnimationListener); + mCircleView.setImageDrawable(mAnimationDrawable); + + mCameraManager = (CameraManager) getContext().getSystemService(Context.CAMERA_SERVICE); + } + + @Override + public void onResume() { + super.onResume(); + + // When the screen is turned off and turned back on, the SurfaceTexture is already + // available, and "onSurfaceTextureAvailable" will not be called. In that case, we can open + // a camera and start preview from here (otherwise, we wait until the surface is ready in + // the SurfaceTextureListener). + if (mTextureView.isAvailable()) { + openCamera(mTextureView.getWidth(), mTextureView.getHeight()); + } else { + mTextureView.setSurfaceTextureListener(mSurfaceTextureListener); + } + } + + @Override + public void onPause() { + super.onPause(); + closeCamera(); + } + + @Override + public void onEnrollmentError(int errMsgId, CharSequence errString) { + mAnimationDrawable.onEnrollmentError(errMsgId, errString); + } + + @Override + public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) { + mAnimationDrawable.onEnrollmentHelp(helpMsgId, helpString); + } + + @Override + public void onEnrollmentProgressChange(int steps, int remaining) { + mAnimationDrawable.onEnrollmentProgressChange(steps, remaining); + } + + public void setListener(ParticleCollection.Listener listener) { + mListener = listener; + } + + /** + * Sets up member variables related to camera. + */ + private void setUpCameraOutputs() { + try { + for (String cameraId : mCameraManager.getCameraIdList()) { + CameraCharacteristics characteristics = + mCameraManager.getCameraCharacteristics(cameraId); + + // Find front facing camera + Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING); + if (facing == null || facing != CameraCharacteristics.LENS_FACING_FRONT) { + continue; + } + mCameraId = cameraId; + + // Get the stream configurations + StreamConfigurationMap map = characteristics.get( + CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); + mPreviewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class)); + break; + } + } catch (CameraAccessException e) { + Log.e(TAG, "Unable to access camera", e); + } + } + + /** + * Opens the camera specified by mCameraId. + * @param width The width of the texture view + * @param height The height of the texture view + */ + private void openCamera(int width, int height) { + try { + setUpCameraOutputs(); + mCameraManager.openCamera(mCameraId, mCameraStateCallback, mHandler); + configureTransform(width, height); + } catch (CameraAccessException e) { + Log.e(TAG, "Unable to open camera", e); + } + } + + /** + * Chooses the optimal resolution for the camera to open. + */ + private Size chooseOptimalSize(Size[] choices) { + for (int i = 0; i < choices.length; i++) { + if (choices[i].getHeight() == MAX_PREVIEW_HEIGHT + && choices[i].getWidth() == MAX_PREVIEW_WIDTH) { + return choices[i]; + } + } + Log.w(TAG, "Unable to find a good resolution"); + return choices[0]; + } + + /** + * Configures the necessary {@link android.graphics.Matrix} transformation to `mTextureView`. + * This method should be called after the camera preview size is determined in + * setUpCameraOutputs and also the size of `mTextureView` is fixed. + * + * @param viewWidth The width of `mTextureView` + * @param viewHeight The height of `mTextureView` + */ + private void configureTransform(int viewWidth, int viewHeight) { + if (mTextureView == null) { + return; + } + + // Fix the aspect ratio + float scaleX = (float) viewWidth / mPreviewSize.getWidth(); + float scaleY = (float) viewHeight / mPreviewSize.getHeight(); + + // Now divide by smaller one so it fills up the original space. + float smaller = Math.min(scaleX, scaleY); + scaleX = scaleX / smaller; + scaleY = scaleY / smaller; + + // Apply the transformation/scale + mTextureView.setTranslationX(getResources().getDimension(R.dimen.face_preview_translate_x)); + mTextureView.setTranslationY(getResources().getDimension(R.dimen.face_preview_translate_y)); + + final TypedValue scale = new TypedValue(); + getResources().getValue(R.dimen.face_preview_scale, scale, true /* resolveRefs */); + mTextureView.setScaleX(scaleX * scale.getFloat()); + mTextureView.setScaleY(scaleY * scale.getFloat()); + } + + private void closeCamera() { + if (mCaptureSession != null) { + mCaptureSession.close(); + mCaptureSession = null; + } + if (mCameraDevice != null) { + mCameraDevice.close(); + mCameraDevice = null; + } + } +} diff --git a/src/com/android/settings/biometrics/face/FaceEnrollSidecar.java b/src/com/android/settings/biometrics/face/FaceEnrollSidecar.java new file mode 100644 index 0000000000..c75f300aa8 --- /dev/null +++ b/src/com/android/settings/biometrics/face/FaceEnrollSidecar.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2018 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.biometrics.face; + +import android.app.Activity; +import android.app.settings.SettingsEnums; +import android.hardware.face.FaceManager; +import android.os.UserHandle; + +import com.android.settings.Utils; +import com.android.settings.biometrics.BiometricEnrollSidecar; + +import java.util.Arrays; + +/** + * Sidecar fragment to handle the state around face enrollment + */ +public class FaceEnrollSidecar extends BiometricEnrollSidecar { + + private final int[] mDisabledFeatures; + + private FaceManager mFaceManager; + + public FaceEnrollSidecar(int[] disabledFeatures) { + mDisabledFeatures = Arrays.copyOf(disabledFeatures, disabledFeatures.length); + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + mFaceManager = Utils.getFaceManagerOrNull(activity); + } + + @Override + public void startEnrollment() { + super.startEnrollment(); + if (mUserId != UserHandle.USER_NULL) { + mFaceManager.setActiveUser(mUserId); + } + + mFaceManager.enroll(mToken, mEnrollmentCancel, + mEnrollmentCallback, mDisabledFeatures); + } + + private FaceManager.EnrollmentCallback mEnrollmentCallback + = new FaceManager.EnrollmentCallback() { + + @Override + public void onEnrollmentProgress(int remaining) { + FaceEnrollSidecar.super.onEnrollmentProgress(remaining); + } + + @Override + public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) { + FaceEnrollSidecar.super.onEnrollmentHelp(helpMsgId, helpString); + } + + @Override + public void onEnrollmentError(int errMsgId, CharSequence errString) { + FaceEnrollSidecar.super.onEnrollmentError(errMsgId, errString); + } + }; + + @Override + public int getMetricsCategory() { + return SettingsEnums.FACE_ENROLL_SIDECAR; + } +} diff --git a/src/com/android/settings/biometrics/face/FaceProfileStatusPreferenceController.java b/src/com/android/settings/biometrics/face/FaceProfileStatusPreferenceController.java new file mode 100644 index 0000000000..196992dbd9 --- /dev/null +++ b/src/com/android/settings/biometrics/face/FaceProfileStatusPreferenceController.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2019 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.biometrics.face; + +import android.content.Context; +import android.os.UserHandle; + +public class FaceProfileStatusPreferenceController extends FaceStatusPreferenceController { + + public static final String KEY_FACE_SETTINGS = "face_settings_profile"; + + public FaceProfileStatusPreferenceController(Context context) { + super(context, KEY_FACE_SETTINGS); + } + + @Override + protected boolean isUserSupported() { + return mProfileChallengeUserId != UserHandle.USER_NULL + && mLockPatternUtils.isSeparateProfileChallengeAllowed(mProfileChallengeUserId); + } + + @Override + protected int getUserId() { + return mProfileChallengeUserId; + } +} diff --git a/src/com/android/settings/biometrics/face/FaceSettings.java b/src/com/android/settings/biometrics/face/FaceSettings.java new file mode 100644 index 0000000000..48370d9fb7 --- /dev/null +++ b/src/com/android/settings/biometrics/face/FaceSettings.java @@ -0,0 +1,272 @@ +/* + * Copyright (C) 2018 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.biometrics.face; + +import static android.app.Activity.RESULT_OK; + +import static com.android.settings.biometrics.BiometricEnrollBase.CONFIRM_REQUEST; +import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_FINISHED; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.Intent; +import android.hardware.face.FaceManager; +import android.os.Bundle; +import android.os.UserHandle; +import android.os.UserManager; +import android.provider.SearchIndexableResource; +import android.util.Log; + +import androidx.preference.Preference; + +import com.android.settings.R; +import com.android.settings.SettingsActivity; +import com.android.settings.Utils; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.password.ChooseLockSettingsHelper; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.search.SearchIndexable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Settings screen for face authentication. + */ +@SearchIndexable +public class FaceSettings extends DashboardFragment { + + private static final String TAG = "FaceSettings"; + private static final String KEY_TOKEN = "hw_auth_token"; + + private UserManager mUserManager; + private FaceManager mFaceManager; + private int mUserId; + private byte[] mToken; + private FaceSettingsAttentionPreferenceController mAttentionController; + private FaceSettingsRemoveButtonPreferenceController mRemoveController; + private FaceSettingsEnrollButtonPreferenceController mEnrollController; + private List<AbstractPreferenceController> mControllers; + + private List<Preference> mTogglePreferences; + private Preference mRemoveButton; + private Preference mEnrollButton; + + private final FaceSettingsRemoveButtonPreferenceController.Listener mRemovalListener = () -> { + + // Disable the toggles until the user re-enrolls + for (Preference preference : mTogglePreferences) { + preference.setEnabled(false); + } + + // Hide the "remove" button and show the "set up face authentication" button. + mRemoveButton.setVisible(false); + mEnrollButton.setVisible(true); + }; + + public static boolean isAvailable(Context context) { + FaceManager manager = Utils.getFaceManagerOrNull(context); + return manager != null && manager.isHardwareDetected(); + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.FACE; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.security_settings_face; + } + + @Override + protected String getLogTag() { + return TAG; + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putByteArray(KEY_TOKEN, mToken); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mToken = getIntent().getByteArrayExtra(KEY_TOKEN); + mUserManager = getPrefContext().getSystemService(UserManager.class); + mFaceManager = getPrefContext().getSystemService(FaceManager.class); + mUserId = getActivity().getIntent().getIntExtra( + Intent.EXTRA_USER_ID, UserHandle.myUserId()); + + Preference keyguardPref = findPreference(FaceSettingsKeyguardPreferenceController.KEY); + Preference appPref = findPreference(FaceSettingsAppPreferenceController.KEY); + Preference attentionPref = findPreference(FaceSettingsAttentionPreferenceController.KEY); + Preference confirmPref = findPreference(FaceSettingsConfirmPreferenceController.KEY); + mTogglePreferences = new ArrayList<>( + Arrays.asList(keyguardPref, appPref, attentionPref, confirmPref)); + + mRemoveButton = findPreference(FaceSettingsRemoveButtonPreferenceController.KEY); + mEnrollButton = findPreference(FaceSettingsEnrollButtonPreferenceController.KEY); + + // There is no better way to do this :/ + for (AbstractPreferenceController controller : mControllers) { + if (controller instanceof FaceSettingsPreferenceController) { + ((FaceSettingsPreferenceController) controller).setUserId(mUserId); + } else if (controller instanceof FaceSettingsEnrollButtonPreferenceController) { + ((FaceSettingsEnrollButtonPreferenceController) controller).setUserId(mUserId); + } + } + mRemoveController.setUserId(mUserId); + + // Don't show keyguard controller for work profile settings. + if (mUserManager.isManagedProfile(mUserId)) { + removePreference(FaceSettingsKeyguardPreferenceController.KEY); + } + + if (savedInstanceState != null) { + mToken = savedInstanceState.getByteArray(KEY_TOKEN); + } + + if (mToken == null) { + final long challenge = mFaceManager.generateChallenge(); + ChooseLockSettingsHelper helper = new ChooseLockSettingsHelper(getActivity(), this); + if (!helper.launchConfirmationActivity(CONFIRM_REQUEST, + getString(R.string.security_settings_face_preference_title), + null, null, challenge, mUserId)) { + Log.e(TAG, "Password not set"); + finish(); + } + } + } + + @Override + public void onResume() { + super.onResume(); + if (mToken != null) { + mAttentionController.setToken(mToken); + mEnrollController.setToken(mToken); + } + + final boolean hasEnrolled = mFaceManager.hasEnrolledTemplates(mUserId); + mEnrollButton.setVisible(!hasEnrolled); + mRemoveButton.setVisible(hasEnrolled); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == CONFIRM_REQUEST) { + if (resultCode == RESULT_FINISHED || resultCode == RESULT_OK) { + mFaceManager.setActiveUser(mUserId); + // The pin/pattern/password was set. + if (data != null) { + mToken = data.getByteArrayExtra( + ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN); + if (mToken != null) { + mAttentionController.setToken(mToken); + mEnrollController.setToken(mToken); + } + } + } + } + + if (mToken == null) { + // Didn't get an authentication, finishing + getActivity().finish(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (getActivity().isFinishing()) { + final int result = mFaceManager.revokeChallenge(); + if (result < 0) { + Log.w(TAG, "revokeChallenge failed, result: " + result); + } + } + } + + @Override + protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { + if (!isAvailable(context)) { + return null; + } + mControllers = buildPreferenceControllers(context, getSettingsLifecycle()); + // There's no great way of doing this right now :/ + for (AbstractPreferenceController controller : mControllers) { + if (controller instanceof FaceSettingsAttentionPreferenceController) { + mAttentionController = (FaceSettingsAttentionPreferenceController) controller; + } else if (controller instanceof FaceSettingsRemoveButtonPreferenceController) { + mRemoveController = (FaceSettingsRemoveButtonPreferenceController) controller; + mRemoveController.setListener(mRemovalListener); + mRemoveController.setActivity((SettingsActivity) getActivity()); + } else if (controller instanceof FaceSettingsEnrollButtonPreferenceController) { + mEnrollController = (FaceSettingsEnrollButtonPreferenceController) controller; + mEnrollController.setActivity((SettingsActivity) getActivity()); + } + } + + return mControllers; + } + + private static List<AbstractPreferenceController> buildPreferenceControllers(Context context, + Lifecycle lifecycle) { + final List<AbstractPreferenceController> controllers = new ArrayList<>(); + controllers.add(new FaceSettingsVideoPreferenceController(context)); + controllers.add(new FaceSettingsKeyguardPreferenceController(context)); + controllers.add(new FaceSettingsAppPreferenceController(context)); + controllers.add(new FaceSettingsAttentionPreferenceController(context)); + controllers.add(new FaceSettingsRemoveButtonPreferenceController(context)); + controllers.add(new FaceSettingsFooterPreferenceController(context)); + controllers.add(new FaceSettingsConfirmPreferenceController(context)); + controllers.add(new FaceSettingsEnrollButtonPreferenceController(context)); + return controllers; + } + + public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex( + Context context, boolean enabled) { + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.security_settings_face; + return Arrays.asList(sir); + } + + @Override + public List<AbstractPreferenceController> createPreferenceControllers( + Context context) { + if (isAvailable(context)) { + return buildPreferenceControllers(context, null /* lifecycle */); + } else { + return null; + } + } + + @Override + protected boolean isPageSearchEnabled(Context context) { + return isAvailable(context); + } + }; + +} diff --git a/src/com/android/settings/biometrics/face/FaceSettingsAppPreferenceController.java b/src/com/android/settings/biometrics/face/FaceSettingsAppPreferenceController.java new file mode 100644 index 0000000000..70c00e54d2 --- /dev/null +++ b/src/com/android/settings/biometrics/face/FaceSettingsAppPreferenceController.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2018 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.biometrics.face; + +import static android.provider.Settings.Secure.FACE_UNLOCK_APP_ENABLED; + +import android.content.Context; +import android.hardware.face.FaceManager; +import android.provider.Settings; + +import com.android.settings.Utils; + +import androidx.preference.Preference; + +/** + * Preference controller for Face settings page controlling the ability to use + * Face authentication in apps (through BiometricPrompt). + */ +public class FaceSettingsAppPreferenceController extends FaceSettingsPreferenceController { + + static final String KEY = "security_settings_face_app"; + + private static final int ON = 1; + private static final int OFF = 0; + private static final int DEFAULT = ON; // face unlock is enabled for BiometricPrompt by default + + private FaceManager mFaceManager; + + public FaceSettingsAppPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + mFaceManager = Utils.getFaceManagerOrNull(context); + } + + public FaceSettingsAppPreferenceController(Context context) { + this(context, KEY); + } + + @Override + public boolean isChecked() { + if (!FaceSettings.isAvailable(mContext)) { + return false; + } + return Settings.Secure.getIntForUser( + mContext.getContentResolver(), FACE_UNLOCK_APP_ENABLED, DEFAULT, getUserId()) == ON; + } + + @Override + public boolean setChecked(boolean isChecked) { + return Settings.Secure.putIntForUser(mContext.getContentResolver(), FACE_UNLOCK_APP_ENABLED, + isChecked ? ON : OFF, getUserId()); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + if (!FaceSettings.isAvailable(mContext)) { + preference.setEnabled(false); + } else if (!mFaceManager.hasEnrolledTemplates(getUserId())) { + preference.setEnabled(false); + } else { + preference.setEnabled(true); + } + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } +} diff --git a/src/com/android/settings/biometrics/face/FaceSettingsAttentionPreferenceController.java b/src/com/android/settings/biometrics/face/FaceSettingsAttentionPreferenceController.java new file mode 100644 index 0000000000..ef90b1eda3 --- /dev/null +++ b/src/com/android/settings/biometrics/face/FaceSettingsAttentionPreferenceController.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2018 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.biometrics.face; + +import android.content.Context; +import android.hardware.face.FaceManager; +import android.hardware.face.FaceManager.GetFeatureCallback; +import android.hardware.face.FaceManager.SetFeatureCallback; +import android.provider.Settings; + +import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreference; + +import com.android.settings.Utils; +import com.android.settings.core.TogglePreferenceController; + +/** + * Preference controller that manages the ability to use face authentication with/without + * user attention. See {@link FaceManager#setRequireAttention(boolean, byte[])}. + */ +public class FaceSettingsAttentionPreferenceController extends FaceSettingsPreferenceController { + + public static final String KEY = "security_settings_face_require_attention"; + + private byte[] mToken; + private FaceManager mFaceManager; + private SwitchPreference mPreference; + + private final SetFeatureCallback mSetFeatureCallback = new SetFeatureCallback() { + @Override + public void onCompleted(boolean success, int feature) { + if (feature == FaceManager.FEATURE_REQUIRE_ATTENTION) { + mPreference.setEnabled(true); + if (!success) { + mPreference.setChecked(!mPreference.isChecked()); + } else { + Settings.Secure.putIntForUser(mContext.getContentResolver(), + Settings.Secure.FACE_UNLOCK_ATTENTION_REQUIRED, + mPreference.isChecked() ? 1 : 0, getUserId()); + } + } + } + }; + + private final GetFeatureCallback mGetFeatureCallback = new GetFeatureCallback() { + @Override + public void onCompleted(boolean success, int feature, boolean value) { + if (feature == FaceManager.FEATURE_REQUIRE_ATTENTION && success) { + if (!mFaceManager.hasEnrolledTemplates(getUserId())) { + mPreference.setEnabled(false); + } else { + mPreference.setEnabled(true); + mPreference.setChecked(value); + } + } + } + }; + + public FaceSettingsAttentionPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + mFaceManager = Utils.getFaceManagerOrNull(context); + } + + public FaceSettingsAttentionPreferenceController(Context context) { + this(context, KEY); + } + + public void setToken(byte[] token) { + mToken = token; + } + + /** + * Displays preference in this controller. + */ + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(KEY); + } + + @Override + public boolean isChecked() { + if (!FaceSettings.isAvailable(mContext)) { + return true; + } + // Set to disabled until we know the true value. + mPreference.setEnabled(false); + mFaceManager.getFeature(FaceManager.FEATURE_REQUIRE_ATTENTION, mGetFeatureCallback); + + // Ideally returns a cached value. + return true; + } + + @Override + public boolean setChecked(boolean isChecked) { + // Optimistically update state and set to disabled until we know it succeeded. + mPreference.setEnabled(false); + mPreference.setChecked(isChecked); + + mFaceManager.setFeature(FaceManager.FEATURE_REQUIRE_ATTENTION, isChecked, mToken, + mSetFeatureCallback); + return true; + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } +} diff --git a/src/com/android/settings/biometrics/face/FaceSettingsConfirmPreferenceController.java b/src/com/android/settings/biometrics/face/FaceSettingsConfirmPreferenceController.java new file mode 100644 index 0000000000..c65cd23342 --- /dev/null +++ b/src/com/android/settings/biometrics/face/FaceSettingsConfirmPreferenceController.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2019 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.biometrics.face; + +import static android.provider.Settings.Secure.FACE_UNLOCK_ALWAYS_REQUIRE_CONFIRMATION; + +import android.content.Context; +import android.hardware.face.FaceManager; +import android.provider.Settings; + +import androidx.preference.Preference; + +import com.android.settings.Utils; +import com.android.settings.core.TogglePreferenceController; + +/** + * Preference controller giving the user an option to always require confirmation. + */ +public class FaceSettingsConfirmPreferenceController extends FaceSettingsPreferenceController { + + static final String KEY = "security_settings_face_require_confirmation"; + + private static final int ON = 1; + private static final int OFF = 0; + private static final int DEFAULT = OFF; + + private FaceManager mFaceManager; + + public FaceSettingsConfirmPreferenceController(Context context) { + this(context, KEY); + } + + public FaceSettingsConfirmPreferenceController(Context context, + String preferenceKey) { + super(context, preferenceKey); + mFaceManager = Utils.getFaceManagerOrNull(context); + } + + @Override + public boolean isChecked() { + return Settings.Secure.getIntForUser(mContext.getContentResolver(), + FACE_UNLOCK_ALWAYS_REQUIRE_CONFIRMATION, DEFAULT, getUserId()) == ON; + } + + @Override + public boolean setChecked(boolean isChecked) { + return Settings.Secure.putIntForUser(mContext.getContentResolver(), + FACE_UNLOCK_ALWAYS_REQUIRE_CONFIRMATION, isChecked ? ON : OFF, getUserId()); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + if (!FaceSettings.isAvailable(mContext)) { + preference.setEnabled(false); + } else if (!mFaceManager.hasEnrolledTemplates(getUserId())) { + preference.setEnabled(false); + } else { + preference.setEnabled(true); + } + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } +} diff --git a/src/com/android/settings/biometrics/face/FaceSettingsEnrollButtonPreferenceController.java b/src/com/android/settings/biometrics/face/FaceSettingsEnrollButtonPreferenceController.java new file mode 100644 index 0000000000..ec7b1948fe --- /dev/null +++ b/src/com/android/settings/biometrics/face/FaceSettingsEnrollButtonPreferenceController.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2019 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.biometrics.face; + +import android.content.Context; +import android.content.Intent; +import android.view.View; +import android.widget.Button; + +import androidx.preference.Preference; + +import com.android.settings.R; +import com.android.settings.SettingsActivity; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.password.ChooseLockSettingsHelper; +import com.android.settingslib.widget.LayoutPreference; + +/** + * Preference controller that allows a user to enroll their face. + */ +public class FaceSettingsEnrollButtonPreferenceController extends BasePreferenceController + implements View.OnClickListener { + + private static final String TAG = "FaceSettings/Remove"; + static final String KEY = "security_settings_face_enroll_faces_container"; + + private int mUserId; + private byte[] mToken; + private SettingsActivity mActivity; + private Button mButton; + + public FaceSettingsEnrollButtonPreferenceController(Context context) { + this(context, KEY); + } + + public FaceSettingsEnrollButtonPreferenceController(Context context, + String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + + mButton = ((LayoutPreference) preference) + .findViewById(R.id.security_settings_face_settings_enroll_button); + mButton.setOnClickListener(this); + } + + @Override + public void onClick(View v) { + final Intent intent = new Intent(); + intent.setClassName("com.android.settings", FaceEnrollIntroduction.class.getName()); + intent.putExtra(Intent.EXTRA_USER_ID, mUserId); + intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, mToken); + mContext.startActivity(intent); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } + + public void setUserId(int userId) { + mUserId = userId; + } + + public void setToken(byte[] token) { + mToken = token; + } + + public void setActivity(SettingsActivity activity) { + mActivity = activity; + } +} diff --git a/src/com/android/settings/biometrics/face/FaceSettingsFooterPreferenceController.java b/src/com/android/settings/biometrics/face/FaceSettingsFooterPreferenceController.java new file mode 100644 index 0000000000..838dc0d980 --- /dev/null +++ b/src/com/android/settings/biometrics/face/FaceSettingsFooterPreferenceController.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2018 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.biometrics.face; + +import android.content.Context; +import android.content.Intent; + +import androidx.preference.Preference; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.utils.AnnotationSpan; +import com.android.settingslib.HelpUtils; +import com.android.settingslib.widget.FooterPreference; + +/** + * Footer for face settings showing the help text and help link. + */ +public class FaceSettingsFooterPreferenceController extends BasePreferenceController { + + private static final String ANNOTATION_URL = "url"; + + public FaceSettingsFooterPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + public FaceSettingsFooterPreferenceController(Context context) { + this(context, FooterPreference.KEY_FOOTER); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + + final Intent helpIntent = HelpUtils.getHelpIntent( + mContext, mContext.getString(R.string.help_url_face), getClass().getName()); + final AnnotationSpan.LinkInfo linkInfo = + new AnnotationSpan.LinkInfo(mContext, ANNOTATION_URL, helpIntent); + preference.setTitle(AnnotationSpan.linkify( + mContext.getText(R.string.security_settings_face_settings_footer), linkInfo)); + } +} diff --git a/src/com/android/settings/biometrics/face/FaceSettingsKeyguardPreferenceController.java b/src/com/android/settings/biometrics/face/FaceSettingsKeyguardPreferenceController.java new file mode 100644 index 0000000000..c64455af30 --- /dev/null +++ b/src/com/android/settings/biometrics/face/FaceSettingsKeyguardPreferenceController.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2018 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.biometrics.face; + +import static android.provider.Settings.Secure.FACE_UNLOCK_KEYGUARD_ENABLED; + +import android.app.admin.DevicePolicyManager; +import android.content.Context; +import android.hardware.face.FaceManager; +import android.os.UserHandle; +import android.provider.Settings; + +import androidx.preference.Preference; + +import com.android.settings.Utils; +import com.android.settings.core.TogglePreferenceController; + +/** + * Preference controller for Face settings page controlling the ability to unlock the phone + * with face. + */ +public class FaceSettingsKeyguardPreferenceController extends FaceSettingsPreferenceController { + + static final String KEY = "security_settings_face_keyguard"; + + private static final int ON = 1; + private static final int OFF = 0; + private static final int DEFAULT = ON; // face unlock is enabled on keyguard by default + + private FaceManager mFaceManager; + + public FaceSettingsKeyguardPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + mFaceManager = Utils.getFaceManagerOrNull(context); + } + + public FaceSettingsKeyguardPreferenceController(Context context) { + this(context, KEY); + } + + @Override + public boolean isChecked() { + if (!FaceSettings.isAvailable(mContext)) { + return false; + } else if (adminDisabled()) { + return false; + } + return Settings.Secure.getIntForUser(mContext.getContentResolver(), + FACE_UNLOCK_KEYGUARD_ENABLED, DEFAULT, getUserId()) == ON; + } + + @Override + public boolean setChecked(boolean isChecked) { + return Settings.Secure.putIntForUser(mContext.getContentResolver(), + FACE_UNLOCK_KEYGUARD_ENABLED, isChecked ? ON : OFF, getUserId()); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + if (!FaceSettings.isAvailable(mContext)) { + preference.setEnabled(false); + } else if (adminDisabled()) { + preference.setEnabled(false); + } else if (!mFaceManager.hasEnrolledTemplates(getUserId())) { + preference.setEnabled(false); + } else { + preference.setEnabled(true); + } + } + + private boolean adminDisabled() { + DevicePolicyManager dpm = + (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE); + return dpm != null && + (dpm.getKeyguardDisabledFeatures(null, UserHandle.myUserId()) + & DevicePolicyManager.KEYGUARD_DISABLE_FACE) + != 0; + } +} diff --git a/src/com/android/settings/biometrics/face/FaceSettingsPreferenceController.java b/src/com/android/settings/biometrics/face/FaceSettingsPreferenceController.java new file mode 100644 index 0000000000..b8ac118551 --- /dev/null +++ b/src/com/android/settings/biometrics/face/FaceSettingsPreferenceController.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2019 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.biometrics.face; + +import android.content.Context; + +import com.android.settings.core.TogglePreferenceController; + +/** + * Abstract base class for all face settings toggles. + */ +public abstract class FaceSettingsPreferenceController extends TogglePreferenceController { + + private int mUserId; + + public FaceSettingsPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + public void setUserId(int userId) { + mUserId = userId; + } + + protected int getUserId() { + return mUserId; + } +} diff --git a/src/com/android/settings/biometrics/face/FaceSettingsRemoveButtonPreferenceController.java b/src/com/android/settings/biometrics/face/FaceSettingsRemoveButtonPreferenceController.java new file mode 100644 index 0000000000..d532a7636f --- /dev/null +++ b/src/com/android/settings/biometrics/face/FaceSettingsRemoveButtonPreferenceController.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2018 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.biometrics.face; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.DialogInterface; +import android.hardware.face.Face; +import android.hardware.face.FaceManager; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.Toast; + +import androidx.preference.Preference; + +import com.android.settings.R; +import com.android.settings.SettingsActivity; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; +import com.android.settingslib.widget.LayoutPreference; + +import java.util.List; + +/** + * Controller for the remove button. This assumes that there is only a single face enrolled. The UI + * will likely change if multiple enrollments are allowed/supported. + */ +public class FaceSettingsRemoveButtonPreferenceController extends BasePreferenceController + implements View.OnClickListener { + + private static final String TAG = "FaceSettings/Remove"; + static final String KEY = "security_settings_face_delete_faces_container"; + + public static class ConfirmRemoveDialog extends InstrumentedDialogFragment { + + private DialogInterface.OnClickListener mOnClickListener; + + @Override + public int getMetricsCategory() { + return SettingsEnums.DIALOG_FACE_REMOVE; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + + builder.setTitle(R.string.security_settings_face_settings_remove_dialog_title) + .setMessage(R.string.security_settings_face_settings_remove_dialog_details) + .setPositiveButton(R.string.okay, mOnClickListener) + .setNegativeButton(R.string.cancel, mOnClickListener); + AlertDialog dialog = builder.create(); + dialog.setCanceledOnTouchOutside(false); + return dialog; + } + + public void setOnClickListener(DialogInterface.OnClickListener listener) { + mOnClickListener = listener; + } + } + + interface Listener { + void onRemoved(); + } + + private Button mButton; + private Listener mListener; + private SettingsActivity mActivity; + private int mUserId; + private boolean mRemoving; + + private final Context mContext; + private final FaceManager mFaceManager; + private final FaceManager.RemovalCallback mRemovalCallback = new FaceManager.RemovalCallback() { + @Override + public void onRemovalError(Face face, int errMsgId, CharSequence errString) { + Log.e(TAG, "Unable to remove face: " + face.getBiometricId() + + " error: " + errMsgId + " " + errString); + Toast.makeText(mContext, errString, Toast.LENGTH_SHORT).show(); + mRemoving = false; + } + + @Override + public void onRemovalSucceeded(Face face, int remaining) { + if (remaining == 0) { + final List<Face> faces = mFaceManager.getEnrolledFaces(mUserId); + if (!faces.isEmpty()) { + mButton.setEnabled(true); + } else { + mRemoving = false; + mListener.onRemoved(); + } + } else { + Log.v(TAG, "Remaining: " + remaining); + } + } + }; + + private final DialogInterface.OnClickListener mOnClickListener + = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (which == DialogInterface.BUTTON_POSITIVE) { + mButton.setEnabled(false); + final List<Face> faces = mFaceManager.getEnrolledFaces(mUserId); + if (faces.isEmpty()) { + Log.e(TAG, "No faces"); + return; + } + if (faces.size() > 1) { + Log.e(TAG, "Multiple enrollments: " + faces.size()); + } + + // Remove the first/only face + mFaceManager.remove(faces.get(0), mUserId, mRemovalCallback); + } else { + mButton.setEnabled(true); + mRemoving = false; + } + } + }; + + public FaceSettingsRemoveButtonPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + mContext = context; + mFaceManager = context.getSystemService(FaceManager.class); + } + + public FaceSettingsRemoveButtonPreferenceController(Context context) { + this(context, KEY); + } + + public void setUserId(int userId) { + mUserId = userId; + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + + mButton = ((LayoutPreference) preference) + .findViewById(R.id.security_settings_face_settings_remove_button); + mButton.setOnClickListener(this); + + if (!FaceSettings.isAvailable(mContext)) { + mButton.setEnabled(false); + } else { + mButton.setEnabled(!mRemoving); + } + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } + + @Override + public String getPreferenceKey() { + return KEY; + } + + @Override + public void onClick(View v) { + if (v == mButton) { + mRemoving = true; + ConfirmRemoveDialog dialog = new ConfirmRemoveDialog(); + dialog.setOnClickListener(mOnClickListener); + dialog.show(mActivity.getSupportFragmentManager(), ConfirmRemoveDialog.class.getName()); + } + } + + public void setListener(Listener listener) { + mListener = listener; + } + + public void setActivity(SettingsActivity activity) { + mActivity = activity; + } +} diff --git a/src/com/android/settings/biometrics/face/FaceSettingsVideoPreferenceController.java b/src/com/android/settings/biometrics/face/FaceSettingsVideoPreferenceController.java new file mode 100644 index 0000000000..4edbb67e5a --- /dev/null +++ b/src/com/android/settings/biometrics/face/FaceSettingsVideoPreferenceController.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2019 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.biometrics.face; + +import android.content.Context; + +import androidx.preference.PreferenceScreen; + +import com.android.settings.widget.VideoPreference; +import com.android.settings.widget.VideoPreferenceController; + +/** + * Preference controller for the video for face settings. + */ +public class FaceSettingsVideoPreferenceController extends VideoPreferenceController { + + private static final String KEY_VIDEO = "security_settings_face_video"; + + private VideoPreference mVideoPreference; + + public FaceSettingsVideoPreferenceController(Context context, + String preferenceKey) { + super(context, preferenceKey); + } + + public FaceSettingsVideoPreferenceController(Context context) { + this(context, KEY_VIDEO); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mVideoPreference = screen.findPreference(KEY_VIDEO); + mVideoPreference.onViewVisible(false /* paused */); + } +} diff --git a/src/com/android/settings/biometrics/face/FaceSquareFrameLayout.java b/src/com/android/settings/biometrics/face/FaceSquareFrameLayout.java new file mode 100644 index 0000000000..3aed5241f9 --- /dev/null +++ b/src/com/android/settings/biometrics/face/FaceSquareFrameLayout.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2018 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.biometrics.face; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.FrameLayout; + +/** + * Square layout that sets the height to be the same as width. + */ +public class FaceSquareFrameLayout extends FrameLayout { + + public FaceSquareFrameLayout(Context context) { + super(context); + } + + public FaceSquareFrameLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public FaceSquareFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public FaceSquareFrameLayout(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // Don't call super, manually set their size below + int size = MeasureSpec.getSize(widthMeasureSpec); + + // Set this frame layout to be a square + setMeasuredDimension(size, size); + + // Set the children to be the same size (square) as well + final int numChildren = getChildCount(); + for (int i = 0; i < numChildren; i++) { + int spec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY); + this.getChildAt(i).measure(spec, spec); + } + } +} diff --git a/src/com/android/settings/biometrics/face/FaceSquareTextureView.java b/src/com/android/settings/biometrics/face/FaceSquareTextureView.java new file mode 100644 index 0000000000..ebbbc27cb7 --- /dev/null +++ b/src/com/android/settings/biometrics/face/FaceSquareTextureView.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2018 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.biometrics.face; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.TextureView; + +/** + * A square {@link TextureView}. + */ +public class FaceSquareTextureView extends TextureView { + + public FaceSquareTextureView(Context context) { + this(context, null); + } + + public FaceSquareTextureView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public FaceSquareTextureView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = MeasureSpec.getSize(heightMeasureSpec); + + if (width < height) { + setMeasuredDimension(width, width); + } else { + setMeasuredDimension(height, height); + } + } +} diff --git a/src/com/android/settings/biometrics/face/FaceStatusPreferenceController.java b/src/com/android/settings/biometrics/face/FaceStatusPreferenceController.java new file mode 100644 index 0000000000..8450577209 --- /dev/null +++ b/src/com/android/settings/biometrics/face/FaceStatusPreferenceController.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2018 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.biometrics.face; + +import android.content.Context; +import android.hardware.face.FaceManager; + +import com.android.settings.R; +import com.android.settings.Settings; +import com.android.settings.Utils; +import com.android.settings.biometrics.BiometricStatusPreferenceController; + +public class FaceStatusPreferenceController extends BiometricStatusPreferenceController { + + private static final String KEY_FACE_SETTINGS = "face_settings"; + + protected final FaceManager mFaceManager; + + public FaceStatusPreferenceController(Context context) { + this(context, KEY_FACE_SETTINGS); + } + + public FaceStatusPreferenceController(Context context, String key) { + super(context, key); + mFaceManager = Utils.getFaceManagerOrNull(context); + } + + @Override + protected boolean isDeviceSupported() { + return mFaceManager != null && mFaceManager.isHardwareDetected(); + } + + @Override + protected boolean hasEnrolledBiometrics() { + return mFaceManager.hasEnrolledTemplates(getUserId()); + } + + @Override + protected String getSummaryTextEnrolled() { + return mContext.getResources() + .getString(R.string.security_settings_face_preference_summary); + } + + @Override + protected String getSummaryTextNoneEnrolled() { + return mContext.getResources() + .getString(R.string.security_settings_face_preference_summary_none); + } + + @Override + protected String getSettingsClassName() { + return Settings.FaceSettingsActivity.class.getName(); + } + + @Override + protected String getEnrollClassName() { + return FaceEnrollIntroduction.class.getName(); + } + +} diff --git a/src/com/android/settings/biometrics/face/ParticleCollection.java b/src/com/android/settings/biometrics/face/ParticleCollection.java new file mode 100644 index 0000000000..399beec3ab --- /dev/null +++ b/src/com/android/settings/biometrics/face/ParticleCollection.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2018 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.biometrics.face; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Rect; + +import com.android.settings.R; +import com.android.settings.biometrics.BiometricEnrollSidecar; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Class that's used to create, maintain, and update the state of each animation particle. Particles + * should have their colors assigned based on their index. Particles are split into primary and + * secondary types - primary types animate twice as fast during the completion effect. The particles + * are updated/drawn in a special order so that the overlap is correct during the final completion + * effect. + */ +public class ParticleCollection implements BiometricEnrollSidecar.Listener { + + private static final String TAG = "AnimationController"; + + private static final int NUM_PARTICLES = 12; + + public static final int STATE_STARTED = 1; // dots are rotating + public static final int STATE_STOPPED_COLORFUL = 2; // dots are not rotating but colorful + public static final int STATE_STOPPED_GRAY = 3; // dots are not rotating and also gray (error) + public static final int STATE_COMPLETE = 4; // face is enrolled + + private final List<AnimationParticle> mParticleList; + private final List<Integer> mPrimariesInProgress; // primary particles not done animating yet + private int mState; + private Listener mListener; + + public interface Listener { + void onEnrolled(); + } + + private final AnimationParticle.Listener mParticleListener = new AnimationParticle.Listener() { + @Override + public void onRingCompleted(int index) { + final boolean wasEmpty = mPrimariesInProgress.isEmpty(); + // We can stop the time animator once the three primary particles have finished + for (int i = 0; i < mPrimariesInProgress.size(); i++) { + if (mPrimariesInProgress.get(i).intValue() == index) { + mPrimariesInProgress.remove(i); + break; + } + } + if (mPrimariesInProgress.isEmpty() && !wasEmpty) { + mListener.onEnrolled(); + } + } + }; + + public ParticleCollection(Context context, Listener listener, Rect bounds, int borderWidth) { + mParticleList = new ArrayList<>(); + mListener = listener; + + final List<Integer> colors = new ArrayList<>(); + final Resources.Theme theme = context.getTheme(); + final Resources resources = context.getResources(); + colors.add(resources.getColor(R.color.face_anim_particle_color_1, theme)); + colors.add(resources.getColor(R.color.face_anim_particle_color_2, theme)); + colors.add(resources.getColor(R.color.face_anim_particle_color_3, theme)); + colors.add(resources.getColor(R.color.face_anim_particle_color_4, theme)); + + // Primary particles expand faster during the completion animation + mPrimariesInProgress = new ArrayList<>(Arrays.asList(0, 4, 8)); + + // Order in which to draw the particles. This is so the final "completion" animation has + // the correct behavior. + final int[] order = {3, 7, 11, 2, 6, 10, 1, 5, 9, 0, 4, 8}; + + for (int i = 0; i < NUM_PARTICLES; i++) { + AnimationParticle particle = new AnimationParticle(context, mParticleListener, bounds, + borderWidth, order[i], NUM_PARTICLES, colors); + if (mPrimariesInProgress.contains(order[i])) { + particle.setAsPrimary(); + } + mParticleList.add(particle); + } + + updateState(STATE_STARTED); + } + + public void update(long t, long dt) { + for (int i = 0; i < mParticleList.size(); i++) { + mParticleList.get(i).update(t, dt); + } + } + + public void draw(Canvas canvas) { + for (int i = 0; i < mParticleList.size(); i++) { + mParticleList.get(i).draw(canvas); + } + } + + private void updateState(int state) { + if (mState != state) { + for (int i = 0; i < mParticleList.size(); i++) { + mParticleList.get(i).updateState(state); + } + mState = state; + } + } + + @Override + public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) { + + } + + @Override + public void onEnrollmentError(int errMsgId, CharSequence errString) { + + } + + @Override + public void onEnrollmentProgressChange(int steps, int remaining) { + if (remaining == 0) { + updateState(STATE_COMPLETE); + } + } +} diff --git a/src/com/android/settings/fingerprint/FingerprintAuthenticateSidecar.java b/src/com/android/settings/biometrics/fingerprint/FingerprintAuthenticateSidecar.java index 1fa59a2fb6..f5aae124f3 100644 --- a/src/com/android/settings/fingerprint/FingerprintAuthenticateSidecar.java +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintAuthenticateSidecar.java @@ -14,13 +14,13 @@ * limitations under the License */ -package com.android.settings.fingerprint; +package com.android.settings.biometrics.fingerprint; +import android.app.settings.SettingsEnums; import android.hardware.fingerprint.FingerprintManager; import android.hardware.fingerprint.FingerprintManager.AuthenticationResult; import android.os.CancellationSignal; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.core.InstrumentedFragment; /** @@ -55,7 +55,7 @@ public class FingerprintAuthenticateSidecar extends InstrumentedFragment { @Override public int getMetricsCategory() { - return MetricsEvent.FINGERPRINT_AUTHENTICATE_SIDECAR; + return SettingsEnums.FINGERPRINT_AUTHENTICATE_SIDECAR; } private FingerprintManager.AuthenticationCallback mAuthenticationCallback = diff --git a/src/com/android/settings/fingerprint/FingerprintEnrollEnrolling.java b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrolling.java index f3c1486559..e92c967c8f 100644 --- a/src/com/android/settings/fingerprint/FingerprintEnrollEnrolling.java +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrolling.java @@ -14,15 +14,12 @@ * limitations under the License */ -package com.android.settings.fingerprint; +package com.android.settings.biometrics.fingerprint; 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.settings.SettingsEnums; import android.content.DialogInterface; import android.content.Intent; import android.graphics.drawable.Animatable2; @@ -32,7 +29,6 @@ import android.graphics.drawable.LayerDrawable; import android.hardware.fingerprint.FingerprintManager; import android.media.AudioAttributes; import android.os.Bundle; -import android.os.UserHandle; import android.os.VibrationEffect; import android.os.Vibrator; import android.text.TextUtils; @@ -40,20 +36,25 @@ import android.view.MotionEvent; import android.view.View; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; -import android.widget.Button; import android.widget.ProgressBar; import android.widget.TextView; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.appcompat.app.AlertDialog; + import com.android.settings.R; +import com.android.settings.biometrics.BiometricEnrollSidecar; +import com.android.settings.biometrics.BiometricErrorDialog; +import com.android.settings.biometrics.BiometricsEnrollEnrolling; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; -import com.android.settings.password.ChooseLockSettingsHelper; + +import com.google.android.setupcompat.template.FooterBarMixin; +import com.google.android.setupcompat.template.FooterButton; +import com.google.android.setupdesign.util.DescriptionStyler; /** * Activity which handles the actual enrolling for fingerprint. */ -public class FingerprintEnrollEnrolling extends FingerprintEnrollBase - implements FingerprintEnrollSidecar.Listener { +public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling { static final String TAG_SIDECAR = "sidecar"; @@ -94,26 +95,61 @@ public class FingerprintEnrollEnrolling extends FingerprintEnrollBase private Interpolator mLinearOutSlowInInterpolator; private Interpolator mFastOutLinearInInterpolator; private int mIconTouchCount; - private FingerprintEnrollSidecar mSidecar; private boolean mAnimationCancelled; private AnimatedVectorDrawable mIconAnimationDrawable; private AnimatedVectorDrawable mIconBackgroundBlinksDrawable; private boolean mRestoring; private Vibrator mVibrator; + public static class FingerprintErrorDialog extends BiometricErrorDialog { + static FingerprintErrorDialog newInstance(CharSequence msg, int msgId) { + FingerprintErrorDialog dialog = new FingerprintErrorDialog(); + Bundle args = new Bundle(); + args.putCharSequence(KEY_ERROR_MSG, msg); + args.putInt(KEY_ERROR_ID, msgId); + dialog.setArguments(args); + return dialog; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.DIALOG_FINGERPINT_ERROR; + } + + @Override + public int getTitleResId() { + return R.string.security_settings_fingerprint_enroll_error_dialog_title; + } + + @Override + public int getOkButtonTextResId() { + return R.string.security_settings_fingerprint_enroll_dialog_ok; + } + } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.fingerprint_enroll_enrolling); setHeaderText(R.string.security_settings_fingerprint_enroll_repeat_title); - mStartMessage = (TextView) findViewById(R.id.start_message); + mStartMessage = (TextView) findViewById(R.id.sud_layout_description); mRepeatMessage = (TextView) findViewById(R.id.repeat_message); mErrorText = (TextView) findViewById(R.id.error_text); mProgressBar = (ProgressBar) findViewById(R.id.fingerprint_progress_bar); mVibrator = getSystemService(Vibrator.class); - Button skipButton = findViewById(R.id.skip_button); - skipButton.setOnClickListener(this); + if (getLayout().shouldApplyPartnerHeavyThemeResource()) { + DescriptionStyler.applyPartnerCustomizationStyle(mRepeatMessage); + } + mFooterBarMixin = getLayout().getMixin(FooterBarMixin.class); + mFooterBarMixin.setSecondaryButton( + new FooterButton.Builder(this) + .setText(R.string.security_settings_fingerprint_enroll_enrolling_skip) + .setListener(this::onSkipButtonClick) + .setButtonType(FooterButton.ButtonType.SKIP) + .setTheme(R.style.SudGlifButton_Secondary) + .build() + ); final LayerDrawable fingerprintDrawable = (LayerDrawable) mProgressBar.getBackground(); mIconAnimationDrawable = (AnimatedVectorDrawable) @@ -149,14 +185,18 @@ public class FingerprintEnrollEnrolling extends FingerprintEnrollBase } @Override + protected BiometricEnrollSidecar getSidecar() { + return new FingerprintEnrollSidecar(); + } + + @Override + protected boolean shouldStartAutomatically() { + return true; + } + + @Override protected void onStart() { super.onStart(); - mSidecar = (FingerprintEnrollSidecar) getFragmentManager().findFragmentByTag(TAG_SIDECAR); - if (mSidecar == null) { - mSidecar = new FingerprintEnrollSidecar(); - getFragmentManager().beginTransaction().add(mSidecar, TAG_SIDECAR).commit(); - } - mSidecar.setListener(this); updateProgress(false /* animate */); updateDescription(); if (mRestoring) { @@ -183,40 +223,7 @@ public class FingerprintEnrollEnrolling extends FingerprintEnrollBase @Override protected void onStop() { super.onStop(); - if (mSidecar != null) { - mSidecar.setListener(null); - } stopIconAnimation(); - if (!isChangingConfigurations()) { - if (mSidecar != null) { - mSidecar.cancelEnrollment(); - getFragmentManager().beginTransaction().remove(mSidecar).commitAllowingStateLoss(); - } - finish(); - } - } - - @Override - public void onBackPressed() { - if (mSidecar != null) { - mSidecar.setListener(null); - mSidecar.cancelEnrollment(); - getFragmentManager().beginTransaction().remove(mSidecar).commitAllowingStateLoss(); - mSidecar = null; - } - super.onBackPressed(); - } - - @Override - public void onClick(View v) { - switch (v.getId()) { - case R.id.skip_button: - setResult(RESULT_SKIP); - finish(); - break; - default: - super.onClick(v); - } } private void animateProgress(int progress) { @@ -236,20 +243,6 @@ public class FingerprintEnrollEnrolling extends FingerprintEnrollBase mIconBackgroundBlinksDrawable.start(); } - private void launchFinish(byte[] token) { - Intent intent = getFinishIntent(); - intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT - | Intent.FLAG_ACTIVITY_CLEAR_TOP - | Intent.FLAG_ACTIVITY_SINGLE_TOP); - intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token); - if (mUserId != UserHandle.USER_NULL) { - intent.putExtra(Intent.EXTRA_USER_ID, mUserId); - } - startActivity(intent); - overridePendingTransition(R.anim.suw_slide_next_in, R.anim.suw_slide_next_out); - finish(); - } - protected Intent getFinishIntent() { return new Intent(this, FingerprintEnrollFinish.class); } @@ -264,9 +257,8 @@ public class FingerprintEnrollEnrolling extends FingerprintEnrollBase } } - @Override - public void onEnrollmentHelp(CharSequence helpString) { + public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) { if (!TextUtils.isEmpty(helpString)) { mErrorText.removeCallbacks(mTouchAgainRunnable); showError(helpString); @@ -324,13 +316,13 @@ public class FingerprintEnrollEnrolling extends FingerprintEnrollBase } private void showErrorDialog(CharSequence msg, int msgId) { - ErrorDialog dlg = ErrorDialog.newInstance(msg, msgId); - dlg.show(getFragmentManager(), ErrorDialog.class.getName()); + BiometricErrorDialog dlg = FingerprintErrorDialog.newInstance(msg, msgId); + dlg.show(getSupportFragmentManager(), FingerprintErrorDialog.class.getName()); } private void showIconTouchDialog() { mIconTouchCount = 0; - new IconTouchDialog().show(getFragmentManager(), null /* tag */); + new IconTouchDialog().show(getSupportFragmentManager(), null /* tag */); } private void showError(CharSequence error) { @@ -431,7 +423,7 @@ public class FingerprintEnrollEnrolling extends FingerprintEnrollBase @Override public int getMetricsCategory() { - return MetricsEvent.FINGERPRINT_ENROLLING; + return SettingsEnums.FINGERPRINT_ENROLLING; } public static class IconTouchDialog extends InstrumentedDialogFragment { @@ -453,57 +445,7 @@ public class FingerprintEnrollEnrolling extends FingerprintEnrollBase @Override public int getMetricsCategory() { - return MetricsEvent.DIALOG_FINGERPRINT_ICON_TOUCH; - } - } - - public static class ErrorDialog extends InstrumentedDialogFragment { - - /** - * 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; - } - - @Override - public int getMetricsCategory() { - return MetricsEvent.DIALOG_FINGERPINT_ERROR; + return SettingsEnums.DIALOG_FINGERPRINT_ICON_TOUCH; } } } diff --git a/src/com/android/settings/fingerprint/FingerprintEnrollFindSensor.java b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFindSensor.java index 95a534cdd4..733fb3f03b 100644 --- a/src/com/android/settings/fingerprint/FingerprintEnrollFindSensor.java +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFindSensor.java @@ -14,34 +14,33 @@ * limitations under the License */ -package com.android.settings.fingerprint; +package com.android.settings.biometrics.fingerprint; +import android.app.settings.SettingsEnums; import android.content.Intent; import android.hardware.fingerprint.FingerprintManager; import android.os.Bundle; -import android.os.UserHandle; -import androidx.annotation.Nullable; import android.view.View; -import android.widget.Button; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.annotation.Nullable; + import com.android.settings.R; import com.android.settings.Utils; -import com.android.settings.fingerprint.FingerprintEnrollSidecar.Listener; +import com.android.settings.biometrics.BiometricEnrollBase; +import com.android.settings.biometrics.BiometricEnrollSidecar.Listener; import com.android.settings.password.ChooseLockSettingsHelper; +import com.google.android.setupcompat.template.FooterBarMixin; +import com.google.android.setupcompat.template.FooterButton; + /** * Activity explaining the fingerprint sensor location for fingerprint enrollment. */ -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"; +public class FingerprintEnrollFindSensor extends BiometricEnrollBase { @Nullable private FingerprintFindSensorAnimation mAnimation; - private boolean mLaunchedConfirmLock; + private FingerprintEnrollSidecar mSidecar; private boolean mNextClicked; @@ -49,20 +48,21 @@ public class FingerprintEnrollFindSensor extends FingerprintEnrollBase { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(getContentView()); - Button skipButton = findViewById(R.id.skip_button); - skipButton.setOnClickListener(this); + mFooterBarMixin = getLayout().getMixin(FooterBarMixin.class); + mFooterBarMixin.setSecondaryButton( + new FooterButton.Builder(this) + .setText(R.string.skip_label) + .setListener(this::onSkipButtonClick) + .setButtonType(FooterButton.ButtonType.SKIP) + .setTheme(R.style.SudGlifButton_Secondary) + .build() + ); setHeaderText(R.string.security_settings_fingerprint_enroll_find_sensor_title); - if (savedInstanceState != null) { - mLaunchedConfirmLock = savedInstanceState.getBoolean(EXTRA_KEY_LAUNCHED_CONFIRM); - mToken = savedInstanceState.getByteArray( - ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN); - } - if (mToken == null && !mLaunchedConfirmLock) { - launchConfirmLock(); - } else if (mToken != null) { - startLookingForFingerprint(); // already confirmed, so start looking for fingerprint - } + + + startLookingForFingerprint(); // already confirmed, so start looking for fingerprint + View animationView = findViewById(R.id.fingerprint_sensor_location_animation); if (animationView instanceof FingerprintFindSensorAnimation) { mAnimation = (FingerprintFindSensorAnimation) animationView; @@ -84,12 +84,13 @@ public class FingerprintEnrollFindSensor extends FingerprintEnrollBase { } private void startLookingForFingerprint() { - mSidecar = (FingerprintEnrollSidecar) getFragmentManager().findFragmentByTag( + mSidecar = (FingerprintEnrollSidecar) getSupportFragmentManager().findFragmentByTag( FingerprintEnrollEnrolling.TAG_SIDECAR); if (mSidecar == null) { mSidecar = new FingerprintEnrollSidecar(); - getFragmentManager().beginTransaction() - .add(mSidecar, FingerprintEnrollEnrolling.TAG_SIDECAR).commit(); + getSupportFragmentManager().beginTransaction() + .add(mSidecar, FingerprintEnrollEnrolling.TAG_SIDECAR) + .commitAllowingStateLoss(); } mSidecar.setListener(new Listener() { @Override @@ -99,7 +100,7 @@ public class FingerprintEnrollFindSensor extends FingerprintEnrollBase { } @Override - public void onEnrollmentHelp(CharSequence helpString) { + public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) { } @Override @@ -128,25 +129,7 @@ public class FingerprintEnrollFindSensor extends FingerprintEnrollBase { } } - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.putBoolean(EXTRA_KEY_LAUNCHED_CONFIRM, mLaunchedConfirmLock); - outState.putByteArray(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, mToken); - } - - @Override - public void onClick(View v) { - switch (v.getId()) { - case R.id.skip_button: - onSkipButtonClick(); - break; - default: - super.onClick(v); - } - } - - protected void onSkipButtonClick() { + protected void onSkipButtonClick(View view) { setResult(RESULT_SKIP); finish(); } @@ -161,24 +144,25 @@ public class FingerprintEnrollFindSensor extends FingerprintEnrollBase { return; } } - getFragmentManager().beginTransaction().remove(mSidecar).commitAllowingStateLoss(); + getSupportFragmentManager().beginTransaction().remove(mSidecar). + commitAllowingStateLoss(); mSidecar = null; - startActivityForResult(getEnrollingIntent(), ENROLLING); + startActivityForResult(getFingerprintEnrollingIntent(), ENROLL_REQUEST); } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == CONFIRM_REQUEST) { - if (resultCode == RESULT_OK) { + if (resultCode == RESULT_OK && data != null) { mToken = data.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN); - overridePendingTransition(R.anim.suw_slide_next_in, R.anim.suw_slide_next_out); + overridePendingTransition(R.anim.sud_slide_next_in, R.anim.sud_slide_next_out); getIntent().putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, mToken); startLookingForFingerprint(); } else { finish(); } - } else if (requestCode == ENROLLING) { + } else if (requestCode == ENROLL_REQUEST) { if (resultCode == RESULT_FINISHED) { setResult(RESULT_FINISHED); finish(); @@ -205,30 +189,8 @@ public class FingerprintEnrollFindSensor extends FingerprintEnrollBase { } } - private void launchConfirmLock() { - long challenge = Utils.getFingerprintManagerOrNull(this).preEnroll(); - ChooseLockSettingsHelper helper = new ChooseLockSettingsHelper(this); - boolean launchedConfirmationActivity = false; - if (mUserId == UserHandle.USER_NULL) { - launchedConfirmationActivity = helper.launchConfirmationActivity(CONFIRM_REQUEST, - getString(R.string.security_settings_fingerprint_preference_title), - null, null, challenge); - } else { - launchedConfirmationActivity = helper.launchConfirmationActivity(CONFIRM_REQUEST, - getString(R.string.security_settings_fingerprint_preference_title), - null, null, challenge, mUserId); - } - if (!launchedConfirmationActivity) { - // 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 public int getMetricsCategory() { - return MetricsEvent.FINGERPRINT_FIND_SENSOR; + return SettingsEnums.FINGERPRINT_FIND_SENSOR; } } diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFinish.java b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFinish.java new file mode 100644 index 0000000000..5f4501580e --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFinish.java @@ -0,0 +1,177 @@ +/* + * 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.biometrics.fingerprint; + +import android.app.settings.SettingsEnums; +import android.content.ComponentName; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.hardware.fingerprint.FingerprintManager; +import android.os.Bundle; +import android.util.Log; +import android.view.View; + +import androidx.annotation.VisibleForTesting; + +import com.android.settings.R; +import com.android.settings.Utils; +import com.android.settings.biometrics.BiometricEnrollBase; +import com.android.settings.password.ChooseLockSettingsHelper; + +import com.google.android.setupcompat.template.FooterBarMixin; +import com.google.android.setupcompat.template.FooterButton; +import com.google.android.setupcompat.util.WizardManagerHelper; + +/** + * Activity which concludes fingerprint enrollment. + */ +public class FingerprintEnrollFinish extends BiometricEnrollBase { + + private static final String TAG = "FingerprintEnrollFinish"; + private static final String ACTION_FINGERPRINT_SETTINGS = + "android.settings.FINGERPRINT_SETTINGS"; + @VisibleForTesting + static final int REQUEST_ADD_ANOTHER = 1; + @VisibleForTesting + static final String FINGERPRINT_SUGGESTION_ACTIVITY = + "com.android.settings.SetupFingerprintSuggestionActivity"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.fingerprint_enroll_finish); + setHeaderText(R.string.security_settings_fingerprint_enroll_finish_title); + + mFooterBarMixin = getLayout().getMixin(FooterBarMixin.class); + mFooterBarMixin.setSecondaryButton( + new FooterButton.Builder(this) + .setText(R.string.fingerprint_enroll_button_add) + .setButtonType(FooterButton.ButtonType.SKIP) + .setTheme(R.style.SudGlifButton_Secondary) + .build() + ); + + mFooterBarMixin.setPrimaryButton( + new FooterButton.Builder(this) + .setText(R.string.security_settings_fingerprint_enroll_done) + .setListener(this::onNextButtonClick) + .setButtonType(FooterButton.ButtonType.NEXT) + .setTheme(R.style.SudGlifButton_Primary) + .build() + ); + } + + @Override + public void onBackPressed() { + super.onBackPressed(); + + updateFingerprintSuggestionEnableState(); + } + + @Override + protected void onResume() { + super.onResume(); + + FooterButton addButton = mFooterBarMixin.getSecondaryButton(); + + final FingerprintManager fpm = Utils.getFingerprintManagerOrNull(this); + boolean hideAddAnother = false; + if (fpm != null) { + int enrolled = fpm.getEnrolledFingerprints(mUserId).size(); + int max = getResources().getInteger( + com.android.internal.R.integer.config_fingerprintMaxTemplatesPerUser); + hideAddAnother = enrolled >= max; + } + if (hideAddAnother) { + // Don't show "Add" button if too many fingerprints already added + addButton.setVisibility(View.INVISIBLE); + } else { + addButton.setOnClickListener(this::onAddAnotherButtonClick); + } + } + + @Override + protected void onNextButtonClick(View view) { + updateFingerprintSuggestionEnableState(); + setResult(RESULT_FINISHED); + if (WizardManagerHelper.isAnySetupWizard(getIntent())) { + postEnroll(); + } else if (mFromSettingsSummary) { + // Only launch fingerprint settings if enrollment was triggered through settings summary + launchFingerprintSettings(); + } + finish(); + } + + private void updateFingerprintSuggestionEnableState() { + final FingerprintManager fpm = Utils.getFingerprintManagerOrNull(this); + if (fpm != null) { + int enrolled = fpm.getEnrolledFingerprints(mUserId).size(); + + // Only show "Add another fingerprint" if the user already enrolled one. + // "Add fingerprint" will be shown in the main flow if the user hasn't enrolled any + // fingerprints. If the user already added more than one fingerprint, they already know + // to add multiple fingerprints so we don't show the suggestion. + int flag = (enrolled == 1) ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED + : PackageManager.COMPONENT_ENABLED_STATE_DISABLED; + + ComponentName componentName = new ComponentName(getApplicationContext(), + FINGERPRINT_SUGGESTION_ACTIVITY); + getPackageManager().setComponentEnabledSetting( + componentName, flag, PackageManager.DONT_KILL_APP); + Log.d(TAG, FINGERPRINT_SUGGESTION_ACTIVITY + " enabled state = " + (enrolled == 1)); + } + } + + private void postEnroll() { + final FingerprintManager fpm = Utils.getFingerprintManagerOrNull(this); + if (fpm != null) { + int result = fpm.postEnroll(); + if (result < 0) { + Log.w(TAG, "postEnroll failed: result = " + result); + } + } + } + + private void launchFingerprintSettings() { + final Intent intent = new Intent(ACTION_FINGERPRINT_SETTINGS); + intent.setPackage(Utils.SETTINGS_PACKAGE_NAME); + intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, mToken); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); + startActivity(intent); + } + + private void onAddAnotherButtonClick(View view) { + startActivityForResult(getFingerprintEnrollingIntent(), REQUEST_ADD_ANOTHER); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + updateFingerprintSuggestionEnableState(); + if (requestCode == REQUEST_ADD_ANOTHER && resultCode != RESULT_CANCELED) { + setResult(resultCode, data); + finish(); + } else { + super.onActivityResult(requestCode, resultCode, data); + } + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.FINGERPRINT_ENROLL_FINISH; + } +} diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollIntroduction.java b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollIntroduction.java new file mode 100644 index 0000000000..d3618dbf37 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollIntroduction.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2018 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.biometrics.fingerprint; + +import android.app.admin.DevicePolicyManager; +import android.app.settings.SettingsEnums; +import android.content.ActivityNotFoundException; +import android.content.Intent; +import android.hardware.fingerprint.FingerprintManager; +import android.os.Bundle; +import android.util.Log; +import android.widget.TextView; + +import com.android.settings.R; +import com.android.settings.Utils; +import com.android.settings.biometrics.BiometricEnrollIntroduction; +import com.android.settings.password.ChooseLockSettingsHelper; +import com.android.settingslib.HelpUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; + +import com.google.android.setupcompat.template.FooterBarMixin; +import com.google.android.setupcompat.template.FooterButton; +import com.google.android.setupdesign.span.LinkSpan; + +public class FingerprintEnrollIntroduction extends BiometricEnrollIntroduction { + + private static final String TAG = "FingerprintIntro"; + + private FingerprintManager mFingerprintManager; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mFingerprintManager = Utils.getFingerprintManagerOrNull(this); + + mFooterBarMixin = getLayout().getMixin(FooterBarMixin.class); + mFooterBarMixin.setSecondaryButton( + new FooterButton.Builder(this) + .setText(R.string.security_settings_face_enroll_introduction_cancel) + .setListener(this::onCancelButtonClick) + .setButtonType(FooterButton.ButtonType.SKIP) + .setTheme(R.style.SudGlifButton_Secondary) + .build() + ); + + mFooterBarMixin.setPrimaryButton( + new FooterButton.Builder(this) + .setText(R.string.wizard_next) + .setListener(this::onNextButtonClick) + .setButtonType(FooterButton.ButtonType.NEXT) + .setTheme(R.style.SudGlifButton_Primary) + .build() + ); + } + + @Override + protected boolean isDisabledByAdmin() { + return RestrictedLockUtilsInternal.checkIfKeyguardFeaturesDisabled( + this, DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT, mUserId) != null; + } + + @Override + protected int getLayoutResource() { + return R.layout.fingerprint_enroll_introduction; + } + + @Override + protected int getHeaderResDisabledByAdmin() { + return R.string.security_settings_fingerprint_enroll_introduction_title_unlock_disabled; + } + + @Override + protected int getHeaderResDefault() { + return R.string.security_settings_fingerprint_enroll_introduction_title; + } + + @Override + protected int getDescriptionResDisabledByAdmin() { + return R.string.security_settings_fingerprint_enroll_introduction_message_unlock_disabled; + } + + @Override + protected FooterButton getCancelButton() { + if (mFooterBarMixin != null) { + return mFooterBarMixin.getSecondaryButton(); + } + return null; + } + + @Override + protected FooterButton getNextButton() { + if (mFooterBarMixin != null) { + return mFooterBarMixin.getPrimaryButton(); + } + return null; + } + + @Override + protected TextView getErrorTextView() { + return findViewById(R.id.error_text); + } + + @Override + protected int checkMaxEnrolled() { + if (mFingerprintManager != null) { + final int max = getResources().getInteger( + com.android.internal.R.integer.config_fingerprintMaxTemplatesPerUser); + final int numEnrolledFingerprints = + mFingerprintManager.getEnrolledFingerprints(mUserId).size(); + if (numEnrolledFingerprints >= max) { + return R.string.fingerprint_intro_error_max; + } + } else { + return R.string.fingerprint_intro_error_unknown; + } + return 0; + } + + @Override + protected long getChallenge() { + mFingerprintManager = Utils.getFingerprintManagerOrNull(this); + if (mFingerprintManager == null) { + return 0; + } + return mFingerprintManager.preEnroll(); + } + + @Override + protected String getExtraKeyForBiometric() { + return ChooseLockSettingsHelper.EXTRA_KEY_FOR_FINGERPRINT; + } + + @Override + protected Intent getEnrollingIntent() { + return new Intent(this, FingerprintEnrollFindSensor.class); + } + + @Override + protected int getConfirmLockTitleResId() { + return R.string.security_settings_fingerprint_preference_title; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.FINGERPRINT_ENROLL_INTRO; + } + + @Override + public void onClick(LinkSpan span) { + if ("url".equals(span.getId())) { + String url = getString(R.string.help_url_fingerprint); + Intent intent = HelpUtils.getHelpIntent(this, url, getClass().getName()); + if (intent == null) { + Log.w(TAG, "Null help intent."); + return; + } + try { + // This needs to be startActivityForResult even though we do not care about the + // actual result because the help app needs to know about who invoked it. + startActivityForResult(intent, LEARN_MORE_REQUEST); + } catch (ActivityNotFoundException e) { + Log.w(TAG, "Activity was not found for intent, " + e); + } + } + } +} diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollSidecar.java b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollSidecar.java new file mode 100644 index 0000000000..b72a802779 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollSidecar.java @@ -0,0 +1,73 @@ +/* + * 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.biometrics.fingerprint; + +import android.app.Activity; +import android.app.settings.SettingsEnums; +import android.hardware.fingerprint.FingerprintManager; +import android.os.UserHandle; + +import com.android.settings.Utils; +import com.android.settings.biometrics.BiometricEnrollSidecar; + +/** + * Sidecar fragment to handle the state around fingerprint enrollment. + */ +public class FingerprintEnrollSidecar extends BiometricEnrollSidecar { + + private FingerprintManager mFingerprintManager; + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + mFingerprintManager = Utils.getFingerprintManagerOrNull(activity); + } + + @Override + protected void startEnrollment() { + super.startEnrollment(); + if (mUserId != UserHandle.USER_NULL) { + mFingerprintManager.setActiveUser(mUserId); + } + mFingerprintManager.enroll(mToken, mEnrollmentCancel, + 0 /* flags */, mUserId, mEnrollmentCallback); + } + + private FingerprintManager.EnrollmentCallback mEnrollmentCallback + = new FingerprintManager.EnrollmentCallback() { + + @Override + public void onEnrollmentProgress(int remaining) { + FingerprintEnrollSidecar.super.onEnrollmentProgress(remaining); + } + + @Override + public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) { + FingerprintEnrollSidecar.super.onEnrollmentHelp(helpMsgId, helpString); + } + + @Override + public void onEnrollmentError(int errMsgId, CharSequence errString) { + FingerprintEnrollSidecar.super.onEnrollmentError(errMsgId, errString); + } + }; + + @Override + public int getMetricsCategory() { + return SettingsEnums.FINGERPRINT_ENROLL_SIDECAR; + } +} diff --git a/src/com/android/settings/fingerprint/FingerprintEnrollSuggestionActivity.java b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollSuggestionActivity.java index 4a4d08d353..fb2c668e33 100644 --- a/src/com/android/settings/fingerprint/FingerprintEnrollSuggestionActivity.java +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollSuggestionActivity.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.settings.fingerprint; +package com.android.settings.biometrics.fingerprint; import android.content.Context; diff --git a/src/com/android/settings/fingerprint/FingerprintFindSensorAnimation.java b/src/com/android/settings/biometrics/fingerprint/FingerprintFindSensorAnimation.java index 07ab99d17c..8ce507e834 100644 --- a/src/com/android/settings/fingerprint/FingerprintFindSensorAnimation.java +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintFindSensorAnimation.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.settings.fingerprint; +package com.android.settings.biometrics.fingerprint; /** * An abstraction for a view that contains an animation that shows the user diff --git a/src/com/android/settings/fingerprint/FingerprintLocationAnimationVideoView.java b/src/com/android/settings/biometrics/fingerprint/FingerprintLocationAnimationVideoView.java index 226d7185fb..1d8e7794dd 100644 --- a/src/com/android/settings/fingerprint/FingerprintLocationAnimationVideoView.java +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintLocationAnimationVideoView.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.settings.fingerprint; +package com.android.settings.biometrics.fingerprint; import android.content.ContentResolver; import android.content.Context; @@ -24,12 +24,13 @@ import android.media.MediaPlayer; import android.media.MediaPlayer.OnInfoListener; import android.media.MediaPlayer.OnPreparedListener; import android.net.Uri; -import androidx.annotation.VisibleForTesting; import android.util.AttributeSet; import android.view.Surface; import android.view.TextureView; import android.view.View; +import androidx.annotation.VisibleForTesting; + import com.android.settings.R; /** diff --git a/src/com/android/settings/fingerprint/FingerprintLocationAnimationView.java b/src/com/android/settings/biometrics/fingerprint/FingerprintLocationAnimationView.java index a26883a04b..99d4ff902f 100644 --- a/src/com/android/settings/fingerprint/FingerprintLocationAnimationView.java +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintLocationAnimationView.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.settings.fingerprint; +package com.android.settings.biometrics.fingerprint; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -62,7 +62,7 @@ public class FingerprintLocationAnimationView extends View implements R.fraction.fingerprint_sensor_location_fraction_x, 1, 1); mFractionCenterY = getResources().getFraction( R.fraction.fingerprint_sensor_location_fraction_y, 1, 1); - @ColorInt int colorAccent = Utils.getColorAccent(context); + @ColorInt int colorAccent = Utils.getColorAccentDefaultColor(context); mDotPaint.setAntiAlias(true); mPulsePaint.setAntiAlias(true); mDotPaint.setColor(colorAccent); diff --git a/src/com/android/settings/fingerprint/FingerprintProfileStatusPreferenceController.java b/src/com/android/settings/biometrics/fingerprint/FingerprintProfileStatusPreferenceController.java index 68d2ade4db..23873f9cb2 100644 --- a/src/com/android/settings/fingerprint/FingerprintProfileStatusPreferenceController.java +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintProfileStatusPreferenceController.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.settings.fingerprint; +package com.android.settings.biometrics.fingerprint; import android.content.Context; import android.os.UserHandle; diff --git a/src/com/android/settings/fingerprint/FingerprintRemoveSidecar.java b/src/com/android/settings/biometrics/fingerprint/FingerprintRemoveSidecar.java index 7caca3fff5..19d0518b9d 100644 --- a/src/com/android/settings/fingerprint/FingerprintRemoveSidecar.java +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintRemoveSidecar.java @@ -14,20 +14,21 @@ * limitations under the License */ -package com.android.settings.fingerprint; +package com.android.settings.biometrics.fingerprint; import android.annotation.Nullable; -import android.content.Context; +import android.app.settings.SettingsEnums; import android.hardware.fingerprint.Fingerprint; import android.hardware.fingerprint.FingerprintManager; import android.os.Bundle; -import com.android.settings.core.InstrumentedFragment; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import android.os.UserHandle; -import java.util.Queue; -import java.util.LinkedList; import android.util.Log; +import com.android.settings.core.InstrumentedFragment; + +import java.util.LinkedList; +import java.util.Queue; + /** * Sidecar fragment to handle the state around fingerprint removal. */ @@ -120,7 +121,7 @@ public class FingerprintRemoveSidecar extends InstrumentedFragment { } final boolean isRemovingFingerprint(int fid) { - return inProgress() && mFingerprintRemoving.getFingerId() == fid; + return inProgress() && mFingerprintRemoving.getBiometricId() == fid; } final boolean inProgress() { @@ -129,7 +130,7 @@ public class FingerprintRemoveSidecar extends InstrumentedFragment { @Override public int getMetricsCategory() { - return MetricsEvent.FINGERPRINT_REMOVE_SIDECAR; + return SettingsEnums.FINGERPRINT_REMOVE_SIDECAR; } } diff --git a/src/com/android/settings/fingerprint/FingerprintSettings.java b/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java index ed691895e8..ad0ae6fd96 100644 --- a/src/com/android/settings/fingerprint/FingerprintSettings.java +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java @@ -14,13 +14,15 @@ * limitations under the License. */ -package com.android.settings.fingerprint; +package com.android.settings.biometrics.fingerprint; +import static com.android.settings.Utils.SETTINGS_PACKAGE_NAME; + import android.app.Activity; -import android.app.AlertDialog; import android.app.Dialog; import android.app.admin.DevicePolicyManager; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -31,31 +33,33 @@ import android.os.Bundle; import android.os.Handler; import android.os.UserHandle; import android.os.UserManager; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; +import android.widget.Toast; + import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; import androidx.preference.Preference; import androidx.preference.Preference.OnPreferenceChangeListener; import androidx.preference.PreferenceGroup; import androidx.preference.PreferenceScreen; import androidx.preference.PreferenceViewHolder; -import android.text.TextUtils; -import android.util.Log; -import android.view.View; -import android.view.WindowManager; -import android.widget.EditText; -import android.widget.Toast; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; import com.android.settings.SubSettings; import com.android.settings.Utils; +import com.android.settings.biometrics.BiometricEnrollBase; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; import com.android.settings.password.ChooseLockGeneric; import com.android.settings.password.ChooseLockSettingsHelper; import com.android.settings.utils.AnnotationSpan; +import com.android.settings.widget.ImeAwareEditText; import com.android.settingslib.HelpUtils; import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; +import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.TwoTargetPreference; import com.android.settingslib.widget.FooterPreference; @@ -69,35 +73,15 @@ public class FingerprintSettings extends SubSettings { private static final String TAG = "FingerprintSettings"; - /** - * 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; - - /** - * 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 public static final String ANNOTATION_URL = "url"; public static final String ANNOTATION_ADMIN_DETAILS = "admin_details"; + private static final int RESULT_FINISHED = BiometricEnrollBase.RESULT_FINISHED; + private static final int RESULT_SKIP = BiometricEnrollBase.RESULT_SKIP; + private static final int RESULT_TIMEOUT = BiometricEnrollBase.RESULT_TIMEOUT; + @Override public Intent getIntent() { Intent modIntent = new Intent(super.getIntent()); @@ -140,7 +124,7 @@ public class FingerprintSettings extends SubSettings { private static final int ADD_FINGERPRINT_REQUEST = 10; - protected static final boolean DEBUG = true; + protected static final boolean DEBUG = false; private FingerprintManager mFingerprintManager; private boolean mInFingerprintLockout; @@ -160,7 +144,7 @@ public class FingerprintSettings extends SubSettings { @Override public void onAuthenticationSucceeded( FingerprintManager.AuthenticationResult result) { - int fingerId = result.getFingerprint().getFingerId(); + int fingerId = result.getFingerprint().getBiometricId(); mHandler.obtainMessage(MSG_FINGER_AUTH_SUCCESS, fingerId, 0).sendToTarget(); } @@ -186,7 +170,7 @@ public class FingerprintSettings extends SubSettings { new FingerprintRemoveSidecar.Listener() { public void onRemovalSucceeded(Fingerprint fingerprint) { mHandler.obtainMessage(MSG_REFRESH_FINGERPRINT_TEMPLATES, - fingerprint.getFingerId(), 0).sendToTarget(); + fingerprint.getBiometricId(), 0).sendToTarget(); updateDialog(); } @@ -282,7 +266,7 @@ public class FingerprintSettings extends SubSettings { @Override public int getMetricsCategory() { - return MetricsEvent.FINGERPRINT; + return SettingsEnums.FINGERPRINT; } @Override @@ -292,6 +276,9 @@ public class FingerprintSettings extends SubSettings { Activity activity = getActivity(); mFingerprintManager = Utils.getFingerprintManagerOrNull(activity); + mToken = getIntent().getByteArrayExtra( + ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN); + mAuthenticateSidecar = (FingerprintAuthenticateSidecar) getFragmentManager().findFragmentByTag(TAG_AUTHENTICATE_SIDECAR); if (mAuthenticateSidecar == null) { @@ -337,7 +324,7 @@ public class FingerprintSettings extends SubSettings { } final FooterPreference pref = mFooterPreferenceMixin.createFooterPreference(); - final EnforcedAdmin admin = RestrictedLockUtils.checkIfKeyguardFeaturesDisabled( + final EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfKeyguardFeaturesDisabled( activity, DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT, mUserId); final AnnotationSpan.LinkInfo adminLinkInfo = new AnnotationSpan.LinkInfo( ANNOTATION_ADMIN_DETAILS, (view) -> { @@ -392,16 +379,16 @@ public class FingerprintSettings extends SubSettings { final Fingerprint item = items.get(i); FingerprintPreference pref = new FingerprintPreference(root.getContext(), this /* onDeleteClickListener */); - pref.setKey(genKey(item.getFingerId())); + pref.setKey(genKey(item.getBiometricId())); pref.setTitle(item.getName()); pref.setFingerprint(item); pref.setPersistent(false); pref.setIcon(R.drawable.ic_fingerprint_24dp); - if (mRemovalSidecar.isRemovingFingerprint(item.getFingerId())) { + if (mRemovalSidecar.isRemovingFingerprint(item.getBiometricId())) { pref.setEnabled(false); } - if (mFingerprintsRenaming.containsKey(item.getFingerId())) { - pref.setTitle(mFingerprintsRenaming.get(item.getFingerId())); + if (mFingerprintsRenaming.containsKey(item.getBiometricId())) { + pref.setTitle(mFingerprintsRenaming.get(item.getBiometricId())); } root.addPreference(pref); pref.setOnPreferenceChangeListener(this); @@ -409,7 +396,7 @@ public class FingerprintSettings extends SubSettings { Preference addPreference = new Preference(root.getContext()); addPreference.setKey(KEY_FINGERPRINT_ADD); addPreference.setTitle(R.string.fingerprint_add_title); - addPreference.setIcon(R.drawable.ic_menu_add); + addPreference.setIcon(R.drawable.ic_add_24dp); root.addPreference(addPreference); addPreference.setOnPreferenceChangeListener(this); updateAddPreference(); @@ -478,7 +465,7 @@ public class FingerprintSettings extends SubSettings { final String key = pref.getKey(); if (KEY_FINGERPRINT_ADD.equals(key)) { Intent intent = new Intent(); - intent.setClassName("com.android.settings", + intent.setClassName(SETTINGS_PACKAGE_NAME, FingerprintEnrollEnrolling.class.getName()); intent.putExtra(Intent.EXTRA_USER_ID, mUserId); intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, mToken); @@ -521,9 +508,9 @@ public class FingerprintSettings extends SubSettings { private void showRenameDialog(final Fingerprint fp) { RenameDialog renameDialog = new RenameDialog(); Bundle args = new Bundle(); - if (mFingerprintsRenaming.containsKey(fp.getFingerId())) { - final Fingerprint f = new Fingerprint(mFingerprintsRenaming.get(fp.getFingerId()), - fp.getGroupId(), fp.getFingerId(), fp.getDeviceId()); + if (mFingerprintsRenaming.containsKey(fp.getBiometricId())) { + final Fingerprint f = new Fingerprint(mFingerprintsRenaming.get(fp.getBiometricId()), + fp.getGroupId(), fp.getBiometricId(), fp.getDeviceId()); args.putParcelable("fingerprint", f); } else { args.putParcelable("fingerprint", fp); @@ -605,6 +592,10 @@ public class FingerprintSettings extends SubSettings { final Drawable highlight = getHighlightDrawable(); if (highlight != null && fpref != null) { final View view = fpref.getView(); + if (view == null) { + // FingerprintPreference is not bound to UI yet, so view is null. + return; + } final int centerX = view.getWidth() / 2; final int centerY = view.getHeight() / 2; highlight.setHotspot(centerX, centerY); @@ -627,7 +618,7 @@ public class FingerprintSettings extends SubSettings { if (!helper.launchConfirmationActivity(CONFIRM_REQUEST, getString(R.string.security_settings_fingerprint_preference_title), null, null, challenge, mUserId)) { - intent.setClassName("com.android.settings", ChooseLockGeneric.class.getName()); + intent.setClassName(SETTINGS_PACKAGE_NAME, ChooseLockGeneric.class.getName()); intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment.MINIMUM_QUALITY_KEY, DevicePolicyManager.PASSWORD_QUALITY_SOMETHING); intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment.HIDE_DISABLED_PREFS, @@ -643,7 +634,7 @@ public class FingerprintSettings extends SubSettings { @VisibleForTesting void deleteFingerPrint(Fingerprint fingerPrint) { mRemovalSidecar.startRemove(fingerPrint, mUserId); - String name = genKey(fingerPrint.getFingerId()); + String name = genKey(fingerPrint.getBiometricId()); Preference prefToRemove = findPreference(name); prefToRemove.setEnabled(false); updateAddPreference(); @@ -684,7 +675,7 @@ public class FingerprintSettings extends SubSettings { @Override public int getMetricsCategory() { - return MetricsEvent.DIALOG_FINGERPINT_EDIT; + return SettingsEnums.DIALOG_FINGERPINT_EDIT; } @Override @@ -706,10 +697,10 @@ public class FingerprintSettings extends SubSettings { @Override public void onClick(DialogInterface dialog, int which) { if (which == DialogInterface.BUTTON_POSITIVE) { - final int fingerprintId = mFp.getFingerId(); + final int fingerprintId = mFp.getBiometricId(); Log.v(TAG, "Removing fpId=" + fingerprintId); mMetricsFeatureProvider.action(getContext(), - MetricsEvent.ACTION_FINGERPRINT_DELETE, + SettingsEnums.ACTION_FINGERPRINT_DELETE, fingerprintId); FingerprintSettingsFragment parent = (FingerprintSettingsFragment) getTargetFragment(); @@ -721,11 +712,7 @@ public class FingerprintSettings extends SubSettings { public static class RenameDialog extends InstrumentedDialogFragment { private Fingerprint mFp; - private EditText mDialogTextField; - private String mFingerName; - private Boolean mTextHadFocus; - private int mTextSelectionStart; - private int mTextSelectionEnd; + private ImeAwareEditText mDialogTextField; private AlertDialog mAlertDialog; private boolean mDeleteInProgress; @@ -736,11 +723,17 @@ public class FingerprintSettings extends SubSettings { @Override public Dialog onCreateDialog(Bundle savedInstanceState) { mFp = getArguments().getParcelable("fingerprint"); + final String fingerName; + final int textSelectionStart; + final int textSelectionEnd; if (savedInstanceState != null) { - mFingerName = savedInstanceState.getString("fingerName"); - mTextHadFocus = savedInstanceState.getBoolean("textHadFocus"); - mTextSelectionStart = savedInstanceState.getInt("startSelection"); - mTextSelectionEnd = savedInstanceState.getInt("endSelection"); + fingerName = savedInstanceState.getString("fingerName"); + textSelectionStart = savedInstanceState.getInt("startSelection", -1); + textSelectionEnd = savedInstanceState.getInt("endSelection", -1); + } else { + fingerName = null; + textSelectionStart = -1; + textSelectionEnd = -1; } mAlertDialog = new AlertDialog.Builder(getActivity()) .setView(R.layout.fingerprint_rename_dialog) @@ -754,12 +747,12 @@ public class FingerprintSettings extends SubSettings { if (!TextUtils.equals(newName, name)) { Log.d(TAG, "rename " + name + " to " + newName); mMetricsFeatureProvider.action(getContext(), - MetricsEvent.ACTION_FINGERPRINT_RENAME, - mFp.getFingerId()); + SettingsEnums.ACTION_FINGERPRINT_RENAME, + mFp.getBiometricId()); FingerprintSettingsFragment parent = (FingerprintSettingsFragment) getTargetFragment(); - parent.renameFingerPrint(mFp.getFingerId(), + parent.renameFingerPrint(mFp.getBiometricId(), newName); } dialog.dismiss(); @@ -769,26 +762,21 @@ public class FingerprintSettings extends SubSettings { mAlertDialog.setOnShowListener(new DialogInterface.OnShowListener() { @Override public void onShow(DialogInterface dialog) { - mDialogTextField = (EditText) mAlertDialog.findViewById( - R.id.fingerprint_rename_field); - CharSequence name = mFingerName == null ? mFp.getName() : mFingerName; + mDialogTextField = mAlertDialog.findViewById(R.id.fingerprint_rename_field); + CharSequence name = fingerName == null ? mFp.getName() : fingerName; mDialogTextField.setText(name); - if (mTextHadFocus == null) { - mDialogTextField.selectAll(); + if (textSelectionStart != -1 && textSelectionEnd != -1) { + mDialogTextField.setSelection(textSelectionStart, textSelectionEnd); } else { - mDialogTextField.setSelection(mTextSelectionStart, mTextSelectionEnd); + mDialogTextField.selectAll(); } if (mDeleteInProgress) { mAlertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).setEnabled(false); } mDialogTextField.requestFocus(); + mDialogTextField.scheduleShowSoftInput(); } }); - if (mTextHadFocus == null || mTextHadFocus) { - // Request the IME - mAlertDialog.getWindow().setSoftInputMode( - WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); - } return mAlertDialog; } @@ -804,7 +792,6 @@ public class FingerprintSettings extends SubSettings { super.onSaveInstanceState(outState); if (mDialogTextField != null) { outState.putString("fingerName", mDialogTextField.getText().toString()); - outState.putBoolean("textHadFocus", mDialogTextField.hasFocus()); outState.putInt("startSelection", mDialogTextField.getSelectionStart()); outState.putInt("endSelection", mDialogTextField.getSelectionEnd()); } @@ -812,7 +799,7 @@ public class FingerprintSettings extends SubSettings { @Override public int getMetricsCategory() { - return MetricsEvent.DIALOG_FINGERPINT_EDIT; + return SettingsEnums.DIALOG_FINGERPINT_EDIT; } } @@ -822,7 +809,7 @@ public class FingerprintSettings extends SubSettings { @Override public int getMetricsCategory() { - return MetricsEvent.DIALOG_FINGERPINT_DELETE_LAST; + return SettingsEnums.DIALOG_FINGERPINT_DELETE_LAST; } @Override diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintStatusPreferenceController.java b/src/com/android/settings/biometrics/fingerprint/FingerprintStatusPreferenceController.java new file mode 100644 index 0000000000..0e1ccd7839 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintStatusPreferenceController.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2018 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.biometrics.fingerprint; + +import android.content.Context; +import android.hardware.fingerprint.FingerprintManager; + +import com.android.settings.R; +import com.android.settings.Utils; +import com.android.settings.biometrics.BiometricStatusPreferenceController; + +public class FingerprintStatusPreferenceController extends BiometricStatusPreferenceController { + + private static final String KEY_FINGERPRINT_SETTINGS = "fingerprint_settings"; + + protected final FingerprintManager mFingerprintManager; + + public FingerprintStatusPreferenceController(Context context) { + this(context, KEY_FINGERPRINT_SETTINGS); + } + + public FingerprintStatusPreferenceController(Context context, String key) { + super(context, key); + mFingerprintManager = Utils.getFingerprintManagerOrNull(context); + } + + @Override + protected boolean isDeviceSupported() { + return mFingerprintManager != null && mFingerprintManager.isHardwareDetected(); + } + + @Override + protected boolean hasEnrolledBiometrics() { + return mFingerprintManager.hasEnrolledFingerprints(getUserId()); + } + + @Override + protected String getSummaryTextEnrolled() { + final int numEnrolled = mFingerprintManager.getEnrolledFingerprints(getUserId()).size(); + return mContext.getResources().getQuantityString( + R.plurals.security_settings_fingerprint_preference_summary, + numEnrolled, numEnrolled); + } + + @Override + protected String getSummaryTextNoneEnrolled() { + return mContext.getString(R.string.security_settings_fingerprint_preference_summary_none); + } + + @Override + protected String getSettingsClassName() { + return FingerprintSettings.class.getName(); + } + + @Override + protected String getEnrollClassName() { + return FingerprintEnrollIntroduction.class.getName(); + } + +} diff --git a/src/com/android/settings/fingerprint/FingerprintSuggestionActivity.java b/src/com/android/settings/biometrics/fingerprint/FingerprintSuggestionActivity.java index fa6aeb4221..307553d7f5 100644 --- a/src/com/android/settings/fingerprint/FingerprintSuggestionActivity.java +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintSuggestionActivity.java @@ -14,24 +14,26 @@ * limitations under the License */ -package com.android.settings.fingerprint; +package com.android.settings.biometrics.fingerprint; import android.app.admin.DevicePolicyManager; import android.content.Context; import android.hardware.fingerprint.FingerprintManager; -import android.widget.Button; import com.android.settings.R; import com.android.settings.Utils; +import com.google.android.setupcompat.template.FooterButton; + public class FingerprintSuggestionActivity extends SetupFingerprintEnrollIntroduction { @Override protected void initViews() { super.initViews(); - final Button cancelButton = findViewById(R.id.fingerprint_cancel_button); - cancelButton.setText(R.string.security_settings_fingerprint_enroll_introduction_cancel); + final FooterButton cancelButton = getCancelButton(); + cancelButton.setText( + this, R.string.security_settings_fingerprint_enroll_introduction_cancel); } @Override diff --git a/src/com/android/settings/fingerprint/SetupFingerprintEnrollEnrolling.java b/src/com/android/settings/biometrics/fingerprint/SetupFingerprintEnrollEnrolling.java index fbbf033aa3..a86711d936 100644 --- a/src/com/android/settings/fingerprint/SetupFingerprintEnrollEnrolling.java +++ b/src/com/android/settings/biometrics/fingerprint/SetupFingerprintEnrollEnrolling.java @@ -14,11 +14,11 @@ * limitations under the License */ -package com.android.settings.fingerprint; +package com.android.settings.biometrics.fingerprint; +import android.app.settings.SettingsEnums; import android.content.Intent; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.SetupWizardUtils; public class SetupFingerprintEnrollEnrolling extends FingerprintEnrollEnrolling { @@ -32,6 +32,6 @@ public class SetupFingerprintEnrollEnrolling extends FingerprintEnrollEnrolling @Override public int getMetricsCategory() { - return MetricsEvent.FINGERPRINT_ENROLLING_SETUP; + return SettingsEnums.FINGERPRINT_ENROLLING_SETUP; } } diff --git a/src/com/android/settings/fingerprint/SetupFingerprintEnrollFindSensor.java b/src/com/android/settings/biometrics/fingerprint/SetupFingerprintEnrollFindSensor.java index 78dcb80afc..8097adc701 100644 --- a/src/com/android/settings/fingerprint/SetupFingerprintEnrollFindSensor.java +++ b/src/com/android/settings/biometrics/fingerprint/SetupFingerprintEnrollFindSensor.java @@ -14,20 +14,21 @@ * limitations under the License */ -package com.android.settings.fingerprint; +package com.android.settings.biometrics.fingerprint; import android.app.Activity; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.FragmentManager; +import android.app.settings.SettingsEnums; import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; import android.os.UserHandle; +import android.view.View; + import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.FragmentManager; -import com.android.internal.logging.nano.MetricsProto; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.SetupWizardUtils; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; @@ -41,7 +42,7 @@ public class SetupFingerprintEnrollFindSensor extends FingerprintEnrollFindSenso } @Override - protected Intent getEnrollingIntent() { + protected Intent getFingerprintEnrollingIntent() { Intent intent = new Intent(this, SetupFingerprintEnrollEnrolling.class); intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, mToken); if (mUserId != UserHandle.USER_NULL) { @@ -52,13 +53,13 @@ public class SetupFingerprintEnrollFindSensor extends FingerprintEnrollFindSenso } @Override - protected void onSkipButtonClick() { - new SkipFingerprintDialog().show(getFragmentManager()); + protected void onSkipButtonClick(View view) { + new SkipFingerprintDialog().show(getSupportFragmentManager()); } @Override public int getMetricsCategory() { - return MetricsEvent.FINGERPRINT_FIND_SENSOR_SETUP; + return SettingsEnums.FINGERPRINT_FIND_SENSOR_SETUP; } public static class SkipFingerprintDialog extends InstrumentedDialogFragment @@ -67,7 +68,7 @@ public class SetupFingerprintEnrollFindSensor extends FingerprintEnrollFindSenso @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DIALOG_FINGERPRINT_SKIP_SETUP; + return SettingsEnums.DIALOG_FINGERPRINT_SKIP_SETUP; } @Override diff --git a/src/com/android/settings/fingerprint/SetupFingerprintEnrollFinish.java b/src/com/android/settings/biometrics/fingerprint/SetupFingerprintEnrollFinish.java index e81203fb7a..971a41047b 100644 --- a/src/com/android/settings/fingerprint/SetupFingerprintEnrollFinish.java +++ b/src/com/android/settings/biometrics/fingerprint/SetupFingerprintEnrollFinish.java @@ -14,21 +14,22 @@ * limitations under the License */ -package com.android.settings.fingerprint; +package com.android.settings.biometrics.fingerprint; +import android.app.settings.SettingsEnums; import android.content.Intent; import android.os.UserHandle; -import android.widget.Button; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.SetupWizardUtils; import com.android.settings.password.ChooseLockSettingsHelper; +import com.google.android.setupcompat.template.FooterButton; + public class SetupFingerprintEnrollFinish extends FingerprintEnrollFinish { @Override - protected Intent getEnrollingIntent() { + protected Intent getFingerprintEnrollingIntent() { Intent intent = new Intent(this, SetupFingerprintEnrollEnrolling.class); intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, mToken); if (mUserId != UserHandle.USER_NULL) { @@ -41,12 +42,12 @@ public class SetupFingerprintEnrollFinish extends FingerprintEnrollFinish { @Override protected void initViews() { super.initViews(); - Button nextButton = findViewById(R.id.next_button); - nextButton.setText(R.string.next_label); + FooterButton nextButton = getNextButton(); + nextButton.setText(this, R.string.next_label); } @Override public int getMetricsCategory() { - return MetricsEvent.FINGERPRINT_ENROLL_FINISH_SETUP; + return SettingsEnums.FINGERPRINT_ENROLL_FINISH_SETUP; } } diff --git a/src/com/android/settings/fingerprint/SetupFingerprintEnrollIntroduction.java b/src/com/android/settings/biometrics/fingerprint/SetupFingerprintEnrollIntroduction.java index 5656a27d54..7df66b789f 100644 --- a/src/com/android/settings/fingerprint/SetupFingerprintEnrollIntroduction.java +++ b/src/com/android/settings/biometrics/fingerprint/SetupFingerprintEnrollIntroduction.java @@ -14,27 +14,36 @@ * limitations under the License */ -package com.android.settings.fingerprint; +package com.android.settings.biometrics.fingerprint; import android.app.Activity; import android.app.KeyguardManager; import android.app.admin.DevicePolicyManager; +import android.app.settings.SettingsEnums; import android.content.Intent; +import android.hardware.fingerprint.FingerprintManager; import android.os.Bundle; import android.os.UserHandle; -import android.widget.Button; +import android.os.storage.StorageManager; +import android.view.View; import android.widget.TextView; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.widget.LockPatternUtils; import com.android.settings.R; import com.android.settings.SetupWizardUtils; +import com.android.settings.Utils; import com.android.settings.password.ChooseLockGeneric.ChooseLockGenericFragment; import com.android.settings.password.SetupChooseLockGeneric; import com.android.settings.password.SetupSkipDialog; -import com.android.settings.password.StorageManagerWrapper; + +import com.google.android.setupcompat.template.FooterButton; public class SetupFingerprintEnrollIntroduction extends FingerprintEnrollIntroduction { + /** + * Returns the number of fingerprint enrolled. + */ + private static final String EXTRA_FINGERPRINT_ENROLLED_COUNT = "fingerprint_enrolled_count"; + private static final String KEY_LOCK_SCREEN_PRESENT = "wasLockScreenPresent"; private boolean mAlreadyHadLockScreenSetup = false; @@ -59,7 +68,7 @@ public class SetupFingerprintEnrollIntroduction extends FingerprintEnrollIntrodu protected Intent getChooseLockIntent() { Intent intent = new Intent(this, SetupChooseLockGeneric.class); - if (StorageManagerWrapper.isFileEncryptedNativeOrEmulated()) { + if (StorageManager.isFileEncryptedNativeOrEmulated()) { intent.putExtra( LockPatternUtils.PASSWORD_TYPE_KEY, DevicePolicyManager.PASSWORD_QUALITY_NUMERIC); @@ -70,7 +79,7 @@ public class SetupFingerprintEnrollIntroduction extends FingerprintEnrollIntrodu } @Override - protected Intent getFindSensorIntent() { + protected Intent getEnrollingIntent() { final Intent intent = new Intent(this, SetupFingerprintEnrollFindSensor.class); SetupWizardUtils.copySetupExtras(getIntent(), intent); return intent; @@ -80,27 +89,34 @@ public class SetupFingerprintEnrollIntroduction extends FingerprintEnrollIntrodu protected void initViews() { super.initViews(); - TextView description = (TextView) findViewById(R.id.description_text); + TextView description = (TextView) findViewById(R.id.sud_layout_description); description.setText( R.string.security_settings_fingerprint_enroll_introduction_message_setup); - Button nextButton = getNextButton(); + FooterButton nextButton = getNextButton(); nextButton.setText( - R.string.security_settings_fingerprint_enroll_introduction_continue_setup); + this, R.string.security_settings_fingerprint_enroll_introduction_continue_setup); - final Button cancelButton = (Button) findViewById(R.id.fingerprint_cancel_button); + final FooterButton cancelButton = getCancelButton(); cancelButton.setText( - R.string.security_settings_fingerprint_enroll_introduction_cancel_setup); + this, R.string.security_settings_fingerprint_enroll_introduction_cancel_setup); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { // if lock was already present, do not return intent data since it must have been // reported in previous attempts - if (requestCode == FINGERPRINT_FIND_SENSOR_REQUEST && isKeyguardSecure() - && !mAlreadyHadLockScreenSetup) { - data = getMetricIntent(data); + if (requestCode == BIOMETRIC_FIND_SENSOR_REQUEST && isKeyguardSecure()) { + if(!mAlreadyHadLockScreenSetup) { + data = getMetricIntent(data); + } + + // Report fingerprint count if user adding a new fingerprint + if(resultCode == RESULT_FINISHED) { + data = setFingerprintCount(data); + } } + super.onActivityResult(requestCode, resultCode, data); } @@ -112,11 +128,24 @@ public class SetupFingerprintEnrollIntroduction extends FingerprintEnrollIntrodu data.putExtra(SetupChooseLockGeneric. SetupChooseLockGenericFragment.EXTRA_PASSWORD_QUALITY, lockPatternUtils.getKeyguardStoredPasswordQuality(UserHandle.myUserId())); + + return data; + } + + private Intent setFingerprintCount(Intent data) { + if (data == null) { + data = new Intent(); + } + final FingerprintManager fpm = Utils.getFingerprintManagerOrNull(this); + if (fpm != null) { + int enrolled = fpm.getEnrolledFingerprints(mUserId).size(); + data.putExtra(EXTRA_FINGERPRINT_ENROLLED_COUNT, enrolled); + } return data; } @Override - protected void onCancelButtonClick() { + protected void onCancelButtonClick(View view) { if (isKeyguardSecure()) { // If the keyguard is already set up securely (maybe the user added a backup screen // lock and skipped fingerprint), return RESULT_SKIP directly. @@ -146,6 +175,6 @@ public class SetupFingerprintEnrollIntroduction extends FingerprintEnrollIntrodu @Override public int getMetricsCategory() { - return MetricsEvent.FINGERPRINT_ENROLL_INTRO_SETUP; + return SettingsEnums.FINGERPRINT_ENROLL_INTRO_SETUP; } } diff --git a/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderController.java b/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderController.java new file mode 100644 index 0000000000..6817d0d3ee --- /dev/null +++ b/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderController.java @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2019 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.bluetooth; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.provider.DeviceConfig; +import android.provider.MediaStore; +import android.util.Log; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.core.SettingsUIDeviceConfig; +import com.android.settings.fuelgauge.BatteryMeterView; +import com.android.settingslib.bluetooth.BluetoothUtils; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnDestroy; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.core.lifecycle.events.OnStop; +import com.android.settingslib.utils.ThreadUtils; +import com.android.settingslib.widget.LayoutPreference; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * This class adds a header with device name and status (connected/disconnected, etc.). + */ +public class AdvancedBluetoothDetailsHeaderController extends BasePreferenceController implements + LifecycleObserver, OnStart, OnStop, OnDestroy, CachedBluetoothDevice.Callback { + private static final String TAG = "AdvancedBtHeaderCtrl"; + + @VisibleForTesting + LayoutPreference mLayoutPreference; + @VisibleForTesting + final Map<String, Bitmap> mIconCache; + private CachedBluetoothDevice mCachedDevice; + @VisibleForTesting + BluetoothAdapter mBluetoothAdapter; + @VisibleForTesting + Handler mHandler = new Handler(Looper.getMainLooper()); + @VisibleForTesting + final BluetoothAdapter.OnMetadataChangedListener mMetadataListener = + new BluetoothAdapter.OnMetadataChangedListener() { + @Override + public void onMetadataChanged(BluetoothDevice device, int key, byte[] value) { + Log.i(TAG, String.format("Metadata updated in Device %s: %d = %s.", device, key, + value == null ? null : new String(value))); + refresh(); + } + }; + + public AdvancedBluetoothDetailsHeaderController(Context context, String prefKey) { + super(context, prefKey); + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + mIconCache = new HashMap<>(); + } + + @Override + public int getAvailabilityStatus() { + final boolean advancedEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI, + SettingsUIDeviceConfig.BT_ADVANCED_HEADER_ENABLED, true); + final boolean untetheredHeadset = BluetoothUtils.getBooleanMetaData( + mCachedDevice.getDevice(), BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET); + return advancedEnabled && untetheredHeadset ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mLayoutPreference = screen.findPreference(getPreferenceKey()); + mLayoutPreference.setVisible(isAvailable()); + + refresh(); + } + + @Override + public void onStart() { + if (!isAvailable()) { + return; + } + mCachedDevice.registerCallback(this::onDeviceAttributesChanged); + mBluetoothAdapter.addOnMetadataChangedListener(mCachedDevice.getDevice(), + mContext.getMainExecutor(), mMetadataListener); + } + + @Override + public void onStop() { + if (!isAvailable()) { + return; + } + mCachedDevice.unregisterCallback(this::onDeviceAttributesChanged); + mBluetoothAdapter.removeOnMetadataChangedListener(mCachedDevice.getDevice(), + mMetadataListener); + } + + @Override + public void onDestroy() { + if (!isAvailable()) { + return; + } + // Destroy icon bitmap associated with this header + for (Bitmap bitmap : mIconCache.values()) { + if (bitmap != null) { + bitmap.recycle(); + } + } + mIconCache.clear(); + } + + public void init(CachedBluetoothDevice cachedBluetoothDevice) { + mCachedDevice = cachedBluetoothDevice; + } + + @VisibleForTesting + void refresh() { + if (mLayoutPreference != null && mCachedDevice != null) { + final TextView title = mLayoutPreference.findViewById(R.id.entity_header_title); + title.setText(mCachedDevice.getName()); + final TextView summary = mLayoutPreference.findViewById(R.id.entity_header_summary); + summary.setText(mCachedDevice.getConnectionSummary(true /* shortSummary */)); + + if (!mCachedDevice.isConnected()) { + updateDisconnectLayout(); + return; + } + + updateSubLayout(mLayoutPreference.findViewById(R.id.layout_left), + BluetoothDevice.METADATA_UNTETHERED_LEFT_ICON, + BluetoothDevice.METADATA_UNTETHERED_LEFT_BATTERY, + BluetoothDevice.METADATA_UNTETHERED_LEFT_CHARGING, + R.string.bluetooth_left_name); + + updateSubLayout(mLayoutPreference.findViewById(R.id.layout_middle), + BluetoothDevice.METADATA_UNTETHERED_CASE_ICON, + BluetoothDevice.METADATA_UNTETHERED_CASE_BATTERY, + BluetoothDevice.METADATA_UNTETHERED_CASE_CHARGING, + R.string.bluetooth_middle_name); + + updateSubLayout(mLayoutPreference.findViewById(R.id.layout_right), + BluetoothDevice.METADATA_UNTETHERED_RIGHT_ICON, + BluetoothDevice.METADATA_UNTETHERED_RIGHT_BATTERY, + BluetoothDevice.METADATA_UNTETHERED_RIGHT_CHARGING, + R.string.bluetooth_right_name); + } + } + + @VisibleForTesting + Drawable createBtBatteryIcon(Context context, int level, boolean charging) { + final BatteryMeterView.BatteryMeterDrawable drawable = + new BatteryMeterView.BatteryMeterDrawable(context, + context.getColor(R.color.meter_background_color)); + drawable.setBatteryLevel(level); + drawable.setColorFilter(new PorterDuffColorFilter( + com.android.settings.Utils.getColorAttrDefaultColor(context, + android.R.attr.colorControlNormal), + PorterDuff.Mode.SRC_IN)); + drawable.setCharging(charging); + + return drawable; + } + + private void updateSubLayout(LinearLayout linearLayout, int iconMetaKey, int batteryMetaKey, + int chargeMetaKey, int titleResId) { + if (linearLayout == null) { + return; + } + final BluetoothDevice bluetoothDevice = mCachedDevice.getDevice(); + final String iconUri = BluetoothUtils.getStringMetaData(bluetoothDevice, iconMetaKey); + if (iconUri != null) { + final ImageView imageView = linearLayout.findViewById(R.id.header_icon); + updateIcon(imageView, iconUri); + } + + final int batteryLevel = BluetoothUtils.getIntMetaData(bluetoothDevice, batteryMetaKey); + final boolean charging = BluetoothUtils.getBooleanMetaData(bluetoothDevice, chargeMetaKey); + if (batteryLevel != BluetoothUtils.META_INT_ERROR) { + linearLayout.setVisibility(View.VISIBLE); + final ImageView imageView = linearLayout.findViewById(R.id.bt_battery_icon); + imageView.setImageDrawable(createBtBatteryIcon(mContext, batteryLevel, charging)); + imageView.setVisibility(View.VISIBLE); + final TextView textView = linearLayout.findViewById(R.id.bt_battery_summary); + textView.setText(com.android.settings.Utils.formatPercentage(batteryLevel)); + textView.setVisibility(View.VISIBLE); + } else { + // Hide it if it doesn't have battery information + linearLayout.setVisibility(View.GONE); + } + + final TextView textView = linearLayout.findViewById(R.id.header_title); + textView.setText(titleResId); + textView.setVisibility(View.VISIBLE); + } + + private void updateDisconnectLayout() { + mLayoutPreference.findViewById(R.id.layout_left).setVisibility(View.GONE); + mLayoutPreference.findViewById(R.id.layout_right).setVisibility(View.GONE); + + // Hide title, battery icon and battery summary + final LinearLayout linearLayout = mLayoutPreference.findViewById(R.id.layout_middle); + linearLayout.setVisibility(View.VISIBLE); + linearLayout.findViewById(R.id.header_title).setVisibility(View.GONE); + linearLayout.findViewById(R.id.bt_battery_summary).setVisibility(View.GONE); + linearLayout.findViewById(R.id.bt_battery_icon).setVisibility(View.GONE); + + // Only show bluetooth icon + final BluetoothDevice bluetoothDevice = mCachedDevice.getDevice(); + final String iconUri = BluetoothUtils.getStringMetaData(bluetoothDevice, + BluetoothDevice.METADATA_MAIN_ICON); + if (iconUri != null) { + final ImageView imageView = linearLayout.findViewById(R.id.header_icon); + updateIcon(imageView, iconUri); + } + } + + /** + * Update icon by {@code iconUri}. If icon exists in cache, use it; otherwise extract it + * from uri in background thread and update it in main thread. + */ + @VisibleForTesting + void updateIcon(ImageView imageView, String iconUri) { + if (mIconCache.containsKey(iconUri)) { + imageView.setImageBitmap(mIconCache.get(iconUri)); + return; + } + + ThreadUtils.postOnBackgroundThread(() -> { + try { + final Bitmap bitmap = MediaStore.Images.Media.getBitmap( + mContext.getContentResolver(), Uri.parse(iconUri)); + ThreadUtils.postOnMainThread(() -> { + mIconCache.put(iconUri, bitmap); + imageView.setImageBitmap(bitmap); + }); + } catch (IOException e) { + Log.e(TAG, "Failed to get bitmap for: " + iconUri); + } + }); + } + + @Override + public void onDeviceAttributesChanged() { + if (mCachedDevice != null) { + refresh(); + } + } +} diff --git a/src/com/android/settings/bluetooth/AlwaysDiscoverable.java b/src/com/android/settings/bluetooth/AlwaysDiscoverable.java index 6f74b54b99..7b94871972 100644 --- a/src/com/android/settings/bluetooth/AlwaysDiscoverable.java +++ b/src/com/android/settings/bluetooth/AlwaysDiscoverable.java @@ -21,14 +21,8 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.os.Handler; -import androidx.annotation.VisibleForTesting; -import android.util.Log; - -import com.android.settingslib.bluetooth.LocalBluetoothAdapter; -import java.util.Timer; -import java.util.TimerTask; +import androidx.annotation.VisibleForTesting; /** Helper class, intended to be used by an Activity, to keep the local Bluetooth adapter in * discoverable mode indefinitely. By default setting the scan mode to @@ -39,15 +33,15 @@ public class AlwaysDiscoverable extends BroadcastReceiver { private static final String TAG = "AlwaysDiscoverable"; private Context mContext; - private LocalBluetoothAdapter mLocalAdapter; + private BluetoothAdapter mBluetoothAdapter; private IntentFilter mIntentFilter; @VisibleForTesting boolean mStarted; - public AlwaysDiscoverable(Context context, LocalBluetoothAdapter localAdapter) { + public AlwaysDiscoverable(Context context) { mContext = context; - mLocalAdapter = localAdapter; + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); mIntentFilter = new IntentFilter(); mIntentFilter.addAction(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED); } @@ -60,8 +54,9 @@ public class AlwaysDiscoverable extends BroadcastReceiver { } mContext.registerReceiver(this, mIntentFilter); mStarted = true; - if (mLocalAdapter.getScanMode() != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) { - mLocalAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE); + if (mBluetoothAdapter.getScanMode() + != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) { + mBluetoothAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE); } } @@ -71,7 +66,7 @@ public class AlwaysDiscoverable extends BroadcastReceiver { } mContext.unregisterReceiver(this); mStarted = false; - mLocalAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE); + mBluetoothAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE); } @Override @@ -80,8 +75,9 @@ public class AlwaysDiscoverable extends BroadcastReceiver { if (action != BluetoothAdapter.ACTION_SCAN_MODE_CHANGED) { return; } - if (mLocalAdapter.getScanMode() != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) { - mLocalAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE); + if (mBluetoothAdapter.getScanMode() + != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) { + mBluetoothAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE); } } } diff --git a/src/com/android/settings/bluetooth/AvailableMediaBluetoothDeviceUpdater.java b/src/com/android/settings/bluetooth/AvailableMediaBluetoothDeviceUpdater.java index 1da1cae504..45ac7f790e 100644 --- a/src/com/android/settings/bluetooth/AvailableMediaBluetoothDeviceUpdater.java +++ b/src/com/android/settings/bluetooth/AvailableMediaBluetoothDeviceUpdater.java @@ -18,14 +18,13 @@ package com.android.settings.bluetooth; import android.bluetooth.BluetoothProfile; import android.content.Context; import android.media.AudioManager; -import androidx.annotation.VisibleForTesting; import android.util.Log; +import androidx.preference.Preference; + import com.android.settings.connecteddevice.DevicePreferenceCallback; import com.android.settings.dashboard.DashboardFragment; -import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.bluetooth.CachedBluetoothDevice; -import androidx.preference.Preference; /** * Controller to maintain available media Bluetooth devices @@ -44,40 +43,12 @@ public class AvailableMediaBluetoothDeviceUpdater extends BluetoothDeviceUpdater mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); } - @VisibleForTesting - AvailableMediaBluetoothDeviceUpdater(DashboardFragment fragment, - DevicePreferenceCallback devicePreferenceCallback, - LocalBluetoothManager localBluetoothManager) { - super(fragment, devicePreferenceCallback, localBluetoothManager); - mAudioManager = (AudioManager) fragment.getContext(). - getSystemService(Context.AUDIO_SERVICE); - } - @Override public void onAudioModeChanged() { forceUpdate(); } @Override - public void onProfileConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state, - int bluetoothProfile) { - if (DBG) { - Log.d(TAG, "onProfileConnectionStateChanged() device: " + - cachedDevice.getName() + ", state: " + state + ", bluetoothProfile: " - + bluetoothProfile); - } - if (state == BluetoothProfile.STATE_CONNECTED) { - if (isFilterMatched(cachedDevice)) { - addPreference(cachedDevice); - } else { - removePreference(cachedDevice); - } - } else if (state == BluetoothProfile.STATE_DISCONNECTED) { - removePreference(cachedDevice); - } - } - - @Override public boolean isFilterMatched(CachedBluetoothDevice cachedDevice) { final int audioMode = mAudioManager.getMode(); final int currentAudioProfile; @@ -110,10 +81,10 @@ public class AvailableMediaBluetoothDeviceUpdater extends BluetoothDeviceUpdater // show the bluetooth device that have headset profile. switch (currentAudioProfile) { case BluetoothProfile.A2DP: - isFilterMatched = cachedDevice.isA2dpDevice(); + isFilterMatched = cachedDevice.isConnectedA2dpDevice(); break; case BluetoothProfile.HEADSET: - isFilterMatched = cachedDevice.isHfpDevice(); + isFilterMatched = cachedDevice.isConnectedHfpDevice(); break; } if (DBG) { diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsButtonsController.java b/src/com/android/settings/bluetooth/BluetoothDetailsButtonsController.java index 20806458cb..a7fae1441f 100644 --- a/src/com/android/settings/bluetooth/BluetoothDetailsButtonsController.java +++ b/src/com/android/settings/bluetooth/BluetoothDetailsButtonsController.java @@ -17,13 +17,14 @@ package com.android.settings.bluetooth; import android.content.Context; -import androidx.preference.PreferenceFragment; + +import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceScreen; import com.android.settings.R; -import com.android.settings.widget.ActionButtonPreference; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.widget.ActionButtonsPreference; /** * This class adds two buttons: one to connect/disconnect from a device (depending on the current @@ -34,9 +35,9 @@ public class BluetoothDetailsButtonsController extends BluetoothDetailsControlle private boolean mIsConnected; private boolean mConnectButtonInitialized; - private ActionButtonPreference mActionButtons; + private ActionButtonsPreference mActionButtons; - public BluetoothDetailsButtonsController(Context context, PreferenceFragment fragment, + public BluetoothDetailsButtonsController(Context context, PreferenceFragmentCompat fragment, CachedBluetoothDevice device, Lifecycle lifecycle) { super(context, fragment, device, lifecycle); mIsConnected = device.isConnected(); @@ -50,10 +51,11 @@ public class BluetoothDetailsButtonsController extends BluetoothDetailsControlle @Override protected void init(PreferenceScreen screen) { - mActionButtons = ((ActionButtonPreference) screen.findPreference(getPreferenceKey())) + mActionButtons = ((ActionButtonsPreference) screen.findPreference( + getPreferenceKey())) .setButton1Text(R.string.forget) + .setButton1Icon(R.drawable.ic_settings_delete) .setButton1OnClickListener((view) -> onForgetButtonPressed()) - .setButton1Positive(false) .setButton1Enabled(true); } @@ -67,17 +69,17 @@ public class BluetoothDetailsButtonsController extends BluetoothDetailsControlle if (!mConnectButtonInitialized || !previouslyConnected) { mActionButtons .setButton2Text(R.string.bluetooth_device_context_disconnect) - .setButton2OnClickListener(view -> mCachedDevice.disconnect()) - .setButton2Positive(false); + .setButton2Icon(R.drawable.ic_settings_close) + .setButton2OnClickListener(view -> mCachedDevice.disconnect()); mConnectButtonInitialized = true; } } else { if (!mConnectButtonInitialized || previouslyConnected) { mActionButtons .setButton2Text(R.string.bluetooth_device_context_connect) + .setButton2Icon(R.drawable.ic_add_24dp) .setButton2OnClickListener( - view -> mCachedDevice.connect(true /* connectAllProfiles */)) - .setButton2Positive(true); + view -> mCachedDevice.connect(true /* connectAllProfiles */)); mConnectButtonInitialized = true; } } @@ -88,4 +90,4 @@ public class BluetoothDetailsButtonsController extends BluetoothDetailsControlle return KEY_ACTION_BUTTONS; } -} +}
\ No newline at end of file diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsController.java b/src/com/android/settings/bluetooth/BluetoothDetailsController.java index 6ea49a6236..bb489044f8 100644 --- a/src/com/android/settings/bluetooth/BluetoothDetailsController.java +++ b/src/com/android/settings/bluetooth/BluetoothDetailsController.java @@ -17,7 +17,8 @@ package com.android.settings.bluetooth; import android.content.Context; -import androidx.preference.PreferenceFragment; + +import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceScreen; import com.android.settings.core.PreferenceControllerMixin; @@ -37,10 +38,10 @@ public abstract class BluetoothDetailsController extends AbstractPreferenceContr OnPause, OnResume { protected final Context mContext; - protected final PreferenceFragment mFragment; + protected final PreferenceFragmentCompat mFragment; protected final CachedBluetoothDevice mCachedDevice; - public BluetoothDetailsController(Context context, PreferenceFragment fragment, + public BluetoothDetailsController(Context context, PreferenceFragmentCompat fragment, CachedBluetoothDevice device, Lifecycle lifecycle) { super(context); mContext = context; @@ -88,4 +89,4 @@ public abstract class BluetoothDetailsController extends AbstractPreferenceContr * should update the preferences it manages based on the new state. */ protected abstract void refresh(); -} +}
\ No newline at end of file diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsHeaderController.java b/src/com/android/settings/bluetooth/BluetoothDetailsHeaderController.java index 0bfc494418..f5096054c7 100644 --- a/src/com/android/settings/bluetooth/BluetoothDetailsHeaderController.java +++ b/src/com/android/settings/bluetooth/BluetoothDetailsHeaderController.java @@ -16,35 +16,36 @@ package com.android.settings.bluetooth; +import android.bluetooth.BluetoothDevice; import android.content.Context; import android.graphics.drawable.Drawable; -import android.util.Log; +import android.provider.DeviceConfig; import android.util.Pair; -import androidx.preference.PreferenceFragment; +import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceScreen; -import com.android.internal.annotations.VisibleForTesting; import com.android.settings.R; -import com.android.settings.applications.LayoutPreference; +import com.android.settings.core.SettingsUIDeviceConfig; import com.android.settings.widget.EntityHeaderController; +import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.bluetooth.CachedBluetoothDevice; -import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.widget.LayoutPreference; /** * This class adds a header with device name and status (connected/disconnected, etc.). */ public class BluetoothDetailsHeaderController extends BluetoothDetailsController { private static final String KEY_DEVICE_HEADER = "bluetooth_device_header"; - private static final String TAG = "BluetoothDetailsHeaderController"; private EntityHeaderController mHeaderController; private LocalBluetoothManager mLocalManager; private CachedBluetoothDeviceManager mDeviceManager; - public BluetoothDetailsHeaderController(Context context, PreferenceFragment fragment, + public BluetoothDetailsHeaderController(Context context, PreferenceFragmentCompat fragment, CachedBluetoothDevice device, Lifecycle lifecycle, LocalBluetoothManager bluetoothManager) { super(context, fragment, device, lifecycle); @@ -53,29 +54,30 @@ public class BluetoothDetailsHeaderController extends BluetoothDetailsController } @Override + public boolean isAvailable() { + final boolean advancedEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI, + SettingsUIDeviceConfig.BT_ADVANCED_HEADER_ENABLED, true); + return !advancedEnabled + || !BluetoothUtils.getBooleanMetaData(mCachedDevice.getDevice(), + BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET); + } + + @Override protected void init(PreferenceScreen screen) { - final LayoutPreference headerPreference = - (LayoutPreference) screen.findPreference(KEY_DEVICE_HEADER); + final LayoutPreference headerPreference = screen.findPreference(KEY_DEVICE_HEADER); mHeaderController = EntityHeaderController.newInstance(mFragment.getActivity(), mFragment, headerPreference.findViewById(R.id.entity_header)); screen.addPreference(headerPreference); } protected void setHeaderProperties() { - final Pair<Drawable, String> pair = com.android.settingslib.bluetooth.Utils - .getBtClassDrawableWithDescription(mContext, mCachedDevice, - mContext.getResources().getFraction(R.fraction.bt_battery_scale_fraction, 1, 1)); + final Pair<Drawable, String> pair = + BluetoothUtils.getBtRainbowDrawableWithDescription(mContext, mCachedDevice); String summaryText = mCachedDevice.getConnectionSummary(); - - if (mCachedDevice.isHearingAidDevice()) { - // For Hearing Aid device, display the other battery status. - final String pairDeviceSummary = mDeviceManager - .getHearingAidPairDeviceSummary(mCachedDevice); - Log.d(TAG, "setHeaderProperties: HearingAid: summaryText=" + summaryText - + ", pairDeviceSummary=" + pairDeviceSummary); - mHeaderController.setSecondSummary(pairDeviceSummary); - } - + // If both the hearing aids are connected, two device status should be shown. + // If Second Summary is unavailable, to set it to null. + mHeaderController.setSecondSummary( + mDeviceManager.getSubDeviceSummary(mCachedDevice)); mHeaderController.setLabel(mCachedDevice.getName()); mHeaderController.setIcon(pair.first); mHeaderController.setIconContentDescription(pair.second); @@ -84,12 +86,14 @@ public class BluetoothDetailsHeaderController extends BluetoothDetailsController @Override protected void refresh() { - setHeaderProperties(); - mHeaderController.done(mFragment.getActivity(), true /* rebindActions */); + if (isAvailable()) { + setHeaderProperties(); + mHeaderController.done(mFragment.getActivity(), true /* rebindActions */); + } } @Override public String getPreferenceKey() { return KEY_DEVICE_HEADER; } -} +}
\ No newline at end of file diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsMacAddressController.java b/src/com/android/settings/bluetooth/BluetoothDetailsMacAddressController.java index 72effe4927..835961dee5 100644 --- a/src/com/android/settings/bluetooth/BluetoothDetailsMacAddressController.java +++ b/src/com/android/settings/bluetooth/BluetoothDetailsMacAddressController.java @@ -17,28 +17,29 @@ package com.android.settings.bluetooth; import android.content.Context; -import androidx.preference.PreferenceFragment; + +import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceScreen; import com.android.settings.R; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.widget.FooterPreference; -import com.android.settingslib.widget.FooterPreferenceMixin; +import com.android.settingslib.widget.FooterPreferenceMixinCompat; /** * This class adds the device MAC address to a footer. */ public class BluetoothDetailsMacAddressController extends BluetoothDetailsController { - FooterPreferenceMixin mFooterPreferenceMixin; + FooterPreferenceMixinCompat mFooterPreferenceMixin; FooterPreference mFooterPreference; public BluetoothDetailsMacAddressController(Context context, - PreferenceFragment fragment, + PreferenceFragmentCompat fragment, CachedBluetoothDevice device, Lifecycle lifecycle) { super(context, fragment, device, lifecycle); - mFooterPreferenceMixin = new FooterPreferenceMixin(fragment, lifecycle); + mFooterPreferenceMixin = new FooterPreferenceMixinCompat(fragment, lifecycle); } @Override @@ -50,6 +51,8 @@ public class BluetoothDetailsMacAddressController extends BluetoothDetailsContro @Override protected void refresh() { + mFooterPreference.setTitle(mContext.getString( + R.string.bluetooth_device_mac_address, mCachedDevice.getAddress())); } @Override @@ -59,4 +62,4 @@ public class BluetoothDetailsMacAddressController extends BluetoothDetailsContro } return mFooterPreference.getKey(); } -} +}
\ No newline at end of file diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsProfilesController.java b/src/com/android/settings/bluetooth/BluetoothDetailsProfilesController.java index 90723540b5..749a38f614 100644 --- a/src/com/android/settings/bluetooth/BluetoothDetailsProfilesController.java +++ b/src/com/android/settings/bluetooth/BluetoothDetailsProfilesController.java @@ -19,13 +19,14 @@ package com.android.settings.bluetooth; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.content.Context; +import android.text.TextUtils; + import androidx.annotation.VisibleForTesting; -import androidx.preference.PreferenceFragment; -import androidx.preference.SwitchPreference; import androidx.preference.Preference; import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceScreen; -import android.text.TextUtils; +import androidx.preference.SwitchPreference; import com.android.settingslib.bluetooth.A2dpProfile; import com.android.settingslib.bluetooth.CachedBluetoothDevice; @@ -44,7 +45,8 @@ import java.util.List; * supports, such as "Phone audio", "Media audio", "Contact sharing", etc. */ public class BluetoothDetailsProfilesController extends BluetoothDetailsController - implements Preference.OnPreferenceClickListener { + implements Preference.OnPreferenceClickListener, + LocalBluetoothProfileManager.ServiceListener { private static final String KEY_PROFILES_GROUP = "bluetooth_profiles"; @VisibleForTesting @@ -55,7 +57,7 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll private CachedBluetoothDevice mCachedDevice; private PreferenceCategory mProfilesContainer; - public BluetoothDetailsProfilesController(Context context, PreferenceFragment fragment, + public BluetoothDetailsProfilesController(Context context, PreferenceFragmentCompat fragment, LocalBluetoothManager manager, CachedBluetoothDevice device, Lifecycle lifecycle) { super(context, fragment, device, lifecycle); mManager = manager; @@ -86,6 +88,7 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll pref.setKey(profile.toString()); pref.setTitle(profile.getNameResource(mCachedDevice.getDevice())); pref.setOnPreferenceClickListener(this); + pref.setOrder(profile.getOrdinal()); return pref; } @@ -97,11 +100,11 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll BluetoothDevice device = mCachedDevice.getDevice(); profilePref.setEnabled(!mCachedDevice.isBusy()); if (profile instanceof MapProfile) { - profilePref.setChecked(mCachedDevice.getMessagePermissionChoice() - == CachedBluetoothDevice.ACCESS_ALLOWED); + profilePref.setChecked(device.getMessageAccessPermission() + == BluetoothDevice.ACCESS_ALLOWED); } else if (profile instanceof PbapServerProfile) { - profilePref.setChecked(mCachedDevice.getPhonebookPermissionChoice() - == CachedBluetoothDevice.ACCESS_ALLOWED); + profilePref.setChecked(device.getPhonebookAccessPermission() + == BluetoothDevice.ACCESS_ALLOWED); } else if (profile instanceof PanProfile) { profilePref.setChecked(profile.getConnectionStatus(device) == BluetoothProfile.STATE_CONNECTED); @@ -129,31 +132,31 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll /** * Helper method to enable a profile for a device. */ - private void enableProfile(LocalBluetoothProfile profile, BluetoothDevice device, - SwitchPreference profilePref) { + private void enableProfile(LocalBluetoothProfile profile) { + final BluetoothDevice bluetoothDevice = mCachedDevice.getDevice(); if (profile instanceof PbapServerProfile) { - mCachedDevice.setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_ALLOWED); + bluetoothDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED); // We don't need to do the additional steps below for this profile. return; } if (profile instanceof MapProfile) { - mCachedDevice.setMessagePermissionChoice(BluetoothDevice.ACCESS_ALLOWED); + bluetoothDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED); } - profile.setPreferred(device, true); + profile.setPreferred(bluetoothDevice, true); mCachedDevice.connectProfile(profile); } /** * Helper method to disable a profile for a device */ - private void disableProfile(LocalBluetoothProfile profile, BluetoothDevice device, - SwitchPreference profilePref) { + private void disableProfile(LocalBluetoothProfile profile) { + final BluetoothDevice bluetoothDevice = mCachedDevice.getDevice(); mCachedDevice.disconnect(profile); - profile.setPreferred(device, false); + profile.setPreferred(bluetoothDevice, false); if (profile instanceof MapProfile) { - mCachedDevice.setMessagePermissionChoice(BluetoothDevice.ACCESS_REJECTED); + bluetoothDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED); } else if (profile instanceof PbapServerProfile) { - mCachedDevice.setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_REJECTED); + bluetoothDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED); } } @@ -174,11 +177,10 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll } } SwitchPreference profilePref = (SwitchPreference) preference; - BluetoothDevice device = mCachedDevice.getDevice(); if (profilePref.isChecked()) { - enableProfile(profile, device, profilePref); + enableProfile(profile); } else { - disableProfile(profile, device, profilePref); + disableProfile(profile); } refreshProfilePreference(profilePref, profile); return true; @@ -190,17 +192,18 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll */ private List<LocalBluetoothProfile> getProfiles() { List<LocalBluetoothProfile> result = mCachedDevice.getConnectableProfiles(); + final BluetoothDevice device = mCachedDevice.getDevice(); - final int pbapPermission = mCachedDevice.getPhonebookPermissionChoice(); + final int pbapPermission = device.getPhonebookAccessPermission(); // Only provide PBAP cabability if the client device has requested PBAP. - if (pbapPermission != CachedBluetoothDevice.ACCESS_UNKNOWN) { + if (pbapPermission != BluetoothDevice.ACCESS_UNKNOWN) { final PbapServerProfile psp = mManager.getProfileManager().getPbapProfile(); result.add(psp); } final MapProfile mapProfile = mManager.getProfileManager().getMapProfile(); - final int mapPermission = mCachedDevice.getMessagePermissionChoice(); - if (mapPermission != CachedBluetoothDevice.ACCESS_UNKNOWN) { + final int mapPermission = device.getMessageAccessPermission(); + if (mapPermission != BluetoothDevice.ACCESS_UNKNOWN) { result.add(mapProfile); } @@ -220,7 +223,7 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll } BluetoothDevice device = mCachedDevice.getDevice(); A2dpProfile a2dp = (A2dpProfile) profile; - if (a2dp.supportsHighQualityAudio(device)) { + if (a2dp.isProfileReady() && a2dp.supportsHighQualityAudio(device)) { SwitchPreference highQualityAudioPref = new SwitchPreference( mProfilesContainer.getContext()); highQualityAudioPref.setKey(HIGH_QUALITY_AUDIO_PREF_TAG); @@ -234,6 +237,28 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll } } + @Override + public void onPause() { + super.onPause(); + mProfileManager.removeServiceListener(this); + } + + @Override + public void onResume() { + super.onResume(); + mProfileManager.addServiceListener(this); + } + + @Override + public void onServiceConnected() { + refresh(); + } + + @Override + public void onServiceDisconnected() { + refresh(); + } + /** * Refreshes the state of the switches for all profiles, possibly adding or removing switches as * needed. @@ -241,7 +266,10 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll @Override protected void refresh() { for (LocalBluetoothProfile profile : getProfiles()) { - SwitchPreference pref = (SwitchPreference) mProfilesContainer.findPreference( + if (!profile.isProfileReady()) { + continue; + } + SwitchPreference pref = mProfilesContainer.findPreference( profile.toString()); if (pref == null) { pref = createProfilePreference(mProfilesContainer.getContext(), profile); @@ -251,7 +279,7 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll refreshProfilePreference(pref, profile); } for (LocalBluetoothProfile removedProfile : mCachedDevice.getRemovedProfiles()) { - SwitchPreference pref = (SwitchPreference) mProfilesContainer.findPreference( + final SwitchPreference pref = mProfilesContainer.findPreference( removedProfile.toString()); if (pref != null) { mProfilesContainer.removePreference(pref); @@ -263,4 +291,4 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll public String getPreferenceKey() { return KEY_PROFILES_GROUP; } -} +}
\ No newline at end of file diff --git a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java index 630c2c57c7..c31b13ecb0 100644 --- a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java +++ b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java @@ -18,17 +18,22 @@ package com.android.settings.bluetooth; import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH; +import android.app.settings.SettingsEnums; import android.bluetooth.BluetoothDevice; import android.content.Context; import android.os.Bundle; -import androidx.annotation.VisibleForTesting; +import android.provider.DeviceConfig; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; -import com.android.internal.logging.nano.MetricsProto; +import androidx.annotation.VisibleForTesting; + import com.android.settings.R; +import com.android.settings.core.SettingsUIDeviceConfig; import com.android.settings.dashboard.RestrictedDashboardFragment; +import com.android.settings.overlay.FeatureFactory; +import com.android.settings.slices.BlockingSlicePrefController; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.core.AbstractPreferenceController; @@ -60,9 +65,12 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment @VisibleForTesting static TestDataFactory sTestDataFactory; - private String mDeviceAddress; - private LocalBluetoothManager mManager; - private CachedBluetoothDevice mCachedDevice; + @VisibleForTesting + String mDeviceAddress; + @VisibleForTesting + LocalBluetoothManager mManager; + @VisibleForTesting + CachedBluetoothDevice mCachedDevice; public BluetoothDeviceDetailsFragment() { super(DISALLOW_CONFIG_BLUETOOTH); @@ -99,12 +107,27 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment mDeviceAddress = getArguments().getString(KEY_DEVICE_ADDRESS); mManager = getLocalBluetoothManager(context); mCachedDevice = getCachedDevice(mDeviceAddress); + if (mCachedDevice == null) { + // Close this page if device is null with invalid device mac address + finish(); + return; + } super.onAttach(context); + use(AdvancedBluetoothDetailsHeaderController.class).init(mCachedDevice); + + final BluetoothFeatureProvider featureProvider = FeatureFactory.getFactory( + context).getBluetoothFeatureProvider(context); + final boolean sliceEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI, + SettingsUIDeviceConfig.BT_SLICE_SETTINGS_ENABLED, true); + + use(BlockingSlicePrefController.class).setSliceUri(sliceEnabled + ? featureProvider.getBluetoothDeviceSettingsUri(mCachedDevice.getDevice()) + : null); } @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.BLUETOOTH_DEVICE_DETAILS; + return SettingsEnums.BLUETOOTH_DEVICE_DETAILS; } @Override @@ -120,7 +143,7 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { MenuItem item = menu.add(0, EDIT_DEVICE_NAME_ITEM_ID, 0, R.string.bluetooth_rename_button); - item.setIcon(R.drawable.ic_mode_edit); + item.setIcon(com.android.internal.R.drawable.ic_mode_edit); item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); super.onCreateOptionsMenu(menu, inflater); } @@ -140,7 +163,7 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment ArrayList<AbstractPreferenceController> controllers = new ArrayList<>(); if (mCachedDevice != null) { - Lifecycle lifecycle = getLifecycle(); + Lifecycle lifecycle = getSettingsLifecycle(); controllers.add(new BluetoothDetailsHeaderController(context, this, mCachedDevice, lifecycle, mManager)); controllers.add(new BluetoothDetailsButtonsController(context, this, mCachedDevice, diff --git a/src/com/android/settings/bluetooth/BluetoothDeviceNamePreferenceController.java b/src/com/android/settings/bluetooth/BluetoothDeviceNamePreferenceController.java index 61bed17fe4..129b218238 100644 --- a/src/com/android/settings/bluetooth/BluetoothDeviceNamePreferenceController.java +++ b/src/com/android/settings/bluetooth/BluetoothDeviceNamePreferenceController.java @@ -21,17 +21,16 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; import android.text.BidiFormatter; import android.text.TextUtils; import android.util.Log; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + import com.android.settings.R; import com.android.settings.core.BasePreferenceController; -import com.android.settingslib.bluetooth.LocalBluetoothAdapter; -import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnStart; import com.android.settingslib.core.lifecycle.events.OnStop; @@ -45,8 +44,7 @@ public class BluetoothDeviceNamePreferenceController extends BasePreferenceContr @VisibleForTesting Preference mPreference; - private LocalBluetoothManager mLocalManager; - protected LocalBluetoothAdapter mLocalAdapter; + protected BluetoothAdapter mBluetoothAdapter; /** * Constructor exclusively used for Slice. @@ -54,19 +52,11 @@ public class BluetoothDeviceNamePreferenceController extends BasePreferenceContr public BluetoothDeviceNamePreferenceController(Context context, String preferenceKey) { super(context, preferenceKey); - mLocalManager = Utils.getLocalBtManager(context); - if (mLocalManager == null) { + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + if (mBluetoothAdapter == null) { Log.e(TAG, "Bluetooth is not supported on this device"); return; } - mLocalAdapter = mLocalManager.getBluetoothAdapter(); - } - - @VisibleForTesting - BluetoothDeviceNamePreferenceController(Context context, LocalBluetoothAdapter localAdapter, - String preferenceKey) { - super(context, preferenceKey); - mLocalAdapter = localAdapter; } @Override @@ -90,7 +80,7 @@ public class BluetoothDeviceNamePreferenceController extends BasePreferenceContr @Override public int getAvailabilityStatus() { - return mLocalAdapter != null ? AVAILABLE : UNSUPPORTED_ON_DEVICE; + return mBluetoothAdapter != null ? AVAILABLE : UNSUPPORTED_ON_DEVICE; } @Override @@ -137,7 +127,7 @@ public class BluetoothDeviceNamePreferenceController extends BasePreferenceContr } protected String getDeviceName() { - return mLocalAdapter.getName(); + return mBluetoothAdapter.getName(); } /** @@ -151,7 +141,8 @@ public class BluetoothDeviceNamePreferenceController extends BasePreferenceContr final String action = intent.getAction(); if (TextUtils.equals(action, BluetoothAdapter.ACTION_LOCAL_NAME_CHANGED)) { - if (mPreference != null && mLocalAdapter != null && mLocalAdapter.isEnabled()) { + if (mPreference != null && mBluetoothAdapter != null + && mBluetoothAdapter.isEnabled()) { updatePreferenceState(mPreference); } } else if (TextUtils.equals(action, BluetoothAdapter.ACTION_STATE_CHANGED)) { diff --git a/src/com/android/settings/bluetooth/BluetoothDevicePreference.java b/src/com/android/settings/bluetooth/BluetoothDevicePreference.java index 39a14add55..74d3b6a059 100644 --- a/src/com/android/settings/bluetooth/BluetoothDevicePreference.java +++ b/src/com/android/settings/bluetooth/BluetoothDevicePreference.java @@ -16,30 +16,34 @@ package com.android.settings.bluetooth; -import android.app.AlertDialog; +import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH; + +import android.app.settings.SettingsEnums; import android.bluetooth.BluetoothDevice; import android.content.Context; import android.content.DialogInterface; import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.os.UserManager; -import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; import android.text.Html; import android.text.TextUtils; import android.util.Pair; import android.util.TypedValue; +import android.view.View; import android.widget.ImageView; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + import com.android.settings.R; import com.android.settings.overlay.FeatureFactory; import com.android.settings.widget.GearPreference; +import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; -import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH; - /** * BluetoothDevicePreference is the preference type used to display each remote * Bluetooth device in the Bluetooth Settings screen. @@ -57,6 +61,8 @@ public final class BluetoothDevicePreference extends GearPreference implements private AlertDialog mDisconnectDialog; private String contentDescription = null; private boolean mHideSecondTarget = false; + @VisibleForTesting + boolean mNeedNotifyHierarchyChanged = false; /* Talk-back descriptions for various BT icons */ Resources mResources; @@ -79,8 +85,8 @@ public final class BluetoothDevicePreference extends GearPreference implements onDeviceAttributesChanged(); } - void rebind() { - notifyChanged(); + public void setNeedNotifyHierarchyChanged(boolean needNotifyHierarchyChanged) { + mNeedNotifyHierarchyChanged = needNotifyHierarchyChanged; } @Override @@ -128,8 +134,8 @@ public final class BluetoothDevicePreference extends GearPreference implements // Null check is done at the framework setSummary(mCachedDevice.getConnectionSummary()); - final Pair<Drawable, String> pair = com.android.settingslib.bluetooth.Utils - .getBtClassDrawableWithDescription(getContext(), mCachedDevice); + final Pair<Drawable, String> pair = + BluetoothUtils.getBtRainbowDrawableWithDescription(getContext(), mCachedDevice); if (pair.first != null) { setIcon(pair.first); contentDescription = pair.second; @@ -143,7 +149,9 @@ public final class BluetoothDevicePreference extends GearPreference implements setVisible(mShowDevicesWithoutNames || mCachedDevice.hasHumanReadableName()); // This could affect ordering, so notify that - notifyHierarchyChanged(); + if (mNeedNotifyHierarchyChanged) { + notifyHierarchyChanged(); + } } @Override @@ -163,6 +171,10 @@ public final class BluetoothDevicePreference extends GearPreference implements final ImageView imageView = (ImageView) view.findViewById(android.R.id.icon); if (imageView != null) { imageView.setContentDescription(contentDescription); + // Set property to prevent Talkback from reading out. + imageView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); + imageView.setElevation( + getContext().getResources().getDimension(R.dimen.bt_icon_elevation)); } super.onBindViewHolder(view); } @@ -201,18 +213,18 @@ public final class BluetoothDevicePreference extends GearPreference implements if (mCachedDevice.isConnected()) { metricsFeatureProvider.action(context, - MetricsEvent.ACTION_SETTINGS_BLUETOOTH_DISCONNECT); + SettingsEnums.ACTION_SETTINGS_BLUETOOTH_DISCONNECT); askDisconnect(); } else if (bondState == BluetoothDevice.BOND_BONDED) { metricsFeatureProvider.action(context, - MetricsEvent.ACTION_SETTINGS_BLUETOOTH_CONNECT); + SettingsEnums.ACTION_SETTINGS_BLUETOOTH_CONNECT); mCachedDevice.connect(true); } else if (bondState == BluetoothDevice.BOND_NONE) { metricsFeatureProvider.action(context, - MetricsEvent.ACTION_SETTINGS_BLUETOOTH_PAIR); + SettingsEnums.ACTION_SETTINGS_BLUETOOTH_PAIR); if (!mCachedDevice.hasHumanReadableName()) { metricsFeatureProvider.action(context, - MetricsEvent.ACTION_SETTINGS_BLUETOOTH_PAIR_DEVICES_WITHOUT_NAMES); + SettingsEnums.ACTION_SETTINGS_BLUETOOTH_PAIR_DEVICES_WITHOUT_NAMES); } pair(); } @@ -244,5 +256,4 @@ public final class BluetoothDevicePreference extends GearPreference implements R.string.bluetooth_pairing_error_message); } } - } diff --git a/src/com/android/settings/bluetooth/BluetoothDeviceRenamePreferenceController.java b/src/com/android/settings/bluetooth/BluetoothDeviceRenamePreferenceController.java index 3b07bd3cd2..d42a1bed28 100644 --- a/src/com/android/settings/bluetooth/BluetoothDeviceRenamePreferenceController.java +++ b/src/com/android/settings/bluetooth/BluetoothDeviceRenamePreferenceController.java @@ -16,15 +16,15 @@ package com.android.settings.bluetooth; -import android.app.Fragment; +import android.app.settings.SettingsEnums; import android.content.Context; +import android.text.TextUtils; + import androidx.annotation.VisibleForTesting; +import androidx.fragment.app.Fragment; import androidx.preference.Preference; -import android.text.TextUtils; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.overlay.FeatureFactory; -import com.android.settingslib.bluetooth.LocalBluetoothAdapter; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; public class BluetoothDeviceRenamePreferenceController extends @@ -41,13 +41,6 @@ public class BluetoothDeviceRenamePreferenceController extends mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider(); } - @VisibleForTesting - BluetoothDeviceRenamePreferenceController(Context context, LocalBluetoothAdapter localAdapter, - String preferenceKey) { - super(context, localAdapter, preferenceKey); - mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider(); - } - /** * Set the {@link Fragment} that used to show {@link LocalDeviceNameDialogFragment} * in {@code handlePreferenceTreeClick} @@ -60,7 +53,7 @@ public class BluetoothDeviceRenamePreferenceController extends @Override protected void updatePreferenceState(final Preference preference) { preference.setSummary(getSummary()); - preference.setVisible(mLocalAdapter != null && mLocalAdapter.isEnabled()); + preference.setVisible(mBluetoothAdapter != null && mBluetoothAdapter.isEnabled()); } @Override @@ -72,8 +65,8 @@ public class BluetoothDeviceRenamePreferenceController extends public boolean handlePreferenceTreeClick(Preference preference) { if (TextUtils.equals(getPreferenceKey(), preference.getKey()) && mFragment != null) { mMetricsFeatureProvider.action(mContext, - MetricsProto.MetricsEvent.ACTION_BLUETOOTH_RENAME); - LocalDeviceNameDialogFragment.newInstance() + SettingsEnums.ACTION_BLUETOOTH_RENAME); + new LocalDeviceNameDialogFragment() .show(mFragment.getFragmentManager(), LocalDeviceNameDialogFragment.TAG); return true; } diff --git a/src/com/android/settings/bluetooth/BluetoothDeviceUpdater.java b/src/com/android/settings/bluetooth/BluetoothDeviceUpdater.java index 71335c7fe7..31055cca05 100644 --- a/src/com/android/settings/bluetooth/BluetoothDeviceUpdater.java +++ b/src/com/android/settings/bluetooth/BluetoothDeviceUpdater.java @@ -15,13 +15,14 @@ */ package com.android.settings.bluetooth; +import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.content.Context; import android.os.Bundle; -import android.os.SystemProperties; +import android.util.Log; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; -import android.util.Log; import com.android.settings.R; import com.android.settings.connecteddevice.DevicePreferenceCallback; @@ -49,17 +50,15 @@ import java.util.Map; public abstract class BluetoothDeviceUpdater implements BluetoothCallback, LocalBluetoothProfileManager.ServiceListener { private static final String TAG = "BluetoothDeviceUpdater"; - private static final String BLUETOOTH_SHOW_DEVICES_WITHOUT_NAMES_PROPERTY = - "persist.bluetooth.showdeviceswithoutnames"; + private static final boolean DBG = false; - protected final LocalBluetoothManager mLocalManager; protected final DevicePreferenceCallback mDevicePreferenceCallback; protected final Map<BluetoothDevice, Preference> mPreferenceMap; protected Context mPrefContext; protected DashboardFragment mFragment; + @VisibleForTesting + protected LocalBluetoothManager mLocalManager; - private final boolean mShowDeviceWithoutNames; - @VisibleForTesting final GearPreference.OnGearClickListener mDeviceProfilesListener = pref -> { launchDeviceDetails(pref); @@ -75,8 +74,6 @@ public abstract class BluetoothDeviceUpdater implements BluetoothCallback, DevicePreferenceCallback devicePreferenceCallback, LocalBluetoothManager localManager) { mFragment = fragment; mDevicePreferenceCallback = devicePreferenceCallback; - mShowDeviceWithoutNames = SystemProperties.getBoolean( - BLUETOOTH_SHOW_DEVICES_WITHOUT_NAMES_PROPERTY, false); mPreferenceMap = new HashMap<>(); mLocalManager = localManager; } @@ -85,6 +82,10 @@ public abstract class BluetoothDeviceUpdater implements BluetoothCallback, * Register the bluetooth event callback and update the list */ public void registerCallback() { + if (mLocalManager == null) { + Log.e(TAG, "registerCallback() Bluetooth is not supported on this device"); + return; + } mLocalManager.setForegroundActivity(mFragment.getContext()); mLocalManager.getEventManager().registerCallback(this); mLocalManager.getProfileManager().addServiceListener(this); @@ -95,6 +96,10 @@ public abstract class BluetoothDeviceUpdater implements BluetoothCallback, * Unregister the bluetooth event callback */ public void unregisterCallback() { + if (mLocalManager == null) { + Log.e(TAG, "unregisterCallback() Bluetooth is not supported on this device"); + return; + } mLocalManager.setForegroundActivity(null); mLocalManager.getEventManager().unregisterCallback(this); mLocalManager.getProfileManager().removeServiceListener(this); @@ -104,22 +109,43 @@ public abstract class BluetoothDeviceUpdater implements BluetoothCallback, * Force to update the list of bluetooth devices */ public void forceUpdate() { - Collection<CachedBluetoothDevice> cachedDevices = + if (mLocalManager == null) { + Log.e(TAG, "forceUpdate() Bluetooth is not supported on this device"); + return; + } + if (BluetoothAdapter.getDefaultAdapter().isEnabled()) { + final Collection<CachedBluetoothDevice> cachedDevices = + mLocalManager.getCachedDeviceManager().getCachedDevicesCopy(); + for (CachedBluetoothDevice cachedBluetoothDevice : cachedDevices) { + update(cachedBluetoothDevice); + } + } else { + removeAllDevicesFromPreference(); + } + } + + public void removeAllDevicesFromPreference() { + if (mLocalManager == null) { + Log.e(TAG, "removeAllDevicesFromPreference() BT is not supported on this device"); + return; + } + final Collection<CachedBluetoothDevice> cachedDevices = mLocalManager.getCachedDeviceManager().getCachedDevicesCopy(); for (CachedBluetoothDevice cachedBluetoothDevice : cachedDevices) { - update(cachedBluetoothDevice); + removePreference(cachedBluetoothDevice); } } @Override public void onBluetoothStateChanged(int bluetoothState) { - forceUpdate(); + if (BluetoothAdapter.STATE_ON == bluetoothState) { + forceUpdate(); + } else if (BluetoothAdapter.STATE_OFF == bluetoothState) { + removeAllDevicesFromPreference(); + } } @Override - public void onScanningStateChanged(boolean started) {} - - @Override public void onDeviceAdded(CachedBluetoothDevice cachedDevice) { update(cachedDevice); } @@ -138,19 +164,22 @@ public abstract class BluetoothDeviceUpdater implements BluetoothCallback, } @Override - public void onConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) {} - - @Override - public void onActiveDeviceChanged(CachedBluetoothDevice activeDevice, int bluetoothProfile) { - } - - @Override - public void onAudioModeChanged() { + public void onProfileConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state, + int bluetoothProfile) { + if (DBG) { + Log.d(TAG, "onProfileConnectionStateChanged() device: " + cachedDevice.getName() + + ", state: " + state + ", bluetoothProfile: " + bluetoothProfile); + } + update(cachedDevice); } @Override - public void onProfileConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state, - int bluetoothProfile) { + public void onAclConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) { + if (DBG) { + Log.d(TAG, "onAclConnectionStateChanged() device: " + cachedDevice.getName() + + ", state: " + state); + } + update(cachedDevice); } @Override @@ -197,7 +226,7 @@ public abstract class BluetoothDeviceUpdater implements BluetoothCallback, if (!mPreferenceMap.containsKey(device)) { BluetoothDevicePreference btPreference = new BluetoothDevicePreference(mPrefContext, cachedDevice, - mShowDeviceWithoutNames); + true /* showDeviceWithoutNames */); btPreference.setOnGearClickListener(mDeviceProfilesListener); if (this instanceof Preference.OnPreferenceClickListener) { btPreference.setOnPreferenceClickListener( @@ -213,9 +242,19 @@ public abstract class BluetoothDeviceUpdater implements BluetoothCallback, */ protected void removePreference(CachedBluetoothDevice cachedDevice) { final BluetoothDevice device = cachedDevice.getDevice(); + final CachedBluetoothDevice subCachedDevice = cachedDevice.getSubDevice(); if (mPreferenceMap.containsKey(device)) { mDevicePreferenceCallback.onDeviceRemoved(mPreferenceMap.get(device)); mPreferenceMap.remove(device); + } else if (subCachedDevice != null) { + // When doing remove, to check if preference maps to sub device. + // This would happen when connection state is changed in detail page that there is no + // callback from SettingsLib. + final BluetoothDevice subDevice = subCachedDevice.getDevice(); + if (mPreferenceMap.containsKey(subDevice)) { + mDevicePreferenceCallback.onDeviceRemoved(mPreferenceMap.get(subDevice)); + mPreferenceMap.remove(subDevice); + } } } @@ -236,7 +275,7 @@ public abstract class BluetoothDeviceUpdater implements BluetoothCallback, new SubSettingLauncher(mFragment.getContext()) .setDestination(BluetoothDeviceDetailsFragment.class.getName()) .setArguments(args) - .setTitle(R.string.device_details_title) + .setTitleRes(R.string.device_details_title) .setSourceMetricsCategory(mFragment.getMetricsCategory()) .launch(); } @@ -250,6 +289,11 @@ public abstract class BluetoothDeviceUpdater implements BluetoothCallback, return false; } final BluetoothDevice device = cachedDevice.getDevice(); + if (DBG) { + Log.d(TAG, "isDeviceConnected() device name : " + cachedDevice.getName() + + ", is connected : " + device.isConnected() + " , is profile connected : " + + cachedDevice.isConnected()); + } return device.getBondState() == BluetoothDevice.BOND_BONDED && device.isConnected(); } } diff --git a/src/com/android/settings/bluetooth/BluetoothDiscoverableEnabler.java b/src/com/android/settings/bluetooth/BluetoothDiscoverableEnabler.java index 189df3a499..0a9003d4e7 100755 --- a/src/com/android/settings/bluetooth/BluetoothDiscoverableEnabler.java +++ b/src/com/android/settings/bluetooth/BluetoothDiscoverableEnabler.java @@ -24,12 +24,12 @@ import android.content.IntentFilter; import android.content.SharedPreferences; import android.os.Handler; import android.os.SystemProperties; -import androidx.preference.Preference; import android.util.Log; +import androidx.preference.Preference; + import com.android.settings.R; import com.android.settingslib.bluetooth.BluetoothDiscoverableTimeoutReceiver; -import com.android.settingslib.bluetooth.LocalBluetoothAdapter; /** * BluetoothDiscoverableEnabler is a helper to manage the "Discoverable" @@ -63,7 +63,7 @@ final class BluetoothDiscoverableEnabler implements Preference.OnPreferenceClick private final Handler mUiHandler; private final Preference mDiscoveryPreference; - private final LocalBluetoothAdapter mLocalAdapter; + private final BluetoothAdapter mBluetoothAdapter; private final SharedPreferences mSharedPreferences; @@ -91,17 +91,16 @@ final class BluetoothDiscoverableEnabler implements Preference.OnPreferenceClick } }; - BluetoothDiscoverableEnabler(LocalBluetoothAdapter adapter, - Preference discoveryPreference) { + BluetoothDiscoverableEnabler(Preference discoveryPreference) { mUiHandler = new Handler(); - mLocalAdapter = adapter; + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); mDiscoveryPreference = discoveryPreference; mSharedPreferences = discoveryPreference.getSharedPreferences(); discoveryPreference.setPersistent(false); } public void resume(Context context) { - if (mLocalAdapter == null) { + if (mBluetoothAdapter == null) { return; } @@ -112,11 +111,11 @@ final class BluetoothDiscoverableEnabler implements Preference.OnPreferenceClick IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED); mContext.registerReceiver(mReceiver, filter); mDiscoveryPreference.setOnPreferenceClickListener(this); - handleModeChanged(mLocalAdapter.getScanMode()); + handleModeChanged(mBluetoothAdapter.getScanMode()); } public void pause() { - if (mLocalAdapter == null) { + if (mBluetoothAdapter == null) { return; } @@ -138,7 +137,8 @@ final class BluetoothDiscoverableEnabler implements Preference.OnPreferenceClick long endTimestamp = System.currentTimeMillis() + timeout * 1000L; LocalBluetoothPreferences.persistDiscoverableEndTimestamp(mContext, endTimestamp); - mLocalAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE, timeout); + mBluetoothAdapter + .setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE, timeout); updateCountdownSummary(); Log.d(TAG, "setEnabled(): enabled = " + enable + "timeout = " + timeout); @@ -150,7 +150,7 @@ final class BluetoothDiscoverableEnabler implements Preference.OnPreferenceClick } } else { - mLocalAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE); + mBluetoothAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE); BluetoothDiscoverableTimeoutReceiver.cancelDiscoverableAlarm(mContext); } } @@ -249,7 +249,7 @@ final class BluetoothDiscoverableEnabler implements Preference.OnPreferenceClick void setNumberOfPairedDevices(int pairedDevices) { mNumberOfPairedDevices = pairedDevices; - handleModeChanged(mLocalAdapter.getScanMode()); + handleModeChanged(mBluetoothAdapter.getScanMode()); } void handleModeChanged(int mode) { @@ -272,7 +272,7 @@ final class BluetoothDiscoverableEnabler implements Preference.OnPreferenceClick } private void updateCountdownSummary() { - int mode = mLocalAdapter.getScanMode(); + int mode = mBluetoothAdapter.getScanMode(); if (mode != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) { return; } diff --git a/src/com/android/settings/bluetooth/BluetoothEnabler.java b/src/com/android/settings/bluetooth/BluetoothEnabler.java index c1bcf5076f..2da9eea431 100644 --- a/src/com/android/settings/bluetooth/BluetoothEnabler.java +++ b/src/com/android/settings/bluetooth/BluetoothEnabler.java @@ -25,13 +25,12 @@ import android.os.UserManager; import android.provider.Settings; import android.widget.Toast; -import com.android.internal.annotations.VisibleForTesting; +import androidx.annotation.VisibleForTesting; + import com.android.settings.R; import com.android.settings.widget.SwitchWidgetController; import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; import com.android.settingslib.WirelessUtils; -import com.android.settingslib.bluetooth.LocalBluetoothAdapter; -import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; /** @@ -44,7 +43,7 @@ public final class BluetoothEnabler implements SwitchWidgetController.OnSwitchCh private final MetricsFeatureProvider mMetricsFeatureProvider; private Context mContext; private boolean mValidListener; - private final LocalBluetoothAdapter mLocalAdapter; + private final BluetoothAdapter mBluetoothAdapter; private final IntentFilter mIntentFilter; private final RestrictionUtils mRestrictionUtils; private SwitchWidgetController.OnSwitchChangeListener mCallback; @@ -64,15 +63,14 @@ public final class BluetoothEnabler implements SwitchWidgetController.OnSwitchCh }; public BluetoothEnabler(Context context, SwitchWidgetController switchController, - MetricsFeatureProvider metricsFeatureProvider, LocalBluetoothManager manager, - int metricsEvent) { - this(context, switchController, metricsFeatureProvider, manager, metricsEvent, + MetricsFeatureProvider metricsFeatureProvider, int metricsEvent) { + this(context, switchController, metricsFeatureProvider, metricsEvent, new RestrictionUtils()); } public BluetoothEnabler(Context context, SwitchWidgetController switchController, - MetricsFeatureProvider metricsFeatureProvider, LocalBluetoothManager manager, - int metricsEvent, RestrictionUtils restrictionUtils) { + MetricsFeatureProvider metricsFeatureProvider, int metricsEvent, + RestrictionUtils restrictionUtils) { mContext = context; mMetricsFeatureProvider = metricsFeatureProvider; mSwitchController = switchController; @@ -80,12 +78,10 @@ public final class BluetoothEnabler implements SwitchWidgetController.OnSwitchCh mValidListener = false; mMetricsEvent = metricsEvent; - if (manager == null) { + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + if (mBluetoothAdapter == null) { // Bluetooth is not supported - mLocalAdapter = null; mSwitchController.setEnabled(false); - } else { - mLocalAdapter = manager.getBluetoothAdapter(); } mIntentFilter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED); mRestrictionUtils = restrictionUtils; @@ -106,14 +102,14 @@ public final class BluetoothEnabler implements SwitchWidgetController.OnSwitchCh final boolean restricted = maybeEnforceRestrictions(); - if (mLocalAdapter == null) { + if (mBluetoothAdapter == null) { mSwitchController.setEnabled(false); return; } // Bluetooth state is not sticky, so set it manually if (!restricted) { - handleStateChanged(mLocalAdapter.getBluetoothState()); + handleStateChanged(mBluetoothAdapter.getState()); } mSwitchController.startListening(); @@ -122,7 +118,7 @@ public final class BluetoothEnabler implements SwitchWidgetController.OnSwitchCh } public void pause() { - if (mLocalAdapter == null) { + if (mBluetoothAdapter == null) { return; } if (mValidListener) { @@ -187,8 +183,8 @@ public final class BluetoothEnabler implements SwitchWidgetController.OnSwitchCh mMetricsFeatureProvider.action(mContext, mMetricsEvent, isChecked); - if (mLocalAdapter != null) { - boolean status = mLocalAdapter.setBluetoothEnabled(isChecked); + if (mBluetoothAdapter != null) { + boolean status = setBluetoothEnabled(isChecked); // If we cannot toggle it ON then reset the UI assets: // a) The switch should be OFF but it should still be togglable (enabled = True) // b) The switch bar should have OFF text. @@ -248,4 +244,8 @@ public final class BluetoothEnabler implements SwitchWidgetController.OnSwitchCh mCallback.onSwitchToggled(isChecked); } } + + private boolean setBluetoothEnabled(boolean isEnabled) { + return isEnabled ? mBluetoothAdapter.enable() : mBluetoothAdapter.disable(); + } } diff --git a/src/com/android/settings/bluetooth/BluetoothFeatureProvider.java b/src/com/android/settings/bluetooth/BluetoothFeatureProvider.java index a6ae31c421..582a26c182 100644 --- a/src/com/android/settings/bluetooth/BluetoothFeatureProvider.java +++ b/src/com/android/settings/bluetooth/BluetoothFeatureProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 The Android Open Source Project + * Copyright (C) 2018 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. @@ -16,17 +16,18 @@ package com.android.settings.bluetooth; +import android.bluetooth.BluetoothDevice; +import android.net.Uri; + /** - * Feature provider for bluetooth feature + * Provider for bluetooth related feature */ public interface BluetoothFeatureProvider { - /** - * @return whether additional pairing page is enabled - */ - boolean isPairingPageEnabled(); /** - * @return whether device details should be shown as a separate page (true) or a dialog (false) + * Get the {@link Uri} that represents extra settings for a specific bluetooth device + * @param bluetoothDevice bluetooth device + * @return {@link Uri} for extra settings */ - boolean isDeviceDetailPageEnabled(); + Uri getBluetoothDeviceSettingsUri(BluetoothDevice bluetoothDevice); } diff --git a/src/com/android/settings/bluetooth/BluetoothFeatureProviderImpl.java b/src/com/android/settings/bluetooth/BluetoothFeatureProviderImpl.java index c4962d6482..cd75951a48 100644 --- a/src/com/android/settings/bluetooth/BluetoothFeatureProviderImpl.java +++ b/src/com/android/settings/bluetooth/BluetoothFeatureProviderImpl.java @@ -1,17 +1,40 @@ +/* + * Copyright (C) 2018 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.bluetooth; +import android.bluetooth.BluetoothDevice; +import android.content.Context; +import android.net.Uri; + /** - * Impl for bluetooth feature provider + * Impl of {@link BluetoothFeatureProvider} */ public class BluetoothFeatureProviderImpl implements BluetoothFeatureProvider { - @Override - public boolean isPairingPageEnabled() { - return false; + private Context mContext; + + public BluetoothFeatureProviderImpl(Context context) { + mContext = context; } @Override - public boolean isDeviceDetailPageEnabled() { - return false; + public Uri getBluetoothDeviceSettingsUri(BluetoothDevice bluetoothDevice) { + final byte[] uriByte = bluetoothDevice.getMetadata( + BluetoothDevice.METADATA_ENHANCED_SETTINGS_UI_URI); + return uriByte == null ? null : Uri.parse(new String(uriByte)); } -}
\ No newline at end of file +} diff --git a/src/com/android/settings/bluetooth/BluetoothFilesPreferenceController.java b/src/com/android/settings/bluetooth/BluetoothFilesPreferenceController.java index 8ce701748c..e96fba342a 100644 --- a/src/com/android/settings/bluetooth/BluetoothFilesPreferenceController.java +++ b/src/com/android/settings/bluetooth/BluetoothFilesPreferenceController.java @@ -16,13 +16,14 @@ package com.android.settings.bluetooth; -import android.content.pm.PackageManager; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.core.BasePreferenceController; import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.overlay.FeatureFactory; @@ -68,7 +69,7 @@ public class BluetoothFilesPreferenceController extends BasePreferenceController public boolean handlePreferenceTreeClick(Preference preference) { if (KEY_RECEIVED_FILES.equals(preference.getKey())) { mMetricsFeatureProvider.action(mContext, - MetricsProto.MetricsEvent.ACTION_BLUETOOTH_FILES); + SettingsEnums.ACTION_BLUETOOTH_FILES); Intent intent = new Intent(ACTION_OPEN_FILES); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); intent.putExtra(EXTRA_DIRECTION, 1 /* DIRECTION_INBOUND */); diff --git a/src/com/android/settings/bluetooth/BluetoothNameDialogFragment.java b/src/com/android/settings/bluetooth/BluetoothNameDialogFragment.java index 85abadeddc..74c39b6f8c 100644 --- a/src/com/android/settings/bluetooth/BluetoothNameDialogFragment.java +++ b/src/com/android/settings/bluetooth/BluetoothNameDialogFragment.java @@ -16,7 +16,6 @@ package com.android.settings.bluetooth; -import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; @@ -29,13 +28,15 @@ import android.text.TextWatcher; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; -import android.view.WindowManager; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; + import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; @@ -43,8 +44,14 @@ import com.android.settings.core.instrumentation.InstrumentedDialogFragment; * Dialog fragment for renaming a Bluetooth device. */ abstract class BluetoothNameDialogFragment extends InstrumentedDialogFragment - implements TextWatcher { - private AlertDialog mAlertDialog; + implements TextWatcher, TextView.OnEditorActionListener { + + // Key to save the edited name and edit status for restoring after rotation + private static final String KEY_NAME = "device_name"; + private static final String KEY_NAME_EDITED = "device_name_edited"; + + @VisibleForTesting + AlertDialog mAlertDialog; private Button mOkButton; EditText mDeviceNameView; @@ -55,10 +62,6 @@ abstract class BluetoothNameDialogFragment extends InstrumentedDialogFragment // This flag is set when the user edits the name (preserved on rotation) private boolean mDeviceNameEdited; - // Key to save the edited name and edit status for restoring after rotation - private static final String KEY_NAME = "device_name"; - private static final String KEY_NAME_EDITED = "device_name_edited"; - /** * @return the title to use for the dialog. */ @@ -123,22 +126,24 @@ abstract class BluetoothNameDialogFragment extends InstrumentedDialogFragment } mDeviceNameView.addTextChangedListener(this); com.android.settings.Utils.setEditTextCursorPosition(mDeviceNameView); - mDeviceNameView.setOnEditorActionListener(new TextView.OnEditorActionListener() { - @Override - public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { - if (actionId == EditorInfo.IME_ACTION_DONE) { - setDeviceName(v.getText().toString()); - mAlertDialog.dismiss(); - return true; // action handled - } else { - return false; // not handled - } - } - }); + mDeviceNameView.setOnEditorActionListener(this); return view; } @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (actionId == EditorInfo.IME_ACTION_DONE) { + setDeviceName(v.getText().toString()); + if (mAlertDialog != null && mAlertDialog.isShowing()) { + mAlertDialog.dismiss(); + } + return true; // action handled + } else { + return false; // not handled + } + } + + @Override public void onDestroy() { super.onDestroy(); mAlertDialog = null; diff --git a/src/com/android/settings/bluetooth/BluetoothPairingController.java b/src/com/android/settings/bluetooth/BluetoothPairingController.java index c39f1d9feb..94bdfe8f75 100644 --- a/src/com/android/settings/bluetooth/BluetoothPairingController.java +++ b/src/com/android/settings/bluetooth/BluetoothPairingController.java @@ -54,8 +54,7 @@ public class BluetoothPairingController implements OnCheckedChangeListener, // Bluetooth dependencies for the connection we are trying to establish private LocalBluetoothManager mBluetoothManager; - @VisibleForTesting - BluetoothDevice mDevice; + private BluetoothDevice mDevice; @VisibleForTesting int mType; private String mUserInput; @@ -189,16 +188,16 @@ public class BluetoothPairingController implements OnCheckedChangeListener, * */ public void setContactSharingState() { - if ((mDevice.getPhonebookAccessPermission() != BluetoothDevice.ACCESS_ALLOWED) - && (mDevice.getPhonebookAccessPermission() != BluetoothDevice.ACCESS_REJECTED)) { - if (mDevice.getBluetoothClass().getDeviceClass() - == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE) { - onCheckedChanged(null, true); - } else { - onCheckedChanged(null, false); - } - } - } + final int permission = mDevice.getPhonebookAccessPermission(); + if (permission == BluetoothDevice.ACCESS_ALLOWED + || (permission == BluetoothDevice.ACCESS_UNKNOWN && mDevice.getBluetoothClass(). + getDeviceClass() == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE)) { + onCheckedChanged(null, true); + } else { + onCheckedChanged(null, false); + } + + } /** * A method for querying if the provided editable is a valid passkey/pin format for this device. diff --git a/src/com/android/settings/bluetooth/BluetoothPairingDetail.java b/src/com/android/settings/bluetooth/BluetoothPairingDetail.java index b3f60967c4..b1a31748ba 100644 --- a/src/com/android/settings/bluetooth/BluetoothPairingDetail.java +++ b/src/com/android/settings/bluetooth/BluetoothPairingDetail.java @@ -18,14 +18,16 @@ package com.android.settings.bluetooth; import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH; +import android.app.settings.SettingsEnums; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.content.Context; import android.os.Bundle; -import androidx.annotation.VisibleForTesting; import android.util.Log; +import android.widget.Toast; + +import androidx.annotation.VisibleForTesting; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.search.Indexable; import com.android.settingslib.bluetooth.BluetoothDeviceFilter; @@ -61,7 +63,7 @@ public class BluetoothPairingDetail extends DeviceListPreferenceFragment impleme public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); mInitialScanStarted = false; - mAlwaysDiscoverable = new AlwaysDiscoverable(getContext(), mLocalAdapter); + mAlwaysDiscoverable = new AlwaysDiscoverable(getContext()); } @Override @@ -72,7 +74,7 @@ public class BluetoothPairingDetail extends DeviceListPreferenceFragment impleme return; } updateBluetooth(); - mAvailableDevicesCategory.setProgress(mLocalAdapter.isDiscovering()); + mAvailableDevicesCategory.setProgress(mBluetoothAdapter.isDiscovering()); } @Override @@ -83,11 +85,11 @@ public class BluetoothPairingDetail extends DeviceListPreferenceFragment impleme @VisibleForTesting void updateBluetooth() { - if (mLocalAdapter.isEnabled()) { - updateContent(mLocalAdapter.getBluetoothState()); + if (mBluetoothAdapter.isEnabled()) { + updateContent(mBluetoothAdapter.getState()); } else { // Turn on bluetooth if it is disabled - mLocalAdapter.enable(); + mBluetoothAdapter.enable(); } } @@ -112,7 +114,7 @@ public class BluetoothPairingDetail extends DeviceListPreferenceFragment impleme @Override public int getMetricsCategory() { - return MetricsEvent.BLUETOOTH_PAIRING; + return SettingsEnums.BLUETOOTH_PAIRING; } @Override @@ -146,11 +148,11 @@ public class BluetoothPairingDetail extends DeviceListPreferenceFragment impleme switch (bluetoothState) { case BluetoothAdapter.STATE_ON: mDevicePreferenceMap.clear(); - mLocalAdapter.setBluetoothEnabled(true); + mBluetoothAdapter.enable(); addDeviceCategory(mAvailableDevicesCategory, R.string.bluetooth_preference_found_media_devices, - BluetoothDeviceFilter.UNBONDED_DEVICE_FILTER, mInitialScanStarted); + BluetoothDeviceFilter.ALL_FILTER, mInitialScanStarted); updateFooterPreference(mFooterPreference); mAlwaysDiscoverable.start(); enableScanning(); @@ -166,6 +168,9 @@ public class BluetoothPairingDetail extends DeviceListPreferenceFragment impleme public void onBluetoothStateChanged(int bluetoothState) { super.onBluetoothStateChanged(bluetoothState); updateContent(bluetoothState); + if (bluetoothState == BluetoothAdapter.STATE_ON) { + showBluetoothTurnedOnToast(); + } } @Override @@ -186,6 +191,17 @@ public class BluetoothPairingDetail extends DeviceListPreferenceFragment impleme } @Override + public void onConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) { + if (mSelectedDevice != null) { + BluetoothDevice device = cachedDevice.getDevice(); + if (device != null && mSelectedDevice.equals(device) + && state == BluetoothAdapter.STATE_CONNECTED) { + finish(); + } + } + } + + @Override public int getHelpResource() { return R.string.help_url_bluetooth; } @@ -205,4 +221,9 @@ public class BluetoothPairingDetail extends DeviceListPreferenceFragment impleme return KEY_AVAIL_DEVICES; } + @VisibleForTesting + void showBluetoothTurnedOnToast() { + Toast.makeText(getContext(), R.string.connected_device_bluetooth_turned_on_toast, + Toast.LENGTH_SHORT).show(); + } } diff --git a/src/com/android/settings/bluetooth/BluetoothPairingDialog.java b/src/com/android/settings/bluetooth/BluetoothPairingDialog.java index f1960e8b18..060c17491a 100644 --- a/src/com/android/settings/bluetooth/BluetoothPairingDialog.java +++ b/src/com/android/settings/bluetooth/BluetoothPairingDialog.java @@ -17,20 +17,21 @@ package com.android.settings.bluetooth; import android.annotation.Nullable; -import android.app.Activity; import android.bluetooth.BluetoothDevice; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; + import androidx.annotation.VisibleForTesting; +import androidx.fragment.app.FragmentActivity; /** * BluetoothPairingDialog asks the user to enter a PIN / Passkey / simple confirmation * for pairing with a remote Bluetooth device. It is an activity that appears as a dialog. */ -public class BluetoothPairingDialog extends Activity { +public class BluetoothPairingDialog extends FragmentActivity { public static final String FRAGMENT_TAG = "bluetooth.pairing.fragment"; private BluetoothPairingController mBluetoothPairingController; @@ -69,7 +70,8 @@ public class BluetoothPairingDialog extends Activity { boolean fragmentFound = true; // check if the fragment has been preloaded BluetoothPairingDialogFragment bluetoothFragment = - (BluetoothPairingDialogFragment) getFragmentManager().findFragmentByTag(FRAGMENT_TAG); + (BluetoothPairingDialogFragment) getSupportFragmentManager(). + findFragmentByTag(FRAGMENT_TAG); // dismiss the fragment if it is already used if (bluetoothFragment != null && (bluetoothFragment.isPairingControllerSet() || bluetoothFragment.isPairingDialogActivitySet())) { @@ -85,7 +87,7 @@ public class BluetoothPairingDialog extends Activity { bluetoothFragment.setPairingDialogActivity(this); // pass the fragment to the manager when it is created from scratch if (!fragmentFound) { - bluetoothFragment.show(getFragmentManager(), FRAGMENT_TAG); + bluetoothFragment.show(getSupportFragmentManager(), FRAGMENT_TAG); } /* * Leave this registered through pause/resume since we still want to diff --git a/src/com/android/settings/bluetooth/BluetoothPairingDialogFragment.java b/src/com/android/settings/bluetooth/BluetoothPairingDialogFragment.java index 18839dc965..d38302d883 100644 --- a/src/com/android/settings/bluetooth/BluetoothPairingDialogFragment.java +++ b/src/com/android/settings/bluetooth/BluetoothPairingDialogFragment.java @@ -15,8 +15,8 @@ */ package com.android.settings.bluetooth; -import android.app.AlertDialog; import android.app.Dialog; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; @@ -35,8 +35,9 @@ import android.widget.CheckBox; import android.widget.EditText; import android.widget.TextView; -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; + import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; @@ -112,7 +113,7 @@ public class BluetoothPairingDialogFragment extends InstrumentedDialogFragment i @Override public int getMetricsCategory() { - return MetricsEvent.BLUETOOTH_DIALOG_FRAGMENT; + return SettingsEnums.BLUETOOTH_DIALOG_FRAGMENT; } /** diff --git a/src/com/android/settings/bluetooth/BluetoothPairingPreferenceController.java b/src/com/android/settings/bluetooth/BluetoothPairingPreferenceController.java index 3d8d038ddf..c835586068 100644 --- a/src/com/android/settings/bluetooth/BluetoothPairingPreferenceController.java +++ b/src/com/android/settings/bluetooth/BluetoothPairingPreferenceController.java @@ -17,6 +17,7 @@ package com.android.settings.bluetooth; import android.content.Context; + import androidx.preference.Preference; import com.android.settings.R; @@ -57,7 +58,7 @@ public class BluetoothPairingPreferenceController extends AbstractPreferenceCont if (KEY_PAIRING.equals(preference.getKey())) { new SubSettingLauncher(mContext) .setDestination(BluetoothPairingDetail.class.getName()) - .setTitle(R.string.bluetooth_pairing_page_title) + .setTitleRes(R.string.bluetooth_pairing_page_title) .setSourceMetricsCategory(mFragment.getMetricsCategory()) .launch(); @@ -75,7 +76,7 @@ public class BluetoothPairingPreferenceController extends AbstractPreferenceCont public Preference createBluetoothPairingPreference(int order) { mPreference = new Preference(mFragment.getPreferenceScreen().getContext()); mPreference.setKey(KEY_PAIRING); - mPreference.setIcon(R.drawable.ic_menu_add); + mPreference.setIcon(R.drawable.ic_add_24dp); mPreference.setOrder(order); mPreference.setTitle(R.string.bluetooth_pairing_pref_title); diff --git a/src/com/android/settings/bluetooth/BluetoothPairingRequest.java b/src/com/android/settings/bluetooth/BluetoothPairingRequest.java index 4d02fd50c3..7da63428c7 100644 --- a/src/com/android/settings/bluetooth/BluetoothPairingRequest.java +++ b/src/com/android/settings/bluetooth/BluetoothPairingRequest.java @@ -20,7 +20,6 @@ import android.bluetooth.BluetoothDevice; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import android.content.res.Resources; import android.os.PowerManager; import android.os.UserHandle; diff --git a/src/com/android/settings/bluetooth/BluetoothPairingService.java b/src/com/android/settings/bluetooth/BluetoothPairingService.java index 08577192b7..48e9eeead2 100644 --- a/src/com/android/settings/bluetooth/BluetoothPairingService.java +++ b/src/com/android/settings/bluetooth/BluetoothPairingService.java @@ -17,15 +17,15 @@ package com.android.settings.bluetooth; import android.app.Notification; -import android.app.NotificationManager; import android.app.NotificationChannel; +import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.bluetooth.BluetoothDevice; -import android.content.IntentFilter; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.res.Resources; import android.os.IBinder; import android.text.TextUtils; diff --git a/src/com/android/settings/bluetooth/BluetoothPermissionActivity.java b/src/com/android/settings/bluetooth/BluetoothPermissionActivity.java index 21e8fb8c93..4dd9a4042e 100644 --- a/src/com/android/settings/bluetooth/BluetoothPermissionActivity.java +++ b/src/com/android/settings/bluetooth/BluetoothPermissionActivity.java @@ -23,18 +23,16 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; -import androidx.preference.Preference; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.TextView; +import androidx.preference.Preference; + import com.android.internal.app.AlertActivity; import com.android.internal.app.AlertController; import com.android.settings.R; -import com.android.settingslib.bluetooth.CachedBluetoothDevice; -import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; -import com.android.settingslib.bluetooth.LocalBluetoothManager; /** * BluetoothPermissionActivity shows a dialog for accepting incoming @@ -198,22 +196,7 @@ public class BluetoothPermissionActivity extends AlertActivity implements private void onNegative() { if (DEBUG) Log.d(TAG, "onNegative"); - - boolean always = true; - if (mRequestType == BluetoothDevice.REQUEST_TYPE_MESSAGE_ACCESS) { - LocalBluetoothManager bluetoothManager = Utils.getLocalBtManager(this); - CachedBluetoothDeviceManager cachedDeviceManager = - bluetoothManager.getCachedDeviceManager(); - CachedBluetoothDevice cachedDevice = cachedDeviceManager.findDevice(mDevice); - if (cachedDevice == null) { - cachedDevice = cachedDeviceManager.addDevice(bluetoothManager.getBluetoothAdapter(), - bluetoothManager.getProfileManager(), - mDevice); - } - always = cachedDevice.checkAndIncreaseMessageRejectionCount(); - } - - sendReplyIntentToReceiver(false, always); + sendReplyIntentToReceiver(false, true); } private void sendReplyIntentToReceiver(final boolean allowed, final boolean always) { diff --git a/src/com/android/settings/bluetooth/BluetoothPermissionRequest.java b/src/com/android/settings/bluetooth/BluetoothPermissionRequest.java index 85c5e45ec0..c5f62b8b4d 100644 --- a/src/com/android/settings/bluetooth/BluetoothPermissionRequest.java +++ b/src/com/android/settings/bluetooth/BluetoothPermissionRequest.java @@ -231,49 +231,48 @@ public final class BluetoothPermissionRequest extends BroadcastReceiver { bluetoothManager.getCachedDeviceManager(); CachedBluetoothDevice cachedDevice = cachedDeviceManager.findDevice(mDevice); if (cachedDevice == null) { - cachedDevice = cachedDeviceManager.addDevice(bluetoothManager.getBluetoothAdapter(), - bluetoothManager.getProfileManager(), mDevice); + cachedDevice = cachedDeviceManager.addDevice(mDevice); } String intentName = BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY; if (mRequestType == BluetoothDevice.REQUEST_TYPE_PHONEBOOK_ACCESS) { - int phonebookPermission = cachedDevice.getPhonebookPermissionChoice(); + int phonebookPermission = mDevice.getPhonebookAccessPermission(); - if (phonebookPermission == CachedBluetoothDevice.ACCESS_UNKNOWN) { + if (phonebookPermission == BluetoothDevice.ACCESS_UNKNOWN) { // Leave 'processed' as false. - } else if (phonebookPermission == CachedBluetoothDevice.ACCESS_ALLOWED) { + } else if (phonebookPermission == BluetoothDevice.ACCESS_ALLOWED) { sendReplyIntentToReceiver(true); processed = true; - } else if (phonebookPermission == CachedBluetoothDevice.ACCESS_REJECTED) { + } else if (phonebookPermission == BluetoothDevice.ACCESS_REJECTED) { sendReplyIntentToReceiver(false); processed = true; } else { Log.e(TAG, "Bad phonebookPermission: " + phonebookPermission); } } else if (mRequestType == BluetoothDevice.REQUEST_TYPE_MESSAGE_ACCESS) { - int messagePermission = cachedDevice.getMessagePermissionChoice(); + int messagePermission = mDevice.getMessageAccessPermission(); - if (messagePermission == CachedBluetoothDevice.ACCESS_UNKNOWN) { + if (messagePermission == BluetoothDevice.ACCESS_UNKNOWN) { // Leave 'processed' as false. - } else if (messagePermission == CachedBluetoothDevice.ACCESS_ALLOWED) { + } else if (messagePermission == BluetoothDevice.ACCESS_ALLOWED) { sendReplyIntentToReceiver(true); processed = true; - } else if (messagePermission == CachedBluetoothDevice.ACCESS_REJECTED) { + } else if (messagePermission == BluetoothDevice.ACCESS_REJECTED) { sendReplyIntentToReceiver(false); processed = true; } else { Log.e(TAG, "Bad messagePermission: " + messagePermission); } } else if(mRequestType == BluetoothDevice.REQUEST_TYPE_SIM_ACCESS) { - int simPermission = cachedDevice.getSimPermissionChoice(); + int simPermission = mDevice.getSimAccessPermission(); - if (simPermission == CachedBluetoothDevice.ACCESS_UNKNOWN) { + if (simPermission == BluetoothDevice.ACCESS_UNKNOWN) { // Leave 'processed' as false. - } else if (simPermission == CachedBluetoothDevice.ACCESS_ALLOWED) { + } else if (simPermission == BluetoothDevice.ACCESS_ALLOWED) { sendReplyIntentToReceiver(true); processed = true; - } else if (simPermission == CachedBluetoothDevice.ACCESS_REJECTED) { + } else if (simPermission == BluetoothDevice.ACCESS_REJECTED) { sendReplyIntentToReceiver(false); processed = true; } else { diff --git a/src/com/android/settings/bluetooth/BluetoothSliceBuilder.java b/src/com/android/settings/bluetooth/BluetoothSliceBuilder.java index 7a731ec203..18df872a28 100644 --- a/src/com/android/settings/bluetooth/BluetoothSliceBuilder.java +++ b/src/com/android/settings/bluetooth/BluetoothSliceBuilder.java @@ -19,43 +19,35 @@ import static android.app.slice.Slice.EXTRA_TOGGLE_STATE; import android.annotation.ColorInt; import android.app.PendingIntent; +import android.app.settings.SettingsEnums; import android.bluetooth.BluetoothAdapter; -import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.Uri; import android.provider.SettingsSlicesContract; + import androidx.core.graphics.drawable.IconCompat; +import androidx.slice.Slice; +import androidx.slice.builders.ListBuilder; +import androidx.slice.builders.ListBuilder.RowBuilder; +import androidx.slice.builders.SliceAction; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.SubSettings; import com.android.settings.connecteddevice.BluetoothDashboardFragment; -import com.android.settings.search.DatabaseIndexingUtils; +import com.android.settings.slices.CustomSliceRegistry; import com.android.settings.slices.SliceBroadcastReceiver; -import com.android.settingslib.bluetooth.LocalBluetoothAdapter; -import com.android.settingslib.bluetooth.LocalBluetoothManager; - -import androidx.slice.Slice; -import androidx.slice.builders.ListBuilder; -import androidx.slice.builders.SliceAction; +import com.android.settings.slices.SliceBuilderUtils; +/** + * Utility class to build a Bluetooth Slice, and handle all associated actions. + */ public class BluetoothSliceBuilder { private static final String TAG = "BluetoothSliceBuilder"; /** - * Backing Uri for the Bluetooth Slice. - */ - public static final Uri BLUETOOTH_URI = new Uri.Builder() - .scheme(ContentResolver.SCHEME_CONTENT) - .authority(SettingsSlicesContract.AUTHORITY) - .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) - .appendPath(SettingsSlicesContract.KEY_BLUETOOTH) - .build(); - - /** * Action notifying a change on the BluetoothSlice. */ public static final String ACTION_BLUETOOTH_SLICE_CHANGED = @@ -72,7 +64,7 @@ public class BluetoothSliceBuilder { } /** - * Return a Bluetooth Slice bound to {@link #BLUETOOTH_URI}. + * Return a Bluetooth Slice bound to {@link CustomSliceRegistry#BLUETOOTH_URI}. * <p> * Note that you should register a listener for {@link #INTENT_FILTER} to get changes for * Bluetooth. @@ -81,17 +73,19 @@ public class BluetoothSliceBuilder { final boolean isBluetoothEnabled = isBluetoothEnabled(); final CharSequence title = context.getText(R.string.bluetooth_settings); final IconCompat icon = IconCompat.createWithResource(context, - R.drawable.ic_settings_bluetooth); - @ColorInt final int color = com.android.settings.Utils.getColorAccent(context); + com.android.internal.R.drawable.ic_settings_bluetooth); + @ColorInt final int color = com.android.settings.Utils.getColorAccent( + context).getDefaultColor(); final PendingIntent toggleAction = getBroadcastIntent(context); final PendingIntent primaryAction = getPrimaryAction(context); - final SliceAction primarySliceAction = new SliceAction(primaryAction, icon, title); - final SliceAction toggleSliceAction = new SliceAction(toggleAction, null /* actionTitle */, - isBluetoothEnabled); + final SliceAction primarySliceAction = SliceAction.createDeeplink(primaryAction, icon, + ListBuilder.ICON_IMAGE, title); + final SliceAction toggleSliceAction = SliceAction.createToggle(toggleAction, + null /* actionTitle */, isBluetoothEnabled); - return new ListBuilder(context, BLUETOOTH_URI, ListBuilder.INFINITY) + return new ListBuilder(context, CustomSliceRegistry.BLUETOOTH_URI, ListBuilder.INFINITY) .setAccentColor(color) - .addRow(b -> b + .addRow(new RowBuilder() .setTitle(title) .addEndItem(toggleSliceAction) .setPrimaryAction(primarySliceAction)) @@ -102,9 +96,9 @@ public class BluetoothSliceBuilder { final String screenTitle = context.getText(R.string.bluetooth_settings_title).toString(); final Uri contentUri = new Uri.Builder().appendPath( SettingsSlicesContract.KEY_BLUETOOTH).build(); - return DatabaseIndexingUtils.buildSearchResultPageIntent(context, + return SliceBuilderUtils.buildSearchResultPageIntent(context, BluetoothDashboardFragment.class.getName(), null /* key */, screenTitle, - MetricsProto.MetricsEvent.SETTINGS_CONNECTED_DEVICE_CATEGORY) + SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY) .setClassName(context.getPackageName(), SubSettings.class.getName()) .setData(contentUri); } @@ -115,10 +109,13 @@ public class BluetoothSliceBuilder { */ public static void handleUriChange(Context context, Intent intent) { final boolean newBluetoothState = intent.getBooleanExtra(EXTRA_TOGGLE_STATE, false); - final LocalBluetoothAdapter adapter = LocalBluetoothManager.getInstance(context, - null /* callback */).getBluetoothAdapter(); + final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); - adapter.setBluetoothEnabled(newBluetoothState); + if (newBluetoothState) { + adapter.enable(); + } else { + adapter.disable(); + } // Do not notifyChange on Uri. The service takes longer to update the current value than it // does for the Slice to check the current value again. Let {@link SliceBroadcastRelay} // handle it. diff --git a/src/com/android/settings/bluetooth/BluetoothSummaryUpdater.java b/src/com/android/settings/bluetooth/BluetoothSummaryUpdater.java index 62fc4eff55..6e826492ae 100644 --- a/src/com/android/settings/bluetooth/BluetoothSummaryUpdater.java +++ b/src/com/android/settings/bluetooth/BluetoothSummaryUpdater.java @@ -19,17 +19,16 @@ package com.android.settings.bluetooth; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.content.Context; -import androidx.annotation.VisibleForTesting; import android.util.Log; +import androidx.annotation.VisibleForTesting; + import com.android.settings.R; import com.android.settings.widget.SummaryUpdater; import com.android.settingslib.bluetooth.BluetoothCallback; import com.android.settingslib.bluetooth.CachedBluetoothDevice; -import com.android.settingslib.bluetooth.LocalBluetoothAdapter; import com.android.settingslib.bluetooth.LocalBluetoothManager; -import java.util.Collection; import java.util.Set; /** @@ -39,15 +38,14 @@ import java.util.Set; public final class BluetoothSummaryUpdater extends SummaryUpdater implements BluetoothCallback { private static final String TAG = "BluetoothSummaryUpdater"; + private final BluetoothAdapter mBluetoothAdapter; private final LocalBluetoothManager mBluetoothManager; - private final LocalBluetoothAdapter mBluetoothAdapter; public BluetoothSummaryUpdater(Context context, OnSummaryChangeListener listener, LocalBluetoothManager bluetoothManager) { super(context, listener); mBluetoothManager = bluetoothManager; - mBluetoothAdapter = mBluetoothManager != null - ? mBluetoothManager.getBluetoothAdapter() : null; + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); } @Override @@ -61,30 +59,6 @@ public final class BluetoothSummaryUpdater extends SummaryUpdater implements Blu } @Override - public void onScanningStateChanged(boolean started) { - } - - @Override - public void onDeviceAdded(CachedBluetoothDevice cachedDevice) { - } - - @Override - public void onDeviceDeleted(CachedBluetoothDevice cachedDevice) { - } - - @Override - public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) { - } - - @Override - public void onActiveDeviceChanged(CachedBluetoothDevice activeDevice, int bluetoothProfile) { - } - - @Override - public void onAudioModeChanged() { - } - - @Override public void register(boolean listening) { if (mBluetoothAdapter == null) { return; diff --git a/src/com/android/settings/bluetooth/BluetoothSwitchPreferenceController.java b/src/com/android/settings/bluetooth/BluetoothSwitchPreferenceController.java index bbc90c4539..48376a779a 100644 --- a/src/com/android/settings/bluetooth/BluetoothSwitchPreferenceController.java +++ b/src/com/android/settings/bluetooth/BluetoothSwitchPreferenceController.java @@ -15,20 +15,18 @@ */ package com.android.settings.bluetooth; +import android.app.settings.SettingsEnums; import android.content.Context; import android.view.View; -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.logging.nano.MetricsProto; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.annotation.VisibleForTesting; + import com.android.settings.R; import com.android.settings.core.SubSettingLauncher; import com.android.settings.location.ScanningSettings; import com.android.settings.overlay.FeatureFactory; import com.android.settings.utils.AnnotationSpan; import com.android.settings.widget.SwitchWidgetController; -import com.android.settingslib.bluetooth.LocalBluetoothAdapter; -import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnStart; import com.android.settingslib.core.lifecycle.events.OnStop; @@ -42,10 +40,6 @@ public class BluetoothSwitchPreferenceController implements LifecycleObserver, OnStart, OnStop, SwitchWidgetController.OnSwitchChangeListener, View.OnClickListener { - @VisibleForTesting - LocalBluetoothAdapter mBluetoothAdapter; - - private LocalBluetoothManager mBluetoothManager; private BluetoothEnabler mBluetoothEnabler; private RestrictionUtils mRestrictionUtils; private SwitchWidgetController mSwitch; @@ -55,15 +49,12 @@ public class BluetoothSwitchPreferenceController public BluetoothSwitchPreferenceController(Context context, SwitchWidgetController switchController, FooterPreference footerPreference) { - this(context, Utils.getLocalBtManager(context), new RestrictionUtils(), switchController, - footerPreference); + this(context, new RestrictionUtils(), switchController, footerPreference); } @VisibleForTesting - public BluetoothSwitchPreferenceController(Context context, - LocalBluetoothManager bluetoothManager, RestrictionUtils restrictionUtils, + public BluetoothSwitchPreferenceController(Context context, RestrictionUtils restrictionUtils, SwitchWidgetController switchController, FooterPreference footerPreference) { - mBluetoothManager = bluetoothManager; mRestrictionUtils = restrictionUtils; mSwitch = switchController; mContext = context; @@ -72,13 +63,10 @@ public class BluetoothSwitchPreferenceController mSwitch.setupView(); updateText(mSwitch.isChecked()); - if (mBluetoothManager != null) { - mBluetoothAdapter = mBluetoothManager.getBluetoothAdapter(); - } mBluetoothEnabler = new BluetoothEnabler(context, switchController, - FeatureFactory.getFactory(context).getMetricsFeatureProvider(), mBluetoothManager, - MetricsEvent.ACTION_SETTINGS_MASTER_SWITCH_BLUETOOTH_TOGGLE, + FeatureFactory.getFactory(context).getMetricsFeatureProvider(), + SettingsEnums.ACTION_SETTINGS_MASTER_SWITCH_BLUETOOTH_TOGGLE, mRestrictionUtils); mBluetoothEnabler.setToggleCallback(this); } @@ -107,7 +95,7 @@ public class BluetoothSwitchPreferenceController // send users to scanning settings if they click on the link in the summary text new SubSettingLauncher(mContext) .setDestination(ScanningSettings.class.getName()) - .setSourceMetricsCategory(MetricsProto.MetricsEvent.BLUETOOTH_FRAGMENT) + .setSourceMetricsCategory(SettingsEnums.BLUETOOTH_FRAGMENT) .launch(); } diff --git a/src/com/android/settings/bluetooth/ConnectedBluetoothDeviceUpdater.java b/src/com/android/settings/bluetooth/ConnectedBluetoothDeviceUpdater.java index 845d2e2d0a..3ae081cc96 100644 --- a/src/com/android/settings/bluetooth/ConnectedBluetoothDeviceUpdater.java +++ b/src/com/android/settings/bluetooth/ConnectedBluetoothDeviceUpdater.java @@ -19,14 +19,13 @@ import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.content.Context; import android.media.AudioManager; -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; import android.util.Log; +import androidx.preference.Preference; + import com.android.settings.connecteddevice.DevicePreferenceCallback; import com.android.settings.dashboard.DashboardFragment; import com.android.settingslib.bluetooth.CachedBluetoothDevice; -import com.android.settingslib.bluetooth.LocalBluetoothManager; /** * Controller to maintain connected bluetooth devices @@ -44,40 +43,12 @@ public class ConnectedBluetoothDeviceUpdater extends BluetoothDeviceUpdater { mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); } - @VisibleForTesting - ConnectedBluetoothDeviceUpdater(DashboardFragment fragment, - DevicePreferenceCallback devicePreferenceCallback, - LocalBluetoothManager localBluetoothManager) { - super(fragment, devicePreferenceCallback, localBluetoothManager); - mAudioManager = (AudioManager) fragment.getContext(). - getSystemService(Context.AUDIO_SERVICE); - } - @Override public void onAudioModeChanged() { forceUpdate(); } @Override - public void onProfileConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state, - int bluetoothProfile) { - if (DBG) { - Log.d(TAG, "onProfileConnectionStateChanged() device: " + - cachedDevice.getName() + ", state: " + state + ", bluetoothProfile: " - + bluetoothProfile); - } - if (state == BluetoothProfile.STATE_CONNECTED) { - if (isFilterMatched(cachedDevice)) { - addPreference(cachedDevice); - } else { - removePreference(cachedDevice); - } - } else if (state == BluetoothProfile.STATE_DISCONNECTED) { - removePreference(cachedDevice); - } - } - - @Override public boolean isFilterMatched(CachedBluetoothDevice cachedDevice) { final int audioMode = mAudioManager.getMode(); final int currentAudioProfile; @@ -111,10 +82,10 @@ public class ConnectedBluetoothDeviceUpdater extends BluetoothDeviceUpdater { // show the bluetooth device that doesn't have headset profile. switch (currentAudioProfile) { case BluetoothProfile.A2DP: - isFilterMatched = !cachedDevice.isA2dpDevice(); + isFilterMatched = !cachedDevice.isConnectedA2dpDevice(); break; case BluetoothProfile.HEADSET: - isFilterMatched = !cachedDevice.isHfpDevice(); + isFilterMatched = !cachedDevice.isConnectedHfpDevice(); break; } if (DBG) { diff --git a/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.java b/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.java index 688fd27e41..4f27a39f9c 100644 --- a/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.java +++ b/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.java @@ -20,22 +20,23 @@ import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.os.Bundle; import android.os.SystemProperties; +import android.text.BidiFormatter; +import android.util.Log; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceGroup; -import android.text.BidiFormatter; -import android.util.Log; import com.android.settings.R; import com.android.settings.dashboard.RestrictedDashboardFragment; import com.android.settingslib.bluetooth.BluetoothCallback; import com.android.settingslib.bluetooth.BluetoothDeviceFilter; import com.android.settingslib.bluetooth.CachedBluetoothDevice; -import com.android.settingslib.bluetooth.LocalBluetoothAdapter; import com.android.settingslib.bluetooth.LocalBluetoothManager; import java.util.Collection; +import java.util.HashMap; import java.util.WeakHashMap; /** @@ -63,14 +64,14 @@ public abstract class DeviceListPreferenceFragment extends BluetoothDevice mSelectedDevice; - LocalBluetoothAdapter mLocalAdapter; + BluetoothAdapter mBluetoothAdapter; LocalBluetoothManager mLocalManager; @VisibleForTesting PreferenceGroup mDeviceListGroup; - final WeakHashMap<CachedBluetoothDevice, BluetoothDevicePreference> mDevicePreferenceMap = - new WeakHashMap<CachedBluetoothDevice, BluetoothDevicePreference>(); + final HashMap<CachedBluetoothDevice, BluetoothDevicePreference> mDevicePreferenceMap = + new HashMap<>(); boolean mShowDevicesWithoutNames; @@ -96,7 +97,7 @@ public abstract class DeviceListPreferenceFragment extends Log.e(TAG, "Bluetooth is not supported on this device"); return; } - mLocalAdapter = mLocalManager.getBluetoothAdapter(); + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); mShowDevicesWithoutNames = SystemProperties.getBoolean( BLUETOOTH_SHOW_DEVICES_WITHOUT_NAMES_PROPERTY, false); @@ -145,7 +146,7 @@ public abstract class DeviceListPreferenceFragment extends @Override public boolean onPreferenceTreeClick(Preference preference) { if (KEY_BT_SCAN.equals(preference.getKey())) { - mLocalAdapter.startScanning(true); + startScanning(); return true; } @@ -171,7 +172,7 @@ public abstract class DeviceListPreferenceFragment extends } // Prevent updates while the list shows one of the state messages - if (mLocalAdapter.getBluetoothState() != BluetoothAdapter.STATE_ON) return; + if (mBluetoothAdapter.getState() != BluetoothAdapter.STATE_ON) return; if (mFilter.matches(cachedDevice.getDevice())) { createDevicePreference(cachedDevice); @@ -192,18 +193,16 @@ public abstract class DeviceListPreferenceFragment extends preference = new BluetoothDevicePreference(getPrefContext(), cachedDevice, mShowDevicesWithoutNames); preference.setKey(key); + //Set hideSecondTarget is true if it's bonded device. + preference.hideSecondTarget(true); mDeviceListGroup.addPreference(preference); - } else { - // Tell the preference it is being re-used in case there is new info in the - // cached device. - preference.rebind(); } initDevicePreference(preference); mDevicePreferenceMap.put(cachedDevice, preference); } - void initDevicePreference(BluetoothDevicePreference preference) { + protected void initDevicePreference(BluetoothDevicePreference preference) { // Does nothing by default } @@ -213,7 +212,7 @@ public abstract class DeviceListPreferenceFragment extends myDevicePreference.setTitle(getString( R.string.bluetooth_footer_mac_message, - bidiFormatter.unicodeWrap(mLocalAdapter.getAddress()))); + bidiFormatter.unicodeWrap(mBluetoothAdapter.getAddress()))); } @Override @@ -226,27 +225,24 @@ public abstract class DeviceListPreferenceFragment extends @VisibleForTesting void enableScanning() { - // LocalBluetoothAdapter already handles repeated scan requests - mLocalAdapter.startScanning(true); + // BluetoothAdapter already handles repeated scan requests + startScanning(); mScanEnabled = true; } @VisibleForTesting void disableScanning() { - mLocalAdapter.stopScanning(); + stopScanning(); mScanEnabled = false; } @Override public void onScanningStateChanged(boolean started) { if (!started && mScanEnabled) { - mLocalAdapter.startScanning(true); + startScanning(); } } - @Override - public void onBluetoothStateChanged(int bluetoothState) {} - /** * Add bluetooth device preferences to {@code preferenceGroup} which satisfy the {@code filter} * @@ -262,22 +258,16 @@ public abstract class DeviceListPreferenceFragment extends cacheRemoveAllPrefs(preferenceGroup); preferenceGroup.setTitle(titleId); mDeviceListGroup = preferenceGroup; - setFilter(filter); if (addCachedDevices) { + // Don't show bonded devices when screen turned back on + setFilter(BluetoothDeviceFilter.UNBONDED_DEVICE_FILTER); addCachedDevices(); } + setFilter(filter); preferenceGroup.setEnabled(true); removeCachedPrefs(preferenceGroup); } - public void onConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) { } - - @Override - public void onActiveDeviceChanged(CachedBluetoothDevice activeDevice, int bluetoothProfile) { } - - @Override - public void onAudioModeChanged() { } - /** * Return the key of the {@link PreferenceGroup} that contains the bluetooth devices */ @@ -286,4 +276,16 @@ public abstract class DeviceListPreferenceFragment extends public boolean shouldShowDevicesWithoutNames() { return mShowDevicesWithoutNames; } + + void startScanning() { + if (!mBluetoothAdapter.isDiscovering()) { + mBluetoothAdapter.startDiscovery(); + } + } + + void stopScanning() { + if (mBluetoothAdapter.isDiscovering()) { + mBluetoothAdapter.cancelDiscovery(); + } + } } diff --git a/src/com/android/settings/bluetooth/DevicePickerActivity.java b/src/com/android/settings/bluetooth/DevicePickerActivity.java index 43ba05d881..77e8ba4e1c 100644 --- a/src/com/android/settings/bluetooth/DevicePickerActivity.java +++ b/src/com/android/settings/bluetooth/DevicePickerActivity.java @@ -16,16 +16,17 @@ package com.android.settings.bluetooth; -import android.app.Activity; import android.os.Bundle; +import androidx.fragment.app.FragmentActivity; + import com.android.settings.R; /** * Activity for Bluetooth device picker dialog. The device picker logic * is implemented in the {@link BluetoothPairingDetail} fragment. */ -public final class DevicePickerActivity extends Activity { +public final class DevicePickerActivity extends FragmentActivity { @Override protected void onCreate(Bundle savedInstanceState) { diff --git a/src/com/android/settings/bluetooth/DevicePickerFragment.java b/src/com/android/settings/bluetooth/DevicePickerFragment.java index 3a6865355e..02625bbad2 100644 --- a/src/com/android/settings/bluetooth/DevicePickerFragment.java +++ b/src/com/android/settings/bluetooth/DevicePickerFragment.java @@ -18,6 +18,7 @@ package com.android.settings.bluetooth; import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH; +import android.app.settings.SettingsEnums; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothDevicePicker; @@ -25,11 +26,11 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.UserManager; -import androidx.annotation.VisibleForTesting; import android.view.Menu; import android.view.MenuInflater; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.annotation.VisibleForTesting; + import com.android.settings.R; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.core.AbstractPreferenceController; @@ -74,7 +75,7 @@ public final class DevicePickerFragment extends DeviceListPreferenceFragment { @Override public int getMetricsCategory() { - return MetricsEvent.BLUETOOTH_DEVICE_PICKER; + return SettingsEnums.BLUETOOTH_DEVICE_PICKER; } @Override @@ -93,7 +94,7 @@ public final class DevicePickerFragment extends DeviceListPreferenceFragment { mSelectedDevice = null; if (mScanAllowed) { enableScanning(); - mAvailableDevicesCategory.setProgress(mLocalAdapter.isDiscovering()); + mAvailableDevicesCategory.setProgress(mBluetoothAdapter.isDiscovering()); } } @@ -151,6 +152,12 @@ public final class DevicePickerFragment extends DeviceListPreferenceFragment { } @Override + protected void initDevicePreference(BluetoothDevicePreference preference) { + super.initDevicePreference(preference); + preference.setNeedNotifyHierarchyChanged(true); + } + + @Override public void onBluetoothStateChanged(int bluetoothState) { super.onBluetoothStateChanged(bluetoothState); diff --git a/src/com/android/settings/bluetooth/DeviceProfilesSettings.java b/src/com/android/settings/bluetooth/DeviceProfilesSettings.java deleted file mode 100644 index d6cdb239d4..0000000000 --- a/src/com/android/settings/bluetooth/DeviceProfilesSettings.java +++ /dev/null @@ -1,433 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settings.bluetooth; - -import android.app.AlertDialog; -import android.app.Dialog; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothProfile; -import android.content.Context; -import android.content.DialogInterface; -import android.os.Bundle; -import androidx.annotation.VisibleForTesting; -import android.text.Html; -import android.text.TextUtils; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.widget.CheckBox; -import android.widget.EditText; -import android.widget.TextView; - -import com.android.internal.logging.nano.MetricsProto; -import com.android.settings.R; -import com.android.settings.core.instrumentation.InstrumentedDialogFragment; -import com.android.settingslib.bluetooth.A2dpProfile; -import com.android.settingslib.bluetooth.CachedBluetoothDevice; -import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; -import com.android.settingslib.bluetooth.LocalBluetoothManager; -import com.android.settingslib.bluetooth.LocalBluetoothProfile; -import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; -import com.android.settingslib.bluetooth.MapProfile; -import com.android.settingslib.bluetooth.PanProfile; -import com.android.settingslib.bluetooth.PbapServerProfile; - -public final class DeviceProfilesSettings extends InstrumentedDialogFragment implements - CachedBluetoothDevice.Callback, DialogInterface.OnClickListener, OnClickListener { - private static final String TAG = "DeviceProfilesSettings"; - - public static final String ARG_DEVICE_ADDRESS = "device_address"; - - private static final String KEY_PROFILE_CONTAINER = "profile_container"; - private static final String KEY_UNPAIR = "unpair"; - private static final String KEY_PBAP_SERVER = "PBAP Server"; - @VisibleForTesting - static final String HIGH_QUALITY_AUDIO_PREF_TAG = "A2dpProfileHighQualityAudio"; - - private CachedBluetoothDevice mCachedDevice; - private LocalBluetoothManager mManager; - private LocalBluetoothProfileManager mProfileManager; - - private ViewGroup mProfileContainer; - private TextView mProfileLabel; - - private AlertDialog mDisconnectDialog; - private boolean mProfileGroupIsRemoved; - - private View mRootView; - - @Override - public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DIALOG_BLUETOOTH_PAIRED_DEVICE_PROFILE; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - mManager = Utils.getLocalBtManager(getActivity()); - CachedBluetoothDeviceManager deviceManager = mManager.getCachedDeviceManager(); - - String address = getArguments().getString(ARG_DEVICE_ADDRESS); - BluetoothDevice remoteDevice = mManager.getBluetoothAdapter().getRemoteDevice(address); - - mCachedDevice = deviceManager.findDevice(remoteDevice); - if (mCachedDevice == null) { - mCachedDevice = deviceManager.addDevice(mManager.getBluetoothAdapter(), - mManager.getProfileManager(), remoteDevice); - } - mProfileManager = mManager.getProfileManager(); - } - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - mRootView = LayoutInflater.from(getContext()).inflate(R.layout.device_profiles_settings, - null); - mProfileContainer = (ViewGroup) mRootView.findViewById(R.id.profiles_section); - mProfileLabel = (TextView) mRootView.findViewById(R.id.profiles_label); - final EditText deviceName = (EditText) mRootView.findViewById(R.id.name); - deviceName.setText(mCachedDevice.getName(), TextView.BufferType.EDITABLE); - return new AlertDialog.Builder(getContext()) - .setView(mRootView) - .setNeutralButton(R.string.forget, this) - .setPositiveButton(R.string.okay, this) - .setTitle(R.string.bluetooth_preference_paired_devices) - .create(); - } - - @Override - public void onClick(DialogInterface dialog, int which) { - switch (which) { - case DialogInterface.BUTTON_POSITIVE: - EditText deviceName = (EditText) mRootView.findViewById(R.id.name); - mCachedDevice.setName(deviceName.getText().toString()); - break; - case DialogInterface.BUTTON_NEUTRAL: - mCachedDevice.unpair(); - break; - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - if (mDisconnectDialog != null) { - mDisconnectDialog.dismiss(); - mDisconnectDialog = null; - } - if (mCachedDevice != null) { - mCachedDevice.unregisterCallback(this); - } - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - } - - @Override - public void onResume() { - super.onResume(); - - mManager.setForegroundActivity(getActivity()); - if (mCachedDevice != null) { - mCachedDevice.registerCallback(this); - if (mCachedDevice.getBondState() == BluetoothDevice.BOND_NONE) { - dismiss(); - return; - } - addPreferencesForProfiles(); - refresh(); - } - } - - @Override - public void onPause() { - super.onPause(); - - if (mCachedDevice != null) { - mCachedDevice.unregisterCallback(this); - } - - mManager.setForegroundActivity(null); - } - - private void addPreferencesForProfiles() { - mProfileContainer.removeAllViews(); - for (LocalBluetoothProfile profile : mCachedDevice.getConnectableProfiles()) { - CheckBox pref = createProfilePreference(profile); - // MAP and PBAP profiles would be added based on permission access - if (!((profile instanceof PbapServerProfile) || - (profile instanceof MapProfile))) { - mProfileContainer.addView(pref); - } - - if (profile instanceof A2dpProfile) { - BluetoothDevice device = mCachedDevice.getDevice(); - A2dpProfile a2dpProfile = (A2dpProfile) profile; - if (a2dpProfile.supportsHighQualityAudio(device)) { - CheckBox highQualityPref = new CheckBox(getActivity()); - highQualityPref.setTag(HIGH_QUALITY_AUDIO_PREF_TAG); - highQualityPref.setOnClickListener(v -> { - a2dpProfile.setHighQualityAudioEnabled(device, highQualityPref.isChecked()); - }); - highQualityPref.setVisibility(View.GONE); - mProfileContainer.addView(highQualityPref); - } - refreshProfilePreference(pref, profile); - } - } - - final int pbapPermission = mCachedDevice.getPhonebookPermissionChoice(); - Log.d(TAG, "addPreferencesForProfiles: pbapPermission = " + pbapPermission); - // Only provide PBAP cabability if the client device has requested PBAP. - if (pbapPermission != CachedBluetoothDevice.ACCESS_UNKNOWN) { - final PbapServerProfile psp = mManager.getProfileManager().getPbapProfile(); - CheckBox pbapPref = createProfilePreference(psp); - mProfileContainer.addView(pbapPref); - } - - final MapProfile mapProfile = mManager.getProfileManager().getMapProfile(); - final int mapPermission = mCachedDevice.getMessagePermissionChoice(); - Log.d(TAG, "addPreferencesForProfiles: mapPermission = " + mapPermission); - if (mapPermission != CachedBluetoothDevice.ACCESS_UNKNOWN) { - CheckBox mapPreference = createProfilePreference(mapProfile); - mProfileContainer.addView(mapPreference); - } - - showOrHideProfileGroup(); - } - - private void showOrHideProfileGroup() { - int numProfiles = mProfileContainer.getChildCount(); - if (!mProfileGroupIsRemoved && numProfiles == 0) { - mProfileContainer.setVisibility(View.GONE); - mProfileLabel.setVisibility(View.GONE); - mProfileGroupIsRemoved = true; - } else if (mProfileGroupIsRemoved && numProfiles != 0) { - mProfileContainer.setVisibility(View.VISIBLE); - mProfileLabel.setVisibility(View.VISIBLE); - mProfileGroupIsRemoved = false; - } - } - - /** - * Creates a checkbox preference for the particular profile. The key will be - * the profile's name. - * - * @param profile The profile for which the preference controls. - * @return A preference that allows the user to choose whether this profile - * will be connected to. - */ - private CheckBox createProfilePreference(LocalBluetoothProfile profile) { - CheckBox pref = new CheckBox(getActivity()); - pref.setTag(profile.toString()); - pref.setText(profile.getNameResource(mCachedDevice.getDevice())); - pref.setOnClickListener(this); - - refreshProfilePreference(pref, profile); - - return pref; - } - - @Override - public void onClick(View v) { - if (v instanceof CheckBox) { - LocalBluetoothProfile prof = getProfileOf(v); - onProfileClicked(prof, (CheckBox) v); - } - } - - private void onProfileClicked(LocalBluetoothProfile profile, CheckBox profilePref) { - BluetoothDevice device = mCachedDevice.getDevice(); - - if (!profilePref.isChecked()) { - // Recheck it, until the dialog is done. - profilePref.setChecked(true); - askDisconnect(mManager.getForegroundActivity(), profile); - } else { - if (profile instanceof MapProfile) { - mCachedDevice.setMessagePermissionChoice(BluetoothDevice.ACCESS_ALLOWED); - } - if (profile instanceof PbapServerProfile) { - mCachedDevice.setPhonebookPermissionChoice(BluetoothDevice.ACCESS_ALLOWED); - refreshProfilePreference(profilePref, profile); - // PBAP server is not preffered profile and cannot initiate connection, so return - return; - } - if (profile.isPreferred(device)) { - // profile is preferred but not connected: disable auto-connect - if (profile instanceof PanProfile) { - mCachedDevice.connectProfile(profile); - } else { - profile.setPreferred(device, false); - } - } else { - profile.setPreferred(device, true); - mCachedDevice.connectProfile(profile); - } - refreshProfilePreference(profilePref, profile); - } - } - - private void askDisconnect(Context context, - final LocalBluetoothProfile profile) { - // local reference for callback - final CachedBluetoothDevice device = mCachedDevice; - String name = device.getName(); - if (TextUtils.isEmpty(name)) { - name = context.getString(R.string.bluetooth_device); - } - - String profileName = context.getString(profile.getNameResource(device.getDevice())); - - String title = context.getString(R.string.bluetooth_disable_profile_title); - String message = context.getString(R.string.bluetooth_disable_profile_message, - profileName, name); - - DialogInterface.OnClickListener disconnectListener = - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - - // Disconnect only when user has selected OK otherwise ignore - if (which == DialogInterface.BUTTON_POSITIVE) { - device.disconnect(profile); - profile.setPreferred(device.getDevice(), false); - if (profile instanceof MapProfile) { - device.setMessagePermissionChoice(BluetoothDevice.ACCESS_REJECTED); - } - if (profile instanceof PbapServerProfile) { - device.setPhonebookPermissionChoice(BluetoothDevice.ACCESS_REJECTED); - } - } - refreshProfilePreference(findProfile(profile.toString()), profile); - } - }; - - mDisconnectDialog = Utils.showDisconnectDialog(context, - mDisconnectDialog, disconnectListener, title, Html.fromHtml(message)); - } - - @Override - public void onDeviceAttributesChanged() { - refresh(); - } - - private void refresh() { - final EditText deviceNameField = (EditText) mRootView.findViewById(R.id.name); - if (deviceNameField != null) { - deviceNameField.setText(mCachedDevice.getName()); - com.android.settings.Utils.setEditTextCursorPosition(deviceNameField); - } - - refreshProfiles(); - } - - private void refreshProfiles() { - for (LocalBluetoothProfile profile : mCachedDevice.getConnectableProfiles()) { - CheckBox profilePref = findProfile(profile.toString()); - if (profilePref == null) { - profilePref = createProfilePreference(profile); - mProfileContainer.addView(profilePref); - } else { - refreshProfilePreference(profilePref, profile); - } - } - for (LocalBluetoothProfile profile : mCachedDevice.getRemovedProfiles()) { - CheckBox profilePref = findProfile(profile.toString()); - if (profilePref != null) { - - if (profile instanceof PbapServerProfile) { - final int pbapPermission = mCachedDevice.getPhonebookPermissionChoice(); - Log.d(TAG, "refreshProfiles: pbapPermission = " + pbapPermission); - if (pbapPermission != CachedBluetoothDevice.ACCESS_UNKNOWN) - continue; - } - if (profile instanceof MapProfile) { - final int mapPermission = mCachedDevice.getMessagePermissionChoice(); - Log.d(TAG, "refreshProfiles: mapPermission = " + mapPermission); - if (mapPermission != CachedBluetoothDevice.ACCESS_UNKNOWN) - continue; - } - Log.d(TAG, "Removing " + profile.toString() + " from profile list"); - mProfileContainer.removeView(profilePref); - } - } - - showOrHideProfileGroup(); - } - - private CheckBox findProfile(String profile) { - return (CheckBox) mProfileContainer.findViewWithTag(profile); - } - - private void refreshProfilePreference(CheckBox profilePref, - LocalBluetoothProfile profile) { - BluetoothDevice device = mCachedDevice.getDevice(); - - // Gray out checkbox while connecting and disconnecting. - profilePref.setEnabled(!mCachedDevice.isBusy()); - - if (profile instanceof MapProfile) { - profilePref.setChecked(mCachedDevice.getMessagePermissionChoice() - == CachedBluetoothDevice.ACCESS_ALLOWED); - - } else if (profile instanceof PbapServerProfile) { - profilePref.setChecked(mCachedDevice.getPhonebookPermissionChoice() - == CachedBluetoothDevice.ACCESS_ALLOWED); - - } else if (profile instanceof PanProfile) { - profilePref.setChecked(profile.getConnectionStatus(device) == - BluetoothProfile.STATE_CONNECTED); - - } else { - profilePref.setChecked(profile.isPreferred(device)); - } - if (profile instanceof A2dpProfile) { - A2dpProfile a2dpProfile = (A2dpProfile) profile; - View v = mProfileContainer.findViewWithTag(HIGH_QUALITY_AUDIO_PREF_TAG); - if (v instanceof CheckBox) { - CheckBox highQualityPref = (CheckBox) v; - highQualityPref.setText(a2dpProfile.getHighQualityAudioOptionLabel(device)); - highQualityPref.setChecked(a2dpProfile.isHighQualityAudioEnabled(device)); - - if (a2dpProfile.isPreferred(device)) { - v.setVisibility(View.VISIBLE); - v.setEnabled(!mCachedDevice.isBusy()); - } else { - v.setVisibility(View.GONE); - } - } - } - } - - private LocalBluetoothProfile getProfileOf(View v) { - if (!(v instanceof CheckBox)) { - return null; - } - String key = (String) v.getTag(); - if (TextUtils.isEmpty(key)) return null; - - try { - return mProfileManager.getProfileByName(key); - } catch (IllegalArgumentException ignored) { - return null; - } - } -} diff --git a/src/com/android/settings/bluetooth/ForgetDeviceDialogFragment.java b/src/com/android/settings/bluetooth/ForgetDeviceDialogFragment.java index 1f3a689da6..6d8fb3380e 100644 --- a/src/com/android/settings/bluetooth/ForgetDeviceDialogFragment.java +++ b/src/com/android/settings/bluetooth/ForgetDeviceDialogFragment.java @@ -17,17 +17,19 @@ package com.android.settings.bluetooth; import android.app.Activity; -import android.app.AlertDialog; import android.app.Dialog; +import android.app.settings.SettingsEnums; import android.bluetooth.BluetoothDevice; import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.logging.nano.MetricsProto; +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; + import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; +import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.LocalBluetoothManager; @@ -57,7 +59,7 @@ public class ForgetDeviceDialogFragment extends InstrumentedDialogFragment { @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DIALOG_BLUETOOTH_PAIRED_DEVICE_FORGET; + return SettingsEnums.DIALOG_BLUETOOTH_PAIRED_DEVICE_FORGET; } @Override @@ -71,13 +73,18 @@ public class ForgetDeviceDialogFragment extends InstrumentedDialogFragment { }; Context context = getContext(); mDevice = getDevice(context); + final boolean untetheredHeadset = BluetoothUtils.getBooleanMetaData( + mDevice.getDevice(), BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET); + AlertDialog dialog = new AlertDialog.Builder(context) .setPositiveButton(R.string.bluetooth_unpair_dialog_forget_confirm_button, onConfirm) .setNegativeButton(android.R.string.cancel, null) .create(); dialog.setTitle(R.string.bluetooth_unpair_dialog_title); - dialog.setMessage(context.getString(R.string.bluetooth_unpair_dialog_body, + dialog.setMessage(context.getString(untetheredHeadset + ? R.string.bluetooth_untethered_unpair_dialog_body + : R.string.bluetooth_unpair_dialog_body, mDevice.getName())); return dialog; } diff --git a/src/com/android/settings/bluetooth/LocalBluetoothPreferences.java b/src/com/android/settings/bluetooth/LocalBluetoothPreferences.java index 19eb2005f1..faed1c3c6a 100644 --- a/src/com/android/settings/bluetooth/LocalBluetoothPreferences.java +++ b/src/com/android/settings/bluetooth/LocalBluetoothPreferences.java @@ -16,14 +16,13 @@ package com.android.settings.bluetooth; -import android.app.QueuedWork; +import android.bluetooth.BluetoothAdapter; import android.content.Context; import android.content.SharedPreferences; import android.content.res.Configuration; import android.text.TextUtils; import android.util.Log; -import com.android.settingslib.bluetooth.LocalBluetoothAdapter; import com.android.settingslib.bluetooth.LocalBluetoothManager; /** @@ -90,7 +89,7 @@ final class LocalBluetoothPreferences { } // If the device was discoverING recently - LocalBluetoothAdapter adapter = manager.getBluetoothAdapter(); + BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); if (adapter != null) { if (adapter.isDiscovering()) { return true; diff --git a/src/com/android/settings/bluetooth/LocalDeviceNameDialogFragment.java b/src/com/android/settings/bluetooth/LocalDeviceNameDialogFragment.java index 029b974d20..c15dd04ae5 100644 --- a/src/com/android/settings/bluetooth/LocalDeviceNameDialogFragment.java +++ b/src/com/android/settings/bluetooth/LocalDeviceNameDialogFragment.java @@ -16,6 +16,7 @@ package com.android.settings.bluetooth; +import android.app.settings.SettingsEnums; import android.bluetooth.BluetoothAdapter; import android.content.BroadcastReceiver; import android.content.Context; @@ -23,19 +24,12 @@ import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; -import com.android.settingslib.bluetooth.LocalBluetoothAdapter; -import com.android.settingslib.bluetooth.LocalBluetoothManager; /** Provides a dialog for changing the advertised name of the local bluetooth adapter. */ public class LocalDeviceNameDialogFragment extends BluetoothNameDialogFragment { public static final String TAG = "LocalAdapterName"; - private LocalBluetoothAdapter mLocalAdapter; - - public static LocalDeviceNameDialogFragment newInstance() { - return new LocalDeviceNameDialogFragment(); - } + private BluetoothAdapter mBluetoothAdapter; private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override @@ -53,8 +47,7 @@ public class LocalDeviceNameDialogFragment extends BluetoothNameDialogFragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - LocalBluetoothManager localManager = Utils.getLocalBtManager(getActivity()); - mLocalAdapter = localManager.getBluetoothAdapter(); + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); } @Override @@ -74,7 +67,7 @@ public class LocalDeviceNameDialogFragment extends BluetoothNameDialogFragment { @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DIALOG_BLUETOOTH_RENAME; + return SettingsEnums.DIALOG_BLUETOOTH_RENAME; } @Override @@ -84,14 +77,14 @@ public class LocalDeviceNameDialogFragment extends BluetoothNameDialogFragment { @Override protected String getDeviceName() { - if (mLocalAdapter != null && mLocalAdapter.isEnabled()) { - return mLocalAdapter.getName(); + if (mBluetoothAdapter != null && mBluetoothAdapter.isEnabled()) { + return mBluetoothAdapter.getName(); } return null; } @Override protected void setDeviceName(String deviceName) { - mLocalAdapter.setName(deviceName); + mBluetoothAdapter.setName(deviceName); } } diff --git a/src/com/android/settings/bluetooth/RemoteDeviceNameDialogFragment.java b/src/com/android/settings/bluetooth/RemoteDeviceNameDialogFragment.java index 4e5acefe61..2ba94a1918 100644 --- a/src/com/android/settings/bluetooth/RemoteDeviceNameDialogFragment.java +++ b/src/com/android/settings/bluetooth/RemoteDeviceNameDialogFragment.java @@ -16,12 +16,13 @@ package com.android.settings.bluetooth; +import android.app.settings.SettingsEnums; import android.bluetooth.BluetoothDevice; import android.content.Context; import android.os.Bundle; -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.logging.nano.MetricsProto; +import androidx.annotation.VisibleForTesting; + import com.android.settings.R; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.LocalBluetoothManager; @@ -57,7 +58,7 @@ public class RemoteDeviceNameDialogFragment extends BluetoothNameDialogFragment @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DIALOG_BLUETOOTH_PAIRED_DEVICE_RENAME; + return SettingsEnums.DIALOG_BLUETOOTH_PAIRED_DEVICE_RENAME; } @Override diff --git a/src/com/android/settings/bluetooth/RequestPermissionActivity.java b/src/com/android/settings/bluetooth/RequestPermissionActivity.java index d2e2060291..54854065e9 100644 --- a/src/com/android/settings/bluetooth/RequestPermissionActivity.java +++ b/src/com/android/settings/bluetooth/RequestPermissionActivity.java @@ -18,7 +18,6 @@ package com.android.settings.bluetooth; import android.annotation.NonNull; import android.app.Activity; -import android.app.AlertDialog; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.content.BroadcastReceiver; @@ -27,28 +26,29 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ApplicationInfo; +import android.content.pm.PackageItemInfo; import android.content.pm.PackageManager; import android.os.Bundle; import android.text.TextUtils; import android.util.Log; +import androidx.appcompat.app.AlertDialog; + import com.android.settings.R; import com.android.settingslib.bluetooth.BluetoothDiscoverableTimeoutReceiver; -import com.android.settingslib.bluetooth.LocalBluetoothAdapter; -import com.android.settingslib.bluetooth.LocalBluetoothManager; /** * RequestPermissionActivity asks the user whether to enable discovery. This is * usually started by an application wanted to start bluetooth and or discovery */ public class RequestPermissionActivity extends Activity implements - DialogInterface.OnClickListener { + DialogInterface.OnClickListener, DialogInterface.OnDismissListener { // Command line to test this // adb shell am start -a android.bluetooth.adapter.action.REQUEST_ENABLE // adb shell am start -a android.bluetooth.adapter.action.REQUEST_DISCOVERABLE // adb shell am start -a android.bluetooth.adapter.action.REQUEST_DISABLE - private static final String TAG = "RequestPermissionActivity"; + private static final String TAG = "BtRequestPermission"; private static final int MAX_DISCOVERABLE_TIMEOUT = 3600; // 1 hr @@ -56,7 +56,7 @@ public class RequestPermissionActivity extends Activity implements static final int REQUEST_ENABLE_DISCOVERABLE = 2; static final int REQUEST_DISABLE = 3; - private LocalBluetoothAdapter mLocalAdapter; + private BluetoothAdapter mBluetoothAdapter; private int mTimeout = BluetoothDiscoverableEnabler.DEFAULT_DISCOVERABLE_TIMEOUT; @@ -74,13 +74,13 @@ public class RequestPermissionActivity extends Activity implements setResult(Activity.RESULT_CANCELED); - // Note: initializes mLocalAdapter and returns true on error + // Note: initializes mBluetoothAdapter and returns true on error if (parseIntent()) { finish(); return; } - int btState = mLocalAdapter.getState(); + int btState = mBluetoothAdapter.getState(); if (mRequest == REQUEST_DISABLE) { switch (btState) { @@ -188,6 +188,7 @@ public class RequestPermissionActivity extends Activity implements builder.setNegativeButton(getString(R.string.deny), this); } + builder.setOnDismissListener(this); mDialog = builder.create(); mDialog.show(); } @@ -202,7 +203,7 @@ public class RequestPermissionActivity extends Activity implements switch (mRequest) { case REQUEST_ENABLE: case REQUEST_ENABLE_DISCOVERABLE: { - if (mLocalAdapter.getBluetoothState() == BluetoothAdapter.STATE_ON) { + if (mBluetoothAdapter.getState() == BluetoothAdapter.STATE_ON) { proceedAndFinish(); } else { // If BT is not up yet, show "Turning on Bluetooth..." @@ -214,7 +215,7 @@ public class RequestPermissionActivity extends Activity implements } break; case REQUEST_DISABLE: { - if (mLocalAdapter.getBluetoothState() == BluetoothAdapter.STATE_OFF) { + if (mBluetoothAdapter.getState() == BluetoothAdapter.STATE_OFF) { proceedAndFinish(); } else { // If BT is not up yet, show "Turning off Bluetooth..." @@ -238,19 +239,23 @@ public class RequestPermissionActivity extends Activity implements break; case DialogInterface.BUTTON_NEGATIVE: - setResult(RESULT_CANCELED); - finish(); + cancelAndFinish(); break; } } + @Override + public void onDismiss(final DialogInterface dialog) { + cancelAndFinish(); + } + private void proceedAndFinish() { int returnCode; if (mRequest == REQUEST_ENABLE || mRequest == REQUEST_DISABLE) { // BT toggled. Done returnCode = RESULT_OK; - } else if (mLocalAdapter.setScanMode( + } else if (mBluetoothAdapter.setScanMode( BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE, mTimeout)) { // If already in discoverable mode, this will extend the timeout. long endTime = System.currentTimeMillis() + (long) mTimeout * 1000; @@ -282,7 +287,7 @@ public class RequestPermissionActivity extends Activity implements } /** - * Parse the received Intent and initialize mLocalBluetoothAdapter. + * Parse the received Intent and initialize mBluetoothAdapter. * @return true if an error occurred; false otherwise */ private boolean parseIntent() { @@ -312,13 +317,6 @@ public class RequestPermissionActivity extends Activity implements return true; } - LocalBluetoothManager manager = Utils.getLocalBtManager(this); - if (manager == null) { - Log.e(TAG, "Error: there's a problem starting Bluetooth"); - setResult(RESULT_CANCELED); - return true; - } - String packageName = getCallingPackage(); if (TextUtils.isEmpty(packageName)) { packageName = getIntent().getStringExtra(Intent.EXTRA_PACKAGE_NAME); @@ -327,7 +325,10 @@ public class RequestPermissionActivity extends Activity implements try { ApplicationInfo applicationInfo = getPackageManager().getApplicationInfo( packageName, 0); - mAppLabel = applicationInfo.loadSafeLabel(getPackageManager()); + mAppLabel = applicationInfo.loadSafeLabel(getPackageManager(), + PackageItemInfo.DEFAULT_MAX_LABEL_SIZE_PX, + PackageItemInfo.SAFE_LABEL_FLAG_TRIM + | PackageItemInfo.SAFE_LABEL_FLAG_FIRST_LINE); } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "Couldn't find app with package name " + packageName); setResult(RESULT_CANCELED); @@ -335,7 +336,12 @@ public class RequestPermissionActivity extends Activity implements } } - mLocalAdapter = manager.getBluetoothAdapter(); + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + if (mBluetoothAdapter == null) { + Log.e(TAG, "Error: there's a problem starting Bluetooth"); + setResult(RESULT_CANCELED); + return true; + } return false; } diff --git a/src/com/android/settings/bluetooth/RequestPermissionHelperActivity.java b/src/com/android/settings/bluetooth/RequestPermissionHelperActivity.java index 3ba38448e2..99ac928151 100644 --- a/src/com/android/settings/bluetooth/RequestPermissionHelperActivity.java +++ b/src/com/android/settings/bluetooth/RequestPermissionHelperActivity.java @@ -21,21 +21,13 @@ import android.app.admin.DevicePolicyManager; import android.bluetooth.BluetoothAdapter; import android.content.DialogInterface; import android.content.Intent; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; import android.os.Bundle; import android.os.UserManager; -import androidx.annotation.NonNull; -import android.text.TextUtils; import android.util.Log; import com.android.internal.app.AlertActivity; import com.android.internal.app.AlertController; -import com.android.internal.util.ArrayUtils; -import com.android.internal.util.CharSequences; import com.android.settings.R; -import com.android.settingslib.bluetooth.LocalBluetoothAdapter; -import com.android.settingslib.bluetooth.LocalBluetoothManager; /** * RequestPermissionHelperActivity asks the user whether to toggle Bluetooth. @@ -55,7 +47,7 @@ public class RequestPermissionHelperActivity extends AlertActivity implements public static final String EXTRA_APP_LABEL = "com.android.settings.bluetooth.extra.APP_LABEL"; - private LocalBluetoothAdapter mLocalAdapter; + private BluetoothAdapter mBluetoothAdapter; private CharSequence mAppLabel; @@ -69,7 +61,7 @@ public class RequestPermissionHelperActivity extends AlertActivity implements setResult(RESULT_CANCELED); - // Note: initializes mLocalAdapter and returns true on error + // Note: initializes mBluetoothAdapter and returns true on error if (!parseIntent()) { finish(); return; @@ -137,20 +129,20 @@ public class RequestPermissionHelperActivity extends AlertActivity implements startActivity(intent); } } else { - mLocalAdapter.enable(); + mBluetoothAdapter.enable(); setResult(Activity.RESULT_OK); } } break; case RequestPermissionActivity.REQUEST_DISABLE: { - mLocalAdapter.disable(); + mBluetoothAdapter.disable(); setResult(Activity.RESULT_OK); } break; } } /** - * Parse the received Intent and initialize mLocalBluetoothAdapter. + * Parse the received Intent and initialize mBluetoothAdapter. * @return true if an error occurred; false otherwise */ private boolean parseIntent() { @@ -173,16 +165,14 @@ public class RequestPermissionHelperActivity extends AlertActivity implements return false; } - LocalBluetoothManager manager = Utils.getLocalBtManager(this); - if (manager == null) { + mAppLabel = getIntent().getCharSequenceExtra(EXTRA_APP_LABEL); + + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + if (mBluetoothAdapter == null) { Log.e(TAG, "Error: there's a problem starting Bluetooth"); return false; } - mAppLabel = getIntent().getCharSequenceExtra(EXTRA_APP_LABEL); - - mLocalAdapter = manager.getBluetoothAdapter(); - return true; } } diff --git a/src/com/android/settings/bluetooth/RestrictionUtils.java b/src/com/android/settings/bluetooth/RestrictionUtils.java index 9c0c481324..21b00cd0c0 100644 --- a/src/com/android/settings/bluetooth/RestrictionUtils.java +++ b/src/com/android/settings/bluetooth/RestrictionUtils.java @@ -19,8 +19,8 @@ package com.android.settings.bluetooth; import android.content.Context; import android.os.UserHandle; -import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; +import com.android.settingslib.RestrictedLockUtilsInternal; /** * A utility class to aid testing. @@ -36,7 +36,7 @@ public class RestrictionUtils { * API. */ public EnforcedAdmin checkIfRestrictionEnforced(Context context, String restriction) { - return RestrictedLockUtils.checkIfRestrictionEnforced( + return RestrictedLockUtilsInternal.checkIfRestrictionEnforced( context, restriction, UserHandle.myUserId()); } diff --git a/src/com/android/settings/bluetooth/SavedBluetoothDeviceUpdater.java b/src/com/android/settings/bluetooth/SavedBluetoothDeviceUpdater.java index 1dfa6472e6..6381b848c9 100644 --- a/src/com/android/settings/bluetooth/SavedBluetoothDeviceUpdater.java +++ b/src/com/android/settings/bluetooth/SavedBluetoothDeviceUpdater.java @@ -16,16 +16,14 @@ package com.android.settings.bluetooth; import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothProfile; import android.content.Context; -import androidx.annotation.VisibleForTesting; +import android.util.Log; + +import androidx.preference.Preference; import com.android.settings.connecteddevice.DevicePreferenceCallback; import com.android.settings.dashboard.DashboardFragment; import com.android.settingslib.bluetooth.CachedBluetoothDevice; -import com.android.settingslib.bluetooth.LocalBluetoothManager; -import androidx.preference.Preference; -import android.util.Log; /** * Maintain and update saved bluetooth devices(bonded but not connected) @@ -33,32 +31,21 @@ import android.util.Log; public class SavedBluetoothDeviceUpdater extends BluetoothDeviceUpdater implements Preference.OnPreferenceClickListener { private static final String TAG = "SavedBluetoothDeviceUpdater"; + private static final boolean DBG = false; public SavedBluetoothDeviceUpdater(Context context, DashboardFragment fragment, DevicePreferenceCallback devicePreferenceCallback) { super(context, fragment, devicePreferenceCallback); } - @VisibleForTesting - SavedBluetoothDeviceUpdater(DashboardFragment fragment, - DevicePreferenceCallback devicePreferenceCallback, - LocalBluetoothManager localBluetoothManager) { - super(fragment, devicePreferenceCallback, localBluetoothManager); - } - - @Override - public void onProfileConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state, - int bluetoothProfile) { - if (state == BluetoothProfile.STATE_CONNECTED) { - removePreference(cachedDevice); - } else if (state == BluetoothProfile.STATE_DISCONNECTED) { - addPreference(cachedDevice); - } - } - @Override public boolean isFilterMatched(CachedBluetoothDevice cachedDevice) { final BluetoothDevice device = cachedDevice.getDevice(); + if (DBG) { + Log.d(TAG, "isFilterMatched() device name : " + cachedDevice.getName() + + ", is connected : " + device.isConnected() + ", is profile connected : " + + cachedDevice.isConnected()); + } return device.getBondState() == BluetoothDevice.BOND_BONDED && !device.isConnected(); } diff --git a/src/com/android/settings/bluetooth/Utf8ByteLengthFilter.java b/src/com/android/settings/bluetooth/Utf8ByteLengthFilter.java index ab498183bc..92bcf93ee7 100644 --- a/src/com/android/settings/bluetooth/Utf8ByteLengthFilter.java +++ b/src/com/android/settings/bluetooth/Utf8ByteLengthFilter.java @@ -19,6 +19,8 @@ package com.android.settings.bluetooth; import android.text.InputFilter; import android.text.Spanned; +import androidx.annotation.Keep; + /** * This filter will constrain edits so that the text length is not * greater than the specified number of bytes using UTF-8 encoding. @@ -40,6 +42,7 @@ import android.text.Spanned; public class Utf8ByteLengthFilter implements InputFilter { private final int mMaxBytes; + @Keep Utf8ByteLengthFilter(int maxBytes) { mMaxBytes = maxBytes; } diff --git a/src/com/android/settings/bluetooth/Utils.java b/src/com/android/settings/bluetooth/Utils.java index daaac314fb..d6e395e0c3 100755 --- a/src/com/android/settings/bluetooth/Utils.java +++ b/src/com/android/settings/bluetooth/Utils.java @@ -16,21 +16,24 @@ package com.android.settings.bluetooth; -import android.app.AlertDialog; +import android.app.settings.SettingsEnums; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.content.Context; import android.content.DialogInterface; import android.provider.Settings; -import androidx.annotation.VisibleForTesting; +import android.util.Log; import android.widget.Toast; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; + import com.android.settings.R; import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.bluetooth.BluetoothUtils; +import com.android.settingslib.bluetooth.BluetoothUtils.ErrorListener; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.bluetooth.LocalBluetoothManager.BluetoothManagerCallback; -import com.android.settingslib.bluetooth.Utils.ErrorListener; /** * Utils is a helper class that contains constants for various @@ -38,8 +41,11 @@ import com.android.settingslib.bluetooth.Utils.ErrorListener; * for creating dialogs. */ public final class Utils { - static final boolean V = com.android.settingslib.bluetooth.Utils.V; // verbose logging - static final boolean D = com.android.settingslib.bluetooth.Utils.D; // regular logging + + private static final String TAG = "BluetoothUtils"; + + static final boolean V = BluetoothUtils.V; // verbose logging + static final boolean D = BluetoothUtils.D; // regular logging private Utils() { } @@ -84,15 +90,10 @@ public final class Utils { return dialog; } - // TODO: wire this up to show connection errors... - static void showConnectingError(Context context, String name) { - showConnectingError(context, name, getLocalBtManager(context)); - } - @VisibleForTesting static void showConnectingError(Context context, String name, LocalBluetoothManager manager) { FeatureFactory.getFactory(context).getMetricsFeatureProvider().visible(context, - MetricsEvent.VIEW_UNKNOWN, MetricsEvent.ACTION_SETTINGS_BLUETOOTH_CONNECT_ERROR); + SettingsEnums.PAGE_UNKNOWN, SettingsEnums.ACTION_SETTINGS_BLUETOOTH_CONNECT_ERROR); showError(context, name, R.string.bluetooth_connecting_error_message, manager); } @@ -105,11 +106,15 @@ public final class Utils { String message = context.getString(messageResId, name); Context activity = manager.getForegroundActivity(); if (manager.isForegroundActivity()) { - new AlertDialog.Builder(activity) - .setTitle(R.string.bluetooth_error_title) - .setMessage(message) - .setPositiveButton(android.R.string.ok, null) - .show(); + try { + new AlertDialog.Builder(activity) + .setTitle(R.string.bluetooth_error_title) + .setMessage(message) + .setPositiveButton(android.R.string.ok, null) + .show(); + } catch (Exception e) { + Log.e(TAG, "Cannot show error dialog.", e); + } } else { Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); } @@ -139,7 +144,7 @@ public final class Utils { @Override public void onBluetoothManagerInitialized(Context appContext, LocalBluetoothManager bluetoothManager) { - com.android.settingslib.bluetooth.Utils.setErrorListener(mErrorListener); + BluetoothUtils.setErrorListener(mErrorListener); } }; diff --git a/src/com/android/settings/connecteddevice/AddDevicePreferenceController.java b/src/com/android/settings/connecteddevice/AddDevicePreferenceController.java index 9730515343..9706c1798c 100644 --- a/src/com/android/settings/connecteddevice/AddDevicePreferenceController.java +++ b/src/com/android/settings/connecteddevice/AddDevicePreferenceController.java @@ -21,11 +21,12 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import com.android.settings.core.BasePreferenceController; import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnStart; import com.android.settingslib.core.lifecycle.events.OnStop; @@ -67,7 +68,7 @@ public class AddDevicePreferenceController extends BasePreferenceController public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); if (isAvailable()) { - mPreference = (Preference) screen.findPreference(getPreferenceKey()); + mPreference = screen.findPreference(getPreferenceKey()); } } diff --git a/src/com/android/settings/connecteddevice/AdvancedConnectedDeviceController.java b/src/com/android/settings/connecteddevice/AdvancedConnectedDeviceController.java index b089b4a935..26c3e34555 100644 --- a/src/com/android/settings/connecteddevice/AdvancedConnectedDeviceController.java +++ b/src/com/android/settings/connecteddevice/AdvancedConnectedDeviceController.java @@ -17,7 +17,9 @@ package com.android.settings.connecteddevice; import android.content.Context; import android.provider.Settings; + import androidx.annotation.VisibleForTesting; + import com.android.settings.R; import com.android.settings.core.BasePreferenceController; import com.android.settings.nfc.NfcPreferenceController; diff --git a/src/com/android/settings/connecteddevice/AdvancedConnectedDeviceDashboardFragment.java b/src/com/android/settings/connecteddevice/AdvancedConnectedDeviceDashboardFragment.java index 5836945954..0d130d9212 100644 --- a/src/com/android/settings/connecteddevice/AdvancedConnectedDeviceDashboardFragment.java +++ b/src/com/android/settings/connecteddevice/AdvancedConnectedDeviceDashboardFragment.java @@ -15,11 +15,11 @@ */ package com.android.settings.connecteddevice; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.pm.PackageManager; import android.provider.SearchIndexableResource; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.bluetooth.BluetoothFilesPreferenceController; import com.android.settings.dashboard.DashboardFragment; @@ -28,6 +28,7 @@ import com.android.settings.print.PrintSettingPreferenceController; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.Arrays; @@ -36,6 +37,7 @@ import java.util.List; /** * This fragment contains all the advanced connection preferences(i.e, Bluetooth, NFC, USB..) */ +@SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC) public class AdvancedConnectedDeviceDashboardFragment extends DashboardFragment { private static final String TAG = "AdvancedConnectedDeviceFrag"; @@ -44,7 +46,7 @@ public class AdvancedConnectedDeviceDashboardFragment extends DashboardFragment @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.CONNECTION_DEVICE_ADVANCED; + return SettingsEnums.CONNECTION_DEVICE_ADVANCED; } @Override @@ -64,7 +66,7 @@ public class AdvancedConnectedDeviceDashboardFragment extends DashboardFragment @Override protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { - return buildControllers(context, getLifecycle()); + return buildControllers(context, getSettingsLifecycle()); } private static List<AbstractPreferenceController> buildControllers(Context context, @@ -72,7 +74,6 @@ public class AdvancedConnectedDeviceDashboardFragment extends DashboardFragment final List<AbstractPreferenceController> controllers = new ArrayList<>(); controllers.add(new BluetoothFilesPreferenceController(context)); - controllers.add(new BluetoothOnWhileDrivingPreferenceController(context)); final PrintSettingPreferenceController printerController = new PrintSettingPreferenceController(context); @@ -106,9 +107,6 @@ public class AdvancedConnectedDeviceDashboardFragment extends DashboardFragment keys.add(AndroidBeamPreferenceController.KEY_ANDROID_BEAM_SETTINGS); } - // Parent duplicate - keys.add(KEY_BLUETOOTH); - return keys; } diff --git a/src/com/android/settings/connecteddevice/AvailableMediaDeviceGroupController.java b/src/com/android/settings/connecteddevice/AvailableMediaDeviceGroupController.java index f94e81a6d4..cac46b0d0c 100644 --- a/src/com/android/settings/connecteddevice/AvailableMediaDeviceGroupController.java +++ b/src/com/android/settings/connecteddevice/AvailableMediaDeviceGroupController.java @@ -19,18 +19,20 @@ import static com.android.settingslib.Utils.isAudioModeOngoingCall; import android.content.Context; import android.content.pm.PackageManager; +import android.util.Log; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceGroup; import androidx.preference.PreferenceScreen; + +import com.android.settings.R; import com.android.settings.bluetooth.AvailableMediaBluetoothDeviceUpdater; import com.android.settings.bluetooth.BluetoothDeviceUpdater; import com.android.settings.bluetooth.Utils; import com.android.settings.core.BasePreferenceController; import com.android.settings.dashboard.DashboardFragment; -import com.android.settings.R; import com.android.settingslib.bluetooth.BluetoothCallback; -import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnStart; @@ -44,12 +46,14 @@ import com.android.settingslib.core.lifecycle.events.OnStop; public class AvailableMediaDeviceGroupController extends BasePreferenceController implements LifecycleObserver, OnStart, OnStop, DevicePreferenceCallback, BluetoothCallback { + private static final String TAG = "AvailableMediaDeviceGroupController"; private static final String KEY = "available_device_list"; @VisibleForTesting PreferenceGroup mPreferenceGroup; + @VisibleForTesting + LocalBluetoothManager mLocalBluetoothManager; private BluetoothDeviceUpdater mBluetoothDeviceUpdater; - private final LocalBluetoothManager mLocalBluetoothManager; public AvailableMediaDeviceGroupController(Context context) { super(context, KEY); @@ -58,12 +62,20 @@ public class AvailableMediaDeviceGroupController extends BasePreferenceControlle @Override public void onStart() { + if (mLocalBluetoothManager == null) { + Log.e(TAG, "onStart() Bluetooth is not supported on this device"); + return; + } mBluetoothDeviceUpdater.registerCallback(); mLocalBluetoothManager.getEventManager().registerCallback(this); } @Override public void onStop() { + if (mLocalBluetoothManager == null) { + Log.e(TAG, "onStop() Bluetooth is not supported on this device"); + return; + } mBluetoothDeviceUpdater.unregisterCallback(); mLocalBluetoothManager.getEventManager().unregisterCallback(this); } @@ -71,9 +83,11 @@ public class AvailableMediaDeviceGroupController extends BasePreferenceControlle @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); + + mPreferenceGroup = screen.findPreference(KEY); + mPreferenceGroup.setVisible(false); + if (isAvailable()) { - mPreferenceGroup = (PreferenceGroup) screen.findPreference(KEY); - mPreferenceGroup.setVisible(false); updateTitle(); mBluetoothDeviceUpdater.setPrefContext(screen.getContext()); mBluetoothDeviceUpdater.forceUpdate(); @@ -83,7 +97,7 @@ public class AvailableMediaDeviceGroupController extends BasePreferenceControlle @Override public int getAvailabilityStatus() { return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH) - ? AVAILABLE + ? AVAILABLE_UNSEARCHABLE : UNSUPPORTED_ON_DEVICE; } @@ -115,42 +129,7 @@ public class AvailableMediaDeviceGroupController extends BasePreferenceControlle @VisibleForTesting public void setBluetoothDeviceUpdater(BluetoothDeviceUpdater bluetoothDeviceUpdater) { - mBluetoothDeviceUpdater = bluetoothDeviceUpdater; - } - - @Override - public void onBluetoothStateChanged(int bluetoothState) { - // do nothing - } - - @Override - public void onScanningStateChanged(boolean started) { - // do nothing - } - - @Override - public void onDeviceAdded(CachedBluetoothDevice cachedDevice) { - // do nothing - } - - @Override - public void onDeviceDeleted(CachedBluetoothDevice cachedDevice) { - // do nothing - } - - @Override - public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) { - // do nothing - } - - @Override - public void onConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) { - // do nothing - } - - @Override - public void onActiveDeviceChanged(CachedBluetoothDevice activeDevice, int bluetoothProfile) { - // do nothing + mBluetoothDeviceUpdater = bluetoothDeviceUpdater; } @Override diff --git a/src/com/android/settings/connecteddevice/BluetoothDashboardFragment.java b/src/com/android/settings/connecteddevice/BluetoothDashboardFragment.java index b30aff1329..ecbda15bd2 100644 --- a/src/com/android/settings/connecteddevice/BluetoothDashboardFragment.java +++ b/src/com/android/settings/connecteddevice/BluetoothDashboardFragment.java @@ -15,26 +15,24 @@ */ package com.android.settings.connecteddevice; +import android.app.settings.SettingsEnums; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothManager; import android.content.Context; import android.os.Bundle; -import androidx.annotation.VisibleForTesting; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.bluetooth.BluetoothDeviceRenamePreferenceController; import com.android.settings.bluetooth.BluetoothSwitchPreferenceController; -import com.android.settings.core.SubSettingLauncher; import com.android.settings.core.TogglePreferenceController; import com.android.settings.dashboard.DashboardFragment; -import com.android.settings.location.ScanningSettings; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.SearchIndexableRaw; import com.android.settings.widget.SwitchBar; import com.android.settings.widget.SwitchBarController; import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.search.SearchIndexable; import com.android.settingslib.widget.FooterPreference; import java.util.ArrayList; @@ -44,6 +42,7 @@ import java.util.List; * Dedicated screen for allowing the user to toggle bluetooth which displays relevant information to * the user based on related settings such as bluetooth scanning. */ +@SearchIndexable(forTarget = SearchIndexable.ALL) public class BluetoothDashboardFragment extends DashboardFragment { private static final String TAG = "BluetoothDashboardFrag"; @@ -55,7 +54,7 @@ public class BluetoothDashboardFragment extends DashboardFragment { @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.BLUETOOTH_FRAGMENT; + return SettingsEnums.BLUETOOTH_FRAGMENT; } @Override @@ -93,7 +92,7 @@ public class BluetoothDashboardFragment extends DashboardFragment { mSwitchBar = activity.getSwitchBar(); mController = new BluetoothSwitchPreferenceController(activity, new SwitchBarController(mSwitchBar), mFooterPreference); - Lifecycle lifecycle = getLifecycle(); + Lifecycle lifecycle = getSettingsLifecycle(); if (lifecycle != null) { lifecycle.addObserver(mController); } diff --git a/src/com/android/settings/connecteddevice/BluetoothOnWhileDrivingPreferenceController.java b/src/com/android/settings/connecteddevice/BluetoothOnWhileDrivingPreferenceController.java deleted file mode 100644 index 6d4e8b848b..0000000000 --- a/src/com/android/settings/connecteddevice/BluetoothOnWhileDrivingPreferenceController.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2018 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.connecteddevice; - -import android.content.Context; -import android.provider.Settings; -import android.util.FeatureFlagUtils; - -import com.android.settings.core.FeatureFlags; -import com.android.settings.core.PreferenceControllerMixin; -import com.android.settings.core.TogglePreferenceController; - -/** Handles a toggle for a setting to turn on Bluetooth while driving. * */ -public class BluetoothOnWhileDrivingPreferenceController extends TogglePreferenceController - implements PreferenceControllerMixin { - static final String KEY_BLUETOOTH_ON_DRIVING = "bluetooth_on_while_driving"; - - public BluetoothOnWhileDrivingPreferenceController(Context context) { - super(context, KEY_BLUETOOTH_ON_DRIVING); - } - - @Override - public int getAvailabilityStatus() { - if (FeatureFlagUtils.isEnabled(mContext, FeatureFlags.BLUETOOTH_WHILE_DRIVING)) { - return AVAILABLE; - } - return CONDITIONALLY_UNAVAILABLE; - } - - @Override - public boolean isChecked() { - return Settings.Secure.getInt( - mContext.getContentResolver(), - Settings.Secure.BLUETOOTH_ON_WHILE_DRIVING, - 0) - != 0; - } - - @Override - public boolean setChecked(boolean isChecked) { - final int value = isChecked ? 1 : 0; - return Settings.Secure.putInt( - mContext.getContentResolver(), Settings.Secure.BLUETOOTH_ON_WHILE_DRIVING, value); - } -} diff --git a/src/com/android/settings/connecteddevice/ConnectedDeviceDashboardFragment.java b/src/com/android/settings/connecteddevice/ConnectedDeviceDashboardFragment.java index cf6bd1392c..cbabb06eb6 100644 --- a/src/com/android/settings/connecteddevice/ConnectedDeviceDashboardFragment.java +++ b/src/com/android/settings/connecteddevice/ConnectedDeviceDashboardFragment.java @@ -15,24 +15,28 @@ */ package com.android.settings.connecteddevice; -import android.app.Activity; +import android.app.settings.SettingsEnums; import android.content.Context; +import android.net.Uri; +import android.provider.DeviceConfig; import android.provider.SearchIndexableResource; + import androidx.annotation.VisibleForTesting; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; +import com.android.settings.core.SettingsUIDeviceConfig; import com.android.settings.dashboard.DashboardFragment; -import com.android.settings.dashboard.SummaryLoader; -import com.android.settings.nfc.NfcPreferenceController; import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.slices.SlicePreferenceController; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +@SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC) public class ConnectedDeviceDashboardFragment extends DashboardFragment { private static final String TAG = "ConnectedDeviceFrag"; @@ -44,7 +48,7 @@ public class ConnectedDeviceDashboardFragment extends DashboardFragment { @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.SETTINGS_CONNECTED_DEVICE_CATEGORY; + return SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY; } @Override @@ -64,7 +68,7 @@ public class ConnectedDeviceDashboardFragment extends DashboardFragment { @Override protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { - return buildPreferenceControllers(context, getLifecycle()); + return buildPreferenceControllers(context, getSettingsLifecycle()); } private static List<AbstractPreferenceController> buildPreferenceControllers(Context context, @@ -84,41 +88,17 @@ public class ConnectedDeviceDashboardFragment extends DashboardFragment { @Override public void onAttach(Context context) { super.onAttach(context); + final boolean nearbyEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI, + SettingsUIDeviceConfig.BT_NEAR_BY_SUGGESTION_ENABLED, true); use(AvailableMediaDeviceGroupController.class).init(this); use(ConnectedDeviceGroupController.class).init(this); use(PreviouslyConnectedDevicePreferenceController.class).init(this); use(DiscoverableFooterPreferenceController.class).init(this); + use(SlicePreferenceController.class).setSliceUri(nearbyEnabled + ? Uri.parse(getString(R.string.config_nearby_devices_slice_uri)) + : null); } - @VisibleForTesting - static class SummaryProvider implements SummaryLoader.SummaryProvider { - - private final Context mContext; - private final SummaryLoader mSummaryLoader; - - public SummaryProvider(Context context, SummaryLoader summaryLoader) { - mContext = context; - mSummaryLoader = summaryLoader; - } - - @Override - public void setListening(boolean listening) { - if (listening) { - mSummaryLoader.setSummary(this, mContext.getText(AdvancedConnectedDeviceController. - getConnectedDevicesSummaryResourceId(mContext))); - } - } - } - - public static final SummaryLoader.SummaryProviderFactory SUMMARY_PROVIDER_FACTORY - = new SummaryLoader.SummaryProviderFactory() { - @Override - public SummaryLoader.SummaryProvider createSummaryProvider(Activity activity, - SummaryLoader summaryLoader) { - return new SummaryProvider(activity, summaryLoader); - } - }; - /** * For Search. */ @@ -137,14 +117,5 @@ public class ConnectedDeviceDashboardFragment extends DashboardFragment { context) { return buildPreferenceControllers(context, null /* lifecycle */); } - - @Override - public List<String> getNonIndexableKeys(Context context) { - List<String> keys = super.getNonIndexableKeys(context); - // Disable because they show dynamic data - keys.add(KEY_AVAILABLE_DEVICES); - keys.add(KEY_CONNECTED_DEVICES); - return keys; - } }; } diff --git a/src/com/android/settings/connecteddevice/ConnectedDeviceGroupController.java b/src/com/android/settings/connecteddevice/ConnectedDeviceGroupController.java index aa0b6cd54f..957737a118 100644 --- a/src/com/android/settings/connecteddevice/ConnectedDeviceGroupController.java +++ b/src/com/android/settings/connecteddevice/ConnectedDeviceGroupController.java @@ -15,8 +15,9 @@ */ package com.android.settings.connecteddevice; -import android.content.pm.PackageManager; import android.content.Context; +import android.content.pm.PackageManager; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceGroup; @@ -31,9 +32,9 @@ import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.overlay.DockUpdaterFeatureProvider; import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnStart; import com.android.settingslib.core.lifecycle.events.OnStop; -import com.android.settingslib.core.lifecycle.LifecycleObserver; /** * Controller to maintain the {@link androidx.preference.PreferenceGroup} for all @@ -72,21 +73,28 @@ public class ConnectedDeviceGroupController extends BasePreferenceController @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - if (isAvailable()) { - mPreferenceGroup = (PreferenceGroup) screen.findPreference(KEY); - mPreferenceGroup.setVisible(false); - mBluetoothDeviceUpdater.setPrefContext(screen.getContext()); + mPreferenceGroup = screen.findPreference(KEY); + mPreferenceGroup.setVisible(false); + + if (isAvailable()) { + final Context context = screen.getContext(); + mBluetoothDeviceUpdater.setPrefContext(context); mBluetoothDeviceUpdater.forceUpdate(); - mConnectedUsbDeviceUpdater.initUsbPreference(screen.getContext()); + mConnectedUsbDeviceUpdater.initUsbPreference(context); + mConnectedDockUpdater.setPreferenceContext(context); mConnectedDockUpdater.forceUpdate(); } } @Override public int getAvailabilityStatus() { - return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH) - ? AVAILABLE + final PackageManager packageManager = mContext.getPackageManager(); + return (packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH) + || packageManager.hasSystemFeature(PackageManager.FEATURE_USB_ACCESSORY) + || packageManager.hasSystemFeature(PackageManager.FEATURE_USB_HOST) + || mConnectedDockUpdater != null) + ? AVAILABLE_UNSEARCHABLE : UNSUPPORTED_ON_DEVICE; } diff --git a/src/com/android/settings/connecteddevice/DiscoverableFooterPreferenceController.java b/src/com/android/settings/connecteddevice/DiscoverableFooterPreferenceController.java index cafc0a3dc2..ead33070ee 100644 --- a/src/com/android/settings/connecteddevice/DiscoverableFooterPreferenceController.java +++ b/src/com/android/settings/connecteddevice/DiscoverableFooterPreferenceController.java @@ -22,23 +22,23 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; -import androidx.preference.PreferenceScreen; import android.text.BidiFormatter; import android.text.TextUtils; -import com.android.internal.annotations.VisibleForTesting; +import androidx.annotation.VisibleForTesting; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; import com.android.settings.bluetooth.AlwaysDiscoverable; import com.android.settings.bluetooth.Utils; import com.android.settings.core.BasePreferenceController; import com.android.settings.dashboard.DashboardFragment; -import com.android.settings.R; -import com.android.settingslib.bluetooth.LocalBluetoothAdapter; import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnPause; import com.android.settingslib.core.lifecycle.events.OnResume; -import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.widget.FooterPreference; -import com.android.settingslib.widget.FooterPreferenceMixin; +import com.android.settingslib.widget.FooterPreferenceMixinCompat; /** * Controller that shows and updates the bluetooth device name @@ -49,10 +49,11 @@ public class DiscoverableFooterPreferenceController extends BasePreferenceContro @VisibleForTesting BroadcastReceiver mBluetoothChangedReceiver; - private FooterPreferenceMixin mFooterPreferenceMixin; + @VisibleForTesting + LocalBluetoothManager mLocalManager; + private FooterPreferenceMixinCompat mFooterPreferenceMixin; private FooterPreference mPreference; - private LocalBluetoothManager mLocalManager; - private LocalBluetoothAdapter mLocalAdapter; + private BluetoothAdapter mBluetoothAdapter; private AlwaysDiscoverable mAlwaysDiscoverable; public DiscoverableFooterPreferenceController(Context context) { @@ -61,8 +62,8 @@ public class DiscoverableFooterPreferenceController extends BasePreferenceContro if (mLocalManager == null) { return; } - mLocalAdapter = mLocalManager.getBluetoothAdapter(); - mAlwaysDiscoverable = new AlwaysDiscoverable(context, mLocalAdapter); + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + mAlwaysDiscoverable = new AlwaysDiscoverable(context); initReceiver(); } @@ -80,11 +81,12 @@ public class DiscoverableFooterPreferenceController extends BasePreferenceContro } public void init(DashboardFragment fragment) { - mFooterPreferenceMixin = new FooterPreferenceMixin(fragment, fragment.getLifecycle()); + mFooterPreferenceMixin = new FooterPreferenceMixinCompat(fragment, + fragment.getSettingsLifecycle()); } @VisibleForTesting - void init(FooterPreferenceMixin footerPreferenceMixin, FooterPreference preference, + void init(FooterPreferenceMixinCompat footerPreferenceMixin, FooterPreference preference, AlwaysDiscoverable alwaysDiscoverable) { mFooterPreferenceMixin = footerPreferenceMixin; mPreference = preference; @@ -112,14 +114,20 @@ public class DiscoverableFooterPreferenceController extends BasePreferenceContro @Override public void onResume() { + if (mLocalManager == null) { + return; + } mContext.registerReceiver(mBluetoothChangedReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)); mAlwaysDiscoverable.start(); - updateFooterPreferenceTitle(mLocalAdapter.getState()); + updateFooterPreferenceTitle(mBluetoothAdapter.getState()); } @Override public void onPause() { + if (mLocalManager == null) { + return; + } mContext.unregisterReceiver(mBluetoothChangedReceiver); mAlwaysDiscoverable.stop(); } @@ -133,7 +141,7 @@ public class DiscoverableFooterPreferenceController extends BasePreferenceContro } private CharSequence getPreferenceTitle() { - final String deviceName = mLocalAdapter.getName(); + final String deviceName = mBluetoothAdapter.getName(); if (TextUtils.isEmpty(deviceName)) { return null; } diff --git a/src/com/android/settings/connecteddevice/PreviouslyConnectedDeviceDashboardFragment.java b/src/com/android/settings/connecteddevice/PreviouslyConnectedDeviceDashboardFragment.java index 195daf372f..709e1dc1c4 100644 --- a/src/com/android/settings/connecteddevice/PreviouslyConnectedDeviceDashboardFragment.java +++ b/src/com/android/settings/connecteddevice/PreviouslyConnectedDeviceDashboardFragment.java @@ -15,13 +15,15 @@ */ package com.android.settings.connecteddevice; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.res.Resources; -import com.android.internal.logging.nano.MetricsProto; + import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.SearchIndexableRaw; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.List; @@ -29,6 +31,7 @@ import java.util.List; /** * This fragment contains previously connected device */ +@SearchIndexable(forTarget = SearchIndexable.MOBILE) public class PreviouslyConnectedDeviceDashboardFragment extends DashboardFragment { private static final String TAG = "PreConnectedDeviceFrag"; @@ -51,7 +54,7 @@ public class PreviouslyConnectedDeviceDashboardFragment extends DashboardFragmen @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.PREVIOUSLY_CONNECTED_DEVICES; + return SettingsEnums.PREVIOUSLY_CONNECTED_DEVICES; } @Override diff --git a/src/com/android/settings/connecteddevice/PreviouslyConnectedDevicePreferenceController.java b/src/com/android/settings/connecteddevice/PreviouslyConnectedDevicePreferenceController.java index 05506a171a..5b23d69cf9 100644 --- a/src/com/android/settings/connecteddevice/PreviouslyConnectedDevicePreferenceController.java +++ b/src/com/android/settings/connecteddevice/PreviouslyConnectedDevicePreferenceController.java @@ -17,8 +17,10 @@ package com.android.settings.connecteddevice; import android.content.Context; import android.content.pm.PackageManager; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; +import androidx.preference.PreferenceGroup; import androidx.preference.PreferenceScreen; import com.android.settings.bluetooth.BluetoothDeviceUpdater; @@ -34,7 +36,9 @@ import com.android.settingslib.core.lifecycle.events.OnStop; public class PreviouslyConnectedDevicePreferenceController extends BasePreferenceController implements LifecycleObserver, OnStart, OnStop, DevicePreferenceCallback { - private Preference mPreference; + private static final int MAX_DEVICE_NUM = 3; + + private PreferenceGroup mPreferenceGroup; private BluetoothDeviceUpdater mBluetoothDeviceUpdater; private DockUpdater mSavedDockUpdater; private int mPreferenceSize; @@ -48,7 +52,8 @@ public class PreviouslyConnectedDevicePreferenceController extends BasePreferenc @Override public int getAvailabilityStatus() { - return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH) + return (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH) + || mSavedDockUpdater != null) ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; } @@ -56,9 +61,13 @@ public class PreviouslyConnectedDevicePreferenceController extends BasePreferenc @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); + mPreferenceGroup = screen.findPreference(getPreferenceKey()); + mPreferenceGroup.setVisible(false); + if (isAvailable()) { - mPreference = screen.findPreference(getPreferenceKey()); - mBluetoothDeviceUpdater.setPrefContext(screen.getContext()); + final Context context = screen.getContext(); + mBluetoothDeviceUpdater.setPrefContext(context); + mSavedDockUpdater.setPreferenceContext(context); } } @@ -66,7 +75,6 @@ public class PreviouslyConnectedDevicePreferenceController extends BasePreferenc public void onStart() { mBluetoothDeviceUpdater.registerCallback(); mSavedDockUpdater.registerCallback(); - updatePreferenceOnSizeChanged(); } @Override @@ -83,13 +91,17 @@ public class PreviouslyConnectedDevicePreferenceController extends BasePreferenc @Override public void onDeviceAdded(Preference preference) { mPreferenceSize++; - updatePreferenceOnSizeChanged(); + if (mPreferenceSize <= MAX_DEVICE_NUM) { + mPreferenceGroup.addPreference(preference); + } + updatePreferenceVisiblity(); } @Override public void onDeviceRemoved(Preference preference) { mPreferenceSize--; - updatePreferenceOnSizeChanged(); + mPreferenceGroup.removePreference(preference); + updatePreferenceVisiblity(); } @VisibleForTesting @@ -103,18 +115,12 @@ public class PreviouslyConnectedDevicePreferenceController extends BasePreferenc } @VisibleForTesting - void setPreferenceSize(int size) { - mPreferenceSize = size; + void setPreferenceGroup(PreferenceGroup preferenceGroup) { + mPreferenceGroup = preferenceGroup; } @VisibleForTesting - void setPreference(Preference preference) { - mPreference = preference; - } - - private void updatePreferenceOnSizeChanged() { - if (isAvailable()) { - mPreference.setEnabled(mPreferenceSize != 0); - } + void updatePreferenceVisiblity() { + mPreferenceGroup.setVisible(mPreferenceSize > 0); } -}
\ No newline at end of file +} diff --git a/src/com/android/settings/connecteddevice/SavedDeviceGroupController.java b/src/com/android/settings/connecteddevice/SavedDeviceGroupController.java index 4bc9cdd637..062fa2db79 100644 --- a/src/com/android/settings/connecteddevice/SavedDeviceGroupController.java +++ b/src/com/android/settings/connecteddevice/SavedDeviceGroupController.java @@ -15,8 +15,9 @@ */ package com.android.settings.connecteddevice; -import android.content.pm.PackageManager; import android.content.Context; +import android.content.pm.PackageManager; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceGroup; @@ -72,18 +73,22 @@ public class SavedDeviceGroupController extends BasePreferenceController @Override public void displayPreference(PreferenceScreen screen) { + mPreferenceGroup = screen.findPreference(KEY); + mPreferenceGroup.setVisible(false); + if (isAvailable()) { - mPreferenceGroup = (PreferenceGroup) screen.findPreference(KEY); - mPreferenceGroup.setVisible(false); - mBluetoothDeviceUpdater.setPrefContext(screen.getContext()); + final Context context = screen.getContext(); + mBluetoothDeviceUpdater.setPrefContext(context); mBluetoothDeviceUpdater.forceUpdate(); + mSavedDockUpdater.setPreferenceContext(context); mSavedDockUpdater.forceUpdate(); } } @Override public int getAvailabilityStatus() { - return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH) + return (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH) + || mSavedDockUpdater != null) ? AVAILABLE : UNSUPPORTED_ON_DEVICE; } diff --git a/src/com/android/settings/connecteddevice/TopLevelConnectedDevicesPreferenceController.java b/src/com/android/settings/connecteddevice/TopLevelConnectedDevicesPreferenceController.java new file mode 100644 index 0000000000..21022838d2 --- /dev/null +++ b/src/com/android/settings/connecteddevice/TopLevelConnectedDevicesPreferenceController.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2018 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.connecteddevice; + +import android.content.Context; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; + +public class TopLevelConnectedDevicesPreferenceController extends BasePreferenceController { + + public TopLevelConnectedDevicesPreferenceController(Context context, + String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public int getAvailabilityStatus() { + return mContext.getResources().getBoolean(R.bool.config_show_top_level_connected_devices) + ? AVAILABLE_UNSEARCHABLE + : UNSUPPORTED_ON_DEVICE; + } + + @Override + public CharSequence getSummary() { + return mContext.getText( + AdvancedConnectedDeviceController.getConnectedDevicesSummaryResourceId(mContext)); + } +} diff --git a/src/com/android/settings/connecteddevice/dock/DockUpdater.java b/src/com/android/settings/connecteddevice/dock/DockUpdater.java index 19ee732417..99ac12475b 100644 --- a/src/com/android/settings/connecteddevice/dock/DockUpdater.java +++ b/src/com/android/settings/connecteddevice/dock/DockUpdater.java @@ -15,6 +15,7 @@ */ package com.android.settings.connecteddevice.dock; +import android.annotation.NonNull; import android.content.Context; /** @@ -40,4 +41,10 @@ public interface DockUpdater { */ default void forceUpdate() { } + + /** + * Set the context to generate the {@link Preference}, so it could get the correct theme. + */ + default void setPreferenceContext(@NonNull Context preferenceContext) { + } } diff --git a/src/com/android/settings/connecteddevice/dock/DockUpdaterFeatureProviderImpl.java b/src/com/android/settings/connecteddevice/dock/DockUpdaterFeatureProviderImpl.java index 7cd2d50851..7caaae609e 100644 --- a/src/com/android/settings/connecteddevice/dock/DockUpdaterFeatureProviderImpl.java +++ b/src/com/android/settings/connecteddevice/dock/DockUpdaterFeatureProviderImpl.java @@ -3,7 +3,6 @@ package com.android.settings.connecteddevice.dock; import android.content.Context; import com.android.settings.connecteddevice.DevicePreferenceCallback; -import com.android.settings.connecteddevice.dock.DockUpdater; import com.android.settings.overlay.DockUpdaterFeatureProvider; /** diff --git a/src/com/android/settings/connecteddevice/usb/ConnectedUsbDeviceUpdater.java b/src/com/android/settings/connecteddevice/usb/ConnectedUsbDeviceUpdater.java index 90df30880f..0be1438407 100644 --- a/src/com/android/settings/connecteddevice/usb/ConnectedUsbDeviceUpdater.java +++ b/src/com/android/settings/connecteddevice/usb/ConnectedUsbDeviceUpdater.java @@ -15,9 +15,13 @@ */ package com.android.settings.connecteddevice.usb; +import static android.hardware.usb.UsbPortStatus.DATA_ROLE_DEVICE; +import static android.hardware.usb.UsbPortStatus.POWER_ROLE_SINK; +import static android.hardware.usb.UsbPortStatus.POWER_ROLE_SOURCE; + import android.content.Context; import android.hardware.usb.UsbManager; -import android.hardware.usb.UsbPort; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; @@ -42,8 +46,8 @@ public class ConnectedUsbDeviceUpdater { UsbConnectionBroadcastReceiver.UsbConnectionListener mUsbConnectionListener = (connected, functions, powerRole, dataRole) -> { if (connected) { - mUsbPreference.setSummary(getSummary(mUsbBackend.getCurrentFunctions(), - mUsbBackend.getPowerRole())); + mUsbPreference.setSummary(getSummary(dataRole == DATA_ROLE_DEVICE + ? functions : UsbManager.FUNCTION_NONE, powerRole)); mDevicePreferenceCallback.onDeviceAdded(mUsbPreference); } else { mDevicePreferenceCallback.onDeviceRemoved(mUsbPreference); @@ -82,7 +86,7 @@ public class ConnectedUsbDeviceUpdater { // New version - uses a separate screen. new SubSettingLauncher(mFragment.getContext()) .setDestination(UsbDetailsFragment.class.getName()) - .setTitle(R.string.device_details_title) + .setTitleRes(R.string.device_details_title) .setSourceMetricsCategory(mFragment.getMetricsCategory()) .launch(); return true; @@ -99,7 +103,7 @@ public class ConnectedUsbDeviceUpdater { public static int getSummary(long functions, int power) { switch (power) { - case UsbPort.POWER_ROLE_SINK: + case POWER_ROLE_SINK: if (functions == UsbManager.FUNCTION_MTP) { return R.string.usb_summary_file_transfers; } else if (functions == UsbManager.FUNCTION_RNDIS) { @@ -111,7 +115,7 @@ public class ConnectedUsbDeviceUpdater { } else { return R.string.usb_summary_charging_only; } - case UsbPort.POWER_ROLE_SOURCE: + case POWER_ROLE_SOURCE: if (functions == UsbManager.FUNCTION_MTP) { return R.string.usb_summary_file_transfers_power; } else if (functions == UsbManager.FUNCTION_RNDIS) { diff --git a/src/com/android/settings/connecteddevice/usb/UsbBackend.java b/src/com/android/settings/connecteddevice/usb/UsbBackend.java index e030757f4d..556f76d553 100644 --- a/src/com/android/settings/connecteddevice/usb/UsbBackend.java +++ b/src/com/android/settings/connecteddevice/usb/UsbBackend.java @@ -15,6 +15,13 @@ */ package com.android.settings.connecteddevice.usb; +import static android.hardware.usb.UsbPortStatus.DATA_ROLE_DEVICE; +import static android.hardware.usb.UsbPortStatus.POWER_ROLE_NONE; +import static android.hardware.usb.UsbPortStatus.POWER_ROLE_SOURCE; +import static android.service.usb.UsbPortStatusProto.DATA_ROLE_HOST; +import static android.service.usb.UsbPortStatusProto.DATA_ROLE_NONE; +import static android.service.usb.UsbPortStatusProto.POWER_ROLE_SINK; + import android.annotation.Nullable; import android.content.Context; import android.content.pm.PackageManager; @@ -24,8 +31,11 @@ import android.hardware.usb.UsbPortStatus; import android.net.ConnectivityManager; import android.os.UserHandle; import android.os.UserManager; + import androidx.annotation.VisibleForTesting; +import java.util.List; + /** * Provides access to underlying system USB functionality. */ @@ -95,30 +105,30 @@ public class UsbBackend { public int getPowerRole() { updatePorts(); - return mPortStatus == null ? UsbPort.POWER_ROLE_NONE : mPortStatus.getCurrentPowerRole(); + return mPortStatus == null ? POWER_ROLE_NONE : mPortStatus.getCurrentPowerRole(); } public int getDataRole() { updatePorts(); - return mPortStatus == null ? UsbPort.DATA_ROLE_NONE : mPortStatus.getCurrentDataRole(); + return mPortStatus == null ? DATA_ROLE_NONE : mPortStatus.getCurrentDataRole(); } public void setPowerRole(int role) { int newDataRole = getDataRole(); if (!areAllRolesSupported()) { switch (role) { - case UsbPort.POWER_ROLE_SINK: - newDataRole = UsbPort.DATA_ROLE_DEVICE; + case POWER_ROLE_SINK: + newDataRole = DATA_ROLE_DEVICE; break; - case UsbPort.POWER_ROLE_SOURCE: - newDataRole = UsbPort.DATA_ROLE_HOST; + case POWER_ROLE_SOURCE: + newDataRole = DATA_ROLE_HOST; break; default: - newDataRole = UsbPort.DATA_ROLE_NONE; + newDataRole = DATA_ROLE_NONE; } } if (mPort != null) { - mUsbManager.setPortRoles(mPort, role, newDataRole); + mPort.setRoles(role, newDataRole); } } @@ -126,31 +136,27 @@ public class UsbBackend { int newPowerRole = getPowerRole(); if (!areAllRolesSupported()) { switch (role) { - case UsbPort.DATA_ROLE_DEVICE: - newPowerRole = UsbPort.POWER_ROLE_SINK; + case DATA_ROLE_DEVICE: + newPowerRole = POWER_ROLE_SINK; break; - case UsbPort.DATA_ROLE_HOST: - newPowerRole = UsbPort.POWER_ROLE_SOURCE; + case DATA_ROLE_HOST: + newPowerRole = POWER_ROLE_SOURCE; break; default: - newPowerRole = UsbPort.POWER_ROLE_NONE; + newPowerRole = POWER_ROLE_NONE; } } if (mPort != null) { - mUsbManager.setPortRoles(mPort, newPowerRole, role); + mPort.setRoles(newPowerRole, role); } } public boolean areAllRolesSupported() { return mPort != null && mPortStatus != null - && mPortStatus - .isRoleCombinationSupported(UsbPort.POWER_ROLE_SINK, UsbPort.DATA_ROLE_DEVICE) - && mPortStatus - .isRoleCombinationSupported(UsbPort.POWER_ROLE_SINK, UsbPort.DATA_ROLE_HOST) - && mPortStatus - .isRoleCombinationSupported(UsbPort.POWER_ROLE_SOURCE, UsbPort.DATA_ROLE_DEVICE) - && mPortStatus - .isRoleCombinationSupported(UsbPort.POWER_ROLE_SOURCE, UsbPort.DATA_ROLE_HOST); + && mPortStatus.isRoleCombinationSupported(POWER_ROLE_SINK, DATA_ROLE_DEVICE) + && mPortStatus.isRoleCombinationSupported(POWER_ROLE_SINK, DATA_ROLE_HOST) + && mPortStatus.isRoleCombinationSupported(POWER_ROLE_SOURCE, DATA_ROLE_DEVICE) + && mPortStatus.isRoleCombinationSupported(POWER_ROLE_SOURCE, DATA_ROLE_HOST); } public static String usbFunctionsToString(long functions) { @@ -204,17 +210,14 @@ public class UsbBackend { private void updatePorts() { mPort = null; mPortStatus = null; - UsbPort[] ports = mUsbManager.getPorts(); - if (ports == null) { - return; - } + List<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; + final int N = ports.size(); for (int i = 0; i < N; i++) { - UsbPortStatus status = mUsbManager.getPortStatus(ports[i]); + UsbPortStatus status = ports.get(i).getStatus(); if (status.isConnected()) { - mPort = ports[i]; + mPort = ports.get(i); mPortStatus = status; break; } diff --git a/src/com/android/settings/connecteddevice/usb/UsbConnectionBroadcastReceiver.java b/src/com/android/settings/connecteddevice/usb/UsbConnectionBroadcastReceiver.java index 1b525352d0..695a714528 100644 --- a/src/com/android/settings/connecteddevice/usb/UsbConnectionBroadcastReceiver.java +++ b/src/com/android/settings/connecteddevice/usb/UsbConnectionBroadcastReceiver.java @@ -20,12 +20,11 @@ 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 com.android.settingslib.core.lifecycle.LifecycleObserver; -import com.android.settingslib.core.lifecycle.events.OnResume; import com.android.settingslib.core.lifecycle.events.OnPause; +import com.android.settingslib.core.lifecycle.events.OnResume; /** * Receiver to receive usb update and use {@link UsbConnectionListener} to invoke callback @@ -49,8 +48,8 @@ public class UsbConnectionBroadcastReceiver extends BroadcastReceiver implements mUsbBackend = backend; mFunctions = UsbManager.FUNCTION_NONE; - mDataRole = UsbPort.DATA_ROLE_NONE; - mPowerRole = UsbPort.POWER_ROLE_NONE; + mDataRole = UsbPortStatus.DATA_ROLE_NONE; + mPowerRole = UsbPortStatus.POWER_ROLE_NONE; } @Override diff --git a/src/com/android/settings/connecteddevice/usb/UsbDefaultFragment.java b/src/com/android/settings/connecteddevice/usb/UsbDefaultFragment.java index e1b3d70049..4c00c3ea07 100644 --- a/src/com/android/settings/connecteddevice/usb/UsbDefaultFragment.java +++ b/src/com/android/settings/connecteddevice/usb/UsbDefaultFragment.java @@ -16,18 +16,23 @@ package com.android.settings.connecteddevice.usb; +import static android.net.ConnectivityManager.TETHERING_USB; + +import android.app.settings.SettingsEnums; import android.content.Context; import android.graphics.drawable.Drawable; +import android.hardware.usb.UsbManager; +import android.net.ConnectivityManager; import android.os.Bundle; -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.logging.nano.MetricsProto; +import androidx.annotation.VisibleForTesting; + import com.android.settings.R; import com.android.settings.Utils; import com.android.settings.widget.RadioButtonPickerFragment; import com.android.settingslib.widget.CandidateInfo; import com.android.settingslib.widget.FooterPreference; -import com.android.settingslib.widget.FooterPreferenceMixin; +import com.android.settingslib.widget.FooterPreferenceMixinCompat; import com.google.android.collect.Lists; @@ -39,24 +44,32 @@ import java.util.List; public class UsbDefaultFragment extends RadioButtonPickerFragment { @VisibleForTesting UsbBackend mUsbBackend; + @VisibleForTesting + ConnectivityManager mConnectivityManager; + @VisibleForTesting + OnStartTetheringCallback mOnStartTetheringCallback = new OnStartTetheringCallback(); + @VisibleForTesting + long mPreviousFunctions; @Override public void onAttach(Context context) { super.onAttach(context); mUsbBackend = new UsbBackend(context); + mConnectivityManager = context.getSystemService(ConnectivityManager.class); } @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { super.onCreatePreferences(savedInstanceState, rootKey); - FooterPreferenceMixin footer = new FooterPreferenceMixin(this, this.getLifecycle()); + FooterPreferenceMixinCompat footer = new FooterPreferenceMixinCompat(this, + this.getSettingsLifecycle()); FooterPreference pref = footer.createFooterPreference(); pref.setTitle(R.string.usb_default_info); } @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.USB_DEFAULT; + return SettingsEnums.USB_DEFAULT; } @Override @@ -103,9 +116,37 @@ public class UsbDefaultFragment extends RadioButtonPickerFragment { @Override protected boolean setDefaultKey(String key) { long functions = UsbBackend.usbFunctionsFromString(key); + mPreviousFunctions = mUsbBackend.getCurrentFunctions(); if (!Utils.isMonkeyRunning()) { - mUsbBackend.setDefaultUsbFunctions(functions); + if (functions == UsbManager.FUNCTION_RNDIS) { + // We need to have entitlement check for usb tethering, so use API in + // ConnectivityManager. + mConnectivityManager.startTethering(TETHERING_USB, true /* showProvisioningUi */, + mOnStartTetheringCallback); + } else { + mUsbBackend.setDefaultUsbFunctions(functions); + } + } return true; } + + @VisibleForTesting + final class OnStartTetheringCallback extends + ConnectivityManager.OnStartTetheringCallback { + + @Override + public void onTetheringStarted() { + super.onTetheringStarted(); + // Set default usb functions again to make internal data persistent + mUsbBackend.setDefaultUsbFunctions(UsbManager.FUNCTION_RNDIS); + } + + @Override + public void onTetheringFailed() { + super.onTetheringFailed(); + mUsbBackend.setDefaultUsbFunctions(mPreviousFunctions); + updateCandidates(); + } + } }
\ No newline at end of file diff --git a/src/com/android/settings/connecteddevice/usb/UsbDetailsController.java b/src/com/android/settings/connecteddevice/usb/UsbDetailsController.java index cb54216649..1219211a3a 100644 --- a/src/com/android/settings/connecteddevice/usb/UsbDetailsController.java +++ b/src/com/android/settings/connecteddevice/usb/UsbDetailsController.java @@ -18,9 +18,10 @@ package com.android.settings.connecteddevice.usb; import android.content.Context; import android.os.Handler; + import androidx.annotation.UiThread; +import androidx.annotation.VisibleForTesting; -import com.android.internal.annotations.VisibleForTesting; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.core.AbstractPreferenceController; diff --git a/src/com/android/settings/connecteddevice/usb/UsbDetailsDataRoleController.java b/src/com/android/settings/connecteddevice/usb/UsbDetailsDataRoleController.java index 2e336e4896..4b08b74249 100644 --- a/src/com/android/settings/connecteddevice/usb/UsbDetailsDataRoleController.java +++ b/src/com/android/settings/connecteddevice/usb/UsbDetailsDataRoleController.java @@ -16,8 +16,12 @@ package com.android.settings.connecteddevice.usb; +import static android.hardware.usb.UsbPortStatus.DATA_ROLE_DEVICE; +import static android.hardware.usb.UsbPortStatus.DATA_ROLE_HOST; +import static android.hardware.usb.UsbPortStatus.DATA_ROLE_NONE; + import android.content.Context; -import android.hardware.usb.UsbPort; + import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceScreen; @@ -53,24 +57,24 @@ public class UsbDetailsDataRoleController extends UsbDetailsController @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - mPreferenceCategory = (PreferenceCategory) screen.findPreference(getPreferenceKey()); - mHostPref = makeRadioPreference(UsbBackend.dataRoleToString(UsbPort.DATA_ROLE_HOST), + mPreferenceCategory = screen.findPreference(getPreferenceKey()); + mHostPref = makeRadioPreference(UsbBackend.dataRoleToString(DATA_ROLE_HOST), R.string.usb_control_host); - mDevicePref = makeRadioPreference(UsbBackend.dataRoleToString(UsbPort.DATA_ROLE_DEVICE), + mDevicePref = makeRadioPreference(UsbBackend.dataRoleToString(DATA_ROLE_DEVICE), R.string.usb_control_device); } @Override protected void refresh(boolean connected, long functions, int powerRole, int dataRole) { - if (dataRole == UsbPort.DATA_ROLE_DEVICE) { + if (dataRole == DATA_ROLE_DEVICE) { mDevicePref.setChecked(true); mHostPref.setChecked(false); mPreferenceCategory.setEnabled(true); - } else if (dataRole == UsbPort.DATA_ROLE_HOST) { + } else if (dataRole == DATA_ROLE_HOST) { mDevicePref.setChecked(false); mHostPref.setChecked(true); mPreferenceCategory.setEnabled(true); - } else if (!connected || dataRole == UsbPort.DATA_ROLE_NONE){ + } else if (!connected || dataRole == DATA_ROLE_NONE) { mPreferenceCategory.setEnabled(false); if (mNextRolePref == null) { // Disconnected with no operation pending, so clear subtexts @@ -79,7 +83,7 @@ public class UsbDetailsDataRoleController extends UsbDetailsController } } - if (mNextRolePref != null && dataRole != UsbPort.DATA_ROLE_NONE) { + if (mNextRolePref != null && dataRole != DATA_ROLE_NONE) { if (UsbBackend.dataRoleFromString(mNextRolePref.getKey()) == dataRole) { // Clear switching text if switch succeeded mNextRolePref.setSummary(""); diff --git a/src/com/android/settings/connecteddevice/usb/UsbDetailsFragment.java b/src/com/android/settings/connecteddevice/usb/UsbDetailsFragment.java index 0ef01d9f9c..6d8de6d5a2 100644 --- a/src/com/android/settings/connecteddevice/usb/UsbDetailsFragment.java +++ b/src/com/android/settings/connecteddevice/usb/UsbDetailsFragment.java @@ -16,16 +16,18 @@ package com.android.settings.connecteddevice.usb; +import android.app.settings.SettingsEnums; import android.content.Context; import android.provider.SearchIndexableResource; + import androidx.annotation.VisibleForTesting; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.search.SearchIndexable; import com.google.android.collect.Lists; @@ -35,6 +37,7 @@ import java.util.List; /** * Controls the USB device details and provides updates to individual controllers. */ +@SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC) public class UsbDetailsFragment extends DashboardFragment { private static final String TAG = UsbDetailsFragment.class.getSimpleName(); @@ -53,7 +56,7 @@ public class UsbDetailsFragment extends DashboardFragment { @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.USB_DEVICE_DETAILS; + return SettingsEnums.USB_DEVICE_DETAILS; } @Override @@ -72,7 +75,7 @@ public class UsbDetailsFragment extends DashboardFragment { mControllers = createControllerList(context, mUsbBackend, this); mUsbReceiver = new UsbConnectionBroadcastReceiver(context, mUsbConnectionListener, mUsbBackend); - this.getLifecycle().addObserver(mUsbReceiver); + this.getSettingsLifecycle().addObserver(mUsbReceiver); return new ArrayList<>(mControllers); } @@ -101,11 +104,6 @@ public class UsbDetailsFragment extends DashboardFragment { } @Override - public List<String> getNonIndexableKeys(Context context) { - return super.getNonIndexableKeys(context); - } - - @Override public List<AbstractPreferenceController> createPreferenceControllers( Context context) { return new ArrayList<>( diff --git a/src/com/android/settings/connecteddevice/usb/UsbDetailsFunctionsController.java b/src/com/android/settings/connecteddevice/usb/UsbDetailsFunctionsController.java index 8ae334c3a8..858a01c541 100644 --- a/src/com/android/settings/connecteddevice/usb/UsbDetailsFunctionsController.java +++ b/src/com/android/settings/connecteddevice/usb/UsbDetailsFunctionsController.java @@ -16,9 +16,14 @@ package com.android.settings.connecteddevice.usb; +import static android.hardware.usb.UsbPortStatus.DATA_ROLE_DEVICE; +import static android.net.ConnectivityManager.TETHERING_USB; + import android.content.Context; import android.hardware.usb.UsbManager; -import android.hardware.usb.UsbPort; +import android.net.ConnectivityManager; + +import androidx.annotation.VisibleForTesting; import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceScreen; @@ -46,23 +51,31 @@ public class UsbDetailsFunctionsController extends UsbDetailsController } private PreferenceCategory mProfilesContainer; + private ConnectivityManager mConnectivityManager; + @VisibleForTesting + OnStartTetheringCallback mOnStartTetheringCallback; + @VisibleForTesting + long mPreviousFunction; public UsbDetailsFunctionsController(Context context, UsbDetailsFragment fragment, UsbBackend backend) { super(context, fragment, backend); + mConnectivityManager = context.getSystemService(ConnectivityManager.class); + mOnStartTetheringCallback = new OnStartTetheringCallback(); + mPreviousFunction = mUsbBackend.getCurrentFunctions(); } @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - mProfilesContainer = (PreferenceCategory) screen.findPreference(getPreferenceKey()); + mProfilesContainer = screen.findPreference(getPreferenceKey()); } /** * Gets a switch preference for the particular option, creating it if needed. */ private RadioButtonPreference getProfilePreference(String key, int titleId) { - RadioButtonPreference pref = (RadioButtonPreference) mProfilesContainer.findPreference(key); + RadioButtonPreference pref = mProfilesContainer.findPreference(key); if (pref == null) { pref = new RadioButtonPreference(mProfilesContainer.getContext()); pref.setKey(key); @@ -75,7 +88,7 @@ public class UsbDetailsFunctionsController extends UsbDetailsController @Override protected void refresh(boolean connected, long functions, int powerRole, int dataRole) { - if (!connected || dataRole != UsbPort.DATA_ROLE_DEVICE) { + if (!connected || dataRole != DATA_ROLE_DEVICE) { mProfilesContainer.setEnabled(false); } else { // Functions are only available in device mode @@ -96,9 +109,28 @@ public class UsbDetailsFunctionsController extends UsbDetailsController @Override public void onRadioButtonClicked(RadioButtonPreference preference) { - long function = UsbBackend.usbFunctionsFromString(preference.getKey()); - if (function != mUsbBackend.getCurrentFunctions() && !Utils.isMonkeyRunning()) { - mUsbBackend.setCurrentFunctions(function); + final long function = UsbBackend.usbFunctionsFromString(preference.getKey()); + final long previousFunction = mUsbBackend.getCurrentFunctions(); + if (function != previousFunction && !Utils.isMonkeyRunning()) { + mPreviousFunction = previousFunction; + + if (function == UsbManager.FUNCTION_RNDIS) { + //Update the UI in advance to make it looks smooth + final RadioButtonPreference prevPref = + (RadioButtonPreference) mProfilesContainer.findPreference( + UsbBackend.usbFunctionsToString(mPreviousFunction)); + if (prevPref != null) { + prevPref.setChecked(false); + preference.setChecked(true); + } + + // We need to have entitlement check for usb tethering, so use API in + // ConnectivityManager. + mConnectivityManager.startTethering(TETHERING_USB, true /* showProvisioningUi */, + mOnStartTetheringCallback); + } else { + mUsbBackend.setCurrentFunctions(function); + } } } @@ -111,4 +143,15 @@ public class UsbDetailsFunctionsController extends UsbDetailsController public String getPreferenceKey() { return "usb_details_functions"; } + + @VisibleForTesting + final class OnStartTetheringCallback extends + ConnectivityManager.OnStartTetheringCallback { + + @Override + public void onTetheringFailed() { + super.onTetheringFailed(); + mUsbBackend.setCurrentFunctions(mPreviousFunction); + } + } } diff --git a/src/com/android/settings/connecteddevice/usb/UsbDetailsHeaderController.java b/src/com/android/settings/connecteddevice/usb/UsbDetailsHeaderController.java index c569e1f9bd..e151258c48 100644 --- a/src/com/android/settings/connecteddevice/usb/UsbDetailsHeaderController.java +++ b/src/com/android/settings/connecteddevice/usb/UsbDetailsHeaderController.java @@ -17,11 +17,12 @@ package com.android.settings.connecteddevice.usb; import android.content.Context; + import androidx.preference.PreferenceScreen; import com.android.settings.R; -import com.android.settings.applications.LayoutPreference; import com.android.settings.widget.EntityHeaderController; +import com.android.settingslib.widget.LayoutPreference; /** * This class adds a header with device name. @@ -39,8 +40,7 @@ public class UsbDetailsHeaderController extends UsbDetailsController { @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - final LayoutPreference headerPreference = - (LayoutPreference) screen.findPreference(KEY_DEVICE_HEADER); + final LayoutPreference headerPreference = screen.findPreference(KEY_DEVICE_HEADER); mHeaderController = EntityHeaderController.newInstance(mFragment.getActivity(), mFragment, headerPreference.findViewById(R.id.entity_header)); } diff --git a/src/com/android/settings/connecteddevice/usb/UsbDetailsPowerRoleController.java b/src/com/android/settings/connecteddevice/usb/UsbDetailsPowerRoleController.java index ae21601e78..13b3076e6f 100644 --- a/src/com/android/settings/connecteddevice/usb/UsbDetailsPowerRoleController.java +++ b/src/com/android/settings/connecteddevice/usb/UsbDetailsPowerRoleController.java @@ -16,13 +16,17 @@ package com.android.settings.connecteddevice.usb; +import static android.hardware.usb.UsbPortStatus.POWER_ROLE_NONE; +import static android.hardware.usb.UsbPortStatus.POWER_ROLE_SINK; +import static android.hardware.usb.UsbPortStatus.POWER_ROLE_SOURCE; + import android.content.Context; -import android.hardware.usb.UsbPort; -import androidx.preference.SwitchPreference; + import androidx.preference.Preference; import androidx.preference.Preference.OnPreferenceClickListener; import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreference; import com.android.settings.R; import com.android.settings.Utils; @@ -39,22 +43,22 @@ public class UsbDetailsPowerRoleController extends UsbDetailsController private int mNextPowerRole; private final Runnable mFailureCallback = () -> { - if (mNextPowerRole != UsbPort.POWER_ROLE_NONE) { + if (mNextPowerRole != POWER_ROLE_NONE) { mSwitchPreference.setSummary(R.string.usb_switching_failed); - mNextPowerRole = UsbPort.POWER_ROLE_NONE; + mNextPowerRole = POWER_ROLE_NONE; } }; public UsbDetailsPowerRoleController(Context context, UsbDetailsFragment fragment, UsbBackend backend) { super(context, fragment, backend); - mNextPowerRole = UsbPort.POWER_ROLE_NONE; + mNextPowerRole = POWER_ROLE_NONE; } @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - mPreferenceCategory = (PreferenceCategory) screen.findPreference(getPreferenceKey()); + mPreferenceCategory = screen.findPreference(getPreferenceKey()); mSwitchPreference = new SwitchPreference(mPreferenceCategory.getContext()); mSwitchPreference.setTitle(R.string.usb_use_power_only); mSwitchPreference.setOnPreferenceClickListener(this); @@ -66,23 +70,24 @@ public class UsbDetailsPowerRoleController extends UsbDetailsController // Hide this option if this is not a PD compatible connection if (connected && !mUsbBackend.areAllRolesSupported()) { mFragment.getPreferenceScreen().removePreference(mPreferenceCategory); - } else if (connected && mUsbBackend.areAllRolesSupported()){ + } else if (connected && mUsbBackend.areAllRolesSupported()) { mFragment.getPreferenceScreen().addPreference(mPreferenceCategory); } - if (powerRole == UsbPort.POWER_ROLE_SOURCE) { + if (powerRole == POWER_ROLE_SOURCE) { mSwitchPreference.setChecked(true); mPreferenceCategory.setEnabled(true); - } else if (powerRole == UsbPort.POWER_ROLE_SINK) { + } else if (powerRole == POWER_ROLE_SINK) { mSwitchPreference.setChecked(false); mPreferenceCategory.setEnabled(true); - } else if (!connected || powerRole == UsbPort.POWER_ROLE_NONE){ + } else if (!connected || powerRole == POWER_ROLE_NONE) { mPreferenceCategory.setEnabled(false); - if (mNextPowerRole == UsbPort.POWER_ROLE_NONE) { + if (mNextPowerRole == POWER_ROLE_NONE) { mSwitchPreference.setSummary(""); } } - if (mNextPowerRole != UsbPort.POWER_ROLE_NONE && powerRole != UsbPort.POWER_ROLE_NONE) { + if (mNextPowerRole != POWER_ROLE_NONE + && powerRole != POWER_ROLE_NONE) { if (mNextPowerRole == powerRole) { // Clear switching text if switch succeeded mSwitchPreference.setSummary(""); @@ -90,16 +95,16 @@ public class UsbDetailsPowerRoleController extends UsbDetailsController // Set failure text if switch failed mSwitchPreference.setSummary(R.string.usb_switching_failed); } - mNextPowerRole = UsbPort.POWER_ROLE_NONE; + mNextPowerRole = POWER_ROLE_NONE; mHandler.removeCallbacks(mFailureCallback); } } @Override public boolean onPreferenceClick(Preference preference) { - int newRole = mSwitchPreference.isChecked() ? UsbPort.POWER_ROLE_SOURCE - : UsbPort.POWER_ROLE_SINK; - if (mUsbBackend.getPowerRole() != newRole && mNextPowerRole == UsbPort.POWER_ROLE_NONE + int newRole = mSwitchPreference.isChecked() ? POWER_ROLE_SOURCE + : POWER_ROLE_SINK; + if (mUsbBackend.getPowerRole() != newRole && mNextPowerRole == POWER_ROLE_NONE && !Utils.isMonkeyRunning()) { mUsbBackend.setPowerRole(newRole); diff --git a/src/com/android/settings/core/BasePreferenceController.java b/src/com/android/settings/core/BasePreferenceController.java index 64b2477203..324b5097b0 100644 --- a/src/com/android/settings/core/BasePreferenceController.java +++ b/src/com/android/settings/core/BasePreferenceController.java @@ -15,13 +15,15 @@ package com.android.settings.core; import android.annotation.IntDef; import android.content.Context; -import android.content.IntentFilter; import android.text.TextUtils; import android.util.Log; -import com.android.settings.search.ResultPayload; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + import com.android.settings.search.SearchIndexableRaw; import com.android.settings.slices.SliceData; +import com.android.settings.slices.Sliceable; import com.android.settingslib.core.AbstractPreferenceController; import java.lang.annotation.Retention; @@ -30,16 +32,13 @@ import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.List; -import androidx.preference.Preference; -import androidx.preference.PreferenceGroup; -import androidx.preference.PreferenceScreen; - /** * Abstract class to consolidate utility between preference controllers and act as an interface * for Slices. The abstract classes that inherit from this class will act as the direct interfaces * for each type when plugging into Slices. */ -public abstract class BasePreferenceController extends AbstractPreferenceController { +public abstract class BasePreferenceController extends AbstractPreferenceController implements + Sliceable { private static final String TAG = "SettingsPrefController"; @@ -50,22 +49,27 @@ public abstract class BasePreferenceController extends AbstractPreferenceControl * {@link #isSupported()}. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({AVAILABLE, UNSUPPORTED_ON_DEVICE, DISABLED_FOR_USER, DISABLED_DEPENDENT_SETTING, - CONDITIONALLY_UNAVAILABLE}) + @IntDef({AVAILABLE, AVAILABLE_UNSEARCHABLE, UNSUPPORTED_ON_DEVICE, DISABLED_FOR_USER, + DISABLED_DEPENDENT_SETTING, CONDITIONALLY_UNAVAILABLE}) public @interface AvailabilityStatus { } /** - * The setting is available. + * The setting is available, and searchable to all search clients. */ public static final int AVAILABLE = 0; /** + * The setting is available, but is not searchable to any search client. + */ + public static final int AVAILABLE_UNSEARCHABLE = 1; + + /** * A generic catch for settings which are currently unavailable, but may become available in * the future. You should use {@link #DISABLED_FOR_USER} or {@link #DISABLED_DEPENDENT_SETTING} * if they describe the condition more accurately. */ - public static final int CONDITIONALLY_UNAVAILABLE = 1; + public static final int CONDITIONALLY_UNAVAILABLE = 2; /** * The setting is not, and will not supported by this device. @@ -73,7 +77,7 @@ public abstract class BasePreferenceController extends AbstractPreferenceControl * There is no guarantee that the setting page exists, and any links to the Setting should take * you to the home page of Settings. */ - public static final int UNSUPPORTED_ON_DEVICE = 2; + public static final int UNSUPPORTED_ON_DEVICE = 3; /** @@ -82,7 +86,7 @@ public abstract class BasePreferenceController extends AbstractPreferenceControl * Links to the Setting should take you to the page of the Setting, even if it cannot be * changed. */ - public static final int DISABLED_FOR_USER = 3; + public static final int DISABLED_FOR_USER = 4; /** * The setting has a dependency in the Settings App which is currently blocking access. @@ -99,10 +103,11 @@ public abstract class BasePreferenceController extends AbstractPreferenceControl * Links to the Setting should take you to the page of the Setting, even if it cannot be * changed. */ - public static final int DISABLED_DEPENDENT_SETTING = 4; + public static final int DISABLED_DEPENDENT_SETTING = 5; protected final String mPreferenceKey; + protected UiBlockListener mUiBlockListener; /** * Instantiate a controller as specified controller type and user-defined key. @@ -115,7 +120,7 @@ public abstract class BasePreferenceController extends AbstractPreferenceControl final Class<?> clazz = Class.forName(controllerName); final Constructor<?> preferenceConstructor = clazz.getConstructor(Context.class, String.class); - final Object[] params = new Object[] {context, key}; + final Object[] params = new Object[]{context, key}; return (BasePreferenceController) preferenceConstructor.newInstance(params); } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalArgumentException | InvocationTargetException | IllegalAccessException e) { @@ -133,7 +138,7 @@ public abstract class BasePreferenceController extends AbstractPreferenceControl try { final Class<?> clazz = Class.forName(controllerName); final Constructor<?> preferenceConstructor = clazz.getConstructor(Context.class); - final Object[] params = new Object[] {context}; + final Object[] params = new Object[]{context}; return (BasePreferenceController) preferenceConstructor.newInstance(params); } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalArgumentException | InvocationTargetException | IllegalAccessException e) { @@ -184,6 +189,7 @@ public abstract class BasePreferenceController extends AbstractPreferenceControl public final boolean isAvailable() { final int availabilityStatus = getAvailabilityStatus(); return (availabilityStatus == AVAILABLE + || availabilityStatus == AVAILABLE_UNSEARCHABLE || availabilityStatus == DISABLED_DEPENDENT_SETTING); } @@ -222,56 +228,24 @@ public abstract class BasePreferenceController extends AbstractPreferenceControl } /** - * @return an {@link IntentFilter} that includes all broadcasts which can affect the state of - * this Setting. - */ - public IntentFilter getIntentFilter() { - return null; - } - - /** - * Determines if the controller should be used as a Slice. - * <p> - * Important criteria for a Slice are: - * - Must be secure - * - Must not be a privacy leak - * - Must be understandable as a stand-alone Setting. - * <p> - * This does not guarantee the setting is available. {@link #isAvailable()} should sill be - * called. - * - * @return {@code true} if the controller should be used externally as a Slice. - */ - public boolean isSliceable() { - return false; - } - - /** - * @return {@code true} if the setting update asynchronously. - * <p> - * For example, a Wifi controller would return true, because it needs to update the radio - * and wait for it to turn on. - */ - public boolean hasAsyncUpdate() { - return false; - } - - /** * Updates non-indexable keys for search provider. * * Called by SearchIndexProvider#getNonIndexableKeys */ public void updateNonIndexableKeys(List<String> keys) { - if (this instanceof AbstractPreferenceController) { - if (!isAvailable()) { - final String key = getPreferenceKey(); - if (TextUtils.isEmpty(key)) { - Log.w(TAG, - "Skipping updateNonIndexableKeys due to empty key " + this.toString()); - return; - } - keys.add(key); + final boolean shouldSuppressFromSearch = !isAvailable() + || getAvailabilityStatus() == AVAILABLE_UNSEARCHABLE; + if (shouldSuppressFromSearch) { + final String key = getPreferenceKey(); + if (TextUtils.isEmpty(key)) { + Log.w(TAG, "Skipping updateNonIndexableKeys due to empty key " + toString()); + return; + } + if (keys.contains(key)) { + Log.w(TAG, "Skipping updateNonIndexableKeys, key already in list. " + toString()); + return; } + keys.add(key); } } @@ -284,12 +258,37 @@ public abstract class BasePreferenceController extends AbstractPreferenceControl } /** - * @return the {@link ResultPayload} corresponding to the search result type for the preference. - * TODO (b/69808376) Remove this method. - * Do not extend this method. It will not launch with P. + * Set {@link UiBlockListener} + * + * @param uiBlockListener listener to set + */ + public void setUiBlockListener(UiBlockListener uiBlockListener) { + mUiBlockListener = uiBlockListener; + } + + /** + * Listener to invoke when background job is finished + */ + public interface UiBlockListener { + /** + * To notify client that UI related background work is finished. + * (i.e. Slice is fully loaded.) + * + * @param controller Controller that contains background work + */ + void onBlockerWorkFinished(BasePreferenceController controller); + } + + /** + * Used for {@link BasePreferenceController} to decide whether it is ui blocker. + * If it is, entire UI will be invisible for a certain period until controller + * invokes {@link UiBlockListener} + * + * This won't block UI thread however has similar side effect. Please use it if you + * want to avoid janky animation(i.e. new preference is added in the middle of page). + * + * This music be used in {@link BasePreferenceController} */ - @Deprecated - public ResultPayload getResultPayload() { - return null; + public interface UiBlocker { } }
\ No newline at end of file diff --git a/src/com/android/settings/core/FeatureFlags.java b/src/com/android/settings/core/FeatureFlags.java index db941a6dd7..fee9c42a82 100644 --- a/src/com/android/settings/core/FeatureFlags.java +++ b/src/com/android/settings/core/FeatureFlags.java @@ -20,11 +20,11 @@ package com.android.settings.core; * This class keeps track of all feature flags in Settings. */ public class FeatureFlags { - public static final String BATTERY_DISPLAY_APP_LIST = "settings_battery_display_app_list"; - public static final String ZONE_PICKER_V2 = "settings_zone_picker_v2"; - public static final String ABOUT_PHONE_V2 = "settings_about_phone_v2"; - public static final String BLUETOOTH_WHILE_DRIVING = "settings_bluetooth_while_driving"; - public static final String DATA_USAGE_SETTINGS_V2 = "settings_data_usage_v2"; public static final String AUDIO_SWITCHER_SETTINGS = "settings_audio_switcher"; + public static final String DYNAMIC_SYSTEM = "settings_dynamic_system"; public static final String HEARING_AID_SETTINGS = "settings_bluetooth_hearing_aid"; + public static final String MOBILE_NETWORK_V2 = "settings_mobile_network_v2"; + public static final String NETWORK_INTERNET_V2 = "settings_network_and_internet_v2"; + public static final String WIFI_DETAILS_DATAUSAGE_HEADER = + "settings_wifi_details_datausage_header"; } diff --git a/src/com/android/settings/core/HideNonSystemOverlayMixin.java b/src/com/android/settings/core/HideNonSystemOverlayMixin.java new file mode 100644 index 0000000000..4b8975db33 --- /dev/null +++ b/src/com/android/settings/core/HideNonSystemOverlayMixin.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2019 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.core; + +import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; + +import static androidx.lifecycle.Lifecycle.Event.ON_START; +import static androidx.lifecycle.Lifecycle.Event.ON_STOP; + +import android.app.Activity; +import android.os.Build; +import android.view.Window; +import android.view.WindowManager; + +import androidx.annotation.VisibleForTesting; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.OnLifecycleEvent; + + +/** + * A mixin that adds window flag to prevent non-system overlays showing on top of Settings + * activities. + */ +public class HideNonSystemOverlayMixin implements LifecycleObserver { + + private final Activity mActivity; + + public HideNonSystemOverlayMixin(Activity activity) { + mActivity = activity; + } + + @VisibleForTesting + boolean isEnabled() { + return !Build.IS_DEBUGGABLE; + } + + @OnLifecycleEvent(ON_START) + public void onStart() { + if (mActivity == null || !isEnabled()) { + return; + } + mActivity.getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); + android.util.EventLog.writeEvent(0x534e4554, "120484087", -1, ""); + } + + + @OnLifecycleEvent(ON_STOP) + public void onStop() { + if (mActivity == null || !isEnabled()) { + return; + } + final Window window = mActivity.getWindow(); + final WindowManager.LayoutParams attrs = window.getAttributes(); + attrs.privateFlags &= ~SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; + window.setAttributes(attrs); + } +} diff --git a/src/com/android/settings/core/InstrumentedActivity.java b/src/com/android/settings/core/InstrumentedActivity.java index 5ec8505bbb..be350a8f3d 100644 --- a/src/com/android/settings/core/InstrumentedActivity.java +++ b/src/com/android/settings/core/InstrumentedActivity.java @@ -32,7 +32,7 @@ public abstract class InstrumentedActivity extends ObservableActivity implements protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Mixin that logs visibility change for activity. - getLifecycle().addObserver(new VisibilityLoggerMixin(getMetricsCategory(), + getSettingsLifecycle().addObserver(new VisibilityLoggerMixin(getMetricsCategory(), FeatureFactory.getFactory(this).getMetricsFeatureProvider())); } } diff --git a/src/com/android/settings/core/InstrumentedFragment.java b/src/com/android/settings/core/InstrumentedFragment.java index b1215b9ac4..427e33aab6 100644 --- a/src/com/android/settings/core/InstrumentedFragment.java +++ b/src/com/android/settings/core/InstrumentedFragment.java @@ -37,8 +37,8 @@ public abstract class InstrumentedFragment extends ObservableFragment implements mVisibilityLoggerMixin = new VisibilityLoggerMixin(getMetricsCategory(), mMetricsFeatureProvider); // Mixin that logs visibility change for activity. - getLifecycle().addObserver(mVisibilityLoggerMixin); - getLifecycle().addObserver(new SurveyMixin(this, getClass().getSimpleName())); + getSettingsLifecycle().addObserver(mVisibilityLoggerMixin); + getSettingsLifecycle().addObserver(new SurveyMixin(this, getClass().getSimpleName())); super.onAttach(context); } diff --git a/src/com/android/settings/core/InstrumentedPreferenceFragment.java b/src/com/android/settings/core/InstrumentedPreferenceFragment.java index 3a3c3d9e19..f5f245fad6 100644 --- a/src/com/android/settings/core/InstrumentedPreferenceFragment.java +++ b/src/com/android/settings/core/InstrumentedPreferenceFragment.java @@ -18,11 +18,13 @@ package com.android.settings.core; import android.content.Context; import android.os.Bundle; -import androidx.annotation.XmlRes; -import androidx.preference.PreferenceScreen; import android.text.TextUtils; import android.util.Log; +import androidx.annotation.XmlRes; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + import com.android.settings.overlay.FeatureFactory; import com.android.settings.survey.SurveyMixin; import com.android.settingslib.core.instrumentation.Instrumentable; @@ -52,8 +54,8 @@ public abstract class InstrumentedPreferenceFragment extends ObservablePreferenc // Mixin that logs visibility change for activity. mVisibilityLoggerMixin = new VisibilityLoggerMixin(getMetricsCategory(), mMetricsFeatureProvider); - getLifecycle().addObserver(mVisibilityLoggerMixin); - getLifecycle().addObserver(new SurveyMixin(this, getClass().getSimpleName())); + getSettingsLifecycle().addObserver(mVisibilityLoggerMixin); + getSettingsLifecycle().addObserver(new SurveyMixin(this, getClass().getSimpleName())); super.onAttach(context); } @@ -77,6 +79,14 @@ public abstract class InstrumentedPreferenceFragment extends ObservablePreferenc updateActivityTitleWithScreenTitle(getPreferenceScreen()); } + @Override + public <T extends Preference> T findPreference(CharSequence key) { + if (key == null) { + return null; + } + return super.findPreference(key); + } + protected final Context getPrefContext() { return getPreferenceManager().getContext(); } diff --git a/src/com/android/settings/core/OnActivityResultListener.java b/src/com/android/settings/core/OnActivityResultListener.java new file mode 100644 index 0000000000..5832c7f892 --- /dev/null +++ b/src/com/android/settings/core/OnActivityResultListener.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2018 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.core; + +import android.app.Activity; +import android.content.Intent; + +/** + * This interface marks a class that it wants to listen to + * {@link Activity#onActivityResult(int, int, Intent)}. + * + * Whenever {@link com.android.settings.SettingsActivity} receives an activity result, it will + * propagate the data to this interface so it has a chance to inspect and handle activity results. + */ +public interface OnActivityResultListener { +} diff --git a/src/com/android/settings/core/PreferenceControllerListHelper.java b/src/com/android/settings/core/PreferenceControllerListHelper.java index 738a6952e8..5879ba443b 100644 --- a/src/com/android/settings/core/PreferenceControllerListHelper.java +++ b/src/com/android/settings/core/PreferenceControllerListHelper.java @@ -54,7 +54,8 @@ public class PreferenceControllerListHelper { List<Bundle> preferenceMetadata; try { preferenceMetadata = PreferenceXmlParserUtils.extractMetadata(context, xmlResId, - MetadataFlag.FLAG_NEED_KEY | MetadataFlag.FLAG_NEED_PREF_CONTROLLER); + MetadataFlag.FLAG_NEED_KEY | MetadataFlag.FLAG_NEED_PREF_CONTROLLER + | MetadataFlag.FLAG_INCLUDE_PREF_SCREEN); } catch (IOException | XmlPullParserException e) { Log.e(TAG, "Failed to parse preference xml for getting controllers", e); return controllers; diff --git a/src/com/android/settings/core/PreferenceControllerMixin.java b/src/com/android/settings/core/PreferenceControllerMixin.java index 23facbaeaa..da0b7e73d1 100644 --- a/src/com/android/settings/core/PreferenceControllerMixin.java +++ b/src/com/android/settings/core/PreferenceControllerMixin.java @@ -18,7 +18,6 @@ package com.android.settings.core; import android.text.TextUtils; import android.util.Log; -import com.android.settings.search.ResultPayload; import com.android.settings.search.SearchIndexableRaw; import com.android.settingslib.core.AbstractPreferenceController; @@ -43,7 +42,12 @@ public interface PreferenceControllerMixin { final String key = ((AbstractPreferenceController) this).getPreferenceKey(); if (TextUtils.isEmpty(key)) { Log.w(TAG, - "Skipping updateNonIndexableKeys due to empty key " + this.toString()); + "Skipping updateNonIndexableKeys due to empty key " + toString()); + return; + } + if (keys.contains(key)) { + Log.w(TAG, "Skipping updateNonIndexableKeys, key already in list. " + + toString()); return; } keys.add(key); @@ -58,15 +62,4 @@ public interface PreferenceControllerMixin { */ default void updateRawDataToIndex(List<SearchIndexableRaw> rawData) { } - - /** - * @return the {@link ResultPayload} corresponding to the search result type for the preference. - * - * Do not rely on this method for intent-based or inline results. It will be removed in the - * unbundling effort. - */ - @Deprecated - default ResultPayload getResultPayload() { - return null; - } } diff --git a/src/com/android/settings/core/PreferenceXmlParserUtils.java b/src/com/android/settings/core/PreferenceXmlParserUtils.java index a0534ab48c..5eebee4ab7 100644 --- a/src/com/android/settings/core/PreferenceXmlParserUtils.java +++ b/src/com/android/settings/core/PreferenceXmlParserUtils.java @@ -23,14 +23,15 @@ import android.content.Context; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; import android.os.Bundle; -import androidx.annotation.IntDef; -import androidx.annotation.VisibleForTesting; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; import android.util.TypedValue; import android.util.Xml; +import androidx.annotation.IntDef; +import androidx.annotation.VisibleForTesting; + import com.android.settings.R; import org.xmlpull.v1.XmlPullParser; @@ -52,7 +53,10 @@ public class PreferenceXmlParserUtils { @VisibleForTesting static final String PREF_SCREEN_TAG = "PreferenceScreen"; private static final List<String> SUPPORTED_PREF_TYPES = Arrays.asList( - "Preference", "PreferenceCategory", "PreferenceScreen"); + "Preference", "PreferenceCategory", "PreferenceScreen", + "com.android.settings.widget.WorkOnlyCategory"); + public static final int PREPEND_VALUE = 0; + public static final int APPEND_VALUE = 1; /** * Flag definition to indicate which metadata should be extracted when @@ -66,9 +70,12 @@ public class PreferenceXmlParserUtils { MetadataFlag.FLAG_NEED_PREF_CONTROLLER, MetadataFlag.FLAG_NEED_PREF_TITLE, MetadataFlag.FLAG_NEED_PREF_SUMMARY, - MetadataFlag.FLAG_NEED_PREF_ICON}) + MetadataFlag.FLAG_NEED_PREF_ICON, + MetadataFlag.FLAG_NEED_SEARCHABLE, + MetadataFlag.FLAG_UNAVAILABLE_SLICE_SUBTITLE}) @Retention(RetentionPolicy.SOURCE) public @interface MetadataFlag { + int FLAG_INCLUDE_PREF_SCREEN = 1; int FLAG_NEED_KEY = 1 << 1; int FLAG_NEED_PREF_TYPE = 1 << 2; @@ -78,6 +85,9 @@ public class PreferenceXmlParserUtils { int FLAG_NEED_PREF_ICON = 1 << 6; int FLAG_NEED_PLATFORM_SLICE_FLAG = 1 << 7; int FLAG_NEED_KEYWORDS = 1 << 8; + int FLAG_NEED_SEARCHABLE = 1 << 9; + int FLAG_NEED_PREF_APPEND = 1 << 10; + int FLAG_UNAVAILABLE_SLICE_SUBTITLE = 1 << 11; } public static final String METADATA_PREF_TYPE = "type"; @@ -88,6 +98,10 @@ public class PreferenceXmlParserUtils { public static final String METADATA_ICON = "icon"; public static final String METADATA_PLATFORM_SLICE_FLAG = "platform_slice"; public static final String METADATA_KEYWORDS = "keywords"; + public static final String METADATA_SEARCHABLE = "searchable"; + public static final String METADATA_APPEND = "staticPreferenceLocation"; + public static final String METADATA_UNAVAILABLE_SLICE_SUBTITLE = + "unavailable_slice_subtitle"; private static final String ENTRIES_SEPARATOR = "|"; @@ -154,18 +168,6 @@ public class PreferenceXmlParserUtils { } /** - * Call {@link #extractMetadata(Context, int, int)} with {@link #METADATA_ICON} instead. - */ - @Deprecated - public static int getDataIcon(Context context, AttributeSet attrs) { - final TypedArray ta = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.Preference); - final int dataIcon = ta.getResourceId(com.android.internal.R.styleable.Icon_icon, 0); - ta.recycle(); - return dataIcon; - } - - /** * Extracts metadata from preference xml and put them into a {@link Bundle}. * * @param xmlResId xml res id of a preference screen @@ -187,14 +189,13 @@ public class PreferenceXmlParserUtils { // Parse next until start tag is found } final int outerDepth = parser.getDepth(); - + final boolean hasPrefScreenFlag = hasFlag(flags, MetadataFlag.FLAG_INCLUDE_PREF_SCREEN); do { if (type != XmlPullParser.START_TAG) { continue; } final String nodeName = parser.getName(); - if (!hasFlag(flags, MetadataFlag.FLAG_INCLUDE_PREF_SCREEN) - && TextUtils.equals(PREF_SCREEN_TAG, nodeName)) { + if (!hasPrefScreenFlag && TextUtils.equals(PREF_SCREEN_TAG, nodeName)) { continue; } if (!SUPPORTED_PREF_TYPES.contains(nodeName) && !nodeName.endsWith("Preference")) { @@ -202,8 +203,14 @@ public class PreferenceXmlParserUtils { } final Bundle preferenceMetadata = new Bundle(); final AttributeSet attrs = Xml.asAttributeSet(parser); + final TypedArray preferenceAttributes = context.obtainStyledAttributes(attrs, R.styleable.Preference); + TypedArray preferenceScreenAttributes = null; + if (hasPrefScreenFlag) { + preferenceScreenAttributes = context.obtainStyledAttributes( + attrs, R.styleable.PreferenceScreen); + } if (hasFlag(flags, MetadataFlag.FLAG_NEED_PREF_TYPE)) { preferenceMetadata.putString(METADATA_PREF_TYPE, nodeName); @@ -231,6 +238,18 @@ public class PreferenceXmlParserUtils { if (hasFlag(flags, MetadataFlag.FLAG_NEED_KEYWORDS)) { preferenceMetadata.putString(METADATA_KEYWORDS, getKeywords(preferenceAttributes)); } + if (hasFlag(flags, MetadataFlag.FLAG_NEED_SEARCHABLE)) { + preferenceMetadata.putBoolean(METADATA_SEARCHABLE, + isSearchable(preferenceAttributes)); + } + if (hasFlag(flags, MetadataFlag.FLAG_NEED_PREF_APPEND) && hasPrefScreenFlag) { + preferenceMetadata.putBoolean(METADATA_APPEND, + isAppended(preferenceScreenAttributes)); + } + if (hasFlag(flags, MetadataFlag.FLAG_UNAVAILABLE_SLICE_SUBTITLE)) { + preferenceMetadata.putString(METADATA_UNAVAILABLE_SLICE_SUBTITLE, + getUnavailableSliceSubtitle(preferenceAttributes)); + } metadata.add(preferenceMetadata); preferenceAttributes.recycle(); @@ -241,14 +260,6 @@ public class PreferenceXmlParserUtils { } /** - * Returns the fragment name if this preference launches a child fragment. - */ - public static String getDataChildFragment(Context context, AttributeSet attrs) { - return getStringData(context, attrs, R.styleable.Preference, - R.styleable.Preference_android_fragment); - } - - /** * Call {@link #extractMetadata(Context, int, int)} with a {@link MetadataFlag} instead. */ @Deprecated @@ -311,7 +322,21 @@ public class PreferenceXmlParserUtils { return styledAttributes.getBoolean(R.styleable.Preference_platform_slice, false /* def */); } - private static String getKeywords(TypedArray styleAttributes) { - return styleAttributes.getString(R.styleable.Preference_keywords); + private static boolean isSearchable(TypedArray styledAttributes) { + return styledAttributes.getBoolean(R.styleable.Preference_searchable, true /* default */); + } + + private static String getKeywords(TypedArray styledAttributes) { + return styledAttributes.getString(R.styleable.Preference_keywords); + } + + private static boolean isAppended(TypedArray styledAttributes) { + return styledAttributes.getInt(R.styleable.PreferenceScreen_staticPreferenceLocation, + PREPEND_VALUE) == APPEND_VALUE; + } + + private static String getUnavailableSliceSubtitle(TypedArray styledAttributes) { + return styledAttributes.getString( + R.styleable.Preference_unavailableSliceSubtitle); } -} +}
\ No newline at end of file diff --git a/src/com/android/settings/core/SettingsBaseActivity.java b/src/com/android/settings/core/SettingsBaseActivity.java new file mode 100644 index 0000000000..5ff81d54aa --- /dev/null +++ b/src/com/android/settings/core/SettingsBaseActivity.java @@ -0,0 +1,211 @@ +/** + * Copyright (C) 2018 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.core; + +import android.annotation.LayoutRes; +import android.annotation.Nullable; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.res.TypedArray; +import android.os.AsyncTask; +import android.os.Bundle; +import android.util.ArraySet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.widget.Toolbar; + +import androidx.fragment.app.FragmentActivity; + +import com.android.settings.R; +import com.android.settings.SubSettings; +import com.android.settings.dashboard.CategoryManager; + +import com.google.android.setupcompat.util.WizardManagerHelper; + +import java.util.ArrayList; +import java.util.List; + +public class SettingsBaseActivity extends FragmentActivity { + + protected static final boolean DEBUG_TIMING = false; + private static final String TAG = "SettingsBaseActivity"; + private static final String DATA_SCHEME_PKG = "package"; + + // Serves as a temporary list of tiles to ignore until we heard back from the PM that they + // are disabled. + private static ArraySet<ComponentName> sTileBlacklist = new ArraySet<>(); + + private final PackageReceiver mPackageReceiver = new PackageReceiver(); + private final List<CategoryListener> mCategoryListeners = new ArrayList<>(); + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + final long startTime = System.currentTimeMillis(); + getLifecycle().addObserver(new HideNonSystemOverlayMixin(this)); + + final TypedArray theme = getTheme().obtainStyledAttributes(android.R.styleable.Theme); + if (!theme.getBoolean(android.R.styleable.Theme_windowNoTitle, false)) { + requestWindowFeature(Window.FEATURE_NO_TITLE); + } + // Apply SetupWizard light theme during setup flow. This is for SubSettings pages. + if (WizardManagerHelper.isAnySetupWizard(getIntent()) && this instanceof SubSettings) { + setTheme(R.style.LightTheme_SubSettings_SetupWizard); + } + super.setContentView(R.layout.settings_base_layout); + + final Toolbar toolbar = findViewById(R.id.action_bar); + if (theme.getBoolean(android.R.styleable.Theme_windowNoTitle, false)) { + toolbar.setVisibility(View.GONE); + return; + } + setActionBar(toolbar); + + if (DEBUG_TIMING) { + Log.d(TAG, "onCreate took " + (System.currentTimeMillis() - startTime) + + " ms"); + } + } + + @Override + public boolean onNavigateUp() { + if (!super.onNavigateUp()) { + finish(); + } + return true; + } + + @Override + protected void onResume() { + super.onResume(); + final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addAction(Intent.ACTION_PACKAGE_CHANGED); + filter.addAction(Intent.ACTION_PACKAGE_REPLACED); + filter.addDataScheme(DATA_SCHEME_PKG); + registerReceiver(mPackageReceiver, filter); + + new CategoriesUpdateTask().execute(); + } + + @Override + protected void onPause() { + unregisterReceiver(mPackageReceiver); + super.onPause(); + } + + public void addCategoryListener(CategoryListener listener) { + mCategoryListeners.add(listener); + } + + public void remCategoryListener(CategoryListener listener) { + mCategoryListeners.remove(listener); + } + + @Override + public void setContentView(@LayoutRes int layoutResID) { + final ViewGroup parent = findViewById(R.id.content_frame); + if (parent != null) { + parent.removeAllViews(); + } + LayoutInflater.from(this).inflate(layoutResID, parent); + } + + @Override + public void setContentView(View view) { + ((ViewGroup) findViewById(R.id.content_frame)).addView(view); + } + + @Override + public void setContentView(View view, ViewGroup.LayoutParams params) { + ((ViewGroup) findViewById(R.id.content_frame)).addView(view, params); + } + + private void onCategoriesChanged() { + final int N = mCategoryListeners.size(); + for (int i = 0; i < N; i++) { + mCategoryListeners.get(i).onCategoriesChanged(); + } + } + + /** + * @return whether or not the enabled state actually changed. + */ + public boolean setTileEnabled(ComponentName component, boolean enabled) { + final PackageManager pm = getPackageManager(); + int state = pm.getComponentEnabledSetting(component); + boolean isEnabled = state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED; + if (isEnabled != enabled || state == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) { + if (enabled) { + sTileBlacklist.remove(component); + } else { + sTileBlacklist.add(component); + } + pm.setComponentEnabledSetting(component, enabled + ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED + : PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP); + return true; + } + return false; + } + + /** + * Updates dashboard categories. Only necessary to call this after setTileEnabled + */ + public void updateCategories() { + new CategoriesUpdateTask().execute(); + } + + public interface CategoryListener { + void onCategoriesChanged(); + } + + private class CategoriesUpdateTask extends AsyncTask<Void, Void, Void> { + + private final CategoryManager mCategoryManager; + + public CategoriesUpdateTask() { + mCategoryManager = CategoryManager.get(SettingsBaseActivity.this); + } + + @Override + protected Void doInBackground(Void... params) { + mCategoryManager.reloadAllCategories(SettingsBaseActivity.this); + return null; + } + + @Override + protected void onPostExecute(Void result) { + mCategoryManager.updateCategoryFromBlacklist(sTileBlacklist); + onCategoriesChanged(); + } + } + + private class PackageReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + new CategoriesUpdateTask().execute(); + } + } +} diff --git a/src/com/android/settings/core/SettingsUIDeviceConfig.java b/src/com/android/settings/core/SettingsUIDeviceConfig.java new file mode 100644 index 0000000000..8c85c82340 --- /dev/null +++ b/src/com/android/settings/core/SettingsUIDeviceConfig.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2019 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.core; + +/** + * Class to store keys for settings related features, which comes from + * {@link android.provider.DeviceConfig} + */ +public class SettingsUIDeviceConfig { + /** + * {@code true} if slice settings is enabled in BT device detail page + */ + public static final String BT_SLICE_SETTINGS_ENABLED = "bt_slice_settings_enabled"; + /** + * {@code true} if advanced header is enabled in BT device detail page + */ + public static final String BT_ADVANCED_HEADER_ENABLED = "bt_advanced_header_enabled"; + /** + * {@code true} if near by device suggestion is enabled in connected device page + */ + public static final String BT_NEAR_BY_SUGGESTION_ENABLED = "bt_near_by_suggestion_enabled"; + + /** + * {@code true} whether or not event_log for generic actions is enabled. Default is true. + */ + public static final String GENERIC_EVENT_LOGGING_ENABLED = "event_logging_enabled"; +} diff --git a/src/com/android/settings/core/SliderPreferenceController.java b/src/com/android/settings/core/SliderPreferenceController.java index 65c71eb421..0a7ece25d4 100644 --- a/src/com/android/settings/core/SliderPreferenceController.java +++ b/src/com/android/settings/core/SliderPreferenceController.java @@ -15,6 +15,7 @@ package com.android.settings.core; import android.content.Context; + import androidx.preference.Preference; import com.android.settings.slices.SliceData; @@ -43,7 +44,7 @@ public abstract class SliderPreferenceController extends BasePreferenceControlle } /** - * @return the value of the Slider's position based on the range: [0, maxSteps). + * @return the value of the Slider's position based on the range: [min, max]. */ public abstract int getSliderPosition(); @@ -56,9 +57,14 @@ public abstract class SliderPreferenceController extends BasePreferenceControlle public abstract boolean setSliderPosition(int position); /** - * @return the number of steps supported by the slider. + * @return the maximum value supported by the slider. + */ + public abstract int getMax(); + + /** + * @return the minimum value supported by the slider. */ - public abstract int getMaxSteps(); + public abstract int getMin(); @Override public int getSliceType() { diff --git a/src/com/android/settings/core/SubSettingLauncher.java b/src/com/android/settings/core/SubSettingLauncher.java index 88fbb701d6..5d9a528202 100644 --- a/src/com/android/settings/core/SubSettingLauncher.java +++ b/src/com/android/settings/core/SubSettingLauncher.java @@ -17,17 +17,19 @@ package com.android.settings.core; import android.annotation.StringRes; -import android.app.Fragment; import android.content.Context; import android.content.Intent; +import android.os.Build; import android.os.Bundle; import android.os.UserHandle; -import androidx.annotation.VisibleForTesting; import android.text.TextUtils; +import androidx.annotation.VisibleForTesting; +import androidx.fragment.app.Fragment; + import com.android.settings.SettingsActivity; import com.android.settings.SubSettings; -import com.android.settingslib.core.instrumentation.VisibilityLoggerMixin; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; public class SubSettingLauncher { @@ -48,29 +50,48 @@ public class SubSettingLauncher { return this; } - public SubSettingLauncher setTitle(@StringRes int titleResId) { - return setTitle(null /*titlePackageName*/, titleResId); + /** + * Set title with resource string id. + * + * @param titleResId res id of string + */ + public SubSettingLauncher setTitleRes(@StringRes int titleResId) { + return setTitleRes(null /*titlePackageName*/, titleResId); } - public SubSettingLauncher setTitle(String titlePackageName, @StringRes int titleResId) { + /** + * Set title with resource string id, and package name to resolve the resource id. + * + * @param titlePackageName package name to resolve resource + * @param titleResId res id of string, will use package name to resolve + */ + public SubSettingLauncher setTitleRes(String titlePackageName, @StringRes int titleResId) { mLaunchRequest.titleResPackageName = titlePackageName; mLaunchRequest.titleResId = titleResId; mLaunchRequest.title = null; return this; } - public SubSettingLauncher setTitle(CharSequence title) { + /** + * Set title with text, + * This method is only for user generated string, + * display text will not update after locale change, + * if title string is from resource id, please use setTitleRes. + * + * @param title text title + */ + public SubSettingLauncher setTitleText(CharSequence title) { mLaunchRequest.title = title; return this; } - public SubSettingLauncher setIsShortCut(boolean isShortCut) { - mLaunchRequest.isShortCut = isShortCut; + public SubSettingLauncher setArguments(Bundle arguments) { + mLaunchRequest.arguments = arguments; return this; } - public SubSettingLauncher setArguments(Bundle arguments) { - mLaunchRequest.arguments = arguments; + public SubSettingLauncher setExtras(Bundle extras) { + mLaunchRequest.extras = extras; return this; } @@ -121,6 +142,7 @@ public class SubSettingLauncher { public Intent toIntent() { final Intent intent = new Intent(Intent.ACTION_MAIN); + copyExtras(intent); intent.setClass(mContext, SubSettings.class); if (TextUtils.isEmpty(mLaunchRequest.destinationName)) { throw new IllegalArgumentException("Destination fragment must be set"); @@ -130,7 +152,7 @@ public class SubSettingLauncher { if (mLaunchRequest.sourceMetricsCategory < 0) { throw new IllegalArgumentException("Source metrics category must be set"); } - intent.putExtra(VisibilityLoggerMixin.EXTRA_SOURCE_METRICS_CATEGORY, + intent.putExtra(MetricsFeatureProvider.EXTRA_SOURCE_METRICS_CATEGORY, mLaunchRequest.sourceMetricsCategory); intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS, mLaunchRequest.arguments); @@ -139,8 +161,6 @@ public class SubSettingLauncher { intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE_RESID, mLaunchRequest.titleResId); intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE, mLaunchRequest.title); - intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_AS_SHORTCUT, - mLaunchRequest.isShortCut); intent.addFlags(mLaunchRequest.flags); return intent; } @@ -167,6 +187,11 @@ public class SubSettingLauncher { listener.startActivityForResult(intent, requestCode); } + private void copyExtras(Intent intent) { + if (mLaunchRequest.extras != null) { + intent.replaceExtras(mLaunchRequest.extras); + } + } /** * Simple container that has information about how to launch a subsetting. */ @@ -175,12 +200,12 @@ public class SubSettingLauncher { int titleResId; String titleResPackageName; CharSequence title; - boolean isShortCut; int sourceMetricsCategory = -100; int flags; Fragment mResultListener; int mRequestCode; UserHandle userHandle; Bundle arguments; + Bundle extras; } } diff --git a/src/com/android/settings/core/TogglePreferenceController.java b/src/com/android/settings/core/TogglePreferenceController.java index 3a199b8965..165d7eb130 100644 --- a/src/com/android/settings/core/TogglePreferenceController.java +++ b/src/com/android/settings/core/TogglePreferenceController.java @@ -14,6 +14,7 @@ package com.android.settings.core; import android.content.Context; + import androidx.preference.Preference; import androidx.preference.TwoStatePreference; diff --git a/src/com/android/settings/core/WorkProfilePreferenceController.java b/src/com/android/settings/core/WorkProfilePreferenceController.java new file mode 100644 index 0000000000..603af20a87 --- /dev/null +++ b/src/com/android/settings/core/WorkProfilePreferenceController.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2019 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.core; + +import android.content.Context; +import android.os.UserHandle; +import android.os.UserManager; +import android.text.TextUtils; + +import androidx.annotation.Nullable; +import androidx.preference.Preference; + +import com.android.settings.Utils; + +/** + * Abstract class to provide additional logic to deal with optional {@link Preference} entries that + * are used only when work profile is enabled. + * + * <p>TODO(b/123376083): Consider merging this into {@link BasePreferenceController}.</p> + */ +public abstract class WorkProfilePreferenceController extends BasePreferenceController { + @Nullable + private final UserHandle mWorkProfileUser; + + /** + * Constructor of {@link WorkProfilePreferenceController}. Called by + * {@link BasePreferenceController#createInstance(Context, String)} through reflection. + * + * @param context {@link Context} to instantiate this controller. + * @param preferenceKey Preference key to be associated with the {@link Preference}. + */ + public WorkProfilePreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + mWorkProfileUser = Utils.getManagedProfile(UserManager.get(context)); + } + + /** + * @return Non-{@code null} {@link UserHandle} when a work profile is enabled. + * Otherwise {@code null}. + */ + @Nullable + protected UserHandle getWorkProfileUser() { + return mWorkProfileUser; + } + + /** + * Called back from {@link #handlePreferenceTreeClick(Preference)} to associate source metrics + * category. + * + * @return One of {@link android.app.settings.SettingsEnums}. + */ + protected abstract int getSourceMetricsCategory(); + + /** + * {@inheritDoc} + * + * <p>When you override this method, do not forget to check {@link #getWorkProfileUser()} to + * see if work profile user actually exists or not.</p> + */ + @AvailabilityStatus + @Override + public int getAvailabilityStatus() { + return mWorkProfileUser != null ? AVAILABLE : DISABLED_FOR_USER; + } + + /** + * Launches the specified fragment for the work profile user if the associated + * {@link Preference} is clicked. Otherwise just forward it to the super class. + * + * @param preference the preference being clicked. + * @return {@code true} if handled. + */ + public boolean handlePreferenceTreeClick(Preference preference) { + if (!TextUtils.equals(preference.getKey(), getPreferenceKey())) { + return super.handlePreferenceTreeClick(preference); + } + new SubSettingLauncher(preference.getContext()) + .setDestination(preference.getFragment()) + .setSourceMetricsCategory(getSourceMetricsCategory()) + .setArguments(preference.getExtras()) + .setUserHandle(mWorkProfileUser) + .launch(); + return true; + } +} diff --git a/src/com/android/settings/core/gateway/SettingsGateway.java b/src/com/android/settings/core/gateway/SettingsGateway.java index 8f5d992c84..5a81e71efb 100644 --- a/src/com/android/settings/core/gateway/SettingsGateway.java +++ b/src/com/android/settings/core/gateway/SettingsGateway.java @@ -17,71 +17,77 @@ package com.android.settings.core.gateway; import com.android.settings.DateTimeSettings; -import com.android.settings.DeviceAdminSettings; import com.android.settings.DisplaySettings; import com.android.settings.IccLockSettings; import com.android.settings.MasterClear; -import com.android.settings.PrivacySettings; import com.android.settings.Settings; import com.android.settings.TestingSettings; import com.android.settings.TetherSettings; import com.android.settings.TrustedCredentialsSettings; +import com.android.settings.accessibility.AccessibilityDetailsSettingsFragment; import com.android.settings.accessibility.AccessibilitySettings; import com.android.settings.accessibility.AccessibilitySettingsForSetupWizard; import com.android.settings.accessibility.CaptionPropertiesFragment; import com.android.settings.accessibility.ToggleDaltonizerPreferenceFragment; import com.android.settings.accounts.AccountDashboardFragment; import com.android.settings.accounts.AccountSyncSettings; -import com.android.settings.accounts.ChooseAccountActivity; +import com.android.settings.accounts.ChooseAccountFragment; import com.android.settings.accounts.ManagedProfileSettings; import com.android.settings.applications.AppAndNotificationDashboardFragment; -import com.android.settings.applications.DefaultAppSettings; -import com.android.settings.applications.DirectoryAccessDetails; -import com.android.settings.applications.ManageDomainUrls; import com.android.settings.applications.ProcessStatsSummary; import com.android.settings.applications.ProcessStatsUi; import com.android.settings.applications.UsageAccessDetails; -import com.android.settings.applications.VrListenerSettings; import com.android.settings.applications.appinfo.AppInfoDashboardFragment; import com.android.settings.applications.appinfo.DrawOverlayDetails; import com.android.settings.applications.appinfo.ExternalSourcesDetails; -import com.android.settings.applications.appinfo.PictureInPictureDetails; -import com.android.settings.applications.appinfo.PictureInPictureSettings; import com.android.settings.applications.appinfo.WriteSettingsDetails; import com.android.settings.applications.appops.BackgroundCheckSummary; import com.android.settings.applications.assist.ManageAssist; import com.android.settings.applications.manageapplications.ManageApplications; +import com.android.settings.applications.managedomainurls.ManageDomainUrls; +import com.android.settings.applications.specialaccess.deviceadmin.DeviceAdminSettings; +import com.android.settings.applications.specialaccess.pictureinpicture.PictureInPictureDetails; +import com.android.settings.applications.specialaccess.pictureinpicture.PictureInPictureSettings; +import com.android.settings.applications.specialaccess.vrlistener.VrListenerSettings; +import com.android.settings.applications.specialaccess.zenaccess.ZenAccessDetails; +import com.android.settings.backup.PrivacySettings; import com.android.settings.backup.ToggleBackupSettingFragment; +import com.android.settings.backup.UserBackupSettingsActivity; +import com.android.settings.biometrics.face.FaceSettings; +import com.android.settings.biometrics.fingerprint.FingerprintSettings; import com.android.settings.bluetooth.BluetoothDeviceDetailsFragment; import com.android.settings.connecteddevice.AdvancedConnectedDeviceDashboardFragment; import com.android.settings.connecteddevice.ConnectedDeviceDashboardFragment; import com.android.settings.connecteddevice.PreviouslyConnectedDeviceDashboardFragment; import com.android.settings.connecteddevice.usb.UsbDetailsFragment; +import com.android.settings.datausage.DataSaverSummary; import com.android.settings.datausage.DataUsageList; import com.android.settings.datausage.DataUsageSummary; -import com.android.settings.datausage.DataUsageSummaryLegacy; import com.android.settings.deletionhelper.AutomaticStorageManagerSettings; import com.android.settings.development.DevelopmentSettingsDashboardFragment; -import com.android.settings.deviceinfo.DeviceInfoSettings; import com.android.settings.deviceinfo.PrivateVolumeForget; import com.android.settings.deviceinfo.PrivateVolumeSettings; import com.android.settings.deviceinfo.PublicVolumeSettings; import com.android.settings.deviceinfo.StorageDashboardFragment; import com.android.settings.deviceinfo.StorageSettings; import com.android.settings.deviceinfo.aboutphone.MyDeviceInfoFragment; +import com.android.settings.deviceinfo.firmwareversion.FirmwareVersionSettings; +import com.android.settings.deviceinfo.legal.ModuleLicensesDashboard; import com.android.settings.display.NightDisplaySettings; import com.android.settings.dream.DreamSettings; import com.android.settings.enterprise.EnterprisePrivacySettings; import com.android.settings.fuelgauge.AdvancedPowerUsageDetail; import com.android.settings.fuelgauge.PowerUsageSummary; +import com.android.settings.fuelgauge.batterysaver.BatterySaverScheduleSettings; import com.android.settings.fuelgauge.batterysaver.BatterySaverSettings; import com.android.settings.gestures.AssistGestureSettings; import com.android.settings.gestures.DoubleTapPowerSettings; import com.android.settings.gestures.DoubleTapScreenSettings; import com.android.settings.gestures.DoubleTwistGestureSettings; -import com.android.settings.gestures.SwipeUpGestureSettings; +import com.android.settings.gestures.GlobalActionsPanelSettings; import com.android.settings.gestures.PickupGestureSettings; import com.android.settings.gestures.SwipeToNotificationSettings; +import com.android.settings.gestures.SystemNavigationGestureSettings; import com.android.settings.inputmethod.AvailableVirtualKeyboardFragment; import com.android.settings.inputmethod.KeyboardLayoutPickerFragment; import com.android.settings.inputmethod.PhysicalKeyboardFragment; @@ -94,31 +100,34 @@ import com.android.settings.location.LocationSettings; import com.android.settings.location.ScanningSettings; import com.android.settings.network.ApnEditor; import com.android.settings.network.ApnSettings; +import com.android.settings.network.MobileNetworkListFragment; import com.android.settings.network.NetworkDashboardFragment; import com.android.settings.nfc.AndroidBeam; import com.android.settings.nfc.PaymentSettings; +import com.android.settings.notification.AppBubbleNotificationSettings; import com.android.settings.notification.AppNotificationSettings; import com.android.settings.notification.ChannelGroupNotificationSettings; import com.android.settings.notification.ChannelNotificationSettings; import com.android.settings.notification.ConfigureNotificationSettings; import com.android.settings.notification.NotificationAccessSettings; +import com.android.settings.notification.NotificationAssistantPicker; import com.android.settings.notification.NotificationStation; import com.android.settings.notification.SoundSettings; import com.android.settings.notification.ZenAccessSettings; import com.android.settings.notification.ZenModeAutomationSettings; -import com.android.settings.notification.ZenModeMsgEventReminderSettings; import com.android.settings.notification.ZenModeBlockedEffectsSettings; import com.android.settings.notification.ZenModeEventRuleSettings; -import com.android.settings.notification.ZenModeRestrictNotificationsSettings; import com.android.settings.notification.ZenModeScheduleRuleSettings; import com.android.settings.notification.ZenModeSettings; import com.android.settings.password.ChooseLockPassword; import com.android.settings.password.ChooseLockPattern; import com.android.settings.print.PrintJobSettingsFragment; import com.android.settings.print.PrintSettingsFragment; +import com.android.settings.privacy.PrivacyDashboardFragment; import com.android.settings.security.CryptKeeperSettings; import com.android.settings.security.LockscreenDashboardFragment; import com.android.settings.security.SecuritySettings; +import com.android.settings.shortcut.CreateShortcut; import com.android.settings.sim.SimSettings; import com.android.settings.support.SupportDashboardActivity; import com.android.settings.system.ResetDashboardFragment; @@ -130,12 +139,13 @@ import com.android.settings.wallpaper.WallpaperTypeSettings; import com.android.settings.webview.WebViewAppPicker; import com.android.settings.wfd.WifiDisplaySettings; import com.android.settings.wifi.ConfigureWifiSettings; -import com.android.settings.wifi.SavedAccessPointsWifiSettings; import com.android.settings.wifi.WifiAPITest; import com.android.settings.wifi.WifiInfo; import com.android.settings.wifi.WifiSettings; +import com.android.settings.wifi.calling.WifiCallingDisclaimerFragment; import com.android.settings.wifi.calling.WifiCallingSettings; import com.android.settings.wifi.p2p.WifiP2pSettings; +import com.android.settings.wifi.savedaccesspoints.SavedAccessPointsWifiSettings; import com.android.settings.wifi.tether.WifiTetherSettings; public class SettingsGateway { @@ -146,6 +156,7 @@ public class SettingsGateway { */ public static final String[] ENTRY_FRAGMENTS = { AdvancedConnectedDeviceDashboardFragment.class.getName(), + CreateShortcut.class.getName(), WifiSettings.class.getName(), ConfigureWifiSettings.class.getName(), SavedAccessPointsWifiSettings.class.getName(), @@ -155,6 +166,7 @@ public class SettingsGateway { WifiTetherSettings.class.getName(), BackgroundCheckSummary.class.getName(), VpnSettings.class.getName(), + DataSaverSummary.class.getName(), DateTimeSettings.class.getName(), LocaleListEditor.class.getName(), AvailableVirtualKeyboardFragment.class.getName(), @@ -163,18 +175,21 @@ public class SettingsGateway { UserDictionaryList.class.getName(), UserDictionarySettings.class.getName(), DisplaySettings.class.getName(), - DeviceInfoSettings.class.getName(), MyDeviceInfoFragment.class.getName(), + ModuleLicensesDashboard.class.getName(), ManageApplications.class.getName(), + FirmwareVersionSettings.class.getName(), ManageAssist.class.getName(), ProcessStatsUi.class.getName(), NotificationStation.class.getName(), LocationSettings.class.getName(), + PrivacyDashboardFragment.class.getName(), ScanningSettings.class.getName(), SecuritySettings.class.getName(), UsageAccessDetails.class.getName(), PrivacySettings.class.getName(), DeviceAdminSettings.class.getName(), + AccessibilityDetailsSettingsFragment.class.getName(), AccessibilitySettings.class.getName(), AccessibilitySettingsForSetupWizard.class.getName(), CaptionPropertiesFragment.class.getName(), @@ -190,19 +205,22 @@ public class SettingsGateway { PowerUsageSummary.class.getName(), AccountSyncSettings.class.getName(), AssistGestureSettings.class.getName(), + FaceSettings.class.getName(), + FingerprintSettings.FingerprintSettingsFragment.class.getName(), SwipeToNotificationSettings.class.getName(), DoubleTapPowerSettings.class.getName(), DoubleTapScreenSettings.class.getName(), PickupGestureSettings.class.getName(), DoubleTwistGestureSettings.class.getName(), - SwipeUpGestureSettings.class.getName(), + SystemNavigationGestureSettings.class.getName(), CryptKeeperSettings.class.getName(), DataUsageSummary.class.getName(), - DataUsageSummaryLegacy.class.getName(), DreamSettings.class.getName(), UserSettings.class.getName(), NotificationAccessSettings.class.getName(), + AppBubbleNotificationSettings.class.getName(), ZenAccessSettings.class.getName(), + ZenAccessDetails.class.getName(), ZenModeAutomationSettings.class.getName(), PrintSettingsFragment.class.getName(), PrintJobSettingsFragment.class.getName(), @@ -218,6 +236,7 @@ public class SettingsGateway { AppInfoDashboardFragment.class.getName(), BatterySaverSettings.class.getName(), AppNotificationSettings.class.getName(), + NotificationAssistantPicker.class.getName(), ChannelNotificationSettings.class.getName(), ChannelGroupNotificationSettings.class.getName(), ApnSettings.class.getName(), @@ -232,13 +251,12 @@ public class SettingsGateway { DrawOverlayDetails.class.getName(), WriteSettingsDetails.class.getName(), ExternalSourcesDetails.class.getName(), - DefaultAppSettings.class.getName(), WallpaperTypeSettings.class.getName(), VrListenerSettings.class.getName(), PictureInPictureSettings.class.getName(), PictureInPictureDetails.class.getName(), ManagedProfileSettings.class.getName(), - ChooseAccountActivity.class.getName(), + ChooseAccountFragment.class.getName(), IccLockSettings.class.getName(), TestingSettings.class.getName(), WifiAPITest.class.getName(), @@ -254,15 +272,18 @@ public class SettingsGateway { ConnectedDeviceDashboardFragment.class.getName(), UsbDetailsFragment.class.getName(), AppAndNotificationDashboardFragment.class.getName(), + WifiCallingDisclaimerFragment.class.getName(), AccountDashboardFragment.class.getName(), EnterprisePrivacySettings.class.getName(), WebViewAppPicker.class.getName(), LockscreenDashboardFragment.class.getName(), BluetoothDeviceDetailsFragment.class.getName(), DataUsageList.class.getName(), - DirectoryAccessDetails.class.getName(), ToggleBackupSettingFragment.class.getName(), PreviouslyConnectedDeviceDashboardFragment.class.getName(), + BatterySaverScheduleSettings.class.getName(), + MobileNetworkListFragment.class.getName(), + GlobalActionsPanelSettings.class.getName() }; public static final String[] SETTINGS_FOR_RESTRICTED = { @@ -275,6 +296,7 @@ public class SettingsGateway { Settings.StorageDashboardActivity.class.getName(), Settings.PowerUsageSummaryActivity.class.getName(), Settings.AccountDashboardActivity.class.getName(), + Settings.PrivacySettingsActivity.class.getName(), Settings.SecurityDashboardActivity.class.getName(), Settings.AccessibilitySettingsActivity.class.getName(), Settings.SystemDashboardActivity.class.getName(), @@ -282,7 +304,6 @@ public class SettingsGateway { // Home page > Network & Internet Settings.WifiSettingsActivity.class.getName(), Settings.DataUsageSummaryActivity.class.getName(), - Settings.SimSettingsActivity.class.getName(), // Home page > Connected devices Settings.BluetoothSettingsActivity.class.getName(), Settings.WifiDisplaySettingsActivity.class.getName(), @@ -290,7 +311,6 @@ public class SettingsGateway { // Home page > Apps & Notifications Settings.UserSettingsActivity.class.getName(), Settings.ConfigureNotificationSettingsActivity.class.getName(), - Settings.AdvancedAppsActivity.class.getName(), Settings.ManageApplicationsActivity.class.getName(), Settings.PaymentSettingsActivity.class.getName(), // Home page > Security & screen lock @@ -298,8 +318,9 @@ public class SettingsGateway { // Home page > System Settings.LanguageAndInputSettingsActivity.class.getName(), Settings.DateTimeSettingsActivity.class.getName(), - Settings.DeviceInfoSettingsActivity.class.getName(), Settings.EnterprisePrivacySettingsActivity.class.getName(), Settings.MyDeviceInfoActivity.class.getName(), + Settings.ModuleLicensesActivity.class.getName(), + UserBackupSettingsActivity.class.getName(), }; } diff --git a/src/com/android/settings/core/instrumentation/SettingsEventLogWriter.java b/src/com/android/settings/core/instrumentation/SettingsEventLogWriter.java new file mode 100644 index 0000000000..a58555fe11 --- /dev/null +++ b/src/com/android/settings/core/instrumentation/SettingsEventLogWriter.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2019 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.core.instrumentation; + +import android.content.Context; +import android.provider.DeviceConfig; + +import com.android.settings.core.SettingsUIDeviceConfig; +import com.android.settingslib.core.instrumentation.EventLogWriter; + +public class SettingsEventLogWriter extends EventLogWriter { + + @Override + public void visible(Context context, int source, int category) { + if (shouldDisableGenericEventLogging()) { + return; + } + super.visible(context, source, category); + } + + @Override + public void hidden(Context context, int category) { + if (shouldDisableGenericEventLogging()) { + return; + } + super.hidden(context, category); + } + + @Override + public void action(Context context, int category, String pkg) { + if (shouldDisableGenericEventLogging()) { + return; + } + super.action(context, category, pkg); + } + + @Override + public void action(Context context, int category, int value) { + if (shouldDisableGenericEventLogging()) { + return; + } + super.action(context, category, value); + } + + @Override + public void action(Context context, int category, boolean value) { + if (shouldDisableGenericEventLogging()) { + return; + } + super.action(context, category, value); + } + + private static boolean shouldDisableGenericEventLogging() { + return !DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI, + SettingsUIDeviceConfig.GENERIC_EVENT_LOGGING_ENABLED, true /* default */); + } +} diff --git a/src/com/android/settings/core/instrumentation/SettingsIntelligenceLogWriter.java b/src/com/android/settings/core/instrumentation/SettingsIntelligenceLogWriter.java new file mode 100644 index 0000000000..9498732e52 --- /dev/null +++ b/src/com/android/settings/core/instrumentation/SettingsIntelligenceLogWriter.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2019 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.core.instrumentation; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Process; +import android.os.UserHandle; +import android.text.TextUtils; +import android.text.format.DateUtils; +import android.util.Log; +import android.util.Pair; + +import androidx.annotation.VisibleForTesting; + +import com.android.settings.R; +import com.android.settings.intelligence.LogProto.SettingsLog; +import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.core.instrumentation.LogWriter; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.LinkedList; +import java.util.List; + +public class SettingsIntelligenceLogWriter implements LogWriter { + private static final String TAG = "IntelligenceLogWriter"; + + private static final String LOG = "logs"; + private static final long MESSAGE_DELAY = DateUtils.MINUTE_IN_MILLIS; // 1 minute + + private List<SettingsLog> mSettingsLogList; + private SendLogHandler mLogHandler; + + public SettingsIntelligenceLogWriter() { + mSettingsLogList = new LinkedList<>(); + final HandlerThread workerThread = new HandlerThread("SettingsIntelligenceLogWriter", + Process.THREAD_PRIORITY_BACKGROUND); + workerThread.start(); + mLogHandler = new SendLogHandler(workerThread.getLooper()); + } + + @Override + public void visible(Context context, int attribution, int pageId) { + action(attribution /* attribution */, + SettingsEnums.PAGE_VISIBLE /* action */, + pageId /* pageId */, + "" /* changedPreferenceKey */, + 0 /* changedPreferenceIntValue */); + } + + @Override + public void hidden(Context context, int pageId) { + action(SettingsEnums.PAGE_UNKNOWN /* attribution */, + SettingsEnums.PAGE_HIDE /* action */, + pageId /* pageId */, + "" /* changedPreferenceKey */, + 0 /* changedPreferenceIntValue */); + } + + @Override + public void action(Context context, int action, Pair<Integer, Object>... taggedData) { + action(SettingsEnums.PAGE_UNKNOWN /* attribution */, + action, + SettingsEnums.PAGE_UNKNOWN /* pageId */, + "" /* changedPreferenceKey */, + 0 /* changedPreferenceIntValue */); + } + + @Override + public void action(Context context, int action, int value) { + action(SettingsEnums.PAGE_UNKNOWN /* attribution */, + action, + SettingsEnums.PAGE_UNKNOWN /* pageId */, + "" /* changedPreferenceKey */, + value /* changedPreferenceIntValue */); + } + + @Override + public void action(Context context, int action, boolean value) { + action(SettingsEnums.PAGE_UNKNOWN /* attribution */, + action, + SettingsEnums.PAGE_UNKNOWN /* pageId */, + "" /* changedPreferenceKey */, + value ? 1 : 0 /* changedPreferenceIntValue */); + } + + @Override + public void action(Context context, int action, String pkg) { + action(SettingsEnums.PAGE_UNKNOWN /* attribution */, + action, + SettingsEnums.PAGE_UNKNOWN /* pageId */, + pkg /* changedPreferenceKey */, + 1 /* changedPreferenceIntValue */); + } + + @Override + public void action(int attribution, int action, int pageId, String key, int value) { + final ZonedDateTime now = ZonedDateTime.now(ZoneId.systemDefault()); + final SettingsLog settingsLog = SettingsLog.newBuilder() + .setAttribution(attribution) + .setAction(action) + .setPageId(pageId) + .setChangedPreferenceKey(key != null ? key : "") + .setChangedPreferenceIntValue(value) + .setTimestamp(now.toString()) + .build(); + mLogHandler.post(() -> { + mSettingsLogList.add(settingsLog); + }); + mLogHandler.scheduleSendLog(); + } + + @VisibleForTesting + static byte[] serialize(List<SettingsLog> settingsLogs) { + final int size = settingsLogs.size(); + final ByteArrayOutputStream bout = new ByteArrayOutputStream(); + final DataOutputStream output = new DataOutputStream(bout); + // Data is "size, length, bytearray, length, bytearray ..." + try { + output.writeInt(size); + for (SettingsLog settingsLog : settingsLogs) { + final byte[] data = settingsLog.toByteArray(); + output.writeInt(data.length); + output.write(data); + } + return bout.toByteArray(); + } catch (Exception e) { + Log.e(TAG, "serialize error", e); + return null; + } finally { + try { + output.close(); + } catch (Exception e) { + Log.e(TAG, "close error", e); + } + } + } + + private class SendLogHandler extends Handler { + + public SendLogHandler(Looper looper) { + super(looper); + } + + public void scheduleSendLog() { + removeCallbacks(mSendLogsRunnable); + postDelayed(mSendLogsRunnable, MESSAGE_DELAY); + } + } + + private final Runnable mSendLogsRunnable = () -> { + final Context context = FeatureFactory.getAppContext(); + if (context == null) { + Log.e(TAG, "context is null"); + return; + } + final String action = context.getString(R.string + .config_settingsintelligence_log_action); + if (!TextUtils.isEmpty(action) && !mSettingsLogList.isEmpty()) { + final Intent intent = new Intent(); + intent.setPackage(context.getString(R.string + .config_settingsintelligence_package_name)); + intent.setAction(action); + intent.putExtra(LOG, serialize(mSettingsLogList)); + context.sendBroadcastAsUser(intent, UserHandle.CURRENT); + mSettingsLogList.clear(); + } + }; +} diff --git a/src/com/android/settings/core/instrumentation/SettingsMetricsFeatureProvider.java b/src/com/android/settings/core/instrumentation/SettingsMetricsFeatureProvider.java new file mode 100644 index 0000000000..01927fd2d5 --- /dev/null +++ b/src/com/android/settings/core/instrumentation/SettingsMetricsFeatureProvider.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2018 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.core.instrumentation; + +import android.content.Context; +import android.util.Log; +import android.util.Pair; + +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; + +public class SettingsMetricsFeatureProvider extends MetricsFeatureProvider { + private static final String TAG = "SettingsMetricsFeature"; + + @Override + protected void installLogWriters() { + mLoggerWriters.add(new StatsLogWriter()); + mLoggerWriters.add(new SettingsEventLogWriter()); + mLoggerWriters.add(new SettingsIntelligenceLogWriter()); + } + + /** + * @deprecated Use {@link #action(int, int, int, String, int)} instead. + */ + @Deprecated + @Override + public void action(Context context, int category, Pair<Integer, Object>... taggedData) { + Log.w(TAG, "action(Pair<Integer, Object>... taggedData) is deprecated, " + + "Use action(int, int, int, String, int) instead."); + super.action(context, category, taggedData); + } +} diff --git a/src/com/android/settings/core/instrumentation/StatsLogWriter.java b/src/com/android/settings/core/instrumentation/StatsLogWriter.java new file mode 100644 index 0000000000..bcdecf35de --- /dev/null +++ b/src/com/android/settings/core/instrumentation/StatsLogWriter.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2018 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.core.instrumentation; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.util.Pair; +import android.util.StatsLog; + +import com.android.settingslib.core.instrumentation.LogWriter; + +public class StatsLogWriter implements LogWriter { + + @Override + public void visible(Context context, int attribution, int pageId) { + StatsLog.write(StatsLog.SETTINGS_UI_CHANGED /* Atom name */, + attribution, + SettingsEnums.PAGE_VISIBLE /* action */, + pageId, + null /* changedPreferenceKey */, + 0 /* changedPreferenceIntValue */); + } + + @Override + public void hidden(Context context, int pageId) { + StatsLog.write(StatsLog.SETTINGS_UI_CHANGED /* Atom name */, + SettingsEnums.PAGE_UNKNOWN /* attribution */, + SettingsEnums.PAGE_HIDE /* action */, + pageId, + null /* changedPreferenceKey */, + 0 /* changedPreferenceIntValue */); + } + + @Override + public void action(Context context, int action, Pair<Integer, Object>... taggedData) { + action(SettingsEnums.PAGE_UNKNOWN /* attribution */, + action, + SettingsEnums.PAGE_UNKNOWN /* pageId */, + null /* changedPreferenceKey */, + 0 /* changedPreferenceIntValue */); + } + + @Override + public void action(Context context, int action, int value) { + action(SettingsEnums.PAGE_UNKNOWN /* attribution */, + action, + SettingsEnums.PAGE_UNKNOWN /* pageId */, + null /* changedPreferenceKey */, + value /* changedPreferenceIntValue */); + } + + @Override + public void action(Context context, int action, boolean value) { + action(SettingsEnums.PAGE_UNKNOWN /* attribution */, + action, + SettingsEnums.PAGE_UNKNOWN /* pageId */, + null /* changedPreferenceKey */, + value ? 1 : 0 /* changedPreferenceIntValue */); + } + + @Override + public void action(Context context, int action, String pkg) { + action(SettingsEnums.PAGE_UNKNOWN /* attribution */, + action, + SettingsEnums.PAGE_UNKNOWN /* pageId */, + pkg /* changedPreferenceKey */, + 1 /* changedPreferenceIntValue */); + } + + @Override + public void action(int attribution, int action, int pageId, String key, int value) { + StatsLog.write(StatsLog.SETTINGS_UI_CHANGED /* atomName */, + attribution, + action, + pageId, + key, + value); + } +} diff --git a/src/com/android/settings/dashboard/CategoryManager.java b/src/com/android/settings/dashboard/CategoryManager.java new file mode 100644 index 0000000000..5cc75c88f9 --- /dev/null +++ b/src/com/android/settings/dashboard/CategoryManager.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settings.dashboard; + +import android.content.ComponentName; +import android.content.Context; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.Log; +import android.util.Pair; + +import androidx.annotation.VisibleForTesting; + +import com.android.settingslib.applications.InterestingConfigChanges; +import com.android.settingslib.drawer.CategoryKey; +import com.android.settingslib.drawer.DashboardCategory; +import com.android.settingslib.drawer.Tile; +import com.android.settingslib.drawer.TileUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +public class CategoryManager { + + private static final String TAG = "CategoryManager"; + + private static CategoryManager sInstance; + private final InterestingConfigChanges mInterestingConfigChanges; + + // Tile cache (key: <packageName, activityName>, value: tile) + private final Map<Pair<String, String>, Tile> mTileByComponentCache; + + // Tile cache (key: category key, value: category) + private final Map<String, DashboardCategory> mCategoryByKeyMap; + + private List<DashboardCategory> mCategories; + + public static CategoryManager get(Context context) { + if (sInstance == null) { + sInstance = new CategoryManager(context); + } + return sInstance; + } + + CategoryManager(Context context) { + mTileByComponentCache = new ArrayMap<>(); + mCategoryByKeyMap = new ArrayMap<>(); + mInterestingConfigChanges = new InterestingConfigChanges(); + mInterestingConfigChanges.applyNewConfig(context.getResources()); + } + + public synchronized DashboardCategory getTilesByCategory(Context context, String categoryKey) { + tryInitCategories(context); + + return mCategoryByKeyMap.get(categoryKey); + } + + public synchronized List<DashboardCategory> getCategories(Context context) { + tryInitCategories(context); + return mCategories; + } + + public synchronized void reloadAllCategories(Context context) { + final boolean forceClearCache = mInterestingConfigChanges.applyNewConfig( + context.getResources()); + mCategories = null; + tryInitCategories(context, forceClearCache); + } + + public synchronized void updateCategoryFromBlacklist(Set<ComponentName> tileBlacklist) { + if (mCategories == null) { + Log.w(TAG, "Category is null, skipping blacklist update"); + } + for (int i = 0; i < mCategories.size(); i++) { + DashboardCategory category = mCategories.get(i); + for (int j = 0; j < category.getTilesCount(); j++) { + Tile tile = category.getTile(j); + if (tileBlacklist.contains(tile.getIntent().getComponent())) { + category.removeTile(j--); + } + } + } + } + + private synchronized void tryInitCategories(Context context) { + // Keep cached tiles by default. The cache is only invalidated when InterestingConfigChange + // happens. + tryInitCategories(context, false /* forceClearCache */); + } + + private synchronized void tryInitCategories(Context context, boolean forceClearCache) { + if (mCategories == null) { + if (forceClearCache) { + mTileByComponentCache.clear(); + } + mCategoryByKeyMap.clear(); + mCategories = TileUtils.getCategories(context, mTileByComponentCache); + for (DashboardCategory category : mCategories) { + mCategoryByKeyMap.put(category.key, category); + } + backwardCompatCleanupForCategory(mTileByComponentCache, mCategoryByKeyMap); + sortCategories(context, mCategoryByKeyMap); + filterDuplicateTiles(mCategoryByKeyMap); + } + } + + @VisibleForTesting + synchronized void backwardCompatCleanupForCategory( + Map<Pair<String, String>, Tile> tileByComponentCache, + Map<String, DashboardCategory> categoryByKeyMap) { + // A package can use a) CategoryKey, b) old category keys, c) both. + // Check if a package uses old category key only. + // If yes, map them to new category key. + + // Build a package name -> tile map first. + final Map<String, List<Tile>> packageToTileMap = new HashMap<>(); + for (Entry<Pair<String, String>, Tile> tileEntry : tileByComponentCache.entrySet()) { + final String packageName = tileEntry.getKey().first; + List<Tile> tiles = packageToTileMap.get(packageName); + if (tiles == null) { + tiles = new ArrayList<>(); + packageToTileMap.put(packageName, tiles); + } + tiles.add(tileEntry.getValue()); + } + + for (Entry<String, List<Tile>> entry : packageToTileMap.entrySet()) { + final List<Tile> tiles = entry.getValue(); + // Loop map, find if all tiles from same package uses old key only. + boolean useNewKey = false; + boolean useOldKey = false; + for (Tile tile : tiles) { + if (CategoryKey.KEY_COMPAT_MAP.containsKey(tile.getCategory())) { + useOldKey = true; + } else { + useNewKey = true; + break; + } + } + // Uses only old key, map them to new keys one by one. + if (useOldKey && !useNewKey) { + for (Tile tile : tiles) { + final String newCategoryKey = + CategoryKey.KEY_COMPAT_MAP.get(tile.getCategory()); + tile.setCategory(newCategoryKey); + // move tile to new category. + DashboardCategory newCategory = categoryByKeyMap.get(newCategoryKey); + if (newCategory == null) { + newCategory = new DashboardCategory(newCategoryKey); + categoryByKeyMap.put(newCategoryKey, newCategory); + } + newCategory.addTile(tile); + } + } + } + } + + /** + * Sort the tiles injected from all apps such that if they have the same priority value, + * they wil lbe sorted by package name. + * <p/> + * A list of tiles are considered sorted when their priority value decreases in a linear + * scan. + */ + @VisibleForTesting + synchronized void sortCategories(Context context, + Map<String, DashboardCategory> categoryByKeyMap) { + for (Entry<String, DashboardCategory> categoryEntry : categoryByKeyMap.entrySet()) { + categoryEntry.getValue().sortTiles(context.getPackageName()); + } + } + + /** + * Filter out duplicate tiles from category. Duplicate tiles are the ones pointing to the + * same intent. + */ + @VisibleForTesting + synchronized void filterDuplicateTiles(Map<String, DashboardCategory> categoryByKeyMap) { + for (Entry<String, DashboardCategory> categoryEntry : categoryByKeyMap.entrySet()) { + final DashboardCategory category = categoryEntry.getValue(); + final int count = category.getTilesCount(); + final Set<ComponentName> components = new ArraySet<>(); + for (int i = count - 1; i >= 0; i--) { + final Tile tile = category.getTile(i); + final ComponentName tileComponent = tile.getIntent().getComponent(); + if (components.contains(tileComponent)) { + category.removeTile(i); + } else { + components.add(tileComponent); + } + } + } + } +} diff --git a/src/com/android/settings/dashboard/DashboardAdapter.java b/src/com/android/settings/dashboard/DashboardAdapter.java deleted file mode 100644 index 27b4525268..0000000000 --- a/src/com/android/settings/dashboard/DashboardAdapter.java +++ /dev/null @@ -1,418 +0,0 @@ -/* - * 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.dashboard; - -import android.app.Activity; -import android.content.Context; -import android.content.pm.PackageManager; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.service.settings.suggestions.Suggestion; -import androidx.annotation.VisibleForTesting; -import androidx.recyclerview.widget.DiffUtil; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import android.text.TextUtils; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; - -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.settings.R; -import com.android.settings.R.id; -import com.android.settings.dashboard.DashboardData.ConditionHeaderData; -import com.android.settings.dashboard.conditional.Condition; -import com.android.settings.dashboard.conditional.ConditionAdapter; -import com.android.settings.dashboard.suggestions.SuggestionAdapter; -import com.android.settings.overlay.FeatureFactory; -import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; -import com.android.settingslib.core.lifecycle.Lifecycle; -import com.android.settingslib.core.lifecycle.LifecycleObserver; -import com.android.settingslib.core.lifecycle.events.OnSaveInstanceState; -import com.android.settingslib.drawer.DashboardCategory; -import com.android.settingslib.drawer.Tile; -import com.android.settingslib.drawer.TileUtils; -import com.android.settingslib.suggestions.SuggestionControllerMixin; -import com.android.settingslib.utils.IconCache; - -import java.util.List; - -public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.DashboardItemHolder> - implements SummaryLoader.SummaryConsumer, SuggestionAdapter.Callback, LifecycleObserver, - OnSaveInstanceState { - public static final String TAG = "DashboardAdapter"; - private static final String STATE_CATEGORY_LIST = "category_list"; - - @VisibleForTesting - static final String STATE_CONDITION_EXPANDED = "condition_expanded"; - - private final IconCache mCache; - private final Context mContext; - private final MetricsFeatureProvider mMetricsFeatureProvider; - private final DashboardFeatureProvider mDashboardFeatureProvider; - private boolean mFirstFrameDrawn; - private RecyclerView mRecyclerView; - private SuggestionAdapter mSuggestionAdapter; - - @VisibleForTesting - DashboardData mDashboardData; - - private View.OnClickListener mTileClickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - //TODO: get rid of setTag/getTag - mDashboardFeatureProvider.openTileIntent((Activity) mContext, (Tile) v.getTag()); - } - }; - - public DashboardAdapter(Context context, Bundle savedInstanceState, - List<Condition> conditions, SuggestionControllerMixin suggestionControllerMixin, - Lifecycle lifecycle) { - - DashboardCategory category = null; - boolean conditionExpanded = false; - - mContext = context; - final FeatureFactory factory = FeatureFactory.getFactory(context); - mMetricsFeatureProvider = factory.getMetricsFeatureProvider(); - mDashboardFeatureProvider = factory.getDashboardFeatureProvider(context); - mCache = new IconCache(context); - mSuggestionAdapter = new SuggestionAdapter(mContext, suggestionControllerMixin, - savedInstanceState, this /* callback */, lifecycle); - - setHasStableIds(true); - - if (savedInstanceState != null) { - category = savedInstanceState.getParcelable(STATE_CATEGORY_LIST); - conditionExpanded = savedInstanceState.getBoolean( - STATE_CONDITION_EXPANDED, conditionExpanded); - } - - if (lifecycle != null) { - lifecycle.addObserver(this); - } - - mDashboardData = new DashboardData.Builder() - .setConditions(conditions) - .setSuggestions(mSuggestionAdapter.getSuggestions()) - .setCategory(category) - .setConditionExpanded(conditionExpanded) - .build(); - } - - public void setSuggestions(List<Suggestion> data) { - final DashboardData prevData = mDashboardData; - mDashboardData = new DashboardData.Builder(prevData) - .setSuggestions(data) - .build(); - notifyDashboardDataChanged(prevData); - } - - public void setCategory(DashboardCategory category) { - final DashboardData prevData = mDashboardData; - Log.d(TAG, "adapter setCategory called"); - mDashboardData = new DashboardData.Builder(prevData) - .setCategory(category) - .build(); - notifyDashboardDataChanged(prevData); - } - - public void setConditions(List<Condition> conditions) { - final DashboardData prevData = mDashboardData; - Log.d(TAG, "adapter setConditions called"); - mDashboardData = new DashboardData.Builder(prevData) - .setConditions(conditions) - .build(); - notifyDashboardDataChanged(prevData); - } - - @Override - public void onSuggestionClosed(Suggestion suggestion) { - final List<Suggestion> list = mDashboardData.getSuggestions(); - if (list == null || list.size() == 0) { - return; - } - if (list.size() == 1) { - // The only suggestion is dismissed, and the the empty suggestion container will - // remain as the dashboard item. Need to refresh the dashboard list. - setSuggestions(null); - } else { - list.remove(suggestion); - setSuggestions(list); - } - } - - @Override - public void notifySummaryChanged(Tile tile) { - final int position = mDashboardData.getPositionByTile(tile); - if (position != DashboardData.POSITION_NOT_FOUND) { - // Since usually tile in parameter and tile in mCategories are same instance, - // which is hard to be detected by DiffUtil, so we notifyItemChanged directly. - notifyItemChanged(position, mDashboardData.getItemTypeByPosition(position)); - } - } - - @Override - public DashboardItemHolder onCreateViewHolder(ViewGroup parent, int viewType) { - final View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false); - if (viewType == R.layout.condition_header) { - return new ConditionHeaderHolder(view); - } - if (viewType == R.layout.condition_container) { - return new ConditionContainerHolder(view); - } - if (viewType == R.layout.suggestion_container) { - return new SuggestionContainerHolder(view); - } - return new DashboardItemHolder(view); - } - - @Override - public void onBindViewHolder(DashboardItemHolder holder, int position) { - final int type = mDashboardData.getItemTypeByPosition(position); - switch (type) { - case R.layout.dashboard_tile: - final Tile tile = (Tile) mDashboardData.getItemEntityByPosition(position); - onBindTile(holder, tile); - holder.itemView.setTag(tile); - holder.itemView.setOnClickListener(mTileClickListener); - break; - case R.layout.suggestion_container: - onBindSuggestion((SuggestionContainerHolder) holder, position); - break; - case R.layout.condition_container: - onBindCondition((ConditionContainerHolder) holder, position); - break; - case R.layout.condition_header: - onBindConditionHeader((ConditionHeaderHolder) holder, - (ConditionHeaderData) mDashboardData.getItemEntityByPosition(position)); - break; - case R.layout.condition_footer: - holder.itemView.setOnClickListener(v -> { - mMetricsFeatureProvider.action(mContext, - MetricsEvent.ACTION_SETTINGS_CONDITION_EXPAND, false); - DashboardData prevData = mDashboardData; - mDashboardData = new DashboardData.Builder(prevData). - setConditionExpanded(false).build(); - notifyDashboardDataChanged(prevData); - scrollToTopOfConditions(); - }); - break; - } - } - - @Override - public long getItemId(int position) { - return mDashboardData.getItemIdByPosition(position); - } - - @Override - public int getItemViewType(int position) { - return mDashboardData.getItemTypeByPosition(position); - } - - @Override - public int getItemCount() { - return mDashboardData.size(); - } - - @Override - public void onAttachedToRecyclerView(RecyclerView recyclerView) { - super.onAttachedToRecyclerView(recyclerView); - // save the view so that we can scroll it when expanding/collapsing the suggestion and - // conditions. - mRecyclerView = recyclerView; - } - - public Object getItem(long itemId) { - return mDashboardData.getItemEntityById(itemId); - } - - public Suggestion getSuggestion(int position) { - return mSuggestionAdapter.getSuggestion(position); - } - - @VisibleForTesting - void notifyDashboardDataChanged(DashboardData prevData) { - if (mFirstFrameDrawn && prevData != null) { - final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DashboardData - .ItemsDataDiffCallback(prevData.getItemList(), mDashboardData.getItemList())); - diffResult.dispatchUpdatesTo(this); - } else { - mFirstFrameDrawn = true; - notifyDataSetChanged(); - } - } - - @VisibleForTesting - void onBindConditionHeader(final ConditionHeaderHolder holder, ConditionHeaderData data) { - holder.icon.setImageDrawable(data.conditionIcons.get(0)); - if (data.conditionCount == 1) { - holder.title.setText(data.title); - holder.summary.setText(null); - holder.icons.setVisibility(View.INVISIBLE); - } else { - holder.title.setText(null); - holder.summary.setText( - mContext.getString(R.string.condition_summary, data.conditionCount)); - updateConditionIcons(data.conditionIcons, holder.icons); - holder.icons.setVisibility(View.VISIBLE); - } - - holder.itemView.setOnClickListener(v -> { - mMetricsFeatureProvider.action(mContext, - MetricsEvent.ACTION_SETTINGS_CONDITION_EXPAND, true); - final DashboardData prevData = mDashboardData; - mDashboardData = new DashboardData.Builder(prevData) - .setConditionExpanded(true).build(); - notifyDashboardDataChanged(prevData); - scrollToTopOfConditions(); - }); - } - - @VisibleForTesting - void onBindCondition(final ConditionContainerHolder holder, int position) { - final ConditionAdapter adapter = new ConditionAdapter(mContext, - (List<Condition>) mDashboardData.getItemEntityByPosition(position), - mDashboardData.isConditionExpanded()); - adapter.addDismissHandling(holder.data); - holder.data.setAdapter(adapter); - holder.data.setLayoutManager(new LinearLayoutManager(mContext)); - } - - @VisibleForTesting - void onBindSuggestion(final SuggestionContainerHolder holder, int position) { - // If there is suggestions to show, it will be at position 0 as we don't show the suggestion - // header anymore. - final List<Suggestion> suggestions = - (List<Suggestion>) mDashboardData.getItemEntityByPosition(position); - if (suggestions != null && suggestions.size() > 0) { - mSuggestionAdapter.setSuggestions(suggestions); - holder.data.setAdapter(mSuggestionAdapter); - } - final LinearLayoutManager layoutManager = new LinearLayoutManager(mContext); - layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL); - holder.data.setLayoutManager(layoutManager); - } - - @VisibleForTesting - void onBindTile(DashboardItemHolder holder, Tile tile) { - Drawable icon = mCache.getIcon(tile.icon); - if (!TextUtils.equals(tile.icon.getResPackage(), mContext.getPackageName()) - && !(icon instanceof RoundedHomepageIcon)) { - icon = new RoundedHomepageIcon(mContext, icon); - try { - if (tile.metaData != null) { - final int colorRes = tile.metaData.getInt( - TileUtils.META_DATA_PREFERENCE_ICON_BACKGROUND_HINT, 0 /* default */); - if (colorRes != 0) { - final int bgColor = mContext.getPackageManager() - .getResourcesForApplication(tile.icon.getResPackage()) - .getColor(colorRes, null /* theme */); - ((RoundedHomepageIcon) icon).setBackgroundColor(bgColor); - } - } - } catch (PackageManager.NameNotFoundException e) { - Log.e(TAG, "Failed to set background color for " + tile.intent.getPackage()); - } - mCache.updateIcon(tile.icon, icon); - } - holder.icon.setImageDrawable(icon); - holder.title.setText(tile.title); - if (!TextUtils.isEmpty(tile.summary)) { - holder.summary.setText(tile.summary); - holder.summary.setVisibility(View.VISIBLE); - } else { - holder.summary.setVisibility(View.GONE); - } - } - - @Override - public void onSaveInstanceState(Bundle outState) { - final DashboardCategory category = mDashboardData.getCategory(); - if (category != null) { - outState.putParcelable(STATE_CATEGORY_LIST, category); - } - outState.putBoolean(STATE_CONDITION_EXPANDED, mDashboardData.isConditionExpanded()); - } - - private void updateConditionIcons(List<Drawable> icons, ViewGroup parent) { - if (icons == null || icons.size() < 2) { - parent.setVisibility(View.INVISIBLE); - return; - } - final LayoutInflater inflater = LayoutInflater.from(parent.getContext()); - parent.removeAllViews(); - for (int i = 1, size = icons.size(); i < size; i++) { - ImageView icon = (ImageView) inflater.inflate( - R.layout.condition_header_icon, parent, false); - icon.setImageDrawable(icons.get(i)); - parent.addView(icon); - } - parent.setVisibility(View.VISIBLE); - } - - private void scrollToTopOfConditions() { - mRecyclerView.scrollToPosition(mDashboardData.hasSuggestion() ? 1 : 0); - } - - public static class DashboardItemHolder extends RecyclerView.ViewHolder { - public final ImageView icon; - public final TextView title; - public final TextView summary; - - public DashboardItemHolder(View itemView) { - super(itemView); - icon = itemView.findViewById(android.R.id.icon); - title = itemView.findViewById(android.R.id.title); - summary = itemView.findViewById(android.R.id.summary); - } - } - - public static class ConditionHeaderHolder extends DashboardItemHolder { - public final LinearLayout icons; - public final ImageView expandIndicator; - - public ConditionHeaderHolder(View itemView) { - super(itemView); - icons = itemView.findViewById(id.additional_icons); - expandIndicator = itemView.findViewById(id.expand_indicator); - } - } - - public static class ConditionContainerHolder extends DashboardItemHolder { - public final RecyclerView data; - - public ConditionContainerHolder(View itemView) { - super(itemView); - data = itemView.findViewById(id.data); - } - } - - public static class SuggestionContainerHolder extends DashboardItemHolder { - public final RecyclerView data; - - public SuggestionContainerHolder(View itemView) { - super(itemView); - data = itemView.findViewById(id.suggestion_list); - } - } - -} diff --git a/src/com/android/settings/dashboard/DashboardData.java b/src/com/android/settings/dashboard/DashboardData.java deleted file mode 100644 index 2ef4abe84b..0000000000 --- a/src/com/android/settings/dashboard/DashboardData.java +++ /dev/null @@ -1,450 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.settings.dashboard; - -import android.annotation.IntDef; -import android.graphics.drawable.Drawable; -import android.service.settings.suggestions.Suggestion; -import androidx.annotation.VisibleForTesting; -import androidx.recyclerview.widget.DiffUtil; -import android.text.TextUtils; - -import com.android.settings.R; -import com.android.settings.dashboard.conditional.Condition; -import com.android.settingslib.drawer.DashboardCategory; -import com.android.settingslib.drawer.Tile; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -/** - * Description about data list used in the DashboardAdapter. In the data list each item can be - * Condition, suggestion or category tile. - * <p> - * ItemsData has inner class Item, which represents the Item in data list. - */ -public class DashboardData { - public static final int POSITION_NOT_FOUND = -1; - public static final int MAX_SUGGESTION_COUNT = 2; - - // stable id for different type of items. - @VisibleForTesting - static final int STABLE_ID_SUGGESTION_CONTAINER = 0; - static final int STABLE_ID_SUGGESTION_CONDITION_DIVIDER = 1; - @VisibleForTesting - static final int STABLE_ID_CONDITION_HEADER = 2; - @VisibleForTesting - static final int STABLE_ID_CONDITION_FOOTER = 3; - @VisibleForTesting - static final int STABLE_ID_CONDITION_CONTAINER = 4; - - private final List<Item> mItems; - private final DashboardCategory mCategory; - private final List<Condition> mConditions; - private final List<Suggestion> mSuggestions; - private final boolean mConditionExpanded; - - private DashboardData(Builder builder) { - mCategory = builder.mCategory; - mConditions = builder.mConditions; - mSuggestions = builder.mSuggestions; - mConditionExpanded = builder.mConditionExpanded; - mItems = new ArrayList<>(); - - buildItemsData(); - } - - public int getItemIdByPosition(int position) { - return mItems.get(position).id; - } - - public int getItemTypeByPosition(int position) { - return mItems.get(position).type; - } - - public Object getItemEntityByPosition(int position) { - return mItems.get(position).entity; - } - - public List<Item> getItemList() { - return mItems; - } - - public int size() { - return mItems.size(); - } - - public Object getItemEntityById(long id) { - for (final Item item : mItems) { - if (item.id == id) { - return item.entity; - } - } - return null; - } - - public DashboardCategory getCategory() { - return mCategory; - } - - public List<Condition> getConditions() { - return mConditions; - } - - public List<Suggestion> getSuggestions() { - return mSuggestions; - } - - public boolean hasSuggestion() { - return sizeOf(mSuggestions) > 0; - } - - public boolean isConditionExpanded() { - return mConditionExpanded; - } - - /** - * Find the position of the object in mItems list, using the equals method to compare - * - * @param entity the object that need to be found in list - * @return position of the object, return POSITION_NOT_FOUND if object isn't in the list - */ - public int getPositionByEntity(Object entity) { - if (entity == null) return POSITION_NOT_FOUND; - - final int size = mItems.size(); - for (int i = 0; i < size; i++) { - final Object item = mItems.get(i).entity; - if (entity.equals(item)) { - return i; - } - } - - return POSITION_NOT_FOUND; - } - - /** - * Find the position of the Tile object. - * <p> - * First, try to find the exact identical instance of the tile object, if not found, - * then try to find a tile has the same title. - * - * @param tile tile that need to be found - * @return position of the object, return INDEX_NOT_FOUND if object isn't in the list - */ - public int getPositionByTile(Tile tile) { - final int size = mItems.size(); - for (int i = 0; i < size; i++) { - final Object entity = mItems.get(i).entity; - if (entity == tile) { - return i; - } else if (entity instanceof Tile && tile.title.equals(((Tile) entity).title)) { - return i; - } - } - - return POSITION_NOT_FOUND; - } - - /** - * Add item into list when {@paramref add} is true. - * - * @param item maybe {@link Condition}, {@link Tile}, {@link DashboardCategory} or null - * @param type type of the item, and value is the layout id - * @param stableId The stable id for this item - * @param add flag about whether to add item into list - */ - private void addToItemList(Object item, int type, int stableId, boolean add) { - if (add) { - mItems.add(new Item(item, type, stableId)); - } - } - - /** - * Build the mItems list using mConditions, mSuggestions, mCategories data - * and mIsShowingAll, mConditionExpanded flag. - */ - private void buildItemsData() { - final List<Condition> conditions = getConditionsToShow(mConditions); - final boolean hasConditions = sizeOf(conditions) > 0; - - final List<Suggestion> suggestions = getSuggestionsToShow(mSuggestions); - final boolean hasSuggestions = sizeOf(suggestions) > 0; - - /* Suggestion container. This is the card view that contains the list of suggestions. - * This will be added whenever the suggestion list is not empty */ - addToItemList(suggestions, R.layout.suggestion_container, - STABLE_ID_SUGGESTION_CONTAINER, hasSuggestions); - - /* Divider between suggestion and conditions if both are present. */ - addToItemList(null /* item */, R.layout.horizontal_divider, - STABLE_ID_SUGGESTION_CONDITION_DIVIDER, hasSuggestions && hasConditions); - - /* Condition header. This will be present when there is condition and it is collapsed */ - addToItemList(new ConditionHeaderData(conditions), - R.layout.condition_header, - STABLE_ID_CONDITION_HEADER, hasConditions && !mConditionExpanded); - - /* Condition container. This is the card view that contains the list of conditions. - * This will be added whenever the condition list is not empty and expanded */ - addToItemList(conditions, R.layout.condition_container, - STABLE_ID_CONDITION_CONTAINER, hasConditions && mConditionExpanded); - - /* Condition footer. This will be present when there is condition and it is expanded */ - addToItemList(null /* item */, R.layout.condition_footer, - STABLE_ID_CONDITION_FOOTER, hasConditions && mConditionExpanded); - - if (mCategory != null) { - final List<Tile> tiles = mCategory.getTiles(); - for (int i = 0; i < tiles.size(); i++) { - final Tile tile = tiles.get(i); - addToItemList(tile, R.layout.dashboard_tile, Objects.hash(tile.title), - true /* add */); - } - } - } - - private static int sizeOf(List<?> list) { - return list == null ? 0 : list.size(); - } - - private List<Condition> getConditionsToShow(List<Condition> conditions) { - if (conditions == null) { - return null; - } - List<Condition> result = new ArrayList<>(); - final int size = conditions == null ? 0 : conditions.size(); - for (int i = 0; i < size; i++) { - final Condition condition = conditions.get(i); - if (condition.shouldShow()) { - result.add(condition); - } - } - return result; - } - - private List<Suggestion> getSuggestionsToShow(List<Suggestion> suggestions) { - if (suggestions == null) { - return null; - } - if (suggestions.size() <= MAX_SUGGESTION_COUNT) { - return suggestions; - } - final List<Suggestion> suggestionsToShow = new ArrayList<>(MAX_SUGGESTION_COUNT); - for (int i = 0; i < MAX_SUGGESTION_COUNT; i++) { - suggestionsToShow.add(suggestions.get(i)); - } - return suggestionsToShow; - } - - /** - * Builder used to build the ItemsData - */ - public static class Builder { - private DashboardCategory mCategory; - private List<Condition> mConditions; - private List<Suggestion> mSuggestions; - private boolean mConditionExpanded; - - public Builder() { - } - - public Builder(DashboardData dashboardData) { - mCategory = dashboardData.mCategory; - mConditions = dashboardData.mConditions; - mSuggestions = dashboardData.mSuggestions; - mConditionExpanded = dashboardData.mConditionExpanded; - } - - public Builder setCategory(DashboardCategory category) { - this.mCategory = category; - return this; - } - - public Builder setConditions(List<Condition> conditions) { - this.mConditions = conditions; - return this; - } - - public Builder setSuggestions(List<Suggestion> suggestions) { - this.mSuggestions = suggestions; - return this; - } - - public Builder setConditionExpanded(boolean expanded) { - this.mConditionExpanded = expanded; - return this; - } - - public DashboardData build() { - return new DashboardData(this); - } - } - - /** - * A DiffCallback to calculate the difference between old and new Item - * List in DashboardData - */ - public static class ItemsDataDiffCallback extends DiffUtil.Callback { - final private List<Item> mOldItems; - final private List<Item> mNewItems; - - public ItemsDataDiffCallback(List<Item> oldItems, List<Item> newItems) { - mOldItems = oldItems; - mNewItems = newItems; - } - - @Override - public int getOldListSize() { - return mOldItems.size(); - } - - @Override - public int getNewListSize() { - return mNewItems.size(); - } - - @Override - public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { - return mOldItems.get(oldItemPosition).id == mNewItems.get(newItemPosition).id; - } - - @Override - public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { - return mOldItems.get(oldItemPosition).equals(mNewItems.get(newItemPosition)); - } - - } - - /** - * An item contains the data needed in the DashboardData. - */ - static class Item { - // valid types in field type - private static final int TYPE_DASHBOARD_TILE = R.layout.dashboard_tile; - private static final int TYPE_SUGGESTION_CONTAINER = - R.layout.suggestion_container; - private static final int TYPE_CONDITION_CONTAINER = - R.layout.condition_container; - private static final int TYPE_CONDITION_HEADER = - R.layout.condition_header; - private static final int TYPE_CONDITION_FOOTER = - R.layout.condition_footer; - private static final int TYPE_SUGGESTION_CONDITION_DIVIDER = R.layout.horizontal_divider; - - @IntDef({TYPE_DASHBOARD_TILE, TYPE_SUGGESTION_CONTAINER, TYPE_CONDITION_CONTAINER, - TYPE_CONDITION_HEADER, TYPE_CONDITION_FOOTER, TYPE_SUGGESTION_CONDITION_DIVIDER}) - @Retention(RetentionPolicy.SOURCE) - public @interface ItemTypes { - } - - /** - * The main data object in item, usually is a {@link Tile}, {@link Condition} - * object. This object can also be null when the - * item is an divider line. Please refer to {@link #buildItemsData()} for - * detail usage of the Item. - */ - public final Object entity; - - /** - * The type of item, value inside is the layout id(e.g. R.layout.dashboard_tile) - */ - @ItemTypes - public final int type; - - /** - * Id of this item, used in the {@link ItemsDataDiffCallback} to identify the same item. - */ - public final int id; - - public Item(Object entity, @ItemTypes int type, int id) { - this.entity = entity; - this.type = type; - this.id = id; - } - - /** - * Override it to make comparision in the {@link ItemsDataDiffCallback} - * - * @param obj object to compared with - * @return true if the same object or has equal value. - */ - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - - if (!(obj instanceof Item)) { - return false; - } - - final Item targetItem = (Item) obj; - if (type != targetItem.type || id != targetItem.id) { - return false; - } - - switch (type) { - case TYPE_DASHBOARD_TILE: - final Tile localTile = (Tile) entity; - final Tile targetTile = (Tile) targetItem.entity; - - // Only check title and summary for dashboard tile - return TextUtils.equals(localTile.title, targetTile.title) - && TextUtils.equals(localTile.summary, targetTile.summary); - case TYPE_SUGGESTION_CONTAINER: - case TYPE_CONDITION_CONTAINER: - // If entity is suggestion and contains remote view, force refresh - final List entities = (List) entity; - if (!entities.isEmpty()) { - Object firstEntity = entities.get(0); - if (firstEntity instanceof Tile - && ((Tile) firstEntity).remoteViews != null) { - return false; - } - } - // Otherwise Fall through to default - default: - return entity == null ? targetItem.entity == null - : entity.equals(targetItem.entity); - } - } - } - - /** - * This class contains the data needed to build the suggestion/condition header. The data can - * also be used to check the diff in DiffUtil.Callback - */ - public static class ConditionHeaderData { - public final List<Drawable> conditionIcons; - public final CharSequence title; - public final int conditionCount; - - public ConditionHeaderData(List<Condition> conditions) { - conditionCount = sizeOf(conditions); - title = conditionCount > 0 ? conditions.get(0).getTitle() : null; - conditionIcons = new ArrayList<>(); - for (int i = 0; conditions != null && i < conditions.size(); i++) { - final Condition condition = conditions.get(i); - conditionIcons.add(condition.getIcon()); - } - } - } - -} diff --git a/src/com/android/settings/dashboard/DashboardFeatureProvider.java b/src/com/android/settings/dashboard/DashboardFeatureProvider.java index b1f66ae479..9f562a0846 100644 --- a/src/com/android/settings/dashboard/DashboardFeatureProvider.java +++ b/src/com/android/settings/dashboard/DashboardFeatureProvider.java @@ -15,8 +15,7 @@ */ package com.android.settings.dashboard; -import android.app.Activity; -import android.content.Context; +import androidx.fragment.app.FragmentActivity; import androidx.preference.Preference; import com.android.settingslib.drawer.DashboardCategory; @@ -35,33 +34,11 @@ public interface DashboardFeatureProvider { DashboardCategory getTilesForCategory(String key); /** - * Get tiles (wrapped as a list of Preference) for key defined in CategoryKey. - * - * @param activity Activity hosting the preference - * @param context UI context to inflate preference - * @param sourceMetricsCategory The context (source) from which an action is performed - * @param key Value from CategoryKey - * @deprecated Pages implementing {@code DashboardFragment} should use - * {@link #getTilesForCategory(String)} instead. Using this method will not get the benefit - * of auto-ordering, progressive disclosure, auto-refreshing summary text etc. - */ - @Deprecated - List<Preference> getPreferencesForCategory(Activity activity, Context context, - int sourceMetricsCategory, String key); - - /** * Get all tiles, grouped by category. */ List<DashboardCategory> getAllCategories(); /** - * Whether or not we should tint icons in setting pages. - * @deprecated in favor of color icons in homepage - */ - @Deprecated - boolean shouldTintIcon(); - - /** * Returns an unique string key for the tile. */ String getDashboardKeyForTile(Tile tile); @@ -70,6 +47,7 @@ public interface DashboardFeatureProvider { * Binds preference to data provided by tile. * * @param activity If tile contains intent to launch, it will be launched from this activity + * @param forceRoundedIcon Whether or not injected tiles from other packages should be forced to rounded icon. * @param sourceMetricsCategory The context (source) from which an action is performed * @param pref The preference to bind data * @param tile The binding data @@ -77,17 +55,12 @@ public interface DashboardFeatureProvider { * @param baseOrder The order offset value. When binding, pref's order is determined by * both this value and tile's own priority. */ - void bindPreferenceToTile(Activity activity, int sourceMetricsCategory, Preference pref, - Tile tile, String key, int baseOrder); - - /** - * Returns additional intent filter action for dashboard tiles - */ - String getExtraIntentAction(); + void bindPreferenceToTile(FragmentActivity activity, boolean forceRoundedIcon, + int sourceMetricsCategory, Preference pref, Tile tile, String key, int baseOrder); /** * Opens a tile to its destination intent. */ - void openTileIntent(Activity activity, Tile tile); + void openTileIntent(FragmentActivity activity, Tile tile); } diff --git a/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java b/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java index c98c982013..64086cb9a3 100644 --- a/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java +++ b/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java @@ -16,40 +16,43 @@ package com.android.settings.dashboard; +import static android.content.Intent.EXTRA_USER; + import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_URI; import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY; import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY_URI; -import android.app.Activity; +import android.app.settings.SettingsEnums; import android.content.ComponentName; import android.content.Context; import android.content.IContentProvider; import android.content.Intent; import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.os.Bundle; +import android.os.UserHandle; import android.provider.Settings; -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; import android.text.TextUtils; import android.util.ArrayMap; import android.util.Log; import android.util.Pair; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.annotation.VisibleForTesting; +import androidx.fragment.app.FragmentActivity; +import androidx.preference.Preference; + import com.android.settings.R; import com.android.settings.SettingsActivity; +import com.android.settings.dashboard.profileselector.ProfileSelectDialog; import com.android.settings.overlay.FeatureFactory; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; -import com.android.settingslib.core.instrumentation.VisibilityLoggerMixin; -import com.android.settingslib.drawer.CategoryManager; import com.android.settingslib.drawer.DashboardCategory; -import com.android.settingslib.drawer.ProfileSelectDialog; import com.android.settingslib.drawer.Tile; import com.android.settingslib.drawer.TileUtils; import com.android.settingslib.utils.ThreadUtils; +import com.android.settingslib.widget.AdaptiveIcon; -import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -59,11 +62,8 @@ import java.util.Map; public class DashboardFeatureProviderImpl implements DashboardFeatureProvider { private static final String TAG = "DashboardFeatureImpl"; - private static final String DASHBOARD_TILE_PREF_KEY_PREFIX = "dashboard_tile_pref_"; private static final String META_DATA_KEY_INTENT_ACTION = "com.android.settings.intent.action"; - @VisibleForTesting - static final String META_DATA_KEY_ORDER = "com.android.settings.order"; protected final Context mContext; @@ -73,7 +73,7 @@ public class DashboardFeatureProviderImpl implements DashboardFeatureProvider { public DashboardFeatureProviderImpl(Context context) { mContext = context.getApplicationContext(); - mCategoryManager = CategoryManager.get(context, getExtraIntentAction()); + mCategoryManager = CategoryManager.get(context); mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider(); mPackageManager = context.getPackageManager(); } @@ -84,83 +84,51 @@ public class DashboardFeatureProviderImpl implements DashboardFeatureProvider { } @Override - public List<Preference> getPreferencesForCategory(Activity activity, Context context, - int sourceMetricsCategory, String key) { - final DashboardCategory category = getTilesForCategory(key); - if (category == null) { - Log.d(TAG, "NO dashboard tiles for " + TAG); - return null; - } - final List<Tile> tiles = category.getTiles(); - if (tiles == null || tiles.isEmpty()) { - Log.d(TAG, "tile list is empty, skipping category " + category.title); - return null; - } - final List<Preference> preferences = new ArrayList<>(); - for (Tile tile : tiles) { - final Preference pref = new Preference(context); - bindPreferenceToTile(activity, sourceMetricsCategory, pref, tile, null /* key */, - Preference.DEFAULT_ORDER /* baseOrder */); - preferences.add(pref); - } - return preferences; - } - - @Override public List<DashboardCategory> getAllCategories() { return mCategoryManager.getCategories(mContext); } @Override - public boolean shouldTintIcon() { - return mContext.getResources().getBoolean(R.bool.config_tintSettingIcon); - } - - @Override public String getDashboardKeyForTile(Tile tile) { - if (tile == null || tile.intent == null) { + if (tile == null) { return null; } - if (!TextUtils.isEmpty(tile.key)) { - return tile.key; + if (tile.hasKey()) { + return tile.getKey(mContext); } final StringBuilder sb = new StringBuilder(DASHBOARD_TILE_PREF_KEY_PREFIX); - final ComponentName component = tile.intent.getComponent(); + final ComponentName component = tile.getIntent().getComponent(); sb.append(component.getClassName()); return sb.toString(); } @Override - public void bindPreferenceToTile(Activity activity, int sourceMetricsCategory, Preference pref, - Tile tile, String key, int baseOrder) { + public void bindPreferenceToTile(FragmentActivity activity, boolean forceRoundedIcon, + int sourceMetricsCategory, Preference pref, Tile tile, String key, int baseOrder) { if (pref == null) { return; } - pref.setTitle(tile.title); + pref.setTitle(tile.getTitle(activity.getApplicationContext())); if (!TextUtils.isEmpty(key)) { pref.setKey(key); } else { pref.setKey(getDashboardKeyForTile(tile)); } bindSummary(pref, tile); - bindIcon(pref, tile); - final Bundle metadata = tile.metaData; + bindIcon(pref, tile, forceRoundedIcon); + final Bundle metadata = tile.getMetaData(); String clsName = null; String action = null; - Integer order = null; + if (metadata != null) { clsName = metadata.getString(SettingsActivity.META_DATA_KEY_FRAGMENT_CLASS); action = metadata.getString(META_DATA_KEY_INTENT_ACTION); - if (metadata.containsKey(META_DATA_KEY_ORDER) - && metadata.get(META_DATA_KEY_ORDER) instanceof Integer) { - order = metadata.getInt(META_DATA_KEY_ORDER); - } } if (!TextUtils.isEmpty(clsName)) { pref.setFragment(clsName); - } else if (tile.intent != null) { - final Intent intent = new Intent(tile.intent); - intent.putExtra(VisibilityLoggerMixin.EXTRA_SOURCE_METRICS_CATEGORY, + } else { + final Intent intent = new Intent(tile.getIntent()); + intent.putExtra(MetricsFeatureProvider.EXTRA_SOURCE_METRICS_CATEGORY, sourceMetricsCategory); if (action != null) { intent.setAction(action); @@ -171,19 +139,12 @@ public class DashboardFeatureProviderImpl implements DashboardFeatureProvider { }); } final String skipOffsetPackageName = activity.getPackageName(); - // If order is set in the meta data, use that order. Otherwise, check the intent priority. - if (order == null && tile.priority != 0) { - // Use negated priority for order, because tile priority is based on intent-filter - // (larger value has higher priority). However pref order defines smaller value has - // higher priority. - order = -tile.priority; - } - if (order != null) { - boolean shouldSkipBaseOrderOffset = false; - if (tile.intent != null) { - shouldSkipBaseOrderOffset = TextUtils.equals( - skipOffsetPackageName, tile.intent.getComponent().getPackageName()); - } + + + if (tile.hasOrder()) { + final int order = tile.getOrder(); + boolean shouldSkipBaseOrderOffset = TextUtils.equals( + skipOffsetPackageName, tile.getIntent().getComponent().getPackageName()); if (shouldSkipBaseOrderOffset || baseOrder == Preference.DEFAULT_ORDER) { pref.setOrder(order); } else { @@ -193,44 +154,36 @@ public class DashboardFeatureProviderImpl implements DashboardFeatureProvider { } @Override - public String getExtraIntentAction() { - return null; - } - - @Override - public void openTileIntent(Activity activity, Tile tile) { + public void openTileIntent(FragmentActivity activity, Tile tile) { if (tile == null) { Intent intent = new Intent(Settings.ACTION_SETTINGS).addFlags( Intent.FLAG_ACTIVITY_CLEAR_TASK); mContext.startActivity(intent); return; } - - if (tile.intent == null) { - return; - } - final Intent intent = new Intent(tile.intent) - .putExtra(VisibilityLoggerMixin.EXTRA_SOURCE_METRICS_CATEGORY, - MetricsEvent.DASHBOARD_SUMMARY) + final Intent intent = new Intent(tile.getIntent()) + .putExtra(MetricsFeatureProvider.EXTRA_SOURCE_METRICS_CATEGORY, + SettingsEnums.DASHBOARD_SUMMARY) .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); - launchIntentOrSelectProfile(activity, tile, intent, MetricsEvent.DASHBOARD_SUMMARY); + launchIntentOrSelectProfile(activity, tile, intent, SettingsEnums.DASHBOARD_SUMMARY); } private void bindSummary(Preference preference, Tile tile) { - if (tile.summary != null) { - preference.setSummary(tile.summary); - } else if (tile.metaData != null - && tile.metaData.containsKey(META_DATA_PREFERENCE_SUMMARY_URI)) { + final CharSequence summary = tile.getSummary(mContext); + if (summary != null) { + preference.setSummary(summary); + } else if (tile.getMetaData() != null + && tile.getMetaData().containsKey(META_DATA_PREFERENCE_SUMMARY_URI)) { // Set a placeholder summary before starting to fetch real summary, this is necessary // to avoid preference height change. preference.setSummary(R.string.summary_placeholder); ThreadUtils.postOnBackgroundThread(() -> { final Map<String, IContentProvider> providerMap = new ArrayMap<>(); - final String uri = tile.metaData.getString(META_DATA_PREFERENCE_SUMMARY_URI); - final String summary = TileUtils.getTextFromUri( + final String uri = tile.getMetaData().getString(META_DATA_PREFERENCE_SUMMARY_URI); + final String summaryFromUri = TileUtils.getTextFromUri( mContext, uri, providerMap, META_DATA_PREFERENCE_SUMMARY); - ThreadUtils.postOnMainThread(() -> preference.setSummary(summary)); + ThreadUtils.postOnMainThread(() -> preference.setSummary(summaryFromUri)); }); } else { preference.setSummary(R.string.summary_placeholder); @@ -238,23 +191,30 @@ public class DashboardFeatureProviderImpl implements DashboardFeatureProvider { } @VisibleForTesting - void bindIcon(Preference preference, Tile tile) { - if (tile.icon != null) { - preference.setIcon(tile.icon.loadDrawable(preference.getContext())); - } else if (tile.metaData != null - && tile.metaData.containsKey(META_DATA_PREFERENCE_ICON_URI)) { + void bindIcon(Preference preference, Tile tile, boolean forceRoundedIcon) { + // Use preference context instead here when get icon from Tile, as we are using the context + // to get the style to tint the icon. Using mContext here won't get the correct style. + final Icon tileIcon = tile.getIcon(preference.getContext()); + if (tileIcon != null) { + Drawable iconDrawable = tileIcon.loadDrawable(preference.getContext()); + if (forceRoundedIcon + && !TextUtils.equals(mContext.getPackageName(), tile.getPackageName())) { + iconDrawable = new AdaptiveIcon(mContext, iconDrawable); + ((AdaptiveIcon) iconDrawable).setBackgroundColor(mContext, tile); + } + preference.setIcon(iconDrawable); + } else if (tile.getMetaData() != null + && tile.getMetaData().containsKey(META_DATA_PREFERENCE_ICON_URI)) { ThreadUtils.postOnBackgroundThread(() -> { + final Intent intent = tile.getIntent(); String packageName = null; - if (tile.intent != null) { - Intent intent = tile.intent; - if (!TextUtils.isEmpty(intent.getPackage())) { - packageName = intent.getPackage(); - } else if (intent.getComponent() != null) { - packageName = intent.getComponent().getPackageName(); - } + if (!TextUtils.isEmpty(intent.getPackage())) { + packageName = intent.getPackage(); + } else if (intent.getComponent() != null) { + packageName = intent.getComponent().getPackageName(); } final Map<String, IContentProvider> providerMap = new ArrayMap<>(); - final String uri = tile.metaData.getString(META_DATA_PREFERENCE_ICON_URI); + final String uri = tile.getMetaData().getString(META_DATA_PREFERENCE_ICON_URI); final Pair<String, Integer> iconInfo = TileUtils.getIconFromUri( mContext, packageName, uri, providerMap); if (iconInfo == null) { @@ -269,21 +229,29 @@ public class DashboardFeatureProviderImpl implements DashboardFeatureProvider { } } - private void launchIntentOrSelectProfile(Activity activity, Tile tile, Intent intent, + private void launchIntentOrSelectProfile(FragmentActivity activity, Tile tile, Intent intent, int sourceMetricCategory) { if (!isIntentResolvable(intent)) { Log.w(TAG, "Cannot resolve intent, skipping. " + intent); return; } ProfileSelectDialog.updateUserHandlesIfNeeded(mContext, tile); - if (tile.userHandle == null) { + + if (tile.userHandle == null || tile.isPrimaryProfileOnly()) { mMetricsFeatureProvider.logDashboardStartIntent(mContext, intent, sourceMetricCategory); activity.startActivityForResult(intent, 0); } else if (tile.userHandle.size() == 1) { mMetricsFeatureProvider.logDashboardStartIntent(mContext, intent, sourceMetricCategory); activity.startActivityForResultAsUser(intent, 0, tile.userHandle.get(0)); } else { - ProfileSelectDialog.show(activity.getFragmentManager(), tile); + final UserHandle userHandle = intent.getParcelableExtra(EXTRA_USER); + if (userHandle != null && tile.userHandle.contains(userHandle)) { + mMetricsFeatureProvider.logDashboardStartIntent( + mContext, intent, sourceMetricCategory); + activity.startActivityForResultAsUser(intent, 0, userHandle); + } else { + ProfileSelectDialog.show(activity.getSupportFragmentManager(), tile); + } } } diff --git a/src/com/android/settings/dashboard/DashboardFragment.java b/src/com/android/settings/dashboard/DashboardFragment.java index bef66c3650..f2e3d73c8f 100644 --- a/src/com/android/settings/dashboard/DashboardFragment.java +++ b/src/com/android/settings/dashboard/DashboardFragment.java @@ -16,32 +16,36 @@ package com.android.settings.dashboard; import android.app.Activity; +import android.app.settings.SettingsEnums; import android.content.Context; -import android.content.res.TypedArray; import android.os.Bundle; -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; -import androidx.preference.PreferenceManager; -import androidx.preference.PreferenceScreen; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; +import androidx.annotation.CallSuper; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceGroup; +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; import com.android.settings.core.BasePreferenceController; import com.android.settings.core.PreferenceControllerListHelper; +import com.android.settings.core.SettingsBaseActivity; import com.android.settings.overlay.FeatureFactory; import com.android.settings.search.Indexable; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.drawer.DashboardCategory; -import com.android.settingslib.drawer.SettingsDrawerActivity; import com.android.settingslib.drawer.Tile; -import com.android.settingslib.drawer.TileUtils; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; @@ -51,8 +55,9 @@ import java.util.Set; * Base fragment for dashboard style UI containing a list of static and dynamic setting items. */ public abstract class DashboardFragment extends SettingsPreferenceFragment - implements SettingsDrawerActivity.CategoryListener, Indexable, - SummaryLoader.SummaryConsumer { + implements SettingsBaseActivity.CategoryListener, Indexable, + SummaryLoader.SummaryConsumer, PreferenceGroup.OnExpandButtonClickListener, + BasePreferenceController.UiBlockListener { private static final String TAG = "DashboardFragment"; private final Map<Class, List<AbstractPreferenceController>> mPreferenceControllers = @@ -63,10 +68,15 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment private DashboardTilePlaceholderPreferenceController mPlaceholderPreferenceController; private boolean mListeningToCategoryChange; private SummaryLoader mSummaryLoader; + private List<String> mSuppressInjectedTileKeys; + @VisibleForTesting + UiBlockerController mBlockerController; @Override public void onAttach(Context context) { super.onAttach(context); + mSuppressInjectedTileKeys = Arrays.asList(context.getResources().getStringArray( + R.array.config_suppress_injected_tile_keys)); mDashboardFeatureProvider = FeatureFactory.getFactory(context). getDashboardFeatureProvider(context); final List<AbstractPreferenceController> controllers = new ArrayList<>(); @@ -88,7 +98,7 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment controllers.addAll(uniqueControllerFromXml); // And wire up with lifecycle. - final Lifecycle lifecycle = getLifecycle(); + final Lifecycle lifecycle = getSettingsLifecycle(); uniqueControllerFromXml .stream() .filter(controller -> controller instanceof LifecycleObserver) @@ -101,6 +111,25 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment for (AbstractPreferenceController controller : controllers) { addPreferenceController(controller); } + + checkUiBlocker(controllers); + } + + @VisibleForTesting + void checkUiBlocker(List<AbstractPreferenceController> controllers) { + final List<String> keys = new ArrayList<>(); + controllers + .stream() + .filter(controller -> controller instanceof BasePreferenceController.UiBlocker) + .forEach(controller -> { + ((BasePreferenceController) controller).setUiBlockListener(this); + keys.add(controller.getPreferenceKey()); + }); + + if (!keys.isEmpty()) { + mBlockerController = new UiBlockerController(keys); + mBlockerController.start(()->updatePreferenceVisibility(mPreferenceControllers)); + } } @Override @@ -144,9 +173,9 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment mSummaryLoader.setListening(true); } final Activity activity = getActivity(); - if (activity instanceof SettingsDrawerActivity) { + if (activity instanceof SettingsBaseActivity) { mListeningToCategoryChange = true; - ((SettingsDrawerActivity) activity).addCategoryListener(this); + ((SettingsBaseActivity) activity).addCategoryListener(this); } } @@ -155,12 +184,12 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment final String key = mDashboardFeatureProvider.getDashboardKeyForTile(tile); final Preference pref = getPreferenceScreen().findPreference(key); if (pref == null) { - Log.d(getLogTag(), - String.format("Can't find pref by key %s, skipping update summary %s/%s", - key, tile.title, tile.summary)); + Log.d(getLogTag(), String.format( + "Can't find pref by key %s, skipping update summary %s", + key, tile.getDescription())); return; } - pref.setSummary(tile.summary); + pref.setSummary(tile.getSummary(pref.getContext())); } @Override @@ -196,8 +225,8 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment } if (mListeningToCategoryChange) { final Activity activity = getActivity(); - if (activity instanceof SettingsDrawerActivity) { - ((SettingsDrawerActivity) activity).remCategoryListener(this); + if (activity instanceof SettingsBaseActivity) { + ((SettingsBaseActivity) activity).remCategoryListener(this); } mListeningToCategoryChange = false; } @@ -206,6 +235,17 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment @Override protected abstract int getPreferenceScreenResId(); + @Override + public void onExpandButtonClick() { + mMetricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, + SettingsEnums.ACTION_SETTINGS_ADVANCED_BUTTON_EXPAND, + getMetricsCategory(), null, 0); + } + + protected boolean shouldForceRoundedIcon() { + return false; + } + protected <T extends AbstractPreferenceController> T use(Class<T> clazz) { List<AbstractPreferenceController> controllerList = mPreferenceControllers.get(clazz); if (controllerList != null) { @@ -249,25 +289,13 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment /** * Returns true if this tile should be displayed */ + @CallSuper protected boolean displayTile(Tile tile) { - return true; - } - - @VisibleForTesting - boolean tintTileIcon(Tile tile) { - if (tile.icon == null) { - return false; - } - // First check if the tile has set the icon tintable metadata. - final Bundle metadata = tile.metaData; - if (metadata != null - && metadata.containsKey(TileUtils.META_DATA_PREFERENCE_ICON_TINTABLE)) { - return metadata.getBoolean(TileUtils.META_DATA_PREFERENCE_ICON_TINTABLE); + if (mSuppressInjectedTileKeys != null && tile.hasKey()) { + // For suppressing injected tiles for OEMs. + return !mSuppressInjectedTileKeys.contains(tile.getKey(getContext())); } - final String pkgName = getContext().getPackageName(); - // If this drawable is coming from outside Settings, tint it to match the color. - return pkgName != null && tile.intent != null - && !pkgName.equals(tile.intent.getComponent().getPackageName()); + return true; } /** @@ -280,6 +308,7 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment } addPreferencesFromResource(resId); final PreferenceScreen screen = getPreferenceScreen(); + screen.setOnExpandButtonClickListener(this); mPreferenceControllers.values().stream().flatMap(Collection::stream).forEach( controller -> controller.displayPreference(screen)); } @@ -296,7 +325,13 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment if (!controller.isAvailable()) { continue; } + final String key = controller.getPreferenceKey(); + if (TextUtils.isEmpty(key)) { + Log.d(TAG, String.format("Preference key is %s in Controller %s", + key, controller.getClass().getSimpleName())); + continue; + } final Preference preference = screen.findPreference(key); if (preference == null) { @@ -314,22 +349,52 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment * DashboardCategory. */ private void refreshAllPreferences(final String TAG) { + final PreferenceScreen screen = getPreferenceScreen(); // First remove old preferences. - if (getPreferenceScreen() != null) { + if (screen != null) { // Intentionally do not cache PreferenceScreen because it will be recreated later. - getPreferenceScreen().removeAll(); + screen.removeAll(); } // Add resource based tiles. displayResourceTiles(); refreshDashboardTiles(TAG); + + final Activity activity = getActivity(); + if (activity != null) { + Log.d(TAG, "All preferences added, reporting fully drawn"); + activity.reportFullyDrawn(); + } + + updatePreferenceVisibility(mPreferenceControllers); + } + + @VisibleForTesting + void updatePreferenceVisibility( + Map<Class, List<AbstractPreferenceController>> preferenceControllers) { + final PreferenceScreen screen = getPreferenceScreen(); + if (screen == null || preferenceControllers == null || mBlockerController == null) { + return; + } + + final boolean visible = mBlockerController.isBlockerFinished(); + for (List<AbstractPreferenceController> controllerList : + preferenceControllers.values()) { + for (AbstractPreferenceController controller : controllerList) { + final String key = controller.getPreferenceKey(); + final Preference preference = findPreference(key); + if (preference != null) { + preference.setVisible(visible && controller.isAvailable()); + } + } + } } /** * Refresh preference items backed by DashboardCategory. */ - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + @VisibleForTesting void refreshDashboardTiles(final String TAG) { final PreferenceScreen screen = getPreferenceScreen(); @@ -341,7 +406,7 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment } final List<Tile> tiles = category.getTiles(); if (tiles == null) { - Log.d(TAG, "tile list is empty, skipping category " + category.title); + Log.d(TAG, "tile list is empty, skipping category " + category.key); return; } // Create a list to track which tiles are to be removed. @@ -354,11 +419,8 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment final Context context = getContext(); mSummaryLoader = new SummaryLoader(getActivity(), getCategoryKey()); mSummaryLoader.setSummaryConsumer(this); - final TypedArray a = context.obtainStyledAttributes(new int[] { - android.R.attr.colorControlNormal}); - final int tintColor = a.getColor(0, context.getColor(android.R.color.white)); - a.recycle(); // Install dashboard tiles. + final boolean forceRoundedIcons = shouldForceRoundedIcon(); for (Tile tile : tiles) { final String key = mDashboardFeatureProvider.getDashboardKeyForTile(tile); if (TextUtils.isEmpty(key)) { @@ -368,19 +430,18 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment if (!displayTile(tile)) { continue; } - if (tintTileIcon(tile)) { - tile.icon.setTint(tintColor); - } if (mDashboardTilePrefKeys.contains(key)) { // Have the key already, will rebind. final Preference preference = screen.findPreference(key); - mDashboardFeatureProvider.bindPreferenceToTile(getActivity(), getMetricsCategory(), - preference, tile, key, mPlaceholderPreferenceController.getOrder()); + mDashboardFeatureProvider.bindPreferenceToTile(getActivity(), forceRoundedIcons, + getMetricsCategory(), preference, tile, key, + mPlaceholderPreferenceController.getOrder()); } else { // Don't have this key, add it. final Preference pref = new Preference(getPrefContext()); - mDashboardFeatureProvider.bindPreferenceToTile(getActivity(), getMetricsCategory(), - pref, tile, key, mPlaceholderPreferenceController.getOrder()); + mDashboardFeatureProvider.bindPreferenceToTile(getActivity(), forceRoundedIcons, + getMetricsCategory(), pref, tile, key, + mPlaceholderPreferenceController.getOrder()); screen.addPreference(pref); mDashboardTilePrefKeys.add(key); } @@ -396,4 +457,9 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment } mSummaryLoader.setListening(true); } + + @Override + public void onBlockerWorkFinished(BasePreferenceController controller) { + mBlockerController.countDown(controller.getPreferenceKey()); + } } diff --git a/src/com/android/settings/dashboard/DashboardFragmentRegistry.java b/src/com/android/settings/dashboard/DashboardFragmentRegistry.java index 67c5958a6d..b499b59d7e 100644 --- a/src/com/android/settings/dashboard/DashboardFragmentRegistry.java +++ b/src/com/android/settings/dashboard/DashboardFragmentRegistry.java @@ -19,25 +19,27 @@ package com.android.settings.dashboard; import android.util.ArrayMap; import com.android.settings.DisplaySettings; +import com.android.settings.LegalSettings; import com.android.settings.accounts.AccountDashboardFragment; import com.android.settings.accounts.AccountDetailDashboardFragment; import com.android.settings.applications.AppAndNotificationDashboardFragment; -import com.android.settings.applications.DefaultAppSettings; import com.android.settings.connecteddevice.AdvancedConnectedDeviceDashboardFragment; import com.android.settings.connecteddevice.ConnectedDeviceDashboardFragment; import com.android.settings.development.DevelopmentSettingsDashboardFragment; -import com.android.settings.deviceinfo.aboutphone.MyDeviceInfoFragment; import com.android.settings.deviceinfo.StorageDashboardFragment; +import com.android.settings.deviceinfo.aboutphone.MyDeviceInfoFragment; import com.android.settings.display.NightDisplaySettings; -import com.android.settings.fuelgauge.batterysaver.BatterySaverSettings; +import com.android.settings.enterprise.EnterprisePrivacySettings; import com.android.settings.fuelgauge.PowerUsageSummary; +import com.android.settings.fuelgauge.batterysaver.BatterySaverSettings; import com.android.settings.gestures.GestureSettings; +import com.android.settings.homepage.TopLevelSettings; import com.android.settings.language.LanguageAndInputSettings; -import com.android.settings.LegalSettings; import com.android.settings.network.NetworkDashboardFragment; import com.android.settings.notification.ConfigureNotificationSettings; import com.android.settings.notification.SoundSettings; import com.android.settings.notification.ZenModeSettings; +import com.android.settings.privacy.PrivacyDashboardFragment; import com.android.settings.security.LockscreenDashboardFragment; import com.android.settings.security.SecuritySettings; import com.android.settings.system.SystemDashboardFragment; @@ -64,6 +66,8 @@ public class DashboardFragmentRegistry { static { PARENT_TO_CATEGORY_KEY_MAP = new ArrayMap<>(); + PARENT_TO_CATEGORY_KEY_MAP.put(TopLevelSettings.class.getName(), + CategoryKey.CATEGORY_HOMEPAGE); PARENT_TO_CATEGORY_KEY_MAP.put( NetworkDashboardFragment.class.getName(), CategoryKey.CATEGORY_NETWORK); PARENT_TO_CATEGORY_KEY_MAP.put(ConnectedDeviceDashboardFragment.class.getName(), @@ -74,8 +78,6 @@ public class DashboardFragmentRegistry { CategoryKey.CATEGORY_APPS); PARENT_TO_CATEGORY_KEY_MAP.put(PowerUsageSummary.class.getName(), CategoryKey.CATEGORY_BATTERY); - PARENT_TO_CATEGORY_KEY_MAP.put(DefaultAppSettings.class.getName(), - CategoryKey.CATEGORY_APPS_DEFAULT); PARENT_TO_CATEGORY_KEY_MAP.put(DisplaySettings.class.getName(), CategoryKey.CATEGORY_DISPLAY); PARENT_TO_CATEGORY_KEY_MAP.put(SoundSettings.class.getName(), @@ -101,9 +103,13 @@ public class DashboardFragmentRegistry { PARENT_TO_CATEGORY_KEY_MAP.put(ZenModeSettings.class.getName(), CategoryKey.CATEGORY_DO_NOT_DISTURB); PARENT_TO_CATEGORY_KEY_MAP.put(GestureSettings.class.getName(), - CategoryKey.CATEGORY_GESTURES); + CategoryKey.CATEGORY_GESTURES); PARENT_TO_CATEGORY_KEY_MAP.put(NightDisplaySettings.class.getName(), - CategoryKey.CATEGORY_NIGHT_DISPLAY); + CategoryKey.CATEGORY_NIGHT_DISPLAY); + PARENT_TO_CATEGORY_KEY_MAP.put(PrivacyDashboardFragment.class.getName(), + CategoryKey.CATEGORY_PRIVACY); + PARENT_TO_CATEGORY_KEY_MAP.put(EnterprisePrivacySettings.class.getName(), + CategoryKey.CATEGORY_ENTERPRISE_PRIVACY); PARENT_TO_CATEGORY_KEY_MAP.put(LegalSettings.class.getName(), CategoryKey.CATEGORY_ABOUT_LEGAL); PARENT_TO_CATEGORY_KEY_MAP.put(MyDeviceInfoFragment.class.getName(), diff --git a/src/com/android/settings/dashboard/DashboardItemAnimator.java b/src/com/android/settings/dashboard/DashboardItemAnimator.java deleted file mode 100644 index 73224ab235..0000000000 --- a/src/com/android/settings/dashboard/DashboardItemAnimator.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.settings.dashboard; - -import androidx.core.view.ViewCompat; -import androidx.recyclerview.widget.DefaultItemAnimator; -import androidx.recyclerview.widget.RecyclerView.ViewHolder; -import com.android.settingslib.drawer.Tile; - -public class DashboardItemAnimator extends DefaultItemAnimator { - - @Override - public boolean animateChange(ViewHolder oldHolder, ViewHolder newHolder, int fromX, int fromY, - int toX, int toY) { - final Object tag = oldHolder.itemView.getTag(); - if (tag instanceof Tile && oldHolder == newHolder) { - // When this view has other move animation running, skip this value to avoid - // animations interrupt each other. - if (!isRunning()) { - fromX += ViewCompat.getTranslationX(oldHolder.itemView); - fromY += ViewCompat.getTranslationY(oldHolder.itemView); - } - - if (fromX == toX && fromY == toY) { - dispatchMoveFinished(oldHolder); - return false; - } - } - return super.animateChange(oldHolder, newHolder, fromX, fromY, toX, toY); - } -} diff --git a/src/com/android/settings/dashboard/DashboardSummary.java b/src/com/android/settings/dashboard/DashboardSummary.java deleted file mode 100644 index e680afdfab..0000000000 --- a/src/com/android/settings/dashboard/DashboardSummary.java +++ /dev/null @@ -1,295 +0,0 @@ -/* - * 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.dashboard; - -import android.app.Activity; -import android.app.LoaderManager; -import android.content.Context; -import android.os.Bundle; -import android.os.Handler; -import android.service.settings.suggestions.Suggestion; -import androidx.annotation.VisibleForTesting; -import androidx.annotation.WorkerThread; -import androidx.recyclerview.widget.LinearLayoutManager; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.settings.R; -import com.android.settings.core.InstrumentedFragment; -import com.android.settings.dashboard.conditional.Condition; -import com.android.settings.dashboard.conditional.ConditionManager; -import com.android.settings.dashboard.conditional.ConditionManager.ConditionListener; -import com.android.settings.dashboard.conditional.FocusRecyclerView; -import com.android.settings.dashboard.conditional.FocusRecyclerView.FocusListener; -import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider; -import com.android.settings.overlay.FeatureFactory; -import com.android.settings.widget.ActionBarShadowController; -import com.android.settingslib.drawer.CategoryKey; -import com.android.settingslib.drawer.DashboardCategory; -import com.android.settingslib.drawer.SettingsDrawerActivity; -import com.android.settingslib.drawer.SettingsDrawerActivity.CategoryListener; -import com.android.settingslib.suggestions.SuggestionControllerMixin; -import com.android.settingslib.utils.ThreadUtils; - -import java.util.List; - -public class DashboardSummary extends InstrumentedFragment - implements CategoryListener, ConditionListener, - FocusListener, SuggestionControllerMixin.SuggestionControllerHost { - public static final boolean DEBUG = false; - private static final boolean DEBUG_TIMING = false; - private static final int MAX_WAIT_MILLIS = 3000; - private static final String TAG = "DashboardSummary"; - - private static final String STATE_SCROLL_POSITION = "scroll_position"; - private static final String STATE_CATEGORIES_CHANGE_CALLED = "categories_change_called"; - - private final Handler mHandler = new Handler(); - - private FocusRecyclerView mDashboard; - private DashboardAdapter mAdapter; - private SummaryLoader mSummaryLoader; - private ConditionManager mConditionManager; - private LinearLayoutManager mLayoutManager; - private SuggestionControllerMixin mSuggestionControllerMixin; - private DashboardFeatureProvider mDashboardFeatureProvider; - @VisibleForTesting - boolean mIsOnCategoriesChangedCalled; - private boolean mOnConditionsChangedCalled; - - private DashboardCategory mStagingCategory; - private List<Suggestion> mStagingSuggestions; - - @Override - public int getMetricsCategory() { - return MetricsEvent.DASHBOARD_SUMMARY; - } - - @Override - public void onAttach(Context context) { - super.onAttach(context); - Log.d(TAG, "Creating SuggestionControllerMixin"); - final SuggestionFeatureProvider suggestionFeatureProvider = FeatureFactory - .getFactory(context) - .getSuggestionFeatureProvider(context); - if (suggestionFeatureProvider.isSuggestionEnabled(context)) { - mSuggestionControllerMixin = new SuggestionControllerMixin(context, this /* host */, - getLifecycle(), suggestionFeatureProvider - .getSuggestionServiceComponent()); - } - } - - @Override - public LoaderManager getLoaderManager() { - if (!isAdded()) { - return null; - } - return super.getLoaderManager(); - } - - @Override - public void onCreate(Bundle savedInstanceState) { - long startTime = System.currentTimeMillis(); - super.onCreate(savedInstanceState); - Log.d(TAG, "Starting DashboardSummary"); - final Activity activity = getActivity(); - mDashboardFeatureProvider = FeatureFactory.getFactory(activity) - .getDashboardFeatureProvider(activity); - - mSummaryLoader = new SummaryLoader(activity, CategoryKey.CATEGORY_HOMEPAGE); - - mConditionManager = ConditionManager.get(activity, false); - getLifecycle().addObserver(mConditionManager); - if (savedInstanceState != null) { - mIsOnCategoriesChangedCalled = - savedInstanceState.getBoolean(STATE_CATEGORIES_CHANGE_CALLED); - } - if (DEBUG_TIMING) { - Log.d(TAG, "onCreate took " + (System.currentTimeMillis() - startTime) + " ms"); - } - } - - @Override - public void onDestroy() { - mSummaryLoader.release(); - super.onDestroy(); - } - - @Override - public void onResume() { - long startTime = System.currentTimeMillis(); - super.onResume(); - - ((SettingsDrawerActivity) getActivity()).addCategoryListener(this); - mSummaryLoader.setListening(true); - final int metricsCategory = getMetricsCategory(); - for (Condition c : mConditionManager.getConditions()) { - if (c.shouldShow()) { - mMetricsFeatureProvider.visible(getContext(), metricsCategory, - c.getMetricsConstant()); - } - } - if (DEBUG_TIMING) { - Log.d(TAG, "onResume took " + (System.currentTimeMillis() - startTime) + " ms"); - } - } - - @Override - public void onPause() { - super.onPause(); - - ((SettingsDrawerActivity) getActivity()).remCategoryListener(this); - mSummaryLoader.setListening(false); - for (Condition c : mConditionManager.getConditions()) { - if (c.shouldShow()) { - mMetricsFeatureProvider.hidden(getContext(), c.getMetricsConstant()); - } - } - } - - @Override - public void onWindowFocusChanged(boolean hasWindowFocus) { - long startTime = System.currentTimeMillis(); - if (hasWindowFocus) { - Log.d(TAG, "Listening for condition changes"); - mConditionManager.addListener(this); - Log.d(TAG, "conditions refreshed"); - mConditionManager.refreshAll(); - } else { - Log.d(TAG, "Stopped listening for condition changes"); - mConditionManager.remListener(this); - } - if (DEBUG_TIMING) { - Log.d(TAG, "onWindowFocusChanged took " - + (System.currentTimeMillis() - startTime) + " ms"); - } - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - if (mLayoutManager == null) { - return; - } - outState.putBoolean(STATE_CATEGORIES_CHANGE_CALLED, mIsOnCategoriesChangedCalled); - outState.putInt(STATE_SCROLL_POSITION, mLayoutManager.findFirstVisibleItemPosition()); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) { - long startTime = System.currentTimeMillis(); - final View root = inflater.inflate(R.layout.dashboard, container, false); - mDashboard = root.findViewById(R.id.dashboard_container); - mLayoutManager = new LinearLayoutManager(getContext()); - mLayoutManager.setOrientation(LinearLayoutManager.VERTICAL); - if (bundle != null) { - int scrollPosition = bundle.getInt(STATE_SCROLL_POSITION); - mLayoutManager.scrollToPosition(scrollPosition); - } - mDashboard.setLayoutManager(mLayoutManager); - mDashboard.setHasFixedSize(true); - mDashboard.setListener(this); - mDashboard.setItemAnimator(new DashboardItemAnimator()); - mAdapter = new DashboardAdapter(getContext(), bundle, - mConditionManager.getConditions(), mSuggestionControllerMixin, getLifecycle()); - mDashboard.setAdapter(mAdapter); - mSummaryLoader.setSummaryConsumer(mAdapter); - ActionBarShadowController.attachToRecyclerView( - getActivity().findViewById(R.id.search_bar_container), getLifecycle(), mDashboard); - rebuildUI(); - if (DEBUG_TIMING) { - Log.d(TAG, "onCreateView took " - + (System.currentTimeMillis() - startTime) + " ms"); - } - return root; - } - - @VisibleForTesting - void rebuildUI() { - ThreadUtils.postOnBackgroundThread(() -> updateCategory()); - } - - @Override - public void onCategoriesChanged() { - // Bypass rebuildUI() on the first call of onCategoriesChanged, since rebuildUI() happens - // in onViewCreated as well when app starts. But, on the subsequent calls we need to - // rebuildUI() because there might be some changes to suggestions and categories. - if (mIsOnCategoriesChangedCalled) { - rebuildUI(); - } - mIsOnCategoriesChangedCalled = true; - } - - @Override - public void onConditionsChanged() { - Log.d(TAG, "onConditionsChanged"); - // Bypass refreshing the conditions on the first call of onConditionsChanged. - // onConditionsChanged is called immediately everytime we start listening to the conditions - // change when we gain window focus. Since the conditions are passed to the adapter's - // constructor when we create the view, the first handling is not necessary. - // But, on the subsequent calls we need to handle it because there might be real changes to - // conditions. - if (mOnConditionsChangedCalled) { - final boolean scrollToTop = - mLayoutManager.findFirstCompletelyVisibleItemPosition() <= 1; - mAdapter.setConditions(mConditionManager.getConditions()); - if (scrollToTop) { - mDashboard.scrollToPosition(0); - } - } else { - mOnConditionsChangedCalled = true; - } - } - - @Override - public void onSuggestionReady(List<Suggestion> suggestions) { - mStagingSuggestions = suggestions; - mAdapter.setSuggestions(suggestions); - if (mStagingCategory != null) { - Log.d(TAG, "Category has loaded, setting category from suggestionReady"); - mHandler.removeCallbacksAndMessages(null); - mAdapter.setCategory(mStagingCategory); - } - } - - @WorkerThread - void updateCategory() { - final DashboardCategory category = mDashboardFeatureProvider.getTilesForCategory( - CategoryKey.CATEGORY_HOMEPAGE); - mSummaryLoader.updateSummaryToCache(category); - mStagingCategory = category; - if (mSuggestionControllerMixin == null) { - ThreadUtils.postOnMainThread(() -> mAdapter.setCategory(mStagingCategory)); - return; - } - if (mSuggestionControllerMixin.isSuggestionLoaded()) { - Log.d(TAG, "Suggestion has loaded, setting suggestion/category"); - ThreadUtils.postOnMainThread(() -> { - if (mStagingSuggestions != null) { - mAdapter.setSuggestions(mStagingSuggestions); - } - mAdapter.setCategory(mStagingCategory); - }); - } else { - Log.d(TAG, "Suggestion NOT loaded, delaying setCategory by " + MAX_WAIT_MILLIS + "ms"); - mHandler.postDelayed(() -> mAdapter.setCategory(mStagingCategory), MAX_WAIT_MILLIS); - } - } -} diff --git a/src/com/android/settings/dashboard/DashboardTilePlaceholderPreferenceController.java b/src/com/android/settings/dashboard/DashboardTilePlaceholderPreferenceController.java index 7b53275286..fc26e5521c 100644 --- a/src/com/android/settings/dashboard/DashboardTilePlaceholderPreferenceController.java +++ b/src/com/android/settings/dashboard/DashboardTilePlaceholderPreferenceController.java @@ -17,6 +17,7 @@ package com.android.settings.dashboard; import android.content.Context; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; diff --git a/src/com/android/settings/dashboard/RestrictedDashboardFragment.java b/src/com/android/settings/dashboard/RestrictedDashboardFragment.java index de0b39667d..f43445ba5d 100644 --- a/src/com/android/settings/dashboard/RestrictedDashboardFragment.java +++ b/src/com/android/settings/dashboard/RestrictedDashboardFragment.java @@ -19,7 +19,6 @@ package com.android.settings.dashboard; import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; import android.app.Activity; -import android.app.AlertDialog; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -32,10 +31,12 @@ import android.os.UserManager; import android.view.View; import android.widget.TextView; -import com.android.settings.enterprise.ActionDisabledByAdminDialogHelper; +import androidx.appcompat.app.AlertDialog; + import com.android.settings.R; import com.android.settings.RestrictedSettingsFragment; -import com.android.settingslib.RestrictedLockUtils; +import com.android.settings.enterprise.ActionDisabledByAdminDialogHelper; +import com.android.settingslib.RestrictedLockUtilsInternal; /** * Base class for settings screens that should be pin protected when in restricted mode or @@ -212,10 +213,10 @@ public abstract class RestrictedDashboardFragment extends DashboardFragment { } public EnforcedAdmin getRestrictionEnforcedAdmin() { - mEnforcedAdmin = RestrictedLockUtils.checkIfRestrictionEnforced(getActivity(), + mEnforcedAdmin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(getActivity(), mRestrictionKey, UserHandle.myUserId()); - if (mEnforcedAdmin != null && mEnforcedAdmin.userId == UserHandle.USER_NULL) { - mEnforcedAdmin.userId = UserHandle.myUserId(); + if (mEnforcedAdmin != null && mEnforcedAdmin.user == null) { + mEnforcedAdmin.user = UserHandle.of(UserHandle.myUserId()); } return mEnforcedAdmin; } diff --git a/src/com/android/settings/dashboard/RoundedHomepageIcon.java b/src/com/android/settings/dashboard/RoundedHomepageIcon.java deleted file mode 100644 index 77acaf3ec1..0000000000 --- a/src/com/android/settings/dashboard/RoundedHomepageIcon.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (C) 2018 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.dashboard; - -import static androidx.annotation.VisibleForTesting.NONE; - -import android.content.Context; -import android.graphics.PorterDuff; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.LayerDrawable; -import androidx.annotation.VisibleForTesting; - -import com.android.settings.R; - -public class RoundedHomepageIcon extends LayerDrawable { - - @VisibleForTesting(otherwise = NONE) - int mBackgroundColor = -1; - - public RoundedHomepageIcon(Context context, Drawable foreground) { - super(new Drawable[] { - context.getDrawable(R.drawable.ic_homepage_generic_background), - foreground - }); - final int insetPx = context.getResources() - .getDimensionPixelSize(R.dimen.dashboard_tile_foreground_image_inset); - setLayerInset(1 /* index */, insetPx, insetPx, insetPx, insetPx); - } - - public void setBackgroundColor(int color) { - mBackgroundColor = color; - getDrawable(0).setColorFilter(color, PorterDuff.Mode.SRC_ATOP); - } -} diff --git a/src/com/android/settings/dashboard/SummaryLoader.java b/src/com/android/settings/dashboard/SummaryLoader.java index a97113147e..059cb93d9d 100644 --- a/src/com/android/settings/dashboard/SummaryLoader.java +++ b/src/com/android/settings/dashboard/SummaryLoader.java @@ -18,6 +18,8 @@ package com.android.settings.dashboard; import android.app.Activity; import android.content.BroadcastReceiver; import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; import android.os.Handler; @@ -25,12 +27,13 @@ import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.os.Process; -import androidx.annotation.VisibleForTesting; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; +import androidx.annotation.VisibleForTesting; + import com.android.settings.SettingsActivity; import com.android.settings.overlay.FeatureFactory; import com.android.settingslib.drawer.DashboardCategory; @@ -41,7 +44,7 @@ import java.lang.reflect.Field; import java.util.List; public class SummaryLoader { - private static final boolean DEBUG = DashboardSummary.DEBUG; + private static final boolean DEBUG = false; private static final String TAG = "SummaryLoader"; public static final String SUMMARY_PROVIDER_FACTORY = "SUMMARY_PROVIDER_FACTORY"; @@ -94,29 +97,30 @@ public class SummaryLoader { return; } if (DEBUG) { - Log.d(TAG, "setSummary " + tile.title + " - " + summary); + Log.d(TAG, "setSummary " + tile.getDescription() + " - " + summary); } - updateSummaryIfNeeded(tile, summary); + updateSummaryIfNeeded(mActivity.getApplicationContext(), tile, summary); }); } @VisibleForTesting - void updateSummaryIfNeeded(Tile tile, CharSequence summary) { - if (TextUtils.equals(tile.summary, summary)) { + void updateSummaryIfNeeded(Context context, Tile tile, CharSequence summary) { + if (TextUtils.equals(tile.getSummary(context), summary)) { if (DEBUG) { - Log.d(TAG, "Summary doesn't change, skipping summary update for " + tile.title); + Log.d(TAG, "Summary doesn't change, skipping summary update for " + + tile.getDescription()); } return; } mSummaryTextMap.put(mDashboardFeatureProvider.getDashboardKeyForTile(tile), summary); - tile.summary = summary; + tile.overrideSummary(summary); if (mSummaryConsumer != null) { mSummaryConsumer.notifySummaryChanged(tile); } else { if (DEBUG) { Log.d(TAG, "SummaryConsumer is null, skipping summary update for " - + tile.title); + + tile.getDescription()); } } } @@ -154,19 +158,20 @@ public class SummaryLoader { } private SummaryProvider getSummaryProvider(Tile tile) { - if (!mActivity.getPackageName().equals(tile.intent.getComponent().getPackageName())) { + if (!mActivity.getPackageName().equals(tile.getPackageName())) { // Not within Settings, can't load Summary directly. // TODO: Load summary indirectly. return null; } - Bundle metaData = getMetaData(tile); + final Bundle metaData = tile.getMetaData(); + final Intent intent = tile.getIntent(); if (metaData == null) { - if (DEBUG) Log.d(TAG, "No metadata specified for " + tile.intent.getComponent()); + Log.d(TAG, "No metadata specified for " + intent.getComponent()); return null; } - String clsName = metaData.getString(SettingsActivity.META_DATA_KEY_FRAGMENT_CLASS); + final String clsName = metaData.getString(SettingsActivity.META_DATA_KEY_FRAGMENT_CLASS); if (clsName == null) { - if (DEBUG) Log.d(TAG, "No fragment specified for " + tile.intent.getComponent()); + Log.d(TAG, "No fragment specified for " + intent.getComponent()); return null; } try { @@ -186,25 +191,18 @@ public class SummaryLoader { return null; } - private Bundle getMetaData(Tile tile) { - return tile.metaData; - } - /** * Registers a receiver and automatically unregisters it when the activity is stopping. * This ensures that the receivers are unregistered immediately, since most summary loader * operations are asynchronous. */ public void registerReceiver(final BroadcastReceiver receiver, final IntentFilter filter) { - mActivity.runOnUiThread(new Runnable() { - @Override - public void run() { - if (!mListening) { - return; - } - mReceivers.add(receiver); - mActivity.registerReceiver(receiver, filter); + mActivity.runOnUiThread(() -> { + if (!mListening) { + return; } + mReceivers.add(receiver); + mActivity.registerReceiver(receiver, filter); }); } @@ -219,7 +217,7 @@ public class SummaryLoader { for (Tile tile : category.getTiles()) { final String key = mDashboardFeatureProvider.getDashboardKeyForTile(tile); if (mSummaryTextMap.containsKey(key)) { - tile.summary = mSummaryTextMap.get(key); + tile.overrideSummary(mSummaryTextMap.get(key)); } } } @@ -245,7 +243,7 @@ public class SummaryLoader { SummaryProvider provider = getSummaryProvider(tile); if (provider != null) { if (DEBUG) Log.d(TAG, "Creating " + tile); - mSummaryProviderMap.put(provider, tile.intent.getComponent()); + mSummaryProviderMap.put(provider, tile.getIntent().getComponent()); } } @@ -257,7 +255,7 @@ public class SummaryLoader { final int tileCount = tiles.size(); for (int j = 0; j < tileCount; j++) { final Tile tile = tiles.get(j); - if (component.equals(tile.intent.getComponent())) { + if (component.equals(tile.getIntent().getComponent())) { return tile; } } diff --git a/src/com/android/settings/dashboard/UiBlockerController.java b/src/com/android/settings/dashboard/UiBlockerController.java new file mode 100644 index 0000000000..710175b112 --- /dev/null +++ b/src/com/android/settings/dashboard/UiBlockerController.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2019 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.dashboard; + +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.android.settings.core.BasePreferenceController; +import com.android.settingslib.utils.ThreadUtils; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * Control ui blocker data and check whether it is finished + * + * @see BasePreferenceController.UiBlocker + * @see BasePreferenceController.UiBlockListener + */ +public class UiBlockerController { + private static final String TAG = "UiBlockerController"; + private static final int TIMEOUT_MILLIS = 500; + + private CountDownLatch mCountDownLatch; + private boolean mBlockerFinished; + private Set<String> mKeys; + private long mTimeoutMillis; + + public UiBlockerController(@NonNull List<String> keys) { + this(keys, TIMEOUT_MILLIS); + } + + public UiBlockerController(@NonNull List<String> keys, long timeout) { + mCountDownLatch = new CountDownLatch(keys.size()); + mBlockerFinished = keys.isEmpty(); + mKeys = new HashSet<>(keys); + mTimeoutMillis = timeout; + } + + /** + * Start background thread, it will invoke {@code finishRunnable} if any condition is met + * + * 1. Waiting time exceeds {@link #mTimeoutMillis} + * 2. All background work that associated with {@link #mCountDownLatch} is finished + */ + public boolean start(Runnable finishRunnable) { + if (mKeys.isEmpty()) { + // Don't need to run finishRunnable because it doesn't start + return false; + } + ThreadUtils.postOnBackgroundThread(() -> { + try { + mCountDownLatch.await(mTimeoutMillis, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + Log.w(TAG, "interrupted"); + } + mBlockerFinished = true; + ThreadUtils.postOnMainThread(finishRunnable); + }); + + return true; + } + + /** + * Return {@code true} if all work finished + */ + public boolean isBlockerFinished() { + return mBlockerFinished; + } + + /** + * Count down latch by {@code key}. It only count down 1 time if same key count down multiple + * times. + */ + public boolean countDown(String key) { + if (mKeys.remove(key)) { + mCountDownLatch.countDown(); + return true; + } + + return false; + } +} diff --git a/src/com/android/settings/dashboard/conditional/AirplaneModeCondition.java b/src/com/android/settings/dashboard/conditional/AirplaneModeCondition.java deleted file mode 100644 index 792a090ddf..0000000000 --- a/src/com/android/settings/dashboard/conditional/AirplaneModeCondition.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * 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.dashboard.conditional; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.graphics.drawable.Drawable; -import android.net.ConnectivityManager; -import android.provider.Settings; -import android.util.Log; - -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.settings.R; -import com.android.settingslib.WirelessUtils; - -public class AirplaneModeCondition extends Condition { - public static String TAG = "APM_Condition"; - - private final Receiver mReceiver; - - private static final IntentFilter AIRPLANE_MODE_FILTER = - new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED); - - public AirplaneModeCondition(ConditionManager conditionManager) { - super(conditionManager); - mReceiver = new Receiver(); - } - - @Override - public void refreshState() { - Log.d(TAG, "APM condition refreshed"); - setActive(WirelessUtils.isAirplaneModeOn(mManager.getContext())); - } - - @Override - protected BroadcastReceiver getReceiver() { - return mReceiver; - } - - @Override - protected IntentFilter getIntentFilter() { - return AIRPLANE_MODE_FILTER; - } - - @Override - public Drawable getIcon() { - return mManager.getContext().getDrawable(R.drawable.ic_airplane); - } - - @Override - protected void setActive(boolean active) { - super.setActive(active); - Log.d(TAG, "setActive was called with " + active); - } - - @Override - public CharSequence getTitle() { - return mManager.getContext().getString(R.string.condition_airplane_title); - } - - @Override - public CharSequence getSummary() { - return mManager.getContext().getString(R.string.condition_airplane_summary); - } - - @Override - public CharSequence[] getActions() { - return new CharSequence[] {mManager.getContext().getString(R.string.condition_turn_off)}; - } - - @Override - public void onPrimaryClick() { - mManager.getContext().startActivity( - new Intent(Settings.ACTION_WIRELESS_SETTINGS) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); - } - - @Override - public void onActionClick(int index) { - if (index == 0) { - ConnectivityManager.from(mManager.getContext()).setAirplaneMode(false); - setActive(false); - } else { - throw new IllegalArgumentException("Unexpected index " + index); - } - } - - @Override - public int getMetricsConstant() { - return MetricsEvent.SETTINGS_CONDITION_AIRPLANE_MODE; - } - - public static class Receiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - if (Intent.ACTION_AIRPLANE_MODE_CHANGED.equals(intent.getAction())) { - ConditionManager.get(context).getCondition(AirplaneModeCondition.class) - .refreshState(); - } - } - } -} diff --git a/src/com/android/settings/dashboard/conditional/BackgroundDataCondition.java b/src/com/android/settings/dashboard/conditional/BackgroundDataCondition.java deleted file mode 100644 index a7e160fc68..0000000000 --- a/src/com/android/settings/dashboard/conditional/BackgroundDataCondition.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * 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.dashboard.conditional; - -import android.content.Intent; -import android.graphics.drawable.Drawable; -import android.net.NetworkPolicyManager; -import android.util.FeatureFlagUtils; - -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.settings.R; -import com.android.settings.Settings; -import com.android.settings.core.FeatureFlags; - -public class BackgroundDataCondition extends Condition { - - public BackgroundDataCondition(ConditionManager manager) { - super(manager); - } - - @Override - public void refreshState() { - setActive(NetworkPolicyManager.from(mManager.getContext()).getRestrictBackground()); - } - - @Override - public Drawable getIcon() { - return mManager.getContext().getDrawable(R.drawable.ic_data_saver); - } - - @Override - public CharSequence getTitle() { - return mManager.getContext().getString(R.string.condition_bg_data_title); - } - - @Override - public CharSequence getSummary() { - return mManager.getContext().getString(R.string.condition_bg_data_summary); - } - - @Override - public CharSequence[] getActions() { - return new CharSequence[] {mManager.getContext().getString(R.string.condition_turn_off)}; - } - - @Override - public void onPrimaryClick() { - final Class activityClass = FeatureFlagUtils.isEnabled(mManager.getContext(), - FeatureFlags.DATA_USAGE_SETTINGS_V2) - ? Settings.DataUsageSummaryActivity.class - : Settings.DataUsageSummaryLegacyActivity.class; - mManager.getContext().startActivity(new Intent(mManager.getContext(), activityClass) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); - } - - @Override - public int getMetricsConstant() { - return MetricsEvent.SETTINGS_CONDITION_BACKGROUND_DATA; - } - - @Override - public void onActionClick(int index) { - if (index == 0) { - NetworkPolicyManager.from(mManager.getContext()).setRestrictBackground(false); - setActive(false); - } else { - throw new IllegalArgumentException("Unexpected index " + index); - } - } -} diff --git a/src/com/android/settings/dashboard/conditional/BatterySaverCondition.java b/src/com/android/settings/dashboard/conditional/BatterySaverCondition.java deleted file mode 100644 index fdd1508db8..0000000000 --- a/src/com/android/settings/dashboard/conditional/BatterySaverCondition.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * 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.dashboard.conditional; - -import android.content.Intent; -import android.graphics.drawable.Drawable; -import android.os.PowerManager; - -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.settings.R; -import com.android.settings.core.SubSettingLauncher; -import com.android.settings.fuelgauge.BatterySaverDrawable; -import com.android.settings.fuelgauge.BatterySaverReceiver; -import com.android.settings.fuelgauge.batterysaver.BatterySaverSettings; -import com.android.settingslib.fuelgauge.BatterySaverUtils; - -public class BatterySaverCondition extends Condition implements - BatterySaverReceiver.BatterySaverListener { - - private final BatterySaverReceiver mReceiver; - - public BatterySaverCondition(ConditionManager manager) { - super(manager); - - mReceiver = new BatterySaverReceiver(manager.getContext()); - mReceiver.setBatterySaverListener(this); - } - - @Override - public void refreshState() { - PowerManager powerManager = mManager.getContext().getSystemService(PowerManager.class); - setActive(powerManager.isPowerSaveMode()); - } - - @Override - public Drawable getIcon() { - return mManager.getContext().getDrawable(R.drawable.ic_battery_saver_accent_24dp); - } - - @Override - public CharSequence getTitle() { - return mManager.getContext().getString(R.string.condition_battery_title); - } - - @Override - public CharSequence getSummary() { - return mManager.getContext().getString(R.string.condition_battery_summary); - } - - @Override - public CharSequence[] getActions() { - return new CharSequence[]{mManager.getContext().getString(R.string.condition_turn_off)}; - } - - @Override - public void onPrimaryClick() { - new SubSettingLauncher(mManager.getContext()) - .setDestination(BatterySaverSettings.class.getName()) - .setSourceMetricsCategory(MetricsEvent.DASHBOARD_SUMMARY) - .setTitle(R.string.battery_saver) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .launch(); - } - - @Override - public void onActionClick(int index) { - if (index == 0) { - BatterySaverUtils.setPowerSaveMode(mManager.getContext(), false, - /*needFirstTimeWarning*/ false); - refreshState(); - } else { - throw new IllegalArgumentException("Unexpected index " + index); - } - } - - @Override - public int getMetricsConstant() { - return MetricsEvent.SETTINGS_CONDITION_BATTERY_SAVER; - } - - @Override - public void onResume() { - mReceiver.setListening(true); - } - - @Override - public void onPause() { - mReceiver.setListening(false); - } - - @Override - public void onPowerSaveModeChanged() { - ConditionManager.get(mManager.getContext()).getCondition(BatterySaverCondition.class) - .refreshState(); - } - - @Override - public void onBatteryChanged(boolean pluggedIn) { - // do nothing - } -} diff --git a/src/com/android/settings/dashboard/conditional/CellularDataCondition.java b/src/com/android/settings/dashboard/conditional/CellularDataCondition.java deleted file mode 100644 index bc0cbd5200..0000000000 --- a/src/com/android/settings/dashboard/conditional/CellularDataCondition.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * 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.dashboard.conditional; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.graphics.drawable.Drawable; -import android.net.ConnectivityManager; -import android.telephony.TelephonyManager; - -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.internal.telephony.TelephonyIntents; -import com.android.settings.R; -import com.android.settings.Settings; - -public class CellularDataCondition extends Condition { - - private final Receiver mReceiver; - - private static final IntentFilter DATA_CONNECTION_FILTER = - new IntentFilter(TelephonyIntents.ACTION_ANY_DATA_CONNECTION_STATE_CHANGED); - - public CellularDataCondition(ConditionManager manager) { - super(manager); - mReceiver = new Receiver(); - } - - @Override - public void refreshState() { - ConnectivityManager connectivity = mManager.getContext().getSystemService( - ConnectivityManager.class); - TelephonyManager telephony = mManager.getContext().getSystemService(TelephonyManager.class); - if (!connectivity.isNetworkSupported(ConnectivityManager.TYPE_MOBILE) - || telephony.getSimState() != TelephonyManager.SIM_STATE_READY) { - setActive(false); - return; - } - setActive(!telephony.isDataEnabled()); - } - - @Override - protected BroadcastReceiver getReceiver() { - return mReceiver; - } - - @Override - protected IntentFilter getIntentFilter() { - return DATA_CONNECTION_FILTER; - } - - @Override - public Drawable getIcon() { - return mManager.getContext().getDrawable(R.drawable.ic_cellular_off); - } - - @Override - public CharSequence getTitle() { - return mManager.getContext().getString(R.string.condition_cellular_title); - } - - @Override - public CharSequence getSummary() { - return mManager.getContext().getString(R.string.condition_cellular_summary); - } - - @Override - public CharSequence[] getActions() { - return new CharSequence[] { mManager.getContext().getString(R.string.condition_turn_on) }; - } - - @Override - public void onPrimaryClick() { - mManager.getContext().startActivity(new Intent(mManager.getContext(), - Settings.DataUsageSummaryActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); - } - - @Override - public void onActionClick(int index) { - if (index == 0) { - TelephonyManager telephony = mManager.getContext().getSystemService( - TelephonyManager.class); - telephony.setDataEnabled(true); - setActive(false); - } else { - throw new IllegalArgumentException("Unexpected index " + index); - } - } - - @Override - public int getMetricsConstant() { - return MetricsEvent.SETTINGS_CONDITION_CELLULAR_DATA; - } - - public static class Receiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - if (TelephonyIntents.ACTION_ANY_DATA_CONNECTION_STATE_CHANGED.equals( - intent.getAction())) { - CellularDataCondition condition = ConditionManager.get(context).getCondition( - CellularDataCondition.class); - if (condition != null) { - condition.refreshState(); - } - } - } - } -} diff --git a/src/com/android/settings/dashboard/conditional/Condition.java b/src/com/android/settings/dashboard/conditional/Condition.java deleted file mode 100644 index f3a3b9c05a..0000000000 --- a/src/com/android/settings/dashboard/conditional/Condition.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * 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.dashboard.conditional; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.IntentFilter; -import android.graphics.drawable.Drawable; -import android.os.PersistableBundle; -import androidx.annotation.VisibleForTesting; - -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.settings.overlay.FeatureFactory; -import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; - -public abstract class Condition { - - private static final String KEY_SILENCE = "silence"; - private static final String KEY_ACTIVE = "active"; - private static final String KEY_LAST_STATE = "last_state"; - - protected final ConditionManager mManager; - protected final MetricsFeatureProvider mMetricsFeatureProvider; - protected boolean mReceiverRegistered; - - private boolean mIsSilenced; - private boolean mIsActive; - private long mLastStateChange; - - // All conditions must live in this package. - Condition(ConditionManager manager) { - this(manager, FeatureFactory.getFactory(manager.getContext()).getMetricsFeatureProvider()); - } - - Condition(ConditionManager manager, MetricsFeatureProvider metricsFeatureProvider) { - mManager = manager; - mMetricsFeatureProvider = metricsFeatureProvider; - } - - void restoreState(PersistableBundle bundle) { - mIsSilenced = bundle.getBoolean(KEY_SILENCE); - mIsActive = bundle.getBoolean(KEY_ACTIVE); - mLastStateChange = bundle.getLong(KEY_LAST_STATE); - } - - boolean saveState(PersistableBundle bundle) { - if (mIsSilenced) { - bundle.putBoolean(KEY_SILENCE, mIsSilenced); - } - if (mIsActive) { - bundle.putBoolean(KEY_ACTIVE, mIsActive); - bundle.putLong(KEY_LAST_STATE, mLastStateChange); - } - return mIsSilenced || mIsActive; - } - - protected void notifyChanged() { - mManager.notifyChanged(this); - } - - public boolean isSilenced() { - return mIsSilenced; - } - - public boolean isActive() { - return mIsActive; - } - - protected void setActive(boolean active) { - if (mIsActive == active) { - return; - } - mIsActive = active; - mLastStateChange = System.currentTimeMillis(); - if (mIsSilenced && !active) { - mIsSilenced = false; - onSilenceChanged(mIsSilenced); - } - notifyChanged(); - } - - public void silence() { - if (!mIsSilenced) { - mIsSilenced = true; - Context context = mManager.getContext(); - mMetricsFeatureProvider.action(context, MetricsEvent.ACTION_SETTINGS_CONDITION_DISMISS, - getMetricsConstant()); - onSilenceChanged(mIsSilenced); - notifyChanged(); - } - } - - @VisibleForTesting - void onSilenceChanged(boolean silenced) { - final BroadcastReceiver receiver = getReceiver(); - if (receiver == null) { - return; - } - if (silenced) { - if (!mReceiverRegistered) { - mManager.getContext().registerReceiver(receiver, getIntentFilter()); - mReceiverRegistered = true; - } - } else { - if (mReceiverRegistered) { - mManager.getContext().unregisterReceiver(receiver); - mReceiverRegistered = false; - } - } - } - - protected BroadcastReceiver getReceiver() { - return null; - } - - protected IntentFilter getIntentFilter() { - return null; - } - - public boolean shouldShow() { - return isActive() && !isSilenced(); - } - - long getLastChange() { - return mLastStateChange; - } - - public void onResume() { - } - - public void onPause() { - } - - // State. - public abstract void refreshState(); - - public abstract int getMetricsConstant(); - - // UI. - public abstract Drawable getIcon(); - public abstract CharSequence getTitle(); - public abstract CharSequence getSummary(); - public abstract CharSequence[] getActions(); - - public abstract void onPrimaryClick(); - public abstract void onActionClick(int index); -} diff --git a/src/com/android/settings/dashboard/conditional/ConditionAdapter.java b/src/com/android/settings/dashboard/conditional/ConditionAdapter.java deleted file mode 100644 index 923f1241a5..0000000000 --- a/src/com/android/settings/dashboard/conditional/ConditionAdapter.java +++ /dev/null @@ -1,186 +0,0 @@ -/* - * Copyright (C) 2017 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.dashboard.conditional; - -import android.content.Context; -import androidx.annotation.VisibleForTesting; -import androidx.recyclerview.widget.RecyclerView; -import androidx.recyclerview.widget.ItemTouchHelper; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; - -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.settings.R; -import com.android.settings.dashboard.DashboardAdapter.DashboardItemHolder; -import com.android.settings.overlay.FeatureFactory; -import com.android.settingslib.WirelessUtils; -import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; - -import java.util.List; -import java.util.Objects; - -public class ConditionAdapter extends RecyclerView.Adapter<DashboardItemHolder> { - public static final String TAG = "ConditionAdapter"; - - private final Context mContext; - private final MetricsFeatureProvider mMetricsFeatureProvider; - private List<Condition> mConditions; - private boolean mExpanded; - - private View.OnClickListener mConditionClickListener = new View.OnClickListener() { - - @Override - public void onClick(View v) { - //TODO: get rid of setTag/getTag - Condition condition = (Condition) v.getTag(); - mMetricsFeatureProvider.action(mContext, - MetricsEvent.ACTION_SETTINGS_CONDITION_CLICK, - condition.getMetricsConstant()); - condition.onPrimaryClick(); - } - }; - - @VisibleForTesting - ItemTouchHelper.SimpleCallback mSwipeCallback = new ItemTouchHelper.SimpleCallback(0, - ItemTouchHelper.START | ItemTouchHelper.END) { - @Override - public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, - RecyclerView.ViewHolder target) { - return true; - } - - @Override - public int getSwipeDirs(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { - return viewHolder.getItemViewType() == R.layout.condition_tile - ? super.getSwipeDirs(recyclerView, viewHolder) : 0; - } - - @Override - public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { - Object item = getItem(viewHolder.getItemId()); - // item can become null when running monkey - if (item != null) { - ((Condition) item).silence(); - } - } - }; - - public ConditionAdapter(Context context, List<Condition> conditions, boolean expanded) { - mContext = context; - mConditions = conditions; - mExpanded = expanded; - mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider(); - - setHasStableIds(true); - } - - public Object getItem(long itemId) { - for (Condition condition : mConditions) { - if (Objects.hash(condition.getTitle()) == itemId) { - return condition; - } - } - return null; - } - - @Override - public DashboardItemHolder onCreateViewHolder(ViewGroup parent, int viewType) { - return new DashboardItemHolder(LayoutInflater.from(parent.getContext()).inflate( - viewType, parent, false)); - } - - @Override - public void onBindViewHolder(DashboardItemHolder holder, int position) { - bindViews(mConditions.get(position), holder, - position == mConditions.size() - 1, mConditionClickListener); - } - - @Override - public long getItemId(int position) { - return Objects.hash(mConditions.get(position).getTitle()); - } - - @Override - public int getItemViewType(int position) { - return R.layout.condition_tile; - } - - @Override - public int getItemCount() { - if (mExpanded) { - return mConditions.size(); - } - return 0; - } - - public void addDismissHandling(final RecyclerView recyclerView) { - final ItemTouchHelper itemTouchHelper = new ItemTouchHelper(mSwipeCallback); - itemTouchHelper.attachToRecyclerView(recyclerView); - } - - private void bindViews(final Condition condition, - DashboardItemHolder view, boolean isLastItem, - View.OnClickListener onClickListener) { - if (condition instanceof AirplaneModeCondition) { - Log.d(TAG, "Airplane mode condition has been bound with " - + "isActive=" + condition.isActive() + ". Airplane mode is currently " + - WirelessUtils.isAirplaneModeOn(condition.mManager.getContext())); - } - View card = view.itemView.findViewById(R.id.content); - card.setTag(condition); - card.setOnClickListener(onClickListener); - view.icon.setImageDrawable(condition.getIcon()); - view.title.setText(condition.getTitle()); - - CharSequence[] actions = condition.getActions(); - final boolean hasButtons = actions.length > 0; - setViewVisibility(view.itemView, R.id.buttonBar, hasButtons); - - view.summary.setText(condition.getSummary()); - for (int i = 0; i < 2; i++) { - Button button = (Button) view.itemView.findViewById(i == 0 - ? R.id.first_action : R.id.second_action); - if (actions.length > i) { - button.setVisibility(View.VISIBLE); - button.setText(actions[i]); - final int index = i; - button.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Context context = v.getContext(); - FeatureFactory.getFactory(context).getMetricsFeatureProvider() - .action(context, MetricsEvent.ACTION_SETTINGS_CONDITION_BUTTON, - condition.getMetricsConstant()); - condition.onActionClick(index); - } - }); - } else { - button.setVisibility(View.GONE); - } - } - setViewVisibility(view.itemView, R.id.divider, !isLastItem); - } - - private void setViewVisibility(View containerView, int viewId, boolean visible) { - View view = containerView.findViewById(viewId); - if (view != null) { - view.setVisibility(visible ? View.VISIBLE : View.GONE); - } - } -} diff --git a/src/com/android/settings/dashboard/conditional/ConditionManager.java b/src/com/android/settings/dashboard/conditional/ConditionManager.java deleted file mode 100644 index 2754d8a5e6..0000000000 --- a/src/com/android/settings/dashboard/conditional/ConditionManager.java +++ /dev/null @@ -1,306 +0,0 @@ -/* - * 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.dashboard.conditional; - -import android.content.Context; -import android.os.AsyncTask; -import android.os.PersistableBundle; -import android.util.Log; -import android.util.Xml; - -import com.android.settingslib.core.lifecycle.LifecycleObserver; -import com.android.settingslib.core.lifecycle.events.OnPause; -import com.android.settingslib.core.lifecycle.events.OnResume; - -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; -import org.xmlpull.v1.XmlSerializer; - -import java.io.File; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -public class ConditionManager implements LifecycleObserver, OnResume, OnPause { - - private static final String TAG = "ConditionManager"; - - private static final boolean DEBUG = false; - - private static final String PKG = "com.android.settings.dashboard.conditional."; - - private static final String FILE_NAME = "condition_state.xml"; - private static final String TAG_CONDITIONS = "cs"; - private static final String TAG_CONDITION = "c"; - private static final String ATTR_CLASS = "cls"; - - private static ConditionManager sInstance; - - private final Context mContext; - private final ArrayList<Condition> mConditions; - private File mXmlFile; - - private final ArrayList<ConditionListener> mListeners = new ArrayList<>(); - - private ConditionManager(Context context, boolean loadConditionsNow) { - mContext = context; - mConditions = new ArrayList<>(); - if (loadConditionsNow) { - Log.d(TAG, "conditions loading synchronously"); - ConditionLoader loader = new ConditionLoader(); - loader.onPostExecute(loader.doInBackground()); - } else { - Log.d(TAG, "conditions loading asychronously"); - new ConditionLoader().execute(); - } - } - - public void refreshAll() { - final int N = mConditions.size(); - for (int i = 0; i < N; i++) { - mConditions.get(i).refreshState(); - } - } - - private void readFromXml(File xmlFile, ArrayList<Condition> conditions) { - if (DEBUG) Log.d(TAG, "Reading from " + xmlFile.toString()); - try { - XmlPullParser parser = Xml.newPullParser(); - FileReader in = new FileReader(xmlFile); - parser.setInput(in); - int state = parser.getEventType(); - - while (state != XmlPullParser.END_DOCUMENT) { - if (TAG_CONDITION.equals(parser.getName())) { - int depth = parser.getDepth(); - String clz = parser.getAttributeValue("", ATTR_CLASS); - if (!clz.startsWith(PKG)) { - clz = PKG + clz; - } - Condition condition = createCondition(Class.forName(clz)); - PersistableBundle bundle = PersistableBundle.restoreFromXml(parser); - if (DEBUG) Log.d(TAG, "Reading " + clz + " -- " + bundle); - if (condition != null) { - condition.restoreState(bundle); - conditions.add(condition); - } else { - Log.e(TAG, "failed to add condition: " + clz); - } - while (parser.getDepth() > depth) { - parser.next(); - } - } - state = parser.next(); - } - in.close(); - } catch (XmlPullParserException | IOException | ClassNotFoundException e) { - Log.w(TAG, "Problem reading " + FILE_NAME, e); - } - } - - private void saveToXml() { - if (DEBUG) Log.d(TAG, "Writing to " + mXmlFile.toString()); - try { - XmlSerializer serializer = Xml.newSerializer(); - FileWriter writer = new FileWriter(mXmlFile); - serializer.setOutput(writer); - - serializer.startDocument("UTF-8", true); - serializer.startTag("", TAG_CONDITIONS); - - final int N = mConditions.size(); - for (int i = 0; i < N; i++) { - PersistableBundle bundle = new PersistableBundle(); - if (mConditions.get(i).saveState(bundle)) { - serializer.startTag("", TAG_CONDITION); - final String clz = mConditions.get(i).getClass().getSimpleName(); - serializer.attribute("", ATTR_CLASS, clz); - bundle.saveToXml(serializer); - serializer.endTag("", TAG_CONDITION); - } - } - - serializer.endTag("", TAG_CONDITIONS); - serializer.flush(); - writer.close(); - } catch (XmlPullParserException | IOException e) { - Log.w(TAG, "Problem writing " + FILE_NAME, e); - } - } - - private void addMissingConditions(ArrayList<Condition> conditions) { - addIfMissing(AirplaneModeCondition.class, conditions); - addIfMissing(HotspotCondition.class, conditions); - addIfMissing(DndCondition.class, conditions); - addIfMissing(BatterySaverCondition.class, conditions); - addIfMissing(CellularDataCondition.class, conditions); - addIfMissing(BackgroundDataCondition.class, conditions); - addIfMissing(WorkModeCondition.class, conditions); - addIfMissing(NightDisplayCondition.class, conditions); - addIfMissing(RingerMutedCondition.class, conditions); - addIfMissing(RingerVibrateCondition.class, conditions); - Collections.sort(conditions, CONDITION_COMPARATOR); - } - - private void addIfMissing(Class<? extends Condition> clz, ArrayList<Condition> conditions) { - if (getCondition(clz, conditions) == null) { - if (DEBUG) Log.d(TAG, "Adding missing " + clz.getName()); - Condition condition = createCondition(clz); - if (condition != null) { - conditions.add(condition); - } - } - } - - private Condition createCondition(Class<?> clz) { - if (AirplaneModeCondition.class == clz) { - return new AirplaneModeCondition(this); - } else if (HotspotCondition.class == clz) { - return new HotspotCondition(this); - } else if (DndCondition.class == clz) { - return new DndCondition(this); - } else if (BatterySaverCondition.class == clz) { - return new BatterySaverCondition(this); - } else if (CellularDataCondition.class == clz) { - return new CellularDataCondition(this); - } else if (BackgroundDataCondition.class == clz) { - return new BackgroundDataCondition(this); - } else if (WorkModeCondition.class == clz) { - return new WorkModeCondition(this); - } else if (NightDisplayCondition.class == clz) { - return new NightDisplayCondition(this); - } else if (RingerMutedCondition.class == clz) { - return new RingerMutedCondition(this); - } else if (RingerVibrateCondition.class == clz) { - return new RingerVibrateCondition(this); - } - Log.e(TAG, "unknown condition class: " + clz.getSimpleName()); - return null; - } - - Context getContext() { - return mContext; - } - - public <T extends Condition> T getCondition(Class<T> clz) { - return getCondition(clz, mConditions); - } - - private <T extends Condition> T getCondition(Class<T> clz, List<Condition> conditions) { - final int N = conditions.size(); - for (int i = 0; i < N; i++) { - if (clz.equals(conditions.get(i).getClass())) { - return (T) conditions.get(i); - } - } - return null; - } - - public List<Condition> getConditions() { - return mConditions; - } - - public List<Condition> getVisibleConditions() { - List<Condition> conditions = new ArrayList<>(); - final int N = mConditions.size(); - for (int i = 0; i < N; i++) { - if (mConditions.get(i).shouldShow()) { - conditions.add(mConditions.get(i)); - } - } - return conditions; - } - - public void notifyChanged(Condition condition) { - saveToXml(); - Collections.sort(mConditions, CONDITION_COMPARATOR); - final int N = mListeners.size(); - for (int i = 0; i < N; i++) { - mListeners.get(i).onConditionsChanged(); - } - } - - public void addListener(ConditionListener listener) { - mListeners.add(listener); - listener.onConditionsChanged(); - } - - public void remListener(ConditionListener listener) { - mListeners.remove(listener); - } - - @Override - public void onResume() { - for (int i = 0, size = mConditions.size(); i < size; i++) { - mConditions.get(i).onResume(); - } - } - - @Override - public void onPause() { - for (int i = 0, size = mConditions.size(); i < size; i++) { - mConditions.get(i).onPause(); - } - } - - private class ConditionLoader extends AsyncTask<Void, Void, ArrayList<Condition>> { - @Override - protected ArrayList<Condition> doInBackground(Void... params) { - Log.d(TAG, "loading conditions from xml"); - ArrayList<Condition> conditions = new ArrayList<>(); - mXmlFile = new File(mContext.getFilesDir(), FILE_NAME); - if (mXmlFile.exists()) { - readFromXml(mXmlFile, conditions); - } - addMissingConditions(conditions); - return conditions; - } - - @Override - protected void onPostExecute(ArrayList<Condition> conditions) { - Log.d(TAG, "conditions loaded from xml, refreshing conditions"); - mConditions.clear(); - mConditions.addAll(conditions); - refreshAll(); - } - } - - public static ConditionManager get(Context context) { - return get(context, true); - } - - public static ConditionManager get(Context context, boolean loadConditionsNow) { - if (sInstance == null) { - sInstance = new ConditionManager(context.getApplicationContext(), loadConditionsNow); - } - return sInstance; - } - - public interface ConditionListener { - void onConditionsChanged(); - } - - private static final Comparator<Condition> CONDITION_COMPARATOR = new Comparator<Condition>() { - @Override - public int compare(Condition lhs, Condition rhs) { - return Long.compare(lhs.getLastChange(), rhs.getLastChange()); - } - }; -} diff --git a/src/com/android/settings/dashboard/conditional/DndCondition.java b/src/com/android/settings/dashboard/conditional/DndCondition.java deleted file mode 100644 index 6bcc67e60b..0000000000 --- a/src/com/android/settings/dashboard/conditional/DndCondition.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * 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.dashboard.conditional; - -import android.app.NotificationManager; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.graphics.drawable.Drawable; -import android.os.PersistableBundle; -import android.provider.Settings; -import android.provider.Settings.Global; -import android.service.notification.ZenModeConfig; -import androidx.annotation.VisibleForTesting; - -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.settings.R; -import com.android.settings.core.SubSettingLauncher; -import com.android.settings.notification.ZenModeSettings; - -public class DndCondition extends Condition { - - private static final String TAG = "DndCondition"; - private static final String KEY_STATE = "state"; - - private boolean mRegistered; - - @VisibleForTesting - static final IntentFilter DND_FILTER = - new IntentFilter(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED_INTERNAL); - @VisibleForTesting - protected ZenModeConfig mConfig; - - private int mZen; - private final Receiver mReceiver; - - public DndCondition(ConditionManager manager) { - super(manager); - mReceiver = new Receiver(); - mManager.getContext().registerReceiver(mReceiver, DND_FILTER); - mRegistered = true; - } - - @Override - public void refreshState() { - NotificationManager notificationManager = - mManager.getContext().getSystemService(NotificationManager.class); - mZen = notificationManager.getZenMode(); - boolean zenModeEnabled = mZen != Settings.Global.ZEN_MODE_OFF; - if (zenModeEnabled) { - mConfig = notificationManager.getZenModeConfig(); - } else { - mConfig = null; - } - setActive(zenModeEnabled); - } - - @Override - boolean saveState(PersistableBundle bundle) { - bundle.putInt(KEY_STATE, mZen); - return super.saveState(bundle); - } - - @Override - void restoreState(PersistableBundle bundle) { - super.restoreState(bundle); - mZen = bundle.getInt(KEY_STATE, Global.ZEN_MODE_OFF); - } - - @Override - public Drawable getIcon() { - return mManager.getContext().getDrawable(R.drawable.ic_do_not_disturb_on_24dp); - } - - @Override - public CharSequence getTitle() { - return mManager.getContext().getString(R.string.condition_zen_title); - } - - @Override - public CharSequence getSummary() { - return ZenModeConfig.getDescription(mManager.getContext(), mZen != Global.ZEN_MODE_OFF, - mConfig, true); - } - - @Override - public CharSequence[] getActions() { - return new CharSequence[] { mManager.getContext().getString(R.string.condition_turn_off) }; - } - - @Override - public void onPrimaryClick() { - new SubSettingLauncher(mManager.getContext()) - .setDestination(ZenModeSettings.class.getName()) - .setSourceMetricsCategory(MetricsEvent.DASHBOARD_SUMMARY) - .setTitle(R.string.zen_mode_settings_title) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .launch(); - } - - @Override - public void onActionClick(int index) { - if (index == 0) { - NotificationManager notificationManager = mManager.getContext().getSystemService( - NotificationManager.class); - notificationManager.setZenMode(Settings.Global.ZEN_MODE_OFF, null, TAG); - setActive(false); - } else { - throw new IllegalArgumentException("Unexpected index " + index); - } - } - - @Override - public int getMetricsConstant() { - return MetricsEvent.SETTINGS_CONDITION_DND; - } - - public static class Receiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - if (NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED_INTERNAL - .equals(intent.getAction())) { - final Condition condition = - ConditionManager.get(context).getCondition(DndCondition.class); - if (condition != null) { - condition.refreshState(); - } - } - } - } - - @Override - public void onResume() { - if (!mRegistered) { - mManager.getContext().registerReceiver(mReceiver, DND_FILTER); - mRegistered = true; - } - } - - @Override - public void onPause() { - if (mRegistered) { - mManager.getContext().unregisterReceiver(mReceiver); - mRegistered = false; - } - } -} diff --git a/src/com/android/settings/dashboard/conditional/HotspotCondition.java b/src/com/android/settings/dashboard/conditional/HotspotCondition.java deleted file mode 100644 index 53e0c07978..0000000000 --- a/src/com/android/settings/dashboard/conditional/HotspotCondition.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * 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.dashboard.conditional; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.graphics.drawable.Drawable; -import android.net.ConnectivityManager; -import android.net.wifi.WifiConfiguration; -import android.net.wifi.WifiManager; -import android.os.UserHandle; -import android.os.UserManager; - -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.settings.R; -import com.android.settings.TetherSettings; -import com.android.settings.core.SubSettingLauncher; -import com.android.settingslib.RestrictedLockUtils; -import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; - -public class HotspotCondition extends Condition { - - private final WifiManager mWifiManager; - private final Receiver mReceiver; - - private static final IntentFilter WIFI_AP_STATE_FILTER = - new IntentFilter(WifiManager.WIFI_AP_STATE_CHANGED_ACTION); - - public HotspotCondition(ConditionManager manager) { - super(manager); - mWifiManager = mManager.getContext().getSystemService(WifiManager.class); - mReceiver = new Receiver(); - } - - @Override - public void refreshState() { - boolean wifiTetherEnabled = mWifiManager.isWifiApEnabled(); - setActive(wifiTetherEnabled); - } - - @Override - protected BroadcastReceiver getReceiver() { - return mReceiver; - } - - @Override - protected IntentFilter getIntentFilter() { - return WIFI_AP_STATE_FILTER; - } - - @Override - public Drawable getIcon() { - return mManager.getContext().getDrawable(R.drawable.ic_hotspot); - } - - private String getSsid() { - WifiConfiguration wifiConfig = mWifiManager.getWifiApConfiguration(); - if (wifiConfig == null) { - return mManager.getContext().getString( - com.android.internal.R.string.wifi_tether_configure_ssid_default); - } else { - return wifiConfig.SSID; - } - } - - @Override - public CharSequence getTitle() { - return mManager.getContext().getString(R.string.condition_hotspot_title); - } - - @Override - public CharSequence getSummary() { - return mManager.getContext().getString(R.string.condition_hotspot_summary, getSsid()); - } - - @Override - public CharSequence[] getActions() { - final Context context = mManager.getContext(); - if (RestrictedLockUtils.hasBaseUserRestriction(context, - UserManager.DISALLOW_CONFIG_TETHERING, UserHandle.myUserId())) { - return new CharSequence[0]; - } - return new CharSequence[] {context.getString(R.string.condition_turn_off)}; - } - - @Override - public void onPrimaryClick() { - new SubSettingLauncher(mManager.getContext()) - .setDestination(TetherSettings.class.getName()) - .setSourceMetricsCategory(MetricsEvent.DASHBOARD_SUMMARY) - .setTitle(R.string.tether_settings_title_all) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .launch(); - } - - @Override - public void onActionClick(int index) { - if (index == 0) { - final Context context = mManager.getContext(); - final EnforcedAdmin admin = RestrictedLockUtils.checkIfRestrictionEnforced(context, - UserManager.DISALLOW_CONFIG_TETHERING, UserHandle.myUserId()); - if (admin != null) { - RestrictedLockUtils.sendShowAdminSupportDetailsIntent(context, admin); - } else { - ConnectivityManager cm = (ConnectivityManager) context.getSystemService( - Context.CONNECTIVITY_SERVICE); - cm.stopTethering(ConnectivityManager.TETHERING_WIFI); - setActive(false); - } - } else { - throw new IllegalArgumentException("Unexpected index " + index); - } - } - - @Override - public int getMetricsConstant() { - return MetricsEvent.SETTINGS_CONDITION_HOTSPOT; - } - - public static class Receiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - if (WifiManager.WIFI_AP_STATE_CHANGED_ACTION.equals(intent.getAction())) { - ConditionManager.get(context).getCondition(HotspotCondition.class) - .refreshState(); - } - } - } -} diff --git a/src/com/android/settings/dashboard/conditional/NightDisplayCondition.java b/src/com/android/settings/dashboard/conditional/NightDisplayCondition.java deleted file mode 100644 index 78278b5dce..0000000000 --- a/src/com/android/settings/dashboard/conditional/NightDisplayCondition.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settings.dashboard.conditional; - -import android.content.Intent; -import android.graphics.drawable.Drawable; - -import com.android.internal.app.ColorDisplayController; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.settings.R; -import com.android.settings.core.SubSettingLauncher; -import com.android.settings.display.NightDisplaySettings; - -public final class NightDisplayCondition extends Condition - implements ColorDisplayController.Callback { - - private ColorDisplayController mController; - - NightDisplayCondition(ConditionManager manager) { - super(manager); - mController = new ColorDisplayController(manager.getContext()); - mController.setListener(this); - } - - @Override - public int getMetricsConstant() { - return MetricsEvent.SETTINGS_CONDITION_NIGHT_DISPLAY; - } - - @Override - public Drawable getIcon() { - return mManager.getContext().getDrawable(R.drawable.ic_settings_night_display); - } - - @Override - public CharSequence getTitle() { - return mManager.getContext().getString(R.string.condition_night_display_title); - } - - @Override - public CharSequence getSummary() { - return mManager.getContext().getString(R.string.condition_night_display_summary); - } - - @Override - public CharSequence[] getActions() { - return new CharSequence[] {mManager.getContext().getString(R.string.condition_turn_off)}; - } - - @Override - public void onPrimaryClick() { - new SubSettingLauncher(mManager.getContext()) - .setDestination(NightDisplaySettings.class.getName()) - .setSourceMetricsCategory(MetricsEvent.DASHBOARD_SUMMARY) - .setTitle(R.string.night_display_title) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .launch(); - } - - @Override - public void onActionClick(int index) { - if (index == 0) { - mController.setActivated(false); - } else { - throw new IllegalArgumentException("Unexpected index " + index); - } - } - - @Override - public void refreshState() { - setActive(mController.isActivated()); - } - - @Override - public void onActivated(boolean activated) { - refreshState(); - } -} diff --git a/src/com/android/settings/dashboard/conditional/RingerMutedCondition.java b/src/com/android/settings/dashboard/conditional/RingerMutedCondition.java deleted file mode 100644 index 7f7bc2be09..0000000000 --- a/src/com/android/settings/dashboard/conditional/RingerMutedCondition.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (C) 2018 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.dashboard.conditional; - -import static android.content.Context.NOTIFICATION_SERVICE; - -import android.app.NotificationManager; -import android.graphics.drawable.Drawable; -import android.media.AudioManager; -import android.provider.Settings; - -import com.android.internal.logging.nano.MetricsProto; -import com.android.settings.R; - -public class RingerMutedCondition extends AbnormalRingerConditionBase { - - private final NotificationManager mNotificationManager; - - RingerMutedCondition(ConditionManager manager) { - super(manager); - mNotificationManager = - (NotificationManager) mManager.getContext().getSystemService(NOTIFICATION_SERVICE); - } - - @Override - public void refreshState() { - int zen = Settings.Global.ZEN_MODE_OFF; - if (mNotificationManager != null) { - zen = mNotificationManager.getZenMode(); - } - final boolean zenModeEnabled = zen != Settings.Global.ZEN_MODE_OFF; - final boolean isSilent = - mAudioManager.getRingerModeInternal() == AudioManager.RINGER_MODE_SILENT; - setActive(isSilent && !zenModeEnabled); - } - - @Override - public int getMetricsConstant() { - return MetricsProto.MetricsEvent.SETTINGS_CONDITION_DEVICE_MUTED; - } - - @Override - public Drawable getIcon() { - return mManager.getContext().getDrawable(R.drawable.ic_notifications_off_24dp); - } - - @Override - public CharSequence getTitle() { - return mManager.getContext().getText(R.string.condition_device_muted_title); - } - - @Override - public CharSequence getSummary() { - return mManager.getContext().getText(R.string.condition_device_muted_summary); - } -} diff --git a/src/com/android/settings/dashboard/conditional/RingerVibrateCondition.java b/src/com/android/settings/dashboard/conditional/RingerVibrateCondition.java deleted file mode 100644 index 6af05c1fae..0000000000 --- a/src/com/android/settings/dashboard/conditional/RingerVibrateCondition.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2018 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.dashboard.conditional; - -import android.graphics.drawable.Drawable; -import android.media.AudioManager; - -import com.android.internal.logging.nano.MetricsProto; -import com.android.settings.R; - -public class RingerVibrateCondition extends AbnormalRingerConditionBase { - - RingerVibrateCondition(ConditionManager manager) { - super(manager); - } - - @Override - public void refreshState() { - setActive(mAudioManager.getRingerModeInternal() == AudioManager.RINGER_MODE_VIBRATE); - } - - @Override - public int getMetricsConstant() { - return MetricsProto.MetricsEvent.SETTINGS_CONDITION_DEVICE_VIBRATE; - } - - @Override - public Drawable getIcon() { - return mManager.getContext().getDrawable(R.drawable.ic_volume_ringer_vibrate); - } - - @Override - public CharSequence getTitle() { - return mManager.getContext().getText(R.string.condition_device_vibrate_title); - } - - @Override - public CharSequence getSummary() { - return mManager.getContext().getText(R.string.condition_device_vibrate_summary); - } -} diff --git a/src/com/android/settings/dashboard/conditional/WorkModeCondition.java b/src/com/android/settings/dashboard/conditional/WorkModeCondition.java deleted file mode 100644 index 941d5b02b6..0000000000 --- a/src/com/android/settings/dashboard/conditional/WorkModeCondition.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settings.dashboard.conditional; - -import android.content.Context; -import android.content.Intent; -import android.content.pm.UserInfo; -import android.graphics.drawable.Drawable; -import android.os.UserHandle; -import android.os.UserManager; - -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.settings.R; -import com.android.settings.Settings; - -import java.util.List; - -public class WorkModeCondition extends Condition { - - private UserManager mUm; - private UserHandle mUserHandle; - - public WorkModeCondition(ConditionManager conditionManager) { - super(conditionManager); - mUm = (UserManager) mManager.getContext().getSystemService(Context.USER_SERVICE); - } - - private void updateUserHandle() { - List<UserInfo> profiles = mUm.getProfiles(UserHandle.myUserId()); - final int profilesCount = profiles.size(); - mUserHandle = null; - for (int i = 0; i < profilesCount; i++) { - UserInfo userInfo = profiles.get(i); - if (userInfo.isManagedProfile()) { - // We assume there's only one managed profile, otherwise UI needs to change. - mUserHandle = userInfo.getUserHandle(); - break; - } - } - } - - @Override - public void refreshState() { - updateUserHandle(); - setActive(mUserHandle != null && mUm.isQuietModeEnabled(mUserHandle)); - } - - @Override - public Drawable getIcon() { - return mManager.getContext().getDrawable(R.drawable.ic_signal_workmode_enable); - } - - @Override - public CharSequence getTitle() { - return mManager.getContext().getString(R.string.condition_work_title); - } - - @Override - public CharSequence getSummary() { - return mManager.getContext().getString(R.string.condition_work_summary); - } - - @Override - public CharSequence[] getActions() { - return new CharSequence[] { - mManager.getContext().getString(R.string.condition_turn_on) - }; - } - - @Override - public void onPrimaryClick() { - mManager.getContext().startActivity(new Intent(mManager.getContext(), - Settings.AccountDashboardActivity.class) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); - } - - @Override - public void onActionClick(int index) { - if (index == 0) { - if (mUserHandle != null) { - mUm.requestQuietModeEnabled(false, mUserHandle); - } - setActive(false); - } else { - throw new IllegalArgumentException("Unexpected index " + index); - } - } - - @Override - public int getMetricsConstant() { - return MetricsEvent.SETTINGS_CONDITION_WORK_MODE; - } -} diff --git a/src/com/android/settings/dashboard/profileselector/ProfileSelectDialog.java b/src/com/android/settings/dashboard/profileselector/ProfileSelectDialog.java new file mode 100644 index 0000000000..7e38d6ac19 --- /dev/null +++ b/src/com/android/settings/dashboard/profileselector/ProfileSelectDialog.java @@ -0,0 +1,95 @@ +/* + * 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.dashboard.profileselector; + +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.os.Bundle; +import android.os.UserHandle; +import android.os.UserManager; +import android.util.Log; + +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.FragmentManager; + +import com.android.settingslib.drawer.Tile; + +import java.util.List; + +public class ProfileSelectDialog extends DialogFragment implements OnClickListener { + + private static final String TAG = "ProfileSelectDialog"; + private static final String ARG_SELECTED_TILE = "selectedTile"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private Tile mSelectedTile; + + public static void show(FragmentManager manager, Tile tile) { + ProfileSelectDialog dialog = new ProfileSelectDialog(); + Bundle args = new Bundle(); + args.putParcelable(ARG_SELECTED_TILE, tile); + dialog.setArguments(args); + dialog.show(manager, "select_profile"); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mSelectedTile = getArguments().getParcelable(ARG_SELECTED_TILE); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + Context context = getActivity(); + AlertDialog.Builder builder = new AlertDialog.Builder(context); + UserAdapter adapter = UserAdapter.createUserAdapter(UserManager.get(context), context, + mSelectedTile.userHandle); + builder.setTitle(com.android.settingslib.R.string.choose_profile) + .setAdapter(adapter, this); + + return builder.create(); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + UserHandle user = mSelectedTile.userHandle.get(which); + // Show menu on top level items. + final Intent intent = mSelectedTile.getIntent(); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + getActivity().startActivityAsUser(intent, user); + } + + public static void updateUserHandlesIfNeeded(Context context, Tile tile) { + List<UserHandle> userHandles = tile.userHandle; + if (tile.userHandle == null || tile.userHandle.size() <= 1) { + return; + } + final UserManager userManager = UserManager.get(context); + for (int i = userHandles.size() - 1; i >= 0; i--) { + if (userManager.getUserInfo(userHandles.get(i).getIdentifier()) == null) { + if (DEBUG) { + Log.d(TAG, "Delete the user: " + userHandles.get(i).getIdentifier()); + } + userHandles.remove(i); + } + } + } +} diff --git a/src/com/android/settings/dashboard/profileselector/UserAdapter.java b/src/com/android/settings/dashboard/profileselector/UserAdapter.java new file mode 100644 index 0000000000..46c87a1600 --- /dev/null +++ b/src/com/android/settings/dashboard/profileselector/UserAdapter.java @@ -0,0 +1,214 @@ +/* + * 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.dashboard.profileselector; + +import android.app.ActivityManager; +import android.content.Context; +import android.content.pm.UserInfo; +import android.database.DataSetObserver; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.UserHandle; +import android.os.UserManager; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.ListAdapter; +import android.widget.SpinnerAdapter; +import android.widget.TextView; + +import com.android.internal.util.UserIcons; +import com.android.settingslib.R; +import com.android.settingslib.drawable.UserIconDrawable; + +import java.util.ArrayList; +import java.util.List; + +/** + * Adapter for a spinner that shows a list of users. + */ +public class UserAdapter implements SpinnerAdapter, ListAdapter { + /** Holder for user details */ + public static class UserDetails { + private final UserHandle mUserHandle; + private final String mName; + private final Drawable mIcon; + + public UserDetails(UserHandle userHandle, UserManager um, Context context) { + mUserHandle = userHandle; + UserInfo userInfo = um.getUserInfo(mUserHandle.getIdentifier()); + Drawable icon; + if (userInfo.isManagedProfile()) { + mName = context.getString(R.string.managed_user_title); + icon = context.getDrawable( + com.android.internal.R.drawable.ic_corp_badge); + } else { + mName = userInfo.name; + final int userId = userInfo.id; + if (um.getUserIcon(userId) != null) { + icon = new BitmapDrawable(context.getResources(), um.getUserIcon(userId)); + } else { + icon = UserIcons.getDefaultUserIcon( + context.getResources(), userId, /* light= */ false); + } + } + this.mIcon = encircle(context, icon); + } + + private static Drawable encircle(Context context, Drawable icon) { + return new UserIconDrawable(UserIconDrawable.getSizeForList(context)) + .setIconDrawable(icon).bake(); + } + } + + private ArrayList<UserDetails> data; + private final LayoutInflater mInflater; + + public UserAdapter(Context context, ArrayList<UserDetails> users) { + if (users == null) { + throw new IllegalArgumentException("A list of user details must be provided"); + } + this.data = users; + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + public UserHandle getUserHandle(int position) { + if (position < 0 || position >= data.size()) { + return null; + } + return data.get(position).mUserHandle; + } + + @Override + public View getDropDownView(int position, View convertView, ViewGroup parent) { + final View row = convertView != null ? convertView : createUser(parent); + + UserDetails user = data.get(position); + ((ImageView) row.findViewById(android.R.id.icon)).setImageDrawable(user.mIcon); + ((TextView) row.findViewById(android.R.id.title)).setText(getTitle(user)); + return row; + } + + private int getTitle(UserDetails user) { + int userHandle = user.mUserHandle.getIdentifier(); + if (userHandle == UserHandle.USER_CURRENT + || userHandle == ActivityManager.getCurrentUser()) { + return R.string.category_personal; + } else { + return R.string.category_work; + } + } + + private View createUser(ViewGroup parent) { + return mInflater.inflate(R.layout.user_preference, parent, false); + } + + @Override + public void registerDataSetObserver(DataSetObserver observer) { + // We don't support observers + } + + @Override + public void unregisterDataSetObserver(DataSetObserver observer) { + // We don't support observers + } + + @Override + public int getCount() { + return data.size(); + } + + @Override + public UserAdapter.UserDetails getItem(int position) { + return data.get(position); + } + + @Override + public long getItemId(int position) { + return data.get(position).mUserHandle.getIdentifier(); + } + + @Override + public boolean hasStableIds() { + return false; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + return getDropDownView(position, convertView, parent); + } + + @Override + public int getItemViewType(int position) { + return 0; + } + + @Override + public int getViewTypeCount() { + return 1; + } + + @Override + public boolean isEmpty() { + return data.isEmpty(); + } + + @Override + public boolean areAllItemsEnabled() { + return true; + } + + @Override + public boolean isEnabled(int position) { + return true; + } + + /** + * Creates a {@link UserAdapter} if there is more than one + * profile on the device. + * + * <p> The adapter can be used to populate a spinner that switches between the Settings + * app on the different profiles. + * + * @return a {@link UserAdapter} or null if there is only one + * profile. + */ + public static UserAdapter createUserSpinnerAdapter(UserManager userManager, Context context) { + List<UserHandle> userProfiles = userManager.getUserProfiles(); + if (userProfiles.size() < 2) { + return null; + } + + UserHandle myUserHandle = new UserHandle(UserHandle.myUserId()); + // The first option should be the current profile + userProfiles.remove(myUserHandle); + userProfiles.add(0, myUserHandle); + + return createUserAdapter(userManager, context, userProfiles); + } + + public static UserAdapter createUserAdapter( + UserManager userManager, Context context, List<UserHandle> userProfiles) { + ArrayList<UserDetails> userDetails = new ArrayList<>(userProfiles.size()); + final int count = userProfiles.size(); + for (int i = 0; i < count; i++) { + userDetails.add(new UserDetails(userProfiles.get(i), userManager, context)); + } + return new UserAdapter(context, userDetails); + } +} diff --git a/src/com/android/settings/dashboard/suggestions/SuggestionAdapter.java b/src/com/android/settings/dashboard/suggestions/SuggestionAdapter.java deleted file mode 100644 index 4a3bc33464..0000000000 --- a/src/com/android/settings/dashboard/suggestions/SuggestionAdapter.java +++ /dev/null @@ -1,276 +0,0 @@ -/* - * Copyright (C) 2017 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.dashboard.suggestions; - -import android.app.PendingIntent; -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Typeface; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; -import android.os.Bundle; -import android.service.settings.suggestions.Suggestion; -import androidx.annotation.VisibleForTesting; -import androidx.recyclerview.widget.RecyclerView; -import android.text.TextUtils; -import android.util.DisplayMetrics; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.widget.ImageView; -import android.widget.LinearLayout; - -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.settings.R; -import com.android.settings.dashboard.DashboardAdapter.DashboardItemHolder; -import com.android.settings.overlay.FeatureFactory; -import com.android.settingslib.Utils; -import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; -import com.android.settingslib.core.lifecycle.Lifecycle; -import com.android.settingslib.core.lifecycle.LifecycleObserver; -import com.android.settingslib.core.lifecycle.events.OnSaveInstanceState; -import com.android.settingslib.suggestions.SuggestionControllerMixin; -import com.android.settingslib.utils.IconCache; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -public class SuggestionAdapter extends RecyclerView.Adapter<DashboardItemHolder> implements - LifecycleObserver, OnSaveInstanceState { - public static final String TAG = "SuggestionAdapter"; - - private static final String STATE_SUGGESTIONS_SHOWN_LOGGED = "suggestions_shown_logged"; - private static final String STATE_SUGGESTION_LIST = "suggestion_list"; - - private final Context mContext; - private final MetricsFeatureProvider mMetricsFeatureProvider; - private final IconCache mCache; - private final ArrayList<String> mSuggestionsShownLogged; - private final SuggestionFeatureProvider mSuggestionFeatureProvider; - private final SuggestionControllerMixin mSuggestionControllerMixin; - private final Callback mCallback; - private final CardConfig mConfig; - - private List<Suggestion> mSuggestions; - - public interface Callback { - /** - * Called when the close button of the suggestion card is clicked. - */ - void onSuggestionClosed(Suggestion suggestion); - } - - public SuggestionAdapter(Context context, SuggestionControllerMixin suggestionControllerMixin, - Bundle savedInstanceState, Callback callback, Lifecycle lifecycle) { - mContext = context; - mSuggestionControllerMixin = suggestionControllerMixin; - mCache = new IconCache(context); - final FeatureFactory factory = FeatureFactory.getFactory(context); - mMetricsFeatureProvider = factory.getMetricsFeatureProvider(); - mSuggestionFeatureProvider = factory.getSuggestionFeatureProvider(context); - mCallback = callback; - if (savedInstanceState != null) { - mSuggestions = savedInstanceState.getParcelableArrayList(STATE_SUGGESTION_LIST); - mSuggestionsShownLogged = savedInstanceState.getStringArrayList( - STATE_SUGGESTIONS_SHOWN_LOGGED); - } else { - mSuggestionsShownLogged = new ArrayList<>(); - } - - if (lifecycle != null) { - lifecycle.addObserver(this); - } - mConfig = CardConfig.get(context); - - setHasStableIds(true); - } - - @Override - public DashboardItemHolder onCreateViewHolder(ViewGroup parent, int viewType) { - return new DashboardItemHolder(LayoutInflater.from(parent.getContext()).inflate( - viewType, parent, false)); - } - - @Override - public void onBindViewHolder(DashboardItemHolder holder, int position) { - final Suggestion suggestion = mSuggestions.get(position); - final String id = suggestion.getId(); - final int suggestionCount = mSuggestions.size(); - if (!mSuggestionsShownLogged.contains(id)) { - mMetricsFeatureProvider.action( - mContext, MetricsEvent.ACTION_SHOW_SETTINGS_SUGGESTION, id); - mSuggestionsShownLogged.add(id); - } - final Icon icon = suggestion.getIcon(); - final Drawable drawable = mCache.getIcon(icon); - if (drawable != null && (suggestion.getFlags() & Suggestion.FLAG_ICON_TINTABLE) != 0) { - drawable.setTint(Utils.getColorAccent(mContext)); - } - holder.icon.setImageDrawable(drawable); - holder.title.setText(suggestion.getTitle()); - holder.title.setTypeface(Typeface.create( - mContext.getString(com.android.internal.R.string.config_headlineFontFamily), - Typeface.NORMAL)); - - if (suggestionCount == 1) { - final CharSequence summary = suggestion.getSummary(); - if (!TextUtils.isEmpty(summary)) { - holder.summary.setText(summary); - holder.summary.setVisibility(View.VISIBLE); - } else { - holder.summary.setVisibility(View.GONE); - } - } else { - mConfig.setCardLayout(holder, position); - } - - final View closeButton = holder.itemView.findViewById(R.id.close_button); - if (closeButton != null) { - closeButton.setOnClickListener(v -> { - mSuggestionFeatureProvider.dismissSuggestion( - mContext, mSuggestionControllerMixin, suggestion); - if (mCallback != null) { - mCallback.onSuggestionClosed(suggestion); - } - }); - } - - View clickHandler = holder.itemView; - // If a view with @android:id/primary is defined, use that as the click handler - // instead. - final View primaryAction = holder.itemView.findViewById(android.R.id.primary); - if (primaryAction != null) { - clickHandler = primaryAction; - } - clickHandler.setOnClickListener(v -> { - mMetricsFeatureProvider.action(mContext, MetricsEvent.ACTION_SETTINGS_SUGGESTION, id); - try { - suggestion.getPendingIntent().send(); - mSuggestionControllerMixin.launchSuggestion(suggestion); - } catch (PendingIntent.CanceledException e) { - Log.w(TAG, "Failed to start suggestion " + suggestion.getTitle()); - } - }); - } - - @Override - public long getItemId(int position) { - return Objects.hash(mSuggestions.get(position).getId()); - } - - @Override - public int getItemViewType(int position) { - final Suggestion suggestion = getSuggestion(position); - if ((suggestion.getFlags() & Suggestion.FLAG_HAS_BUTTON) != 0) { - return R.layout.suggestion_tile_with_button; - } - if (getItemCount() == 1) { - return R.layout.suggestion_tile; - } - return R.layout.suggestion_tile_two_cards; - } - - @Override - public int getItemCount() { - return mSuggestions.size(); - } - - public Suggestion getSuggestion(int position) { - final long itemId = getItemId(position); - if (mSuggestions == null) { - return null; - } - for (Suggestion suggestion : mSuggestions) { - if (Objects.hash(suggestion.getId()) == itemId) { - return suggestion; - } - } - return null; - } - - public void removeSuggestion(Suggestion suggestion) { - final int position = mSuggestions.indexOf(suggestion); - mSuggestions.remove(suggestion); - notifyItemRemoved(position); - } - - @Override - public void onSaveInstanceState(Bundle outState) { - if (mSuggestions != null) { - outState.putParcelableArrayList(STATE_SUGGESTION_LIST, - new ArrayList<>(mSuggestions)); - } - outState.putStringArrayList(STATE_SUGGESTIONS_SHOWN_LOGGED, mSuggestionsShownLogged); - } - - public void setSuggestions(List<Suggestion> suggestions) { - mSuggestions = suggestions; - } - - public List<Suggestion> getSuggestions() { - return mSuggestions; - } - - @VisibleForTesting - static class CardConfig { - // Card start/end margin - private final int mMarginInner; - private final int mMarginOuter; - private final WindowManager mWindowManager; - - private static CardConfig sConfig; - - private CardConfig(Context context) { - mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); - final Resources res = context.getResources(); - mMarginInner = - res.getDimensionPixelOffset(R.dimen.suggestion_card_inner_margin); - mMarginOuter = - res.getDimensionPixelOffset(R.dimen.suggestion_card_outer_margin); - } - - public static CardConfig get(Context context) { - if (sConfig == null) { - sConfig = new CardConfig(context); - } - return sConfig; - } - - @VisibleForTesting - void setCardLayout(DashboardItemHolder holder, int position) { - final LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( - getWidthForTwoCrads(), LinearLayout.LayoutParams.WRAP_CONTENT); - params.setMarginStart(position == 0 ? mMarginOuter : mMarginInner); - params.setMarginEnd(position != 0 ? mMarginOuter : 0); - holder.itemView.setLayoutParams(params); - } - - private int getWidthForTwoCrads() { - return (getScreenWidth() - mMarginInner - mMarginOuter * 2) / 2; - } - - @VisibleForTesting - int getScreenWidth() { - final DisplayMetrics metrics = new DisplayMetrics(); - mWindowManager.getDefaultDisplay().getMetrics(metrics); - return metrics.widthPixels; - } - } - -} diff --git a/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProvider.java b/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProvider.java index ef3513c4d4..b755ba7861 100644 --- a/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProvider.java +++ b/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProvider.java @@ -19,14 +19,8 @@ package com.android.settings.dashboard.suggestions; import android.content.ComponentName; import android.content.Context; import android.content.SharedPreferences; -import android.service.settings.suggestions.Suggestion; -import androidx.annotation.NonNull; -import android.util.Pair; - -import com.android.settingslib.drawer.Tile; -import com.android.settingslib.suggestions.SuggestionControllerMixin; -import java.util.List; +import androidx.annotation.NonNull; /** Interface should be implemented if you have added new suggestions */ public interface SuggestionFeatureProvider { @@ -41,11 +35,6 @@ public interface SuggestionFeatureProvider { */ ComponentName getSuggestionServiceComponent(); - /** - * Returns true if smart suggestion should be used instead of xml based SuggestionParser. - */ - boolean isSmartSuggestionEnabled(Context context); - /** Return true if the suggestion has already been completed and does not need to be shown */ boolean isSuggestionComplete(Context context, @NonNull ComponentName suggestion); @@ -53,20 +42,4 @@ public interface SuggestionFeatureProvider { * Returns the {@link SharedPreferences} that holds metadata for suggestions. */ SharedPreferences getSharedPrefs(Context context); - - /** - * Only keep top few suggestions from exclusive suggestions. - */ - void filterExclusiveSuggestions(List<Tile> suggestions); - - /** - * Dismisses a suggestion. - */ - void dismissSuggestion(Context context, SuggestionControllerMixin suggestionMixin, - Suggestion suggestion); - - /** - * Returns common tagged data for suggestion logging. - */ - Pair<Integer, Object>[] getLoggingTaggedData(Context context); } diff --git a/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProviderImpl.java b/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProviderImpl.java index 6ff7ef3795..35905546a0 100644 --- a/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProviderImpl.java +++ b/src/com/android/settings/dashboard/suggestions/SuggestionFeatureProviderImpl.java @@ -20,34 +20,25 @@ import android.app.ActivityManager; import android.content.ComponentName; import android.content.Context; import android.content.SharedPreferences; -import android.service.settings.suggestions.Suggestion; + import androidx.annotation.NonNull; -import android.util.Log; -import android.util.Pair; -import com.android.internal.logging.nano.MetricsProto; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.Settings.NightDisplaySuggestionActivity; +import com.android.settings.biometrics.fingerprint.FingerprintEnrollSuggestionActivity; +import com.android.settings.biometrics.fingerprint.FingerprintSuggestionActivity; import com.android.settings.display.NightDisplayPreferenceController; -import com.android.settings.fingerprint.FingerprintEnrollSuggestionActivity; -import com.android.settings.fingerprint.FingerprintSuggestionActivity; import com.android.settings.notification.ZenOnboardingActivity; import com.android.settings.notification.ZenSuggestionActivity; import com.android.settings.overlay.FeatureFactory; import com.android.settings.password.ScreenLockSuggestionActivity; -import com.android.settings.support.NewDeviceIntroSuggestionActivity; +import com.android.settings.wallpaper.StyleSuggestionActivity; import com.android.settings.wallpaper.WallpaperSuggestionActivity; import com.android.settings.wifi.calling.WifiCallingSuggestionActivity; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; -import com.android.settingslib.drawer.Tile; -import com.android.settingslib.suggestions.SuggestionControllerMixin; - -import java.util.List; public class SuggestionFeatureProviderImpl implements SuggestionFeatureProvider { private static final String TAG = "SuggestionFeature"; - private static final int EXCLUSIVE_SUGGESTION_MAX_COUNT = 3; private static final String SHARED_PREF_FILENAME = "suggestions"; @@ -68,15 +59,12 @@ public class SuggestionFeatureProviderImpl implements SuggestionFeatureProvider } @Override - public boolean isSmartSuggestionEnabled(Context context) { - return false; - } - - @Override public boolean isSuggestionComplete(Context context, @NonNull ComponentName component) { final String className = component.getClassName(); if (className.equals(WallpaperSuggestionActivity.class.getName())) { return WallpaperSuggestionActivity.isSuggestionComplete(context); + } else if (className.equals(StyleSuggestionActivity.class.getName())) { + return StyleSuggestionActivity.isSuggestionComplete(context); } else if (className.equals(FingerprintSuggestionActivity.class.getName())) { return FingerprintSuggestionActivity.isSuggestionComplete(context); } else if (className.equals(FingerprintEnrollSuggestionActivity.class.getName())) { @@ -87,8 +75,6 @@ public class SuggestionFeatureProviderImpl implements SuggestionFeatureProvider return WifiCallingSuggestionActivity.isSuggestionComplete(context); } else if (className.equals(NightDisplaySuggestionActivity.class.getName())) { return NightDisplayPreferenceController.isSuggestionComplete(context); - } else if (className.equals(NewDeviceIntroSuggestionActivity.class.getName())) { - return NewDeviceIntroSuggestionActivity.isSuggestionComplete(context); } else if (className.equals(ZenSuggestionActivity.class.getName())) { return ZenOnboardingActivity.isSuggestionComplete(context); } @@ -105,35 +91,4 @@ public class SuggestionFeatureProviderImpl implements SuggestionFeatureProvider mMetricsFeatureProvider = FeatureFactory.getFactory(appContext) .getMetricsFeatureProvider(); } - - @Override - public void filterExclusiveSuggestions(List<Tile> suggestions) { - if (suggestions == null) { - return; - } - for (int i = suggestions.size() - 1; i >= EXCLUSIVE_SUGGESTION_MAX_COUNT; i--) { - Log.d(TAG, "Removing exclusive suggestion"); - suggestions.remove(i); - } - } - - @Override - public void dismissSuggestion(Context context, SuggestionControllerMixin mixin, - Suggestion suggestion) { - if (mixin == null || suggestion == null || context == null) { - return; - } - mMetricsFeatureProvider.action( - context, MetricsProto.MetricsEvent.ACTION_SETTINGS_DISMISS_SUGGESTION, - suggestion.getId()); - mixin.dismissSuggestion(suggestion); - } - - @Override - public Pair<Integer, Object>[] getLoggingTaggedData(Context context) { - final boolean isSmartSuggestionEnabled = isSmartSuggestionEnabled(context); - return new Pair[] {Pair.create( - MetricsEvent.FIELD_SETTINGS_SMART_SUGGESTIONS_ENABLED, - isSmartSuggestionEnabled ? 1 : 0)}; - } } diff --git a/src/com/android/settings/dashboard/suggestions/SuggestionStateProvider.java b/src/com/android/settings/dashboard/suggestions/SuggestionStateProvider.java index 1474f83e55..8547db0ff0 100644 --- a/src/com/android/settings/dashboard/suggestions/SuggestionStateProvider.java +++ b/src/com/android/settings/dashboard/suggestions/SuggestionStateProvider.java @@ -25,9 +25,10 @@ import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; -import androidx.annotation.VisibleForTesting; import android.util.Log; +import androidx.annotation.VisibleForTesting; + import com.android.settings.overlay.FeatureFactory; public class SuggestionStateProvider extends ContentProvider { diff --git a/src/com/android/settings/datausage/AppDataUsage.java b/src/com/android/settings/datausage/AppDataUsage.java index 79adfdab35..a86459e6e0 100644 --- a/src/com/android/settings/datausage/AppDataUsage.java +++ b/src/com/android/settings/datausage/AppDataUsage.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2018 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 @@ -17,51 +17,53 @@ package com.android.settings.datausage; import static android.net.NetworkPolicyManager.POLICY_REJECT_METERED_BACKGROUND; import android.app.Activity; -import android.app.LoaderManager; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; -import android.content.Loader; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; -import android.net.INetworkStatsSession; -import android.net.NetworkPolicy; -import android.net.NetworkStatsHistory; import android.net.NetworkTemplate; -import android.net.TrafficStats; import android.os.Bundle; -import android.os.RemoteException; import android.os.UserHandle; -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; -import androidx.preference.PreferenceCategory; +import android.telephony.SubscriptionManager; import android.util.ArraySet; import android.util.IconDrawableFactory; import android.util.Log; import android.view.View; import android.widget.AdapterView; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.annotation.VisibleForTesting; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.Loader; +import androidx.preference.Preference; +import androidx.preference.Preference.OnPreferenceChangeListener; +import androidx.preference.PreferenceCategory; + import com.android.settings.R; import com.android.settings.applications.AppInfoBase; import com.android.settings.widget.EntityHeaderController; import com.android.settingslib.AppItem; -import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; +import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.RestrictedSwitchPreference; -import com.android.settingslib.net.ChartData; -import com.android.settingslib.net.ChartDataLoader; +import com.android.settingslib.net.NetworkCycleDataForUid; +import com.android.settingslib.net.NetworkCycleDataForUidLoader; import com.android.settingslib.net.UidDetail; import com.android.settingslib.net.UidDetailProvider; -import com.android.settingslib.wrapper.PackageManagerWrapper; -public class AppDataUsage extends DataUsageBase implements Preference.OnPreferenceChangeListener, +import java.util.ArrayList; +import java.util.List; + +public class AppDataUsage extends DataUsageBaseFragment implements OnPreferenceChangeListener, DataSaverBackend.Listener { private static final String TAG = "AppDataUsage"; - public static final String ARG_APP_ITEM = "app_item"; - public static final String ARG_NETWORK_TEMPLATE = "network_template"; + static final String ARG_APP_ITEM = "app_item"; + static final String ARG_NETWORK_TEMPLATE = "network_template"; + static final String ARG_NETWORK_CYCLES = "network_cycles"; + static final String ARG_SELECTED_CYCLE = "selected_cycle"; private static final String KEY_TOTAL_USAGE = "total_usage"; private static final String KEY_FOREGROUND_USAGE = "foreground_usage"; @@ -72,10 +74,10 @@ public class AppDataUsage extends DataUsageBase implements Preference.OnPreferen private static final String KEY_CYCLE = "cycle"; private static final String KEY_UNRESTRICTED_DATA = "unrestricted_data_saver"; - private static final int LOADER_CHART_DATA = 2; + private static final int LOADER_APP_USAGE_DATA = 2; private static final int LOADER_APP_PREF = 3; - private PackageManagerWrapper mPackageManagerWrapper; + private PackageManager mPackageManager; private final ArraySet<String> mPackages = new ArraySet<>(); private Preference mTotalUsage; private Preference mForegroundUsage; @@ -85,41 +87,41 @@ public class AppDataUsage extends DataUsageBase implements Preference.OnPreferen private PreferenceCategory mAppList; private Drawable mIcon; - private CharSequence mLabel; - private String mPackageName; - private INetworkStatsSession mStatsSession; + @VisibleForTesting + CharSequence mLabel; + @VisibleForTesting + String mPackageName; private CycleAdapter mCycleAdapter; - private long mStart; - private long mEnd; - private ChartData mChartData; - private NetworkTemplate mTemplate; - private NetworkPolicy mPolicy; + private List<NetworkCycleDataForUid> mUsageData; + @VisibleForTesting + NetworkTemplate mTemplate; private AppItem mAppItem; private Intent mAppSettingsIntent; private SpinnerPreference mCycle; private RestrictedSwitchPreference mUnrestrictedData; private DataSaverBackend mDataSaverBackend; + private Context mContext; + private ArrayList<Long> mCycles; + private long mSelectedCycle; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); - mPackageManagerWrapper = new PackageManagerWrapper(getPackageManager()); + mContext = getContext(); + mPackageManager = getPackageManager(); final Bundle args = getArguments(); - try { - mStatsSession = services.mStatsService.openSession(); - } catch (RemoteException e) { - throw new RuntimeException(e); - } - mAppItem = (args != null) ? (AppItem) args.getParcelable(ARG_APP_ITEM) : null; mTemplate = (args != null) ? (NetworkTemplate) args.getParcelable(ARG_NETWORK_TEMPLATE) : null; + mCycles = (args != null) ? (ArrayList) args.getSerializable(ARG_NETWORK_CYCLES) + : null; + mSelectedCycle = (args != null) ? args.getLong(ARG_SELECTED_CYCLE) : 0L; + if (mTemplate == null) { - Context context = getContext(); - mTemplate = DataUsageUtils.getDefaultTemplate(context, - DataUsageUtils.getDefaultSubscriptionId(context)); + mTemplate = DataUsageUtils.getDefaultTemplate(mContext, + SubscriptionManager.getDefaultDataSubscriptionId()); } if (mAppItem == null) { int uid = (args != null) ? args.getInt(AppInfoBase.ARG_PACKAGE_UID, -1) @@ -137,44 +139,46 @@ public class AppDataUsage extends DataUsageBase implements Preference.OnPreferen addUid(mAppItem.uids.keyAt(i)); } } - addPreferencesFromResource(R.xml.app_data_usage); mTotalUsage = findPreference(KEY_TOTAL_USAGE); mForegroundUsage = findPreference(KEY_FOREGROUND_USAGE); mBackgroundUsage = findPreference(KEY_BACKGROUND_USAGE); - mCycle = (SpinnerPreference) findPreference(KEY_CYCLE); - mCycleAdapter = new CycleAdapter(getContext(), mCycle, mCycleListener, false); + mCycle = findPreference(KEY_CYCLE); + mCycleAdapter = new CycleAdapter(mContext, mCycle, mCycleListener); + + final UidDetailProvider uidDetailProvider = getUidDetailProvider(); if (mAppItem.key > 0) { - if (mPackages.size() != 0) { - try { - ApplicationInfo info = mPackageManagerWrapper.getApplicationInfoAsUser( - mPackages.valueAt(0), 0, UserHandle.getUserId(mAppItem.key)); - mIcon = IconDrawableFactory.newInstance(getActivity()).getBadgedIcon(info); - mLabel = info.loadLabel(mPackageManagerWrapper.getPackageManager()); - mPackageName = info.packageName; - } catch (PackageManager.NameNotFoundException e) { - } - } if (!UserHandle.isApp(mAppItem.key)) { + final UidDetail uidDetail = uidDetailProvider.getUidDetail(mAppItem.key, true); + mIcon = uidDetail.icon; + mLabel = uidDetail.label; removePreference(KEY_UNRESTRICTED_DATA); removePreference(KEY_RESTRICT_BACKGROUND); } else { - mRestrictBackground = (RestrictedSwitchPreference) findPreference( - KEY_RESTRICT_BACKGROUND); + if (mPackages.size() != 0) { + try { + final ApplicationInfo info = mPackageManager.getApplicationInfoAsUser( + mPackages.valueAt(0), 0, UserHandle.getUserId(mAppItem.key)); + mIcon = IconDrawableFactory.newInstance(getActivity()).getBadgedIcon(info); + mLabel = info.loadLabel(mPackageManager); + mPackageName = info.packageName; + } catch (PackageManager.NameNotFoundException e) { + } + } + mRestrictBackground = findPreference(KEY_RESTRICT_BACKGROUND); mRestrictBackground.setOnPreferenceChangeListener(this); - mUnrestrictedData = (RestrictedSwitchPreference) findPreference( - KEY_UNRESTRICTED_DATA); + mUnrestrictedData = findPreference(KEY_UNRESTRICTED_DATA); mUnrestrictedData.setOnPreferenceChangeListener(this); } - mDataSaverBackend = new DataSaverBackend(getContext()); + mDataSaverBackend = new DataSaverBackend(mContext); mAppSettings = findPreference(KEY_APP_SETTINGS); mAppSettingsIntent = new Intent(Intent.ACTION_MANAGE_NETWORK_USAGE); mAppSettingsIntent.addCategory(Intent.CATEGORY_DEFAULT); - PackageManager pm = getPackageManager(); + final PackageManager pm = getPackageManager(); boolean matchFound = false; for (String packageName : mPackages) { mAppSettingsIntent.setPackage(packageName); @@ -189,14 +193,15 @@ public class AppDataUsage extends DataUsageBase implements Preference.OnPreferen } if (mPackages.size() > 1) { - mAppList = (PreferenceCategory) findPreference(KEY_APP_LIST); - getLoaderManager().initLoader(LOADER_APP_PREF, Bundle.EMPTY, mAppPrefCallbacks); + mAppList = findPreference(KEY_APP_LIST); + LoaderManager.getInstance(this).restartLoader(LOADER_APP_PREF, Bundle.EMPTY, + mAppPrefCallbacks); } else { removePreference(KEY_APP_LIST); } } else { final Context context = getActivity(); - UidDetail uidDetail = new UidDetailProvider(context).getUidDetail(mAppItem.key, true); + final UidDetail uidDetail = uidDetailProvider.getUidDetail(mAppItem.key, true); mIcon = uidDetail.icon; mLabel = uidDetail.label; mPackageName = context.getPackageName(); @@ -209,20 +214,13 @@ public class AppDataUsage extends DataUsageBase implements Preference.OnPreferen } @Override - public void onDestroy() { - TrafficStats.closeQuietly(mStatsSession); - super.onDestroy(); - } - - @Override public void onResume() { super.onResume(); if (mDataSaverBackend != null) { mDataSaverBackend.addListener(this); } - mPolicy = services.mPolicyEditor.getPolicy(mTemplate); - getLoaderManager().restartLoader(LOADER_CHART_DATA, - ChartDataLoader.buildArgs(mTemplate, mAppItem), mChartDataCallbacks); + LoaderManager.getInstance(this).restartLoader(LOADER_APP_USAGE_DATA, null /* args */, + mUidDataCallbacks); updatePrefs(); } @@ -258,14 +256,29 @@ public class AppDataUsage extends DataUsageBase implements Preference.OnPreferen return super.onPreferenceTreeClick(preference); } + @Override + protected int getPreferenceScreenResId() { + return R.xml.app_data_usage; + } + + @Override + protected String getLogTag() { + return TAG; + } + @VisibleForTesting void updatePrefs() { updatePrefs(getAppRestrictBackground(), getUnrestrictData()); } + @VisibleForTesting + UidDetailProvider getUidDetailProvider() { + return new UidDetailProvider(mContext); + } + private void updatePrefs(boolean restrictBackground, boolean unrestrictData) { - final EnforcedAdmin admin = RestrictedLockUtils.checkIfMeteredDataRestricted( - getContext(), mPackageName, UserHandle.getUserId(mAppItem.key)); + final EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfMeteredDataRestricted( + mContext, mPackageName, UserHandle.getUserId(mAppItem.key)); if (mRestrictBackground != null) { mRestrictBackground.setChecked(!restrictBackground); mRestrictBackground.setDisabledByAdmin(admin); @@ -282,7 +295,7 @@ public class AppDataUsage extends DataUsageBase implements Preference.OnPreferen } private void addUid(int uid) { - String[] packages = getPackageManager().getPackagesForUid(uid); + String[] packages = mPackageManager.getPackagesForUid(uid); if (packages != null) { for (int i = 0; i < packages.length; i++) { mPackages.add(packages[i]); @@ -290,26 +303,23 @@ public class AppDataUsage extends DataUsageBase implements Preference.OnPreferen } } - private void bindData() { + @VisibleForTesting + void bindData(int position) { final long backgroundBytes, foregroundBytes; - if (mChartData == null || mStart == 0) { + if (mUsageData == null || position >= mUsageData.size()) { backgroundBytes = foregroundBytes = 0; mCycle.setVisible(false); } else { mCycle.setVisible(true); - final long now = System.currentTimeMillis(); - NetworkStatsHistory.Entry entry = null; - entry = mChartData.detailDefault.getValues(mStart, mEnd, now, entry); - backgroundBytes = entry.rxBytes + entry.txBytes; - entry = mChartData.detailForeground.getValues(mStart, mEnd, now, entry); - foregroundBytes = entry.rxBytes + entry.txBytes; + final NetworkCycleDataForUid data = mUsageData.get(position); + backgroundBytes = data.getBackgroudUsage(); + foregroundBytes = data.getForegroudUsage(); } final long totalBytes = backgroundBytes + foregroundBytes; - final Context context = getContext(); - mTotalUsage.setSummary(DataUsageUtils.formatDataUsage(context, totalBytes)); - mForegroundUsage.setSummary(DataUsageUtils.formatDataUsage(context, foregroundBytes)); - mBackgroundUsage.setSummary(DataUsageUtils.formatDataUsage(context, backgroundBytes)); + mTotalUsage.setSummary(DataUsageUtils.formatDataUsage(mContext, totalBytes)); + mForegroundUsage.setSummary(DataUsageUtils.formatDataUsage(mContext, foregroundBytes)); + mBackgroundUsage.setSummary(DataUsageUtils.formatDataUsage(mContext, backgroundBytes)); } private boolean getAppRestrictBackground() { @@ -333,7 +343,7 @@ public class AppDataUsage extends DataUsageBase implements Preference.OnPreferen int uid = 0; if (pkg != null) { try { - uid = mPackageManagerWrapper.getPackageUidAsUser(pkg, + uid = mPackageManager.getPackageUidAsUser(pkg, UserHandle.getUserId(mAppItem.key)); } catch (PackageManager.NameNotFoundException e) { Log.w(TAG, "Skipping UID because cannot find package " + pkg); @@ -345,7 +355,7 @@ public class AppDataUsage extends DataUsageBase implements Preference.OnPreferen final Activity activity = getActivity(); final Preference pref = EntityHeaderController .newInstance(activity, this, null /* header */) - .setRecyclerView(getListView(), getLifecycle()) + .setRecyclerView(getListView(), getSettingsLifecycle()) .setUid(uid) .setHasAppInfoLink(showInfoButton) .setButtonActions(EntityHeaderController.ActionType.ACTION_NONE, @@ -359,18 +369,14 @@ public class AppDataUsage extends DataUsageBase implements Preference.OnPreferen @Override public int getMetricsCategory() { - return MetricsEvent.APP_DATA_USAGE; + return SettingsEnums.APP_DATA_USAGE; } private AdapterView.OnItemSelectedListener mCycleListener = new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { - final CycleAdapter.CycleItem cycle = (CycleAdapter.CycleItem) mCycle.getSelectedItem(); - - mStart = cycle.start; - mEnd = cycle.end; - bindData(); + bindData(position); } @Override @@ -379,24 +385,56 @@ public class AppDataUsage extends DataUsageBase implements Preference.OnPreferen } }; - private final LoaderManager.LoaderCallbacks<ChartData> mChartDataCallbacks = - new LoaderManager.LoaderCallbacks<ChartData>() { - @Override - public Loader<ChartData> onCreateLoader(int id, Bundle args) { - return new ChartDataLoader(getActivity(), mStatsSession, args); - } + @VisibleForTesting + final LoaderManager.LoaderCallbacks<List<NetworkCycleDataForUid>> mUidDataCallbacks = + new LoaderManager.LoaderCallbacks<List<NetworkCycleDataForUid>>() { + @Override + public Loader<List<NetworkCycleDataForUid>> onCreateLoader(int id, Bundle args) { + final NetworkCycleDataForUidLoader.Builder builder + = NetworkCycleDataForUidLoader.builder(mContext); + builder.setRetrieveDetail(true) + .setNetworkTemplate(mTemplate); + if (mAppItem.category == AppItem.CATEGORY_USER) { + for (int i = 0; i < mAppItem.uids.size(); i++) { + builder.addUid(mAppItem.uids.keyAt(i)); + } + } else { + builder.addUid(mAppItem.key); + } + if (mCycles != null) { + builder.setCycles(mCycles); + } + return builder.build(); + } - @Override - public void onLoadFinished(Loader<ChartData> loader, ChartData data) { - mChartData = data; - mCycleAdapter.updateCycleList(mPolicy, mChartData); - bindData(); - } + @Override + public void onLoadFinished(Loader<List<NetworkCycleDataForUid>> loader, + List<NetworkCycleDataForUid> data) { + mUsageData = data; + mCycleAdapter.updateCycleList(data); + if (mSelectedCycle > 0L) { + final int numCycles = data.size(); + int position = 0; + for (int i = 0; i < numCycles; i++) { + final NetworkCycleDataForUid cycleData = data.get(i); + if (cycleData.getEndTime() == mSelectedCycle) { + position = i; + break; + } + } + if (position > 0) { + mCycle.setSelection(position); + } + bindData(position); + } else { + bindData(0 /* position */); + } + } - @Override - public void onLoaderReset(Loader<ChartData> loader) { - } - }; + @Override + public void onLoaderReset(Loader<List<NetworkCycleDataForUid>> loader) { + } + }; private final LoaderManager.LoaderCallbacks<ArraySet<Preference>> mAppPrefCallbacks = new LoaderManager.LoaderCallbacks<ArraySet<Preference>>() { diff --git a/src/com/android/settings/datausage/AppDataUsageActivity.java b/src/com/android/settings/datausage/AppDataUsageActivity.java index 2b8e42dcda..82a3a4526a 100644 --- a/src/com/android/settings/datausage/AppDataUsageActivity.java +++ b/src/com/android/settings/datausage/AppDataUsageActivity.java @@ -62,7 +62,7 @@ public class AppDataUsageActivity extends SettingsActivity { args.putParcelable(AppDataUsage.ARG_APP_ITEM, appItem); intent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, args); intent.putExtra(EXTRA_SHOW_FRAGMENT, AppDataUsage.class.getName()); - intent.putExtra(EXTRA_SHOW_FRAGMENT_TITLE_RESID, R.string.app_data_usage); + intent.putExtra(EXTRA_SHOW_FRAGMENT_TITLE_RESID, R.string.data_usage_app_summary_title); super.onCreate(savedInstanceState); } diff --git a/src/com/android/settings/datausage/AppDataUsagePreference.java b/src/com/android/settings/datausage/AppDataUsagePreference.java index 7595eb56a4..f5a204a224 100644 --- a/src/com/android/settings/datausage/AppDataUsagePreference.java +++ b/src/com/android/settings/datausage/AppDataUsagePreference.java @@ -15,15 +15,16 @@ package com.android.settings.datausage; import android.content.Context; -import androidx.preference.PreferenceViewHolder; import android.view.View; import android.widget.ProgressBar; -import com.android.settings.widget.AppPreference; +import androidx.preference.PreferenceViewHolder; + import com.android.settingslib.AppItem; import com.android.settingslib.net.UidDetail; import com.android.settingslib.net.UidDetailProvider; import com.android.settingslib.utils.ThreadUtils; +import com.android.settingslib.widget.apppreference.AppPreference; public class AppDataUsagePreference extends AppPreference { diff --git a/src/com/android/settings/datausage/AppPrefLoader.java b/src/com/android/settings/datausage/AppPrefLoader.java index 690ac2dbfc..1e0a55434f 100644 --- a/src/com/android/settings/datausage/AppPrefLoader.java +++ b/src/com/android/settings/datausage/AppPrefLoader.java @@ -19,11 +19,13 @@ package com.android.settings.datausage; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; -import androidx.preference.Preference; import android.util.ArraySet; -import com.android.settingslib.utils.AsyncLoader; -public class AppPrefLoader extends AsyncLoader<ArraySet<Preference>> { +import androidx.preference.Preference; + +import com.android.settingslib.utils.AsyncLoaderCompat; + +public class AppPrefLoader extends AsyncLoaderCompat<ArraySet<Preference>> { private ArraySet<String> mPackages; private PackageManager mPackageManager; private Context mPrefContext; diff --git a/src/com/android/settings/datausage/BillingCyclePreference.java b/src/com/android/settings/datausage/BillingCyclePreference.java index 9ed6ea7df0..23c577c3d8 100644 --- a/src/com/android/settings/datausage/BillingCyclePreference.java +++ b/src/com/android/settings/datausage/BillingCyclePreference.java @@ -14,21 +14,17 @@ package com.android.settings.datausage; -import static android.net.NetworkPolicy.CYCLE_NONE; - +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; import android.net.NetworkTemplate; import android.os.Bundle; import android.os.RemoteException; -import androidx.preference.Preference; import android.util.AttributeSet; -import android.util.FeatureFlagUtils; -import com.android.internal.logging.nano.MetricsProto; -import com.android.settings.R; +import androidx.preference.Preference; -import com.android.settings.core.FeatureFlags; +import com.android.settings.R; import com.android.settings.core.SubSettingLauncher; import com.android.settings.datausage.CellDataPreference.DataStateListener; @@ -60,14 +56,8 @@ public class BillingCyclePreference extends Preference implements TemplatePrefer mTemplate = template; mSubId = subId; mServices = services; - final int cycleDay = services.mPolicyEditor.getPolicyCycleDay(mTemplate); - if (FeatureFlagUtils.isEnabled(getContext(), FeatureFlags.DATA_USAGE_SETTINGS_V2)) { - setSummary(null); - } else if (cycleDay != CYCLE_NONE) { - setSummary(getContext().getString(R.string.billing_cycle_fragment_summary, cycleDay)); - } else { - setSummary(null); - } + setSummary(null); + setIntent(getIntent()); } @@ -88,8 +78,8 @@ public class BillingCyclePreference extends Preference implements TemplatePrefer return new SubSettingLauncher(getContext()) .setDestination(BillingCycleSettings.class.getName()) .setArguments(args) - .setTitle(R.string.billing_cycle) - .setSourceMetricsCategory(MetricsProto.MetricsEvent.VIEW_UNKNOWN) + .setTitleRes(R.string.billing_cycle) + .setSourceMetricsCategory(SettingsEnums.PAGE_UNKNOWN) .toIntent(); } diff --git a/src/com/android/settings/datausage/BillingCyclePreferenceController.java b/src/com/android/settings/datausage/BillingCyclePreferenceController.java new file mode 100644 index 0000000000..88bf63c72f --- /dev/null +++ b/src/com/android/settings/datausage/BillingCyclePreferenceController.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2019 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.datausage; + +import android.content.Context; +import android.net.INetworkStatsService; +import android.net.NetworkPolicyManager; +import android.net.NetworkTemplate; +import android.os.INetworkManagementService; +import android.os.ServiceManager; +import android.os.UserManager; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; + +import com.android.settings.core.BasePreferenceController; +import com.android.settingslib.NetworkPolicyEditor; + +import androidx.preference.PreferenceScreen; + +public class BillingCyclePreferenceController extends BasePreferenceController { + private int mSubscriptionId; + + public BillingCyclePreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + public void init(int subscriptionId) { + mSubscriptionId = subscriptionId; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + BillingCyclePreference preference = screen.findPreference(getPreferenceKey()); + + TemplatePreference.NetworkServices services = new TemplatePreference.NetworkServices(); + services.mNetworkService = INetworkManagementService.Stub.asInterface( + ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE)); + services.mStatsService = INetworkStatsService.Stub.asInterface( + ServiceManager.getService(Context.NETWORK_STATS_SERVICE)); + services.mPolicyManager = mContext.getSystemService(NetworkPolicyManager.class); + services.mPolicyEditor = new NetworkPolicyEditor(services.mPolicyManager); + services.mTelephonyManager = mContext.getSystemService(TelephonyManager.class); + services.mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class); + services.mUserManager = mContext.getSystemService(UserManager.class); + + NetworkTemplate template = DataUsageUtils.getMobileTemplate(mContext, mSubscriptionId); + + preference.setTemplate(template, mSubscriptionId, services); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } +} diff --git a/src/com/android/settings/datausage/BillingCycleSettings.java b/src/com/android/settings/datausage/BillingCycleSettings.java index 5cdf22a897..87b8198a00 100644 --- a/src/com/android/settings/datausage/BillingCycleSettings.java +++ b/src/com/android/settings/datausage/BillingCycleSettings.java @@ -14,23 +14,19 @@ package com.android.settings.datausage; -import static android.net.NetworkPolicy.CYCLE_NONE; import static android.net.NetworkPolicy.LIMIT_DISABLED; import static android.net.NetworkPolicy.WARNING_DISABLED; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.Fragment; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.DialogInterface; import android.content.res.Resources; import android.net.NetworkPolicy; import android.net.NetworkTemplate; import android.os.Bundle; -import androidx.preference.SwitchPreference; -import androidx.preference.Preference; +import android.provider.SearchIndexableResource; import android.text.format.Time; -import android.util.FeatureFlagUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -38,15 +34,25 @@ import android.widget.EditText; import android.widget.NumberPicker; import android.widget.Spinner; -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; +import androidx.preference.Preference; +import androidx.preference.SwitchPreference; + import com.android.settings.R; -import com.android.settings.core.FeatureFlags; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; import com.android.settingslib.NetworkPolicyEditor; import com.android.settingslib.net.DataUsageController; +import com.android.settingslib.search.SearchIndexable; -public class BillingCycleSettings extends DataUsageBase implements +import java.util.ArrayList; +import java.util.List; + +@SearchIndexable +public class BillingCycleSettings extends DataUsageBaseFragment implements Preference.OnPreferenceChangeListener, DataUsageEditController { private static final String TAG = "BillingCycleSettings"; @@ -63,10 +69,12 @@ public class BillingCycleSettings extends DataUsageBase implements private static final String KEY_BILLING_CYCLE = "billing_cycle"; private static final String KEY_SET_DATA_WARNING = "set_data_warning"; private static final String KEY_DATA_WARNING = "data_warning"; - @VisibleForTesting static final String KEY_SET_DATA_LIMIT = "set_data_limit"; + @VisibleForTesting + static final String KEY_SET_DATA_LIMIT = "set_data_limit"; private static final String KEY_DATA_LIMIT = "data_limit"; - private NetworkTemplate mNetworkTemplate; + @VisibleForTesting + NetworkTemplate mNetworkTemplate; private Preference mBillingCycle; private Preference mDataWarning; private SwitchPreference mEnableDataWarning; @@ -76,11 +84,11 @@ public class BillingCycleSettings extends DataUsageBase implements @VisibleForTesting void setUpForTest(NetworkPolicyEditor policyEditor, - Preference billingCycle, - Preference dataLimit, - Preference dataWarning, - SwitchPreference enableLimit, - SwitchPreference enableWarning) { + Preference billingCycle, + Preference dataLimit, + Preference dataWarning, + SwitchPreference enableLimit, + SwitchPreference enableWarning) { services.mPolicyEditor = policyEditor; mBillingCycle = billingCycle; mDataLimit = dataLimit; @@ -93,12 +101,16 @@ public class BillingCycleSettings extends DataUsageBase implements public void onCreate(Bundle icicle) { super.onCreate(icicle); - mDataUsageController = new DataUsageController(getContext()); + final Context context = getContext(); + mDataUsageController = new DataUsageController(context); Bundle args = getArguments(); mNetworkTemplate = args.getParcelable(DataUsageList.EXTRA_NETWORK_TEMPLATE); + if (mNetworkTemplate == null) { + mNetworkTemplate = DataUsageUtils.getDefaultTemplate(context, + DataUsageUtils.getDefaultSubscriptionId(context)); + } - addPreferencesFromResource(R.xml.billing_cycle); mBillingCycle = findPreference(KEY_BILLING_CYCLE); mEnableDataWarning = (SwitchPreference) findPreference(KEY_SET_DATA_WARNING); mEnableDataWarning.setOnPreferenceChangeListener(this); @@ -118,14 +130,7 @@ public class BillingCycleSettings extends DataUsageBase implements @VisibleForTesting void updatePrefs() { - final int cycleDay = services.mPolicyEditor.getPolicyCycleDay(mNetworkTemplate); - if (FeatureFlagUtils.isEnabled(getContext(), FeatureFlags.DATA_USAGE_SETTINGS_V2)) { - mBillingCycle.setSummary(null); - } else if (cycleDay != CYCLE_NONE) { - mBillingCycle.setSummary(getString(R.string.billing_cycle_fragment_summary, cycleDay)); - } else { - mBillingCycle.setSummary(null); - } + mBillingCycle.setSummary(null); final long warningBytes = services.mPolicyEditor.getPolicyWarningBytes(mNetworkTemplate); if (warningBytes != WARNING_DISABLED) { mDataWarning.setSummary(DataUsageUtils.formatDataUsage(getContext(), warningBytes)); @@ -188,7 +193,17 @@ public class BillingCycleSettings extends DataUsageBase implements @Override public int getMetricsCategory() { - return MetricsEvent.BILLING_CYCLE; + return SettingsEnums.BILLING_CYCLE; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.billing_cycle; + } + + @Override + protected String getLogTag() { + return TAG; } @VisibleForTesting @@ -325,7 +340,7 @@ public class BillingCycleSettings extends DataUsageBase implements @Override public int getMetricsCategory() { - return MetricsEvent.DIALOG_BILLING_BYTE_LIMIT; + return SettingsEnums.DIALOG_BILLING_BYTE_LIMIT; } } @@ -351,7 +366,7 @@ public class BillingCycleSettings extends DataUsageBase implements @Override public int getMetricsCategory() { - return MetricsEvent.DIALOG_BILLING_CYCLE; + return SettingsEnums.DIALOG_BILLING_CYCLE; } @Override @@ -402,7 +417,8 @@ public class BillingCycleSettings extends DataUsageBase implements */ public static class ConfirmLimitFragment extends InstrumentedDialogFragment implements DialogInterface.OnClickListener { - @VisibleForTesting static final String EXTRA_LIMIT_BYTES = "limitBytes"; + @VisibleForTesting + static final String EXTRA_LIMIT_BYTES = "limitBytes"; public static final float FLOAT = 1.2f; public static void show(BillingCycleSettings parent) { @@ -430,7 +446,7 @@ public class BillingCycleSettings extends DataUsageBase implements @Override public int getMetricsCategory() { - return MetricsEvent.DIALOG_BILLING_CONFIRM_LIMIT; + return SettingsEnums.DIALOG_BILLING_CONFIRM_LIMIT; } @Override @@ -457,4 +473,24 @@ public class BillingCycleSettings extends DataUsageBase implements .putBoolean(KEY_SET_DATA_LIMIT, true).apply(); } } + + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, + boolean enabled) { + final ArrayList<SearchIndexableResource> result = new ArrayList<>(); + + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.billing_cycle; + result.add(sir); + return result; + } + + @Override + protected boolean isPageSearchEnabled(Context context) { + return DataUsageUtils.hasMobileData(context); + } + }; + } diff --git a/src/com/android/settings/datausage/CellDataPreference.java b/src/com/android/settings/datausage/CellDataPreference.java index fb97cd8932..af7e16e221 100644 --- a/src/com/android/settings/datausage/CellDataPreference.java +++ b/src/com/android/settings/datausage/CellDataPreference.java @@ -14,7 +14,7 @@ package com.android.settings.datausage; -import android.app.AlertDialog; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.DialogInterface; import android.database.ContentObserver; @@ -25,9 +25,6 @@ import android.os.Looper; import android.os.Parcel; import android.os.Parcelable; import android.provider.Settings.Global; -import androidx.annotation.VisibleForTesting; -import androidx.core.content.res.TypedArrayUtils; -import androidx.preference.PreferenceViewHolder; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; @@ -36,15 +33,19 @@ import android.util.Log; import android.view.View; import android.widget.Checkable; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog.Builder; +import androidx.core.content.res.TypedArrayUtils; +import androidx.preference.PreferenceViewHolder; + import com.android.settings.R; import com.android.settings.Utils; import com.android.settings.overlay.FeatureFactory; -import com.android.settingslib.CustomDialogPreference; +import com.android.settingslib.CustomDialogPreferenceCompat; import java.util.List; -public class CellDataPreference extends CustomDialogPreference implements TemplatePreference { +public class CellDataPreference extends CustomDialogPreferenceCompat implements TemplatePreference { private static final String TAG = "CellDataPreference"; @@ -135,7 +136,7 @@ public class CellDataPreference extends CustomDialogPreference implements Templa protected void performClick(View view) { final Context context = getContext(); FeatureFactory.getFactory(context).getMetricsFeatureProvider() - .action(context, MetricsEvent.ACTION_CELL_DATA_TOGGLE, !mChecked); + .action(context, SettingsEnums.ACTION_CELL_DATA_TOGGLE, !mChecked); final SubscriptionInfo currentSir = mSubscriptionManager.getActiveSubscriptionInfo( mSubId); final SubscriptionInfo nextSir = mSubscriptionManager.getDefaultDataSubscriptionInfo(); @@ -195,7 +196,7 @@ public class CellDataPreference extends CustomDialogPreference implements Templa } @Override - protected void onPrepareDialogBuilder(AlertDialog.Builder builder, + protected void onPrepareDialogBuilder(Builder builder, DialogInterface.OnClickListener listener) { if (mMultiSimDialog) { showMultiSimDialog(builder, listener); @@ -204,7 +205,7 @@ public class CellDataPreference extends CustomDialogPreference implements Templa } } - private void showDisableDialog(AlertDialog.Builder builder, + private void showDisableDialog(Builder builder, DialogInterface.OnClickListener listener) { builder.setTitle(null) .setMessage(R.string.data_usage_disable_mobile) @@ -212,7 +213,7 @@ public class CellDataPreference extends CustomDialogPreference implements Templa .setNegativeButton(android.R.string.cancel, null); } - private void showMultiSimDialog(AlertDialog.Builder builder, + private void showMultiSimDialog(Builder builder, DialogInterface.OnClickListener listener) { final SubscriptionInfo currentSir = mSubscriptionManager.getActiveSubscriptionInfo(mSubId); final SubscriptionInfo nextSir = mSubscriptionManager.getDefaultDataSubscriptionInfo(); @@ -231,7 +232,8 @@ public class CellDataPreference extends CustomDialogPreference implements Templa } private void disableDataForOtherSubscriptions(int subId) { - List<SubscriptionInfo> subInfoList = mSubscriptionManager.getActiveSubscriptionInfoList(); + List<SubscriptionInfo> subInfoList = mSubscriptionManager + .getActiveSubscriptionInfoList(true); if (subInfoList != null) { for (SubscriptionInfo subInfo : subInfoList) { if (subInfo.getSubscriptionId() != subId) { diff --git a/src/com/android/settings/datausage/ChartDataUsagePreference.java b/src/com/android/settings/datausage/ChartDataUsagePreference.java index 2a58840ed8..17f23c4ec8 100644 --- a/src/com/android/settings/datausage/ChartDataUsagePreference.java +++ b/src/com/android/settings/datausage/ChartDataUsagePreference.java @@ -16,11 +16,7 @@ package com.android.settings.datausage; import android.content.Context; import android.net.NetworkPolicy; -import android.net.NetworkStatsHistory; import android.net.TrafficStats; -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.text.format.Formatter; @@ -28,9 +24,17 @@ import android.text.style.ForegroundColorSpan; import android.util.AttributeSet; import android.util.SparseIntArray; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + import com.android.settings.R; import com.android.settings.Utils; -import com.android.settings.graph.UsageView; +import com.android.settings.widget.UsageView; +import com.android.settingslib.net.NetworkCycleChartData; +import com.android.settingslib.net.NetworkCycleData; + +import java.util.List; public class ChartDataUsagePreference extends Preference { @@ -44,28 +48,30 @@ public class ChartDataUsagePreference extends Preference { private NetworkPolicy mPolicy; private long mStart; private long mEnd; - private NetworkStatsHistory mNetwork; + private NetworkCycleChartData mNetworkCycleChartData; private int mSecondaryColor; private int mSeriesColor; public ChartDataUsagePreference(Context context, AttributeSet attrs) { super(context, attrs); setSelectable(false); - mLimitColor = Utils.getColorAttr(context, android.R.attr.colorError); - mWarningColor = Utils.getColorAttr(context, android.R.attr.textColorSecondary); + mLimitColor = Utils.getColorAttrDefaultColor(context, android.R.attr.colorError); + mWarningColor = Utils.getColorAttrDefaultColor(context, android.R.attr.textColorSecondary); setLayoutResource(R.layout.data_usage_graph); } @Override public void onBindViewHolder(PreferenceViewHolder holder) { super.onBindViewHolder(holder); - UsageView chart = (UsageView) holder.findViewById(R.id.data_usage); - if (mNetwork == null) return; + final UsageView chart = (UsageView) holder.findViewById(R.id.data_usage); + if (mNetworkCycleChartData == null) { + return; + } - int top = getTop(); + final int top = getTop(); chart.clearPaths(); chart.configureGraph(toInt(mEnd - mStart), top); - calcPoints(chart); + calcPoints(chart, mNetworkCycleChartData.getUsageBuckets()); chart.setBottomLabels(new CharSequence[] { Utils.formatDateRange(getContext(), mStart, mStart), Utils.formatDateRange(getContext(), mEnd, mEnd), @@ -75,43 +81,33 @@ public class ChartDataUsagePreference extends Preference { } public int getTop() { - NetworkStatsHistory.Entry entry = null; - long totalData = 0; - final int start = mNetwork.getIndexBefore(mStart); - final int end = mNetwork.getIndexAfter(mEnd); - - for (int i = start; i <= end; i++) { - entry = mNetwork.getValues(i, entry); - - // increment by current bucket total - totalData += entry.rxBytes + entry.txBytes; - } - long policyMax = mPolicy != null ? Math.max(mPolicy.limitBytes, mPolicy.warningBytes) : 0; + final long totalData = mNetworkCycleChartData.getTotalUsage(); + final long policyMax = + mPolicy != null ? Math.max(mPolicy.limitBytes, mPolicy.warningBytes) : 0; return (int) (Math.max(totalData, policyMax) / RESOLUTION); } @VisibleForTesting - void calcPoints(UsageView chart) { - SparseIntArray points = new SparseIntArray(); - NetworkStatsHistory.Entry entry = null; - - long totalData = 0; - - final int start = mNetwork.getIndexAfter(mStart); - final int end = mNetwork.getIndexAfter(mEnd); - if (start < 0) return; - + void calcPoints(UsageView chart, List<NetworkCycleData> usageSummary) { + if (usageSummary == null) { + return; + } + final SparseIntArray points = new SparseIntArray(); points.put(0, 0); - for (int i = start; i <= end; i++) { - entry = mNetwork.getValues(i, entry); - final long startTime = entry.bucketStart; - final long endTime = startTime + entry.bucketDuration; + final long now = System.currentTimeMillis(); + long totalData = 0; + for (NetworkCycleData data : usageSummary) { + final long startTime = data.getStartTime(); + if (startTime > now) { + break; + } + final long endTime = data.getEndTime(); // increment by current bucket total - totalData += entry.rxBytes + entry.txBytes; + totalData += data.getTotalUsage(); - if (i == 0) { + if (points.size() == 1) { points.put(toInt(startTime - mStart) - 1, -1); } points.put(toInt(startTime - mStart + 1), (int) (totalData / RESOLUTION)); @@ -167,12 +163,6 @@ public class ChartDataUsagePreference extends Preference { notifyChanged(); } - public void setVisibleRange(long start, long end) { - mStart = start; - mEnd = end; - notifyChanged(); - } - public long getInspectStart() { return mStart; } @@ -181,8 +171,10 @@ public class ChartDataUsagePreference extends Preference { return mEnd; } - public void setNetworkStats(NetworkStatsHistory network) { - mNetwork = network; + public void setNetworkCycleData(NetworkCycleChartData data) { + mNetworkCycleChartData = data; + mStart = data.getStartTime(); + mEnd = data.getEndTime(); notifyChanged(); } diff --git a/src/com/android/settings/datausage/CycleAdapter.java b/src/com/android/settings/datausage/CycleAdapter.java index e5c4e43034..74d27be7e6 100644 --- a/src/com/android/settings/datausage/CycleAdapter.java +++ b/src/com/android/settings/datausage/CycleAdapter.java @@ -20,26 +20,25 @@ import android.net.NetworkStatsHistory; import android.text.format.DateUtils; import android.util.Pair; import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import com.android.settings.R; import com.android.settings.Utils; import com.android.settingslib.net.ChartData; +import com.android.settingslib.net.NetworkCycleData; +import com.android.settingslib.widget.settingsspinner.SettingsSpinnerAdapter; import java.time.ZonedDateTime; import java.util.Iterator; +import java.util.List; import java.util.Objects; -public class CycleAdapter extends ArrayAdapter<CycleAdapter.CycleItem> { +public class CycleAdapter extends SettingsSpinnerAdapter<CycleAdapter.CycleItem> { private final SpinnerInterface mSpinner; private final AdapterView.OnItemSelectedListener mListener; public CycleAdapter(Context context, SpinnerInterface spinner, - AdapterView.OnItemSelectedListener listener, boolean isHeader) { - super(context, isHeader ? R.layout.filter_spinner_item - : R.layout.data_usage_cycle_item); - setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + AdapterView.OnItemSelectedListener listener) { + super(context); mSpinner = spinner; mListener = listener; mSpinner.setAdapter(this); @@ -68,7 +67,8 @@ public class CycleAdapter extends ArrayAdapter<CycleAdapter.CycleItem> { * {@link NetworkStatsHistory} data. Always selects the newest item, * updating the inspection range on chartData. */ - public boolean updateCycleList(NetworkPolicy policy, ChartData chartData) { + @Deprecated + public boolean updateCycleList(NetworkPolicy policy, ChartData chartData) { // stash away currently selected cycle to try restoring below final CycleAdapter.CycleItem previousItem = (CycleAdapter.CycleItem) mSpinner.getSelectedItem(); @@ -150,6 +150,37 @@ public class CycleAdapter extends ArrayAdapter<CycleAdapter.CycleItem> { } /** + * Rebuild list based on network data. Always selects the newest item, + * updating the inspection range on chartData. + */ + public boolean updateCycleList(List<? extends NetworkCycleData> cycleData) { + // stash away currently selected cycle to try restoring below + final CycleAdapter.CycleItem previousItem = (CycleAdapter.CycleItem) + mSpinner.getSelectedItem(); + clear(); + + final Context context = getContext(); + for (NetworkCycleData data : cycleData) { + add(new CycleAdapter.CycleItem(context, data.getStartTime(), data.getEndTime())); + } + + // force pick the current cycle (first item) + if (getCount() > 0) { + final int position = findNearestPosition(previousItem); + mSpinner.setSelection(position); + + // only force-update cycle when changed; skipping preserves any + // user-defined inspection region. + final CycleAdapter.CycleItem selectedItem = getItem(position); + if (!Objects.equals(selectedItem, previousItem)) { + mListener.onItemSelected(null, null, position, 0); + return false; + } + } + return true; + } + + /** * List item that reflects a specific data usage cycle. */ public static class CycleItem implements Comparable<CycleItem> { @@ -189,8 +220,11 @@ public class CycleAdapter extends ArrayAdapter<CycleAdapter.CycleItem> { public interface SpinnerInterface { void setAdapter(CycleAdapter cycleAdapter); + void setOnItemSelectedListener(AdapterView.OnItemSelectedListener listener); + Object getSelectedItem(); + void setSelection(int position); } } diff --git a/src/com/android/settings/datausage/DataSaverBackend.java b/src/com/android/settings/datausage/DataSaverBackend.java index b59da9d7ff..de28b07fcb 100644 --- a/src/com/android/settings/datausage/DataSaverBackend.java +++ b/src/com/android/settings/datausage/DataSaverBackend.java @@ -18,13 +18,13 @@ import static android.net.NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND; import static android.net.NetworkPolicyManager.POLICY_NONE; import static android.net.NetworkPolicyManager.POLICY_REJECT_METERED_BACKGROUND; +import android.app.settings.SettingsEnums; import android.content.Context; import android.net.INetworkPolicyListener; import android.net.NetworkPolicyManager; import android.os.RemoteException; import android.util.SparseIntArray; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.overlay.FeatureFactory; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; import com.android.settingslib.utils.ThreadUtils; @@ -73,7 +73,7 @@ public class DataSaverBackend { public void setDataSaverEnabled(boolean enabled) { mPolicyManager.setRestrictBackground(enabled); mMetricsFeatureProvider.action( - mContext, MetricsEvent.ACTION_DATA_SAVER_MODE, enabled ? 1 : 0); + mContext, SettingsEnums.ACTION_DATA_SAVER_MODE, enabled ? 1 : 0); } public void refreshWhitelist() { @@ -86,7 +86,7 @@ public class DataSaverBackend { mUidPolicies.put(uid, policy); if (whitelisted) { mMetricsFeatureProvider.action( - mContext, MetricsEvent.ACTION_DATA_SAVER_WHITELIST, packageName); + mContext, SettingsEnums.ACTION_DATA_SAVER_WHITELIST, packageName); } } @@ -95,19 +95,10 @@ public class DataSaverBackend { return mUidPolicies.get(uid, POLICY_NONE) == POLICY_ALLOW_METERED_BACKGROUND; } - public int getWhitelistedCount() { - int count = 0; - loadWhitelist(); - for (int i = 0; i < mUidPolicies.size(); i++) { - if (mUidPolicies.valueAt(i) == POLICY_ALLOW_METERED_BACKGROUND) { - count++; - } - } - return count; - } - private void loadWhitelist() { - if (mWhitelistInitialized) return; + if (mWhitelistInitialized) { + return; + } for (int uid : mPolicyManager.getUidsWithPolicy(POLICY_ALLOW_METERED_BACKGROUND)) { mUidPolicies.put(uid, POLICY_ALLOW_METERED_BACKGROUND); @@ -125,7 +116,7 @@ public class DataSaverBackend { mUidPolicies.put(uid, policy); if (blacklisted) { mMetricsFeatureProvider.action( - mContext, MetricsEvent.ACTION_DATA_SAVER_BLACKLIST, packageName); + mContext, SettingsEnums.ACTION_DATA_SAVER_BLACKLIST, packageName); } } @@ -135,7 +126,9 @@ public class DataSaverBackend { } private void loadBlacklist() { - if (mBlacklistInitialized) return; + if (mBlacklistInitialized) { + return; + } for (int uid : mPolicyManager.getUidsWithPolicy(POLICY_REJECT_METERED_BACKGROUND)) { mUidPolicies.put(uid, POLICY_REJECT_METERED_BACKGROUND); } @@ -212,7 +205,9 @@ public class DataSaverBackend { public interface Listener { void onDataSaverChanged(boolean isDataSaving); + void onWhitelistStatusChanged(int uid, boolean isWhitelisted); + void onBlacklistStatusChanged(int uid, boolean isBlacklisted); } } diff --git a/src/com/android/settings/datausage/DataSaverPreference.java b/src/com/android/settings/datausage/DataSaverPreference.java index 3e29896a38..79e4c130a0 100644 --- a/src/com/android/settings/datausage/DataSaverPreference.java +++ b/src/com/android/settings/datausage/DataSaverPreference.java @@ -15,8 +15,10 @@ package com.android.settings.datausage; import android.content.Context; -import androidx.preference.Preference; import android.util.AttributeSet; + +import androidx.preference.Preference; + import com.android.settings.R; public class DataSaverPreference extends Preference implements DataSaverBackend.Listener { diff --git a/src/com/android/settings/datausage/DataSaverSummary.java b/src/com/android/settings/datausage/DataSaverSummary.java index 47887b3d49..1ab8c79ad9 100644 --- a/src/com/android/settings/datausage/DataSaverSummary.java +++ b/src/com/android/settings/datausage/DataSaverSummary.java @@ -15,25 +15,34 @@ package com.android.settings.datausage; import android.app.Application; +import android.app.settings.SettingsEnums; +import android.content.Context; import android.os.Bundle; -import androidx.preference.Preference; +import android.provider.SearchIndexableResource; +import android.telephony.SubscriptionManager; import android.widget.Switch; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.preference.Preference; + import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.SettingsPreferenceFragment; import com.android.settings.applications.AppStateBaseBridge.Callback; import com.android.settings.datausage.DataSaverBackend.Listener; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; import com.android.settings.widget.SwitchBar; import com.android.settings.widget.SwitchBar.OnSwitchChangeListener; import com.android.settingslib.applications.ApplicationsState; import com.android.settingslib.applications.ApplicationsState.AppEntry; import com.android.settingslib.applications.ApplicationsState.Callbacks; import com.android.settingslib.applications.ApplicationsState.Session; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; +import java.util.List; +@SearchIndexable public class DataSaverSummary extends SettingsPreferenceFragment implements OnSwitchChangeListener, Listener, Callback, Callbacks { @@ -61,7 +70,7 @@ public class DataSaverSummary extends SettingsPreferenceFragment (Application) getContext().getApplicationContext()); mDataSaverBackend = new DataSaverBackend(getContext()); mDataUsageBridge = new AppStateDataUsageBridge(mApplicationsState, this, mDataSaverBackend); - mSession = mApplicationsState.newSession(this, getLifecycle()); + mSession = mApplicationsState.newSession(this, getSettingsLifecycle()); } @Override @@ -93,7 +102,7 @@ public class DataSaverSummary extends SettingsPreferenceFragment @Override public void onSwitchChanged(Switch switchView, boolean isChecked) { - synchronized(this) { + synchronized (this) { if (mSwitching) { return; } @@ -104,7 +113,7 @@ public class DataSaverSummary extends SettingsPreferenceFragment @Override public int getMetricsCategory() { - return MetricsEvent.DATA_SAVER_SUMMARY; + return SettingsEnums.DATA_SAVER_SUMMARY; } @Override @@ -114,7 +123,7 @@ public class DataSaverSummary extends SettingsPreferenceFragment @Override public void onDataSaverChanged(boolean isDataSaving) { - synchronized(this) { + synchronized (this) { mSwitchBar.setChecked(isDataSaving); mSwitching = false; } @@ -189,4 +198,25 @@ public class DataSaverSummary extends SettingsPreferenceFragment public void onLoadEntriesCompleted() { } + + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, + boolean enabled) { + final ArrayList<SearchIndexableResource> result = new ArrayList<>(); + + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.data_saver; + result.add(sir); + return result; + } + + @Override + protected boolean isPageSearchEnabled(Context context) { + return DataUsageUtils.hasMobileData(context) + && DataUsageUtils.getDefaultSubscriptionId(context) + != SubscriptionManager.INVALID_SUBSCRIPTION_ID; + } + }; } diff --git a/src/com/android/settings/datausage/DataUsageBase.java b/src/com/android/settings/datausage/DataUsageBase.java deleted file mode 100644 index b889a2f581..0000000000 --- a/src/com/android/settings/datausage/DataUsageBase.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file - * except in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the - * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package com.android.settings.datausage; - -import static android.net.ConnectivityManager.TYPE_ETHERNET; - -import android.content.Context; -import android.net.ConnectivityManager; -import android.net.INetworkStatsService; -import android.net.INetworkStatsSession; -import android.net.NetworkPolicy; -import android.net.NetworkPolicyManager; -import android.net.NetworkTemplate; -import android.net.TrafficStats; -import android.os.Bundle; -import android.os.INetworkManagementService; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.os.SystemProperties; -import android.os.UserManager; -import android.telephony.SubscriptionManager; -import android.telephony.TelephonyManager; -import android.util.Log; -import com.android.settings.SettingsPreferenceFragment; -import com.android.settingslib.NetworkPolicyEditor; - -/** - * @deprecated please use {@link DataUsageBaseFragment} instead. - */ -@Deprecated -public abstract class DataUsageBase extends SettingsPreferenceFragment { - private static final String TAG = "DataUsageBase"; - private static final String ETHERNET = "ethernet"; - - protected final TemplatePreference.NetworkServices services = - new TemplatePreference.NetworkServices(); - - @Override - public void onCreate(Bundle icicle) { - super.onCreate(icicle); - final Context context = getActivity(); - - services.mNetworkService = INetworkManagementService.Stub.asInterface( - ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE)); - services.mStatsService = INetworkStatsService.Stub.asInterface( - ServiceManager.getService(Context.NETWORK_STATS_SERVICE)); - services.mPolicyManager = NetworkPolicyManager.from(context); - - services.mPolicyEditor = new NetworkPolicyEditor(services.mPolicyManager); - - services.mTelephonyManager = TelephonyManager.from(context); - services.mSubscriptionManager = SubscriptionManager.from(context); - services.mUserManager = UserManager.get(context); - } - - @Override - public void onResume() { - super.onResume(); - services.mPolicyEditor.read(); - } - - protected boolean isAdmin() { - return services.mUserManager.isAdminUser(); - } - - protected boolean isMobileDataAvailable(int subId) { - return services.mSubscriptionManager.getActiveSubscriptionInfo(subId) != null; - } - - protected boolean isNetworkPolicyModifiable(NetworkPolicy policy, int subId) { - return policy != null && isBandwidthControlEnabled() && services.mUserManager.isAdminUser() - && isDataEnabled(subId); - } - - private boolean isDataEnabled(int subId) { - if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { - return true; - } - return services.mTelephonyManager.getDataEnabled(subId); - } - - protected boolean isBandwidthControlEnabled() { - try { - return services.mNetworkService.isBandwidthControlEnabled(); - } catch (RemoteException e) { - Log.w(TAG, "problem talking with INetworkManagementService: ", e); - return false; - } - } - - /** - * Test if device has an ethernet network connection. - */ - public boolean hasEthernet(Context context) { - if (DataUsageUtils.TEST_RADIOS) { - return SystemProperties.get(DataUsageUtils.TEST_RADIOS_PROP).contains(ETHERNET); - } - - final ConnectivityManager conn = ConnectivityManager.from(context); - final boolean hasEthernet = conn.isNetworkSupported(TYPE_ETHERNET); - - final long ethernetBytes; - try { - INetworkStatsSession statsSession = services.mStatsService.openSession(); - if (statsSession != null) { - ethernetBytes = statsSession.getSummaryForNetwork( - NetworkTemplate.buildTemplateEthernet(), Long.MIN_VALUE, Long.MAX_VALUE) - .getTotalBytes(); - TrafficStats.closeQuietly(statsSession); - } else { - ethernetBytes = 0; - } - } catch (RemoteException e) { - throw new RuntimeException(e); - } - - // only show ethernet when both hardware present and traffic has occurred - return hasEthernet && ethernetBytes > 0; - } -} diff --git a/src/com/android/settings/datausage/DataUsageBaseFragment.java b/src/com/android/settings/datausage/DataUsageBaseFragment.java index e9c73ff8cc..f6e88cc102 100644 --- a/src/com/android/settings/datausage/DataUsageBaseFragment.java +++ b/src/com/android/settings/datausage/DataUsageBaseFragment.java @@ -14,27 +14,19 @@ package com.android.settings.datausage; -import static android.net.ConnectivityManager.TYPE_ETHERNET; - import android.content.Context; -import android.net.ConnectivityManager; import android.net.INetworkStatsService; -import android.net.INetworkStatsSession; import android.net.NetworkPolicy; import android.net.NetworkPolicyManager; -import android.net.NetworkTemplate; -import android.net.TrafficStats; import android.os.Bundle; import android.os.INetworkManagementService; import android.os.RemoteException; import android.os.ServiceManager; -import android.os.SystemProperties; import android.os.UserManager; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; import android.util.Log; -import com.android.settings.SettingsPreferenceFragment; import com.android.settings.dashboard.DashboardFragment; import com.android.settingslib.NetworkPolicyEditor; @@ -54,7 +46,7 @@ public abstract class DataUsageBaseFragment extends DashboardFragment { ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE)); services.mStatsService = INetworkStatsService.Stub.asInterface( ServiceManager.getService(Context.NETWORK_STATS_SERVICE)); - services.mPolicyManager = (NetworkPolicyManager)context + services.mPolicyManager = (NetworkPolicyManager) context .getSystemService(Context.NETWORK_POLICY_SERVICE); services.mPolicyEditor = new NetworkPolicyEditor(services.mPolicyManager); @@ -98,35 +90,4 @@ public abstract class DataUsageBaseFragment extends DashboardFragment { return false; } } - - /** - * Test if device has an ethernet network connection. - * TODO(b/77590489): Remove this method when DataUsageSummaryLegacy is deprecated. - */ - public boolean hasEthernet(Context context) { - if (DataUsageUtils.TEST_RADIOS) { - return SystemProperties.get(DataUsageUtils.TEST_RADIOS_PROP).contains(ETHERNET); - } - - final ConnectivityManager conn = ConnectivityManager.from(context); - final boolean hasEthernet = conn.isNetworkSupported(TYPE_ETHERNET); - - final long ethernetBytes; - try { - INetworkStatsSession statsSession = services.mStatsService.openSession(); - if (statsSession != null) { - ethernetBytes = statsSession.getSummaryForNetwork( - NetworkTemplate.buildTemplateEthernet(), Long.MIN_VALUE, Long.MAX_VALUE) - .getTotalBytes(); - TrafficStats.closeQuietly(statsSession); - } else { - ethernetBytes = 0; - } - } catch (RemoteException e) { - throw new RuntimeException(e); - } - - // only show ethernet when both hardware present and traffic has occurred - return hasEthernet && ethernetBytes > 0; - } } diff --git a/src/com/android/settings/datausage/DataUsageEditController.java b/src/com/android/settings/datausage/DataUsageEditController.java index adc0fe7dd0..edc30b1d4c 100644 --- a/src/com/android/settings/datausage/DataUsageEditController.java +++ b/src/com/android/settings/datausage/DataUsageEditController.java @@ -15,6 +15,7 @@ package com.android.settings.datausage; import android.net.NetworkTemplate; + import com.android.settingslib.NetworkPolicyEditor; /** diff --git a/src/com/android/settings/datausage/DataUsageInfoController.java b/src/com/android/settings/datausage/DataUsageInfoController.java index 115e45e50a..a471c39a73 100644 --- a/src/com/android/settings/datausage/DataUsageInfoController.java +++ b/src/com/android/settings/datausage/DataUsageInfoController.java @@ -15,6 +15,7 @@ package com.android.settings.datausage; import android.net.NetworkPolicy; + import com.android.settingslib.net.DataUsageController.DataUsageInfo; /** diff --git a/src/com/android/settings/datausage/DataUsageList.java b/src/com/android/settings/datausage/DataUsageList.java index 2ab75ee026..f477ba3019 100644 --- a/src/com/android/settings/datausage/DataUsageList.java +++ b/src/com/android/settings/datausage/DataUsageList.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2018 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 @@ -14,56 +14,54 @@ package com.android.settings.datausage; -import static android.net.ConnectivityManager.TYPE_MOBILE; import static android.net.NetworkPolicyManager.POLICY_REJECT_METERED_BACKGROUND; +import static android.net.NetworkStatsHistory.FIELD_RX_BYTES; +import static android.net.NetworkStatsHistory.FIELD_TX_BYTES; import static android.net.TrafficStats.UID_REMOVED; import static android.net.TrafficStats.UID_TETHERING; -import static android.telephony.TelephonyManager.SIM_STATE_READY; +import android.app.Activity; import android.app.ActivityManager; -import android.app.LoaderManager.LoaderCallbacks; +import android.app.settings.SettingsEnums; +import android.app.usage.NetworkStats; +import android.app.usage.NetworkStats.Bucket; import android.content.Context; import android.content.Intent; -import android.content.Loader; import android.content.pm.UserInfo; import android.graphics.Color; import android.net.ConnectivityManager; -import android.net.INetworkStatsSession; import android.net.NetworkPolicy; -import android.net.NetworkStats; -import android.net.NetworkStatsHistory; import android.net.NetworkTemplate; -import android.net.TrafficStats; -import android.os.AsyncTask; import android.os.Bundle; -import android.os.RemoteException; -import android.os.SystemProperties; +import android.os.Process; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; -import androidx.preference.PreferenceGroup; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; -import android.text.format.DateUtils; import android.util.Log; import android.util.SparseArray; import android.view.View; import android.widget.AdapterView; import android.widget.AdapterView.OnItemSelectedListener; +import android.widget.ImageView; import android.widget.Spinner; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.annotation.VisibleForTesting; +import androidx.loader.app.LoaderManager.LoaderCallbacks; +import androidx.loader.content.Loader; +import androidx.preference.Preference; +import androidx.preference.PreferenceGroup; + import com.android.settings.R; import com.android.settings.core.SubSettingLauncher; import com.android.settings.datausage.CycleAdapter.SpinnerInterface; import com.android.settings.widget.LoadingViewController; import com.android.settingslib.AppItem; -import com.android.settingslib.net.ChartData; -import com.android.settingslib.net.ChartDataLoader; -import com.android.settingslib.net.SummaryForAllUidLoader; +import com.android.settingslib.net.NetworkCycleChartData; +import com.android.settingslib.net.NetworkCycleChartDataLoader; +import com.android.settingslib.net.NetworkStatsSummaryLoader; import com.android.settingslib.net.UidDetailProvider; import java.util.ArrayList; @@ -74,17 +72,21 @@ import java.util.List; * Panel showing data usage history across various networks, including options * to inspect based on usage cycle and control through {@link NetworkPolicy}. */ -public class DataUsageList extends DataUsageBase { +public class DataUsageList extends DataUsageBaseFragment { - public static final String EXTRA_SUB_ID = "sub_id"; - public static final String EXTRA_NETWORK_TEMPLATE = "network_template"; + static final String EXTRA_SUB_ID = "sub_id"; + static final String EXTRA_NETWORK_TEMPLATE = "network_template"; + static final String EXTRA_NETWORK_TYPE = "network_type"; - private static final String TAG = "DataUsage"; + private static final String TAG = "DataUsageList"; private static final boolean LOGD = false; private static final String KEY_USAGE_AMOUNT = "usage_amount"; private static final String KEY_CHART_DATA = "chart_data"; private static final String KEY_APPS_GROUP = "apps_group"; + private static final String KEY_TEMPLATE = "template"; + private static final String KEY_APP = "app"; + private static final String KEY_FIELDS = "fields"; private static final int LOADER_CHART_DATA = 2; private static final int LOADER_SUMMARY = 3; @@ -97,51 +99,47 @@ public class DataUsageList extends DataUsageBase { } }; - private INetworkStatsSession mStatsSession; - private ChartDataUsagePreference mChart; - @VisibleForTesting NetworkTemplate mTemplate; @VisibleForTesting int mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; - private ChartData mChartData; + @VisibleForTesting + int mNetworkType; + @VisibleForTesting + Spinner mCycleSpinner; + @VisibleForTesting + LoadingViewController mLoadingViewController; - private LoadingViewController mLoadingViewController; + private ChartDataUsagePreference mChart; + private TelephonyManager mTelephonyManager; + private List<NetworkCycleChartData> mCycleData; + private ArrayList<Long> mCycles; private UidDetailProvider mUidDetailProvider; private CycleAdapter mCycleAdapter; - private Spinner mCycleSpinner; private Preference mUsageAmount; private PreferenceGroup mApps; private View mHeader; - @Override public int getMetricsCategory() { - return MetricsEvent.DATA_USAGE_LIST; + return SettingsEnums.DATA_USAGE_LIST; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - final Context context = getActivity(); + final Activity activity = getActivity(); if (!isBandwidthControlEnabled()) { Log.w(TAG, "No bandwidth control; leaving"); - getActivity().finish(); - } - - try { - mStatsSession = services.mStatsService.openSession(); - } catch (RemoteException e) { - throw new RuntimeException(e); + activity.finish(); } - mUidDetailProvider = new UidDetailProvider(context); - - addPreferencesFromResource(R.xml.data_usage_list); + mUidDetailProvider = new UidDetailProvider(activity); + mTelephonyManager = activity.getSystemService(TelephonyManager.class); mUsageAmount = findPreference(KEY_USAGE_AMOUNT); - mChart = (ChartDataUsagePreference) findPreference(KEY_CHART_DATA); - mApps = (PreferenceGroup) findPreference(KEY_APPS_GROUP); + mChart = findPreference(KEY_CHART_DATA); + mApps = findPreference(KEY_APPS_GROUP); processArgument(); } @@ -155,12 +153,13 @@ public class DataUsageList extends DataUsageBase { args.putParcelable(DataUsageList.EXTRA_NETWORK_TEMPLATE, mTemplate); new SubSettingLauncher(getContext()) .setDestination(BillingCycleSettings.class.getName()) - .setTitle(R.string.billing_cycle) + .setTitleRes(R.string.billing_cycle) .setSourceMetricsCategory(getMetricsCategory()) .setArguments(args) .launch(); }); mCycleSpinner = mHeader.findViewById(R.id.filter_spinner); + mCycleSpinner.setVisibility(View.GONE); mCycleAdapter = new CycleAdapter(mCycleSpinner.getContext(), new SpinnerInterface() { @Override public void setAdapter(CycleAdapter cycleAdapter) { @@ -181,7 +180,7 @@ public class DataUsageList extends DataUsageBase { public void setSelection(int position) { mCycleSpinner.setSelection(position); } - }, mCycleListener, true); + }, mCycleListener); mLoadingViewController = new LoadingViewController( getView().findViewById(R.id.loading_container), getListView()); @@ -193,28 +192,6 @@ public class DataUsageList extends DataUsageBase { super.onResume(); mDataStateListener.setListener(true, mSubId, getContext()); updateBody(); - - // kick off background task to update stats - new AsyncTask<Void, Void, Void>() { - @Override - protected Void doInBackground(Void... params) { - try { - // wait a few seconds before kicking off - Thread.sleep(2 * DateUtils.SECOND_IN_MILLIS); - services.mStatsService.forceUpdate(); - } catch (InterruptedException e) { - } catch (RemoteException e) { - } - return null; - } - - @Override - protected void onPostExecute(Void result) { - if (isAdded()) { - updateBody(); - } - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } @Override @@ -228,16 +205,25 @@ public class DataUsageList extends DataUsageBase { mUidDetailProvider.clearCache(); mUidDetailProvider = null; - TrafficStats.closeQuietly(mStatsSession); - super.onDestroy(); } + @Override + protected int getPreferenceScreenResId() { + return R.xml.data_usage_list; + } + + @Override + protected String getLogTag() { + return TAG; + } + void processArgument() { final Bundle args = getArguments(); if (args != null) { mSubId = args.getInt(EXTRA_SUB_ID, SubscriptionManager.INVALID_SUBSCRIPTION_ID); mTemplate = args.getParcelable(EXTRA_NETWORK_TEMPLATE); + mNetworkType = args.getInt(EXTRA_NETWORK_TYPE, ConnectivityManager.TYPE_MOBILE); } if (mTemplate == null && mSubId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { final Intent intent = getIntent(); @@ -248,8 +234,7 @@ public class DataUsageList extends DataUsageBase { } /** - * Update body content based on current tab. Loads - * {@link NetworkStatsHistory} and {@link NetworkPolicy} from system, and + * Update body content based on current tab. Loads network cycle data from system, and * binds them to visible controls. */ private void updateBody() { @@ -261,7 +246,7 @@ public class DataUsageList extends DataUsageBase { // TODO: consider chaining two loaders together instead of reloading // network history when showing app detail. getLoaderManager().restartLoader(LOADER_CHART_DATA, - ChartDataLoader.buildArgs(mTemplate, null), mChartDataCallbacks); + buildArgs(mTemplate), mNetworkCycleDataCallbacks); // detail mode can change visible menus, invalidate getActivity().invalidateOptionsMenu(); @@ -281,17 +266,27 @@ public class DataUsageList extends DataUsageBase { mChart.setColors(seriesColor, secondaryColor); } + private Bundle buildArgs(NetworkTemplate template) { + final Bundle args = new Bundle(); + args.putParcelable(KEY_TEMPLATE, template); + args.putParcelable(KEY_APP, null); + args.putInt(KEY_FIELDS, FIELD_RX_BYTES | FIELD_TX_BYTES); + return args; + } + /** * Update chart sweeps and cycle list to reflect {@link NetworkPolicy} for * current {@link #mTemplate}. */ - private void updatePolicy() { + @VisibleForTesting + void updatePolicy() { final NetworkPolicy policy = services.mPolicyEditor.getPolicy(mTemplate); final View configureButton = mHeader.findViewById(R.id.filter_settings); //SUB SELECT if (isNetworkPolicyModifiable(policy, mSubId) && isMobileDataAvailable(mSubId)) { mChart.setNetworkPolicy(policy); configureButton.setVisibility(View.VISIBLE); + ((ImageView) configureButton).setColorFilter(android.R.color.white); } else { // controls are disabled; don't bind warning/limit sweeps mChart.setNetworkPolicy(null); @@ -299,7 +294,7 @@ public class DataUsageList extends DataUsageBase { } // generate cycle list based on policy and available history - if (mCycleAdapter.updateCycleList(policy, mChartData)) { + if (mCycleAdapter.updateCycleList(mCycleData)) { updateDetailData(); } } @@ -312,46 +307,40 @@ public class DataUsageList extends DataUsageBase { private void updateDetailData() { if (LOGD) Log.d(TAG, "updateDetailData()"); - final long start = mChart.getInspectStart(); - final long end = mChart.getInspectEnd(); - final long now = System.currentTimeMillis(); - - final Context context = getActivity(); - - NetworkStatsHistory.Entry entry = null; - if (mChartData != null) { - entry = mChartData.network.getValues(start, end, now, null); - } - // kick off loader for detailed stats - getLoaderManager().restartLoader(LOADER_SUMMARY, - SummaryForAllUidLoader.buildArgs(mTemplate, start, end), mSummaryCallbacks); + getLoaderManager().restartLoader(LOADER_SUMMARY, null /* args */, + mNetworkStatsDetailCallbacks); - final long totalBytes = entry != null ? entry.rxBytes + entry.txBytes : 0; - final CharSequence totalPhrase = DataUsageUtils.formatDataUsage(context, totalBytes); + final long totalBytes = mCycleData != null && !mCycleData.isEmpty() + ? mCycleData.get(mCycleSpinner.getSelectedItemPosition()).getTotalUsage() : 0; + final CharSequence totalPhrase = DataUsageUtils.formatDataUsage(getActivity(), totalBytes); mUsageAmount.setTitle(getString(R.string.data_used_template, totalPhrase)); } /** * Bind the given {@link NetworkStats}, or {@code null} to clear list. */ - public void bindStats(NetworkStats stats, int[] restrictedUids) { - ArrayList<AppItem> items = new ArrayList<>(); + private void bindStats(NetworkStats stats, int[] restrictedUids) { + mApps.removeAll(); + if (stats == null) { + if (LOGD) { + Log.d(TAG, "No network stats data. App list cleared."); + } + return; + } + + final ArrayList<AppItem> items = new ArrayList<>(); long largest = 0; final int currentUserId = ActivityManager.getCurrentUser(); - UserManager userManager = UserManager.get(getContext()); + final UserManager userManager = UserManager.get(getContext()); final List<UserHandle> profiles = userManager.getUserProfiles(); final SparseArray<AppItem> knownItems = new SparseArray<AppItem>(); - NetworkStats.Entry entry = null; - final int size = stats != null ? stats.size() : 0; - for (int i = 0; i < size; i++) { - entry = stats.getValues(i, entry); - + final Bucket bucket = new Bucket(); + while (stats.hasNextBucket() && stats.getNextBucket(bucket)) { // Decide how to collapse items together - final int uid = entry.uid; - + final int uid = bucket.getUid(); final int collapseKey; final int category; final int userId = UserHandle.getUserId(uid); @@ -360,8 +349,8 @@ public class DataUsageList extends DataUsageBase { if (userId != currentUserId) { // Add to a managed user item. final int managedKey = UidDetailProvider.buildKeyForUser(userId); - largest = accumulate(managedKey, knownItems, entry, AppItem.CATEGORY_USER, - items, largest); + largest = accumulate(managedKey, knownItems, bucket, + AppItem.CATEGORY_USER, items, largest); } // Add to app item. collapseKey = uid; @@ -378,15 +367,17 @@ public class DataUsageList extends DataUsageBase { category = AppItem.CATEGORY_USER; } } - } else if (uid == UID_REMOVED || uid == UID_TETHERING) { + } else if (uid == UID_REMOVED || uid == UID_TETHERING + || uid == Process.OTA_UPDATE_UID) { collapseKey = uid; category = AppItem.CATEGORY_APP; } else { collapseKey = android.os.Process.SYSTEM_UID; category = AppItem.CATEGORY_APP; } - largest = accumulate(collapseKey, knownItems, entry, category, items, largest); + largest = accumulate(collapseKey, knownItems, bucket, category, items, largest); } + stats.close(); final int restrictedUidsMax = restrictedUids.length; for (int i = 0; i < restrictedUidsMax; ++i) { @@ -407,7 +398,6 @@ public class DataUsageList extends DataUsageBase { } Collections.sort(items); - mApps.removeAll(); for (int i = 0; i < items.size(); i++) { final int percentTotal = largest != 0 ? (int) (items.get(i).total * 100 / largest) : 0; AppDataUsagePreference preference = new AppDataUsagePreference(getContext(), @@ -425,14 +415,27 @@ public class DataUsageList extends DataUsageBase { } } - private void startAppDataUsage(AppItem item) { + @VisibleForTesting + void startAppDataUsage(AppItem item) { final Bundle args = new Bundle(); args.putParcelable(AppDataUsage.ARG_APP_ITEM, item); args.putParcelable(AppDataUsage.ARG_NETWORK_TEMPLATE, mTemplate); + if (mCycles == null) { + mCycles = new ArrayList<>(); + for (NetworkCycleChartData data : mCycleData) { + if (mCycles.isEmpty()) { + mCycles.add(data.getEndTime()); + } + mCycles.add(data.getStartTime()); + } + } + args.putSerializable(AppDataUsage.ARG_NETWORK_CYCLES, mCycles); + args.putLong(AppDataUsage.ARG_SELECTED_CYCLE, + mCycleData.get(mCycleSpinner.getSelectedItemPosition()).getEndTime()); new SubSettingLauncher(getContext()) .setDestination(AppDataUsage.class.getName()) - .setTitle(R.string.app_data_usage) + .setTitleRes(R.string.data_usage_app_summary_title) .setArguments(args) .setSourceMetricsCategory(getMetricsCategory()) .launch(); @@ -444,12 +447,12 @@ public class DataUsageList extends DataUsageBase { * * @param collapseKey the collapse key used to map the item. * @param knownItems collection of known (already existing) items. - * @param entry the network stats entry to extract data usage from. + * @param bucket the network stats bucket to extract data usage from. * @param itemCategory the item is categorized on the list view by this category. Must be */ private static long accumulate(int collapseKey, final SparseArray<AppItem> knownItems, - NetworkStats.Entry entry, int itemCategory, ArrayList<AppItem> items, long largest) { - final int uid = entry.uid; + Bucket bucket, int itemCategory, ArrayList<AppItem> items, long largest) { + final int uid = bucket.getUid(); AppItem item = knownItems.get(collapseKey); if (item == null) { item = new AppItem(collapseKey); @@ -458,67 +461,10 @@ public class DataUsageList extends DataUsageBase { knownItems.put(item.key, item); } item.addUid(uid); - item.total += entry.rxBytes + entry.txBytes; + item.total += bucket.getRxBytes() + bucket.getTxBytes(); return Math.max(largest, item.total); } - /** - * Test if device has a mobile data radio with SIM in ready state. - */ - public static boolean hasReadyMobileRadio(Context context) { - if (DataUsageUtils.TEST_RADIOS) { - return SystemProperties.get(DataUsageUtils.TEST_RADIOS_PROP).contains("mobile"); - } - - final ConnectivityManager conn = ConnectivityManager.from(context); - final TelephonyManager tele = TelephonyManager.from(context); - - final List<SubscriptionInfo> subInfoList = - SubscriptionManager.from(context).getActiveSubscriptionInfoList(); - // No activated Subscriptions - if (subInfoList == null) { - if (LOGD) Log.d(TAG, "hasReadyMobileRadio: subInfoList=null"); - return false; - } - // require both supported network and ready SIM - boolean isReady = true; - for (SubscriptionInfo subInfo : subInfoList) { - isReady = isReady & tele.getSimState(subInfo.getSimSlotIndex()) == SIM_STATE_READY; - if (LOGD) Log.d(TAG, "hasReadyMobileRadio: subInfo=" + subInfo); - } - boolean retVal = conn.isNetworkSupported(TYPE_MOBILE) && isReady; - if (LOGD) { - Log.d(TAG, "hasReadyMobileRadio:" - + " conn.isNetworkSupported(TYPE_MOBILE)=" - + conn.isNetworkSupported(TYPE_MOBILE) - + " isReady=" + isReady); - } - return retVal; - } - - /* - * TODO: consider adding to TelephonyManager or SubscriptionManager. - */ - public static boolean hasReadyMobileRadio(Context context, int subId) { - if (DataUsageUtils.TEST_RADIOS) { - return SystemProperties.get(DataUsageUtils.TEST_RADIOS_PROP).contains("mobile"); - } - - final ConnectivityManager conn = ConnectivityManager.from(context); - final TelephonyManager tele = TelephonyManager.from(context); - final int slotId = SubscriptionManager.getSlotIndex(subId); - final boolean isReady = tele.getSimState(slotId) == SIM_STATE_READY; - - boolean retVal = conn.isNetworkSupported(TYPE_MOBILE) && isReady; - if (LOGD) { - Log.d(TAG, "hasReadyMobileRadio: subId=" + subId - + " conn.isNetworkSupported(TYPE_MOBILE)=" - + conn.isNetworkSupported(TYPE_MOBILE) - + " isReady=" + isReady); - } - return retVal; - } - private OnItemSelectedListener mCycleListener = new OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { @@ -532,7 +478,7 @@ public class DataUsageList extends DataUsageBase { // update chart to show selected cycle, and update detail data // to match updated sweep bounds. - mChart.setVisibleRange(cycle.start, cycle.end); + mChart.setNetworkCycleData(mCycleData.get(position)); updateDetailData(); } @@ -543,35 +489,41 @@ public class DataUsageList extends DataUsageBase { } }; - private final LoaderCallbacks<ChartData> mChartDataCallbacks = new LoaderCallbacks< - ChartData>() { + @VisibleForTesting + final LoaderCallbacks<List<NetworkCycleChartData>> mNetworkCycleDataCallbacks = + new LoaderCallbacks<List<NetworkCycleChartData>>() { @Override - public Loader<ChartData> onCreateLoader(int id, Bundle args) { - return new ChartDataLoader(getActivity(), mStatsSession, args); + public Loader<List<NetworkCycleChartData>> onCreateLoader(int id, Bundle args) { + return NetworkCycleChartDataLoader.builder(getContext()) + .setNetworkTemplate(mTemplate) + .build(); } @Override - public void onLoadFinished(Loader<ChartData> loader, ChartData data) { + public void onLoadFinished(Loader<List<NetworkCycleChartData>> loader, + List<NetworkCycleChartData> data) { mLoadingViewController.showContent(false /* animate */); - mChartData = data; - mChart.setNetworkStats(mChartData.network); - + mCycleData = data; // calculate policy cycles based on available data updatePolicy(); + mCycleSpinner.setVisibility(View.VISIBLE); } @Override - public void onLoaderReset(Loader<ChartData> loader) { - mChartData = null; - mChart.setNetworkStats(null); + public void onLoaderReset(Loader<List<NetworkCycleChartData>> loader) { + mCycleData = null; } }; - private final LoaderCallbacks<NetworkStats> mSummaryCallbacks = new LoaderCallbacks< - NetworkStats>() { + private final LoaderCallbacks<NetworkStats> mNetworkStatsDetailCallbacks = + new LoaderCallbacks<NetworkStats>() { @Override public Loader<NetworkStats> onCreateLoader(int id, Bundle args) { - return new SummaryForAllUidLoader(getActivity(), mStatsSession, args); + return new NetworkStatsSummaryLoader.Builder(getContext()) + .setStartTime(mChart.getInspectStart()) + .setEndTime(mChart.getInspectEnd()) + .setNetworkTemplate(mTemplate) + .build(); } @Override diff --git a/src/com/android/settings/datausage/DataUsagePreference.java b/src/com/android/settings/datausage/DataUsagePreference.java index 9e6684c368..644ea9bf76 100644 --- a/src/com/android/settings/datausage/DataUsagePreference.java +++ b/src/com/android/settings/datausage/DataUsagePreference.java @@ -14,19 +14,20 @@ package com.android.settings.datausage; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; import android.content.res.TypedArray; +import android.net.ConnectivityManager; import android.net.NetworkTemplate; import android.os.Bundle; +import android.util.AttributeSet; + +import androidx.annotation.VisibleForTesting; import androidx.core.content.res.TypedArrayUtils; import androidx.preference.Preference; -import android.util.AttributeSet; -import android.util.FeatureFlagUtils; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; -import com.android.settings.core.FeatureFlags; import com.android.settings.core.SubSettingLauncher; import com.android.settingslib.net.DataUsageController; @@ -48,52 +49,51 @@ public class DataUsagePreference extends Preference implements TemplatePreferenc } @Override - public void setTemplate(NetworkTemplate template, int subId, - NetworkServices services) { + public void setTemplate(NetworkTemplate template, int subId, NetworkServices services) { mTemplate = template; mSubId = subId; - DataUsageController controller = new DataUsageController(getContext()); - DataUsageController.DataUsageInfo usageInfo = controller.getDataUsageInfo(mTemplate); - if (FeatureFlagUtils.isEnabled(getContext(), FeatureFlags.DATA_USAGE_SETTINGS_V2)) { - if (mTemplate.isMatchRuleMobile()) { - setTitle(R.string.app_cellular_data_usage); - } else { - setTitle(mTitleRes); - setSummary(getContext().getString(R.string.data_usage_template, - DataUsageUtils.formatDataUsage(getContext(), usageInfo.usageLevel), - usageInfo.period)); - } + final DataUsageController controller = getDataUsageController(); + if (mTemplate.isMatchRuleMobile()) { + setTitle(R.string.app_cellular_data_usage); } else { + final DataUsageController.DataUsageInfo usageInfo = + controller.getDataUsageInfo(mTemplate); setTitle(mTitleRes); setSummary(getContext().getString(R.string.data_usage_template, DataUsageUtils.formatDataUsage(getContext(), usageInfo.usageLevel), - usageInfo.period)); + usageInfo.period)); + } + final long usageLevel = controller.getHistoricalUsageLevel(template); + if (usageLevel > 0L) { + setIntent(getIntent()); + } else { + setIntent(null); + setEnabled(false); } - setIntent(getIntent()); } @Override public Intent getIntent() { final Bundle args = new Bundle(); + final SubSettingLauncher launcher; args.putParcelable(DataUsageList.EXTRA_NETWORK_TEMPLATE, mTemplate); args.putInt(DataUsageList.EXTRA_SUB_ID, mSubId); - final SubSettingLauncher launcher = new SubSettingLauncher(getContext()) - .setArguments(args) - .setDestination(DataUsageList.class.getName()) - .setSourceMetricsCategory(MetricsProto.MetricsEvent.VIEW_UNKNOWN); - if (FeatureFlagUtils.isEnabled(getContext(), FeatureFlags.DATA_USAGE_SETTINGS_V2)) { - if (mTemplate.isMatchRuleMobile()) { - launcher.setTitle(R.string.app_cellular_data_usage); - } else { - launcher.setTitle(mTitleRes); - } + args.putInt(DataUsageList.EXTRA_NETWORK_TYPE, mTemplate.isMatchRuleMobile() + ? ConnectivityManager.TYPE_MOBILE : ConnectivityManager.TYPE_WIFI); + launcher = new SubSettingLauncher(getContext()) + .setArguments(args) + .setDestination(DataUsageList.class.getName()) + .setSourceMetricsCategory(SettingsEnums.PAGE_UNKNOWN); + if (mTemplate.isMatchRuleMobile()) { + launcher.setTitleRes(R.string.app_cellular_data_usage); } else { - if (mTitleRes > 0) { - launcher.setTitle(mTitleRes); - } else { - launcher.setTitle(getTitle()); - } + launcher.setTitleRes(mTitleRes); } return launcher.toIntent(); } + + @VisibleForTesting + DataUsageController getDataUsageController() { + return new DataUsageController(getContext()); + } } diff --git a/src/com/android/settings/datausage/DataUsageSummary.java b/src/com/android/settings/datausage/DataUsageSummary.java index b6e72dd83d..6597ecb4c8 100644 --- a/src/com/android/settings/datausage/DataUsageSummary.java +++ b/src/com/android/settings/datausage/DataUsageSummary.java @@ -15,15 +15,10 @@ package com.android.settings.datausage; import android.app.Activity; -import android.content.ComponentName; +import android.app.settings.SettingsEnums; import android.content.Context; -import android.content.Intent; import android.net.NetworkTemplate; import android.os.Bundle; -import android.provider.SearchIndexableResource; -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.SubscriptionPlan; @@ -33,14 +28,14 @@ import android.text.SpannableString; import android.text.TextUtils; import android.text.format.Formatter; import android.text.style.RelativeSizeSpan; -import android.view.MenuItem; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + import com.android.settings.R; import com.android.settings.Utils; import com.android.settings.dashboard.SummaryLoader; -import com.android.settings.search.BaseSearchIndexProvider; -import com.android.settings.search.Indexable; import com.android.settingslib.NetworkPolicyEditor; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.net.DataUsageController; @@ -51,8 +46,7 @@ import java.util.List; /** * Settings preference fragment that displays data usage summary. */ -public class DataUsageSummary extends DataUsageBaseFragment implements Indexable, - DataUsageEditController { +public class DataUsageSummary extends DataUsageBaseFragment implements DataUsageEditController { private static final String TAG = "DataUsageSummary"; @@ -88,12 +82,12 @@ public class DataUsageSummary extends DataUsageBaseFragment implements Indexable boolean hasMobileData = DataUsageUtils.hasMobileData(context); - int defaultSubId = DataUsageUtils.getDefaultSubscriptionId(context); + final int defaultSubId = SubscriptionManager.getDefaultDataSubscriptionId(); if (defaultSubId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { hasMobileData = false; } mDefaultTemplate = DataUsageUtils.getDefaultTemplate(context, defaultSubId); - mSummaryPreference = (DataUsageSummaryPreference) findPreference(KEY_STATUS_HEADER); + mSummaryPreference = findPreference(KEY_STATUS_HEADER); if (!hasMobileData || !isAdmin()) { removePreference(KEY_RESTRICT_BACKGROUND); @@ -119,20 +113,6 @@ public class DataUsageSummary extends DataUsageBaseFragment implements Indexable } @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.data_usage_menu_cellular_networks: { - final Intent intent = new Intent(Intent.ACTION_MAIN); - intent.setComponent(new ComponentName("com.android.phone", - "com.android.phone.MobileNetworkSettings")); - startActivity(intent); - return true; - } - } - return false; - } - - @Override public boolean onPreferenceTreeClick(Preference preference) { if (preference == findPreference(KEY_STATUS_HEADER)) { BillingCycleSettings.BytesEditorFragment.show(this, false); @@ -156,9 +136,10 @@ public class DataUsageSummary extends DataUsageBaseFragment implements Indexable final Activity activity = getActivity(); final ArrayList<AbstractPreferenceController> controllers = new ArrayList<>(); mSummaryController = - new DataUsageSummaryPreferenceController(activity, getLifecycle(), this); + new DataUsageSummaryPreferenceController(activity, getSettingsLifecycle(), this, + DataUsageUtils.getDefaultSubscriptionId(activity)); controllers.add(mSummaryController); - getLifecycle().addObserver(mSummaryController); + getSettingsLifecycle().addObserver(mSummaryController); return controllers; } @@ -170,7 +151,8 @@ public class DataUsageSummary extends DataUsageBaseFragment implements Indexable private void addMobileSection(int subId, SubscriptionInfo subInfo) { TemplatePreferenceCategory category = (TemplatePreferenceCategory) inflatePreferences(R.xml.data_usage_cellular); - category.setTemplate(getNetworkTemplate(subId), subId, services); + category.setTemplate(DataUsageUtils.getMobileTemplate(getContext(), subId), + subId, services); category.pushTemplates(services); if (subInfo != null && !TextUtils.isEmpty(subInfo.getDisplayName())) { Preference title = category.findPreference(KEY_MOBILE_USAGE_TITLE); @@ -204,13 +186,6 @@ public class DataUsageSummary extends DataUsageBaseFragment implements Indexable return pref; } - private NetworkTemplate getNetworkTemplate(int subscriptionId) { - NetworkTemplate mobileAll = NetworkTemplate.buildTemplateMobileAll( - services.mTelephonyManager.getSubscriberId(subscriptionId)); - return NetworkTemplate.normalize(mobileAll, - services.mTelephonyManager.getMergedSubscriberIds()); - } - @Override public void onResume() { super.onResume(); @@ -257,7 +232,7 @@ public class DataUsageSummary extends DataUsageBaseFragment implements Indexable @Override public int getMetricsCategory() { - return MetricsEvent.DATA_USAGE_SUMMARY; + return SettingsEnums.DATA_USAGE_SUMMARY; } @Override @@ -298,8 +273,7 @@ public class DataUsageSummary extends DataUsageBaseFragment implements Indexable formatUsedData())); } else { final DataUsageController.DataUsageInfo info = - mDataController - .getDataUsageInfo(NetworkTemplate.buildTemplateWifiWildcard()); + mDataController.getWifiDataUsageInfo(); if (info == null) { mSummaryLoader.setSummary(this, null); @@ -349,51 +323,4 @@ public class DataUsageSummary extends DataUsageBaseFragment implements Indexable public static final SummaryLoader.SummaryProviderFactory SUMMARY_PROVIDER_FACTORY = SummaryProvider::new; - - /** - * For search - */ - public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = - new BaseSearchIndexProvider() { - - @Override - public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, - boolean enabled) { - List<SearchIndexableResource> resources = new ArrayList<>(); - SearchIndexableResource resource = new SearchIndexableResource(context); - resource.xmlResId = R.xml.data_usage; - resources.add(resource); - - resource = new SearchIndexableResource(context); - resource.xmlResId = R.xml.data_usage_cellular; - resources.add(resource); - - resource = new SearchIndexableResource(context); - resource.xmlResId = R.xml.data_usage_wifi; - resources.add(resource); - - return resources; - } - - @Override - public List<String> getNonIndexableKeys(Context context) { - List<String> keys = super.getNonIndexableKeys(context); - - if (!DataUsageUtils.hasMobileData(context)) { - keys.add(KEY_MOBILE_USAGE_TITLE); - keys.add(KEY_MOBILE_DATA_USAGE_TOGGLE); - keys.add(KEY_MOBILE_DATA_USAGE); - keys.add(KEY_MOBILE_BILLING_CYCLE); - } - - if (!DataUsageUtils.hasWifiRadio(context)) { - keys.add(KEY_WIFI_DATA_USAGE); - } - - // This title is named Wifi, and will confuse users. - keys.add(KEY_WIFI_USAGE_TITLE); - - return keys; - } - }; } diff --git a/src/com/android/settings/datausage/DataUsageSummaryLegacy.java b/src/com/android/settings/datausage/DataUsageSummaryLegacy.java deleted file mode 100644 index 1818b305e3..0000000000 --- a/src/com/android/settings/datausage/DataUsageSummaryLegacy.java +++ /dev/null @@ -1,408 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file - * except in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the - * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package com.android.settings.datausage; - -import android.app.Activity; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.net.NetworkPolicyManager; -import android.net.NetworkTemplate; -import android.os.Bundle; -import android.os.UserManager; -import android.provider.SearchIndexableResource; -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; -import android.telephony.SubscriptionInfo; -import android.telephony.SubscriptionManager; -import android.text.BidiFormatter; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.TextUtils; -import android.text.format.Formatter; -import android.text.style.RelativeSizeSpan; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; - -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.settings.R; -import com.android.settings.SummaryPreference; -import com.android.settings.Utils; -import com.android.settings.dashboard.SummaryLoader; -import com.android.settings.search.BaseSearchIndexProvider; -import com.android.settings.search.Indexable; -import com.android.settingslib.NetworkPolicyEditor; -import com.android.settingslib.core.AbstractPreferenceController; -import com.android.settingslib.net.DataUsageController; - -import java.util.ArrayList; -import java.util.List; - -/** - * Legacy {@link DataUsageSummary} fragment. - */ -public class DataUsageSummaryLegacy extends DataUsageBaseFragment implements Indexable, - DataUsageEditController { - - private static final String TAG = "DataUsageSummaryLegacy"; - - static final boolean LOGD = false; - - public static final String KEY_RESTRICT_BACKGROUND = "restrict_background_legacy"; - - private static final String KEY_STATUS_HEADER = "status_header"; - private static final String KEY_LIMIT_SUMMARY = "limit_summary"; - - // Mobile data keys - public static final String KEY_MOBILE_USAGE_TITLE = "mobile_category"; - public static final String KEY_MOBILE_DATA_USAGE_TOGGLE = "data_usage_enable"; - public static final String KEY_MOBILE_DATA_USAGE = "cellular_data_usage"; - public static final String KEY_MOBILE_BILLING_CYCLE = "billing_preference"; - - // Wifi keys - public static final String KEY_WIFI_USAGE_TITLE = "wifi_category"; - public static final String KEY_WIFI_DATA_USAGE = "wifi_data_usage"; - - private DataUsageController mDataUsageController; - private DataUsageInfoController mDataInfoController; - private SummaryPreference mSummaryPreference; - private Preference mLimitPreference; - private NetworkTemplate mDefaultTemplate; - private int mDataUsageTemplate; - private NetworkPolicyEditor mPolicyEditor; - - @Override - public int getHelpResource() { - return R.string.help_url_data_usage; - } - - @Override - public void onCreate(Bundle icicle) { - super.onCreate(icicle); - - final Context context = getContext(); - NetworkPolicyManager policyManager = NetworkPolicyManager.from(context); - mPolicyEditor = new NetworkPolicyEditor(policyManager); - - boolean hasMobileData = DataUsageUtils.hasMobileData(context); - mDataUsageController = new DataUsageController(context); - mDataInfoController = new DataUsageInfoController(); - - int defaultSubId = DataUsageUtils.getDefaultSubscriptionId(context); - if (defaultSubId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { - hasMobileData = false; - } - mDefaultTemplate = DataUsageUtils.getDefaultTemplate(context, defaultSubId); - mSummaryPreference = (SummaryPreference) findPreference(KEY_STATUS_HEADER); - - if (!hasMobileData || !isAdmin()) { - removePreference(KEY_RESTRICT_BACKGROUND); - } - if (hasMobileData) { - mLimitPreference = findPreference(KEY_LIMIT_SUMMARY); - List<SubscriptionInfo> subscriptions = - services.mSubscriptionManager.getActiveSubscriptionInfoList(); - if (subscriptions == null || subscriptions.size() == 0) { - addMobileSection(defaultSubId); - } - for (int i = 0; subscriptions != null && i < subscriptions.size(); i++) { - SubscriptionInfo subInfo = subscriptions.get(i); - if (subscriptions.size() > 1) { - addMobileSection(subInfo.getSubscriptionId(), subInfo); - } else { - addMobileSection(subInfo.getSubscriptionId()); - } - } - mSummaryPreference.setSelectable(true); - } else { - removePreference(KEY_LIMIT_SUMMARY); - mSummaryPreference.setSelectable(false); - } - boolean hasWifiRadio = DataUsageUtils.hasWifiRadio(context); - if (hasWifiRadio) { - addWifiSection(); - } - if (hasEthernet(context)) { - addEthernetSection(); - } - mDataUsageTemplate = hasMobileData ? R.string.cell_data_template - : hasWifiRadio ? R.string.wifi_data_template - : R.string.ethernet_data_template; - - setHasOptionsMenu(true); - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - if (UserManager.get(getContext()).isAdminUser()) { - inflater.inflate(R.menu.data_usage, menu); - } - super.onCreateOptionsMenu(menu, inflater); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.data_usage_menu_cellular_networks: { - final Intent intent = new Intent(Intent.ACTION_MAIN); - intent.setComponent(new ComponentName("com.android.phone", - "com.android.phone.MobileNetworkSettings")); - startActivity(intent); - return true; - } - } - return false; - } - - @Override - public boolean onPreferenceTreeClick(Preference preference) { - if (preference == findPreference(KEY_STATUS_HEADER)) { - BillingCycleSettings.BytesEditorFragment.show(this, false); - return false; - } - return super.onPreferenceTreeClick(preference); - } - - @Override - protected int getPreferenceScreenResId() { - return R.xml.data_usage_legacy; - } - - @Override - protected String getLogTag() { - return TAG; - } - - @Override - protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { - return null; - } - - private void addMobileSection(int subId) { - addMobileSection(subId, null); - } - - private void addMobileSection(int subId, SubscriptionInfo subInfo) { - TemplatePreferenceCategory category = (TemplatePreferenceCategory) - inflatePreferences(R.xml.data_usage_cellular); - category.setTemplate(getNetworkTemplate(subId), subId, services); - category.pushTemplates(services); - if (subInfo != null && !TextUtils.isEmpty(subInfo.getDisplayName())) { - Preference title = category.findPreference(KEY_MOBILE_USAGE_TITLE); - title.setTitle(subInfo.getDisplayName()); - } - } - - private void addWifiSection() { - TemplatePreferenceCategory category = (TemplatePreferenceCategory) - inflatePreferences(R.xml.data_usage_wifi); - category.setTemplate(NetworkTemplate.buildTemplateWifiWildcard(), 0, services); - } - - private void addEthernetSection() { - TemplatePreferenceCategory category = (TemplatePreferenceCategory) - inflatePreferences(R.xml.data_usage_ethernet); - category.setTemplate(NetworkTemplate.buildTemplateEthernet(), 0, services); - } - - private Preference inflatePreferences(int resId) { - PreferenceScreen rootPreferences = getPreferenceManager().inflateFromResource( - getPrefContext(), resId, null); - Preference pref = rootPreferences.getPreference(0); - rootPreferences.removeAll(); - - PreferenceScreen screen = getPreferenceScreen(); - pref.setOrder(screen.getPreferenceCount()); - screen.addPreference(pref); - - return pref; - } - - private NetworkTemplate getNetworkTemplate(int subscriptionId) { - NetworkTemplate mobileAll = NetworkTemplate.buildTemplateMobileAll( - services.mTelephonyManager.getSubscriberId(subscriptionId)); - return NetworkTemplate.normalize(mobileAll, - services.mTelephonyManager.getMergedSubscriberIds()); - } - - @Override - public void onResume() { - super.onResume(); - updateState(); - } - - @VisibleForTesting - static CharSequence formatUsage(Context context, String template, long usageLevel) { - final float LARGER_SIZE = 1.25f * 1.25f; // (1/0.8)^2 - final float SMALLER_SIZE = 1.0f / LARGER_SIZE; // 0.8^2 - final int FLAGS = Spannable.SPAN_INCLUSIVE_INCLUSIVE; - - final Formatter.BytesResult usedResult = Formatter.formatBytes(context.getResources(), - usageLevel, Formatter.FLAG_CALCULATE_ROUNDED); - final SpannableString enlargedValue = new SpannableString(usedResult.value); - enlargedValue.setSpan(new RelativeSizeSpan(LARGER_SIZE), 0, enlargedValue.length(), FLAGS); - - final SpannableString amountTemplate = new SpannableString( - context.getString(com.android.internal.R.string.fileSizeSuffix) - .replace("%1$s", "^1").replace("%2$s", "^2")); - final CharSequence formattedUsage = TextUtils.expandTemplate(amountTemplate, - enlargedValue, usedResult.units); - - final SpannableString fullTemplate = new SpannableString(template); - fullTemplate.setSpan(new RelativeSizeSpan(SMALLER_SIZE), 0, fullTemplate.length(), FLAGS); - return TextUtils.expandTemplate(fullTemplate, - BidiFormatter.getInstance().unicodeWrap(formattedUsage.toString())); - } - - private void updateState() { - DataUsageController.DataUsageInfo info = mDataUsageController.getDataUsageInfo( - mDefaultTemplate); - Context context = getContext(); - mDataInfoController.updateDataLimit(info, - services.mPolicyEditor.getPolicy(mDefaultTemplate)); - - if (mSummaryPreference != null) { - mSummaryPreference.setTitle( - formatUsage(context, getString(mDataUsageTemplate), info.usageLevel)); - final long limit = mDataInfoController.getSummaryLimit(info); - mSummaryPreference.setSummary(info.period); - if (limit <= 0) { - mSummaryPreference.setChartEnabled(false); - } else { - mSummaryPreference.setChartEnabled(true); - mSummaryPreference.setLabels(Formatter.formatFileSize(context, 0), - Formatter.formatFileSize(context, limit)); - mSummaryPreference.setRatios(info.usageLevel / (float) limit, 0, - (limit - info.usageLevel) / (float) limit); - } - } - if (mLimitPreference != null && (info.warningLevel > 0 || info.limitLevel > 0)) { - String warning = Formatter.formatFileSize(context, info.warningLevel); - String limit = Formatter.formatFileSize(context, info.limitLevel); - mLimitPreference.setSummary(getString(info.limitLevel <= 0 ? R.string.cell_warning_only - : R.string.cell_warning_and_limit, warning, limit)); - } else if (mLimitPreference != null) { - mLimitPreference.setSummary(null); - } - - PreferenceScreen screen = getPreferenceScreen(); - for (int i = 1; i < screen.getPreferenceCount(); i++) { - ((TemplatePreferenceCategory) screen.getPreference(i)).pushTemplates(services); - } - } - - @Override - public int getMetricsCategory() { - return MetricsEvent.DATA_USAGE_SUMMARY; - } - - @Override - public NetworkPolicyEditor getNetworkPolicyEditor() { - return services.mPolicyEditor; - } - - @Override - public NetworkTemplate getNetworkTemplate() { - return mDefaultTemplate; - } - - @Override - public void updateDataUsage() { - updateState(); - } - - private static class SummaryProvider - implements SummaryLoader.SummaryProvider { - - private final Activity mActivity; - private final SummaryLoader mSummaryLoader; - private final DataUsageController mDataController; - - public SummaryProvider(Activity activity, SummaryLoader summaryLoader) { - mActivity = activity; - mSummaryLoader = summaryLoader; - mDataController = new DataUsageController(activity); - } - - @Override - public void setListening(boolean listening) { - if (listening) { - DataUsageController.DataUsageInfo info = mDataController.getDataUsageInfo(); - String used; - if (info == null) { - used = Formatter.formatFileSize(mActivity, 0); - } else if (info.limitLevel <= 0) { - used = Formatter.formatFileSize(mActivity, info.usageLevel); - } else { - used = Utils.formatPercentage(info.usageLevel, info.limitLevel); - } - mSummaryLoader.setSummary(this, - mActivity.getString(R.string.data_usage_summary_format, used)); - } - } - } - - public static final SummaryLoader.SummaryProviderFactory SUMMARY_PROVIDER_FACTORY - = SummaryProvider::new; - - /** - * For search - */ - public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = - new BaseSearchIndexProvider() { - - @Override - public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, - boolean enabled) { - List<SearchIndexableResource> resources = new ArrayList<>(); - SearchIndexableResource resource = new SearchIndexableResource(context); - resource.xmlResId = R.xml.data_usage_legacy; - resources.add(resource); - - resource = new SearchIndexableResource(context); - resource.xmlResId = R.xml.data_usage_cellular; - resources.add(resource); - - resource = new SearchIndexableResource(context); - resource.xmlResId = R.xml.data_usage_wifi; - resources.add(resource); - - return resources; - } - - @Override - public List<String> getNonIndexableKeys(Context context) { - List<String> keys = super.getNonIndexableKeys(context); - - if (!DataUsageUtils.hasMobileData(context)) { - keys.add(KEY_MOBILE_USAGE_TITLE); - keys.add(KEY_MOBILE_DATA_USAGE_TOGGLE); - keys.add(KEY_MOBILE_DATA_USAGE); - keys.add(KEY_MOBILE_BILLING_CYCLE); - } - - if (!DataUsageUtils.hasWifiRadio(context)) { - keys.add(KEY_WIFI_DATA_USAGE); - } - - // This title is named Wifi, and will confuse users. - keys.add(KEY_WIFI_USAGE_TITLE); - - return keys; - } - }; -} diff --git a/src/com/android/settings/datausage/DataUsageSummaryPreference.java b/src/com/android/settings/datausage/DataUsageSummaryPreference.java index b687127ca6..93df2f1bcd 100644 --- a/src/com/android/settings/datausage/DataUsageSummaryPreference.java +++ b/src/com/android/settings/datausage/DataUsageSummaryPreference.java @@ -17,13 +17,13 @@ package com.android.settings.datausage; import android.annotation.AttrRes; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; import android.graphics.Typeface; +import android.net.ConnectivityManager; import android.net.NetworkTemplate; import android.os.Bundle; -import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; import android.text.Spannable; import android.text.SpannableString; import android.text.TextUtils; @@ -35,11 +35,14 @@ import android.widget.Button; import android.widget.ProgressBar; import android.widget.TextView; -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.logging.nano.MetricsProto; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + import com.android.settings.R; import com.android.settings.core.SubSettingLauncher; import com.android.settingslib.Utils; +import com.android.settingslib.net.DataUsageController; import com.android.settingslib.utils.StringUtil; import java.util.Objects; @@ -66,13 +69,15 @@ public class DataUsageSummaryPreference extends Preference { private boolean mDefaultTextColorSet; private int mDefaultTextColor; private int mNumPlans; + /** The specified un-initialized value for cycle time */ + private final long CYCLE_TIME_UNINITIAL_VALUE = 0; /** The ending time of the billing cycle in milliseconds since epoch. */ private long mCycleEndTimeMs; /** The time of the last update in standard milliseconds since the epoch */ private long mSnapshotTimeMs; /** Name of carrier, or null if not available */ private CharSequence mCarrierName; - private String mLimitInfoText; + private CharSequence mLimitInfoText; private Intent mLaunchIntent; /** Progress to display on ProgressBar */ @@ -91,13 +96,14 @@ public class DataUsageSummaryPreference extends Preference { /** WiFi only mode */ private boolean mWifiMode; private String mUsagePeriod; + private boolean mSingleWifi; // Shows only one specified WiFi network usage public DataUsageSummaryPreference(Context context, AttributeSet attrs) { super(context, attrs); setLayoutResource(R.layout.data_usage_summary_preference); } - public void setLimitInfo(String text) { + public void setLimitInfo(CharSequence text) { if (!Objects.equals(text, mLimitInfoText)) { mLimitInfoText = text; notifyChanged(); @@ -139,9 +145,10 @@ public class DataUsageSummaryPreference extends Preference { notifyChanged(); } - void setWifiMode(boolean isWifiMode, String usagePeriod) { + void setWifiMode(boolean isWifiMode, String usagePeriod, boolean isSingleWifi) { mWifiMode = isWifiMode; mUsagePeriod = usagePeriod; + mSingleWifi = isSingleWifi; notifyChanged(); } @@ -168,7 +175,16 @@ public class DataUsageSummaryPreference extends Preference { Button launchButton = (Button) holder.findViewById(R.id.launch_mdp_app_button); TextView limitInfo = (TextView) holder.findViewById(R.id.data_limits); - if (mWifiMode) { + if (mWifiMode && mSingleWifi) { + updateCycleTimeText(holder); + + usageTitle.setVisibility(View.GONE); + launchButton.setVisibility(View.GONE); + carrierInfo.setVisibility(View.GONE); + + limitInfo.setVisibility(TextUtils.isEmpty(mLimitInfoText) ? View.GONE : View.VISIBLE); + limitInfo.setText(mLimitInfoText); + } else if (mWifiMode) { usageTitle.setText(R.string.data_usage_wifi_title); usageTitle.setVisibility(View.VISIBLE); TextView cycleTime = (TextView) holder.findViewById(R.id.cycle_left_time); @@ -176,9 +192,14 @@ public class DataUsageSummaryPreference extends Preference { carrierInfo.setVisibility(View.GONE); limitInfo.setVisibility(View.GONE); - launchButton.setOnClickListener((view) -> { - launchWifiDataUsage(getContext()); - }); + final long usageLevel = getHistoricalUsageLevel(); + if (usageLevel > 0L) { + launchButton.setOnClickListener((view) -> { + launchWifiDataUsage(getContext()); + }); + } else { + launchButton.setEnabled(false); + } launchButton.setText(R.string.launch_wifi_text); launchButton.setVisibility(View.VISIBLE); } else { @@ -199,15 +220,17 @@ public class DataUsageSummaryPreference extends Preference { } } - private static void launchWifiDataUsage(Context context) { + @VisibleForTesting + static void launchWifiDataUsage(Context context) { final Bundle args = new Bundle(1); args.putParcelable(DataUsageList.EXTRA_NETWORK_TEMPLATE, NetworkTemplate.buildTemplateWifiWildcard()); + args.putInt(DataUsageList.EXTRA_NETWORK_TYPE, ConnectivityManager.TYPE_WIFI); final SubSettingLauncher launcher = new SubSettingLauncher(context) .setArguments(args) .setDestination(DataUsageList.class.getName()) - .setSourceMetricsCategory(MetricsProto.MetricsEvent.VIEW_UNKNOWN); - launcher.setTitle(context.getString(R.string.wifi_data_usage)); + .setSourceMetricsCategory(SettingsEnums.PAGE_UNKNOWN); + launcher.setTitleRes(R.string.wifi_data_usage); launcher.launch(); } @@ -255,6 +278,13 @@ public class DataUsageSummaryPreference extends Preference { private void updateCycleTimeText(PreferenceViewHolder holder) { TextView cycleTime = (TextView) holder.findViewById(R.id.cycle_left_time); + // Takes zero as a special case which value is never set. + if (mCycleEndTimeMs == CYCLE_TIME_UNINITIAL_VALUE) { + cycleTime.setVisibility(View.GONE); + return; + } + + cycleTime.setVisibility(View.VISIBLE); long millisLeft = mCycleEndTimeMs - System.currentTimeMillis(); if (millisLeft <= 0) { cycleTime.setText(getContext().getString(R.string.billing_cycle_none_left)); @@ -330,4 +360,11 @@ public class DataUsageSummaryPreference extends Preference { carrierInfo.setTextColor(Utils.getColorAttr(getContext(), colorId)); carrierInfo.setTypeface(typeface); } + + @VisibleForTesting + long getHistoricalUsageLevel() { + final DataUsageController controller = new DataUsageController(getContext()); + return controller.getHistoricalUsageLevel(NetworkTemplate.buildTemplateWifiWildcard()); + } + } diff --git a/src/com/android/settings/datausage/DataUsageSummaryPreferenceController.java b/src/com/android/settings/datausage/DataUsageSummaryPreferenceController.java index e973e6126f..600b9e8236 100644 --- a/src/com/android/settings/datausage/DataUsageSummaryPreferenceController.java +++ b/src/com/android/settings/datausage/DataUsageSummaryPreferenceController.java @@ -21,9 +21,6 @@ import android.content.Context; import android.content.Intent; import android.net.NetworkPolicyManager; import android.net.NetworkTemplate; -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; -import androidx.recyclerview.widget.RecyclerView; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.SubscriptionPlan; @@ -31,6 +28,11 @@ import android.text.TextUtils; import android.util.Log; import android.util.RecurrenceRule; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceFragmentCompat; +import androidx.recyclerview.widget.RecyclerView; + import com.android.internal.util.CollectionUtils; import com.android.settings.R; import com.android.settings.core.BasePreferenceController; @@ -45,9 +47,9 @@ import com.android.settingslib.net.DataUsageController; import java.util.List; /** - * This is the controller for the top of the data usage screen that retrieves carrier data from the - * new subscriptions framework API if available. The controller reads subscription information from - * the framework and falls back to legacy usage data if none are available. + * This is the controller for a data usage header that retrieves carrier data from the new + * subscriptions framework API if available. The controller reads subscription information from the + * framework and falls back to legacy usage data if none are available. */ public class DataUsageSummaryPreferenceController extends BasePreferenceController implements PreferenceControllerMixin, LifecycleObserver, OnStart { @@ -61,11 +63,11 @@ public class DataUsageSummaryPreferenceController extends BasePreferenceControll private final Activity mActivity; private final EntityHeaderController mEntityHeaderController; private final Lifecycle mLifecycle; - private final DataUsageSummary mDataUsageSummary; - private final DataUsageController mDataUsageController; - private final DataUsageInfoController mDataInfoController; + private final PreferenceFragmentCompat mFragment; + protected final DataUsageController mDataUsageController; + protected final DataUsageInfoController mDataInfoController; private final NetworkTemplate mDefaultTemplate; - private final NetworkPolicyEditor mPolicyEditor; + protected final NetworkPolicyEditor mPolicyEditor; private final int mDataUsageTemplate; private final boolean mHasMobileData; private final SubscriptionManager mSubscriptionManager; @@ -92,28 +94,31 @@ public class DataUsageSummaryPreferenceController extends BasePreferenceControll private long mCycleStart; /** The ending time of the billing cycle in ms since the epoch */ private long mCycleEnd; + /** The subscription that we should show usage for. */ + private int mSubscriptionId; private Intent mManageSubscriptionIntent; public DataUsageSummaryPreferenceController(Activity activity, - Lifecycle lifecycle, DataUsageSummary dataUsageSummary) { + Lifecycle lifecycle, PreferenceFragmentCompat fragment, int subscriptionId) { super(activity, KEY); mActivity = activity; mEntityHeaderController = EntityHeaderController.newInstance(activity, - dataUsageSummary, null); + fragment, null); mLifecycle = lifecycle; - mDataUsageSummary = dataUsageSummary; + mFragment = fragment; + mSubscriptionId = subscriptionId; - final int defaultSubId = DataUsageUtils.getDefaultSubscriptionId(activity); - mDefaultTemplate = DataUsageUtils.getDefaultTemplate(activity, defaultSubId); - NetworkPolicyManager policyManager = NetworkPolicyManager.from(activity); + mDefaultTemplate = DataUsageUtils.getDefaultTemplate(activity, mSubscriptionId); + NetworkPolicyManager policyManager = activity.getSystemService(NetworkPolicyManager.class); mPolicyEditor = new NetworkPolicyEditor(policyManager); mHasMobileData = DataUsageUtils.hasMobileData(activity) - && defaultSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID; + && mSubscriptionId != SubscriptionManager.INVALID_SUBSCRIPTION_ID; mDataUsageController = new DataUsageController(activity); + mDataUsageController.setSubscriptionId(mSubscriptionId); mDataInfoController = new DataUsageInfoController(); if (mHasMobileData) { @@ -140,7 +145,8 @@ public class DataUsageSummaryPreferenceController extends BasePreferenceControll Activity activity, Lifecycle lifecycle, EntityHeaderController entityHeaderController, - DataUsageSummary dataUsageSummary) { + PreferenceFragmentCompat fragment, + int subscriptionId) { super(activity, KEY); mDataUsageController = dataUsageController; mDataInfoController = dataInfoController; @@ -152,12 +158,13 @@ public class DataUsageSummaryPreferenceController extends BasePreferenceControll mActivity = activity; mLifecycle = lifecycle; mEntityHeaderController = entityHeaderController; - mDataUsageSummary = dataUsageSummary; + mFragment = fragment; + mSubscriptionId = subscriptionId; } @Override public void onStart() { - RecyclerView view = mDataUsageSummary.getListView(); + RecyclerView view = mFragment.getListView(); mEntityHeaderController.setRecyclerView(view, mLifecycle); mEntityHeaderController.styleActionBar(mActivity); } @@ -192,11 +199,13 @@ public class DataUsageSummaryPreferenceController extends BasePreferenceControll if (DataUsageUtils.hasSim(mActivity)) { info = mDataUsageController.getDataUsageInfo(mDefaultTemplate); mDataInfoController.updateDataLimit(info, mPolicyEditor.getPolicy(mDefaultTemplate)); - summaryPreference.setWifiMode(/* isWifiMode */ false, /* usagePeriod */ null); + summaryPreference.setWifiMode(/* isWifiMode */ false, + /* usagePeriod */ null, /* isSingleWifi */ false); } else { info = mDataUsageController.getDataUsageInfo( NetworkTemplate.buildTemplateWifiWildcard()); - summaryPreference.setWifiMode(/* isWifiMode */ true, /* usagePeriod */ info.period); + summaryPreference.setWifiMode(/* isWifiMode */ true, /* usagePeriod */ + info.period, /* isSingleWifi */ false); summaryPreference.setLimitInfo(null); summaryPreference.setUsageNumbers(info.usageLevel, /* dataPlanSize */ -1L, @@ -215,18 +224,18 @@ public class DataUsageSummaryPreferenceController extends BasePreferenceControll } if (info.warningLevel > 0 && info.limitLevel > 0) { - summaryPreference.setLimitInfo(TextUtils.expandTemplate( - mContext.getText(R.string.cell_data_warning_and_limit), - DataUsageUtils.formatDataUsage(mContext, info.warningLevel), - DataUsageUtils.formatDataUsage(mContext, info.limitLevel)).toString()); + summaryPreference.setLimitInfo(TextUtils.expandTemplate( + mContext.getText(R.string.cell_data_warning_and_limit), + DataUsageUtils.formatDataUsage(mContext, info.warningLevel), + DataUsageUtils.formatDataUsage(mContext, info.limitLevel))); } else if (info.warningLevel > 0) { - summaryPreference.setLimitInfo(TextUtils.expandTemplate( - mContext.getText(R.string.cell_data_warning), - DataUsageUtils.formatDataUsage(mContext, info.warningLevel)).toString()); + summaryPreference.setLimitInfo(TextUtils.expandTemplate( + mContext.getText(R.string.cell_data_warning), + DataUsageUtils.formatDataUsage(mContext, info.warningLevel))); } else if (info.limitLevel > 0) { summaryPreference.setLimitInfo(TextUtils.expandTemplate( mContext.getText(R.string.cell_data_limit), - DataUsageUtils.formatDataUsage(mContext, info.limitLevel)).toString()); + DataUsageUtils.formatDataUsage(mContext, info.limitLevel))); } else { summaryPreference.setLimitInfo(null); } @@ -258,12 +267,18 @@ public class DataUsageSummaryPreferenceController extends BasePreferenceControll mCycleEnd = info.cycleEnd; mSnapshotTime = -1L; - final int defaultSubId = SubscriptionManager.getDefaultSubscriptionId(); - final SubscriptionInfo subInfo = mSubscriptionManager.getDefaultDataSubscriptionInfo(); + SubscriptionInfo subInfo = mSubscriptionManager.getActiveSubscriptionInfo(mSubscriptionId); + if (subInfo == null) { + subInfo = mSubscriptionManager.getAllSubscriptionInfoList().stream().filter( + i -> i.getSubscriptionId() == mSubscriptionId).findFirst().orElse(null); + } if (subInfo != null && mHasMobileData) { mCarrierName = subInfo.getCarrierName(); - List<SubscriptionPlan> plans = mSubscriptionManager.getSubscriptionPlans(defaultSubId); - final SubscriptionPlan primaryPlan = getPrimaryPlan(mSubscriptionManager, defaultSubId); + List<SubscriptionPlan> plans = mSubscriptionManager.getSubscriptionPlans( + mSubscriptionId); + final SubscriptionPlan primaryPlan = getPrimaryPlan(mSubscriptionManager, + mSubscriptionId); + if (primaryPlan != null) { mDataplanCount = plans.size(); mDataplanSize = primaryPlan.getDataLimitBytes(); @@ -282,8 +297,8 @@ public class DataUsageSummaryPreferenceController extends BasePreferenceControll } } mManageSubscriptionIntent = - mSubscriptionManager.createManageSubscriptionIntent(defaultSubId); - Log.i(TAG, "Have " + mDataplanCount + " plans, dflt sub-id " + defaultSubId + mSubscriptionManager.createManageSubscriptionIntent(mSubscriptionId); + Log.i(TAG, "Have " + mDataplanCount + " plans, dflt sub-id " + mSubscriptionId + ", intent " + mManageSubscriptionIntent); } diff --git a/src/com/android/settings/datausage/DataUsageUtils.java b/src/com/android/settings/datausage/DataUsageUtils.java index 803bc37589..bc9499b55d 100644 --- a/src/com/android/settings/datausage/DataUsageUtils.java +++ b/src/com/android/settings/datausage/DataUsageUtils.java @@ -14,16 +14,16 @@ package com.android.settings.datausage; +import static android.net.ConnectivityManager.TYPE_MOBILE; import static android.net.ConnectivityManager.TYPE_WIFI; +import static android.telephony.TelephonyManager.SIM_STATE_READY; +import android.app.usage.NetworkStats.Bucket; +import android.app.usage.NetworkStatsManager; import android.content.Context; import android.net.ConnectivityManager; -import android.net.INetworkStatsService; -import android.net.INetworkStatsSession; import android.net.NetworkTemplate; -import android.net.TrafficStats; import android.os.RemoteException; -import android.os.ServiceManager; import android.os.SystemProperties; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; @@ -31,16 +31,20 @@ import android.telephony.TelephonyManager; import android.text.BidiFormatter; import android.text.format.Formatter; import android.text.format.Formatter.BytesResult; +import android.util.Log; +import java.util.ArrayList; import java.util.List; /** * Utility methods for data usage classes. */ -public final class DataUsageUtils { +public final class DataUsageUtils extends com.android.settingslib.net.DataUsageUtils { static final boolean TEST_RADIOS = false; static final String TEST_RADIOS_PROP = "test.radios"; + private static final boolean LOGD = false; private static final String ETHERNET = "ethernet"; + private static final String TAG = "DataUsageUtils"; private DataUsageUtils() { } @@ -64,28 +68,25 @@ public final class DataUsageUtils { } final ConnectivityManager conn = ConnectivityManager.from(context); - final boolean hasEthernet = conn.isNetworkSupported(ConnectivityManager.TYPE_ETHERNET); + if (!conn.isNetworkSupported(ConnectivityManager.TYPE_ETHERNET)) { + return false; + } - final long ethernetBytes; + final TelephonyManager telephonyManager = TelephonyManager.from(context); + final NetworkStatsManager networkStatsManager = + context.getSystemService(NetworkStatsManager.class); + boolean hasEthernetUsage = false; try { - INetworkStatsService statsService = INetworkStatsService.Stub.asInterface( - ServiceManager.getService(Context.NETWORK_STATS_SERVICE)); - - INetworkStatsSession statsSession = statsService.openSession(); - if (statsSession != null) { - ethernetBytes = statsSession.getSummaryForNetwork( - NetworkTemplate.buildTemplateEthernet(), Long.MIN_VALUE, Long.MAX_VALUE) - .getTotalBytes(); - TrafficStats.closeQuietly(statsSession); - } else { - ethernetBytes = 0; + final Bucket bucket = networkStatsManager.querySummaryForUser( + ConnectivityManager.TYPE_ETHERNET, telephonyManager.getSubscriberId(), + 0L /* startTime */, System.currentTimeMillis() /* endTime */); + if (bucket != null) { + hasEthernetUsage = bucket.getRxBytes() > 0 || bucket.getTxBytes() > 0; } } catch (RemoteException e) { - throw new RuntimeException(e); + Log.e(TAG, "Exception querying network detail.", e); } - - // only show ethernet when both hardware present and traffic has occurred - return hasEthernet && ethernetBytes > 0; + return hasEthernetUsage; } /** @@ -99,6 +100,42 @@ public final class DataUsageUtils { } /** + * Test if device has a mobile data radio with SIM in ready state. + */ + public static boolean hasReadyMobileRadio(Context context) { + if (DataUsageUtils.TEST_RADIOS) { + return SystemProperties.get(DataUsageUtils.TEST_RADIOS_PROP).contains("mobile"); + } + final List<SubscriptionInfo> subInfoList = + SubscriptionManager.from(context).getActiveSubscriptionInfoList(true); + // No activated Subscriptions + if (subInfoList == null) { + if (LOGD) { + Log.d(TAG, "hasReadyMobileRadio: subInfoList=null"); + } + return false; + } + final TelephonyManager tele = TelephonyManager.from(context); + // require both supported network and ready SIM + boolean isReady = true; + for (SubscriptionInfo subInfo : subInfoList) { + isReady = isReady & tele.getSimState(subInfo.getSimSlotIndex()) == SIM_STATE_READY; + if (LOGD) { + Log.d(TAG, "hasReadyMobileRadio: subInfo=" + subInfo); + } + } + final ConnectivityManager conn = ConnectivityManager.from(context); + final boolean retVal = conn.isNetworkSupported(TYPE_MOBILE) && isReady; + if (LOGD) { + Log.d(TAG, "hasReadyMobileRadio:" + + " conn.isNetworkSupported(TYPE_MOBILE)=" + + conn.isNetworkSupported(TYPE_MOBILE) + + " isReady=" + isReady); + } + return retVal; + } + + /** * Whether device has a Wi-Fi data radio. */ public static boolean hasWifiRadio(Context context) { @@ -106,8 +143,7 @@ public final class DataUsageUtils { return SystemProperties.get(TEST_RADIOS_PROP).contains("wifi"); } - ConnectivityManager connectivityManager = - context.getSystemService(ConnectivityManager.class); + final ConnectivityManager connectivityManager = ConnectivityManager.from(context); return connectivityManager != null && connectivityManager.isNetworkSupported(TYPE_WIFI); } @@ -143,17 +179,14 @@ public final class DataUsageUtils { * Returns the default network template based on the availability of mobile data, Wifi. Returns * ethernet template if both mobile data and Wifi are not available. */ - static NetworkTemplate getDefaultTemplate(Context context, int defaultSubId) { + public static NetworkTemplate getDefaultTemplate(Context context, int defaultSubId) { if (hasMobileData(context) && defaultSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { - TelephonyManager telephonyManager = TelephonyManager.from(context); - NetworkTemplate mobileAll = NetworkTemplate.buildTemplateMobileAll( - telephonyManager.getSubscriberId(defaultSubId)); - return NetworkTemplate.normalize(mobileAll, - telephonyManager.getMergedSubscriberIds()); + return getMobileTemplate(context, defaultSubId); } else if (hasWifiRadio(context)) { return NetworkTemplate.buildTemplateWifiWildcard(); } else { return NetworkTemplate.buildTemplateEthernet(); } } + } diff --git a/src/com/android/settings/datausage/NetworkRestrictionsPreference.java b/src/com/android/settings/datausage/NetworkRestrictionsPreference.java index e4c6beddcc..9afc0c32ab 100644 --- a/src/com/android/settings/datausage/NetworkRestrictionsPreference.java +++ b/src/com/android/settings/datausage/NetworkRestrictionsPreference.java @@ -16,9 +16,10 @@ package com.android.settings.datausage; import android.content.Context; import android.net.NetworkTemplate; -import androidx.preference.Preference; import android.util.AttributeSet; +import androidx.preference.Preference; + public class NetworkRestrictionsPreference extends Preference implements TemplatePreference { public NetworkRestrictionsPreference(Context context, AttributeSet attrs) { diff --git a/src/com/android/settings/datausage/SpinnerPreference.java b/src/com/android/settings/datausage/SpinnerPreference.java index c0355b1445..67298a14bb 100644 --- a/src/com/android/settings/datausage/SpinnerPreference.java +++ b/src/com/android/settings/datausage/SpinnerPreference.java @@ -15,13 +15,15 @@ package com.android.settings.datausage; import android.content.Context; -import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; import android.util.AttributeSet; import android.view.View; import android.widget.AdapterView; -import android.widget.Spinner; + +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + import com.android.settings.R; +import com.android.settingslib.widget.settingsspinner.SettingsSpinner; public class SpinnerPreference extends Preference implements CycleAdapter.SpinnerInterface { @@ -61,7 +63,7 @@ public class SpinnerPreference extends Preference implements CycleAdapter.Spinne @Override public void onBindViewHolder(PreferenceViewHolder holder) { super.onBindViewHolder(holder); - Spinner spinner = (Spinner) holder.findViewById(R.id.cycles_spinner); + SettingsSpinner spinner = (SettingsSpinner) holder.findViewById(R.id.cycles_spinner); spinner.setAdapter(mAdapter); spinner.setSelection(mPosition); spinner.setOnItemSelectedListener(mOnSelectedListener); diff --git a/src/com/android/settings/datausage/TemplatePreference.java b/src/com/android/settings/datausage/TemplatePreference.java index 4b1cd0cf82..158c3b67c2 100644 --- a/src/com/android/settings/datausage/TemplatePreference.java +++ b/src/com/android/settings/datausage/TemplatePreference.java @@ -19,9 +19,9 @@ import android.net.NetworkPolicyManager; import android.net.NetworkTemplate; import android.os.INetworkManagementService; import android.os.UserManager; -import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; + import com.android.settingslib.NetworkPolicyEditor; public interface TemplatePreference { diff --git a/src/com/android/settings/datausage/TemplatePreferenceCategory.java b/src/com/android/settings/datausage/TemplatePreferenceCategory.java index 814a259547..d26b9b18ae 100644 --- a/src/com/android/settings/datausage/TemplatePreferenceCategory.java +++ b/src/com/android/settings/datausage/TemplatePreferenceCategory.java @@ -16,9 +16,10 @@ package com.android.settings.datausage; import android.content.Context; import android.net.NetworkTemplate; +import android.util.AttributeSet; + import androidx.preference.Preference; import androidx.preference.PreferenceCategory; -import android.util.AttributeSet; public class TemplatePreferenceCategory extends PreferenceCategory implements TemplatePreference { diff --git a/src/com/android/settings/datausage/UnrestrictedDataAccess.java b/src/com/android/settings/datausage/UnrestrictedDataAccess.java index 7aa716c491..d40537f52d 100644 --- a/src/com/android/settings/datausage/UnrestrictedDataAccess.java +++ b/src/com/android/settings/datausage/UnrestrictedDataAccess.java @@ -14,64 +14,43 @@ package com.android.settings.datausage; -import static com.android.settingslib.RestrictedLockUtils.checkIfMeteredDataRestricted; - -import android.app.Application; +import android.app.settings.SettingsEnums; import android.content.Context; import android.os.Bundle; -import android.os.UserHandle; -import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; +import android.provider.SearchIndexableResource; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; -import com.android.settings.SettingsPreferenceFragment; -import com.android.settings.applications.AppStateBaseBridge; -import com.android.settings.applications.appinfo.AppInfoDashboardFragment; -import com.android.settings.datausage.AppStateDataUsageBridge.DataUsageState; -import com.android.settings.overlay.FeatureFactory; -import com.android.settings.widget.AppSwitchPreference; -import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; -import com.android.settingslib.RestrictedPreferenceHelper; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; import com.android.settingslib.applications.ApplicationsState; -import com.android.settingslib.applications.ApplicationsState.AppEntry; import com.android.settingslib.applications.ApplicationsState.AppFilter; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; +import java.util.List; + +@SearchIndexable +public class UnrestrictedDataAccess extends DashboardFragment { -public class UnrestrictedDataAccess extends SettingsPreferenceFragment - implements ApplicationsState.Callbacks, AppStateBaseBridge.Callback, - Preference.OnPreferenceChangeListener { + private static final String TAG = "UnrestrictedDataAccess"; private static final int MENU_SHOW_SYSTEM = Menu.FIRST + 42; private static final String EXTRA_SHOW_SYSTEM = "show_system"; - private ApplicationsState mApplicationsState; - private AppStateDataUsageBridge mDataUsageBridge; - private ApplicationsState.Session mSession; - private DataSaverBackend mDataSaverBackend; private boolean mShowSystem; - private boolean mExtraLoaded; private AppFilter mFilter; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); - setAnimationAllowed(true); - mApplicationsState = ApplicationsState.getInstance( - (Application) getContext().getApplicationContext()); - mDataSaverBackend = new DataSaverBackend(getContext()); - mDataUsageBridge = new AppStateDataUsageBridge(mApplicationsState, this, mDataSaverBackend); - mSession = mApplicationsState.newSession(this, getLifecycle()); mShowSystem = icicle != null && icicle.getBoolean(EXTRA_SHOW_SYSTEM); - mFilter = mShowSystem ? ApplicationsState.FILTER_ALL_ENABLED - : ApplicationsState.FILTER_DOWNLOADED_AND_LAUNCHER; - setHasOptionsMenu(true); + + use(UnrestrictedDataAccessPreferenceController.class).setParentFragment(this); } @Override @@ -89,9 +68,10 @@ public class UnrestrictedDataAccess extends SettingsPreferenceFragment item.setTitle(mShowSystem ? R.string.menu_hide_system : R.string.menu_show_system); mFilter = mShowSystem ? ApplicationsState.FILTER_ALL_ENABLED : ApplicationsState.FILTER_DOWNLOADED_AND_LAUNCHER; - if (mExtraLoaded) { - rebuild(); - } + + use(UnrestrictedDataAccessPreferenceController.class).setFilter(mFilter); + use(UnrestrictedDataAccessPreferenceController.class).rebuild(); + break; } return super.onOptionsItemSelected(item); @@ -106,31 +86,15 @@ public class UnrestrictedDataAccess extends SettingsPreferenceFragment @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - setLoading(true, false); - } - - @Override - public void onResume() { - super.onResume(); - mDataUsageBridge.resume(); } @Override - public void onPause() { - super.onPause(); - mDataUsageBridge.pause(); - } - - @Override - public void onDestroy() { - super.onDestroy(); - mDataUsageBridge.release(); - } - - @Override - public void onExtraInfoUpdated() { - mExtraLoaded = true; - rebuild(); + public void onAttach(Context context) { + super.onAttach(context); + mFilter = mShowSystem ? ApplicationsState.FILTER_ALL_ENABLED + : ApplicationsState.FILTER_DOWNLOADED_AND_LAUNCHER; + use(UnrestrictedDataAccessPreferenceController.class).setSession(getSettingsLifecycle()); + use(UnrestrictedDataAccessPreferenceController.class).setFilter(mFilter); } @Override @@ -138,79 +102,14 @@ public class UnrestrictedDataAccess extends SettingsPreferenceFragment return R.string.help_url_unrestricted_data_access; } - private void rebuild() { - ArrayList<AppEntry> apps = mSession.rebuild(mFilter, ApplicationsState.ALPHA_COMPARATOR); - if (apps != null) { - onRebuildComplete(apps); - } - } - @Override - public void onRunningStateChanged(boolean running) { - - } - - @Override - public void onPackageListChanged() { - - } - - @Override - public void onRebuildComplete(ArrayList<AppEntry> apps) { - if (getContext() == null) return; - cacheRemoveAllPrefs(getPreferenceScreen()); - final int N = apps.size(); - for (int i = 0; i < N; i++) { - AppEntry entry = apps.get(i); - if (!shouldAddPreference(entry)) { - continue; - } - String key = entry.info.packageName + "|" + entry.info.uid; - AccessPreference preference = (AccessPreference) getCachedPreference(key); - if (preference == null) { - preference = new AccessPreference(getPrefContext(), entry); - preference.setKey(key); - preference.setOnPreferenceChangeListener(this); - getPreferenceScreen().addPreference(preference); - } else { - preference.setDisabledByAdmin(checkIfMeteredDataRestricted(getContext(), - entry.info.packageName, UserHandle.getUserId(entry.info.uid))); - preference.reuse(); - } - preference.setOrder(i); - } - setLoading(false, true); - removeCachedPrefs(getPreferenceScreen()); - } - - @Override - public void onPackageIconChanged() { - - } - - @Override - public void onPackageSizeChanged(String packageName) { - - } - - @Override - public void onAllSizesComputed() { - - } - - @Override - public void onLauncherInfoChanged() { - - } - - @Override - public void onLoadEntriesCompleted() { - + protected String getLogTag() { + return TAG; } @Override public int getMetricsCategory() { - return MetricsEvent.DATA_USAGE_UNRESTRICTED_ACCESS; + return SettingsEnums.DATA_USAGE_UNRESTRICTED_ACCESS; } @Override @@ -218,172 +117,17 @@ public class UnrestrictedDataAccess extends SettingsPreferenceFragment return R.xml.unrestricted_data_access_settings; } - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - if (preference instanceof AccessPreference) { - AccessPreference accessPreference = (AccessPreference) preference; - boolean whitelisted = newValue == Boolean.TRUE; - logSpecialPermissionChange(whitelisted, accessPreference.mEntry.info.packageName); - mDataSaverBackend.setIsWhitelisted(accessPreference.mEntry.info.uid, - accessPreference.mEntry.info.packageName, whitelisted); - accessPreference.mState.isDataSaverWhitelisted = whitelisted; - return true; - } - return false; - } - - @VisibleForTesting - void logSpecialPermissionChange(boolean whitelisted, String packageName) { - int logCategory = whitelisted ? MetricsEvent.APP_SPECIAL_PERMISSION_UNL_DATA_ALLOW - : MetricsEvent.APP_SPECIAL_PERMISSION_UNL_DATA_DENY; - FeatureFactory.getFactory(getContext()).getMetricsFeatureProvider().action(getContext(), - logCategory, packageName); - } - - @VisibleForTesting - boolean shouldAddPreference(AppEntry app) { - return app != null && UserHandle.isApp(app.info.uid); - } - - @VisibleForTesting - class AccessPreference extends AppSwitchPreference - implements DataSaverBackend.Listener { - private final AppEntry mEntry; - private final DataUsageState mState; - private final RestrictedPreferenceHelper mHelper; - - public AccessPreference(final Context context, AppEntry entry) { - super(context); - setWidgetLayoutResource(R.layout.restricted_switch_widget); - mHelper = new RestrictedPreferenceHelper(context, this, null); - mEntry = entry; - mState = (DataUsageState) mEntry.extraInfo; - mEntry.ensureLabel(getContext()); - setDisabledByAdmin(checkIfMeteredDataRestricted(context, entry.info.packageName, - UserHandle.getUserId(entry.info.uid))); - setState(); - if (mEntry.icon != null) { - setIcon(mEntry.icon); - } - } - - @Override - public void onAttached() { - super.onAttached(); - mDataSaverBackend.addListener(this); - } - - @Override - public void onDetached() { - mDataSaverBackend.remListener(this); - super.onDetached(); - } - - @Override - protected void onClick() { - if (mState.isDataSaverBlacklisted) { - // app is blacklisted, launch App Data Usage screen - AppInfoDashboardFragment.startAppInfoFragment(AppDataUsage.class, - R.string.app_data_usage, - null /* arguments */, - UnrestrictedDataAccess.this, - mEntry); - } else { - // app is not blacklisted, let superclass handle toggle switch - super.onClick(); - } - } - - @Override - public void performClick() { - if (!mHelper.performClick()) { - super.performClick(); - } - } + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, + boolean enabled) { + final ArrayList<SearchIndexableResource> result = new ArrayList<>(); - // Sets UI state based on whitelist/blacklist status. - private void setState() { - setTitle(mEntry.label); - if (mState != null) { - setChecked(mState.isDataSaverWhitelisted); - if (isDisabledByAdmin()) { - setSummary(R.string.disabled_by_admin); - } else if (mState.isDataSaverBlacklisted) { - setSummary(R.string.restrict_background_blacklisted); - } else { - setSummary(""); + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.unrestricted_data_access_settings; + result.add(sir); + return result; } - } - } - - public void reuse() { - setState(); - notifyChanged(); - } - - @Override - public void onBindViewHolder(PreferenceViewHolder holder) { - if (mEntry.icon == null) { - holder.itemView.post(new Runnable() { - @Override - public void run() { - // Ensure we have an icon before binding. - mApplicationsState.ensureIcon(mEntry); - // This might trigger us to bind again, but it gives an easy way to only - // load the icon once its needed, so its probably worth it. - setIcon(mEntry.icon); - } - }); - } - final boolean disabledByAdmin = isDisabledByAdmin(); - final View widgetFrame = holder.findViewById(android.R.id.widget_frame); - if (disabledByAdmin) { - widgetFrame.setVisibility(View.VISIBLE); - } else { - widgetFrame.setVisibility(mState != null && mState.isDataSaverBlacklisted - ? View.INVISIBLE : View.VISIBLE); - } - super.onBindViewHolder(holder); - - mHelper.onBindViewHolder(holder); - holder.findViewById(R.id.restricted_icon).setVisibility( - disabledByAdmin ? View.VISIBLE : View.GONE); - holder.findViewById(android.R.id.switch_widget).setVisibility( - disabledByAdmin ? View.GONE : View.VISIBLE); - } - - @Override - public void onDataSaverChanged(boolean isDataSaving) { - } - - @Override - public void onWhitelistStatusChanged(int uid, boolean isWhitelisted) { - if (mState != null && mEntry.info.uid == uid) { - mState.isDataSaverWhitelisted = isWhitelisted; - reuse(); - } - } - - @Override - public void onBlacklistStatusChanged(int uid, boolean isBlacklisted) { - if (mState != null && mEntry.info.uid == uid) { - mState.isDataSaverBlacklisted = isBlacklisted; - reuse(); - } - } - - public void setDisabledByAdmin(EnforcedAdmin admin) { - mHelper.setDisabledByAdmin(admin); - } - - public boolean isDisabledByAdmin() { - return mHelper.isDisabledByAdmin(); - } - - @VisibleForTesting - public AppEntry getEntryForTest() { - return mEntry; - } - } - + }; } diff --git a/src/com/android/settings/datausage/UnrestrictedDataAccessPreference.java b/src/com/android/settings/datausage/UnrestrictedDataAccessPreference.java new file mode 100644 index 0000000000..97ed5ac939 --- /dev/null +++ b/src/com/android/settings/datausage/UnrestrictedDataAccessPreference.java @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2018 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.datausage; + +import static com.android.settingslib.RestrictedLockUtilsInternal.checkIfMeteredDataRestricted; + +import android.content.Context; +import android.os.UserHandle; +import android.view.View; + +import androidx.preference.PreferenceViewHolder; + +import com.android.settings.R; +import com.android.settings.applications.appinfo.AppInfoDashboardFragment; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.widget.AppSwitchPreference; +import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; +import com.android.settingslib.RestrictedPreferenceHelper; +import com.android.settingslib.applications.ApplicationsState; +import com.android.settingslib.applications.ApplicationsState.AppEntry; + +public class UnrestrictedDataAccessPreference extends AppSwitchPreference implements + DataSaverBackend.Listener { + + private final ApplicationsState mApplicationsState; + private final AppEntry mEntry; + private final AppStateDataUsageBridge.DataUsageState mDataUsageState; + private final DataSaverBackend mDataSaverBackend; + private final DashboardFragment mParentFragment; + private final RestrictedPreferenceHelper mHelper; + + public UnrestrictedDataAccessPreference(final Context context, AppEntry entry, + ApplicationsState applicationsState, DataSaverBackend dataSaverBackend, + DashboardFragment parentFragment) { + super(context); + setWidgetLayoutResource(R.layout.restricted_switch_widget); + mHelper = new RestrictedPreferenceHelper(context, this, null); + mEntry = entry; + mDataUsageState = (AppStateDataUsageBridge.DataUsageState) mEntry.extraInfo; + mEntry.ensureLabel(context); + mApplicationsState = applicationsState; + mDataSaverBackend = dataSaverBackend; + mParentFragment = parentFragment; + setDisabledByAdmin(checkIfMeteredDataRestricted(context, entry.info.packageName, + UserHandle.getUserId(entry.info.uid))); + updateState(); + setKey(generateKey(mEntry)); + if (mEntry.icon != null) { + setIcon(mEntry.icon); + } + } + + static String generateKey(final AppEntry entry) { + return entry.info.packageName + "|" + entry.info.uid; + } + + @Override + public void onAttached() { + super.onAttached(); + mDataSaverBackend.addListener(this); + } + + @Override + public void onDetached() { + mDataSaverBackend.remListener(this); + super.onDetached(); + } + + @Override + protected void onClick() { + if (mDataUsageState.isDataSaverBlacklisted) { + // app is blacklisted, launch App Data Usage screen + AppInfoDashboardFragment.startAppInfoFragment(AppDataUsage.class, + R.string.data_usage_app_summary_title, + null /* arguments */, + mParentFragment, + mEntry); + } else { + // app is not blacklisted, let superclass handle toggle switch + super.onClick(); + } + } + + @Override + public void performClick() { + if (!mHelper.performClick()) { + super.performClick(); + } + } + + @Override + public void onBindViewHolder(PreferenceViewHolder holder) { + if (mEntry.icon == null) { + holder.itemView.post(new Runnable() { + @Override + public void run() { + // Ensure we have an icon before binding. + mApplicationsState.ensureIcon(mEntry); + // This might trigger us to bind again, but it gives an easy way to only + // load the icon once its needed, so its probably worth it. + setIcon(mEntry.icon); + } + }); + } + final boolean disabledByAdmin = isDisabledByAdmin(); + final View widgetFrame = holder.findViewById(android.R.id.widget_frame); + if (disabledByAdmin) { + widgetFrame.setVisibility(View.VISIBLE); + } else { + widgetFrame.setVisibility( + mDataUsageState != null && mDataUsageState.isDataSaverBlacklisted + ? View.INVISIBLE : View.VISIBLE); + } + super.onBindViewHolder(holder); + + mHelper.onBindViewHolder(holder); + holder.findViewById(R.id.restricted_icon).setVisibility( + disabledByAdmin ? View.VISIBLE : View.GONE); + holder.findViewById(android.R.id.switch_widget).setVisibility( + disabledByAdmin ? View.GONE : View.VISIBLE); + } + + @Override + public void onDataSaverChanged(boolean isDataSaving) { + } + + @Override + public void onWhitelistStatusChanged(int uid, boolean isWhitelisted) { + if (mDataUsageState != null && mEntry.info.uid == uid) { + mDataUsageState.isDataSaverWhitelisted = isWhitelisted; + updateState(); + } + } + + @Override + public void onBlacklistStatusChanged(int uid, boolean isBlacklisted) { + if (mDataUsageState != null && mEntry.info.uid == uid) { + mDataUsageState.isDataSaverBlacklisted = isBlacklisted; + updateState(); + } + } + + public AppStateDataUsageBridge.DataUsageState getDataUsageState() { + return mDataUsageState; + } + + public AppEntry getEntry() { + return mEntry; + } + + public boolean isDisabledByAdmin() { + return mHelper.isDisabledByAdmin(); + } + + public void setDisabledByAdmin(EnforcedAdmin admin) { + mHelper.setDisabledByAdmin(admin); + } + + // Sets UI state based on whitelist/blacklist status. + public void updateState() { + setTitle(mEntry.label); + if (mDataUsageState != null) { + setChecked(mDataUsageState.isDataSaverWhitelisted); + if (isDisabledByAdmin()) { + setSummary(R.string.disabled_by_admin); + } else if (mDataUsageState.isDataSaverBlacklisted) { + setSummary(R.string.restrict_background_blacklisted); + } else { + setSummary(""); + } + } + notifyChanged(); + } +} diff --git a/src/com/android/settings/datausage/UnrestrictedDataAccessPreferenceController.java b/src/com/android/settings/datausage/UnrestrictedDataAccessPreferenceController.java new file mode 100644 index 0000000000..d8656caa3b --- /dev/null +++ b/src/com/android/settings/datausage/UnrestrictedDataAccessPreferenceController.java @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2018 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.datausage; + +import static com.android.settingslib.RestrictedLockUtilsInternal.checkIfMeteredDataRestricted; + +import android.app.Application; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.os.UserHandle; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.applications.AppStateBaseBridge; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.applications.ApplicationsState; +import com.android.settingslib.applications.ApplicationsState.AppEntry; +import com.android.settingslib.applications.ApplicationsState.AppFilter; +import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnDestroy; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.core.lifecycle.events.OnStop; + +import java.util.ArrayList; +import java.util.Set; +import java.util.TreeSet; + + +public class UnrestrictedDataAccessPreferenceController extends BasePreferenceController implements + LifecycleObserver, OnStart, OnStop, OnDestroy, ApplicationsState.Callbacks, + AppStateBaseBridge.Callback, Preference.OnPreferenceChangeListener { + + private final ApplicationsState mApplicationsState; + private final AppStateDataUsageBridge mDataUsageBridge; + private final DataSaverBackend mDataSaverBackend; + private ApplicationsState.Session mSession; + private AppFilter mFilter; + private DashboardFragment mParentFragment; + private PreferenceScreen mScreen; + private boolean mExtraLoaded; + + public UnrestrictedDataAccessPreferenceController(Context context, String key) { + super(context, key); + mApplicationsState = ApplicationsState.getInstance( + (Application) context.getApplicationContext()); + mDataSaverBackend = new DataSaverBackend(context); + mDataUsageBridge = new AppStateDataUsageBridge(mApplicationsState, this, mDataSaverBackend); + } + + public void setFilter(AppFilter filter) { + mFilter = filter; + } + + public void setParentFragment(DashboardFragment parentFragment) { + mParentFragment = parentFragment; + } + + public void setSession(Lifecycle lifecycle) { + mSession = mApplicationsState.newSession(this, lifecycle); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mScreen = screen; + } + + @Override + public int getAvailabilityStatus() { + return mContext.getResources().getBoolean(R.bool.config_show_data_saver) + ? AVAILABLE_UNSEARCHABLE + : UNSUPPORTED_ON_DEVICE; + } + + @Override + public void onStart() { + mDataUsageBridge.resume(); + } + + @Override + public void onStop() { + mDataUsageBridge.pause(); + } + + @Override + public void onDestroy() { + mDataUsageBridge.release(); + } + + @Override + public void onExtraInfoUpdated() { + mExtraLoaded = true; + rebuild(); + } + + @Override + public void onRunningStateChanged(boolean running) { + + } + + @Override + public void onPackageListChanged() { + + } + + @Override + public void onRebuildComplete(ArrayList<AppEntry> apps) { + if (apps == null) { + return; + } + + // Create apps key set for removing useless preferences + final Set<String> appsKeySet = new TreeSet<>(); + // Add or update preferences + final int N = apps.size(); + for (int i = 0; i < N; i++) { + final AppEntry entry = apps.get(i); + if (!shouldAddPreference(entry)) { + continue; + } + final String prefkey = UnrestrictedDataAccessPreference.generateKey(entry); + appsKeySet.add(prefkey); + UnrestrictedDataAccessPreference preference = + (UnrestrictedDataAccessPreference) mScreen.findPreference(prefkey); + if (preference == null) { + preference = new UnrestrictedDataAccessPreference(mScreen.getContext(), entry, + mApplicationsState, mDataSaverBackend, mParentFragment); + preference.setOnPreferenceChangeListener(this); + mScreen.addPreference(preference); + } else { + preference.setDisabledByAdmin(checkIfMeteredDataRestricted(mContext, + entry.info.packageName, UserHandle.getUserId(entry.info.uid))); + preference.updateState(); + } + preference.setOrder(i); + } + + // Remove useless preferences + removeUselessPrefs(appsKeySet); + } + + @Override + public void onPackageIconChanged() { + + } + + @Override + public void onPackageSizeChanged(String packageName) { + + } + + @Override + public void onAllSizesComputed() { + + } + + @Override + public void onLauncherInfoChanged() { + + } + + @Override + public void onLoadEntriesCompleted() { + + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + if (preference instanceof UnrestrictedDataAccessPreference) { + final UnrestrictedDataAccessPreference + accessPreference = (UnrestrictedDataAccessPreference) preference; + boolean whitelisted = newValue == Boolean.TRUE; + logSpecialPermissionChange(whitelisted, accessPreference.getEntry().info.packageName); + mDataSaverBackend.setIsWhitelisted(accessPreference.getEntry().info.uid, + accessPreference.getEntry().info.packageName, whitelisted); + accessPreference.getDataUsageState().isDataSaverWhitelisted = whitelisted; + return true; + } + return false; + } + + public void rebuild() { + if (!mExtraLoaded) { + return; + } + + final ArrayList<AppEntry> apps = mSession.rebuild(mFilter, + ApplicationsState.ALPHA_COMPARATOR); + if (apps != null) { + onRebuildComplete(apps); + } + } + + private void removeUselessPrefs(final Set<String> appsKeySet) { + final int prefCount = mScreen.getPreferenceCount(); + String prefKey; + if (prefCount > 0) { + for (int i = prefCount - 1; i >= 0; i--) { + Preference pref = mScreen.getPreference(i); + prefKey = pref.getKey(); + if (!appsKeySet.isEmpty() && appsKeySet.contains(prefKey)) { + continue; + } + mScreen.removePreference(pref); + } + } + } + + @VisibleForTesting + void logSpecialPermissionChange(boolean whitelisted, String packageName) { + final int logCategory = whitelisted ? SettingsEnums.APP_SPECIAL_PERMISSION_UNL_DATA_ALLOW + : SettingsEnums.APP_SPECIAL_PERMISSION_UNL_DATA_DENY; + FeatureFactory.getFactory(mContext).getMetricsFeatureProvider().action(mContext, + logCategory, packageName); + } + + @VisibleForTesting + static boolean shouldAddPreference(AppEntry app) { + return app != null && UserHandle.isApp(app.info.uid); + } +} diff --git a/src/com/android/settings/datausage/WifiDataUsageSummaryPreferenceController.java b/src/com/android/settings/datausage/WifiDataUsageSummaryPreferenceController.java new file mode 100644 index 0000000000..0551fc2391 --- /dev/null +++ b/src/com/android/settings/datausage/WifiDataUsageSummaryPreferenceController.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2019 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.datausage; + +import android.app.Activity; +import android.net.NetworkTemplate; +import android.telephony.SubscriptionManager; + +import androidx.preference.Preference; +import androidx.preference.PreferenceFragmentCompat; + +import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.net.DataUsageController; + +/** + * The controller displays a data usage chart for the specified Wi-Fi network. + */ +public class WifiDataUsageSummaryPreferenceController extends DataUsageSummaryPreferenceController { + final String mNetworkId; + + public WifiDataUsageSummaryPreferenceController(Activity activity, + Lifecycle lifecycle, PreferenceFragmentCompat fragment, CharSequence networkId) { + super(activity, lifecycle, fragment, SubscriptionManager.INVALID_SUBSCRIPTION_ID); + + if (networkId == null) { + mNetworkId = null; + } else { + mNetworkId = String.valueOf(networkId); + } + } + + @Override + public void updateState(Preference preference) { + if (preference == null) { + return; + } + + final DataUsageSummaryPreference mPreference = (DataUsageSummaryPreference) preference; + // TODO(b/126299427): Currently gets data usage of whole Wi-Fi networks, but should get + // specified one. + final NetworkTemplate template = NetworkTemplate.buildTemplateWifi(mNetworkId); + final DataUsageController.DataUsageInfo info = mDataUsageController.getDataUsageInfo( + template); + mDataInfoController.updateDataLimit(info, mPolicyEditor.getPolicy(template)); + + mPreference.setWifiMode(/* isWifiMode */ true, /* usagePeriod */ + info.period, /* isSingleWifi */ true); + mPreference.setChartEnabled(true); + // Treats Wi-Fi network as unlimited network, which has same usage level and limited level. + mPreference.setUsageNumbers(info.usageLevel, info.usageLevel, /* hasMobileData */ false); + + // TODO(b/126142293): Passpoint Wi-Fi should have limit of data usage and time remaining + mPreference.setProgress(100); + mPreference.setLabels(DataUsageUtils.formatDataUsage(mContext, /* sizeBytes */ 0), + DataUsageUtils.formatDataUsage(mContext, info.usageLevel)); + } +} diff --git a/src/com/android/settings/datetime/AutoTimeFormatPreferenceController.java b/src/com/android/settings/datetime/AutoTimeFormatPreferenceController.java index 42ea41a905..7b7e064066 100644 --- a/src/com/android/settings/datetime/AutoTimeFormatPreferenceController.java +++ b/src/com/android/settings/datetime/AutoTimeFormatPreferenceController.java @@ -19,13 +19,16 @@ package com.android.settings.datetime; import android.content.Context; import android.provider.Settings; import android.provider.Settings.System; -import androidx.preference.SwitchPreference; -import androidx.preference.Preference; -import androidx.preference.TwoStatePreference; import android.text.TextUtils; import android.text.format.DateFormat; + +import androidx.preference.Preference; +import androidx.preference.SwitchPreference; +import androidx.preference.TwoStatePreference; + import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.core.AbstractPreferenceController; + import java.util.Locale; public class AutoTimeFormatPreferenceController extends AbstractPreferenceController diff --git a/src/com/android/settings/datetime/AutoTimePreferenceController.java b/src/com/android/settings/datetime/AutoTimePreferenceController.java index 22c01ec780..bf6ecb418e 100644 --- a/src/com/android/settings/datetime/AutoTimePreferenceController.java +++ b/src/com/android/settings/datetime/AutoTimePreferenceController.java @@ -18,10 +18,12 @@ package com.android.settings.datetime; import android.content.Context; import android.provider.Settings; + import androidx.preference.Preference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.RestrictedSwitchPreference; import com.android.settingslib.core.AbstractPreferenceController; @@ -73,6 +75,6 @@ public class AutoTimePreferenceController extends AbstractPreferenceController } private RestrictedLockUtils.EnforcedAdmin getEnforcedAdminProperty() { - return RestrictedLockUtils.checkIfAutoTimeRequired(mContext); + return RestrictedLockUtilsInternal.checkIfAutoTimeRequired(mContext); } } diff --git a/src/com/android/settings/datetime/AutoTimeZonePreferenceController.java b/src/com/android/settings/datetime/AutoTimeZonePreferenceController.java index f9a4821fa8..4426bde99f 100644 --- a/src/com/android/settings/datetime/AutoTimeZonePreferenceController.java +++ b/src/com/android/settings/datetime/AutoTimeZonePreferenceController.java @@ -18,8 +18,9 @@ package com.android.settings.datetime; import android.content.Context; import android.provider.Settings; -import androidx.preference.SwitchPreference; + import androidx.preference.Preference; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.Utils; diff --git a/src/com/android/settings/datetime/DatePreferenceController.java b/src/com/android/settings/datetime/DatePreferenceController.java index 856722ca46..1704bd6feb 100644 --- a/src/com/android/settings/datetime/DatePreferenceController.java +++ b/src/com/android/settings/datetime/DatePreferenceController.java @@ -20,12 +20,13 @@ import android.app.Activity; import android.app.AlarmManager; import android.app.DatePickerDialog; import android.content.Context; -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; import android.text.TextUtils; import android.text.format.DateFormat; import android.widget.DatePicker; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; + import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.RestrictedPreference; import com.android.settingslib.core.AbstractPreferenceController; diff --git a/src/com/android/settings/datetime/TimeFormatPreferenceController.java b/src/com/android/settings/datetime/TimeFormatPreferenceController.java index 2be6b80afc..3ad879a279 100644 --- a/src/com/android/settings/datetime/TimeFormatPreferenceController.java +++ b/src/com/android/settings/datetime/TimeFormatPreferenceController.java @@ -19,12 +19,13 @@ package com.android.settings.datetime; import android.content.Context; import android.content.Intent; import android.provider.Settings; -import androidx.preference.SwitchPreference; -import androidx.preference.Preference; -import androidx.preference.TwoStatePreference; import android.text.TextUtils; import android.text.format.DateFormat; +import androidx.preference.Preference; +import androidx.preference.SwitchPreference; +import androidx.preference.TwoStatePreference; + import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.core.AbstractPreferenceController; diff --git a/src/com/android/settings/datetime/TimePreferenceController.java b/src/com/android/settings/datetime/TimePreferenceController.java index 58173711a9..3ca26bc8a7 100644 --- a/src/com/android/settings/datetime/TimePreferenceController.java +++ b/src/com/android/settings/datetime/TimePreferenceController.java @@ -20,11 +20,12 @@ import android.app.Activity; import android.app.AlarmManager; import android.app.TimePickerDialog; import android.content.Context; -import androidx.preference.Preference; import android.text.TextUtils; import android.text.format.DateFormat; import android.widget.TimePicker; +import androidx.preference.Preference; + import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.RestrictedPreference; import com.android.settingslib.core.AbstractPreferenceController; diff --git a/src/com/android/settings/datetime/TimeZonePreferenceController.java b/src/com/android/settings/datetime/TimeZonePreferenceController.java index 92169e6dec..a19f055921 100644 --- a/src/com/android/settings/datetime/TimeZonePreferenceController.java +++ b/src/com/android/settings/datetime/TimeZonePreferenceController.java @@ -17,9 +17,9 @@ package com.android.settings.datetime; import android.content.Context; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; -import android.util.FeatureFlagUtils; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.RestrictedPreference; diff --git a/src/com/android/settings/datetime/timezone/BaseTimeZoneAdapter.java b/src/com/android/settings/datetime/timezone/BaseTimeZoneAdapter.java index 1a868b8f29..66735c8a5e 100644 --- a/src/com/android/settings/datetime/timezone/BaseTimeZoneAdapter.java +++ b/src/com/android/settings/datetime/timezone/BaseTimeZoneAdapter.java @@ -17,11 +17,6 @@ package com.android.settings.datetime.timezone; import android.icu.text.BreakIterator; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import androidx.annotation.WorkerThread; -import androidx.recyclerview.widget.RecyclerView; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; @@ -29,6 +24,12 @@ import android.view.ViewGroup; import android.widget.Filter; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.annotation.WorkerThread; +import androidx.recyclerview.widget.RecyclerView; + import com.android.settings.R; import com.android.settings.datetime.timezone.BaseTimeZonePicker.OnListItemClickListener; @@ -76,9 +77,10 @@ public class BaseTimeZoneAdapter<T extends BaseTimeZoneAdapter.AdapterItem> @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { LayoutInflater inflater = LayoutInflater.from(parent.getContext()); - switch(viewType) { + switch (viewType) { case TYPE_HEADER: { - final View view = inflater.inflate(R.layout.preference_category_material_settings, + final View view = inflater.inflate( + R.layout.time_zone_search_header, parent, false); return new HeaderViewHolder(view); } @@ -135,7 +137,8 @@ public class BaseTimeZoneAdapter<T extends BaseTimeZoneAdapter.AdapterItem> return mShowHeader && position == 0; } - public @NonNull ArrayFilter getFilter() { + @NonNull + public ArrayFilter getFilter() { if (mFilter == null) { mFilter = new ArrayFilter(); } @@ -152,14 +155,18 @@ public class BaseTimeZoneAdapter<T extends BaseTimeZoneAdapter.AdapterItem> public interface AdapterItem { CharSequence getTitle(); + CharSequence getSummary(); + String getIconText(); + String getCurrentTime(); /** * @return unique non-negative number */ long getItemId(); + String[] getSearchKeys(); } diff --git a/src/com/android/settings/datetime/timezone/BaseTimeZoneInfoPicker.java b/src/com/android/settings/datetime/timezone/BaseTimeZoneInfoPicker.java index 83b85c0704..1ed851674e 100644 --- a/src/com/android/settings/datetime/timezone/BaseTimeZoneInfoPicker.java +++ b/src/com/android/settings/datetime/timezone/BaseTimeZoneInfoPicker.java @@ -23,6 +23,7 @@ import android.content.res.Resources; import android.icu.text.DateFormat; import android.icu.text.SimpleDateFormat; import android.icu.util.Calendar; + import androidx.annotation.Nullable; import com.android.settings.R; diff --git a/src/com/android/settings/datetime/timezone/BaseTimeZonePicker.java b/src/com/android/settings/datetime/timezone/BaseTimeZonePicker.java index 6d93ac91de..887b9f2af9 100644 --- a/src/com/android/settings/datetime/timezone/BaseTimeZonePicker.java +++ b/src/com/android/settings/datetime/timezone/BaseTimeZonePicker.java @@ -17,9 +17,6 @@ package com.android.settings.datetime.timezone; import android.os.Bundle; -import androidx.annotation.VisibleForTesting; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -30,7 +27,9 @@ import android.widget.LinearLayout; import android.widget.SearchView; import android.widget.TextView; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + import com.android.settings.R; import com.android.settings.core.InstrumentedFragment; import com.android.settings.datetime.timezone.model.TimeZoneData; diff --git a/src/com/android/settings/datetime/timezone/BaseTimeZonePreferenceController.java b/src/com/android/settings/datetime/timezone/BaseTimeZonePreferenceController.java index 3e731f6271..8a0d73708c 100644 --- a/src/com/android/settings/datetime/timezone/BaseTimeZonePreferenceController.java +++ b/src/com/android/settings/datetime/timezone/BaseTimeZonePreferenceController.java @@ -17,6 +17,7 @@ package com.android.settings.datetime.timezone; import android.content.Context; + import androidx.preference.Preference; import com.android.settings.core.BasePreferenceController; diff --git a/src/com/android/settings/datetime/timezone/FixedOffsetPicker.java b/src/com/android/settings/datetime/timezone/FixedOffsetPicker.java index f134051cf6..a0431cc735 100644 --- a/src/com/android/settings/datetime/timezone/FixedOffsetPicker.java +++ b/src/com/android/settings/datetime/timezone/FixedOffsetPicker.java @@ -16,9 +16,9 @@ package com.android.settings.datetime.timezone; +import android.app.settings.SettingsEnums; import android.icu.util.TimeZone; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.datetime.timezone.model.TimeZoneData; @@ -45,7 +45,7 @@ public class FixedOffsetPicker extends BaseTimeZoneInfoPicker { @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.SETTINGS_ZONE_PICKER_FIXED_OFFSET; + return SettingsEnums.SETTINGS_ZONE_PICKER_FIXED_OFFSET; } @Override diff --git a/src/com/android/settings/datetime/timezone/RegionPreferenceController.java b/src/com/android/settings/datetime/timezone/RegionPreferenceController.java index efb3e4def7..53959a6951 100644 --- a/src/com/android/settings/datetime/timezone/RegionPreferenceController.java +++ b/src/com/android/settings/datetime/timezone/RegionPreferenceController.java @@ -17,7 +17,6 @@ package com.android.settings.datetime.timezone; import android.content.Context; import android.icu.text.LocaleDisplayNames; -import androidx.preference.Preference; import java.util.Locale; diff --git a/src/com/android/settings/datetime/timezone/RegionSearchPicker.java b/src/com/android/settings/datetime/timezone/RegionSearchPicker.java index e660c33064..07986e29a9 100644 --- a/src/com/android/settings/datetime/timezone/RegionSearchPicker.java +++ b/src/com/android/settings/datetime/timezone/RegionSearchPicker.java @@ -17,14 +17,15 @@ package com.android.settings.datetime.timezone; import android.app.Activity; +import android.app.settings.SettingsEnums; import android.content.Intent; import android.icu.text.Collator; import android.icu.text.LocaleDisplayNames; import android.os.Bundle; -import androidx.annotation.VisibleForTesting; import android.util.Log; -import com.android.internal.logging.nano.MetricsProto; +import androidx.annotation.VisibleForTesting; + import com.android.settings.R; import com.android.settings.core.SubSettingLauncher; import com.android.settings.datetime.timezone.BaseTimeZoneAdapter.AdapterItem; @@ -53,7 +54,7 @@ public class RegionSearchPicker extends BaseTimeZonePicker { @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.SETTINGS_ZONE_PICKER_REGION; + return SettingsEnums.SETTINGS_ZONE_PICKER_REGION; } @Override diff --git a/src/com/android/settings/datetime/timezone/RegionZonePicker.java b/src/com/android/settings/datetime/timezone/RegionZonePicker.java index 7a3378acb5..8e4aa05532 100644 --- a/src/com/android/settings/datetime/timezone/RegionZonePicker.java +++ b/src/com/android/settings/datetime/timezone/RegionZonePicker.java @@ -16,16 +16,17 @@ package com.android.settings.datetime.timezone; +import android.app.settings.SettingsEnums; import android.content.Intent; import android.icu.text.Collator; import android.icu.text.LocaleDisplayNames; import android.icu.util.TimeZone; import android.os.Bundle; +import android.util.Log; + import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import android.util.Log; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.datetime.timezone.model.FilteredCountryTimeZones; import com.android.settings.datetime.timezone.model.TimeZoneData; @@ -54,7 +55,7 @@ public class RegionZonePicker extends BaseTimeZoneInfoPicker { @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.SETTINGS_ZONE_PICKER_TIME_ZONE; + return SettingsEnums.SETTINGS_ZONE_PICKER_TIME_ZONE; } @Override diff --git a/src/com/android/settings/datetime/timezone/RegionZonePreferenceController.java b/src/com/android/settings/datetime/timezone/RegionZonePreferenceController.java index 79539c3577..a297ce6085 100644 --- a/src/com/android/settings/datetime/timezone/RegionZonePreferenceController.java +++ b/src/com/android/settings/datetime/timezone/RegionZonePreferenceController.java @@ -17,6 +17,7 @@ package com.android.settings.datetime.timezone; import android.content.Context; + import androidx.preference.Preference; import com.android.settings.R; diff --git a/src/com/android/settings/datetime/timezone/TimeZoneInfoPreferenceController.java b/src/com/android/settings/datetime/timezone/TimeZoneInfoPreferenceController.java index 413458db90..a819f101a8 100644 --- a/src/com/android/settings/datetime/timezone/TimeZoneInfoPreferenceController.java +++ b/src/com/android/settings/datetime/timezone/TimeZoneInfoPreferenceController.java @@ -24,6 +24,7 @@ import android.icu.util.BasicTimeZone; import android.icu.util.Calendar; import android.icu.util.TimeZone; import android.icu.util.TimeZoneTransition; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; diff --git a/src/com/android/settings/datetime/timezone/TimeZoneSettings.java b/src/com/android/settings/datetime/timezone/TimeZoneSettings.java index 3548899658..d557bf93e6 100644 --- a/src/com/android/settings/datetime/timezone/TimeZoneSettings.java +++ b/src/com/android/settings/datetime/timezone/TimeZoneSettings.java @@ -18,19 +18,20 @@ package com.android.settings.datetime.timezone; import android.app.Activity; import android.app.AlarmManager; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.icu.util.TimeZone; import android.os.Bundle; -import androidx.annotation.VisibleForTesting; -import androidx.preference.PreferenceCategory; import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; -import com.android.internal.logging.nano.MetricsProto; +import androidx.annotation.VisibleForTesting; +import androidx.preference.PreferenceCategory; + import com.android.settings.R; import com.android.settings.core.SubSettingLauncher; import com.android.settings.dashboard.DashboardFragment; @@ -68,13 +69,14 @@ public class TimeZoneSettings extends DashboardFragment { private Locale mLocale; private boolean mSelectByRegion; private TimeZoneData mTimeZoneData; + private Intent mPendingZonePickerRequestResult; private String mSelectedTimeZoneId; private TimeZoneInfo.Formatter mTimeZoneInfoFormatter; @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.ZONE_PICKER; + return SettingsEnums.ZONE_PICKER; } @Override @@ -135,12 +137,10 @@ public class TimeZoneSettings extends DashboardFragment { switch (requestCode) { case REQUEST_CODE_REGION_PICKER: case REQUEST_CODE_ZONE_PICKER: { - String regionId = data.getStringExtra(RegionSearchPicker.EXTRA_RESULT_REGION_ID); - String tzId = data.getStringExtra(RegionZonePicker.EXTRA_RESULT_TIME_ZONE_ID); - // Ignore the result if user didn't change the region or time zone. - if (!Objects.equals(regionId, use(RegionPreferenceController.class).getRegionId()) - || !Objects.equals(tzId, mSelectedTimeZoneId)) { - onRegionZoneChanged(regionId, tzId); + if (mTimeZoneData == null) { + mPendingZonePickerRequestResult = data; + } else { + onZonePickerRequestResult(mTimeZoneData, data); } break; } @@ -165,8 +165,11 @@ public class TimeZoneSettings extends DashboardFragment { mTimeZoneData = timeZoneData; setupForCurrentTimeZone(); getActivity().invalidateOptionsMenu(); + if (mPendingZonePickerRequestResult != null) { + onZonePickerRequestResult(timeZoneData, mPendingZonePickerRequestResult); + mPendingZonePickerRequestResult = null; + } } - } private void startRegionPicker() { @@ -225,9 +228,17 @@ public class TimeZoneSettings extends DashboardFragment { updatePreferenceStates(); } - private void onRegionZoneChanged(String regionId, String tzId) { + private void onZonePickerRequestResult(TimeZoneData timeZoneData, Intent data) { + String regionId = data.getStringExtra(RegionSearchPicker.EXTRA_RESULT_REGION_ID); + String tzId = data.getStringExtra(RegionZonePicker.EXTRA_RESULT_TIME_ZONE_ID); + // Ignore the result if user didn't change the region or time zone. + if (Objects.equals(regionId, use(RegionPreferenceController.class).getRegionId()) + && Objects.equals(tzId, mSelectedTimeZoneId)) { + return; + } + FilteredCountryTimeZones countryTimeZones = - mTimeZoneData.lookupCountryTimeZones(regionId); + timeZoneData.lookupCountryTimeZones(regionId); if (countryTimeZones == null || !countryTimeZones.getTimeZoneIds().contains(tzId)) { Log.e(TAG, "Unknown time zone id is selected: " + tzId); return; diff --git a/src/com/android/settings/datetime/timezone/model/TimeZoneDataLoader.java b/src/com/android/settings/datetime/timezone/model/TimeZoneDataLoader.java index 9223207281..e901833260 100644 --- a/src/com/android/settings/datetime/timezone/model/TimeZoneDataLoader.java +++ b/src/com/android/settings/datetime/timezone/model/TimeZoneDataLoader.java @@ -16,14 +16,15 @@ package com.android.settings.datetime.timezone.model; -import android.app.LoaderManager; import android.content.Context; -import android.content.Loader; import android.os.Bundle; -import com.android.settingslib.utils.AsyncLoader; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.Loader; -public class TimeZoneDataLoader extends AsyncLoader<TimeZoneData> { +import com.android.settingslib.utils.AsyncLoaderCompat; + +public class TimeZoneDataLoader extends AsyncLoaderCompat<TimeZoneData> { public TimeZoneDataLoader(Context context) { super(context); diff --git a/src/com/android/settings/deletionhelper/ActivationWarningFragment.java b/src/com/android/settings/deletionhelper/ActivationWarningFragment.java index f7d46d1c43..8e3f8cabe3 100644 --- a/src/com/android/settings/deletionhelper/ActivationWarningFragment.java +++ b/src/com/android/settings/deletionhelper/ActivationWarningFragment.java @@ -16,11 +16,12 @@ package com.android.settings.deletionhelper; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.DialogFragment; -import android.content.DialogInterface; import android.os.Bundle; + +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; + import com.android.settings.R; /** diff --git a/src/com/android/settings/deletionhelper/AutomaticStorageManagerDescriptionPreferenceController.java b/src/com/android/settings/deletionhelper/AutomaticStorageManagerDescriptionPreferenceController.java index 92aa61246f..d11e68b64a 100644 --- a/src/com/android/settings/deletionhelper/AutomaticStorageManagerDescriptionPreferenceController.java +++ b/src/com/android/settings/deletionhelper/AutomaticStorageManagerDescriptionPreferenceController.java @@ -16,11 +16,12 @@ package com.android.settings.deletionhelper; import android.content.ContentResolver; import android.content.Context; import android.provider.Settings; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; import android.text.format.DateUtils; import android.text.format.Formatter; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.Utils; diff --git a/src/com/android/settings/deletionhelper/AutomaticStorageManagerSettings.java b/src/com/android/settings/deletionhelper/AutomaticStorageManagerSettings.java index 9e41e62a4e..0bc9dc9f04 100644 --- a/src/com/android/settings/deletionhelper/AutomaticStorageManagerSettings.java +++ b/src/com/android/settings/deletionhelper/AutomaticStorageManagerSettings.java @@ -16,18 +16,19 @@ package com.android.settings.deletionhelper; +import android.app.settings.SettingsEnums; import android.content.ContentResolver; import android.content.Context; import android.os.Bundle; import android.provider.Settings; -import androidx.preference.DropDownPreference; -import androidx.preference.Preference; -import androidx.preference.Preference.OnPreferenceChangeListener; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.preference.DropDownPreference; +import androidx.preference.Preference; +import androidx.preference.Preference.OnPreferenceChangeListener; + import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.Utils; @@ -36,6 +37,7 @@ import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; import com.android.settings.widget.SwitchBar; import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.List; @@ -44,6 +46,7 @@ import java.util.List; * AutomaticStorageManagerSettings is the Settings screen for configuration and management of the * automatic storage manager. */ +@SearchIndexable public class AutomaticStorageManagerSettings extends DashboardFragment implements OnPreferenceChangeListener { private static final String KEY_DAYS = "days"; @@ -135,7 +138,7 @@ public class AutomaticStorageManagerSettings extends DashboardFragment @Override public int getMetricsCategory() { - return MetricsEvent.STORAGE_MANAGER_SETTINGS; + return SettingsEnums.STORAGE_MANAGER_SETTINGS; } @Override diff --git a/src/com/android/settings/deletionhelper/AutomaticStorageManagerSwitchBarController.java b/src/com/android/settings/deletionhelper/AutomaticStorageManagerSwitchBarController.java index 011deffc95..c54d7893a9 100644 --- a/src/com/android/settings/deletionhelper/AutomaticStorageManagerSwitchBarController.java +++ b/src/com/android/settings/deletionhelper/AutomaticStorageManagerSwitchBarController.java @@ -16,18 +16,19 @@ package com.android.settings.deletionhelper; -import android.app.FragmentManager; +import android.app.settings.SettingsEnums; import android.content.Context; import android.os.SystemProperties; import android.provider.Settings; -import androidx.preference.Preference; import android.widget.Switch; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.fragment.app.FragmentManager; +import androidx.preference.Preference; + import com.android.internal.util.Preconditions; import com.android.settings.widget.SwitchBar; -import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; import com.android.settingslib.Utils; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; /** Handles the logic for flipping the storage management toggle on a {@link SwitchBar}. */ public class AutomaticStorageManagerSwitchBarController @@ -63,7 +64,7 @@ public class AutomaticStorageManagerSwitchBarController @Override public void onSwitchChanged(Switch switchView, boolean isChecked) { - mMetrics.action(mContext, MetricsEvent.ACTION_TOGGLE_STORAGE_MANAGER, isChecked); + mMetrics.action(mContext, SettingsEnums.ACTION_TOGGLE_STORAGE_MANAGER, isChecked); mDaysToRetainPreference.setEnabled(isChecked); Settings.Secure.putInt( mContext.getContentResolver(), diff --git a/src/com/android/settings/development/AbstractBluetoothA2dpPreferenceController.java b/src/com/android/settings/development/AbstractBluetoothA2dpPreferenceController.java index 9608f17da1..0f429c729f 100644 --- a/src/com/android/settings/development/AbstractBluetoothA2dpPreferenceController.java +++ b/src/com/android/settings/development/AbstractBluetoothA2dpPreferenceController.java @@ -21,6 +21,7 @@ import android.bluetooth.BluetoothCodecConfig; import android.bluetooth.BluetoothCodecStatus; import android.bluetooth.BluetoothDevice; import android.content.Context; + import androidx.annotation.VisibleForTesting; import androidx.preference.ListPreference; import androidx.preference.Preference; @@ -64,7 +65,7 @@ public abstract class AbstractBluetoothA2dpPreferenceController extends public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - mPreference = (ListPreference) screen.findPreference(getPreferenceKey()); + mPreference = screen.findPreference(getPreferenceKey()); // Set a default value because BluetoothCodecConfig is null initially. mPreference.setValue(mListValues[getDefaultIndex()]); @@ -176,7 +177,7 @@ public abstract class AbstractBluetoothA2dpPreferenceController extends @VisibleForTesting void setCodecConfigPreference(BluetoothDevice device, - BluetoothCodecConfig config) { + BluetoothCodecConfig config) { mBluetoothA2dp.setCodecConfigPreference(device, config); } diff --git a/src/com/android/settings/development/AdbPreferenceController.java b/src/com/android/settings/development/AdbPreferenceController.java index 86622eec5a..468c5bdd5c 100644 --- a/src/com/android/settings/development/AdbPreferenceController.java +++ b/src/com/android/settings/development/AdbPreferenceController.java @@ -18,6 +18,7 @@ package com.android.settings.development; import android.content.Context; + import androidx.annotation.Nullable; import androidx.preference.Preference; diff --git a/src/com/android/settings/development/AllowAppsOnExternalPreferenceController.java b/src/com/android/settings/development/AllowAppsOnExternalPreferenceController.java index c785bb67e2..14474f2978 100644 --- a/src/com/android/settings/development/AllowAppsOnExternalPreferenceController.java +++ b/src/com/android/settings/development/AllowAppsOnExternalPreferenceController.java @@ -18,9 +18,10 @@ package com.android.settings.development; import android.content.Context; import android.provider.Settings; + import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; import androidx.preference.Preference; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; diff --git a/src/com/android/settings/development/AnimatorDurationScalePreferenceController.java b/src/com/android/settings/development/AnimatorDurationScalePreferenceController.java index 35943951db..766352a6ca 100644 --- a/src/com/android/settings/development/AnimatorDurationScalePreferenceController.java +++ b/src/com/android/settings/development/AnimatorDurationScalePreferenceController.java @@ -19,10 +19,11 @@ package com.android.settings.development; import android.content.Context; import android.os.RemoteException; import android.os.ServiceManager; +import android.view.IWindowManager; + import androidx.annotation.VisibleForTesting; import androidx.preference.ListPreference; import androidx.preference.Preference; -import android.view.IWindowManager; import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; diff --git a/src/com/android/settings/development/AppPicker.java b/src/com/android/settings/development/AppPicker.java index 433f31a23e..04f318ffa5 100644 --- a/src/com/android/settings/development/AppPicker.java +++ b/src/com/android/settings/development/AppPicker.java @@ -45,9 +45,11 @@ public class AppPicker extends ListActivity { public static final String EXTRA_REQUESTIING_PERMISSION = "com.android.settings.extra.REQUESTIING_PERMISSION"; public static final String EXTRA_DEBUGGABLE = "com.android.settings.extra.DEBUGGABLE"; + public static final String EXTRA_NON_SYSTEM = "com.android.settings.extra.NON_SYSTEM"; private String mPermissionName; private boolean mDebuggableOnly; + private boolean mNonSystemOnly; @Override protected void onCreate(Bundle icicle) { @@ -55,6 +57,7 @@ public class AppPicker extends ListActivity { mPermissionName = getIntent().getStringExtra(EXTRA_REQUESTIING_PERMISSION); mDebuggableOnly = getIntent().getBooleanExtra(EXTRA_DEBUGGABLE, false); + mNonSystemOnly = getIntent().getBooleanExtra(EXTRA_NON_SYSTEM, false); mAdapter = new AppListAdapter(this); if (mAdapter.getCount() <= 0) { @@ -113,6 +116,11 @@ public class AppPicker extends ListActivity { } } + // Filter out apps that are system apps if requested + if (mNonSystemOnly && ai.isSystemApp()) { + continue; + } + // Filter out apps that do not request the permission if required. if (mPermissionName != null) { boolean requestsPermission = false; diff --git a/src/com/android/settings/development/AppsNotRespondingPreferenceController.java b/src/com/android/settings/development/AppsNotRespondingPreferenceController.java index 19db369d28..15e2663343 100644 --- a/src/com/android/settings/development/AppsNotRespondingPreferenceController.java +++ b/src/com/android/settings/development/AppsNotRespondingPreferenceController.java @@ -17,9 +17,10 @@ package com.android.settings.development; import android.content.Context; import android.provider.Settings; + import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; import androidx.preference.Preference; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; diff --git a/src/com/android/settings/development/ArtVerifierPreferenceController.java b/src/com/android/settings/development/ArtVerifierPreferenceController.java new file mode 100644 index 0000000000..4b2f030bcd --- /dev/null +++ b/src/com/android/settings/development/ArtVerifierPreferenceController.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2019 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.development; + +import android.content.Context; +import android.provider.Settings; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.SwitchPreference; + +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settingslib.development.DeveloperOptionsPreferenceController; + +public class ArtVerifierPreferenceController extends DeveloperOptionsPreferenceController + implements Preference.OnPreferenceChangeListener, PreferenceControllerMixin { + + private static final String ART_VERIFIER_FOR_DEBUGGABLE = "art_verifier_for_debuggable"; + + @VisibleForTesting + static final int SETTING_VALUE_ON = 1; + @VisibleForTesting + static final int SETTING_VALUE_OFF = 0; + + public ArtVerifierPreferenceController(Context context) { + super(context); + } + + @Override + public String getPreferenceKey() { + return ART_VERIFIER_FOR_DEBUGGABLE; + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + final boolean isEnabled = (Boolean) newValue; + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.ART_VERIFIER_VERIFY_DEBUGGABLE, + isEnabled ? SETTING_VALUE_ON : SETTING_VALUE_OFF); + return true; + } + + @Override + public void updateState(Preference preference) { + final int verifyDebuggable = Settings.Global.getInt( + mContext.getContentResolver(), + Settings.Global.ART_VERIFIER_VERIFY_DEBUGGABLE, SETTING_VALUE_ON); + ((SwitchPreference) mPreference).setChecked(verifyDebuggable != SETTING_VALUE_OFF); + } + + @Override + protected void onDeveloperOptionsSwitchDisabled() { + super.onDeveloperOptionsSwitchDisabled(); + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.ART_VERIFIER_VERIFY_DEBUGGABLE, SETTING_VALUE_ON); + ((SwitchPreference) mPreference).setChecked(true); + } +} diff --git a/src/com/android/settings/development/AutomaticSystemServerHeapDumpPreferenceController.java b/src/com/android/settings/development/AutomaticSystemServerHeapDumpPreferenceController.java new file mode 100644 index 0000000000..aa76bb8f14 --- /dev/null +++ b/src/com/android/settings/development/AutomaticSystemServerHeapDumpPreferenceController.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2019 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.development; + +import android.content.Context; +import android.os.Build; +import android.os.UserManager; +import android.provider.Settings; + +import androidx.preference.Preference; +import androidx.preference.SwitchPreference; + +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settingslib.development.DeveloperOptionsPreferenceController; + +public class AutomaticSystemServerHeapDumpPreferenceController extends + DeveloperOptionsPreferenceController implements Preference.OnPreferenceChangeListener, + PreferenceControllerMixin { + + private static final String KEY_AUTOMATIC_SYSTEM_SERVER_HEAP_DUMPS = + "automatic_system_server_heap_dumps"; + + private static final int SETTING_VALUE_OFF = 0; + private static final int SETTING_VALUE_ON = 1; + + private final UserManager mUserManager; + private final boolean mIsConfigEnabled; + + public AutomaticSystemServerHeapDumpPreferenceController(Context context) { + super(context); + mIsConfigEnabled = context.getResources().getBoolean( + com.android.internal.R.bool.config_debugEnableAutomaticSystemServerHeapDumps); + mUserManager = context.getSystemService(UserManager.class); + } + + @Override + public boolean isAvailable() { + return Build.IS_DEBUGGABLE && mIsConfigEnabled + && !mUserManager.hasUserRestriction(UserManager.DISALLOW_DEBUGGING_FEATURES); + } + + @Override + public String getPreferenceKey() { + return KEY_AUTOMATIC_SYSTEM_SERVER_HEAP_DUMPS; + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + final boolean isEnabled = (Boolean) newValue; + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Global.ENABLE_AUTOMATIC_SYSTEM_SERVER_HEAP_DUMPS, + isEnabled ? SETTING_VALUE_ON : SETTING_VALUE_OFF); + return true; + } + + @Override + public void updateState(Preference preference) { + final int mode = Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Global.ENABLE_AUTOMATIC_SYSTEM_SERVER_HEAP_DUMPS, SETTING_VALUE_ON); + ((SwitchPreference) mPreference).setChecked(mode != SETTING_VALUE_OFF); + } + + @Override + protected void onDeveloperOptionsSwitchDisabled() { + super.onDeveloperOptionsSwitchDisabled(); + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Global.ENABLE_AUTOMATIC_SYSTEM_SERVER_HEAP_DUMPS, SETTING_VALUE_OFF); + ((SwitchPreference) mPreference).setChecked(false); + } +} diff --git a/src/com/android/settings/development/BackgroundProcessLimitPreferenceController.java b/src/com/android/settings/development/BackgroundProcessLimitPreferenceController.java index 7333aad656..690d079257 100644 --- a/src/com/android/settings/development/BackgroundProcessLimitPreferenceController.java +++ b/src/com/android/settings/development/BackgroundProcessLimitPreferenceController.java @@ -20,6 +20,7 @@ import android.app.ActivityManager; import android.app.IActivityManager; import android.content.Context; import android.os.RemoteException; + import androidx.annotation.VisibleForTesting; import androidx.preference.ListPreference; import androidx.preference.Preference; diff --git a/src/com/android/settings/development/BluetoothA2dpHwOffloadPreferenceController.java b/src/com/android/settings/development/BluetoothA2dpHwOffloadPreferenceController.java index 96d2f5f42c..95e663bf50 100644 --- a/src/com/android/settings/development/BluetoothA2dpHwOffloadPreferenceController.java +++ b/src/com/android/settings/development/BluetoothA2dpHwOffloadPreferenceController.java @@ -18,9 +18,9 @@ package com.android.settings.development; import android.content.Context; import android.os.SystemProperties; -import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; + import androidx.preference.Preference; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; @@ -66,6 +66,25 @@ public class BluetoothA2dpHwOffloadPreferenceController extends DeveloperOptions } } + @Override + protected void onDeveloperOptionsSwitchDisabled() { + super.onDeveloperOptionsSwitchDisabled(); + final boolean offloadSupported = + SystemProperties.getBoolean(A2DP_OFFLOAD_SUPPORTED_PROPERTY, false); + if (offloadSupported) { + ((SwitchPreference) mPreference).setChecked(false); + SystemProperties.set(A2DP_OFFLOAD_DISABLED_PROPERTY, "false"); + } + } + + public boolean isDefaultValue() { + final boolean offloadSupported = + SystemProperties.getBoolean(A2DP_OFFLOAD_SUPPORTED_PROPERTY, false); + final boolean offloadDisabled = + SystemProperties.getBoolean(A2DP_OFFLOAD_DISABLED_PROPERTY, false); + return offloadSupported ? !offloadDisabled : true; + } + public void onA2dpHwDialogConfirmed() { final boolean offloadDisabled = SystemProperties.getBoolean(A2DP_OFFLOAD_DISABLED_PROPERTY, false); diff --git a/src/com/android/settings/development/BluetoothA2dpHwOffloadRebootDialog.java b/src/com/android/settings/development/BluetoothA2dpHwOffloadRebootDialog.java index 6fe03dd1fe..95ef0197a4 100644 --- a/src/com/android/settings/development/BluetoothA2dpHwOffloadRebootDialog.java +++ b/src/com/android/settings/development/BluetoothA2dpHwOffloadRebootDialog.java @@ -16,15 +16,15 @@ package com.android.settings.development; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.FragmentManager; -import android.content.Context; +import android.app.settings.SettingsEnums; import android.content.DialogInterface; import android.os.Bundle; import android.os.PowerManager; -import com.android.internal.logging.nano.MetricsProto; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.FragmentManager; + import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; @@ -35,7 +35,7 @@ public class BluetoothA2dpHwOffloadRebootDialog extends InstrumentedDialogFragme public static void show(DevelopmentSettingsDashboardFragment host, BluetoothA2dpHwOffloadPreferenceController controller) { - final FragmentManager manager = host.getActivity().getFragmentManager(); + final FragmentManager manager = host.getActivity().getSupportFragmentManager(); if (manager.findFragmentByTag(TAG) == null) { final BluetoothA2dpHwOffloadRebootDialog dialog = new BluetoothA2dpHwOffloadRebootDialog(); @@ -46,7 +46,7 @@ public class BluetoothA2dpHwOffloadRebootDialog extends InstrumentedDialogFragme @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DIALOG_BLUETOOTH_DISABLE_A2DP_HW_OFFLOAD; + return SettingsEnums.DIALOG_BLUETOOTH_DISABLE_A2DP_HW_OFFLOAD; } @Override diff --git a/src/com/android/settings/development/BluetoothAbsoluteVolumePreferenceController.java b/src/com/android/settings/development/BluetoothAbsoluteVolumePreferenceController.java index 4ea2456f46..77f0f507f8 100644 --- a/src/com/android/settings/development/BluetoothAbsoluteVolumePreferenceController.java +++ b/src/com/android/settings/development/BluetoothAbsoluteVolumePreferenceController.java @@ -18,9 +18,10 @@ package com.android.settings.development; import android.content.Context; import android.os.SystemProperties; + import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; import androidx.preference.Preference; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; diff --git a/src/com/android/settings/development/BluetoothAvrcpVersionPreferenceController.java b/src/com/android/settings/development/BluetoothAvrcpVersionPreferenceController.java index 688f125d1a..136ddad5fc 100644 --- a/src/com/android/settings/development/BluetoothAvrcpVersionPreferenceController.java +++ b/src/com/android/settings/development/BluetoothAvrcpVersionPreferenceController.java @@ -18,10 +18,11 @@ package com.android.settings.development; import android.content.Context; import android.os.SystemProperties; +import android.text.TextUtils; + import androidx.annotation.VisibleForTesting; import androidx.preference.ListPreference; import androidx.preference.Preference; -import android.text.TextUtils; import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; diff --git a/src/com/android/settings/development/BluetoothDeviceNoNamePreferenceController.java b/src/com/android/settings/development/BluetoothDeviceNoNamePreferenceController.java index 3e913bdfca..849e981981 100644 --- a/src/com/android/settings/development/BluetoothDeviceNoNamePreferenceController.java +++ b/src/com/android/settings/development/BluetoothDeviceNoNamePreferenceController.java @@ -18,9 +18,10 @@ package com.android.settings.development; import android.content.Context; import android.os.SystemProperties; + import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; import androidx.preference.Preference; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; diff --git a/src/com/android/settings/development/BluetoothMaxConnectedAudioDevicesPreferenceController.java b/src/com/android/settings/development/BluetoothMaxConnectedAudioDevicesPreferenceController.java index 6ad3689334..ee6af1c878 100644 --- a/src/com/android/settings/development/BluetoothMaxConnectedAudioDevicesPreferenceController.java +++ b/src/com/android/settings/development/BluetoothMaxConnectedAudioDevicesPreferenceController.java @@ -18,6 +18,7 @@ package com.android.settings.development; import android.content.Context; import android.os.SystemProperties; + import androidx.annotation.VisibleForTesting; import androidx.preference.ListPreference; import androidx.preference.Preference; diff --git a/src/com/android/settings/development/BluetoothSnoopLogPreferenceController.java b/src/com/android/settings/development/BluetoothSnoopLogPreferenceController.java index 4b1bf5cb29..d698436176 100644 --- a/src/com/android/settings/development/BluetoothSnoopLogPreferenceController.java +++ b/src/com/android/settings/development/BluetoothSnoopLogPreferenceController.java @@ -17,11 +17,19 @@ package com.android.settings.development; import android.content.Context; +import android.os.Build; import android.os.SystemProperties; +import android.provider.Settings; +import android.text.TextUtils; + +import android.util.Log; + import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; +import androidx.preference.ListPreference; import androidx.preference.Preference; +import androidx.preference.SwitchPreference; +import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; @@ -30,11 +38,39 @@ public class BluetoothSnoopLogPreferenceController extends DeveloperOptionsPrefe private static final String PREFERENCE_KEY = "bt_hci_snoop_log"; @VisibleForTesting - static final String BLUETOOTH_BTSNOOP_ENABLE_PROPERTY = - "persist.bluetooth.btsnoopenable"; + static final int BTSNOOP_LOG_MODE_DISABLED_INDEX = 0; + @VisibleForTesting + static final int BTSNOOP_LOG_MODE_FILTERED_INDEX = 1; + @VisibleForTesting + static final int BTSNOOP_LOG_MODE_FULL_INDEX = 2; + @VisibleForTesting + static final String BLUETOOTH_BTSNOOP_LOG_MODE_PROPERTY = "persist.bluetooth.btsnooplogmode"; + + private final String[] mListValues; + private final String[] mListEntries; public BluetoothSnoopLogPreferenceController(Context context) { super(context); + mListValues = context.getResources().getStringArray(R.array.bt_hci_snoop_log_values); + mListEntries = context.getResources().getStringArray(R.array.bt_hci_snoop_log_entries); + } + + // Default mode is DISABLED. It can also be changed by modifying the global setting. + public int getDefaultModeIndex() { + if (!Build.IS_DEBUGGABLE) { + return BTSNOOP_LOG_MODE_DISABLED_INDEX; + } + + final String default_mode = Settings.Global.getString(mContext.getContentResolver(), + Settings.Global.BLUETOOTH_BTSNOOP_DEFAULT_MODE); + + for (int i = 0; i < mListValues.length; i++) { + if (TextUtils.equals(default_mode, mListValues[i])) { + return i; + } + } + + return BTSNOOP_LOG_MODE_DISABLED_INDEX; } @Override @@ -44,23 +80,32 @@ public class BluetoothSnoopLogPreferenceController extends DeveloperOptionsPrefe @Override public boolean onPreferenceChange(Preference preference, Object newValue) { - final boolean enableBtSnoopLog = (Boolean) newValue; - SystemProperties.set(BLUETOOTH_BTSNOOP_ENABLE_PROPERTY, Boolean.toString(enableBtSnoopLog)); + SystemProperties.set(BLUETOOTH_BTSNOOP_LOG_MODE_PROPERTY, newValue.toString()); + updateState(mPreference); return true; } @Override public void updateState(Preference preference) { - super.updateState(preference); - final boolean enableBtSnoopLog = SystemProperties.getBoolean( - BLUETOOTH_BTSNOOP_ENABLE_PROPERTY, false /* def */); - ((SwitchPreference) mPreference).setChecked(enableBtSnoopLog); + final ListPreference listPreference = (ListPreference) preference; + final String currentValue = SystemProperties.get(BLUETOOTH_BTSNOOP_LOG_MODE_PROPERTY); + + int index = getDefaultModeIndex(); + for (int i = 0; i < mListValues.length; i++) { + if (TextUtils.equals(currentValue, mListValues[i])) { + index = i; + break; + } + } + listPreference.setValue(mListValues[index]); + listPreference.setSummary(mListEntries[index]); } @Override protected void onDeveloperOptionsSwitchDisabled() { super.onDeveloperOptionsSwitchDisabled(); - SystemProperties.set(BLUETOOTH_BTSNOOP_ENABLE_PROPERTY, Boolean.toString(false)); - ((SwitchPreference) mPreference).setChecked(false); + SystemProperties.set(BLUETOOTH_BTSNOOP_LOG_MODE_PROPERTY, null); + ((ListPreference) mPreference).setValue(mListValues[getDefaultModeIndex()]); + ((ListPreference) mPreference).setSummary(mListEntries[getDefaultModeIndex()]); } } diff --git a/src/com/android/settings/development/BubbleGlobalPreferenceController.java b/src/com/android/settings/development/BubbleGlobalPreferenceController.java new file mode 100644 index 0000000000..86d7be1046 --- /dev/null +++ b/src/com/android/settings/development/BubbleGlobalPreferenceController.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2019 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.development; + +import static android.provider.Settings.Secure.NOTIFICATION_BUBBLES; + +import android.content.Context; +import android.provider.Settings; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.SwitchPreference; + +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settingslib.development.DeveloperOptionsPreferenceController; + +public class BubbleGlobalPreferenceController extends DeveloperOptionsPreferenceController + implements Preference.OnPreferenceChangeListener, PreferenceControllerMixin { + + @VisibleForTesting + static final int ON = 1; + @VisibleForTesting + static final int OFF = 0; + + public BubbleGlobalPreferenceController(Context context) { + super(context); + } + + @Override + public String getPreferenceKey() { + return NOTIFICATION_BUBBLES; + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + writeSetting((boolean) newValue); + return true; + } + + @Override + public void updateState(Preference preference) { + ((SwitchPreference) mPreference).setChecked(isEnabled()); + } + + @Override + protected void onDeveloperOptionsSwitchDisabled() { + super.onDeveloperOptionsSwitchDisabled(); + writeSetting(false /* isEnabled */); + updateState(mPreference); + } + + private boolean isEnabled() { + return Settings.Secure.getInt(mContext.getContentResolver(), + NOTIFICATION_BUBBLES, OFF) == ON; + } + + private void writeSetting(boolean isEnabled) { + Settings.Secure.putInt(mContext.getContentResolver(), + NOTIFICATION_BUBBLES, isEnabled ? ON : OFF); + } +} diff --git a/src/com/android/settings/development/BugReportInPowerPreferenceController.java b/src/com/android/settings/development/BugReportInPowerPreferenceController.java index 22b9f2b86a..99ced77325 100644 --- a/src/com/android/settings/development/BugReportInPowerPreferenceController.java +++ b/src/com/android/settings/development/BugReportInPowerPreferenceController.java @@ -19,9 +19,10 @@ package com.android.settings.development; import android.content.Context; import android.os.UserManager; import android.provider.Settings; + import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; import androidx.preference.Preference; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; diff --git a/src/com/android/settings/development/CameraLaserSensorPreferenceController.java b/src/com/android/settings/development/CameraLaserSensorPreferenceController.java index 74f879462c..ec0ca24b6e 100644 --- a/src/com/android/settings/development/CameraLaserSensorPreferenceController.java +++ b/src/com/android/settings/development/CameraLaserSensorPreferenceController.java @@ -18,11 +18,12 @@ package com.android.settings.development; import android.content.Context; import android.os.SystemProperties; -import androidx.preference.SwitchPreference; -import androidx.preference.Preference; import android.text.TextUtils; -import com.android.internal.annotations.VisibleForTesting; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.SwitchPreference; + import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; diff --git a/src/com/android/settings/development/ClearAdbKeysPreferenceController.java b/src/com/android/settings/development/ClearAdbKeysPreferenceController.java index d94d428d76..b39d874432 100644 --- a/src/com/android/settings/development/ClearAdbKeysPreferenceController.java +++ b/src/com/android/settings/development/ClearAdbKeysPreferenceController.java @@ -17,16 +17,17 @@ package com.android.settings.development; import android.content.Context; -import android.hardware.usb.IUsbManager; +import android.debug.IAdbManager; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserManager; import android.sysprop.AdbProperties; +import android.text.TextUtils; +import android.util.Log; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.text.TextUtils; -import android.util.Log; import com.android.settings.Utils; import com.android.settings.core.PreferenceControllerMixin; @@ -38,7 +39,7 @@ public class ClearAdbKeysPreferenceController extends DeveloperOptionsPreference private static final String TAG = "ClearAdbPrefCtrl"; private static final String CLEAR_ADB_KEYS = "clear_adb_keys"; - private final IUsbManager mUsbManager; + private final IAdbManager mAdbManager; private final DevelopmentSettingsDashboardFragment mFragment; public ClearAdbKeysPreferenceController(Context context, @@ -46,7 +47,7 @@ public class ClearAdbKeysPreferenceController extends DeveloperOptionsPreference super(context); mFragment = fragment; - mUsbManager = IUsbManager.Stub.asInterface(ServiceManager.getService(Context.USB_SERVICE)); + mAdbManager = IAdbManager.Stub.asInterface(ServiceManager.getService(Context.ADB_SERVICE)); } @Override @@ -90,7 +91,7 @@ public class ClearAdbKeysPreferenceController extends DeveloperOptionsPreference public void onClearAdbKeysConfirmed() { try { - mUsbManager.clearUsbDebuggingKeys(); + mAdbManager.clearDebuggingKeys(); } catch (RemoteException e) { Log.e(TAG, "Unable to clear adb keys", e); } diff --git a/src/com/android/settings/development/ClearAdbKeysWarningDialog.java b/src/com/android/settings/development/ClearAdbKeysWarningDialog.java index 61b55a02f6..cdf8a95fdc 100644 --- a/src/com/android/settings/development/ClearAdbKeysWarningDialog.java +++ b/src/com/android/settings/development/ClearAdbKeysWarningDialog.java @@ -16,14 +16,15 @@ package com.android.settings.development; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.Fragment; -import android.app.FragmentManager; +import android.app.settings.SettingsEnums; import android.content.DialogInterface; import android.os.Bundle; -import com.android.internal.logging.nano.MetricsProto; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; + import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; @@ -33,7 +34,7 @@ public class ClearAdbKeysWarningDialog extends InstrumentedDialogFragment implem public static final String TAG = "ClearAdbKeysDlg"; public static void show(Fragment host) { - final FragmentManager manager = host.getActivity().getFragmentManager(); + final FragmentManager manager = host.getActivity().getSupportFragmentManager(); if (manager.findFragmentByTag(TAG) == null) { final ClearAdbKeysWarningDialog dialog = new ClearAdbKeysWarningDialog(); @@ -44,7 +45,7 @@ public class ClearAdbKeysWarningDialog extends InstrumentedDialogFragment implem @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DIALOG_CLEAR_ADB_KEYS; + return SettingsEnums.DIALOG_CLEAR_ADB_KEYS; } @Override diff --git a/src/com/android/settings/development/ColorModePreference.java b/src/com/android/settings/development/ColorModePreference.java index 54ca468785..9e5666ef22 100644 --- a/src/com/android/settings/development/ColorModePreference.java +++ b/src/com/android/settings/development/ColorModePreference.java @@ -21,10 +21,11 @@ import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManager.DisplayListener; import android.os.Handler; import android.os.Looper; -import androidx.preference.SwitchPreference; import android.util.AttributeSet; import android.view.Display; +import androidx.preference.SwitchPreference; + import com.android.settings.R; import java.util.ArrayList; diff --git a/src/com/android/settings/development/CoolColorTemperaturePreferenceController.java b/src/com/android/settings/development/CoolColorTemperaturePreferenceController.java index ae459acbca..54df6badb0 100644 --- a/src/com/android/settings/development/CoolColorTemperaturePreferenceController.java +++ b/src/com/android/settings/development/CoolColorTemperaturePreferenceController.java @@ -18,10 +18,11 @@ package com.android.settings.development; import android.content.Context; import android.os.SystemProperties; +import android.widget.Toast; + import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; import androidx.preference.Preference; -import android.widget.Toast; +import androidx.preference.SwitchPreference; import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; diff --git a/src/com/android/settings/development/DarkUIPreferenceController.java b/src/com/android/settings/development/DarkUIPreferenceController.java deleted file mode 100644 index fc457716bc..0000000000 --- a/src/com/android/settings/development/DarkUIPreferenceController.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright (C) 2018 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.development; - -import android.app.UiModeManager; -import android.content.Context; -import androidx.annotation.VisibleForTesting; -import androidx.preference.ListPreference; -import androidx.preference.Preference; - -import com.android.settings.R; -import com.android.settings.core.PreferenceControllerMixin; -import com.android.settingslib.development.DeveloperOptionsPreferenceController; - -public class DarkUIPreferenceController extends DeveloperOptionsPreferenceController - implements Preference.OnPreferenceChangeListener, PreferenceControllerMixin { - - private static final String DARK_UI_KEY = "dark_ui_mode"; - private final UiModeManager mUiModeManager; - - public DarkUIPreferenceController(Context context) { - this(context, context.getSystemService(UiModeManager.class)); - } - - @VisibleForTesting - DarkUIPreferenceController(Context context, UiModeManager uiModeManager) { - super(context); - mUiModeManager = uiModeManager; - } - - @Override - public String getPreferenceKey() { - return DARK_UI_KEY; - } - - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - mUiModeManager.setNightMode(modeToInt((String) newValue)); - updateSummary(preference); - return true; - } - - @Override - public void updateState(Preference preference) { - updateSummary(preference); - } - - private void updateSummary(Preference preference) { - int mode = mUiModeManager.getNightMode(); - ((ListPreference) preference).setValue(modeToString(mode)); - preference.setSummary(modeToDescription(mode)); - } - - private String modeToDescription(int mode) { - String[] values = mContext.getResources().getStringArray(R.array.dark_ui_mode_entries); - switch (mode) { - case UiModeManager.MODE_NIGHT_AUTO: - return values[0]; - case UiModeManager.MODE_NIGHT_YES: - return values[1]; - case UiModeManager.MODE_NIGHT_NO: - default: - return values[2]; - - } - } - - private String modeToString(int mode) { - switch (mode) { - case UiModeManager.MODE_NIGHT_AUTO: - return "auto"; - case UiModeManager.MODE_NIGHT_YES: - return "yes"; - case UiModeManager.MODE_NIGHT_NO: - default: - return "no"; - - } - } - - private int modeToInt(String mode) { - switch (mode) { - case "auto": - return UiModeManager.MODE_NIGHT_AUTO; - case "yes": - return UiModeManager.MODE_NIGHT_YES; - case "no": - default: - return UiModeManager.MODE_NIGHT_NO; - } - } -} diff --git a/src/com/android/settings/development/DebugGpuOverdrawPreferenceController.java b/src/com/android/settings/development/DebugGpuOverdrawPreferenceController.java index 9b3f1d8347..da4b94b159 100644 --- a/src/com/android/settings/development/DebugGpuOverdrawPreferenceController.java +++ b/src/com/android/settings/development/DebugGpuOverdrawPreferenceController.java @@ -18,11 +18,12 @@ package com.android.settings.development; import android.content.Context; import android.os.SystemProperties; -import androidx.preference.ListPreference; -import androidx.preference.Preference; import android.text.TextUtils; import android.view.ThreadedRenderer; +import androidx.preference.ListPreference; +import androidx.preference.Preference; + import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; diff --git a/src/com/android/settings/development/DebugNonRectClipOperationsPreferenceController.java b/src/com/android/settings/development/DebugNonRectClipOperationsPreferenceController.java index 03e3dab18e..8f15c8697b 100644 --- a/src/com/android/settings/development/DebugNonRectClipOperationsPreferenceController.java +++ b/src/com/android/settings/development/DebugNonRectClipOperationsPreferenceController.java @@ -18,11 +18,12 @@ package com.android.settings.development; import android.content.Context; import android.os.SystemProperties; -import androidx.preference.ListPreference; -import androidx.preference.Preference; import android.text.TextUtils; import android.view.ThreadedRenderer; +import androidx.preference.ListPreference; +import androidx.preference.Preference; + import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; diff --git a/src/com/android/settings/development/DebugViewAttributesPreferenceController.java b/src/com/android/settings/development/DebugViewAttributesPreferenceController.java index 8950b86bdc..04d819c86a 100644 --- a/src/com/android/settings/development/DebugViewAttributesPreferenceController.java +++ b/src/com/android/settings/development/DebugViewAttributesPreferenceController.java @@ -18,9 +18,10 @@ package com.android.settings.development; import android.content.Context; import android.provider.Settings; + import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; import androidx.preference.Preference; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; diff --git a/src/com/android/settings/development/WifiConnectedMacRandomizationPreferenceController.java b/src/com/android/settings/development/DesktopModePreferenceController.java index 9582ebe4ad..528af8aee8 100644 --- a/src/com/android/settings/development/WifiConnectedMacRandomizationPreferenceController.java +++ b/src/com/android/settings/development/DesktopModePreferenceController.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 The Android Open Source Project + * Copyright (C) 2018 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. @@ -16,64 +16,64 @@ package com.android.settings.development; +import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS; + import android.content.Context; +import android.os.Build; import android.provider.Settings; + import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; import androidx.preference.Preference; +import androidx.preference.SwitchPreference; -import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; -public class WifiConnectedMacRandomizationPreferenceController extends - DeveloperOptionsPreferenceController implements Preference.OnPreferenceChangeListener, - PreferenceControllerMixin { +public class DesktopModePreferenceController extends DeveloperOptionsPreferenceController + implements Preference.OnPreferenceChangeListener, PreferenceControllerMixin { - private static final String WIFI_CONNECTED_MAC_RANDOMIZATION_KEY = - "wifi_connected_mac_randomization"; + private static final String FORCE_DESKTOP_MODE_KEY = "force_desktop_mode_on_external_displays"; @VisibleForTesting - static final int SETTING_VALUE_ON = 1; - @VisibleForTesting static final int SETTING_VALUE_OFF = 0; + @VisibleForTesting + static final int SETTING_VALUE_ON = 1; - public WifiConnectedMacRandomizationPreferenceController(Context context) { + public DesktopModePreferenceController(Context context) { super(context); } @Override - public boolean isAvailable() { - return mContext.getResources().getBoolean( - R.bool.config_wifi_support_connected_mac_randomization); - } - - @Override public String getPreferenceKey() { - return WIFI_CONNECTED_MAC_RANDOMIZATION_KEY; + return FORCE_DESKTOP_MODE_KEY; } @Override public boolean onPreferenceChange(Preference preference, Object newValue) { final boolean isEnabled = (Boolean) newValue; Settings.Global.putInt(mContext.getContentResolver(), - Settings.Global.WIFI_CONNECTED_MAC_RANDOMIZATION_ENABLED, + DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS, isEnabled ? SETTING_VALUE_ON : SETTING_VALUE_OFF); return true; } @Override public void updateState(Preference preference) { - final int enableMode = Settings.Global.getInt(mContext.getContentResolver(), - Settings.Global.WIFI_CONNECTED_MAC_RANDOMIZATION_ENABLED, SETTING_VALUE_OFF); - ((SwitchPreference) mPreference).setChecked(enableMode != SETTING_VALUE_OFF); + final int mode = Settings.Global.getInt(mContext.getContentResolver(), + DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS, SETTING_VALUE_OFF); + ((SwitchPreference) mPreference).setChecked(mode != SETTING_VALUE_OFF); } @Override protected void onDeveloperOptionsSwitchDisabled() { super.onDeveloperOptionsSwitchDisabled(); Settings.Global.putInt(mContext.getContentResolver(), - Settings.Global.WIFI_CONNECTED_MAC_RANDOMIZATION_ENABLED, SETTING_VALUE_OFF); + DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS, SETTING_VALUE_OFF); ((SwitchPreference) mPreference).setChecked(false); } + + @VisibleForTesting + String getBuildType() { + return Build.TYPE; + } } diff --git a/src/com/android/settings/development/DevelopmentOptionsActivityRequestCodes.java b/src/com/android/settings/development/DevelopmentOptionsActivityRequestCodes.java index b7b27591df..564f2c35c4 100644 --- a/src/com/android/settings/development/DevelopmentOptionsActivityRequestCodes.java +++ b/src/com/android/settings/development/DevelopmentOptionsActivityRequestCodes.java @@ -25,4 +25,10 @@ public interface DevelopmentOptionsActivityRequestCodes { int REQUEST_CODE_DEBUG_APP = 1; int REQUEST_MOCK_LOCATION_APP = 2; + + int REQUEST_CODE_ANGLE_ALL_USE_ANGLE = 3; + + int REQUEST_CODE_ANGLE_DRIVER_PKGS = 4; + + int REQUEST_CODE_ANGLE_DRIVER_VALUES = 5; } diff --git a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java index 8518c7446e..aba95c80cc 100644 --- a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java +++ b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java @@ -17,6 +17,7 @@ package com.android.settings.development; import android.app.Activity; +import android.app.settings.SettingsEnums; import android.bluetooth.BluetoothA2dp; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothCodecStatus; @@ -28,19 +29,21 @@ import android.content.IntentFilter; import android.os.Bundle; import android.os.UserManager; import android.provider.SearchIndexableResource; -import androidx.annotation.VisibleForTesting; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Switch; -import com.android.internal.logging.nano.MetricsProto; +import androidx.annotation.VisibleForTesting; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.Utils; import com.android.settings.dashboard.RestrictedDashboardFragment; +import com.android.settings.development.autofill.AutofillLoggingLevelPreferenceController; +import com.android.settings.development.autofill.AutofillResetOptionsPreferenceController; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; import com.android.settings.widget.SwitchBar; @@ -49,11 +52,13 @@ import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.development.DeveloperOptionsPreferenceController; import com.android.settingslib.development.DevelopmentSettingsEnabler; import com.android.settingslib.development.SystemPropPoker; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +@SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC) public class DevelopmentSettingsDashboardFragment extends RestrictedDashboardFragment implements SwitchBar.OnSwitchChangeListener, OemUnlockDialogHost, AdbDialogHost, AdbClearKeysDialogHost, LogPersistDialogHost, @@ -163,7 +168,8 @@ public class DevelopmentSettingsDashboardFragment extends RestrictedDashboardFra // Set up master switch mSwitchBar = ((SettingsActivity) getActivity()).getSwitchBar(); mSwitchBarController = new DevelopmentSwitchBarController( - this /* DevelopmentSettings */, mSwitchBar, mIsAvailable, getLifecycle()); + this /* DevelopmentSettings */, mSwitchBar, mIsAvailable, + getSettingsLifecycle()); mSwitchBar.show(); // Restore UI state based on whether developer options is enabled @@ -201,7 +207,7 @@ public class DevelopmentSettingsDashboardFragment extends RestrictedDashboardFra @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DEVELOPMENT; + return SettingsEnums.DEVELOPMENT; } @Override @@ -215,7 +221,16 @@ public class DevelopmentSettingsDashboardFragment extends RestrictedDashboardFra if (isChecked) { EnableDevelopmentSettingWarningDialog.show(this /* host */); } else { - disableDeveloperOptions(); + final BluetoothA2dpHwOffloadPreferenceController controller = + getDevelopmentOptionsController( + BluetoothA2dpHwOffloadPreferenceController.class); + // If A2DP hardware offload isn't default value, we must reboot after disable + // developer options. Show a dialog for the user to confirm. + if (controller == null || controller.isDefaultValue()) { + disableDeveloperOptions(); + } else { + DisableDevSettingsDialogFragment.show(this /* host */); + } } } } @@ -315,8 +330,8 @@ public class DevelopmentSettingsDashboardFragment extends RestrictedDashboardFra mPreferenceControllers = new ArrayList<>(); return null; } - mPreferenceControllers = buildPreferenceControllers(context, getActivity(), getLifecycle(), - this /* devOptionsDashboardFragment */, + mPreferenceControllers = buildPreferenceControllers(context, getActivity(), + getSettingsLifecycle(), this /* devOptionsDashboardFragment */, new BluetoothA2dpConfigStore()); return mPreferenceControllers; } @@ -374,16 +389,25 @@ public class DevelopmentSettingsDashboardFragment extends RestrictedDashboardFra mSwitchBar.setChecked(false); } + void onDisableDevelopmentOptionsConfirmed() { + disableDeveloperOptions(); + } + + void onDisableDevelopmentOptionsRejected() { + // Reset the toggle + mSwitchBar.setChecked(true); + } + private static List<AbstractPreferenceController> buildPreferenceControllers(Context context, Activity activity, Lifecycle lifecycle, DevelopmentSettingsDashboardFragment fragment, BluetoothA2dpConfigStore bluetoothA2dpConfigStore) { final List<AbstractPreferenceController> controllers = new ArrayList<>(); controllers.add(new MemoryUsagePreferenceController(context)); controllers.add(new BugReportPreferenceController(context)); + controllers.add(new SystemServerHeapDumpPreferenceController(context)); controllers.add(new LocalBackupPasswordPreferenceController(context)); controllers.add(new StayAwakePreferenceController(context, lifecycle)); controllers.add(new HdcpCheckingPreferenceController(context)); - controllers.add(new DarkUIPreferenceController(context)); controllers.add(new BluetoothSnoopLogPreferenceController(context)); controllers.add(new OemUnlockPreferenceController(context, activity, fragment)); controllers.add(new FileEncryptionPreferenceController(context)); @@ -395,18 +419,20 @@ public class DevelopmentSettingsDashboardFragment extends RestrictedDashboardFra controllers.add(new ClearAdbKeysPreferenceController(context, fragment)); controllers.add(new LocalTerminalPreferenceController(context)); controllers.add(new BugReportInPowerPreferenceController(context)); + controllers.add(new AutomaticSystemServerHeapDumpPreferenceController(context)); controllers.add(new MockLocationAppPreferenceController(context, fragment)); controllers.add(new DebugViewAttributesPreferenceController(context)); controllers.add(new SelectDebugAppPreferenceController(context, fragment)); controllers.add(new WaitForDebuggerPreferenceController(context)); controllers.add(new EnableGpuDebugLayersPreferenceController(context)); controllers.add(new VerifyAppsOverUsbPreferenceController(context)); + controllers.add(new ArtVerifierPreferenceController(context)); controllers.add(new LogdSizePreferenceController(context)); controllers.add(new LogPersistPreferenceController(context, fragment, lifecycle)); controllers.add(new CameraLaserSensorPreferenceController(context)); controllers.add(new WifiDisplayCertificationPreferenceController(context)); controllers.add(new WifiVerboseLoggingPreferenceController(context)); - controllers.add(new WifiConnectedMacRandomizationPreferenceController(context)); + controllers.add(new WifiScanThrottlingPreferenceController(context)); controllers.add(new MobileDataAlwaysOnPreferenceController(context)); controllers.add(new TetheringHardwareAccelPreferenceController(context)); controllers.add(new BluetoothDeviceNoNamePreferenceController(context)); @@ -434,11 +460,11 @@ public class DevelopmentSettingsDashboardFragment extends RestrictedDashboardFra controllers.add(new TransitionAnimationScalePreferenceController(context)); controllers.add(new AnimatorDurationScalePreferenceController(context)); controllers.add(new SecondaryDisplayPreferenceController(context)); - controllers.add(new ForceGpuRenderingPreferenceController(context)); controllers.add(new GpuViewUpdatesPreferenceController(context)); controllers.add(new HardwareLayersUpdatesPreferenceController(context)); controllers.add(new DebugGpuOverdrawPreferenceController(context)); controllers.add(new DebugNonRectClipOperationsPreferenceController(context)); + controllers.add(new ForceDarkPreferenceController(context)); controllers.add(new ForceMSAAPreferenceController(context)); controllers.add(new HardwareOverlaysPreferenceController(context)); controllers.add(new SimulateColorSpacePreferenceController(context)); @@ -453,7 +479,9 @@ public class DevelopmentSettingsDashboardFragment extends RestrictedDashboardFra controllers.add(new AllowAppsOnExternalPreferenceController(context)); controllers.add(new ResizableActivityPreferenceController(context)); controllers.add(new FreeformWindowsPreferenceController(context)); + controllers.add(new DesktopModePreferenceController(context)); controllers.add(new ShortcutManagerThrottlingPreferenceController(context)); + controllers.add(new BubbleGlobalPreferenceController(context)); controllers.add(new EnableGnssRawMeasFullTrackingPreferenceController(context)); controllers.add(new DefaultLaunchPreferenceController(context, "running_apps")); controllers.add(new DefaultLaunchPreferenceController(context, "demo_mode")); @@ -464,6 +492,16 @@ public class DevelopmentSettingsDashboardFragment extends RestrictedDashboardFra controllers.add(new DefaultLaunchPreferenceController(context, "density")); controllers.add(new DefaultLaunchPreferenceController(context, "background_check")); controllers.add(new DefaultLaunchPreferenceController(context, "inactive_apps")); + controllers.add(new AutofillLoggingLevelPreferenceController(context, lifecycle)); + controllers.add(new AutofillResetOptionsPreferenceController(context)); + controllers.add(new OverlayCategoryPreferenceController(context, + "android.theme.customization.accent_color")); + controllers.add(new OverlayCategoryPreferenceController(context, + "android.theme.customization.font")); + controllers.add(new OverlayCategoryPreferenceController(context, + "android.theme.customization.adaptive_icon_shape")); + controllers.add(new TrustAgentsExtendUnlockPreferenceController(context)); + controllers.add(new TrustLostLocksScreenPreferenceController(context)); return controllers; } diff --git a/src/com/android/settings/development/DisableAutomaticUpdatesPreferenceController.java b/src/com/android/settings/development/DisableAutomaticUpdatesPreferenceController.java index 5ecb67633d..f10e171c8c 100644 --- a/src/com/android/settings/development/DisableAutomaticUpdatesPreferenceController.java +++ b/src/com/android/settings/development/DisableAutomaticUpdatesPreferenceController.java @@ -18,9 +18,10 @@ package com.android.settings.development; import android.content.Context; import android.provider.Settings; + import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; import androidx.preference.Preference; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; diff --git a/src/com/android/settings/development/DisableDevSettingsDialogFragment.java b/src/com/android/settings/development/DisableDevSettingsDialogFragment.java new file mode 100644 index 0000000000..803030eb83 --- /dev/null +++ b/src/com/android/settings/development/DisableDevSettingsDialogFragment.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2018 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.development; + +import android.app.Dialog; +import android.app.settings.SettingsEnums; +import android.content.DialogInterface; +import android.os.Bundle; +import android.os.PowerManager; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; + +import com.android.settings.R; +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; + +public class DisableDevSettingsDialogFragment extends InstrumentedDialogFragment + implements DialogInterface.OnClickListener { + + public static final String TAG = "DisableDevSettingDlg"; + + @VisibleForTesting + static DisableDevSettingsDialogFragment newInstance() { + final DisableDevSettingsDialogFragment dialog = new DisableDevSettingsDialogFragment(); + return dialog; + } + + public static void show(DevelopmentSettingsDashboardFragment host) { + final DisableDevSettingsDialogFragment dialog = new DisableDevSettingsDialogFragment(); + dialog.setTargetFragment(host, 0 /* requestCode */); + final FragmentManager manager = host.getActivity().getSupportFragmentManager(); + dialog.show(manager, TAG); + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.DIALOG_DISABLE_DEVELOPMENT_OPTIONS; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + // Reuse the same text of disable_a2dp_hw_offload_dialog. + // The text is generic enough to be used for turning off Dev options. + return new AlertDialog.Builder(getActivity()) + .setMessage(R.string.bluetooth_disable_a2dp_hw_offload_dialog_message) + .setTitle(R.string.bluetooth_disable_a2dp_hw_offload_dialog_title) + .setPositiveButton( + R.string.bluetooth_disable_a2dp_hw_offload_dialog_confirm, this) + .setNegativeButton( + R.string.bluetooth_disable_a2dp_hw_offload_dialog_cancel, this) + .create(); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + Fragment fragment = getTargetFragment(); + if (!(fragment instanceof DevelopmentSettingsDashboardFragment)){ + Log.e(TAG, "getTargetFragment return unexpected type"); + } + + final DevelopmentSettingsDashboardFragment host = + (DevelopmentSettingsDashboardFragment) fragment; + if (which == DialogInterface.BUTTON_POSITIVE) { + host.onDisableDevelopmentOptionsConfirmed(); + PowerManager pm = getContext().getSystemService(PowerManager.class); + pm.reboot(null); + } else { + host.onDisableDevelopmentOptionsRejected(); + } + } +} diff --git a/src/com/android/settings/development/DisableLogPersistWarningDialog.java b/src/com/android/settings/development/DisableLogPersistWarningDialog.java index 1ab3a92a69..76035cca06 100644 --- a/src/com/android/settings/development/DisableLogPersistWarningDialog.java +++ b/src/com/android/settings/development/DisableLogPersistWarningDialog.java @@ -16,14 +16,15 @@ package com.android.settings.development; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.Fragment; -import android.app.FragmentManager; +import android.app.settings.SettingsEnums; import android.content.DialogInterface; import android.os.Bundle; -import com.android.internal.logging.nano.MetricsProto; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; + import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; @@ -37,7 +38,7 @@ public class DisableLogPersistWarningDialog extends InstrumentedDialogFragment i return; } final Fragment hostFragment = (Fragment) host; - final FragmentManager manager = hostFragment.getActivity().getFragmentManager(); + final FragmentManager manager = hostFragment.getActivity().getSupportFragmentManager(); if (manager.findFragmentByTag(TAG) == null) { final DisableLogPersistWarningDialog dialog = new DisableLogPersistWarningDialog(); @@ -48,7 +49,7 @@ public class DisableLogPersistWarningDialog extends InstrumentedDialogFragment i @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DIALOG_LOG_PERSIST; + return SettingsEnums.DIALOG_LOG_PERSIST; } @Override diff --git a/src/com/android/settings/development/EmulateDisplayCutoutPreferenceController.java b/src/com/android/settings/development/EmulateDisplayCutoutPreferenceController.java index c9bb9b76bd..ef88baa134 100644 --- a/src/com/android/settings/development/EmulateDisplayCutoutPreferenceController.java +++ b/src/com/android/settings/development/EmulateDisplayCutoutPreferenceController.java @@ -16,155 +16,31 @@ package com.android.settings.development; -import static android.os.UserHandle.USER_SYSTEM; - import android.content.Context; +import android.content.om.IOverlayManager; import android.content.pm.PackageManager; -import androidx.annotation.VisibleForTesting; -import androidx.preference.ListPreference; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; -import android.text.TextUtils; +import android.os.ServiceManager; import android.view.DisplayCutout; -import com.android.settings.R; -import com.android.settings.core.PreferenceControllerMixin; -import com.android.settings.wrapper.OverlayManagerWrapper; -import com.android.settings.wrapper.OverlayManagerWrapper.OverlayInfo; -import com.android.settingslib.development.DeveloperOptionsPreferenceController; - -import java.util.Comparator; -import java.util.List; +import androidx.annotation.VisibleForTesting; -public class EmulateDisplayCutoutPreferenceController extends - DeveloperOptionsPreferenceController implements Preference.OnPreferenceChangeListener, - PreferenceControllerMixin { +public class EmulateDisplayCutoutPreferenceController extends OverlayCategoryPreferenceController { private static final String KEY = "display_cutout_emulation"; - private static final Comparator<OverlayInfo> OVERLAY_INFO_COMPARATOR = - Comparator.comparingInt(a -> a.priority); - - private final OverlayManagerWrapper mOverlayManager; - private final boolean mAvailable; - - private ListPreference mPreference; - private PackageManager mPackageManager; @VisibleForTesting EmulateDisplayCutoutPreferenceController(Context context, PackageManager packageManager, - OverlayManagerWrapper overlayManager) { - super(context); - mOverlayManager = overlayManager; - mPackageManager = packageManager; - mAvailable = overlayManager != null && getOverlayInfos().length > 0; + IOverlayManager overlayManager) { + super(context, packageManager, overlayManager, DisplayCutout.EMULATION_OVERLAY_CATEGORY); } public EmulateDisplayCutoutPreferenceController(Context context) { - this(context, context.getPackageManager(), new OverlayManagerWrapper()); - } - - @Override - public boolean isAvailable() { - return mAvailable; + this(context, context.getPackageManager(), IOverlayManager.Stub + .asInterface(ServiceManager.getService(Context.OVERLAY_SERVICE))); } @Override public String getPreferenceKey() { return KEY; } - - @Override - public void displayPreference(PreferenceScreen screen) { - super.displayPreference(screen); - setPreference((ListPreference) screen.findPreference(getPreferenceKey())); - } - - @VisibleForTesting - void setPreference(ListPreference preference) { - mPreference = preference; - } - - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - return setEmulationOverlay((String) newValue); - } - - private boolean setEmulationOverlay(String packageName) { - OverlayInfo[] overlays = getOverlayInfos(); - String currentPackageName = null; - for (OverlayInfo o : overlays) { - if (o.isEnabled()) { - currentPackageName = o.packageName; - } - } - - if (TextUtils.isEmpty(packageName) && TextUtils.isEmpty(currentPackageName) - || TextUtils.equals(packageName, currentPackageName)) { - // Already set. - return true; - } - - final boolean result; - if (TextUtils.isEmpty(packageName)) { - result = mOverlayManager.setEnabled(currentPackageName, false, USER_SYSTEM); - } else { - result = mOverlayManager.setEnabledExclusiveInCategory(packageName, USER_SYSTEM); - } - updateState(mPreference); - return result; - } - - @Override - public void updateState(Preference preference) { - OverlayInfo[] overlays = getOverlayInfos(); - - CharSequence[] pkgs = new CharSequence[overlays.length + 1]; - CharSequence[] labels = new CharSequence[pkgs.length]; - - int current = 0; - pkgs[0] = ""; - labels[0] = mContext.getString(R.string.display_cutout_emulation_device_default); - - for (int i = 0; i < overlays.length; i++) { - OverlayInfo o = overlays[i]; - pkgs[i+1] = o.packageName; - if (o.isEnabled()) { - current = i+1; - } - } - for (int i = 1; i < pkgs.length; i++) { - try { - labels[i] = mPackageManager.getApplicationInfo(pkgs[i].toString(), 0) - .loadLabel(mPackageManager); - } catch (PackageManager.NameNotFoundException e) { - labels[i] = pkgs[i]; - } - } - - mPreference.setEntries(labels); - mPreference.setEntryValues(pkgs); - mPreference.setValueIndex(current); - mPreference.setSummary(labels[current]); - } - - private OverlayInfo[] getOverlayInfos() { - @SuppressWarnings("unchecked") List<OverlayInfo> overlayInfos = - mOverlayManager.getOverlayInfosForTarget("android", USER_SYSTEM); - for (int i = overlayInfos.size() - 1; i >= 0; i--) { - if (!DisplayCutout.EMULATION_OVERLAY_CATEGORY.equals( - overlayInfos.get(i).category)) { - overlayInfos.remove(i); - } - } - overlayInfos.sort(OVERLAY_INFO_COMPARATOR); - return overlayInfos.toArray(new OverlayInfo[overlayInfos.size()]); - } - - @Override - protected void onDeveloperOptionsSwitchDisabled() { - super.onDeveloperOptionsSwitchDisabled(); - setEmulationOverlay(""); - updateState(mPreference); - } - } diff --git a/src/com/android/settings/development/EnableAdbWarningDialog.java b/src/com/android/settings/development/EnableAdbWarningDialog.java index 9829f70d1e..1fcd350f1f 100644 --- a/src/com/android/settings/development/EnableAdbWarningDialog.java +++ b/src/com/android/settings/development/EnableAdbWarningDialog.java @@ -16,14 +16,15 @@ package com.android.settings.development; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.Fragment; -import android.app.FragmentManager; +import android.app.settings.SettingsEnums; import android.content.DialogInterface; import android.os.Bundle; -import com.android.internal.logging.nano.MetricsProto; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; + import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; @@ -33,7 +34,7 @@ public class EnableAdbWarningDialog extends InstrumentedDialogFragment implement public static final String TAG = "EnableAdbDialog"; public static void show(Fragment host) { - final FragmentManager manager = host.getActivity().getFragmentManager(); + final FragmentManager manager = host.getActivity().getSupportFragmentManager(); if (manager.findFragmentByTag(TAG) == null) { final EnableAdbWarningDialog dialog = new EnableAdbWarningDialog(); dialog.setTargetFragment(host, 0 /* requestCode */); @@ -43,7 +44,7 @@ public class EnableAdbWarningDialog extends InstrumentedDialogFragment implement @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DIALOG_ENABLE_ADB; + return SettingsEnums.DIALOG_ENABLE_ADB; } @Override diff --git a/src/com/android/settings/development/EnableDevelopmentSettingWarningDialog.java b/src/com/android/settings/development/EnableDevelopmentSettingWarningDialog.java index 3c3d645911..983f55ed63 100644 --- a/src/com/android/settings/development/EnableDevelopmentSettingWarningDialog.java +++ b/src/com/android/settings/development/EnableDevelopmentSettingWarningDialog.java @@ -16,13 +16,14 @@ package com.android.settings.development; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.FragmentManager; +import android.app.settings.SettingsEnums; import android.content.DialogInterface; import android.os.Bundle; -import com.android.internal.logging.nano.MetricsProto; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.FragmentManager; + import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; @@ -36,7 +37,7 @@ public class EnableDevelopmentSettingWarningDialog extends InstrumentedDialogFra final EnableDevelopmentSettingWarningDialog dialog = new EnableDevelopmentSettingWarningDialog(); dialog.setTargetFragment(host, 0 /* requestCode */); - final FragmentManager manager = host.getActivity().getFragmentManager(); + final FragmentManager manager = host.getActivity().getSupportFragmentManager(); if (manager.findFragmentByTag(TAG) == null) { dialog.show(manager, TAG); } @@ -44,7 +45,7 @@ public class EnableDevelopmentSettingWarningDialog extends InstrumentedDialogFra @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DIALOG_ENABLE_DEVELOPMENT_OPTIONS; + return SettingsEnums.DIALOG_ENABLE_DEVELOPMENT_OPTIONS; } @Override diff --git a/src/com/android/settings/development/EnableGnssRawMeasFullTrackingPreferenceController.java b/src/com/android/settings/development/EnableGnssRawMeasFullTrackingPreferenceController.java index 86663682d6..6348d623e1 100644 --- a/src/com/android/settings/development/EnableGnssRawMeasFullTrackingPreferenceController.java +++ b/src/com/android/settings/development/EnableGnssRawMeasFullTrackingPreferenceController.java @@ -18,9 +18,9 @@ package com.android.settings.development; import android.content.Context; import android.provider.Settings; -import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; + import androidx.preference.Preference; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; diff --git a/src/com/android/settings/development/EnableGpuDebugLayersPreferenceController.java b/src/com/android/settings/development/EnableGpuDebugLayersPreferenceController.java index 9882975ff6..163605fb17 100644 --- a/src/com/android/settings/development/EnableGpuDebugLayersPreferenceController.java +++ b/src/com/android/settings/development/EnableGpuDebugLayersPreferenceController.java @@ -18,9 +18,10 @@ package com.android.settings.development; import android.content.Context; import android.provider.Settings; + import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; import androidx.preference.Preference; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; diff --git a/src/com/android/settings/development/EnableOemUnlockSettingWarningDialog.java b/src/com/android/settings/development/EnableOemUnlockSettingWarningDialog.java index 2486ef5358..9bfe9d2e72 100644 --- a/src/com/android/settings/development/EnableOemUnlockSettingWarningDialog.java +++ b/src/com/android/settings/development/EnableOemUnlockSettingWarningDialog.java @@ -16,14 +16,15 @@ package com.android.settings.development; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.Fragment; -import android.app.FragmentManager; +import android.app.settings.SettingsEnums; import android.content.DialogInterface; import android.os.Bundle; -import com.android.internal.logging.nano.MetricsProto; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; + import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; @@ -33,7 +34,7 @@ public class EnableOemUnlockSettingWarningDialog extends InstrumentedDialogFragm public static final String TAG = "EnableOemUnlockDlg"; public static void show(Fragment host) { - final FragmentManager manager = host.getActivity().getFragmentManager(); + final FragmentManager manager = host.getActivity().getSupportFragmentManager(); if (manager.findFragmentByTag(TAG) == null) { final EnableOemUnlockSettingWarningDialog dialog = new EnableOemUnlockSettingWarningDialog(); @@ -44,7 +45,7 @@ public class EnableOemUnlockSettingWarningDialog extends InstrumentedDialogFragm @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DIALOG_ENABLE_OEM_UNLOCKING; + return SettingsEnums.DIALOG_ENABLE_OEM_UNLOCKING; } @Override diff --git a/src/com/android/settings/development/FileEncryptionPreferenceController.java b/src/com/android/settings/development/FileEncryptionPreferenceController.java index a988fdd904..82a58bad8c 100644 --- a/src/com/android/settings/development/FileEncryptionPreferenceController.java +++ b/src/com/android/settings/development/FileEncryptionPreferenceController.java @@ -20,10 +20,10 @@ import android.content.Context; import android.os.RemoteException; import android.os.ServiceManager; import android.os.storage.IStorageManager; +import android.text.TextUtils; import android.sysprop.CryptoProperties; import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; -import android.text.TextUtils; import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; diff --git a/src/com/android/settings/development/ForceGpuRenderingPreferenceController.java b/src/com/android/settings/development/ForceDarkPreferenceController.java index 82ab190551..90e67e0613 100644 --- a/src/com/android/settings/development/ForceGpuRenderingPreferenceController.java +++ b/src/com/android/settings/development/ForceDarkPreferenceController.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 The Android Open Source Project + * Copyright (C) 2018 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. @@ -18,51 +18,49 @@ package com.android.settings.development; import android.content.Context; import android.os.SystemProperties; -import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; +import android.view.ThreadedRenderer; + import androidx.preference.Preference; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; import com.android.settingslib.development.SystemPropPoker; -public class ForceGpuRenderingPreferenceController extends DeveloperOptionsPreferenceController +public class ForceDarkPreferenceController extends DeveloperOptionsPreferenceController implements Preference.OnPreferenceChangeListener, PreferenceControllerMixin { - private static final String FORCE_HARDWARE_UI_KEY = "force_hw_ui"; - - @VisibleForTesting - static final String HARDWARE_UI_PROPERTY = "persist.sys.ui.hw"; + private static final String HWUI_FORCE_DARK = "hwui_force_dark"; - public ForceGpuRenderingPreferenceController(Context context) { + public ForceDarkPreferenceController(Context context) { super(context); } @Override public String getPreferenceKey() { - return FORCE_HARDWARE_UI_KEY; + return HWUI_FORCE_DARK; } @Override public boolean onPreferenceChange(Preference preference, Object newValue) { final boolean isEnabled = (Boolean) newValue; - SystemProperties.set(HARDWARE_UI_PROPERTY, - isEnabled ? Boolean.toString(true) : Boolean.toString(false)); + SystemProperties.set(ThreadedRenderer.DEBUG_FORCE_DARK, + isEnabled ? "true" : null); SystemPropPoker.getInstance().poke(); return true; } @Override public void updateState(Preference preference) { - final boolean isEnabled = SystemProperties.getBoolean(HARDWARE_UI_PROPERTY, - false /* default */); + final boolean isEnabled = SystemProperties.getBoolean( + ThreadedRenderer.DEBUG_FORCE_DARK, false /* default */); ((SwitchPreference) mPreference).setChecked(isEnabled); } @Override protected void onDeveloperOptionsSwitchDisabled() { super.onDeveloperOptionsSwitchDisabled(); - SystemProperties.set(HARDWARE_UI_PROPERTY, Boolean.toString(false)); + SystemProperties.set(ThreadedRenderer.DEBUG_FORCE_DARK, null); ((SwitchPreference) mPreference).setChecked(false); } } diff --git a/src/com/android/settings/development/ForceMSAAPreferenceController.java b/src/com/android/settings/development/ForceMSAAPreferenceController.java index cb3afa57a0..a1a4d66317 100644 --- a/src/com/android/settings/development/ForceMSAAPreferenceController.java +++ b/src/com/android/settings/development/ForceMSAAPreferenceController.java @@ -18,9 +18,9 @@ package com.android.settings.development; import android.content.Context; import android.sysprop.DisplayProperties; -import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; + import androidx.preference.Preference; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; diff --git a/src/com/android/settings/development/FreeformWindowsPreferenceController.java b/src/com/android/settings/development/FreeformWindowsPreferenceController.java index e6c3813d44..4d38480946 100644 --- a/src/com/android/settings/development/FreeformWindowsPreferenceController.java +++ b/src/com/android/settings/development/FreeformWindowsPreferenceController.java @@ -19,10 +19,10 @@ package com.android.settings.development; import android.content.Context; import android.os.Build; import android.provider.Settings; + import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; import androidx.preference.Preference; -import android.text.TextUtils; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; @@ -36,19 +36,12 @@ public class FreeformWindowsPreferenceController extends DeveloperOptionsPrefere static final int SETTING_VALUE_OFF = 0; @VisibleForTesting static final int SETTING_VALUE_ON = 1; - @VisibleForTesting - static final String USER_BUILD_TYPE = "user"; public FreeformWindowsPreferenceController(Context context) { super(context); } @Override - public boolean isAvailable() { - return !TextUtils.equals(USER_BUILD_TYPE, getBuildType()); - } - - @Override public String getPreferenceKey() { return ENABLE_FREEFORM_SUPPORT_KEY; } diff --git a/src/com/android/settings/development/GlobalSettingSwitchPreferenceController.java b/src/com/android/settings/development/GlobalSettingSwitchPreferenceController.java index a7613763a7..d79c231e33 100644 --- a/src/com/android/settings/development/GlobalSettingSwitchPreferenceController.java +++ b/src/com/android/settings/development/GlobalSettingSwitchPreferenceController.java @@ -18,8 +18,9 @@ package com.android.settings.development; import android.content.Context; import android.provider.Settings; -import androidx.preference.SwitchPreference; + import androidx.preference.Preference; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; diff --git a/src/com/android/settings/development/GpuViewUpdatesPreferenceController.java b/src/com/android/settings/development/GpuViewUpdatesPreferenceController.java index 6b8ead5f5e..0087e28709 100644 --- a/src/com/android/settings/development/GpuViewUpdatesPreferenceController.java +++ b/src/com/android/settings/development/GpuViewUpdatesPreferenceController.java @@ -18,10 +18,11 @@ package com.android.settings.development; import android.content.Context; import android.os.SystemProperties; -import androidx.preference.SwitchPreference; -import androidx.preference.Preference; import android.view.ThreadedRenderer; +import androidx.preference.Preference; +import androidx.preference.SwitchPreference; + import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; import com.android.settingslib.development.SystemPropPoker; diff --git a/src/com/android/settings/development/HardwareLayersUpdatesPreferenceController.java b/src/com/android/settings/development/HardwareLayersUpdatesPreferenceController.java index 4c501b6fb7..8ffbdcdc1c 100644 --- a/src/com/android/settings/development/HardwareLayersUpdatesPreferenceController.java +++ b/src/com/android/settings/development/HardwareLayersUpdatesPreferenceController.java @@ -18,10 +18,11 @@ package com.android.settings.development; import android.content.Context; import android.os.SystemProperties; -import androidx.preference.SwitchPreference; -import androidx.preference.Preference; import android.view.ThreadedRenderer; +import androidx.preference.Preference; +import androidx.preference.SwitchPreference; + import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; import com.android.settingslib.development.SystemPropPoker; diff --git a/src/com/android/settings/development/HardwareOverlaysPreferenceController.java b/src/com/android/settings/development/HardwareOverlaysPreferenceController.java index 69f1ba1003..c10e1070a4 100644 --- a/src/com/android/settings/development/HardwareOverlaysPreferenceController.java +++ b/src/com/android/settings/development/HardwareOverlaysPreferenceController.java @@ -21,9 +21,10 @@ import android.os.IBinder; import android.os.Parcel; import android.os.RemoteException; import android.os.ServiceManager; + import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; import androidx.preference.Preference; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; diff --git a/src/com/android/settings/development/HdcpCheckingPreferenceController.java b/src/com/android/settings/development/HdcpCheckingPreferenceController.java index 6dcca77520..52fe8e04a7 100644 --- a/src/com/android/settings/development/HdcpCheckingPreferenceController.java +++ b/src/com/android/settings/development/HdcpCheckingPreferenceController.java @@ -19,10 +19,11 @@ package com.android.settings.development; import android.content.Context; import android.os.Build; import android.os.SystemProperties; +import android.text.TextUtils; + import androidx.annotation.VisibleForTesting; import androidx.preference.ListPreference; import androidx.preference.Preference; -import android.text.TextUtils; import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; diff --git a/src/com/android/settings/development/KeepActivitiesPreferenceController.java b/src/com/android/settings/development/KeepActivitiesPreferenceController.java index 839397b68e..0ba2c42dbc 100644 --- a/src/com/android/settings/development/KeepActivitiesPreferenceController.java +++ b/src/com/android/settings/development/KeepActivitiesPreferenceController.java @@ -21,10 +21,11 @@ import android.app.IActivityManager; import android.content.Context; import android.os.RemoteException; import android.provider.Settings; + import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; diff --git a/src/com/android/settings/development/LocalBackupPasswordPreferenceController.java b/src/com/android/settings/development/LocalBackupPasswordPreferenceController.java index 18e3a061cd..d970f4266e 100644 --- a/src/com/android/settings/development/LocalBackupPasswordPreferenceController.java +++ b/src/com/android/settings/development/LocalBackupPasswordPreferenceController.java @@ -21,6 +21,7 @@ import android.content.Context; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserManager; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; diff --git a/src/com/android/settings/development/LocalTerminalPreferenceController.java b/src/com/android/settings/development/LocalTerminalPreferenceController.java index be60ed7c18..d48615c7fc 100644 --- a/src/com/android/settings/development/LocalTerminalPreferenceController.java +++ b/src/com/android/settings/development/LocalTerminalPreferenceController.java @@ -3,14 +3,14 @@ package com.android.settings.development; import android.content.Context; import android.content.pm.PackageManager; import android.os.UserManager; + import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; -import com.android.settingslib.wrapper.PackageManagerWrapper; public class LocalTerminalPreferenceController extends DeveloperOptionsPreferenceController implements Preference.OnPreferenceChangeListener, PreferenceControllerMixin { @@ -20,7 +20,7 @@ public class LocalTerminalPreferenceController extends DeveloperOptionsPreferenc @VisibleForTesting static final String TERMINAL_APP_PACKAGE = "com.android.terminal"; - private PackageManagerWrapper mPackageManager; + private PackageManager mPackageManager; private UserManager mUserManager; public LocalTerminalPreferenceController(Context context) { @@ -43,7 +43,7 @@ public class LocalTerminalPreferenceController extends DeveloperOptionsPreferenc public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - mPackageManager = getPackageManagerWrapper(); + mPackageManager = getPackageManager(); if (isAvailable() && !isEnabled()) { mPreference.setEnabled(false); @@ -82,8 +82,8 @@ public class LocalTerminalPreferenceController extends DeveloperOptionsPreferenc } @VisibleForTesting - PackageManagerWrapper getPackageManagerWrapper() { - return new PackageManagerWrapper(mContext.getPackageManager()); + PackageManager getPackageManager() { + return mContext.getPackageManager(); } private boolean isPackageInstalled(String packageName) { diff --git a/src/com/android/settings/development/LogPersistPreferenceController.java b/src/com/android/settings/development/LogPersistPreferenceController.java index 9c7a0378cb..1386cec370 100644 --- a/src/com/android/settings/development/LogPersistPreferenceController.java +++ b/src/com/android/settings/development/LogPersistPreferenceController.java @@ -17,6 +17,7 @@ package com.android.settings.development; import android.content.Context; + import androidx.annotation.Nullable; import androidx.preference.Preference; diff --git a/src/com/android/settings/development/LogdSizePreferenceController.java b/src/com/android/settings/development/LogdSizePreferenceController.java index cae0766847..3a62e63175 100644 --- a/src/com/android/settings/development/LogdSizePreferenceController.java +++ b/src/com/android/settings/development/LogdSizePreferenceController.java @@ -17,6 +17,7 @@ package com.android.settings.development; import android.content.Context; + import androidx.preference.Preference; import com.android.settings.core.PreferenceControllerMixin; diff --git a/src/com/android/settings/development/MemoryUsagePreferenceController.java b/src/com/android/settings/development/MemoryUsagePreferenceController.java index 84120797c3..1b20e70a57 100644 --- a/src/com/android/settings/development/MemoryUsagePreferenceController.java +++ b/src/com/android/settings/development/MemoryUsagePreferenceController.java @@ -17,10 +17,11 @@ package com.android.settings.development; import android.content.Context; +import android.text.format.Formatter; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.text.format.Formatter; import com.android.settings.R; import com.android.settings.applications.ProcStatsData; diff --git a/src/com/android/settings/development/MobileDataAlwaysOnPreferenceController.java b/src/com/android/settings/development/MobileDataAlwaysOnPreferenceController.java index 031ff9efdd..b2fa693104 100644 --- a/src/com/android/settings/development/MobileDataAlwaysOnPreferenceController.java +++ b/src/com/android/settings/development/MobileDataAlwaysOnPreferenceController.java @@ -18,9 +18,10 @@ package com.android.settings.development; import android.content.Context; import android.provider.Settings; + import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; import androidx.preference.Preference; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; @@ -65,7 +66,7 @@ public class MobileDataAlwaysOnPreferenceController extends protected void onDeveloperOptionsSwitchDisabled() { super.onDeveloperOptionsSwitchDisabled(); Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.MOBILE_DATA_ALWAYS_ON, - SETTING_VALUE_OFF); - ((SwitchPreference) mPreference).setChecked(false); + SETTING_VALUE_ON); + ((SwitchPreference) mPreference).setChecked(true); } } diff --git a/src/com/android/settings/development/MockLocationAppPreferenceController.java b/src/com/android/settings/development/MockLocationAppPreferenceController.java index bbce58262d..45d6be8c68 100644 --- a/src/com/android/settings/development/MockLocationAppPreferenceController.java +++ b/src/com/android/settings/development/MockLocationAppPreferenceController.java @@ -16,8 +16,7 @@ package com.android.settings.development; -import static com.android.settings.development.DevelopmentOptionsActivityRequestCodes - .REQUEST_MOCK_LOCATION_APP; +import static com.android.settings.development.DevelopmentOptionsActivityRequestCodes.REQUEST_MOCK_LOCATION_APP; import android.Manifest; import android.app.Activity; @@ -26,13 +25,13 @@ import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; -import androidx.preference.Preference; import android.text.TextUtils; +import androidx.preference.Preference; + import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; -import com.android.settingslib.wrapper.PackageManagerWrapper; import java.util.List; @@ -44,7 +43,7 @@ public class MockLocationAppPreferenceController extends DeveloperOptionsPrefere private final DevelopmentSettingsDashboardFragment mFragment; private final AppOpsManager mAppsOpsManager; - private final PackageManagerWrapper mPackageManager; + private final PackageManager mPackageManager; public MockLocationAppPreferenceController(Context context, DevelopmentSettingsDashboardFragment fragment) { @@ -52,7 +51,7 @@ public class MockLocationAppPreferenceController extends DeveloperOptionsPrefere mFragment = fragment; mAppsOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); - mPackageManager = new PackageManagerWrapper(context.getPackageManager()); + mPackageManager = context.getPackageManager(); } @Override diff --git a/src/com/android/settings/development/NotificationChannelWarningsPreferenceController.java b/src/com/android/settings/development/NotificationChannelWarningsPreferenceController.java index 5e159c65d5..775b70871f 100644 --- a/src/com/android/settings/development/NotificationChannelWarningsPreferenceController.java +++ b/src/com/android/settings/development/NotificationChannelWarningsPreferenceController.java @@ -19,9 +19,10 @@ package com.android.settings.development; import android.content.Context; import android.os.Build; import android.provider.Settings; + import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; import androidx.preference.Preference; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; diff --git a/src/com/android/settings/development/OemLockInfoDialog.java b/src/com/android/settings/development/OemLockInfoDialog.java index 6d7581272d..c540daae68 100644 --- a/src/com/android/settings/development/OemLockInfoDialog.java +++ b/src/com/android/settings/development/OemLockInfoDialog.java @@ -16,13 +16,14 @@ package com.android.settings.development; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.Fragment; -import android.app.FragmentManager; +import android.app.settings.SettingsEnums; import android.os.Bundle; -import com.android.internal.logging.nano.MetricsProto; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; + import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; @@ -40,7 +41,7 @@ public class OemLockInfoDialog extends InstrumentedDialogFragment { @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DIALOG_OEM_LOCK_INFO; + return SettingsEnums.DIALOG_OEM_LOCK_INFO; } @Override diff --git a/src/com/android/settings/development/OemUnlockPreferenceController.java b/src/com/android/settings/development/OemUnlockPreferenceController.java index 243cd3f281..529970a0b4 100644 --- a/src/com/android/settings/development/OemUnlockPreferenceController.java +++ b/src/com/android/settings/development/OemUnlockPreferenceController.java @@ -16,20 +16,21 @@ package com.android.settings.development; -import static com.android.settings.development.DevelopmentOptionsActivityRequestCodes - .REQUEST_CODE_ENABLE_OEM_UNLOCK; +import static com.android.settings.development.DevelopmentOptionsActivityRequestCodes.REQUEST_CODE_ENABLE_OEM_UNLOCK; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.res.Resources; +import android.os.Build; import android.os.UserHandle; import android.os.UserManager; import android.service.oemlock.OemLockManager; +import android.telephony.TelephonyManager; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.telephony.TelephonyManager; import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; @@ -41,6 +42,7 @@ public class OemUnlockPreferenceController extends DeveloperOptionsPreferenceCon Preference.OnPreferenceChangeListener, PreferenceControllerMixin, OnActivityResultListener { private static final String PREFERENCE_KEY = "oem_unlock_enable"; + private static final String TAG = "OemUnlockPreferenceController"; private final OemLockManager mOemLockManager; private final UserManager mUserManager; @@ -52,7 +54,12 @@ public class OemUnlockPreferenceController extends DeveloperOptionsPreferenceCon public OemUnlockPreferenceController(Context context, Activity activity, DevelopmentSettingsDashboardFragment fragment) { super(context); - mOemLockManager = (OemLockManager) context.getSystemService(Context.OEM_LOCK_SERVICE); + + if (Build.IS_EMULATOR && Build.IS_ENG) { + mOemLockManager = null; + } else { + mOemLockManager = (OemLockManager) context.getSystemService(Context.OEM_LOCK_SERVICE); + } mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE); mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); mFragment = fragment; @@ -77,7 +84,7 @@ public class OemUnlockPreferenceController extends DeveloperOptionsPreferenceCon public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - mPreference = (RestrictedSwitchPreference) screen.findPreference(getPreferenceKey()); + mPreference = screen.findPreference(getPreferenceKey()); } @Override diff --git a/src/com/android/settings/development/OverlayCategoryPreferenceController.java b/src/com/android/settings/development/OverlayCategoryPreferenceController.java new file mode 100644 index 0000000000..6e0b2d00f6 --- /dev/null +++ b/src/com/android/settings/development/OverlayCategoryPreferenceController.java @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2018 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.development; + +import static android.os.UserHandle.USER_SYSTEM; + +import android.content.Context; +import android.content.om.IOverlayManager; +import android.content.om.OverlayInfo; +import android.content.pm.PackageManager; +import android.os.AsyncTask; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.text.TextUtils; +import android.util.Log; +import android.widget.Toast; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.ListPreference; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settingslib.development.DeveloperOptionsPreferenceController; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * Preference controller to allow users to choose an overlay from a list for a given category. + * The chosen overlay is enabled exclusively within its category. A default option is also + * exposed that disables all overlays in the given category. + */ +public class OverlayCategoryPreferenceController extends DeveloperOptionsPreferenceController + implements Preference.OnPreferenceChangeListener, PreferenceControllerMixin { + private static final String TAG = "OverlayCategoryPC"; + @VisibleForTesting + static final String PACKAGE_DEVICE_DEFAULT = "package_device_default"; + private static final String OVERLAY_TARGET_PACKAGE = "android"; + private static final Comparator<OverlayInfo> OVERLAY_INFO_COMPARATOR = + Comparator.comparingInt(a -> a.priority); + private final IOverlayManager mOverlayManager; + private final boolean mAvailable; + private final String mCategory; + private final PackageManager mPackageManager; + + private ListPreference mPreference; + + @VisibleForTesting + OverlayCategoryPreferenceController(Context context, PackageManager packageManager, + IOverlayManager overlayManager, String category) { + super(context); + mOverlayManager = overlayManager; + mPackageManager = packageManager; + mCategory = category; + mAvailable = overlayManager != null && !getOverlayInfos().isEmpty(); + } + + public OverlayCategoryPreferenceController(Context context, String category) { + this(context, context.getPackageManager(), IOverlayManager.Stub + .asInterface(ServiceManager.getService(Context.OVERLAY_SERVICE)), category); + } + + @Override + public boolean isAvailable() { + return mAvailable; + } + + @Override + public String getPreferenceKey() { + return mCategory; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + setPreference(screen.findPreference(getPreferenceKey())); + } + + @VisibleForTesting + void setPreference(ListPreference preference) { + mPreference = preference; + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + return setOverlay((String) newValue); + } + + private boolean setOverlay(String packageName) { + final String currentPackageName = getOverlayInfos().stream() + .filter(info -> info.isEnabled()) + .map(info -> info.packageName) + .findFirst() + .orElse(null); + + if (PACKAGE_DEVICE_DEFAULT.equals(packageName) && TextUtils.isEmpty(currentPackageName) + || TextUtils.equals(packageName, currentPackageName)) { + // Already set. + return true; + } + + new AsyncTask<Void, Void, Boolean>() { + @Override + protected Boolean doInBackground(Void... params) { + try { + if (PACKAGE_DEVICE_DEFAULT.equals(packageName)) { + return mOverlayManager.setEnabled(currentPackageName, false, USER_SYSTEM); + } else { + return mOverlayManager.setEnabledExclusiveInCategory(packageName, + USER_SYSTEM); + } + } catch (RemoteException re) { + Log.w(TAG, "Error enabling overlay.", re); + return false; + } + } + + @Override + protected void onPostExecute(Boolean success) { + updateState(mPreference); + if (!success) { + Toast.makeText( + mContext, R.string.overlay_toast_failed_to_apply, Toast.LENGTH_LONG) + .show(); + } + } + }.execute(); + + return true; // Assume success; toast on failure. + } + + @Override + public void updateState(Preference preference) { + final List<String> pkgs = new ArrayList<>(); + final List<String> labels = new ArrayList<>(); + + String selectedPkg = PACKAGE_DEVICE_DEFAULT; + String selectedLabel = mContext.getString(R.string.overlay_option_device_default); + + // Add the default package / label before all of the overlays + pkgs.add(selectedPkg); + labels.add(selectedLabel); + + for (OverlayInfo overlayInfo : getOverlayInfos()) { + pkgs.add(overlayInfo.packageName); + try { + labels.add(mPackageManager.getApplicationInfo(overlayInfo.packageName, 0) + .loadLabel(mPackageManager).toString()); + } catch (PackageManager.NameNotFoundException e) { + labels.add(overlayInfo.packageName); + } + if (overlayInfo.isEnabled()) { + selectedPkg = pkgs.get(pkgs.size() - 1); + selectedLabel = labels.get(labels.size() - 1); + } + } + + mPreference.setEntries(labels.toArray(new String[labels.size()])); + mPreference.setEntryValues(pkgs.toArray(new String[pkgs.size()])); + mPreference.setValue(selectedPkg); + mPreference.setSummary(selectedLabel); + } + + private List<OverlayInfo> getOverlayInfos() { + final List<OverlayInfo> filteredInfos = new ArrayList<>(); + try { + List<OverlayInfo> overlayInfos = mOverlayManager + .getOverlayInfosForTarget(OVERLAY_TARGET_PACKAGE, USER_SYSTEM); + for (OverlayInfo overlayInfo : overlayInfos) { + if (mCategory.equals(overlayInfo.category)) { + filteredInfos.add(overlayInfo); + } + } + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + filteredInfos.sort(OVERLAY_INFO_COMPARATOR); + return filteredInfos; + } + + @Override + protected void onDeveloperOptionsSwitchDisabled() { + super.onDeveloperOptionsSwitchDisabled(); + // TODO b/133222035: remove these developer settings when the + // Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES setting is used + setOverlay(PACKAGE_DEVICE_DEFAULT); + updateState(mPreference); + } + +} diff --git a/src/com/android/settings/development/PictureColorModePreferenceController.java b/src/com/android/settings/development/PictureColorModePreferenceController.java index fe07083e45..1acf5d02ba 100644 --- a/src/com/android/settings/development/PictureColorModePreferenceController.java +++ b/src/com/android/settings/development/PictureColorModePreferenceController.java @@ -17,6 +17,7 @@ package com.android.settings.development; import android.content.Context; + import androidx.annotation.VisibleForTesting; import androidx.preference.PreferenceScreen; @@ -55,7 +56,7 @@ public class PictureColorModePreferenceController extends DeveloperOptionsPrefer @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - mPreference = (ColorModePreference) screen.findPreference(getPreferenceKey()); + mPreference = screen.findPreference(getPreferenceKey()); if (mPreference != null) { mPreference.updateCurrentAndSupported(); } diff --git a/src/com/android/settings/development/PointerLocationPreferenceController.java b/src/com/android/settings/development/PointerLocationPreferenceController.java index 079184c7dd..0fd0137cb2 100644 --- a/src/com/android/settings/development/PointerLocationPreferenceController.java +++ b/src/com/android/settings/development/PointerLocationPreferenceController.java @@ -18,9 +18,10 @@ package com.android.settings.development; import android.content.Context; import android.provider.Settings; + import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; import androidx.preference.Preference; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; diff --git a/src/com/android/settings/development/ProfileGpuRenderingPreferenceController.java b/src/com/android/settings/development/ProfileGpuRenderingPreferenceController.java index ba9ce7746d..52e730ba36 100644 --- a/src/com/android/settings/development/ProfileGpuRenderingPreferenceController.java +++ b/src/com/android/settings/development/ProfileGpuRenderingPreferenceController.java @@ -18,11 +18,12 @@ package com.android.settings.development; import android.content.Context; import android.os.SystemProperties; -import androidx.preference.ListPreference; -import androidx.preference.Preference; import android.text.TextUtils; import android.view.ThreadedRenderer; +import androidx.preference.ListPreference; +import androidx.preference.Preference; + import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; diff --git a/src/com/android/settings/development/ResizableActivityPreferenceController.java b/src/com/android/settings/development/ResizableActivityPreferenceController.java index 9ae9216402..c74e995540 100644 --- a/src/com/android/settings/development/ResizableActivityPreferenceController.java +++ b/src/com/android/settings/development/ResizableActivityPreferenceController.java @@ -18,9 +18,10 @@ package com.android.settings.development; import android.content.Context; import android.provider.Settings; + import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; import androidx.preference.Preference; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; diff --git a/src/com/android/settings/development/RtlLayoutPreferenceController.java b/src/com/android/settings/development/RtlLayoutPreferenceController.java index 9abd997c81..913259efe1 100644 --- a/src/com/android/settings/development/RtlLayoutPreferenceController.java +++ b/src/com/android/settings/development/RtlLayoutPreferenceController.java @@ -19,9 +19,10 @@ package com.android.settings.development; import android.content.Context; import android.provider.Settings; import android.sysprop.DisplayProperties; + import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; import androidx.preference.Preference; +import androidx.preference.SwitchPreference; import com.android.internal.app.LocalePicker; import com.android.settings.core.PreferenceControllerMixin; diff --git a/src/com/android/settings/development/SecondaryDisplayPreferenceController.java b/src/com/android/settings/development/SecondaryDisplayPreferenceController.java index 21f9881ec0..3e1653d8a4 100644 --- a/src/com/android/settings/development/SecondaryDisplayPreferenceController.java +++ b/src/com/android/settings/development/SecondaryDisplayPreferenceController.java @@ -18,9 +18,10 @@ package com.android.settings.development; import android.content.Context; import android.provider.Settings; +import android.text.TextUtils; + import androidx.preference.ListPreference; import androidx.preference.Preference; -import android.text.TextUtils; import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; diff --git a/src/com/android/settings/development/SecureSettingSwitchPreferenceController.java b/src/com/android/settings/development/SecureSettingSwitchPreferenceController.java index 8974c0a8ef..a72c0e05e1 100644 --- a/src/com/android/settings/development/SecureSettingSwitchPreferenceController.java +++ b/src/com/android/settings/development/SecureSettingSwitchPreferenceController.java @@ -18,8 +18,9 @@ package com.android.settings.development; import android.content.Context; import android.provider.Settings; -import androidx.preference.SwitchPreference; + import androidx.preference.Preference; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; diff --git a/src/com/android/settings/development/SelectDebugAppPreferenceController.java b/src/com/android/settings/development/SelectDebugAppPreferenceController.java index 823bf2e9fa..b882c70c61 100644 --- a/src/com/android/settings/development/SelectDebugAppPreferenceController.java +++ b/src/com/android/settings/development/SelectDebugAppPreferenceController.java @@ -16,8 +16,7 @@ package com.android.settings.development; -import static com.android.settings.development.DevelopmentOptionsActivityRequestCodes - .REQUEST_CODE_DEBUG_APP; +import static com.android.settings.development.DevelopmentOptionsActivityRequestCodes.REQUEST_CODE_DEBUG_APP; import android.app.Activity; import android.content.Context; @@ -25,13 +24,13 @@ import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.provider.Settings; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; -import com.android.settingslib.wrapper.PackageManagerWrapper; public class SelectDebugAppPreferenceController extends DeveloperOptionsPreferenceController implements PreferenceControllerMixin, OnActivityResultListener { @@ -39,13 +38,13 @@ public class SelectDebugAppPreferenceController extends DeveloperOptionsPreferen private static final String DEBUG_APP_KEY = "debug_app"; private final DevelopmentSettingsDashboardFragment mFragment; - private final PackageManagerWrapper mPackageManager; + private final PackageManager mPackageManager; public SelectDebugAppPreferenceController(Context context, DevelopmentSettingsDashboardFragment fragment) { super(context); mFragment = fragment; - mPackageManager = new PackageManagerWrapper(mContext.getPackageManager()); + mPackageManager = mContext.getPackageManager(); } @Override diff --git a/src/com/android/settings/development/ShortcutManagerThrottlingPreferenceController.java b/src/com/android/settings/development/ShortcutManagerThrottlingPreferenceController.java index f7f5b6ddd1..05ddf3abba 100644 --- a/src/com/android/settings/development/ShortcutManagerThrottlingPreferenceController.java +++ b/src/com/android/settings/development/ShortcutManagerThrottlingPreferenceController.java @@ -20,11 +20,12 @@ import android.content.Context; import android.content.pm.IShortcutService; import android.os.RemoteException; import android.os.ServiceManager; -import androidx.preference.Preference; import android.text.TextUtils; import android.util.Log; import android.widget.Toast; +import androidx.preference.Preference; + import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; diff --git a/src/com/android/settings/development/ShowFirstCrashDialogPreferenceController.java b/src/com/android/settings/development/ShowFirstCrashDialogPreferenceController.java index d0df9c8d3a..5e0cda8294 100644 --- a/src/com/android/settings/development/ShowFirstCrashDialogPreferenceController.java +++ b/src/com/android/settings/development/ShowFirstCrashDialogPreferenceController.java @@ -18,9 +18,10 @@ package com.android.settings.development; import android.content.Context; import android.provider.Settings; + import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; import androidx.preference.Preference; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; diff --git a/src/com/android/settings/development/ShowLayoutBoundsPreferenceController.java b/src/com/android/settings/development/ShowLayoutBoundsPreferenceController.java index 67be6f8e6f..f78a2970c1 100644 --- a/src/com/android/settings/development/ShowLayoutBoundsPreferenceController.java +++ b/src/com/android/settings/development/ShowLayoutBoundsPreferenceController.java @@ -18,9 +18,9 @@ package com.android.settings.development; import android.content.Context; import android.sysprop.DisplayProperties; -import androidx.preference.SwitchPreference; + import androidx.preference.Preference; -import android.view.View; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; diff --git a/src/com/android/settings/development/ShowSurfaceUpdatesPreferenceController.java b/src/com/android/settings/development/ShowSurfaceUpdatesPreferenceController.java index ffd3da27cd..5dadb6ff6c 100644 --- a/src/com/android/settings/development/ShowSurfaceUpdatesPreferenceController.java +++ b/src/com/android/settings/development/ShowSurfaceUpdatesPreferenceController.java @@ -21,9 +21,10 @@ import android.os.IBinder; import android.os.Parcel; import android.os.RemoteException; import android.os.ServiceManager; + import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; import androidx.preference.Preference; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; diff --git a/src/com/android/settings/development/ShowTapsPreferenceController.java b/src/com/android/settings/development/ShowTapsPreferenceController.java index 87fbf6928b..25d421ddf2 100644 --- a/src/com/android/settings/development/ShowTapsPreferenceController.java +++ b/src/com/android/settings/development/ShowTapsPreferenceController.java @@ -18,9 +18,10 @@ package com.android.settings.development; import android.content.Context; import android.provider.Settings; + import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; import androidx.preference.Preference; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; diff --git a/src/com/android/settings/development/SimulateColorSpacePreferenceController.java b/src/com/android/settings/development/SimulateColorSpacePreferenceController.java index ed16b685b7..56fa76d379 100644 --- a/src/com/android/settings/development/SimulateColorSpacePreferenceController.java +++ b/src/com/android/settings/development/SimulateColorSpacePreferenceController.java @@ -20,10 +20,11 @@ import android.content.ContentResolver; import android.content.Context; import android.content.res.Resources; import android.provider.Settings; +import android.view.accessibility.AccessibilityManager; + import androidx.annotation.VisibleForTesting; import androidx.preference.ListPreference; import androidx.preference.Preference; -import android.view.accessibility.AccessibilityManager; import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; diff --git a/src/com/android/settings/development/StayAwakePreferenceController.java b/src/com/android/settings/development/StayAwakePreferenceController.java index 31eb71aff8..25a92b260b 100644 --- a/src/com/android/settings/development/StayAwakePreferenceController.java +++ b/src/com/android/settings/development/StayAwakePreferenceController.java @@ -23,12 +23,14 @@ import android.net.Uri; import android.os.BatteryManager; import android.os.Handler; import android.provider.Settings; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.RestrictedSwitchPreference; import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.core.lifecycle.LifecycleObserver; @@ -38,7 +40,7 @@ import com.android.settingslib.development.DeveloperOptionsPreferenceController; public class StayAwakePreferenceController extends DeveloperOptionsPreferenceController - implements Preference.OnPreferenceChangeListener, LifecycleObserver, OnResume, OnPause, + implements Preference.OnPreferenceChangeListener, LifecycleObserver, OnResume, OnPause, PreferenceControllerMixin { private static final String TAG = "StayAwakeCtrl"; @@ -70,7 +72,7 @@ public class StayAwakePreferenceController extends DeveloperOptionsPreferenceCon @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - mPreference = (RestrictedSwitchPreference) screen.findPreference(getPreferenceKey()); + mPreference = screen.findPreference(getPreferenceKey()); } @Override @@ -129,7 +131,7 @@ public class StayAwakePreferenceController extends DeveloperOptionsPreferenceCon // will lock... in this case we can't allow the user to turn // on "stay awake when plugged in" because that would defeat the // restriction. - return RestrictedLockUtils.checkIfMaximumTimeToLockIsSet(mContext); + return RestrictedLockUtilsInternal.checkIfMaximumTimeToLockIsSet(mContext); } @VisibleForTesting diff --git a/src/com/android/settings/development/StrictModePreferenceController.java b/src/com/android/settings/development/StrictModePreferenceController.java index 2af301c0d6..c28e476621 100644 --- a/src/com/android/settings/development/StrictModePreferenceController.java +++ b/src/com/android/settings/development/StrictModePreferenceController.java @@ -21,10 +21,11 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.StrictMode; import android.os.SystemProperties; +import android.view.IWindowManager; + import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; import androidx.preference.Preference; -import android.view.IWindowManager; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; diff --git a/src/com/android/settings/development/SystemServerHeapDumpPreferenceController.java b/src/com/android/settings/development/SystemServerHeapDumpPreferenceController.java new file mode 100644 index 0000000000..e6701ce248 --- /dev/null +++ b/src/com/android/settings/development/SystemServerHeapDumpPreferenceController.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2019 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.development; + +import android.app.ActivityManager; +import android.content.Context; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.os.RemoteException; +import android.os.UserManager; +import android.util.Log; +import android.widget.Toast; + +import androidx.preference.Preference; + +import com.android.settings.R; +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settingslib.development.DeveloperOptionsPreferenceController; + +public class SystemServerHeapDumpPreferenceController extends DeveloperOptionsPreferenceController + implements PreferenceControllerMixin { + + private static final String KEY_SYSTEM_SERVER_HEAP_DUMP = "system_server_heap_dump"; + + /** How long to keep the preference disabled before re-enabling. */ + private static final long ENABLE_TIMEOUT_MILLIS = 5000L; + + private final UserManager mUserManager; + + private Handler mHandler; + + public SystemServerHeapDumpPreferenceController(Context context) { + super(context); + + mUserManager = context.getSystemService(UserManager.class); + mHandler = new Handler(Looper.getMainLooper()); + } + + @Override + public boolean isAvailable() { + return Build.IS_DEBUGGABLE + && !mUserManager.hasUserRestriction(UserManager.DISALLOW_DEBUGGING_FEATURES); + } + + @Override + public String getPreferenceKey() { + return KEY_SYSTEM_SERVER_HEAP_DUMP; + } + + @Override + public boolean handlePreferenceTreeClick(Preference preference) { + if (!KEY_SYSTEM_SERVER_HEAP_DUMP.equals(preference.getKey())) { + return false; + } + try { + // Temporarily disable the preference so the user doesn't start two dumps in a row. + preference.setEnabled(false); + Toast.makeText(mContext, R.string.capturing_system_heap_dump_message, + Toast.LENGTH_SHORT).show(); + ActivityManager.getService().requestSystemServerHeapDump(); + mHandler.postDelayed(() -> preference.setEnabled(true), ENABLE_TIMEOUT_MILLIS); + return true; + } catch (RemoteException e) { + Log.e(TAG, "error taking system heap dump", e); + Toast.makeText(mContext, R.string.error_capturing_system_heap_dump_message, + Toast.LENGTH_SHORT).show(); + } + return false; + } +} diff --git a/src/com/android/settings/development/SystemSettingSwitchPreferenceController.java b/src/com/android/settings/development/SystemSettingSwitchPreferenceController.java index 7174b457e0..476cf50ffe 100644 --- a/src/com/android/settings/development/SystemSettingSwitchPreferenceController.java +++ b/src/com/android/settings/development/SystemSettingSwitchPreferenceController.java @@ -18,8 +18,9 @@ package com.android.settings.development; import android.content.Context; import android.provider.Settings; -import androidx.preference.SwitchPreference; + import androidx.preference.Preference; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; diff --git a/src/com/android/settings/development/TetheringHardwareAccelPreferenceController.java b/src/com/android/settings/development/TetheringHardwareAccelPreferenceController.java index 72b0bf3a31..f838a76cfa 100644 --- a/src/com/android/settings/development/TetheringHardwareAccelPreferenceController.java +++ b/src/com/android/settings/development/TetheringHardwareAccelPreferenceController.java @@ -18,9 +18,10 @@ package com.android.settings.development; import android.content.Context; import android.provider.Settings; + import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; import androidx.preference.Preference; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; diff --git a/src/com/android/settings/development/TransitionAnimationScalePreferenceController.java b/src/com/android/settings/development/TransitionAnimationScalePreferenceController.java index 6f57f88834..0a8e941ea5 100644 --- a/src/com/android/settings/development/TransitionAnimationScalePreferenceController.java +++ b/src/com/android/settings/development/TransitionAnimationScalePreferenceController.java @@ -19,10 +19,11 @@ package com.android.settings.development; import android.content.Context; import android.os.RemoteException; import android.os.ServiceManager; +import android.view.IWindowManager; + import androidx.annotation.VisibleForTesting; import androidx.preference.ListPreference; import androidx.preference.Preference; -import android.view.IWindowManager; import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; diff --git a/src/com/android/settings/development/TrustAgentsExtendUnlockPreferenceController.java b/src/com/android/settings/development/TrustAgentsExtendUnlockPreferenceController.java new file mode 100644 index 0000000000..0834f9b96e --- /dev/null +++ b/src/com/android/settings/development/TrustAgentsExtendUnlockPreferenceController.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2018 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.development; + +import android.content.Context; +import android.provider.Settings; + +import androidx.preference.Preference; +import androidx.preference.SwitchPreference; + +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settingslib.development.DeveloperOptionsPreferenceController; + +public class TrustAgentsExtendUnlockPreferenceController extends + DeveloperOptionsPreferenceController implements + Preference.OnPreferenceChangeListener, PreferenceControllerMixin { + + private static final String KEY_TRUST_AGENTS_EXTEND_UNLOCK = + "security_setting_trust_agents_extend_unlock"; + + public TrustAgentsExtendUnlockPreferenceController(Context context) { + super(context); + } + + @Override + public String getPreferenceKey() { + return KEY_TRUST_AGENTS_EXTEND_UNLOCK; + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + final boolean isEnabled = (Boolean) newValue; + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.TRUST_AGENTS_EXTEND_UNLOCK, isEnabled ? 1 : 0); + return true; + } + + @Override + public void updateState(Preference preference) { + int trustAgentsExtendUnlock = Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.TRUST_AGENTS_EXTEND_UNLOCK, 0); + ((SwitchPreference) mPreference).setChecked(trustAgentsExtendUnlock != 0); + } +} diff --git a/src/com/android/settings/development/TrustLostLocksScreenPreferenceController.java b/src/com/android/settings/development/TrustLostLocksScreenPreferenceController.java new file mode 100644 index 0000000000..3800fd6b88 --- /dev/null +++ b/src/com/android/settings/development/TrustLostLocksScreenPreferenceController.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2018 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.development; + +import android.content.Context; +import android.provider.Settings; + +import androidx.preference.Preference; +import androidx.preference.SwitchPreference; + +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settingslib.development.DeveloperOptionsPreferenceController; + +public class TrustLostLocksScreenPreferenceController + extends DeveloperOptionsPreferenceController implements + Preference.OnPreferenceChangeListener, PreferenceControllerMixin { + + private static final String KEY_TRUST_LOST_LOCKS_SCREEN = + "security_setting_trust_lost_locks_screen"; + + public TrustLostLocksScreenPreferenceController(Context context) { + super(context); + } + + @Override + public String getPreferenceKey() { + return KEY_TRUST_LOST_LOCKS_SCREEN; + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + final boolean isEnabled = (Boolean) newValue; + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.LOCK_SCREEN_WHEN_TRUST_LOST, isEnabled ? 1 : 0); + return true; + } + + @Override + public void updateState(Preference preference) { + int lockOnTrustLost = Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.LOCK_SCREEN_WHEN_TRUST_LOST, 0); + ((SwitchPreference) mPreference).setChecked(lockOnTrustLost != 0); + } +} diff --git a/src/com/android/settings/development/UsbAudioRoutingPreferenceController.java b/src/com/android/settings/development/UsbAudioRoutingPreferenceController.java index c73412dbb3..335a48de82 100644 --- a/src/com/android/settings/development/UsbAudioRoutingPreferenceController.java +++ b/src/com/android/settings/development/UsbAudioRoutingPreferenceController.java @@ -18,9 +18,10 @@ package com.android.settings.development; import android.content.Context; import android.provider.Settings; + import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; import androidx.preference.Preference; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; diff --git a/src/com/android/settings/development/VerifyAppsOverUsbPreferenceController.java b/src/com/android/settings/development/VerifyAppsOverUsbPreferenceController.java index f151134705..20b8f1f002 100644 --- a/src/com/android/settings/development/VerifyAppsOverUsbPreferenceController.java +++ b/src/com/android/settings/development/VerifyAppsOverUsbPreferenceController.java @@ -18,19 +18,20 @@ package com.android.settings.development; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import com.android.settings.core.PreferenceControllerMixin; -import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; +import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.RestrictedSwitchPreference; import com.android.settingslib.development.DeveloperOptionsPreferenceController; -import com.android.settingslib.wrapper.PackageManagerWrapper; import java.util.List; @@ -56,7 +57,8 @@ public class VerifyAppsOverUsbPreferenceController extends DeveloperOptionsPrefe class RestrictedLockUtilsDelegate { public EnforcedAdmin checkIfRestrictionEnforced( Context context, String userRestriction, int userId) { - return RestrictedLockUtils.checkIfRestrictionEnforced(context, userRestriction, userId); + return RestrictedLockUtilsInternal.checkIfRestrictionEnforced(context, userRestriction, + userId); } } @@ -65,12 +67,12 @@ public class VerifyAppsOverUsbPreferenceController extends DeveloperOptionsPrefe new RestrictedLockUtilsDelegate(); // This field is accessed using reflection in the test, please keep name in sync. - private final PackageManagerWrapper mPackageManager; + private final PackageManager mPackageManager; public VerifyAppsOverUsbPreferenceController(Context context) { super(context); - mPackageManager = new PackageManagerWrapper(context.getPackageManager()); + mPackageManager = context.getPackageManager(); } @Override diff --git a/src/com/android/settings/development/WaitForDebuggerPreferenceController.java b/src/com/android/settings/development/WaitForDebuggerPreferenceController.java index 9959f12ce1..80cb225002 100644 --- a/src/com/android/settings/development/WaitForDebuggerPreferenceController.java +++ b/src/com/android/settings/development/WaitForDebuggerPreferenceController.java @@ -16,8 +16,7 @@ package com.android.settings.development; -import static com.android.settings.development.DevelopmentOptionsActivityRequestCodes - .REQUEST_CODE_DEBUG_APP; +import static com.android.settings.development.DevelopmentOptionsActivityRequestCodes.REQUEST_CODE_DEBUG_APP; import android.app.Activity; import android.app.ActivityManager; @@ -26,10 +25,11 @@ import android.content.Context; import android.content.Intent; import android.os.RemoteException; import android.provider.Settings; +import android.text.TextUtils; + import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; import androidx.preference.Preference; -import android.text.TextUtils; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; diff --git a/src/com/android/settings/development/WebViewAppPreferenceController.java b/src/com/android/settings/development/WebViewAppPreferenceController.java index 7796cfda35..37653ccfd9 100644 --- a/src/com/android/settings/development/WebViewAppPreferenceController.java +++ b/src/com/android/settings/development/WebViewAppPreferenceController.java @@ -18,17 +18,19 @@ package com.android.settings.development; import android.content.Context; import android.content.pm.PackageInfo; -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; +import android.content.pm.PackageManager; +import android.os.UserHandle; import android.text.TextUtils; import android.util.Log; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; + import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.webview.WebViewUpdateServiceWrapper; import com.android.settingslib.applications.DefaultAppInfo; import com.android.settingslib.development.DeveloperOptionsPreferenceController; -import com.android.settingslib.wrapper.PackageManagerWrapper; public class WebViewAppPreferenceController extends DeveloperOptionsPreferenceController implements PreferenceControllerMixin { @@ -36,13 +38,13 @@ public class WebViewAppPreferenceController extends DeveloperOptionsPreferenceCo private static final String TAG = "WebViewAppPrefCtrl"; private static final String WEBVIEW_APP_KEY = "select_webview_provider"; - private final PackageManagerWrapper mPackageManager; + private final PackageManager mPackageManager; private final WebViewUpdateServiceWrapper mWebViewUpdateServiceWrapper; public WebViewAppPreferenceController(Context context) { super(context); - mPackageManager = new PackageManagerWrapper(context.getPackageManager()); + mPackageManager = context.getPackageManager(); mWebViewUpdateServiceWrapper = new WebViewUpdateServiceWrapper(); } @@ -65,7 +67,7 @@ public class WebViewAppPreferenceController extends DeveloperOptionsPreferenceCo @VisibleForTesting DefaultAppInfo getDefaultAppInfo() { final PackageInfo currentPackage = mWebViewUpdateServiceWrapper.getCurrentWebViewPackage(); - return new DefaultAppInfo(mContext, mPackageManager, + return new DefaultAppInfo(mContext, mPackageManager, UserHandle.myUserId(), currentPackage == null ? null : currentPackage.applicationInfo); } diff --git a/src/com/android/settings/development/WifiDisplayCertificationPreferenceController.java b/src/com/android/settings/development/WifiDisplayCertificationPreferenceController.java index 71ca8f1956..0236f1585e 100644 --- a/src/com/android/settings/development/WifiDisplayCertificationPreferenceController.java +++ b/src/com/android/settings/development/WifiDisplayCertificationPreferenceController.java @@ -18,9 +18,10 @@ package com.android.settings.development; import android.content.Context; import android.provider.Settings; + import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; import androidx.preference.Preference; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; diff --git a/src/com/android/settings/development/WifiScanThrottlingPreferenceController.java b/src/com/android/settings/development/WifiScanThrottlingPreferenceController.java new file mode 100644 index 0000000000..a069827601 --- /dev/null +++ b/src/com/android/settings/development/WifiScanThrottlingPreferenceController.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2019 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.development; + +import android.content.Context; +import android.provider.Settings; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.SwitchPreference; + +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settingslib.development.DeveloperOptionsPreferenceController; + +public class WifiScanThrottlingPreferenceController extends DeveloperOptionsPreferenceController + implements Preference.OnPreferenceChangeListener, PreferenceControllerMixin { + private static final String WIFI_SCAN_THROTTLING_KEY = "wifi_scan_throttling"; + @VisibleForTesting + static final int SETTING_THROTTLING_ENABLE_VALUE_ON = 1; // default is throttling enabled. + @VisibleForTesting + static final int SETTING_THROTTLING_ENABLE_VALUE_OFF = 0; + + public WifiScanThrottlingPreferenceController(Context context) { + super(context); + } + + @Override + public String getPreferenceKey() { + return WIFI_SCAN_THROTTLING_KEY; + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + final boolean isEnabled = (Boolean) newValue; + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.WIFI_SCAN_THROTTLE_ENABLED, + isEnabled + ? SETTING_THROTTLING_ENABLE_VALUE_ON + : SETTING_THROTTLING_ENABLE_VALUE_OFF); + return true; + } + + @Override + public void updateState(Preference preference) { + final int scanThrottleEnabled = Settings.Global.getInt( + mContext.getContentResolver(), Settings.Global.WIFI_SCAN_THROTTLE_ENABLED, + SETTING_THROTTLING_ENABLE_VALUE_ON); + ((SwitchPreference) mPreference).setChecked( + scanThrottleEnabled == SETTING_THROTTLING_ENABLE_VALUE_ON); + } + + @Override + protected void onDeveloperOptionsSwitchDisabled() { + super.onDeveloperOptionsSwitchDisabled(); + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.WIFI_SCAN_THROTTLE_ENABLED, SETTING_THROTTLING_ENABLE_VALUE_ON); + ((SwitchPreference) mPreference).setChecked(true); + } +} diff --git a/src/com/android/settings/development/WifiVerboseLoggingPreferenceController.java b/src/com/android/settings/development/WifiVerboseLoggingPreferenceController.java index c3c9ca3ec1..de53a2d714 100644 --- a/src/com/android/settings/development/WifiVerboseLoggingPreferenceController.java +++ b/src/com/android/settings/development/WifiVerboseLoggingPreferenceController.java @@ -18,9 +18,10 @@ package com.android.settings.development; import android.content.Context; import android.net.wifi.WifiManager; + import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; import androidx.preference.Preference; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; diff --git a/src/com/android/settings/development/WindowAnimationScalePreferenceController.java b/src/com/android/settings/development/WindowAnimationScalePreferenceController.java index 6414372470..70b2e2de18 100644 --- a/src/com/android/settings/development/WindowAnimationScalePreferenceController.java +++ b/src/com/android/settings/development/WindowAnimationScalePreferenceController.java @@ -19,10 +19,11 @@ package com.android.settings.development; import android.content.Context; import android.os.RemoteException; import android.os.ServiceManager; +import android.view.IWindowManager; + import androidx.annotation.VisibleForTesting; import androidx.preference.ListPreference; import androidx.preference.Preference; -import android.view.IWindowManager; import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; diff --git a/src/com/android/settings/development/autofill/AbstractGlobalSettingsPreference.java b/src/com/android/settings/development/autofill/AbstractGlobalSettingsPreference.java new file mode 100644 index 0000000000..491deac9bb --- /dev/null +++ b/src/com/android/settings/development/autofill/AbstractGlobalSettingsPreference.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2018 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.development.autofill; + +import android.content.Context; +import android.provider.Settings; +import android.text.InputType; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.EditText; + +import com.android.settings.Utils; +import com.android.settingslib.CustomEditTextPreferenceCompat; + +/** + * Base class for Autofill integer properties that are backed by + * {@link android.provider.Settings.Global}. + */ +abstract class AbstractGlobalSettingsPreference extends CustomEditTextPreferenceCompat { + + private static final String TAG = "AbstractGlobalSettingsPreference"; + + private final String mKey; + private final int mDefaultValue; + + private final AutofillDeveloperSettingsObserver mObserver; + + protected AbstractGlobalSettingsPreference(Context context, AttributeSet attrs, + String key, int defaultValue) { + super(context, attrs); + + mKey = key; + mDefaultValue = defaultValue; + mObserver = new AutofillDeveloperSettingsObserver(context, () -> updateSummary()); + } + + @Override + public void onAttached() { + super.onAttached(); + + mObserver.register(); + updateSummary(); + } + + @Override + public void onDetached() { + mObserver.unregister(); + + super.onDetached(); + } + + private String getCurrentValue() { + final int value = Settings.Global.getInt(getContext().getContentResolver(), + mKey, mDefaultValue); + + return Integer.toString(value); + } + + private void updateSummary() { + setSummary(getCurrentValue()); + + } + + @Override + protected void onBindDialogView(View view) { + super.onBindDialogView(view); + + EditText editText = view.findViewById(android.R.id.edit); + if (editText != null) { + editText.setInputType(InputType.TYPE_CLASS_NUMBER); + editText.setText(getCurrentValue()); + Utils.setEditTextCursorPosition(editText); + } + } + + @Override + protected void onDialogClosed(boolean positiveResult) { + if (positiveResult) { + final String stringValue = getText(); + int newValue = mDefaultValue; + try { + newValue = Integer.parseInt(stringValue); + } catch (Exception e) { + Log.e(TAG, "Error converting '" + stringValue + "' to integer. Using " + + mDefaultValue + " instead"); + } + Settings.Global.putInt(getContext().getContentResolver(), mKey, newValue); + } + } +} diff --git a/src/com/android/settings/development/autofill/AutofillDeveloperSettingsObserver.java b/src/com/android/settings/development/autofill/AutofillDeveloperSettingsObserver.java new file mode 100644 index 0000000000..90266e03a6 --- /dev/null +++ b/src/com/android/settings/development/autofill/AutofillDeveloperSettingsObserver.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2018 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.development.autofill; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.os.UserHandle; +import android.provider.Settings; + +final class AutofillDeveloperSettingsObserver extends ContentObserver { + + private final Runnable mChangeCallback; + private final ContentResolver mResolver; + + public AutofillDeveloperSettingsObserver(Context context, Runnable changeCallback) { + super(new Handler(Looper.getMainLooper())); + + mResolver = context.getContentResolver(); + mChangeCallback = changeCallback; + } + + public void register() { + mResolver.registerContentObserver(Settings.Global.getUriFor( + Settings.Global.AUTOFILL_LOGGING_LEVEL), false, this, + UserHandle.USER_ALL); + mResolver.registerContentObserver(Settings.Global.getUriFor( + Settings.Global.AUTOFILL_MAX_PARTITIONS_SIZE), false, this, + UserHandle.USER_ALL); + mResolver.registerContentObserver(Settings.Global.getUriFor( + Settings.Global.AUTOFILL_MAX_VISIBLE_DATASETS), false, this, + UserHandle.USER_ALL); + } + + public void unregister() { + mResolver.unregisterContentObserver(this); + } + + @Override + public void onChange(boolean selfChange, Uri uri, int userId) { + mChangeCallback.run(); // Run Forrest, Run! + } +} diff --git a/src/com/android/settings/development/autofill/AutofillLoggingLevelPreferenceController.java b/src/com/android/settings/development/autofill/AutofillLoggingLevelPreferenceController.java new file mode 100644 index 0000000000..f955f5e00a --- /dev/null +++ b/src/com/android/settings/development/autofill/AutofillLoggingLevelPreferenceController.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2018 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.development.autofill; + +import android.content.Context; +import android.content.res.Resources; +import android.provider.Settings; +import android.util.Log; +import android.view.autofill.AutofillManager; + +import androidx.preference.ListPreference; +import androidx.preference.Preference; + +import com.android.settings.R; +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnDestroy; +import com.android.settingslib.development.DeveloperOptionsPreferenceController; + +public final class AutofillLoggingLevelPreferenceController + extends DeveloperOptionsPreferenceController + implements PreferenceControllerMixin, Preference.OnPreferenceChangeListener, + LifecycleObserver, OnDestroy { + + private static final String TAG = "AutofillLoggingLevelPreferenceController"; + private static final String AUTOFILL_LOGGING_LEVEL_KEY = "autofill_logging_level"; + + private final String[] mListValues; + private final String[] mListSummaries; + private final AutofillDeveloperSettingsObserver mObserver; + + public AutofillLoggingLevelPreferenceController(Context context, Lifecycle lifecycle) { + super(context); + + Resources resources = context.getResources(); + mListValues = resources.getStringArray(R.array.autofill_logging_level_values); + mListSummaries = resources.getStringArray(R.array.autofill_logging_level_entries); + mObserver = new AutofillDeveloperSettingsObserver(mContext, () -> updateOptions()); + mObserver.register(); + + if (lifecycle != null) { + lifecycle.addObserver(this); + } + } + + @Override + public void onDestroy() { + mObserver.unregister(); + } + + @Override + public String getPreferenceKey() { + return AUTOFILL_LOGGING_LEVEL_KEY; + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + writeLevel(newValue); + updateOptions(); + return true; + } + + @Override + public void updateState(Preference preference) { + updateOptions(); + } + + @Override + protected void onDeveloperOptionsSwitchDisabled() { + super.onDeveloperOptionsSwitchDisabled(); + writeLevel(null); + } + + private void updateOptions() { + if (mPreference == null) { + // TODO: there should be a hook on AbstractPreferenceController where we could + // unregister mObserver and avoid this check + Log.v(TAG, "ignoring Settings update because UI is gone"); + return; + } + final int level = Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.AUTOFILL_LOGGING_LEVEL, AutofillManager.DEFAULT_LOGGING_LEVEL); + + final int index; + if (level == AutofillManager.FLAG_ADD_CLIENT_DEBUG) { + index = 1; + } else if (level == AutofillManager.FLAG_ADD_CLIENT_VERBOSE) { + index = 2; + } else { + index = 0; + } + final ListPreference listPreference = (ListPreference) mPreference; + listPreference.setValue(mListValues[index]); + listPreference.setSummary(mListSummaries[index]); + } + + private void writeLevel(Object newValue) { + int level = AutofillManager.NO_LOGGING; + if (newValue instanceof String) { + level = Integer.parseInt((String) newValue); + } + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.AUTOFILL_LOGGING_LEVEL, level); + } +} diff --git a/src/com/android/settings/development/autofill/AutofillMaxPartitionsPreference.java b/src/com/android/settings/development/autofill/AutofillMaxPartitionsPreference.java new file mode 100644 index 0000000000..f8099b7fa6 --- /dev/null +++ b/src/com/android/settings/development/autofill/AutofillMaxPartitionsPreference.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2018 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.development.autofill; + +import android.content.Context; +import android.provider.Settings; +import android.util.AttributeSet; +import android.view.autofill.AutofillManager; + +public final class AutofillMaxPartitionsPreference extends AbstractGlobalSettingsPreference { + + public AutofillMaxPartitionsPreference(Context context, AttributeSet attrs) { + super(context, attrs, Settings.Global.AUTOFILL_MAX_PARTITIONS_SIZE, + AutofillManager.DEFAULT_MAX_PARTITIONS_SIZE); + } +} diff --git a/src/com/android/settings/development/autofill/AutofillPreferenceCategory.java b/src/com/android/settings/development/autofill/AutofillPreferenceCategory.java new file mode 100644 index 0000000000..cbfbdd3392 --- /dev/null +++ b/src/com/android/settings/development/autofill/AutofillPreferenceCategory.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2018 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.development.autofill; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.provider.Settings; +import android.util.AttributeSet; +import android.util.Log; +import android.view.autofill.AutofillManager; + +import androidx.preference.PreferenceCategory; + +public final class AutofillPreferenceCategory extends PreferenceCategory { + + private static final String TAG = "AutofillPreferenceCategory"; + private static final long DELAYED_MESSAGE_TIME_MS = 2000; + + private final ContentResolver mContentResolver; + private final ContentObserver mSettingsObserver; + private final Handler mHandler = new Handler(Looper.getMainLooper()); + + public AutofillPreferenceCategory(Context context, AttributeSet attrs) { + super(context, attrs); + + mSettingsObserver = new ContentObserver(mHandler) { + @Override + public void onChange(boolean selfChange, Uri uri, int userId) { + // We cannot apply the change yet because AutofillManager.isEnabled() state is + // updated by a ContentObserver as well and there's no guarantee of which observer + // is called first - hence, it's possible that the state didn't change here yet. + mHandler.postDelayed(() -> notifyDependencyChange(shouldDisableDependents()), + DELAYED_MESSAGE_TIME_MS); + } + }; + mContentResolver = context.getContentResolver(); + } + + @Override + public void onAttached() { + super.onAttached(); + + mContentResolver.registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.AUTOFILL_SERVICE), false, + mSettingsObserver); + } + + @Override + public void onDetached() { + mContentResolver.unregisterContentObserver(mSettingsObserver); + + super.onDetached(); + } + + // PreferenceCategory.isEnabled() always return false, so we rather not change that logic + // decide whether the children should be shown using isAutofillEnabled() instead. + private boolean isAutofillEnabled() { + final AutofillManager afm = getContext().getSystemService(AutofillManager.class); + final boolean enabled = afm != null && afm.isEnabled(); + Log.v(TAG, "isAutofillEnabled(): " + enabled); + return enabled; + } + + @Override + public boolean shouldDisableDependents() { + final boolean shouldIt = !isAutofillEnabled(); + Log.v(TAG, "shouldDisableDependents(): " + shouldIt); + return shouldIt; + } +} diff --git a/src/com/android/settings/development/autofill/AutofillResetOptionsPreferenceController.java b/src/com/android/settings/development/autofill/AutofillResetOptionsPreferenceController.java new file mode 100644 index 0000000000..d30d0bb25e --- /dev/null +++ b/src/com/android/settings/development/autofill/AutofillResetOptionsPreferenceController.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2018 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.development.autofill; + +import android.content.ContentResolver; +import android.content.Context; +import android.provider.Settings; +import android.text.TextUtils; +import android.view.autofill.AutofillManager; +import android.widget.Toast; + +import androidx.preference.Preference; + +import com.android.settings.R; +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settingslib.development.DeveloperOptionsPreferenceController; + +public final class AutofillResetOptionsPreferenceController + extends DeveloperOptionsPreferenceController + implements PreferenceControllerMixin { + + private static final String AUTOFILL_RESET_OPTIONS_KEY = "autofill_reset_developer_options"; + + public AutofillResetOptionsPreferenceController(Context context) { + super(context); + } + + @Override + public String getPreferenceKey() { + return AUTOFILL_RESET_OPTIONS_KEY; + } + + @Override + public boolean handlePreferenceTreeClick(Preference preference) { + if (!TextUtils.equals(AUTOFILL_RESET_OPTIONS_KEY, preference.getKey())) { + return false; + } + final ContentResolver contentResolver = mContext.getContentResolver(); + Settings.Global.putInt(contentResolver, Settings.Global.AUTOFILL_LOGGING_LEVEL, + AutofillManager.DEFAULT_LOGGING_LEVEL); + Settings.Global.putInt(contentResolver, Settings.Global.AUTOFILL_MAX_PARTITIONS_SIZE, + AutofillManager.DEFAULT_MAX_PARTITIONS_SIZE); + Settings.Global.putInt(contentResolver, Settings.Global.AUTOFILL_MAX_VISIBLE_DATASETS, 0); + Toast.makeText(mContext, R.string.autofill_reset_developer_options_complete, + Toast.LENGTH_SHORT).show(); + return true; + } +} diff --git a/src/com/android/settings/development/autofill/AutofillVisibleDatasetsPreference.java b/src/com/android/settings/development/autofill/AutofillVisibleDatasetsPreference.java new file mode 100644 index 0000000000..2f0d15f9fb --- /dev/null +++ b/src/com/android/settings/development/autofill/AutofillVisibleDatasetsPreference.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2018 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.development.autofill; + +import android.content.Context; +import android.provider.Settings; +import android.util.AttributeSet; + +public final class AutofillVisibleDatasetsPreference extends AbstractGlobalSettingsPreference { + + public AutofillVisibleDatasetsPreference(Context context, AttributeSet attrs) { + super(context, attrs, Settings.Global.AUTOFILL_MAX_VISIBLE_DATASETS, 0); + } +} diff --git a/src/com/android/settings/development/featureflags/FeatureFlagFooterPreferenceController.java b/src/com/android/settings/development/featureflags/FeatureFlagFooterPreferenceController.java index a86b8bb12a..a0d7036740 100644 --- a/src/com/android/settings/development/featureflags/FeatureFlagFooterPreferenceController.java +++ b/src/com/android/settings/development/featureflags/FeatureFlagFooterPreferenceController.java @@ -22,18 +22,18 @@ import com.android.settings.R; import com.android.settings.core.BasePreferenceController; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnStart; -import com.android.settingslib.widget.FooterPreferenceMixin; +import com.android.settingslib.widget.FooterPreferenceMixinCompat; public class FeatureFlagFooterPreferenceController extends BasePreferenceController implements LifecycleObserver, OnStart { - private FooterPreferenceMixin mFooterMixin; + private FooterPreferenceMixinCompat mFooterMixin; public FeatureFlagFooterPreferenceController(Context context) { super(context, "feature_flag_footer_pref"); } - public void setFooterMixin(FooterPreferenceMixin mixin) { + public void setFooterMixin(FooterPreferenceMixinCompat mixin) { mFooterMixin = mixin; } diff --git a/src/com/android/settings/development/featureflags/FeatureFlagPersistent.java b/src/com/android/settings/development/featureflags/FeatureFlagPersistent.java index 30d5cd76f3..59318c471d 100644 --- a/src/com/android/settings/development/featureflags/FeatureFlagPersistent.java +++ b/src/com/android/settings/development/featureflags/FeatureFlagPersistent.java @@ -20,7 +20,6 @@ import android.content.Context; import android.os.SystemProperties; import android.text.TextUtils; import android.util.FeatureFlagUtils; -import android.util.Log; import androidx.annotation.VisibleForTesting; @@ -36,6 +35,8 @@ public class FeatureFlagPersistent { static { PERSISTENT_FLAGS = new HashSet<>(); PERSISTENT_FLAGS.add(FeatureFlags.HEARING_AID_SETTINGS); + PERSISTENT_FLAGS.add(FeatureFlags.NETWORK_INTERNET_V2); + PERSISTENT_FLAGS.add(FeatureFlags.DYNAMIC_SYSTEM); } public static boolean isEnabled(Context context, String feature) { diff --git a/src/com/android/settings/development/featureflags/FeatureFlagPreference.java b/src/com/android/settings/development/featureflags/FeatureFlagPreference.java index af3cc65fc6..0e0c7a6883 100644 --- a/src/com/android/settings/development/featureflags/FeatureFlagPreference.java +++ b/src/com/android/settings/development/featureflags/FeatureFlagPreference.java @@ -17,11 +17,9 @@ package com.android.settings.development.featureflags; import android.content.Context; -import android.util.Log; +import android.util.FeatureFlagUtils; import androidx.preference.SwitchPreference; -import android.util.FeatureFlagUtils; -import android.util.Log; public class FeatureFlagPreference extends SwitchPreference { diff --git a/src/com/android/settings/development/featureflags/FeatureFlagsDashboard.java b/src/com/android/settings/development/featureflags/FeatureFlagsDashboard.java index 2e9cac9d57..613fb17e39 100644 --- a/src/com/android/settings/development/featureflags/FeatureFlagsDashboard.java +++ b/src/com/android/settings/development/featureflags/FeatureFlagsDashboard.java @@ -16,24 +16,30 @@ package com.android.settings.development.featureflags; +import android.app.settings.SettingsEnums; import android.content.Context; +import android.provider.SearchIndexableResource; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.development.DevelopmentSettingsEnabler; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.List; +@SearchIndexable public class FeatureFlagsDashboard extends DashboardFragment { private static final String TAG = "FeatureFlagsDashboard"; @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.SETTINGS_FEATURE_FLAGS_DASHBOARD; + return SettingsEnums.SETTINGS_FEATURE_FLAGS_DASHBOARD; } @Override @@ -59,13 +65,43 @@ public class FeatureFlagsDashboard extends DashboardFragment { @Override protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { + return buildPrefControllers(context, getSettingsLifecycle()); + } + + private static List<AbstractPreferenceController> buildPrefControllers(Context context, + Lifecycle lifecycle) { final List<AbstractPreferenceController> controllers = new ArrayList<>(); - final Lifecycle lifecycle = getLifecycle(); final FeatureFlagFooterPreferenceController footerController = new FeatureFlagFooterPreferenceController(context); - controllers.add(new FeatureFlagsPreferenceController(context, lifecycle)); + if (lifecycle != null) { + lifecycle.addObserver(footerController); + } controllers.add(footerController); - lifecycle.addObserver(footerController); return controllers; } + + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, + boolean enabled) { + final List<SearchIndexableResource> result = new ArrayList<>(); + + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.feature_flags_settings; + result.add(sir); + return result; + } + + @Override + protected boolean isPageSearchEnabled(Context context) { + return DevelopmentSettingsEnabler.isDevelopmentSettingsEnabled(context); + } + + @Override + public List<AbstractPreferenceController> createPreferenceControllers( + Context context) { + return buildPrefControllers(context, null /* lifecycle */); + } + }; } diff --git a/src/com/android/settings/development/featureflags/FeatureFlagsPreferenceController.java b/src/com/android/settings/development/featureflags/FeatureFlagsPreferenceController.java index 960979c40f..94636e9c71 100644 --- a/src/com/android/settings/development/featureflags/FeatureFlagsPreferenceController.java +++ b/src/com/android/settings/development/featureflags/FeatureFlagsPreferenceController.java @@ -17,58 +17,50 @@ package com.android.settings.development.featureflags; import android.content.Context; -import androidx.preference.PreferenceScreen; +import android.os.Build; import android.util.FeatureFlagUtils; -import com.android.settings.core.PreferenceControllerMixin; -import com.android.settingslib.core.AbstractPreferenceController; -import com.android.settingslib.core.lifecycle.Lifecycle; +import androidx.preference.PreferenceGroup; +import androidx.preference.PreferenceScreen; + +import com.android.settings.core.BasePreferenceController; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnStart; import java.util.Map; -public class FeatureFlagsPreferenceController extends AbstractPreferenceController - implements PreferenceControllerMixin, LifecycleObserver, OnStart { - - private PreferenceScreen mScreen; +public class FeatureFlagsPreferenceController extends BasePreferenceController + implements LifecycleObserver, OnStart { - public FeatureFlagsPreferenceController(Context context, Lifecycle lifecycle) { - super(context); - if (lifecycle != null) { - lifecycle.addObserver(this); - } - } + private PreferenceGroup mGroup; - @Override - public boolean isAvailable() { - return true; + public FeatureFlagsPreferenceController(Context context, String key) { + super(context, key); } @Override - public String getPreferenceKey() { - return null; + public int getAvailabilityStatus() { + return Build.IS_DEBUGGABLE ? AVAILABLE : UNSUPPORTED_ON_DEVICE; } @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - mScreen = screen; + mGroup = screen.findPreference(getPreferenceKey()); } @Override public void onStart() { - if (mScreen == null) { + if (mGroup == null) { return; } final Map<String, String> featureMap = FeatureFlagUtils.getAllFeatureFlags(); if (featureMap == null) { return; } - mScreen.removeAll(); - final Context prefContext = mScreen.getContext(); - for (String feature : featureMap.keySet()) { - mScreen.addPreference(new FeatureFlagPreference(prefContext, feature)); - } + mGroup.removeAll(); + final Context prefContext = mGroup.getContext(); + featureMap.keySet().stream().sorted().forEach(feature -> + mGroup.addPreference(new FeatureFlagPreference(prefContext, feature))); } } diff --git a/src/com/android/settings/development/gamedriver/GameDriverAppPreferenceController.java b/src/com/android/settings/development/gamedriver/GameDriverAppPreferenceController.java index 659310c258..489408d481 100644 --- a/src/com/android/settings/development/gamedriver/GameDriverAppPreferenceController.java +++ b/src/com/android/settings/development/gamedriver/GameDriverAppPreferenceController.java @@ -55,8 +55,8 @@ import java.util.Set; */ public class GameDriverAppPreferenceController extends BasePreferenceController implements Preference.OnPreferenceChangeListener, - GameDriverContentObserver.OnGameDriverContentChangedListener, LifecycleObserver, - OnStart, OnStop { + GameDriverContentObserver.OnGameDriverContentChangedListener, LifecycleObserver, + OnStart, OnStop { private final Context mContext; private final ContentResolver mContentResolver; @@ -64,12 +64,14 @@ public class GameDriverAppPreferenceController extends BasePreferenceController private final String mPreferenceTitle; private final String mPreferenceDefault; private final String mPreferenceGameDriver; + private final String mPreferencePrereleaseDriver; private final String mPreferenceSystem; @VisibleForTesting GameDriverContentObserver mGameDriverContentObserver; private final List<AppInfo> mAppInfos; private final Set<String> mDevOptInApps; + private final Set<String> mDevPrereleaseOptInApps; private final Set<String> mDevOptOutApps; private PreferenceGroup mPreferenceGroup; @@ -88,6 +90,8 @@ public class GameDriverAppPreferenceController extends BasePreferenceController mPreferenceDefault = resources.getString(R.string.game_driver_app_preference_default); mPreferenceGameDriver = resources.getString(R.string.game_driver_app_preference_game_driver); + mPreferencePrereleaseDriver = + resources.getString(R.string.game_driver_app_preference_prerelease_driver); mPreferenceSystem = resources.getString(R.string.game_driver_app_preference_system); // TODO: Move this task to background if there's potential ANR/Jank. @@ -96,6 +100,8 @@ public class GameDriverAppPreferenceController extends BasePreferenceController mDevOptInApps = getGlobalSettingsString(mContentResolver, Settings.Global.GAME_DRIVER_OPT_IN_APPS); + mDevPrereleaseOptInApps = getGlobalSettingsString( + mContentResolver, Settings.Global.GAME_DRIVER_PRERELEASE_OPT_IN_APPS); mDevOptOutApps = getGlobalSettingsString(mContentResolver, Settings.Global.GAME_DRIVER_OPT_OUT_APPS); } @@ -103,9 +109,9 @@ public class GameDriverAppPreferenceController extends BasePreferenceController @Override public int getAvailabilityStatus() { return DevelopmentSettingsEnabler.isDevelopmentSettingsEnabled(mContext) - && (Settings.Global.getInt(mContentResolver, - Settings.Global.GAME_DRIVER_ALL_APPS, GAME_DRIVER_DEFAULT) - != GAME_DRIVER_OFF) + && (Settings.Global.getInt(mContentResolver, + Settings.Global.GAME_DRIVER_ALL_APPS, GAME_DRIVER_DEFAULT) + != GAME_DRIVER_OFF) ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; } @@ -113,7 +119,7 @@ public class GameDriverAppPreferenceController extends BasePreferenceController @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - mPreferenceGroup = (PreferenceGroup) screen.findPreference(getPreferenceKey()); + mPreferenceGroup = screen.findPreference(getPreferenceKey()); final Context context = mPreferenceGroup.getContext(); for (AppInfo appInfo : mAppInfos) { @@ -134,15 +140,7 @@ public class GameDriverAppPreferenceController extends BasePreferenceController @Override public void updateState(Preference preference) { - // This is a workaround, because PreferenceGroup.setVisible is not applied to the - // preferences inside the group. - final boolean isGroupAvailable = isAvailable(); - final PreferenceGroup group = (PreferenceGroup) preference; - for (int idx = 0; idx < group.getPreferenceCount(); idx++) { - final Preference pref = group.getPreference(idx); - pref.setVisible(isGroupAvailable); - } - preference.setVisible(isGroupAvailable); + preference.setVisible(isAvailable()); } @Override @@ -155,21 +153,31 @@ public class GameDriverAppPreferenceController extends BasePreferenceController // opt-in and opt-out apps. Then set the new summary text. if (value.equals(mPreferenceSystem)) { mDevOptInApps.remove(packageName); + mDevPrereleaseOptInApps.remove(packageName); mDevOptOutApps.add(packageName); } else if (value.equals(mPreferenceGameDriver)) { mDevOptInApps.add(packageName); + mDevPrereleaseOptInApps.remove(packageName); + mDevOptOutApps.remove(packageName); + } else if (value.equals(mPreferencePrereleaseDriver)) { + mDevOptInApps.remove(packageName); + mDevPrereleaseOptInApps.add(packageName); mDevOptOutApps.remove(packageName); } else { mDevOptInApps.remove(packageName); + mDevPrereleaseOptInApps.remove(packageName); mDevOptOutApps.remove(packageName); } listPref.setValue(value); listPref.setSummary(value); - // Push the updated Sets for opt-in and opt-out apps to - // corresponding Settings.Global.GAME_DRIVER_OPT_(IN|OUT)_APPS + // Push the updated Sets for stable/prerelease opt-in and opt-out apps to + // corresponding Settings.Global.GAME_DRIVER(_PRERELEASE)?_OPT_(IN|OUT)_APPS Settings.Global.putString(mContentResolver, Settings.Global.GAME_DRIVER_OPT_IN_APPS, String.join(",", mDevOptInApps)); + Settings.Global.putString(mContentResolver, + Settings.Global.GAME_DRIVER_PRERELEASE_OPT_IN_APPS, + String.join(",", mDevPrereleaseOptInApps)); Settings.Global.putString(mContentResolver, Settings.Global.GAME_DRIVER_OPT_OUT_APPS, String.join(",", mDevOptOutApps)); @@ -187,6 +195,7 @@ public class GameDriverAppPreferenceController extends BasePreferenceController info = applicationInfo; label = packageManager.getApplicationLabel(applicationInfo).toString(); } + final ApplicationInfo info; final String label; } @@ -240,10 +249,13 @@ public class GameDriverAppPreferenceController extends BasePreferenceController listPreference.setEntryValues(mEntryList); // Initialize preference default and summary with the opt in/out choices - // from Settings.Global.GAME_DRIVER_OPT_(IN|OUT)_APPS + // from Settings.Global.GAME_DRIVER(_PRERELEASE)?_OPT_(IN|OUT)_APPS if (mDevOptOutApps.contains(packageName)) { listPreference.setValue(mPreferenceSystem); listPreference.setSummary(mPreferenceSystem); + } else if (mDevPrereleaseOptInApps.contains(packageName)) { + listPreference.setValue(mPreferencePrereleaseDriver); + listPreference.setSummary(mPreferencePrereleaseDriver); } else if (mDevOptInApps.contains(packageName)) { listPreference.setValue(mPreferenceGameDriver); listPreference.setSummary(mPreferenceGameDriver); diff --git a/src/com/android/settings/development/gamedriver/GameDriverDashboard.java b/src/com/android/settings/development/gamedriver/GameDriverDashboard.java index b79af71999..db456bd930 100644 --- a/src/com/android/settings/development/gamedriver/GameDriverDashboard.java +++ b/src/com/android/settings/development/gamedriver/GameDriverDashboard.java @@ -16,28 +16,35 @@ package com.android.settings.development.gamedriver; +import android.app.settings.SettingsEnums; import android.content.Context; import android.os.Bundle; +import android.provider.SearchIndexableResource; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; import com.android.settings.widget.SwitchBar; import com.android.settings.widget.SwitchBarController; +import com.android.settingslib.development.DevelopmentSettingsEnabler; +import com.android.settingslib.search.SearchIndexable; +import java.util.ArrayList; import java.util.List; /** * Dashboard for Game Driver preferences. */ +@SearchIndexable public class GameDriverDashboard extends DashboardFragment { private static final String TAG = "GameDriverDashboard"; @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.SETTINGS_GAME_DRIVER_DASHBOARD; + return SettingsEnums.SETTINGS_GAME_DRIVER_DASHBOARD; } @Override @@ -64,7 +71,25 @@ public class GameDriverDashboard extends DashboardFragment { final GameDriverGlobalSwitchBarController switchBarController = new GameDriverGlobalSwitchBarController( activity, new SwitchBarController(switchBar)); - getLifecycle().addObserver(switchBarController); + getSettingsLifecycle().addObserver(switchBarController); switchBar.show(); } + + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex( + Context context, boolean enabled) { + final List<SearchIndexableResource> result = new ArrayList<>(); + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.game_driver_settings; + result.add(sir); + return result; + } + + @Override + protected boolean isPageSearchEnabled(Context context) { + return DevelopmentSettingsEnabler.isDevelopmentSettingsEnabled(context); + } + }; } diff --git a/src/com/android/settings/development/gamedriver/GameDriverEnableForAllAppsPreferenceController.java b/src/com/android/settings/development/gamedriver/GameDriverEnableForAllAppsPreferenceController.java index 7f9b74b80b..290e4b2a1d 100644 --- a/src/com/android/settings/development/gamedriver/GameDriverEnableForAllAppsPreferenceController.java +++ b/src/com/android/settings/development/gamedriver/GameDriverEnableForAllAppsPreferenceController.java @@ -18,15 +18,17 @@ package com.android.settings.development.gamedriver; import android.content.ContentResolver; import android.content.Context; +import android.content.res.Resources; import android.os.Handler; import android.os.Looper; import android.provider.Settings; import androidx.annotation.VisibleForTesting; +import androidx.preference.ListPreference; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import androidx.preference.SwitchPreference; +import com.android.settings.R; import com.android.settings.core.BasePreferenceController; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnStart; @@ -43,19 +45,30 @@ public class GameDriverEnableForAllAppsPreferenceController extends BasePreferen public static final int GAME_DRIVER_DEFAULT = 0; public static final int GAME_DRIVER_ALL_APPS = 1; - public static final int GAME_DRIVER_OFF = 2; + public static final int GAME_DRIVER_PRERELEASE_ALL_APPS = 2; + public static final int GAME_DRIVER_OFF = 3; private final Context mContext; private final ContentResolver mContentResolver; + private final String mPreferenceDefault; + private final String mPreferenceGameDriver; + private final String mPreferencePrereleaseDriver; @VisibleForTesting GameDriverContentObserver mGameDriverContentObserver; - private SwitchPreference mPreference; + private ListPreference mPreference; public GameDriverEnableForAllAppsPreferenceController(Context context, String key) { super(context, key); mContext = context; mContentResolver = context.getContentResolver(); + + final Resources resources = context.getResources(); + mPreferenceDefault = resources.getString(R.string.game_driver_app_preference_default); + mPreferenceGameDriver = + resources.getString(R.string.game_driver_app_preference_game_driver); + mPreferencePrereleaseDriver = + resources.getString(R.string.game_driver_app_preference_prerelease_driver); mGameDriverContentObserver = new GameDriverContentObserver(new Handler(Looper.getMainLooper()), this); } @@ -73,7 +86,7 @@ public class GameDriverEnableForAllAppsPreferenceController extends BasePreferen @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - mPreference = (SwitchPreference) screen.findPreference(getPreferenceKey()); + mPreference = screen.findPreference(getPreferenceKey()); mPreference.setOnPreferenceChangeListener(this); } @@ -89,31 +102,44 @@ public class GameDriverEnableForAllAppsPreferenceController extends BasePreferen @Override public void updateState(Preference preference) { - final SwitchPreference switchPreference = (SwitchPreference) preference; - switchPreference.setVisible(isAvailable()); - switchPreference.setChecked( - Settings.Global.getInt( - mContentResolver, Settings.Global.GAME_DRIVER_ALL_APPS, GAME_DRIVER_DEFAULT) - == GAME_DRIVER_ALL_APPS); + final ListPreference listPref = (ListPreference) preference; + listPref.setVisible(isAvailable()); + final int currentChoice = Settings.Global.getInt( + mContentResolver, Settings.Global.GAME_DRIVER_ALL_APPS, GAME_DRIVER_DEFAULT); + if (currentChoice == GAME_DRIVER_ALL_APPS) { + listPref.setValue(mPreferenceGameDriver); + listPref.setSummary(mPreferenceGameDriver); + } else if (currentChoice == GAME_DRIVER_PRERELEASE_ALL_APPS) { + listPref.setValue(mPreferencePrereleaseDriver); + listPref.setSummary(mPreferencePrereleaseDriver); + } else { + listPref.setValue(mPreferenceDefault); + listPref.setSummary(mPreferenceDefault); + } } @Override public boolean onPreferenceChange(Preference preference, Object newValue) { - final boolean isChecked = (boolean) newValue; - final int gameDriver = Settings.Global.getInt( + final ListPreference listPref = (ListPreference) preference; + final String value = newValue.toString(); + final int currentChoice = Settings.Global.getInt( mContentResolver, Settings.Global.GAME_DRIVER_ALL_APPS, GAME_DRIVER_DEFAULT); - - if (isChecked && gameDriver == GAME_DRIVER_ALL_APPS) { - return true; + final int userChoice; + if (value.equals(mPreferenceGameDriver)) { + userChoice = GAME_DRIVER_ALL_APPS; + } else if (value.equals(mPreferencePrereleaseDriver)) { + userChoice = GAME_DRIVER_PRERELEASE_ALL_APPS; + } else { + userChoice = GAME_DRIVER_DEFAULT; } + listPref.setValue(value); + listPref.setSummary(value); - if (!isChecked && (gameDriver == GAME_DRIVER_DEFAULT || gameDriver == GAME_DRIVER_OFF)) { - return true; + if (userChoice != currentChoice) { + Settings.Global.putInt( + mContentResolver, Settings.Global.GAME_DRIVER_ALL_APPS, userChoice); } - Settings.Global.putInt(mContentResolver, Settings.Global.GAME_DRIVER_ALL_APPS, - isChecked ? GAME_DRIVER_ALL_APPS : GAME_DRIVER_DEFAULT); - return true; } diff --git a/src/com/android/settings/development/gamedriver/GameDriverFooterPreferenceController.java b/src/com/android/settings/development/gamedriver/GameDriverFooterPreferenceController.java index bacbf95fa3..12156df026 100644 --- a/src/com/android/settings/development/gamedriver/GameDriverFooterPreferenceController.java +++ b/src/com/android/settings/development/gamedriver/GameDriverFooterPreferenceController.java @@ -60,14 +60,14 @@ public class GameDriverFooterPreferenceController extends BasePreferenceControll return Settings.Global.getInt( mContentResolver, Settings.Global.GAME_DRIVER_ALL_APPS, GAME_DRIVER_DEFAULT) == GAME_DRIVER_OFF - ? AVAILABLE + ? AVAILABLE_UNSEARCHABLE : CONDITIONALLY_UNAVAILABLE; } @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - mPreference = (FooterPreference) screen.findPreference(getPreferenceKey()); + mPreference = screen.findPreference(getPreferenceKey()); } @Override diff --git a/src/com/android/settings/development/gamedriver/GameDriverGlobalSwitchBarController.java b/src/com/android/settings/development/gamedriver/GameDriverGlobalSwitchBarController.java index d84c28f3f5..15f71e0375 100644 --- a/src/com/android/settings/development/gamedriver/GameDriverGlobalSwitchBarController.java +++ b/src/com/android/settings/development/gamedriver/GameDriverGlobalSwitchBarController.java @@ -19,6 +19,7 @@ package com.android.settings.development.gamedriver; import static com.android.settings.development.gamedriver.GameDriverEnableForAllAppsPreferenceController.GAME_DRIVER_ALL_APPS; import static com.android.settings.development.gamedriver.GameDriverEnableForAllAppsPreferenceController.GAME_DRIVER_DEFAULT; import static com.android.settings.development.gamedriver.GameDriverEnableForAllAppsPreferenceController.GAME_DRIVER_OFF; +import static com.android.settings.development.gamedriver.GameDriverEnableForAllAppsPreferenceController.GAME_DRIVER_PRERELEASE_ALL_APPS; import android.content.ContentResolver; import android.content.Context; @@ -83,7 +84,8 @@ public class GameDriverGlobalSwitchBarController mContentResolver, Settings.Global.GAME_DRIVER_ALL_APPS, GAME_DRIVER_DEFAULT); if (isChecked - && (gameDriver == GAME_DRIVER_DEFAULT || gameDriver == GAME_DRIVER_ALL_APPS)) { + && (gameDriver == GAME_DRIVER_DEFAULT || gameDriver == GAME_DRIVER_ALL_APPS + || gameDriver == GAME_DRIVER_PRERELEASE_ALL_APPS)) { return true; } diff --git a/src/com/android/settings/development/qstile/DevelopmentTileConfigFragment.java b/src/com/android/settings/development/qstile/DevelopmentTileConfigFragment.java index 24bb5d1ab5..c6f0ecd3ae 100644 --- a/src/com/android/settings/development/qstile/DevelopmentTileConfigFragment.java +++ b/src/com/android/settings/development/qstile/DevelopmentTileConfigFragment.java @@ -16,16 +16,21 @@ package com.android.settings.development.qstile; +import android.app.settings.SettingsEnums; import android.content.Context; +import android.provider.SearchIndexableResource; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; -import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; +import com.android.settingslib.development.DevelopmentSettingsEnabler; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.List; +@SearchIndexable public class DevelopmentTileConfigFragment extends DashboardFragment { private static final String TAG = "DevelopmentTileConfig"; @@ -40,14 +45,26 @@ public class DevelopmentTileConfigFragment extends DashboardFragment { } @Override - protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { - final List<AbstractPreferenceController> controllers = new ArrayList<>(); - controllers.add(new DevelopmentTilePreferenceController(context)); - return controllers; - } - - @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DEVELOPMENT_QS_TILE_CONFIG; + return SettingsEnums.DEVELOPMENT_QS_TILE_CONFIG; } + + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, + boolean enabled) { + final List<SearchIndexableResource> result = new ArrayList<>(); + + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.development_tile_settings; + result.add(sir); + return result; + } + + @Override + protected boolean isPageSearchEnabled(Context context) { + return DevelopmentSettingsEnabler.isDevelopmentSettingsEnabled(context); + } + }; } diff --git a/src/com/android/settings/development/qstile/DevelopmentTilePreferenceController.java b/src/com/android/settings/development/qstile/DevelopmentTilePreferenceController.java index 71d3d07888..c271bc99b9 100644 --- a/src/com/android/settings/development/qstile/DevelopmentTilePreferenceController.java +++ b/src/com/android/settings/development/qstile/DevelopmentTilePreferenceController.java @@ -25,44 +25,40 @@ import android.content.pm.ServiceInfo; import android.os.RemoteException; import android.os.ServiceManager; import android.service.quicksettings.TileService; +import android.util.Log; + import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.util.Log; +import androidx.preference.SwitchPreference; import com.android.internal.statusbar.IStatusBarService; -import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settings.core.BasePreferenceController; import java.util.List; -public class DevelopmentTilePreferenceController extends AbstractPreferenceController { +public class DevelopmentTilePreferenceController extends BasePreferenceController { private static final String TAG = "DevTilePrefController"; private final OnChangeHandler mOnChangeHandler; private final PackageManager mPackageManager; - public DevelopmentTilePreferenceController(Context context) { - super(context); + public DevelopmentTilePreferenceController(Context context, String key) { + super(context, key); mOnChangeHandler = new OnChangeHandler(context); mPackageManager = context.getPackageManager(); } @Override - public boolean isAvailable() { - return true; - } - - @Override - public String getPreferenceKey() { - return null; + public int getAvailabilityStatus() { + return AVAILABLE; } @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - Context context = screen.getContext(); - Intent intent = new Intent(TileService.ACTION_QS_TILE) + final Context context = screen.getContext(); + final Intent intent = new Intent(TileService.ACTION_QS_TILE) .setPackage(context.getPackageName()); final List<ResolveInfo> resolveInfos = mPackageManager.queryIntentServices(intent, PackageManager.MATCH_DISABLED_COMPONENTS); diff --git a/src/com/android/settings/development/qstile/DevelopmentTiles.java b/src/com/android/settings/development/qstile/DevelopmentTiles.java index 8dd56d5671..bb791abef8 100644 --- a/src/com/android/settings/development/qstile/DevelopmentTiles.java +++ b/src/com/android/settings/development/qstile/DevelopmentTiles.java @@ -16,7 +16,12 @@ package com.android.settings.development.qstile; +import android.app.settings.SettingsEnums; +import android.content.ComponentName; import android.content.Context; +import android.content.pm.PackageManager; +import android.hardware.SensorPrivacyManager; +import android.app.KeyguardManager; import android.os.IBinder; import android.os.Parcel; import android.os.RemoteException; @@ -26,15 +31,19 @@ import android.provider.Settings; import android.service.quicksettings.Tile; import android.service.quicksettings.TileService; import android.sysprop.DisplayProperties; -import androidx.annotation.VisibleForTesting; import android.util.Log; import android.view.IWindowManager; import android.view.ThreadedRenderer; -import android.view.View; import android.view.WindowManagerGlobal; import android.widget.Toast; +import androidx.annotation.VisibleForTesting; + import com.android.internal.app.LocalePicker; +import com.android.internal.statusbar.IStatusBarService; +import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; +import com.android.settingslib.development.DevelopmentSettingsEnabler; import com.android.settingslib.development.SystemPropPoker; public abstract class DevelopmentTiles extends TileService { @@ -51,7 +60,32 @@ public abstract class DevelopmentTiles extends TileService { } public void refresh() { - getQsTile().setState(isEnabled() ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE); + final int state; + if (!DevelopmentSettingsEnabler.isDevelopmentSettingsEnabled(this)) { + // Reset to disabled state if dev option is off. + if (isEnabled()) { + setIsEnabled(false); + SystemPropPoker.getInstance().poke(); + } + final ComponentName cn = new ComponentName(getPackageName(), getClass().getName()); + try { + getPackageManager().setComponentEnabledSetting( + cn, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP); + final IStatusBarService statusBarService = IStatusBarService.Stub.asInterface( + ServiceManager.checkService(Context.STATUS_BAR_SERVICE)); + if (statusBarService != null) { + statusBarService.remTile(cn); + } + } catch (RemoteException e) { + Log.e(TAG, "Failed to modify QS tile for component " + + cn.toString(), e); + } + state = Tile.STATE_UNAVAILABLE; + } else { + state = isEnabled() ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE; + } + getQsTile().setState(state); getQsTile().updateTile(); } @@ -125,7 +159,8 @@ public abstract class DevelopmentTiles extends TileService { IWindowManager wm = WindowManagerGlobal.getWindowManagerService(); try { return wm.getAnimationScale(0) != 1; - } catch (RemoteException e) { } + } catch (RemoteException e) { + } return false; } @@ -137,7 +172,8 @@ public abstract class DevelopmentTiles extends TileService { wm.setAnimationScale(0, scale); wm.setAnimationScale(1, scale); wm.setAnimationScale(2, scale); - } catch (RemoteException e) { } + } catch (RemoteException e) { + } } } @@ -242,4 +278,45 @@ public abstract class DevelopmentTiles extends TileService { } } } -}
\ No newline at end of file + + /** + * Tile to toggle sensors off to control camera, mic, and sensors managed by the SensorManager. + */ + public static class SensorsOff extends DevelopmentTiles { + private Context mContext; + private SensorPrivacyManager mSensorPrivacyManager; + private KeyguardManager mKeyguardManager; + private MetricsFeatureProvider mMetricsFeatureProvider; + private boolean mIsEnabled; + + @Override + public void onCreate() { + super.onCreate(); + mContext = getApplicationContext(); + mSensorPrivacyManager = (SensorPrivacyManager) mContext.getSystemService( + Context.SENSOR_PRIVACY_SERVICE); + mIsEnabled = mSensorPrivacyManager.isSensorPrivacyEnabled(); + mMetricsFeatureProvider = FeatureFactory.getFactory( + mContext).getMetricsFeatureProvider(); + mKeyguardManager = (KeyguardManager) mContext.getSystemService( + Context.KEYGUARD_SERVICE); + } + + @Override + protected boolean isEnabled() { + return mIsEnabled; + } + + @Override + public void setIsEnabled(boolean isEnabled) { + // Don't allow sensors to be reenabled from the lock screen. + if (mIsEnabled && mKeyguardManager.isKeyguardLocked()) { + return; + } + mMetricsFeatureProvider.action(getApplicationContext(), SettingsEnums.QS_SENSOR_PRIVACY, + isEnabled); + mIsEnabled = isEnabled; + mSensorPrivacyManager.setSensorPrivacy(isEnabled); + } + } +} diff --git a/src/com/android/settings/deviceinfo/BrandedAccountPreferenceController.java b/src/com/android/settings/deviceinfo/BrandedAccountPreferenceController.java index ca2175b4c1..bdd76fca2a 100644 --- a/src/com/android/settings/deviceinfo/BrandedAccountPreferenceController.java +++ b/src/com/android/settings/deviceinfo/BrandedAccountPreferenceController.java @@ -17,12 +17,13 @@ package com.android.settings.deviceinfo; import android.accounts.Account; +import android.app.settings.SettingsEnums; import android.content.Context; import android.os.Bundle; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.accounts.AccountDetailDashboardFragment; import com.android.settings.accounts.AccountFeatureProvider; @@ -31,11 +32,10 @@ import com.android.settings.core.SubSettingLauncher; import com.android.settings.overlay.FeatureFactory; public class BrandedAccountPreferenceController extends BasePreferenceController { - private static final String KEY_PREFERENCE_TITLE = "branded_account"; private final Account[] mAccounts; - public BrandedAccountPreferenceController(Context context) { - super(context, KEY_PREFERENCE_TITLE); + public BrandedAccountPreferenceController(Context context, String key) { + super(context, key); final AccountFeatureProvider accountFeatureProvider = FeatureFactory.getFactory( mContext).getAccountFeatureProvider(); mAccounts = accountFeatureProvider.getAccounts(mContext); @@ -43,6 +43,10 @@ public class BrandedAccountPreferenceController extends BasePreferenceController @Override public int getAvailabilityStatus() { + if (!mContext.getResources().getBoolean( + R.bool.config_show_branded_account_in_device_info)) { + return UNSUPPORTED_ON_DEVICE; + } if (mAccounts != null && mAccounts.length > 0) { return AVAILABLE; } @@ -54,7 +58,7 @@ public class BrandedAccountPreferenceController extends BasePreferenceController super.displayPreference(screen); final AccountFeatureProvider accountFeatureProvider = FeatureFactory.getFactory( mContext).getAccountFeatureProvider(); - final Preference accountPreference = screen.findPreference(KEY_PREFERENCE_TITLE); + final Preference accountPreference = screen.findPreference(getPreferenceKey()); if (accountPreference != null && (mAccounts == null || mAccounts.length == 0)) { screen.removePreference(accountPreference); return; @@ -72,9 +76,9 @@ public class BrandedAccountPreferenceController extends BasePreferenceController new SubSettingLauncher(mContext) .setDestination(AccountDetailDashboardFragment.class.getName()) - .setTitle(R.string.account_sync_title) + .setTitleRes(R.string.account_sync_title) .setArguments(args) - .setSourceMetricsCategory(MetricsEvent.DEVICEINFO) + .setSourceMetricsCategory(SettingsEnums.DEVICEINFO) .launch(); return true; }); diff --git a/src/com/android/settings/deviceinfo/BuildNumberPreferenceController.java b/src/com/android/settings/deviceinfo/BuildNumberPreferenceController.java index 707d660b2c..dd522bfa1d 100644 --- a/src/com/android/settings/deviceinfo/BuildNumberPreferenceController.java +++ b/src/com/android/settings/deviceinfo/BuildNumberPreferenceController.java @@ -17,7 +17,7 @@ package com.android.settings.deviceinfo; import android.app.Activity; -import android.app.Fragment; +import android.app.settings.SettingsEnums; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -25,37 +25,35 @@ import android.content.pm.ResolveInfo; import android.os.Build; import android.os.UserHandle; import android.os.UserManager; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; import android.text.BidiFormatter; import android.text.TextUtils; -import android.util.Pair; import android.widget.Toast; +import androidx.preference.Preference; + import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.Utils; -import com.android.settings.core.PreferenceControllerMixin; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.core.InstrumentedPreferenceFragment; import com.android.settings.overlay.FeatureFactory; import com.android.settings.password.ChooseLockSettingsHelper; +import com.android.settings.slices.Sliceable; import com.android.settingslib.RestrictedLockUtils; -import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; -import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.core.lifecycle.LifecycleObserver; -import com.android.settingslib.core.lifecycle.events.OnResume; +import com.android.settingslib.core.lifecycle.events.OnStart; import com.android.settingslib.development.DevelopmentSettingsEnabler; -public class BuildNumberPreferenceController extends AbstractPreferenceController implements - PreferenceControllerMixin, LifecycleObserver, OnResume { +public class BuildNumberPreferenceController extends BasePreferenceController implements + LifecycleObserver, OnStart { static final int TAPS_TO_BE_A_DEVELOPER = 7; static final int REQUEST_CONFIRM_PASSWORD_FOR_DEV_PREF = 100; - private static final String KEY_BUILD_NUMBER = "build_number"; - - private final Activity mActivity; - private final Fragment mFragment; + private Activity mActivity; + private InstrumentedPreferenceFragment mFragment; private final UserManager mUm; private final MetricsFeatureProvider mMetricsFeatureProvider; @@ -65,47 +63,27 @@ public class BuildNumberPreferenceController extends AbstractPreferenceControlle private int mDevHitCountdown; private boolean mProcessingLastDevHit; - public BuildNumberPreferenceController(Context context, Activity activity, Fragment fragment, - Lifecycle lifecycle) { - super(context); - mActivity = activity; - mFragment = fragment; + public BuildNumberPreferenceController(Context context, String key) { + super(context, key); mUm = (UserManager) context.getSystemService(Context.USER_SERVICE); mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider(); - if (lifecycle != null) { - lifecycle.addObserver(this); - } } - @Override - public void displayPreference(PreferenceScreen screen) { - super.displayPreference(screen); - final Preference preference = screen.findPreference(KEY_BUILD_NUMBER); - if (preference != null) { - try { - preference.setSummary(BidiFormatter.getInstance().unicodeWrap(Build.DISPLAY)); - preference.setEnabled(true); - } catch (Exception e) { - preference.setSummary(R.string.device_info_default); - } - } - } - - @Override - public String getPreferenceKey() { - return KEY_BUILD_NUMBER; + public void setHost(InstrumentedPreferenceFragment fragment) { + mFragment = fragment; + mActivity = fragment.getActivity(); } @Override - public boolean isAvailable() { - return true; + public CharSequence getSummary() { + return BidiFormatter.getInstance().unicodeWrap(Build.DISPLAY); } @Override - public void onResume() { - mDebuggingFeaturesDisallowedAdmin = RestrictedLockUtils.checkIfRestrictionEnforced( + public void onStart() { + mDebuggingFeaturesDisallowedAdmin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced( mContext, UserManager.DISALLOW_DEBUGGING_FEATURES, UserHandle.myUserId()); - mDebuggingFeaturesDisallowedBySystem = RestrictedLockUtils.hasBaseUserRestriction( + mDebuggingFeaturesDisallowedBySystem = RestrictedLockUtilsInternal.hasBaseUserRestriction( mContext, UserManager.DISALLOW_DEBUGGING_FEATURES, UserHandle.myUserId()); mDevHitCountdown = DevelopmentSettingsEnabler.isDevelopmentSettingsEnabled(mContext) ? -1 : TAPS_TO_BE_A_DEVELOPER; @@ -113,8 +91,33 @@ public class BuildNumberPreferenceController extends AbstractPreferenceControlle } @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } + + @Override + public boolean isSliceable() { + return true; + } + + @Override + public boolean isCopyableSlice() { + return true; + } + + @Override + public boolean useDynamicSliceSummary() { + return true; + } + + @Override + public void copy() { + Sliceable.setCopyContent(mContext, getSummary(), mContext.getText(R.string.build_number)); + } + + @Override public boolean handlePreferenceTreeClick(Preference preference) { - if (!TextUtils.equals(preference.getKey(), KEY_BUILD_NUMBER)) { + if (!TextUtils.equals(preference.getKey(), getPreferenceKey())) { return false; } if (Utils.isMonkeyRunning()) { @@ -123,14 +126,14 @@ public class BuildNumberPreferenceController extends AbstractPreferenceControlle // Don't enable developer options for secondary non-demo users. if (!(mUm.isAdminUser() || mUm.isDemoUser())) { mMetricsFeatureProvider.action( - mContext, MetricsEvent.ACTION_SETTINGS_BUILD_NUMBER_PREF); + mContext, SettingsEnums.ACTION_SETTINGS_BUILD_NUMBER_PREF); return false; } // Don't enable developer options until device has been provisioned if (!Utils.isDeviceProvisioned(mContext)) { mMetricsFeatureProvider.action( - mContext, MetricsEvent.ACTION_SETTINGS_BUILD_NUMBER_PREF); + mContext, SettingsEnums.ACTION_SETTINGS_BUILD_NUMBER_PREF); return false; } @@ -143,7 +146,7 @@ public class BuildNumberPreferenceController extends AbstractPreferenceControlle .setPackage(componentName.getPackageName()) .setAction("com.android.settings.action.REQUEST_DEBUG_FEATURES"); final ResolveInfo resolveInfo = mContext.getPackageManager().resolveActivity( - requestDebugFeatures, 0); + requestDebugFeatures, 0); if (resolveInfo != null) { mContext.startActivity(requestDebugFeatures); return false; @@ -156,7 +159,7 @@ public class BuildNumberPreferenceController extends AbstractPreferenceControlle mDebuggingFeaturesDisallowedAdmin); } mMetricsFeatureProvider.action( - mContext, MetricsEvent.ACTION_SETTINGS_BUILD_NUMBER_PREF); + mContext, SettingsEnums.ACTION_SETTINGS_BUILD_NUMBER_PREF); return false; } @@ -174,9 +177,11 @@ public class BuildNumberPreferenceController extends AbstractPreferenceControlle enableDevelopmentSettings(); } mMetricsFeatureProvider.action( - mContext, MetricsEvent.ACTION_SETTINGS_BUILD_NUMBER_PREF, - Pair.create(MetricsEvent.FIELD_SETTINGS_BUILD_NUMBER_DEVELOPER_MODE_ENABLED, - mProcessingLastDevHit ? 0 : 1)); + mMetricsFeatureProvider.getAttribution(mActivity), + MetricsEvent.FIELD_SETTINGS_BUILD_NUMBER_DEVELOPER_MODE_ENABLED, + mFragment.getMetricsCategory(), + null, + mProcessingLastDevHit ? 0 : 1); } else if (mDevHitCountdown > 0 && mDevHitCountdown < (TAPS_TO_BE_A_DEVELOPER - 2)) { if (mDevHitToast != null) { @@ -189,10 +194,13 @@ public class BuildNumberPreferenceController extends AbstractPreferenceControlle Toast.LENGTH_SHORT); mDevHitToast.show(); } + mMetricsFeatureProvider.action( - mContext, MetricsEvent.ACTION_SETTINGS_BUILD_NUMBER_PREF, - Pair.create(MetricsEvent.FIELD_SETTINGS_BUILD_NUMBER_DEVELOPER_MODE_ENABLED, - 0)); + mMetricsFeatureProvider.getAttribution(mActivity), + MetricsEvent.FIELD_SETTINGS_BUILD_NUMBER_DEVELOPER_MODE_ENABLED, + mFragment.getMetricsCategory(), + null, + 0); } else if (mDevHitCountdown < 0) { if (mDevHitToast != null) { mDevHitToast.cancel(); @@ -201,9 +209,11 @@ public class BuildNumberPreferenceController extends AbstractPreferenceControlle Toast.LENGTH_LONG); mDevHitToast.show(); mMetricsFeatureProvider.action( - mContext, MetricsEvent.ACTION_SETTINGS_BUILD_NUMBER_PREF, - Pair.create(MetricsEvent.FIELD_SETTINGS_BUILD_NUMBER_DEVELOPER_MODE_ENABLED, - 1)); + mMetricsFeatureProvider.getAttribution(mActivity), + MetricsEvent.FIELD_SETTINGS_BUILD_NUMBER_DEVELOPER_MODE_ENABLED, + mFragment.getMetricsCategory(), + null, + 1); } return true; } diff --git a/src/com/android/settings/deviceinfo/DeviceInfoSettings.java b/src/com/android/settings/deviceinfo/DeviceInfoSettings.java deleted file mode 100644 index 5a4dfc2623..0000000000 --- a/src/com/android/settings/deviceinfo/DeviceInfoSettings.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright (C) 2008 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settings.deviceinfo; - -import android.app.Activity; -import android.app.Fragment; -import android.content.Context; -import android.content.Intent; -import android.provider.SearchIndexableResource; -import androidx.annotation.VisibleForTesting; -import android.telephony.TelephonyManager; - -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.settings.R; -import com.android.settings.dashboard.DashboardFragment; -import com.android.settings.dashboard.SummaryLoader; -import com.android.settings.deviceinfo.firmwareversion.FirmwareVersionPreferenceController; -import com.android.settings.deviceinfo.imei.ImeiInfoPreferenceController; -import com.android.settings.deviceinfo.simstatus.SimStatusPreferenceController; -import com.android.settings.search.BaseSearchIndexProvider; -import com.android.settings.search.Indexable; -import com.android.settingslib.core.AbstractPreferenceController; -import com.android.settingslib.core.lifecycle.Lifecycle; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -public class DeviceInfoSettings extends DashboardFragment implements Indexable { - - private static final String LOG_TAG = "DeviceInfoSettings"; - - private static final String KEY_LEGAL_CONTAINER = "legal_container"; - - @VisibleForTesting - static final int SIM_PREFERENCES_COUNT = 3; - @VisibleForTesting - static final int NON_SIM_PREFERENCES_COUNT = 2; - - @Override - public int getMetricsCategory() { - return MetricsEvent.DEVICEINFO; - } - - @Override - public int getHelpResource() { - return R.string.help_uri_about; - } - - @Override - public int getInitialExpandedChildCount() { - final TelephonyManager telephonyManager = (TelephonyManager) getContext() - .getSystemService(Context.TELEPHONY_SERVICE); - return Math.max(SIM_PREFERENCES_COUNT, - SIM_PREFERENCES_COUNT * telephonyManager.getPhoneCount()) - + NON_SIM_PREFERENCES_COUNT; - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - final BuildNumberPreferenceController buildNumberPreferenceController = - use(BuildNumberPreferenceController.class); - if (buildNumberPreferenceController.onActivityResult(requestCode, resultCode, data)) { - return; - } - super.onActivityResult(requestCode, resultCode, data); - } - - @Override - protected String getLogTag() { - return LOG_TAG; - } - - @Override - protected int getPreferenceScreenResId() { - return R.xml.device_info_settings; - } - - @Override - protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { - return buildPreferenceControllers(context, getActivity(), this /* fragment */, - getLifecycle()); - } - - private static class SummaryProvider implements SummaryLoader.SummaryProvider { - - private final SummaryLoader mSummaryLoader; - - public SummaryProvider(SummaryLoader summaryLoader) { - mSummaryLoader = summaryLoader; - } - - @Override - public void setListening(boolean listening) { - if (listening) { - mSummaryLoader.setSummary(this, DeviceModelPreferenceController.getDeviceModel()); - } - } - } - - public static final SummaryLoader.SummaryProviderFactory SUMMARY_PROVIDER_FACTORY - = new SummaryLoader.SummaryProviderFactory() { - @Override - public SummaryLoader.SummaryProvider createSummaryProvider(Activity activity, - SummaryLoader summaryLoader) { - return new SummaryProvider(summaryLoader); - } - }; - - private static List<AbstractPreferenceController> buildPreferenceControllers(Context context, - Activity activity, Fragment fragment, Lifecycle lifecycle) { - final List<AbstractPreferenceController> controllers = new ArrayList<>(); - controllers.add(new PhoneNumberPreferenceController(context)); - controllers.add(new SimStatusPreferenceController(context, fragment)); - controllers.add(new DeviceModelPreferenceController(context, fragment)); - controllers.add(new ImeiInfoPreferenceController(context, fragment)); - controllers.add(new FirmwareVersionPreferenceController(context, fragment)); - controllers.add(new IpAddressPreferenceController(context, lifecycle)); - controllers.add(new WifiMacAddressPreferenceController(context, lifecycle)); - controllers.add(new BluetoothAddressPreferenceController(context, lifecycle)); - controllers.add(new RegulatoryInfoPreferenceController(context)); - controllers.add(new SafetyInfoPreferenceController(context)); - controllers.add(new ManualPreferenceController(context)); - controllers.add(new FeedbackPreferenceController(fragment, context)); - controllers.add(new FccEquipmentIdPreferenceController(context)); - controllers.add( - new BuildNumberPreferenceController(context, activity, fragment, lifecycle)); - return controllers; - } - - /** - * For Search. - */ - public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = - new BaseSearchIndexProvider() { - - @Override - public List<SearchIndexableResource> getXmlResourcesToIndex( - Context context, boolean enabled) { - final SearchIndexableResource sir = new SearchIndexableResource(context); - sir.xmlResId = R.xml.device_info_settings; - return Arrays.asList(sir); - } - - @Override - public List<AbstractPreferenceController> createPreferenceControllers( - Context context) { - return buildPreferenceControllers(context, null /*activity */, - null /* fragment */, null /* lifecycle */); - } - - @Override - public List<String> getNonIndexableKeys(Context context) { - List<String> keys = super.getNonIndexableKeys(context); - keys.add(KEY_LEGAL_CONTAINER); - return keys; - } - }; -} diff --git a/src/com/android/settings/deviceinfo/DeviceModelPreferenceController.java b/src/com/android/settings/deviceinfo/DeviceModelPreferenceController.java deleted file mode 100644 index 1fc54cd675..0000000000 --- a/src/com/android/settings/deviceinfo/DeviceModelPreferenceController.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (C) 2017 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.app.Fragment; -import android.content.Context; -import android.os.Build; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; -import android.text.TextUtils; - -import com.android.settings.R; -import com.android.settings.core.PreferenceControllerMixin; -import com.android.settingslib.DeviceInfoUtils; -import com.android.settingslib.core.AbstractPreferenceController; - -public class DeviceModelPreferenceController extends AbstractPreferenceController implements - PreferenceControllerMixin { - - private static final String KEY_DEVICE_MODEL = "device_model"; - - private final Fragment mHost; - - public DeviceModelPreferenceController(Context context, Fragment host) { - super(context); - mHost = host; - } - - @Override - public boolean isAvailable() { - return mContext.getResources().getBoolean(R.bool.config_show_device_model); - } - - @Override - public void displayPreference(PreferenceScreen screen) { - super.displayPreference(screen); - final Preference pref = screen.findPreference(KEY_DEVICE_MODEL); - if (pref != null) { - pref.setSummary(mContext.getResources().getString(R.string.model_summary, - getDeviceModel())); - } - } - - @Override - public String getPreferenceKey() { - return KEY_DEVICE_MODEL; - } - - @Override - public boolean handlePreferenceTreeClick(Preference preference) { - if (!TextUtils.equals(preference.getKey(), KEY_DEVICE_MODEL)) { - return false; - } - final HardwareInfoDialogFragment fragment = HardwareInfoDialogFragment.newInstance(); - fragment.show(mHost.getFragmentManager(), HardwareInfoDialogFragment.TAG); - return true; - } - - public static String getDeviceModel() { - return Build.MODEL + DeviceInfoUtils.getMsvSuffix(); - } -} diff --git a/src/com/android/settings/deviceinfo/DeviceNamePreferenceController.java b/src/com/android/settings/deviceinfo/DeviceNamePreferenceController.java index fc89dc1942..fb20a2e652 100644 --- a/src/com/android/settings/deviceinfo/DeviceNamePreferenceController.java +++ b/src/com/android/settings/deviceinfo/DeviceNamePreferenceController.java @@ -16,53 +16,48 @@ package com.android.settings.deviceinfo; -import android.annotation.Nullable; +import android.bluetooth.BluetoothAdapter; import android.content.Context; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiManager; import android.os.Build; import android.os.Bundle; import android.provider.Settings; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; import android.text.SpannedString; -import com.android.internal.annotations.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; +import com.android.settings.R; import com.android.settings.bluetooth.BluetoothLengthDeviceNameFilter; import com.android.settings.core.BasePreferenceController; import com.android.settings.widget.ValidatedEditTextPreference; import com.android.settings.wifi.tether.WifiDeviceNameTextValidator; -import com.android.settingslib.bluetooth.LocalBluetoothAdapter; -import com.android.settingslib.bluetooth.LocalBluetoothManager; -import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnCreate; import com.android.settingslib.core.lifecycle.events.OnSaveInstanceState; public class DeviceNamePreferenceController extends BasePreferenceController implements ValidatedEditTextPreference.Validator, - Preference.OnPreferenceChangeListener, - LifecycleObserver, - OnSaveInstanceState, - OnCreate { - private static final String PREF_KEY = "device_name"; - public static final int DEVICE_NAME_SET_WARNING_ID = 1; + Preference.OnPreferenceChangeListener, + LifecycleObserver, + OnSaveInstanceState, + OnCreate { private static final String KEY_PENDING_DEVICE_NAME = "key_pending_device_name"; private String mDeviceName; protected WifiManager mWifiManager; + private final BluetoothAdapter mBluetoothAdapter; private final WifiDeviceNameTextValidator mWifiDeviceNameTextValidator; private ValidatedEditTextPreference mPreference; - @Nullable - private LocalBluetoothManager mBluetoothManager; private DeviceNamePreferenceHost mHost; private String mPendingDeviceName; - public DeviceNamePreferenceController(Context context) { - super(context, PREF_KEY); + public DeviceNamePreferenceController(Context context, String key) { + super(context, key); mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); mWifiDeviceNameTextValidator = new WifiDeviceNameTextValidator(); + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); initializeDeviceName(); } @@ -70,7 +65,7 @@ public class DeviceNamePreferenceController extends BasePreferenceController @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - mPreference = (ValidatedEditTextPreference) screen.findPreference(PREF_KEY); + mPreference = screen.findPreference(getPreferenceKey()); final CharSequence deviceName = getSummary(); mPreference.setSummary(deviceName); mPreference.setText(deviceName.toString()); @@ -92,12 +87,9 @@ public class DeviceNamePreferenceController extends BasePreferenceController @Override public int getAvailabilityStatus() { - return AVAILABLE; - } - - @Override - public String getPreferenceKey() { - return PREF_KEY; + return mContext.getResources().getBoolean(R.bool.config_show_device_name) + ? AVAILABLE + : UNSUPPORTED_ON_DEVICE; } @Override @@ -117,13 +109,11 @@ public class DeviceNamePreferenceController extends BasePreferenceController return mWifiDeviceNameTextValidator.isTextValid(deviceName); } - public void setLocalBluetoothManager(LocalBluetoothManager localBluetoothManager) { - mBluetoothManager = localBluetoothManager; - } - - public void confirmDeviceName() { - if (mPendingDeviceName != null) { + public void updateDeviceName(boolean update) { + if (update && mPendingDeviceName != null) { setDeviceName(mPendingDeviceName); + } else { + mPreference.setText(getSummary().toString()); } } @@ -148,14 +138,8 @@ public class DeviceNamePreferenceController extends BasePreferenceController } private void setBluetoothDeviceName(String deviceName) { - // Bluetooth manager doesn't exist for certain devices. - if (mBluetoothManager == null) { - return; - } - - final LocalBluetoothAdapter localBluetoothAdapter = mBluetoothManager.getBluetoothAdapter(); - if (localBluetoothAdapter != null) { - localBluetoothAdapter.setName(getFilteredBluetoothString(deviceName)); + if (mBluetoothAdapter != null) { + mBluetoothAdapter.setName(getFilteredBluetoothString(deviceName)); } } @@ -164,7 +148,8 @@ public class DeviceNamePreferenceController extends BasePreferenceController * For more information, see {@link com.android.settings.bluetooth.BluetoothNameDialogFragment}. */ private static final String getFilteredBluetoothString(final String deviceName) { - CharSequence filteredSequence = new BluetoothLengthDeviceNameFilter().filter(deviceName, 0, deviceName.length(), + CharSequence filteredSequence = new BluetoothLengthDeviceNameFilter().filter(deviceName, 0, + deviceName.length(), new SpannedString(""), 0, 0); // null -> use the original diff --git a/src/com/android/settings/deviceinfo/FccEquipmentIdPreferenceController.java b/src/com/android/settings/deviceinfo/FccEquipmentIdPreferenceController.java index 62077ca2b1..1150838dac 100644 --- a/src/com/android/settings/deviceinfo/FccEquipmentIdPreferenceController.java +++ b/src/com/android/settings/deviceinfo/FccEquipmentIdPreferenceController.java @@ -17,9 +17,10 @@ package com.android.settings.deviceinfo; import android.content.Context; import android.os.SystemProperties; +import android.text.TextUtils; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.text.TextUtils; import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; diff --git a/src/com/android/settings/deviceinfo/FeedbackPreferenceController.java b/src/com/android/settings/deviceinfo/FeedbackPreferenceController.java index 52721821a4..d94586be09 100644 --- a/src/com/android/settings/deviceinfo/FeedbackPreferenceController.java +++ b/src/com/android/settings/deviceinfo/FeedbackPreferenceController.java @@ -15,12 +15,13 @@ */ package com.android.settings.deviceinfo; -import android.app.Fragment; import android.content.Context; import android.content.Intent; -import androidx.preference.Preference; import android.text.TextUtils; +import androidx.fragment.app.Fragment; +import androidx.preference.Preference; + import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.DeviceInfoUtils; import com.android.settingslib.core.AbstractPreferenceController; diff --git a/src/com/android/settings/deviceinfo/HardwareInfoDialogFragment.java b/src/com/android/settings/deviceinfo/HardwareInfoDialogFragment.java deleted file mode 100644 index 58155af1ca..0000000000 --- a/src/com/android/settings/deviceinfo/HardwareInfoDialogFragment.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (C) 2017 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.app.AlertDialog; -import android.app.Dialog; -import android.os.Build; -import android.os.Bundle; -import android.os.SystemProperties; -import androidx.annotation.VisibleForTesting; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.TextView; - -import com.android.internal.logging.nano.MetricsProto; -import com.android.settings.R; -import com.android.settings.core.instrumentation.InstrumentedDialogFragment; - -public class HardwareInfoDialogFragment extends InstrumentedDialogFragment { - - public static final String TAG = "HardwareInfo"; - - @Override - public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DIALOG_SETTINGS_HARDWARE_INFO; - } - - public static HardwareInfoDialogFragment newInstance() { - final HardwareInfoDialogFragment fragment = new HardwareInfoDialogFragment(); - return fragment; - } - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()) - .setTitle(R.string.hardware_info) - .setPositiveButton(android.R.string.ok, null); - final View content = LayoutInflater.from(builder.getContext()) - .inflate(R.layout.dialog_hardware_info, null /* parent */); - // Model - setText(content, R.id.model_label, R.id.model_value, - DeviceModelPreferenceController.getDeviceModel()); - - // Serial number - setText(content, R.id.serial_number_label, R.id.serial_number_value, getSerialNumber()); - - // Hardware rev - setText(content, R.id.hardware_rev_label, R.id.hardware_rev_value, - SystemProperties.get("ro.boot.hardware.revision")); - - return builder.setView(content).create(); - } - - @VisibleForTesting - void setText(View content, int labelViewId, int valueViewId, String value) { - if (content == null) { - return; - } - final View labelView = content.findViewById(labelViewId); - final TextView valueView = content.findViewById(valueViewId); - if (!TextUtils.isEmpty(value)) { - labelView.setVisibility(View.VISIBLE); - valueView.setVisibility(View.VISIBLE); - valueView.setText(value); - } else { - labelView.setVisibility(View.GONE); - valueView.setVisibility(View.GONE); - } - } - - @VisibleForTesting - String getSerialNumber() { - return Build.getSerial(); - } -} diff --git a/src/com/android/settings/deviceinfo/HardwareInfoPreferenceController.java b/src/com/android/settings/deviceinfo/HardwareInfoPreferenceController.java new file mode 100644 index 0000000000..29f1391207 --- /dev/null +++ b/src/com/android/settings/deviceinfo/HardwareInfoPreferenceController.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2017 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.os.Build; +import android.util.Log; + +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; +import com.android.settingslib.DeviceInfoUtils; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; + +public class HardwareInfoPreferenceController extends BasePreferenceController { + + private static final String TAG = "DeviceModelPrefCtrl"; + + public HardwareInfoPreferenceController(Context context, String key) { + super(context, key); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + } + + @Override + public int getAvailabilityStatus() { + return mContext.getResources().getBoolean(R.bool.config_show_device_model) + ? AVAILABLE_UNSEARCHABLE : UNSUPPORTED_ON_DEVICE; + } + + @Override + public CharSequence getSummary() { + return mContext.getResources().getString(R.string.model_summary, getDeviceModel()); + } + + public static String getDeviceModel() { + FutureTask<String> msvSuffixTask = new FutureTask<>(() -> DeviceInfoUtils.getMsvSuffix()); + + msvSuffixTask.run(); + try { + // Wait for msv suffix value. + final String msvSuffix = msvSuffixTask.get(); + return Build.MODEL + msvSuffix; + } catch (ExecutionException e) { + Log.e(TAG, "Execution error, so we only show model name"); + } catch (InterruptedException e) { + Log.e(TAG, "Interruption error, so we only show model name"); + } + // If we can't get an msv suffix value successfully, + // it's better to return model name. + return Build.MODEL; + } +} diff --git a/src/com/android/settings/deviceinfo/IpAddressPreferenceController.java b/src/com/android/settings/deviceinfo/IpAddressPreferenceController.java index 1af63975f5..6ba45b35ee 100644 --- a/src/com/android/settings/deviceinfo/IpAddressPreferenceController.java +++ b/src/com/android/settings/deviceinfo/IpAddressPreferenceController.java @@ -18,9 +18,8 @@ package com.android.settings.deviceinfo; import android.content.Context; -import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.R; - +import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.deviceinfo.AbstractIpAddressPreferenceController; diff --git a/src/com/android/settings/deviceinfo/KernelVersionPreferenceController.java b/src/com/android/settings/deviceinfo/KernelVersionPreferenceController.java index c0ae99a1ee..5c2b641710 100644 --- a/src/com/android/settings/deviceinfo/KernelVersionPreferenceController.java +++ b/src/com/android/settings/deviceinfo/KernelVersionPreferenceController.java @@ -16,6 +16,7 @@ package com.android.settings.deviceinfo; import android.content.Context; + import androidx.preference.Preference; import com.android.settings.core.PreferenceControllerMixin; diff --git a/src/com/android/settings/deviceinfo/PhoneNumberPreferenceController.java b/src/com/android/settings/deviceinfo/PhoneNumberPreferenceController.java index 95cd09c395..537c705d0b 100644 --- a/src/com/android/settings/deviceinfo/PhoneNumberPreferenceController.java +++ b/src/com/android/settings/deviceinfo/PhoneNumberPreferenceController.java @@ -16,27 +16,31 @@ package com.android.settings.deviceinfo; +import static android.content.Context.CLIPBOARD_SERVICE; + +import android.content.ClipData; +import android.content.ClipboardManager; import android.content.Context; -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; import android.text.BidiFormatter; import android.text.TextDirectionHeuristics; import android.text.TextUtils; +import android.widget.Toast; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; import com.android.settings.R; -import com.android.settings.core.PreferenceControllerMixin; +import com.android.settings.core.BasePreferenceController; import com.android.settingslib.DeviceInfoUtils; -import com.android.settingslib.core.AbstractPreferenceController; import java.util.ArrayList; import java.util.List; -public class PhoneNumberPreferenceController extends AbstractPreferenceController implements - PreferenceControllerMixin { +public class PhoneNumberPreferenceController extends BasePreferenceController { private final static String KEY_PHONE_NUMBER = "phone_number"; @@ -44,21 +48,20 @@ public class PhoneNumberPreferenceController extends AbstractPreferenceControlle private final SubscriptionManager mSubscriptionManager; private final List<Preference> mPreferenceList = new ArrayList<>(); - public PhoneNumberPreferenceController(Context context) { - super(context); - mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); - mSubscriptionManager = (SubscriptionManager) context.getSystemService( - Context.TELEPHONY_SUBSCRIPTION_SERVICE); + public PhoneNumberPreferenceController(Context context, String key) { + super(context, key); + mTelephonyManager = mContext.getSystemService(TelephonyManager.class); + mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class); } @Override - public String getPreferenceKey() { - return KEY_PHONE_NUMBER; + public int getAvailabilityStatus() { + return mTelephonyManager.isVoiceCapable() ? AVAILABLE : UNSUPPORTED_ON_DEVICE; } @Override - public boolean isAvailable() { - return mTelephonyManager.isVoiceCapable(); + public CharSequence getSummary() { + return getFirstPhoneNumber(); } @Override @@ -88,10 +91,47 @@ public class PhoneNumberPreferenceController extends AbstractPreferenceControlle } } + @Override + public boolean isSliceable() { + return true; + } + + @Override + public boolean isCopyableSlice() { + return true; + } + + @Override + public boolean useDynamicSliceSummary() { + return true; + } + + @Override + public void copy() { + final ClipboardManager clipboard = (ClipboardManager) mContext.getSystemService( + CLIPBOARD_SERVICE); + clipboard.setPrimaryClip(ClipData.newPlainText("text", getFirstPhoneNumber())); + + final String toast = mContext.getString(R.string.copyable_slice_toast, + mContext.getText(R.string.status_number)); + Toast.makeText(mContext, toast, Toast.LENGTH_SHORT).show(); + } + + private CharSequence getFirstPhoneNumber() { + final List<SubscriptionInfo> subscriptionInfoList = + mSubscriptionManager.getActiveSubscriptionInfoList(true); + if (subscriptionInfoList == null || subscriptionInfoList.isEmpty()) { + return mContext.getText(R.string.device_info_default); + } + + // For now, We only return first result for slice view. + return getFormattedPhoneNumber(subscriptionInfoList.get(0)); + } + private CharSequence getPhoneNumber(int simSlot) { final SubscriptionInfo subscriptionInfo = getSubscriptionInfo(simSlot); if (subscriptionInfo == null) { - return mContext.getString(R.string.device_info_default); + return mContext.getText(R.string.device_info_default); } return getFormattedPhoneNumber(subscriptionInfo); @@ -106,7 +146,7 @@ public class PhoneNumberPreferenceController extends AbstractPreferenceControlle @VisibleForTesting SubscriptionInfo getSubscriptionInfo(int simSlot) { final List<SubscriptionInfo> subscriptionInfoList = - mSubscriptionManager.getActiveSubscriptionInfoList(); + mSubscriptionManager.getActiveSubscriptionInfoList(true); if (subscriptionInfoList != null) { for (SubscriptionInfo info : subscriptionInfoList) { if (info.getSimSlotIndex() == simSlot) { diff --git a/src/com/android/settings/deviceinfo/PrivateVolumeForget.java b/src/com/android/settings/deviceinfo/PrivateVolumeForget.java index b6d50cef75..bd0772c668 100644 --- a/src/com/android/settings/deviceinfo/PrivateVolumeForget.java +++ b/src/com/android/settings/deviceinfo/PrivateVolumeForget.java @@ -16,10 +16,8 @@ package com.android.settings.deviceinfo; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.DialogFragment; -import android.app.Fragment; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; @@ -33,19 +31,31 @@ import android.view.ViewGroup; import android.widget.Button; import android.widget.TextView; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; + import com.android.settings.R; -import com.android.settings.SettingsPreferenceFragment; +import com.android.settings.core.InstrumentedFragment; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; +import com.android.settings.search.actionbar.SearchMenuController; -public class PrivateVolumeForget extends SettingsPreferenceFragment { - private static final String TAG_FORGET_CONFIRM = "forget_confirm"; +public class PrivateVolumeForget extends InstrumentedFragment { + @VisibleForTesting + static final String TAG_FORGET_CONFIRM = "forget_confirm"; private VolumeRecord mRecord; @Override public int getMetricsCategory() { - return MetricsEvent.DEVICEINFO_STORAGE; + return SettingsEnums.DEVICEINFO_STORAGE; + } + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + setHasOptionsMenu(true); + SearchMenuController.init(this /* host */); } @Override @@ -87,7 +97,7 @@ public class PrivateVolumeForget extends SettingsPreferenceFragment { @Override public int getMetricsCategory() { - return MetricsEvent.DIALOG_VOLUME_FORGET; + return SettingsEnums.DIALOG_VOLUME_FORGET; } public static void show(Fragment parent, String fsUuid) { @@ -116,12 +126,12 @@ public class PrivateVolumeForget extends SettingsPreferenceFragment { builder.setPositiveButton(R.string.storage_menu_forget, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - storage.forgetVolume(fsUuid); - getActivity().finish(); - } - }); + @Override + public void onClick(DialogInterface dialog, int which) { + storage.forgetVolume(fsUuid); + getActivity().finish(); + } + }); builder.setNegativeButton(R.string.cancel, null); return builder.create(); diff --git a/src/com/android/settings/deviceinfo/PrivateVolumeFormat.java b/src/com/android/settings/deviceinfo/PrivateVolumeFormat.java index 4a63e64a44..dadff3c1db 100644 --- a/src/com/android/settings/deviceinfo/PrivateVolumeFormat.java +++ b/src/com/android/settings/deviceinfo/PrivateVolumeFormat.java @@ -21,6 +21,7 @@ import static android.os.storage.DiskInfo.EXTRA_DISK_ID; import static com.android.settings.deviceinfo.StorageWizardBase.EXTRA_FORMAT_FORGET_UUID; import static com.android.settings.deviceinfo.StorageWizardBase.EXTRA_FORMAT_PRIVATE; +import android.app.settings.SettingsEnums; import android.content.Intent; import android.os.Bundle; import android.os.storage.DiskInfo; @@ -34,17 +35,16 @@ import android.view.ViewGroup; import android.widget.Button; import android.widget.TextView; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; -import com.android.settings.core.InstrumentedPreferenceFragment; +import com.android.settings.core.InstrumentedFragment; -public class PrivateVolumeFormat extends InstrumentedPreferenceFragment { +public class PrivateVolumeFormat extends InstrumentedFragment { private VolumeInfo mVolume; private DiskInfo mDisk; @Override public int getMetricsCategory() { - return MetricsEvent.DEVICEINFO_STORAGE; + return SettingsEnums.DEVICEINFO_STORAGE; } @Override diff --git a/src/com/android/settings/deviceinfo/PrivateVolumeOptionMenuController.java b/src/com/android/settings/deviceinfo/PrivateVolumeOptionMenuController.java index 75fc73b2f0..00a79a03f1 100644 --- a/src/com/android/settings/deviceinfo/PrivateVolumeOptionMenuController.java +++ b/src/com/android/settings/deviceinfo/PrivateVolumeOptionMenuController.java @@ -18,18 +18,17 @@ package com.android.settings.deviceinfo; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; import android.os.storage.VolumeInfo; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import com.android.settings.R; -import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnCreateOptionsMenu; import com.android.settingslib.core.lifecycle.events.OnOptionsItemSelected; import com.android.settingslib.core.lifecycle.events.OnPrepareOptionsMenu; -import com.android.settingslib.wrapper.PackageManagerWrapper; import java.util.Objects; @@ -42,10 +41,10 @@ public class PrivateVolumeOptionMenuController implements LifecycleObserver, OnC private Context mContext; private VolumeInfo mVolumeInfo; - private PackageManagerWrapper mPm; + private PackageManager mPm; public PrivateVolumeOptionMenuController( - Context context, VolumeInfo volumeInfo, PackageManagerWrapper packageManager) { + Context context, VolumeInfo volumeInfo, PackageManager packageManager) { mContext = context; mVolumeInfo = volumeInfo; mPm = packageManager; diff --git a/src/com/android/settings/deviceinfo/PrivateVolumeSettings.java b/src/com/android/settings/deviceinfo/PrivateVolumeSettings.java index 7fcedb7e6e..8e6158ba23 100644 --- a/src/com/android/settings/deviceinfo/PrivateVolumeSettings.java +++ b/src/com/android/settings/deviceinfo/PrivateVolumeSettings.java @@ -16,9 +16,8 @@ package com.android.settings.deviceinfo; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.Fragment; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -36,10 +35,6 @@ import android.os.storage.StorageManager; import android.os.storage.VolumeInfo; import android.os.storage.VolumeRecord; import android.provider.DocumentsContract; -import androidx.preference.Preference; -import androidx.preference.PreferenceCategory; -import androidx.preference.PreferenceGroup; -import androidx.preference.PreferenceScreen; import android.text.TextUtils; import android.text.format.Formatter; import android.text.format.Formatter.BytesResult; @@ -51,7 +46,13 @@ import android.view.MenuItem; import android.view.View; import android.widget.EditText; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceGroup; +import androidx.preference.PreferenceScreen; + import com.android.settings.R; import com.android.settings.Settings.StorageUseActivity; import com.android.settings.SettingsPreferenceFragment; @@ -139,7 +140,7 @@ public class PrivateVolumeSettings extends SettingsPreferenceFragment { @Override public int getMetricsCategory() { - return MetricsEvent.DEVICEINFO_STORAGE; + return SettingsEnums.DEVICEINFO_STORAGE; } @Override @@ -202,7 +203,7 @@ public class PrivateVolumeSettings extends SettingsPreferenceFragment { setTitle(); // Valid options may have changed - getFragmentManager().invalidateOptionsMenu(); + getActivity().invalidateOptionsMenu(); final Context context = getActivity(); final PreferenceScreen screen = getPreferenceScreen(); @@ -423,41 +424,41 @@ public class PrivateVolumeSettings extends SettingsPreferenceFragment { public boolean onOptionsItemSelected(MenuItem item) { final Context context = getActivity(); final Bundle args = new Bundle(); - switch (item.getItemId()) { - case R.id.storage_rename: - RenameFragment.show(this, mVolume); - return true; - case R.id.storage_mount: - new MountTask(context, mVolume).execute(); - return true; - case R.id.storage_unmount: - args.putString(VolumeInfo.EXTRA_VOLUME_ID, mVolume.getId()); - new SubSettingLauncher(context) - .setDestination(PrivateVolumeUnmount.class.getCanonicalName()) - .setTitle(R.string.storage_menu_unmount) - .setSourceMetricsCategory(getMetricsCategory()) - .setArguments(args) - .launch(); - return true; - case R.id.storage_format: - args.putString(VolumeInfo.EXTRA_VOLUME_ID, mVolume.getId()); - new SubSettingLauncher(context) - .setDestination(PrivateVolumeFormat.class.getCanonicalName()) - .setTitle(R.string.storage_menu_format) - .setSourceMetricsCategory(getMetricsCategory()) - .setArguments(args) - .launch(); - return true; - case R.id.storage_migrate: - final Intent intent = new Intent(context, StorageWizardMigrateConfirm.class); - intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, mVolume.getId()); - startActivity(intent); - return true; - case R.id.storage_free: - final Intent deletion_helper_intent = - new Intent(StorageManager.ACTION_MANAGE_STORAGE); - startActivity(deletion_helper_intent); - return true; + int i = item.getItemId(); + if (i == R.id.storage_rename) { + RenameFragment.show(this, mVolume); + return true; + } else if (i == R.id.storage_mount) { + new MountTask(context, mVolume).execute(); + return true; + } else if (i == R.id.storage_unmount) { + args.putString(VolumeInfo.EXTRA_VOLUME_ID, mVolume.getId()); + new SubSettingLauncher(context) + .setDestination(PrivateVolumeUnmount.class.getCanonicalName()) + .setTitleRes(R.string.storage_menu_unmount) + .setSourceMetricsCategory(getMetricsCategory()) + .setArguments(args) + .launch(); + return true; + } else if (i == R.id.storage_format) { + args.putString(VolumeInfo.EXTRA_VOLUME_ID, mVolume.getId()); + new SubSettingLauncher(context) + .setDestination(PrivateVolumeFormat.class.getCanonicalName()) + .setTitleRes(R.string.storage_menu_format) + .setSourceMetricsCategory(getMetricsCategory()) + .setArguments(args) + .launch(); + return true; + } else if (i == R.id.storage_migrate) { + final Intent intent = new Intent(context, StorageWizardMigrateConfirm.class); + intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, mVolume.getId()); + startActivity(intent); + return true; + } else if (i == R.id.storage_free) { + final Intent deletion_helper_intent = + new Intent(StorageManager.ACTION_MANAGE_STORAGE); + startActivity(deletion_helper_intent); + return true; } return super.onOptionsItemSelected(item); } @@ -475,56 +476,42 @@ public class PrivateVolumeSettings extends SettingsPreferenceFragment { itemTitleId = 0; } Intent intent = null; - switch (itemTitleId) { - case R.string.storage_detail_apps: { - Bundle args = new Bundle(); - args.putString(ManageApplications.EXTRA_CLASSNAME, - StorageUseActivity.class.getName()); - args.putString(ManageApplications.EXTRA_VOLUME_UUID, mVolume.getFsUuid()); - args.putString(ManageApplications.EXTRA_VOLUME_NAME, mVolume.getDescription()); - args.putInt( - ManageApplications.EXTRA_STORAGE_TYPE, - ManageApplications.STORAGE_TYPE_LEGACY); - intent = new SubSettingLauncher(getActivity()) - .setDestination(ManageApplications.class.getName()) - .setArguments(args) - .setTitle(R.string.apps_storage) - .setSourceMetricsCategory(getMetricsCategory()) - .toIntent(); - - } break; - case R.string.storage_detail_images: { - intent = getIntentForStorage(AUTHORITY_MEDIA, "images_root"); - } break; - case R.string.storage_detail_videos: { - intent = getIntentForStorage(AUTHORITY_MEDIA, "videos_root"); - } break; - case R.string.storage_detail_audio: { - intent = getIntentForStorage(AUTHORITY_MEDIA, "audio_root"); - } break; - case R.string.storage_detail_system: { - SystemInfoFragment.show(this); - return true; - - } - case R.string.storage_detail_other: { - OtherInfoFragment.show(this, mStorageManager.getBestVolumeDescription(mVolume), - mSharedVolume, userId); - return true; - - } - case R.string.storage_detail_cached: { - ConfirmClearCacheFragment.show(this); - return true; - - } - case R.string.storage_menu_explore: { - intent = mSharedVolume.buildBrowseIntent(); - } break; - case 0: { - UserInfoFragment.show(this, pref.getTitle(), pref.getSummary()); - return true; - } + if (itemTitleId == R.string.storage_detail_apps) { + Bundle args = new Bundle(); + args.putString(ManageApplications.EXTRA_CLASSNAME, + StorageUseActivity.class.getName()); + args.putString(ManageApplications.EXTRA_VOLUME_UUID, mVolume.getFsUuid()); + args.putString(ManageApplications.EXTRA_VOLUME_NAME, mVolume.getDescription()); + args.putInt( + ManageApplications.EXTRA_STORAGE_TYPE, + ManageApplications.STORAGE_TYPE_LEGACY); + intent = new SubSettingLauncher(getActivity()) + .setDestination(ManageApplications.class.getName()) + .setArguments(args) + .setTitleRes(R.string.apps_storage) + .setSourceMetricsCategory(getMetricsCategory()) + .toIntent(); + } else if (itemTitleId == R.string.storage_detail_images) { + intent = getIntentForStorage(AUTHORITY_MEDIA, "images_root"); + } else if (itemTitleId == R.string.storage_detail_videos) { + intent = getIntentForStorage(AUTHORITY_MEDIA, "videos_root"); + } else if (itemTitleId == R.string.storage_detail_audio) { + intent = getIntentForStorage(AUTHORITY_MEDIA, "audio_root"); + } else if (itemTitleId == R.string.storage_detail_system) { + SystemInfoFragment.show(this); + return true; + } else if (itemTitleId == R.string.storage_detail_other) { + OtherInfoFragment.show(this, mStorageManager.getBestVolumeDescription(mVolume), + mSharedVolume, userId); + return true; + } else if (itemTitleId == R.string.storage_detail_cached) { + ConfirmClearCacheFragment.show(this); + return true; + } else if (itemTitleId == R.string.storage_menu_explore) { + intent = mSharedVolume.buildBrowseIntent(); + } else if (itemTitleId == 0) { + UserInfoFragment.show(this, pref.getTitle(), pref.getSummary()); + return true; } if (intent != null) { @@ -568,75 +555,64 @@ public class PrivateVolumeSettings extends SettingsPreferenceFragment { } catch (NumberFormatException e) { itemTitleId = 0; } - switch (itemTitleId) { - case R.string.storage_detail_system: { - updatePreference(item, mSystemSize); - accountedSize += mSystemSize; - if (LOGV) Log.v(TAG, "mSystemSize: " + mSystemSize - + " accountedSize: " + accountedSize); - } break; - case R.string.storage_detail_apps: { - updatePreference(item, details.appsSize.get(userId)); - accountedSize += details.appsSize.get(userId); - if (LOGV) Log.v(TAG, "appsSize: " + details.appsSize.get(userId) - + " accountedSize: " + accountedSize); - } break; - case R.string.storage_detail_images: { - final long imagesSize = totalValues(details, userId, - Environment.DIRECTORY_DCIM, Environment.DIRECTORY_PICTURES); - updatePreference(item, imagesSize); - accountedSize += imagesSize; - if (LOGV) Log.v(TAG, "imagesSize: " + imagesSize - + " accountedSize: " + accountedSize); - } break; - case R.string.storage_detail_videos: { - final long videosSize = totalValues(details, userId, - Environment.DIRECTORY_MOVIES); - updatePreference(item, videosSize); - accountedSize += videosSize; - if (LOGV) Log.v(TAG, "videosSize: " + videosSize - + " accountedSize: " + accountedSize); - } break; - case R.string.storage_detail_audio: { - final long audioSize = totalValues(details, userId, - Environment.DIRECTORY_MUSIC, - Environment.DIRECTORY_ALARMS, Environment.DIRECTORY_NOTIFICATIONS, - Environment.DIRECTORY_RINGTONES, Environment.DIRECTORY_PODCASTS); - updatePreference(item, audioSize); - accountedSize += audioSize; - if (LOGV) Log.v(TAG, "audioSize: " + audioSize - + " accountedSize: " + accountedSize); - } break; - case R.string.storage_detail_other: { - final long downloadsSize = totalValues(details, userId, - Environment.DIRECTORY_DOWNLOADS); - final long miscSize = details.miscSize.get(userId); - totalDownloadsSize += downloadsSize; - totalMiscSize += miscSize; - accountedSize += miscSize + downloadsSize; - - if (LOGV) - Log.v(TAG, "miscSize for " + userId + ": " + miscSize + "(total: " - + totalMiscSize + ") \ndownloadsSize: " + downloadsSize + "(total: " - + totalDownloadsSize + ") accountedSize: " + accountedSize); - - // Cannot display 'Other' until all known items are accounted for. - otherItem = item; - } break; - case R.string.storage_detail_cached: { - updatePreference(item, details.cacheSize); - accountedSize += details.cacheSize; - if (LOGV) - Log.v(TAG, "cacheSize: " + details.cacheSize + " accountedSize: " - + accountedSize); - } break; - case 0: { - final long userSize = details.usersSize.get(userId); - updatePreference(item, userSize); - accountedSize += userSize; - if (LOGV) Log.v(TAG, "userSize: " + userSize - + " accountedSize: " + accountedSize); - } break; + // Cannot display 'Other' until all known items are accounted for. + if (itemTitleId == R.string.storage_detail_system) { + updatePreference(item, mSystemSize); + accountedSize += mSystemSize; + if (LOGV) Log.v(TAG, "mSystemSize: " + mSystemSize + + " accountedSize: " + accountedSize); + } else if (itemTitleId == R.string.storage_detail_apps) { + updatePreference(item, details.appsSize.get(userId)); + accountedSize += details.appsSize.get(userId); + if (LOGV) Log.v(TAG, "appsSize: " + details.appsSize.get(userId) + + " accountedSize: " + accountedSize); + } else if (itemTitleId == R.string.storage_detail_images) { + final long imagesSize = totalValues(details, userId, + Environment.DIRECTORY_DCIM, Environment.DIRECTORY_PICTURES); + updatePreference(item, imagesSize); + accountedSize += imagesSize; + if (LOGV) Log.v(TAG, "imagesSize: " + imagesSize + + " accountedSize: " + accountedSize); + } else if (itemTitleId == R.string.storage_detail_videos) { + final long videosSize = totalValues(details, userId, + Environment.DIRECTORY_MOVIES); + updatePreference(item, videosSize); + accountedSize += videosSize; + if (LOGV) Log.v(TAG, "videosSize: " + videosSize + + " accountedSize: " + accountedSize); + } else if (itemTitleId == R.string.storage_detail_audio) { + final long audioSize = totalValues(details, userId, + Environment.DIRECTORY_MUSIC, + Environment.DIRECTORY_ALARMS, Environment.DIRECTORY_NOTIFICATIONS, + Environment.DIRECTORY_RINGTONES, Environment.DIRECTORY_PODCASTS); + updatePreference(item, audioSize); + accountedSize += audioSize; + if (LOGV) Log.v(TAG, "audioSize: " + audioSize + + " accountedSize: " + accountedSize); + } else if (itemTitleId == R.string.storage_detail_other) { + final long downloadsSize = totalValues(details, userId, + Environment.DIRECTORY_DOWNLOADS); + final long miscSize = details.miscSize.get(userId); + totalDownloadsSize += downloadsSize; + totalMiscSize += miscSize; + accountedSize += miscSize + downloadsSize; + if (LOGV) + Log.v(TAG, "miscSize for " + userId + ": " + miscSize + "(total: " + + totalMiscSize + ") \ndownloadsSize: " + downloadsSize + "(total: " + + totalDownloadsSize + ") accountedSize: " + accountedSize); + otherItem = item; + } else if (itemTitleId == R.string.storage_detail_cached) { + updatePreference(item, details.cacheSize); + accountedSize += details.cacheSize; + if (LOGV) + Log.v(TAG, "cacheSize: " + details.cacheSize + " accountedSize: " + + accountedSize); + } else if (itemTitleId == 0) { + final long userSize = details.usersSize.get(userId); + updatePreference(item, userSize); + accountedSize += userSize; + if (LOGV) Log.v(TAG, "userSize: " + userSize + + " accountedSize: " + accountedSize); } } if (otherItem != null) { @@ -706,7 +682,7 @@ public class PrivateVolumeSettings extends SettingsPreferenceFragment { @Override public int getMetricsCategory() { - return MetricsEvent.DIALOG_VOLUME_RENAME; + return SettingsEnums.DIALOG_VOLUME_RENAME; } @Override @@ -754,7 +730,7 @@ public class PrivateVolumeSettings extends SettingsPreferenceFragment { @Override public int getMetricsCategory() { - return MetricsEvent.DIALOG_STORAGE_SYSTEM_INFO; + return SettingsEnums.DIALOG_STORAGE_SYSTEM_INFO; } @Override @@ -785,7 +761,7 @@ public class PrivateVolumeSettings extends SettingsPreferenceFragment { @Override public int getMetricsCategory() { - return MetricsEvent.DIALOG_STORAGE_OTHER_INFO; + return SettingsEnums.DIALOG_STORAGE_OTHER_INFO; } @Override @@ -827,7 +803,7 @@ public class PrivateVolumeSettings extends SettingsPreferenceFragment { @Override public int getMetricsCategory() { - return MetricsEvent.DIALOG_STORAGE_USER_INFO; + return SettingsEnums.DIALOG_STORAGE_USER_INFO; } @Override @@ -861,7 +837,7 @@ public class PrivateVolumeSettings extends SettingsPreferenceFragment { @Override public int getMetricsCategory() { - return MetricsEvent.DIALOG_STORAGE_CLEAR_CACHE; + return SettingsEnums.DIALOG_STORAGE_CLEAR_CACHE; } @Override diff --git a/src/com/android/settings/deviceinfo/PrivateVolumeUnmount.java b/src/com/android/settings/deviceinfo/PrivateVolumeUnmount.java index 0aa5b920f5..3289df9ae3 100644 --- a/src/com/android/settings/deviceinfo/PrivateVolumeUnmount.java +++ b/src/com/android/settings/deviceinfo/PrivateVolumeUnmount.java @@ -16,6 +16,7 @@ package com.android.settings.deviceinfo; +import android.app.settings.SettingsEnums; import android.os.Bundle; import android.os.storage.DiskInfo; import android.os.storage.StorageManager; @@ -28,18 +29,25 @@ import android.view.ViewGroup; import android.widget.Button; import android.widget.TextView; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; -import com.android.settings.SettingsPreferenceFragment; +import com.android.settings.core.InstrumentedFragment; import com.android.settings.deviceinfo.StorageSettings.UnmountTask; +import com.android.settings.search.actionbar.SearchMenuController; -public class PrivateVolumeUnmount extends SettingsPreferenceFragment { +public class PrivateVolumeUnmount extends InstrumentedFragment { private VolumeInfo mVolume; private DiskInfo mDisk; @Override public int getMetricsCategory() { - return MetricsEvent.DEVICEINFO_STORAGE; + return SettingsEnums.DEVICEINFO_STORAGE; + } + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + setHasOptionsMenu(true); + SearchMenuController.init(this /* host */); } @Override diff --git a/src/com/android/settings/deviceinfo/PublicVolumeSettings.java b/src/com/android/settings/deviceinfo/PublicVolumeSettings.java index 5982be68a2..fc74ab8c21 100644 --- a/src/com/android/settings/deviceinfo/PublicVolumeSettings.java +++ b/src/com/android/settings/deviceinfo/PublicVolumeSettings.java @@ -17,6 +17,7 @@ package com.android.settings.deviceinfo; import android.app.ActivityManager; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.res.Resources; import android.net.Uri; @@ -28,8 +29,6 @@ import android.os.storage.StorageManager; import android.os.storage.VolumeInfo; import android.os.storage.VolumeRecord; import android.provider.DocumentsContract; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; import android.text.TextUtils; import android.text.format.Formatter; import android.text.format.Formatter.BytesResult; @@ -37,7 +36,9 @@ import android.view.View; import android.view.ViewGroup; import android.widget.Button; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + import com.android.internal.util.Preconditions; import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; @@ -76,7 +77,7 @@ public class PublicVolumeSettings extends SettingsPreferenceFragment { @Override public int getMetricsCategory() { - return MetricsEvent.DEVICEINFO_STORAGE; + return SettingsEnums.DEVICEINFO_STORAGE; } @Override diff --git a/src/com/android/settings/deviceinfo/StorageDashboardFragment.java b/src/com/android/settings/deviceinfo/StorageDashboardFragment.java index 5575790c32..c9cec578df 100644 --- a/src/com/android/settings/deviceinfo/StorageDashboardFragment.java +++ b/src/com/android/settings/deviceinfo/StorageDashboardFragment.java @@ -17,10 +17,9 @@ package com.android.settings.deviceinfo; import android.app.Activity; -import android.app.LoaderManager; +import android.app.settings.SettingsEnums; import android.app.usage.StorageStatsManager; import android.content.Context; -import android.content.Loader; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.UserHandle; @@ -28,11 +27,13 @@ import android.os.UserManager; import android.os.storage.StorageManager; import android.os.storage.VolumeInfo; import android.provider.SearchIndexableResource; -import androidx.annotation.VisibleForTesting; import android.util.SparseArray; import android.view.View; -import com.android.internal.logging.nano.MetricsProto; +import androidx.annotation.VisibleForTesting; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.Loader; + import com.android.settings.R; import com.android.settings.Utils; import com.android.settings.dashboard.DashboardFragment; @@ -51,18 +52,21 @@ import com.android.settingslib.applications.StorageStatsSource; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.deviceinfo.PrivateStorageInfo; import com.android.settingslib.deviceinfo.StorageManagerVolumeProvider; -import com.android.settingslib.wrapper.PackageManagerWrapper; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -public class StorageDashboardFragment extends DashboardFragment implements +@SearchIndexable +public class StorageDashboardFragment extends DashboardFragment + implements LoaderManager.LoaderCallbacks<SparseArray<StorageAsyncLoader.AppsStorageResult>> { private static final String TAG = "StorageDashboardFrag"; private static final int STORAGE_JOB_ID = 0; private static final int ICON_JOB_ID = 1; private static final int VOLUME_SIZE_JOB_ID = 2; + private static final int OPTIONS_MENU_MIGRATE_DATA = 100; private VolumeInfo mVolume; private PrivateStorageInfo mStorageInfo; @@ -90,11 +94,18 @@ public class StorageDashboardFragment extends DashboardFragment implements initializeOptionsMenu(activity); } + @Override + public void onAttach(Context context) { + super.onAttach(context); + use(AutomaticStorageManagementSwitchPreferenceController.class).setFragmentManager( + getFragmentManager()); + } + @VisibleForTesting void initializeOptionsMenu(Activity activity) { mOptionMenuController = new PrivateVolumeOptionMenuController( - activity, mVolume, new PackageManagerWrapper(activity.getPackageManager())); - getLifecycle().addObserver(mOptionMenuController); + activity, mVolume, activity.getPackageManager()); + getSettingsLifecycle().addObserver(mOptionMenuController); setHasOptionsMenu(true); activity.invalidateOptionsMenu(); } @@ -108,7 +119,7 @@ public class StorageDashboardFragment extends DashboardFragment implements final Activity activity = getActivity(); EntityHeaderController.newInstance(activity, this /*fragment*/, null /* header view */) - .setRecyclerView(getListView(), getLifecycle()) + .setRecyclerView(getListView(), getSettingsLifecycle()) .styleActionBar(activity); } @@ -158,7 +169,7 @@ public class StorageDashboardFragment extends DashboardFragment implements @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.SETTINGS_STORAGE_CATEGORY; + return SettingsEnums.SETTINGS_STORAGE_CATEGORY; } @Override @@ -186,11 +197,6 @@ public class StorageDashboardFragment extends DashboardFragment implements mSecondaryUsers = SecondaryUserController.getSecondaryUserControllers(context, userManager); controllers.addAll(mSecondaryUsers); - final AutomaticStorageManagementSwitchPreferenceController asmController = - new AutomaticStorageManagementSwitchPreferenceController( - context, mMetricsFeatureProvider, getFragmentManager()); - getLifecycle().addObserver(asmController); - controllers.add(asmController); return controllers; } @@ -250,7 +256,7 @@ public class StorageDashboardFragment extends DashboardFragment implements return new StorageAsyncLoader(context, context.getSystemService(UserManager.class), mVolume.fsUuid, new StorageStatsSource(context), - new PackageManagerWrapper(context.getPackageManager())); + context.getPackageManager()); } @Override diff --git a/src/com/android/settings/deviceinfo/StorageItemPreference.java b/src/com/android/settings/deviceinfo/StorageItemPreference.java index 6cf85b1fc2..0766a740b3 100644 --- a/src/com/android/settings/deviceinfo/StorageItemPreference.java +++ b/src/com/android/settings/deviceinfo/StorageItemPreference.java @@ -18,12 +18,12 @@ package com.android.settings.deviceinfo; import android.content.Context; import android.content.res.Resources; -import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; import android.util.AttributeSet; -import android.view.View; import android.widget.ProgressBar; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + import com.android.settings.R; import com.android.settings.utils.FileSizeFormatter; diff --git a/src/com/android/settings/deviceinfo/StorageProfileFragment.java b/src/com/android/settings/deviceinfo/StorageProfileFragment.java index 5e66fdd0de..360265cfbb 100644 --- a/src/com/android/settings/deviceinfo/StorageProfileFragment.java +++ b/src/com/android/settings/deviceinfo/StorageProfileFragment.java @@ -16,18 +16,19 @@ package com.android.settings.deviceinfo; -import android.app.LoaderManager; +import android.app.settings.SettingsEnums; import android.content.Context; -import android.content.Loader; import android.os.Bundle; import android.os.UserHandle; import android.os.UserManager; import android.os.storage.StorageManager; import android.os.storage.VolumeInfo; -import androidx.annotation.VisibleForTesting; import android.util.SparseArray; -import com.android.internal.logging.nano.MetricsProto; +import androidx.annotation.VisibleForTesting; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.Loader; + import com.android.settings.R; import com.android.settings.Utils; import com.android.settings.dashboard.DashboardFragment; @@ -37,7 +38,6 @@ import com.android.settings.deviceinfo.storage.StorageItemPreferenceController; import com.android.settingslib.applications.StorageStatsSource; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.deviceinfo.StorageManagerVolumeProvider; -import com.android.settingslib.wrapper.PackageManagerWrapper; import java.util.ArrayList; import java.util.List; @@ -83,7 +83,7 @@ public class StorageProfileFragment extends DashboardFragment @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.SETTINGS_STORAGE_PROFILE; + return SettingsEnums.SETTINGS_STORAGE_PROFILE; } @Override @@ -118,7 +118,7 @@ public class StorageProfileFragment extends DashboardFragment context.getSystemService(UserManager.class), mVolume.fsUuid, new StorageStatsSource(context), - new PackageManagerWrapper(context.getPackageManager())); + context.getPackageManager()); } @Override diff --git a/src/com/android/settings/deviceinfo/StorageSettings.java b/src/com/android/settings/deviceinfo/StorageSettings.java index df4a218004..e791168de0 100644 --- a/src/com/android/settings/deviceinfo/StorageSettings.java +++ b/src/com/android/settings/deviceinfo/StorageSettings.java @@ -18,14 +18,11 @@ package com.android.settings.deviceinfo; import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.Fragment; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; -import android.graphics.Color; -import android.graphics.drawable.Drawable; import android.os.AsyncTask; import android.os.Bundle; import android.os.UserHandle; @@ -35,30 +32,32 @@ import android.os.storage.StorageEventListener; import android.os.storage.StorageManager; import android.os.storage.VolumeInfo; import android.os.storage.VolumeRecord; -import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; -import androidx.preference.PreferenceCategory; import android.text.TextUtils; import android.text.format.Formatter; import android.text.format.Formatter.BytesResult; import android.util.Log; import android.widget.Toast; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; + import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; import com.android.settings.core.SubSettingLauncher; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; -import com.android.settings.dashboard.SummaryLoader; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; import com.android.settings.search.SearchIndexableRaw; import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.deviceinfo.PrivateStorageInfo; import com.android.settingslib.deviceinfo.StorageManagerVolumeProvider; +import com.android.settingslib.search.SearchIndexable; -import java.text.NumberFormat; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -67,22 +66,13 @@ import java.util.List; * Panel showing both internal storage (both built-in storage and private * volumes) and removable storage (public volumes). */ +@SearchIndexable public class StorageSettings extends SettingsPreferenceFragment implements Indexable { static final String TAG = "StorageSettings"; private static final String TAG_VOLUME_UNMOUNTED = "volume_unmounted"; private static final String TAG_DISK_INIT = "disk_init"; - private static final int METRICS_CATEGORY = MetricsEvent.DEVICEINFO_STORAGE; - - static final int COLOR_PUBLIC = Color.parseColor("#ff9e9e9e"); - - static final int[] COLOR_PRIVATE = new int[]{ - Color.parseColor("#ff26a69a"), - Color.parseColor("#ffab47bc"), - Color.parseColor("#fff2a600"), - Color.parseColor("#ffec407a"), - Color.parseColor("#ffc0ca33"), - }; + private static final int METRICS_CATEGORY = SettingsEnums.DEVICEINFO_STORAGE; private StorageManager mStorageManager; @@ -160,8 +150,6 @@ public class StorageSettings extends SettingsPreferenceFragment implements Index mInternalCategory.addPreference(mInternalSummary); - int privateCount = 0; - final StorageManagerVolumeProvider smvp = new StorageManagerVolumeProvider(mStorageManager); final PrivateStorageInfo info = PrivateStorageInfo.getPrivateStorageInfo(smvp); final long privateTotalBytes = info.totalBytes; @@ -172,15 +160,20 @@ public class StorageSettings extends SettingsPreferenceFragment implements Index for (VolumeInfo vol : volumes) { if (vol.getType() == VolumeInfo.TYPE_PRIVATE) { - final long volumeTotalBytes = PrivateStorageInfo.getTotalSize(vol, - sTotalInternalStorage); - final int color = COLOR_PRIVATE[privateCount++ % COLOR_PRIVATE.length]; - mInternalCategory.addPreference( - new StorageVolumePreference(context, vol, color, volumeTotalBytes)); + + if (vol.getState() == VolumeInfo.STATE_UNMOUNTABLE) { + mInternalCategory.addPreference( + new StorageVolumePreference(context, vol, 0)); + } else { + final long volumeTotalBytes = PrivateStorageInfo.getTotalSize(vol, + sTotalInternalStorage); + mInternalCategory.addPreference( + new StorageVolumePreference(context, vol, volumeTotalBytes)); + } } else if (vol.getType() == VolumeInfo.TYPE_PUBLIC || vol.getType() == VolumeInfo.TYPE_STUB) { mExternalCategory.addPreference( - new StorageVolumePreference(context, vol, COLOR_PUBLIC, 0)); + new StorageVolumePreference(context, vol, 0)); } } @@ -190,15 +183,11 @@ public class StorageSettings extends SettingsPreferenceFragment implements Index if (rec.getType() == VolumeInfo.TYPE_PRIVATE && mStorageManager.findVolumeByUuid(rec.getFsUuid()) == null) { // TODO: add actual storage type to record - final Drawable icon = context.getDrawable(R.drawable.ic_sim_sd); - icon.mutate(); - icon.setTint(COLOR_PUBLIC); - final Preference pref = new Preference(context); pref.setKey(rec.getFsUuid()); pref.setTitle(rec.getNickname()); pref.setSummary(com.android.internal.R.string.ext_media_status_missing); - pref.setIcon(icon); + pref.setIcon(R.drawable.ic_sim_sd); mInternalCategory.addPreference(pref); } } @@ -238,7 +227,7 @@ public class StorageSettings extends SettingsPreferenceFragment implements Index new SubSettingLauncher(getActivity()) .setDestination(StorageDashboardFragment.class.getName()) .setArguments(args) - .setTitle(R.string.storage_settings) + .setTitleRes(R.string.storage_settings) .setSourceMetricsCategory(getMetricsCategory()) .launch(); finish(); @@ -285,7 +274,7 @@ public class StorageSettings extends SettingsPreferenceFragment implements Index if (VolumeInfo.ID_PRIVATE_INTERNAL.equals(vol.getId())) { new SubSettingLauncher(getContext()) .setDestination(StorageDashboardFragment.class.getCanonicalName()) - .setTitle(R.string.storage_settings) + .setTitleRes(R.string.storage_settings) .setSourceMetricsCategory(getMetricsCategory()) .setArguments(args) .launch(); @@ -296,7 +285,7 @@ public class StorageSettings extends SettingsPreferenceFragment implements Index sTotalInternalStorage)); new SubSettingLauncher(getContext()) .setDestination(PrivateVolumeSettings.class.getCanonicalName()) - .setTitle(-1) + .setTitleRes(-1) .setSourceMetricsCategory(getMetricsCategory()) .setArguments(args) .launch(); @@ -321,7 +310,7 @@ public class StorageSettings extends SettingsPreferenceFragment implements Index args.putString(VolumeRecord.EXTRA_FS_UUID, key); new SubSettingLauncher(getContext()) .setDestination(PrivateVolumeForget.class.getCanonicalName()) - .setTitle(R.string.storage_menu_forget) + .setTitleRes(R.string.storage_menu_forget) .setSourceMetricsCategory(getMetricsCategory()) .setArguments(args) .launch(); @@ -352,7 +341,7 @@ public class StorageSettings extends SettingsPreferenceFragment implements Index args.putString(VolumeInfo.EXTRA_VOLUME_ID, vol.getId()); new SubSettingLauncher(context) .setDestination(PublicVolumeSettings.class.getCanonicalName()) - .setTitle(-1) + .setTitleRes(-1) .setSourceMetricsCategory(METRICS_CATEGORY) .setArguments(args) .launch(); @@ -445,7 +434,7 @@ public class StorageSettings extends SettingsPreferenceFragment implements Index @Override public int getMetricsCategory() { - return MetricsEvent.DIALOG_VOLUME_UNMOUNT; + return SettingsEnums.DIALOG_VOLUME_UNMOUNT; } @Override @@ -471,10 +460,11 @@ public class StorageSettings extends SettingsPreferenceFragment implements Index * @return {@code true} iff a intent was shown. */ private boolean wasAdminSupportIntentShown(@NonNull String restriction) { - EnforcedAdmin admin = RestrictedLockUtils.checkIfRestrictionEnforced( - getActivity(), restriction, UserHandle.myUserId()); + EnforcedAdmin admin = RestrictedLockUtilsInternal + .checkIfRestrictionEnforced(getActivity(), restriction, + UserHandle.myUserId()); boolean hasBaseUserRestriction = - RestrictedLockUtils.hasBaseUserRestriction( + RestrictedLockUtilsInternal.hasBaseUserRestriction( getActivity(), restriction, UserHandle.myUserId()); if (admin != null && !hasBaseUserRestriction) { RestrictedLockUtils.sendShowAdminSupportDetailsIntent(getActivity(), @@ -510,7 +500,7 @@ public class StorageSettings extends SettingsPreferenceFragment implements Index public static class DiskInitFragment extends InstrumentedDialogFragment { @Override public int getMetricsCategory() { - return MetricsEvent.DIALOG_VOLUME_INIT; + return SettingsEnums.DIALOG_VOLUME_INIT; } public static void show(Fragment parent, int resId, String diskId) { @@ -551,41 +541,6 @@ public class StorageSettings extends SettingsPreferenceFragment implements Index } } - private static class SummaryProvider implements SummaryLoader.SummaryProvider { - private final Context mContext; - private final SummaryLoader mLoader; - private final StorageManagerVolumeProvider mStorageManagerVolumeProvider; - - private SummaryProvider(Context context, SummaryLoader loader) { - mContext = context; - mLoader = loader; - final StorageManager storageManager = mContext.getSystemService(StorageManager.class); - mStorageManagerVolumeProvider = new StorageManagerVolumeProvider(storageManager); - } - - @Override - public void setListening(boolean listening) { - if (listening) { - updateSummary(); - } - } - - private void updateSummary() { - // TODO: Register listener. - final NumberFormat percentageFormat = NumberFormat.getPercentInstance(); - final PrivateStorageInfo info = PrivateStorageInfo.getPrivateStorageInfo( - mStorageManagerVolumeProvider); - double privateUsedBytes = info.totalBytes - info.freeBytes; - mLoader.setSummary(this, mContext.getString(R.string.storage_summary, - percentageFormat.format(privateUsedBytes / info.totalBytes), - Formatter.formatFileSize(mContext, info.freeBytes))); - } - } - - - public static final SummaryLoader.SummaryProviderFactory SUMMARY_PROVIDER_FACTORY - = (activity, summaryLoader) -> new SummaryProvider(activity, summaryLoader); - /** Enable indexing of searchable data */ public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = new BaseSearchIndexProvider() { diff --git a/src/com/android/settings/deviceinfo/StorageSummaryPreference.java b/src/com/android/settings/deviceinfo/StorageSummaryPreference.java index 292ee44be1..72b67de1dd 100644 --- a/src/com/android/settings/deviceinfo/StorageSummaryPreference.java +++ b/src/com/android/settings/deviceinfo/StorageSummaryPreference.java @@ -18,13 +18,14 @@ package com.android.settings.deviceinfo; import android.content.Context; import android.graphics.Color; -import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; import android.util.MathUtils; import android.view.View; import android.widget.ProgressBar; import android.widget.TextView; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + import com.android.settings.R; public class StorageSummaryPreference extends Preference { diff --git a/src/com/android/settings/deviceinfo/StorageUnmountReceiver.java b/src/com/android/settings/deviceinfo/StorageUnmountReceiver.java index 095cf15af4..81ac97b21d 100644 --- a/src/com/android/settings/deviceinfo/StorageUnmountReceiver.java +++ b/src/com/android/settings/deviceinfo/StorageUnmountReceiver.java @@ -16,6 +16,8 @@ package com.android.settings.deviceinfo; +import static com.android.settings.deviceinfo.StorageSettings.TAG; + import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -25,8 +27,6 @@ import android.util.Log; import com.android.settings.deviceinfo.StorageSettings.UnmountTask; -import static com.android.settings.deviceinfo.StorageSettings.TAG; - public class StorageUnmountReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { diff --git a/src/com/android/settings/deviceinfo/StorageVolumePreference.java b/src/com/android/settings/deviceinfo/StorageVolumePreference.java index b6466cae98..6294ab90a1 100644 --- a/src/com/android/settings/deviceinfo/StorageVolumePreference.java +++ b/src/com/android/settings/deviceinfo/StorageVolumePreference.java @@ -23,8 +23,6 @@ import android.graphics.Color; import android.graphics.drawable.Drawable; import android.os.storage.StorageManager; import android.os.storage.VolumeInfo; -import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; import android.text.format.Formatter; import android.util.Log; import android.view.View; @@ -32,12 +30,15 @@ import android.view.View.OnClickListener; import android.widget.ImageView; import android.widget.ProgressBar; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + import com.android.settings.R; import com.android.settings.deviceinfo.StorageSettings.UnmountTask; import com.android.settingslib.Utils; -import java.io.IOException; import java.io.File; +import java.io.IOException; /** * Preference line representing a single {@link VolumeInfo}, possibly including @@ -49,16 +50,16 @@ public class StorageVolumePreference extends Preference { private final StorageManager mStorageManager; private final VolumeInfo mVolume; - private int mColor; private int mUsedPercent = -1; + private ColorStateList mColorTintList; // TODO: ideally, VolumeInfo should have a total physical size. - public StorageVolumePreference(Context context, VolumeInfo volume, int color, long totalBytes) { + public StorageVolumePreference(Context context, VolumeInfo volume, long totalBytes) { super(context); mStorageManager = context.getSystemService(StorageManager.class); mVolume = volume; - mColor = color; + mColorTintList = Utils.getColorAttr(context, android.R.attr.colorControlNormal); setLayoutResource(R.layout.storage_volume); @@ -106,8 +107,10 @@ public class StorageVolumePreference extends Preference { } if (freeBytes < mStorageManager.getStorageLowBytes(path)) { - mColor = Utils.getColorAttr(context, android.R.attr.colorError); + mColorTintList = Utils.getColorAttr(context, android.R.attr.colorError); icon = context.getDrawable(R.drawable.ic_warning_24dp); + icon.mutate(); + icon.setTintList(mColorTintList); } } else { @@ -115,8 +118,6 @@ public class StorageVolumePreference extends Preference { mUsedPercent = -1; } - icon.mutate(); - icon.setTint(mColor); setIcon(icon); if (volume.getType() == VolumeInfo.TYPE_PUBLIC @@ -129,7 +130,6 @@ public class StorageVolumePreference extends Preference { public void onBindViewHolder(PreferenceViewHolder view) { final ImageView unmount = (ImageView) view.findViewById(R.id.unmount); if (unmount != null) { - unmount.setImageTintList(ColorStateList.valueOf(Color.parseColor("#8a000000"))); unmount.setOnClickListener(mUnmountListener); } @@ -137,7 +137,7 @@ public class StorageVolumePreference extends Preference { if (mVolume.getType() == VolumeInfo.TYPE_PRIVATE && mUsedPercent != -1) { progress.setVisibility(View.VISIBLE); progress.setProgress(mUsedPercent); - progress.setProgressTintList(ColorStateList.valueOf(mColor)); + progress.setProgressTintList(mColorTintList); } else { progress.setVisibility(View.GONE); } diff --git a/src/com/android/settings/deviceinfo/StorageWizardBase.java b/src/com/android/settings/deviceinfo/StorageWizardBase.java index 40fc249f69..92afa56cc0 100644 --- a/src/com/android/settings/deviceinfo/StorageWizardBase.java +++ b/src/com/android/settings/deviceinfo/StorageWizardBase.java @@ -23,8 +23,8 @@ import static com.android.settings.deviceinfo.StorageSettings.TAG; import android.annotation.LayoutRes; import android.annotation.NonNull; -import android.app.Activity; import android.content.Intent; +import android.content.res.Resources.Theme; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.SystemClock; @@ -36,20 +36,24 @@ import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.View; -import android.widget.Button; import android.widget.FrameLayout; import android.widget.ProgressBar; import android.widget.TextView; +import androidx.fragment.app.FragmentActivity; + import com.android.settings.R; import com.android.settingslib.Utils; -import com.android.setupwizardlib.GlifLayout; + +import com.google.android.setupcompat.template.FooterBarMixin; +import com.google.android.setupcompat.template.FooterButton; +import com.google.android.setupdesign.GlifLayout; import java.text.NumberFormat; import java.util.List; import java.util.Objects; -public abstract class StorageWizardBase extends Activity { +public abstract class StorageWizardBase extends FragmentActivity { protected static final String EXTRA_FORMAT_FORGET_UUID = "format_forget_uuid"; protected static final String EXTRA_FORMAT_PRIVATE = "format_private"; protected static final String EXTRA_FORMAT_SLOW = "format_slow"; @@ -60,8 +64,9 @@ public abstract class StorageWizardBase extends Activity { protected VolumeInfo mVolume; protected DiskInfo mDisk; - private Button mBack; - private Button mNext; + private FooterBarMixin mFooterBarMixin; + private FooterButton mBack; + private FooterButton mNext; @Override protected void onCreate(Bundle savedInstanceState) { @@ -90,8 +95,25 @@ public abstract class StorageWizardBase extends Activity { public void setContentView(@LayoutRes int layoutResID) { super.setContentView(layoutResID); - mBack = requireViewById(R.id.storage_back_button); - mNext = requireViewById(R.id.storage_next_button); + mFooterBarMixin = getGlifLayout().getMixin(FooterBarMixin.class); + mFooterBarMixin.setSecondaryButton( + new FooterButton.Builder(this) + .setText(R.string.wizard_back) + .setListener(this::onNavigateBack) + .setButtonType(FooterButton.ButtonType.OTHER) + .setTheme(R.style.SudGlifButton_Secondary) + .build() + ); + mFooterBarMixin.setPrimaryButton( + new FooterButton.Builder(this) + .setText(R.string.wizard_next) + .setListener(this::onNavigateNext) + .setButtonType(FooterButton.ButtonType.NEXT) + .setTheme(R.style.SudGlifButton_Primary) + .build() + ); + mBack = mFooterBarMixin.getSecondaryButton(); + mNext = mFooterBarMixin.getPrimaryButton(); setIcon(com.android.internal.R.drawable.ic_sd_card_48dp); } @@ -102,11 +124,17 @@ public abstract class StorageWizardBase extends Activity { super.onDestroy(); } - protected Button getBackButton() { + @Override + protected void onApplyThemeResource(Theme theme, int resid, boolean first) { + theme.applyStyle(R.style.SetupWizardPartnerResource, true); + super.onApplyThemeResource(theme, resid, first); + } + + protected FooterButton getBackButton() { return mBack; } - protected Button getNextButton() { + protected FooterButton getNextButton() { return mNext; } @@ -159,10 +187,18 @@ public abstract class StorageWizardBase extends Activity { mNext.setVisibility(View.VISIBLE); } + protected void setBackButtonVisibility(int visible) { + mBack.setVisibility(visible); + } + + protected void setNextButtonVisibility(int visible) { + mNext.setVisibility(visible); + } + protected void setIcon(int resId) { final GlifLayout layout = getGlifLayout(); final Drawable icon = getDrawable(resId).mutate(); - icon.setTint(Utils.getColorAccent(layout.getContext())); + icon.setTintList(Utils.getColorAccent(layout.getContext())); layout.setIcon(icon); } diff --git a/src/com/android/settings/deviceinfo/StorageWizardFormatConfirm.java b/src/com/android/settings/deviceinfo/StorageWizardFormatConfirm.java index 7173f08e1f..9c18a0da60 100644 --- a/src/com/android/settings/deviceinfo/StorageWizardFormatConfirm.java +++ b/src/com/android/settings/deviceinfo/StorageWizardFormatConfirm.java @@ -21,9 +21,8 @@ import static android.os.storage.DiskInfo.EXTRA_DISK_ID; import static com.android.settings.deviceinfo.StorageWizardBase.EXTRA_FORMAT_FORGET_UUID; import static com.android.settings.deviceinfo.StorageWizardBase.EXTRA_FORMAT_PRIVATE; -import android.app.Activity; -import android.app.AlertDialog; import android.app.Dialog; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; import android.os.Bundle; @@ -31,26 +30,28 @@ import android.os.storage.DiskInfo; import android.os.storage.StorageManager; import android.text.TextUtils; -import com.android.internal.logging.nano.MetricsProto; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.FragmentActivity; + import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; public class StorageWizardFormatConfirm extends InstrumentedDialogFragment { private static final String TAG_FORMAT_WARNING = "format_warning"; - public static void showPublic(Activity activity, String diskId) { + public static void showPublic(FragmentActivity activity, String diskId) { show(activity, diskId, null, false); } - public static void showPublic(Activity activity, String diskId, String forgetUuid) { + public static void showPublic(FragmentActivity activity, String diskId, String forgetUuid) { show(activity, diskId, forgetUuid, false); } - public static void showPrivate(Activity activity, String diskId) { + public static void showPrivate(FragmentActivity activity, String diskId) { show(activity, diskId, null, true); } - private static void show(Activity activity, String diskId, String formatForgetUuid, + private static void show(FragmentActivity activity, String diskId, String formatForgetUuid, boolean formatPrivate) { final Bundle args = new Bundle(); args.putString(EXTRA_DISK_ID, diskId); @@ -59,12 +60,12 @@ public class StorageWizardFormatConfirm extends InstrumentedDialogFragment { final StorageWizardFormatConfirm fragment = new StorageWizardFormatConfirm(); fragment.setArguments(args); - fragment.showAllowingStateLoss(activity.getFragmentManager(), TAG_FORMAT_WARNING); + fragment.show(activity.getSupportFragmentManager(), TAG_FORMAT_WARNING); } @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DIALOG_VOLUME_FORMAT; + return SettingsEnums.DIALOG_VOLUME_FORMAT; } @Override diff --git a/src/com/android/settings/deviceinfo/StorageWizardFormatProgress.java b/src/com/android/settings/deviceinfo/StorageWizardFormatProgress.java index 3d654ef5c5..88968b3bd2 100644 --- a/src/com/android/settings/deviceinfo/StorageWizardFormatProgress.java +++ b/src/com/android/settings/deviceinfo/StorageWizardFormatProgress.java @@ -30,6 +30,7 @@ import android.os.SystemProperties; import android.os.storage.StorageManager; import android.os.storage.VolumeInfo; import android.util.Log; +import android.view.View; import android.widget.Toast; import com.android.settings.R; @@ -59,8 +60,9 @@ public class StorageWizardFormatProgress extends StorageWizardBase { setHeaderText(R.string.storage_wizard_format_progress_title, getDiskShortDescription()); setBodyText(R.string.storage_wizard_format_progress_body, getDiskDescription()); - - mTask = (PartitionTask) getLastNonConfigurationInstance(); + setBackButtonVisibility(View.INVISIBLE); + setNextButtonVisibility(View.INVISIBLE); + mTask = (PartitionTask) getLastCustomNonConfigurationInstance(); if (mTask == null) { mTask = new PartitionTask(); mTask.setActivity(this); @@ -71,7 +73,7 @@ public class StorageWizardFormatProgress extends StorageWizardBase { } @Override - public Object onRetainNonConfigurationInstance() { + public Object onRetainCustomNonConfigurationInstance() { return mTask; } diff --git a/src/com/android/settings/deviceinfo/StorageWizardFormatSlow.java b/src/com/android/settings/deviceinfo/StorageWizardFormatSlow.java index f1ac1c2499..79f97de851 100644 --- a/src/com/android/settings/deviceinfo/StorageWizardFormatSlow.java +++ b/src/com/android/settings/deviceinfo/StorageWizardFormatSlow.java @@ -16,6 +16,7 @@ package com.android.settings.deviceinfo; +import android.app.settings.SettingsEnums; import android.content.Intent; import android.os.Bundle; import android.os.storage.DiskInfo; @@ -23,7 +24,6 @@ import android.os.storage.VolumeInfo; import android.text.TextUtils; import android.view.View; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.overlay.FeatureFactory; @@ -58,7 +58,7 @@ public class StorageWizardFormatSlow extends StorageWizardBase { @Override public void onNavigateBack(View view) { FeatureFactory.getFactory(this).getMetricsFeatureProvider().action(this, - MetricsEvent.ACTION_STORAGE_BENCHMARK_SLOW_ABORT); + SettingsEnums.ACTION_STORAGE_BENCHMARK_SLOW_ABORT); final Intent intent = new Intent(this, StorageWizardInit.class); startActivity(intent); @@ -70,11 +70,11 @@ public class StorageWizardFormatSlow extends StorageWizardBase { if (view != null) { // User made an explicit choice to continue when slow FeatureFactory.getFactory(this).getMetricsFeatureProvider().action(this, - MetricsEvent.ACTION_STORAGE_BENCHMARK_SLOW_CONTINUE); + SettingsEnums.ACTION_STORAGE_BENCHMARK_SLOW_CONTINUE); } else { // User made an implicit choice to continue when fast FeatureFactory.getFactory(this).getMetricsFeatureProvider().action(this, - MetricsEvent.ACTION_STORAGE_BENCHMARK_FAST_CONTINUE); + SettingsEnums.ACTION_STORAGE_BENCHMARK_FAST_CONTINUE); } final String forgetUuid = getIntent().getStringExtra(EXTRA_FORMAT_FORGET_UUID); diff --git a/src/com/android/settings/deviceinfo/StorageWizardInit.java b/src/com/android/settings/deviceinfo/StorageWizardInit.java index 076e606cb3..426395c24f 100644 --- a/src/com/android/settings/deviceinfo/StorageWizardInit.java +++ b/src/com/android/settings/deviceinfo/StorageWizardInit.java @@ -17,6 +17,7 @@ package com.android.settings.deviceinfo; import android.app.ActivityManager; +import android.app.settings.SettingsEnums; import android.content.Intent; import android.os.Bundle; import android.os.UserManager; @@ -25,12 +26,10 @@ import android.os.storage.VolumeInfo; import android.view.View; import android.widget.Button; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.overlay.FeatureFactory; public class StorageWizardInit extends StorageWizardBase { - private Button mExternal; private Button mInternal; private boolean mIsPermittedToAdopt; @@ -49,13 +48,13 @@ public class StorageWizardInit extends StorageWizardBase { setHeaderText(R.string.storage_wizard_init_v2_title, getDiskShortDescription()); - mExternal = requireViewById(R.id.storage_wizard_init_external); mInternal = requireViewById(R.id.storage_wizard_init_internal); setBackButtonText(R.string.storage_wizard_init_v2_later); - + setNextButtonVisibility(View.INVISIBLE); if (!mDisk.isAdoptable()) { // If not adoptable, we only have one choice + mInternal.setEnabled(false); onNavigateExternal(null); } else if (!mIsPermittedToAdopt) { // TODO: Show a message about why this is disabled for guest and @@ -73,7 +72,7 @@ public class StorageWizardInit extends StorageWizardBase { if (view != null) { // User made an explicit choice for external FeatureFactory.getFactory(this).getMetricsFeatureProvider().action(this, - MetricsEvent.ACTION_STORAGE_INIT_EXTERNAL); + SettingsEnums.ACTION_STORAGE_INIT_EXTERNAL); } if (mVolume != null && mVolume.getType() == VolumeInfo.TYPE_PUBLIC @@ -96,7 +95,7 @@ public class StorageWizardInit extends StorageWizardBase { if (view != null) { // User made an explicit choice for internal FeatureFactory.getFactory(this).getMetricsFeatureProvider().action(this, - MetricsEvent.ACTION_STORAGE_INIT_INTERNAL); + SettingsEnums.ACTION_STORAGE_INIT_INTERNAL); } StorageWizardFormatConfirm.showPrivate(this, mDisk.getId()); diff --git a/src/com/android/settings/deviceinfo/StorageWizardMigrateConfirm.java b/src/com/android/settings/deviceinfo/StorageWizardMigrateConfirm.java index 48ec0e313d..1cae22eca9 100644 --- a/src/com/android/settings/deviceinfo/StorageWizardMigrateConfirm.java +++ b/src/com/android/settings/deviceinfo/StorageWizardMigrateConfirm.java @@ -18,6 +18,7 @@ package com.android.settings.deviceinfo; import static com.android.settings.deviceinfo.StorageSettings.TAG; +import android.app.settings.SettingsEnums; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.UserInfo; @@ -31,7 +32,6 @@ import android.util.Log; import android.view.View; import android.widget.Toast; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.overlay.FeatureFactory; import com.android.settings.password.ChooseLockSettingsHelper; @@ -83,7 +83,7 @@ public class StorageWizardMigrateConfirm extends StorageWizardBase { @Override public void onNavigateBack(View view) { FeatureFactory.getFactory(this).getMetricsFeatureProvider().action(this, - MetricsEvent.ACTION_STORAGE_MIGRATE_LATER); + SettingsEnums.ACTION_STORAGE_MIGRATE_LATER); if (mDisk != null) { final Intent intent = new Intent(this, StorageWizardReady.class); @@ -137,7 +137,7 @@ public class StorageWizardMigrateConfirm extends StorageWizardBase { } FeatureFactory.getFactory(this).getMetricsFeatureProvider().action(this, - MetricsEvent.ACTION_STORAGE_MIGRATE_NOW); + SettingsEnums.ACTION_STORAGE_MIGRATE_NOW); final Intent intent = new Intent(this, StorageWizardMigrateProgress.class); intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, mVolume.getId()); diff --git a/src/com/android/settings/deviceinfo/StorageWizardMigrateProgress.java b/src/com/android/settings/deviceinfo/StorageWizardMigrateProgress.java index 60f3cb5d4c..b6f2a8d2fa 100644 --- a/src/com/android/settings/deviceinfo/StorageWizardMigrateProgress.java +++ b/src/com/android/settings/deviceinfo/StorageWizardMigrateProgress.java @@ -28,6 +28,7 @@ import android.os.Bundle; import android.os.Handler; import android.os.storage.DiskInfo; import android.util.Log; +import android.view.View; import android.widget.Toast; import com.android.settings.R; @@ -51,7 +52,8 @@ public class StorageWizardMigrateProgress extends StorageWizardBase { setIcon(R.drawable.ic_swap_horiz); setHeaderText(R.string.storage_wizard_migrate_progress_v2_title); setAuxChecklist(); - + setBackButtonVisibility(View.INVISIBLE); + setNextButtonVisibility(View.INVISIBLE); // Register for updates and push through current status getPackageManager().registerMoveCallback(mCallback, new Handler()); mCallback.onStatusChanged(mMoveId, getPackageManager().getMoveStatus(mMoveId), -1); diff --git a/src/com/android/settings/deviceinfo/StorageWizardMoveConfirm.java b/src/com/android/settings/deviceinfo/StorageWizardMoveConfirm.java index 10b78af0d7..8dc878e24a 100644 --- a/src/com/android/settings/deviceinfo/StorageWizardMoveConfirm.java +++ b/src/com/android/settings/deviceinfo/StorageWizardMoveConfirm.java @@ -73,6 +73,7 @@ public class StorageWizardMoveConfirm extends StorageWizardBase { setBodyText(R.string.storage_wizard_move_confirm_body, appName, volumeName); setNextButtonText(R.string.move_app); + setBackButtonVisibility(View.INVISIBLE); } @Override diff --git a/src/com/android/settings/deviceinfo/StorageWizardMoveProgress.java b/src/com/android/settings/deviceinfo/StorageWizardMoveProgress.java index 1f393c5128..1966c9534a 100644 --- a/src/com/android/settings/deviceinfo/StorageWizardMoveProgress.java +++ b/src/com/android/settings/deviceinfo/StorageWizardMoveProgress.java @@ -16,6 +16,11 @@ package com.android.settings.deviceinfo; +import static android.content.Intent.EXTRA_TITLE; +import static android.content.pm.PackageManager.EXTRA_MOVE_ID; + +import static com.android.settings.deviceinfo.StorageSettings.TAG; + import android.content.pm.PackageManager; import android.content.pm.PackageManager.MoveCallback; import android.os.Bundle; @@ -26,10 +31,6 @@ import android.widget.Toast; import com.android.settings.R; -import static android.content.Intent.EXTRA_TITLE; -import static android.content.pm.PackageManager.EXTRA_MOVE_ID; -import static com.android.settings.deviceinfo.StorageSettings.TAG; - public class StorageWizardMoveProgress extends StorageWizardBase { private int mMoveId; @@ -49,7 +50,8 @@ public class StorageWizardMoveProgress extends StorageWizardBase { setIcon(R.drawable.ic_swap_horiz); setHeaderText(R.string.storage_wizard_move_progress_title, appName); setBodyText(R.string.storage_wizard_move_progress_body, volumeName, appName); - + setBackButtonVisibility(View.INVISIBLE); + setNextButtonVisibility(View.INVISIBLE); // Register for updates and push through current status getPackageManager().registerMoveCallback(mCallback, new Handler()); mCallback.onStatusChanged(mMoveId, getPackageManager().getMoveStatus(mMoveId), -1); @@ -88,8 +90,6 @@ public class StorageWizardMoveProgress extends StorageWizardBase { return getString(R.string.move_error_device_admin); case PackageManager.MOVE_FAILED_DOESNT_EXIST: return getString(R.string.does_not_exist); - case PackageManager.MOVE_FAILED_FORWARD_LOCKED: - return getString(R.string.app_forward_locked); case PackageManager.MOVE_FAILED_INVALID_LOCATION: return getString(R.string.invalid_location); case PackageManager.MOVE_FAILED_SYSTEM_PACKAGE: diff --git a/src/com/android/settings/deviceinfo/StorageWizardReady.java b/src/com/android/settings/deviceinfo/StorageWizardReady.java index fdb8d8a493..813bcc6e20 100644 --- a/src/com/android/settings/deviceinfo/StorageWizardReady.java +++ b/src/com/android/settings/deviceinfo/StorageWizardReady.java @@ -50,6 +50,7 @@ public class StorageWizardReady extends StorageWizardBase { } setNextButtonText(R.string.done); + setBackButtonVisibility(View.INVISIBLE); } @Override diff --git a/src/com/android/settings/deviceinfo/TopLevelStoragePreferenceController.java b/src/com/android/settings/deviceinfo/TopLevelStoragePreferenceController.java new file mode 100644 index 0000000000..fdc5feb573 --- /dev/null +++ b/src/com/android/settings/deviceinfo/TopLevelStoragePreferenceController.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2018 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.os.storage.StorageManager; +import android.text.format.Formatter; + +import androidx.preference.Preference; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; +import com.android.settingslib.deviceinfo.PrivateStorageInfo; +import com.android.settingslib.deviceinfo.StorageManagerVolumeProvider; +import com.android.settingslib.utils.ThreadUtils; + +import java.text.NumberFormat; + +public class TopLevelStoragePreferenceController extends BasePreferenceController { + + private final StorageManager mStorageManager; + private final StorageManagerVolumeProvider mStorageManagerVolumeProvider; + + public TopLevelStoragePreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + mStorageManager = mContext.getSystemService(StorageManager.class); + mStorageManagerVolumeProvider = new StorageManagerVolumeProvider(mStorageManager); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE_UNSEARCHABLE; + } + + @Override + protected void refreshSummary(Preference preference) { + if (preference == null) { + return; + } + + ThreadUtils.postOnBackgroundThread(() -> { + final NumberFormat percentageFormat = NumberFormat.getPercentInstance(); + final PrivateStorageInfo info = PrivateStorageInfo.getPrivateStorageInfo( + mStorageManagerVolumeProvider); + final double privateUsedBytes = info.totalBytes - info.freeBytes; + + ThreadUtils.postOnMainThread(() -> { + preference.setSummary(mContext.getString(R.string.storage_summary, + percentageFormat.format(privateUsedBytes / info.totalBytes), + Formatter.formatFileSize(mContext, info.freeBytes))); + }); + }); + } +} diff --git a/src/com/android/settings/deviceinfo/UptimePreferenceController.java b/src/com/android/settings/deviceinfo/UptimePreferenceController.java new file mode 100644 index 0000000000..4f02594530 --- /dev/null +++ b/src/com/android/settings/deviceinfo/UptimePreferenceController.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2018 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 com.android.settings.core.PreferenceControllerMixin; +import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.deviceinfo.AbstractUptimePreferenceController; + +/** + * Concrete subclass of uptime preference controller + */ +public class UptimePreferenceController extends AbstractUptimePreferenceController + implements PreferenceControllerMixin { + public UptimePreferenceController(Context context, Lifecycle lifecycle) { + super(context, lifecycle); + } + + // This space intentionally left blank +}
\ No newline at end of file diff --git a/src/com/android/settings/deviceinfo/WifiMacAddressPreferenceController.java b/src/com/android/settings/deviceinfo/WifiMacAddressPreferenceController.java index 8375310c10..c7005966c4 100644 --- a/src/com/android/settings/deviceinfo/WifiMacAddressPreferenceController.java +++ b/src/com/android/settings/deviceinfo/WifiMacAddressPreferenceController.java @@ -18,8 +18,8 @@ package com.android.settings.deviceinfo; import android.content.Context; -import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.R; +import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.deviceinfo.AbstractWifiMacAddressPreferenceController; diff --git a/src/com/android/settings/deviceinfo/aboutphone/DeviceNameWarningDialog.java b/src/com/android/settings/deviceinfo/aboutphone/DeviceNameWarningDialog.java index 9808069f3e..b38f13ff36 100644 --- a/src/com/android/settings/deviceinfo/aboutphone/DeviceNameWarningDialog.java +++ b/src/com/android/settings/deviceinfo/aboutphone/DeviceNameWarningDialog.java @@ -16,13 +16,14 @@ package com.android.settings.deviceinfo.aboutphone; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.FragmentManager; +import android.app.settings.SettingsEnums; import android.content.DialogInterface; import android.os.Bundle; -import com.android.internal.logging.nano.MetricsProto; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.FragmentManager; + import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; @@ -35,7 +36,7 @@ public class DeviceNameWarningDialog extends InstrumentedDialogFragment public static final String TAG = "DeviceNameWarningDlg"; public static void show(MyDeviceInfoFragment host) { - final FragmentManager manager = host.getActivity().getFragmentManager(); + final FragmentManager manager = host.getActivity().getSupportFragmentManager(); if (manager.findFragmentByTag(TAG) != null) { return; } @@ -47,7 +48,7 @@ public class DeviceNameWarningDialog extends InstrumentedDialogFragment @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DIALOG_ENABLE_DEVELOPMENT_OPTIONS; + return SettingsEnums.DIALOG_ENABLE_DEVELOPMENT_OPTIONS; } @Override @@ -65,7 +66,9 @@ public class DeviceNameWarningDialog extends InstrumentedDialogFragment public void onClick(DialogInterface dialog, int which) { final MyDeviceInfoFragment host = (MyDeviceInfoFragment) getTargetFragment(); if (which == DialogInterface.BUTTON_POSITIVE) { - host.onSetDeviceNameConfirm(); + host.onSetDeviceNameConfirm(true); + } else { + host.onSetDeviceNameConfirm(false); } } } diff --git a/src/com/android/settings/deviceinfo/aboutphone/MyDeviceInfoFragment.java b/src/com/android/settings/deviceinfo/aboutphone/MyDeviceInfoFragment.java index 09262a0647..e6afb7b821 100644 --- a/src/com/android/settings/deviceinfo/aboutphone/MyDeviceInfoFragment.java +++ b/src/com/android/settings/deviceinfo/aboutphone/MyDeviceInfoFragment.java @@ -16,9 +16,8 @@ package com.android.settings.deviceinfo.aboutphone; -import static com.android.settings.bluetooth.Utils.getLocalBtManager; - import android.app.Activity; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; import android.content.pm.UserInfo; @@ -27,48 +26,45 @@ import android.os.UserManager; import android.provider.SearchIndexableResource; import android.view.View; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.Utils; -import com.android.settings.accounts.EmergencyInfoPreferenceController; -import com.android.settings.applications.LayoutPreference; import com.android.settings.dashboard.DashboardFragment; -import com.android.settings.dashboard.SummaryLoader; import com.android.settings.deviceinfo.BluetoothAddressPreferenceController; -import com.android.settings.deviceinfo.BrandedAccountPreferenceController; import com.android.settings.deviceinfo.BuildNumberPreferenceController; -import com.android.settings.deviceinfo.DeviceModelPreferenceController; import com.android.settings.deviceinfo.DeviceNamePreferenceController; import com.android.settings.deviceinfo.FccEquipmentIdPreferenceController; import com.android.settings.deviceinfo.FeedbackPreferenceController; import com.android.settings.deviceinfo.IpAddressPreferenceController; import com.android.settings.deviceinfo.ManualPreferenceController; -import com.android.settings.deviceinfo.PhoneNumberPreferenceController; import com.android.settings.deviceinfo.RegulatoryInfoPreferenceController; import com.android.settings.deviceinfo.SafetyInfoPreferenceController; +import com.android.settings.deviceinfo.UptimePreferenceController; import com.android.settings.deviceinfo.WifiMacAddressPreferenceController; -import com.android.settings.deviceinfo.firmwareversion.FirmwareVersionPreferenceController; import com.android.settings.deviceinfo.imei.ImeiInfoPreferenceController; import com.android.settings.deviceinfo.simstatus.SimStatusPreferenceController; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.widget.EntityHeaderController; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.search.SearchIndexable; +import com.android.settingslib.widget.LayoutPreference; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +@SearchIndexable public class MyDeviceInfoFragment extends DashboardFragment implements DeviceNamePreferenceController.DeviceNamePreferenceHost { - private static final String LOG_TAG = "MyDeviceInfoFragment"; + private static final String LOG_TAG = "MyDeviceInfoFragment"; private static final String KEY_MY_DEVICE_INFO_HEADER = "my_device_info_header"; - private static final String KEY_LEGAL_CONTAINER = "legal_container"; + + private BuildNumberPreferenceController mBuildNumberPreferenceController; @Override public int getMetricsCategory() { - return MetricsEvent.DEVICEINFO; + return SettingsEnums.DEVICEINFO; } @Override @@ -77,8 +73,17 @@ public class MyDeviceInfoFragment extends DashboardFragment } @Override - public void onResume() { - super.onResume(); + public void onAttach(Context context) { + super.onAttach(context); + use(ImeiInfoPreferenceController.class).setHost(this /* parent */); + use(DeviceNamePreferenceController.class).setHost(this /* parent */); + mBuildNumberPreferenceController = use(BuildNumberPreferenceController.class); + mBuildNumberPreferenceController.setHost(this /* parent */); + } + + @Override + public void onStart() { + super.onStart(); initHeader(); } @@ -94,31 +99,13 @@ public class MyDeviceInfoFragment extends DashboardFragment @Override protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { - return buildPreferenceControllers(context, getActivity(), this /* fragment */, - getLifecycle()); + return buildPreferenceControllers(context, this /* fragment */, getSettingsLifecycle()); } private static List<AbstractPreferenceController> buildPreferenceControllers( - Context context, - Activity activity, - MyDeviceInfoFragment fragment, - Lifecycle lifecycle) { + Context context, MyDeviceInfoFragment fragment, Lifecycle lifecycle) { final List<AbstractPreferenceController> controllers = new ArrayList<>(); - controllers.add(new EmergencyInfoPreferenceController(context)); - controllers.add(new PhoneNumberPreferenceController(context)); - controllers.add(new BrandedAccountPreferenceController(context)); - DeviceNamePreferenceController deviceNamePreferenceController = - new DeviceNamePreferenceController(context); - deviceNamePreferenceController.setLocalBluetoothManager(getLocalBtManager(context)); - deviceNamePreferenceController.setHost(fragment); - if (lifecycle != null) { - lifecycle.addObserver(deviceNamePreferenceController); - } - controllers.add(deviceNamePreferenceController); controllers.add(new SimStatusPreferenceController(context, fragment)); - controllers.add(new DeviceModelPreferenceController(context, fragment)); - controllers.add(new ImeiInfoPreferenceController(context, fragment)); - controllers.add(new FirmwareVersionPreferenceController(context, fragment)); controllers.add(new IpAddressPreferenceController(context, lifecycle)); controllers.add(new WifiMacAddressPreferenceController(context, lifecycle)); controllers.add(new BluetoothAddressPreferenceController(context, lifecycle)); @@ -127,16 +114,13 @@ public class MyDeviceInfoFragment extends DashboardFragment controllers.add(new ManualPreferenceController(context)); controllers.add(new FeedbackPreferenceController(fragment, context)); controllers.add(new FccEquipmentIdPreferenceController(context)); - controllers.add( - new BuildNumberPreferenceController(context, activity, fragment, lifecycle)); + controllers.add(new UptimePreferenceController(context, lifecycle)); return controllers; } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { - final BuildNumberPreferenceController buildNumberPreferenceController = - use(BuildNumberPreferenceController.class); - if (buildNumberPreferenceController.onActivityResult(requestCode, resultCode, data)) { + if (mBuildNumberPreferenceController.onActivityResult(requestCode, resultCode, data)) { return; } super.onActivityResult(requestCode, resultCode, data); @@ -145,22 +129,29 @@ public class MyDeviceInfoFragment extends DashboardFragment private void initHeader() { // TODO: Migrate into its own controller. final LayoutPreference headerPreference = - (LayoutPreference) getPreferenceScreen().findPreference(KEY_MY_DEVICE_INFO_HEADER); - final View appSnippet = headerPreference.findViewById(R.id.entity_header); + getPreferenceScreen().findPreference(KEY_MY_DEVICE_INFO_HEADER); + final boolean shouldDisplayHeader = getContext().getResources().getBoolean( + R.bool.config_show_device_header_in_device_info); + headerPreference.setVisible(shouldDisplayHeader); + if (!shouldDisplayHeader) { + return; + } + final View headerView = headerPreference.findViewById(R.id.entity_header); final Activity context = getActivity(); final Bundle bundle = getArguments(); - EntityHeaderController controller = EntityHeaderController - .newInstance(context, this, appSnippet) - .setRecyclerView(getListView(), getLifecycle()) + final EntityHeaderController controller = EntityHeaderController + .newInstance(context, this, headerView) + .setRecyclerView(getListView(), getSettingsLifecycle()) .setButtonActions(EntityHeaderController.ActionType.ACTION_NONE, EntityHeaderController.ActionType.ACTION_NONE); // TODO: There may be an avatar setting action we can use here. final int iconId = bundle.getInt("icon_id", 0); if (iconId == 0) { - UserManager userManager = (UserManager) getActivity().getSystemService( + final UserManager userManager = (UserManager) getActivity().getSystemService( Context.USER_SERVICE); - UserInfo info = Utils.getExistingUser(userManager, android.os.Process.myUserHandle()); + final UserInfo info = Utils.getExistingUser(userManager, + android.os.Process.myUserHandle()); controller.setLabel(info.name); controller.setIcon( com.android.settingslib.Utils.getUserIcon(getActivity(), userManager, info)); @@ -174,30 +165,11 @@ public class MyDeviceInfoFragment extends DashboardFragment DeviceNameWarningDialog.show(this); } - public void onSetDeviceNameConfirm() { + public void onSetDeviceNameConfirm(boolean confirm) { final DeviceNamePreferenceController controller = use(DeviceNamePreferenceController.class); - controller.confirmDeviceName(); + controller.updateDeviceName(confirm); } - private static class SummaryProvider implements SummaryLoader.SummaryProvider { - - private final SummaryLoader mSummaryLoader; - - public SummaryProvider(SummaryLoader summaryLoader) { - mSummaryLoader = summaryLoader; - } - - @Override - public void setListening(boolean listening) { - if (listening) { - mSummaryLoader.setSummary(this, DeviceModelPreferenceController.getDeviceModel()); - } - } - } - - public static final SummaryLoader.SummaryProviderFactory SUMMARY_PROVIDER_FACTORY - = (activity, summaryLoader) -> new SummaryProvider(summaryLoader); - /** * For Search. */ @@ -215,16 +187,8 @@ public class MyDeviceInfoFragment extends DashboardFragment @Override public List<AbstractPreferenceController> createPreferenceControllers( Context context) { - return buildPreferenceControllers(context, null /*activity */, - null /* fragment */, null /* lifecycle */); - } - - @Override - public List<String> getNonIndexableKeys(Context context) { - List<String> keys = super.getNonIndexableKeys(context); - // The legal container is duplicated, so we ignore it here. - keys.add(KEY_LEGAL_CONTAINER); - return keys; + return buildPreferenceControllers(context, null /* fragment */, + null /* lifecycle */); } }; } diff --git a/src/com/android/settings/deviceinfo/aboutphone/TopLevelAboutDevicePreferenceController.java b/src/com/android/settings/deviceinfo/aboutphone/TopLevelAboutDevicePreferenceController.java new file mode 100644 index 0000000000..dbee4430e7 --- /dev/null +++ b/src/com/android/settings/deviceinfo/aboutphone/TopLevelAboutDevicePreferenceController.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2018 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.aboutphone; + +import android.content.Context; + +import com.android.settings.core.BasePreferenceController; +import com.android.settings.deviceinfo.DeviceNamePreferenceController; + +public class TopLevelAboutDevicePreferenceController extends BasePreferenceController { + + public TopLevelAboutDevicePreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } + + @Override + public CharSequence getSummary() { + final DeviceNamePreferenceController deviceNamePreferenceController = + new DeviceNamePreferenceController(mContext, "dummy_key"); + return deviceNamePreferenceController.getSummary(); + } +} diff --git a/src/com/android/settings/deviceinfo/firmwareversion/BasebandVersionDialogController.java b/src/com/android/settings/deviceinfo/firmwareversion/BasebandVersionDialogController.java deleted file mode 100644 index 21663f6820..0000000000 --- a/src/com/android/settings/deviceinfo/firmwareversion/BasebandVersionDialogController.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2017 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.firmwareversion; - -import android.content.Context; -import android.os.SystemProperties; -import androidx.annotation.VisibleForTesting; - -import com.android.settings.R; -import com.android.settings.Utils; - -public class BasebandVersionDialogController { - - @VisibleForTesting - static final int BASEBAND_VERSION_LABEL_ID = R.id.baseband_version_label; - @VisibleForTesting - static final int BASEBAND_VERSION_VALUE_ID = R.id.baseband_version_value; - @VisibleForTesting - static final String BASEBAND_PROPERTY = "gsm.version.baseband"; - - private final FirmwareVersionDialogFragment mDialog; - - public BasebandVersionDialogController(FirmwareVersionDialogFragment dialog) { - mDialog = dialog; - } - - /** - * Updates the baseband version field of the dialog. - */ - public void initialize() { - final Context context = mDialog.getContext(); - if (Utils.isWifiOnly(context)) { - mDialog.removeSettingFromScreen(BASEBAND_VERSION_LABEL_ID); - mDialog.removeSettingFromScreen(BASEBAND_VERSION_VALUE_ID); - return; - } - - mDialog.setText(BASEBAND_VERSION_VALUE_ID, SystemProperties.get(BASEBAND_PROPERTY, - context.getString(R.string.device_info_default))); - } -} diff --git a/src/com/android/settings/deviceinfo/firmwareversion/BasebandVersionPreferenceController.java b/src/com/android/settings/deviceinfo/firmwareversion/BasebandVersionPreferenceController.java new file mode 100644 index 0000000000..dd3d560282 --- /dev/null +++ b/src/com/android/settings/deviceinfo/firmwareversion/BasebandVersionPreferenceController.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2019 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.firmwareversion; + +import android.content.Context; +import android.os.SystemProperties; + +import androidx.annotation.VisibleForTesting; + +import com.android.settings.R; +import com.android.settings.Utils; +import com.android.settings.core.BasePreferenceController; + +public class BasebandVersionPreferenceController extends BasePreferenceController { + + @VisibleForTesting + static final String BASEBAND_PROPERTY = "gsm.version.baseband"; + + public BasebandVersionPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public int getAvailabilityStatus() { + return !Utils.isWifiOnly(mContext) ? AVAILABLE : UNSUPPORTED_ON_DEVICE; + } + + @Override + public CharSequence getSummary() { + return SystemProperties.get(BASEBAND_PROPERTY, + mContext.getString(R.string.device_info_default)); + } +} diff --git a/src/com/android/settings/deviceinfo/firmwareversion/BuildNumberDialogController.java b/src/com/android/settings/deviceinfo/firmwareversion/BuildNumberDialogController.java deleted file mode 100644 index 6dc651abad..0000000000 --- a/src/com/android/settings/deviceinfo/firmwareversion/BuildNumberDialogController.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (C) 2017 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.firmwareversion; - -import android.os.Build; -import androidx.annotation.VisibleForTesting; -import android.text.BidiFormatter; - -import com.android.settings.R; - -public class BuildNumberDialogController { - - @VisibleForTesting - static final int BUILD_NUMBER_VALUE_ID = R.id.build_number_value; - - private final FirmwareVersionDialogFragment mDialog; - - public BuildNumberDialogController(FirmwareVersionDialogFragment dialog) { - mDialog = dialog; - } - - /** - * Updates the build number to the dialog. - */ - public void initialize() { - mDialog.setText(BUILD_NUMBER_VALUE_ID, - BidiFormatter.getInstance().unicodeWrap(Build.DISPLAY)); - } -} diff --git a/src/com/android/settings/deviceinfo/firmwareversion/FirmwareVersionDialogController.java b/src/com/android/settings/deviceinfo/firmwareversion/FirmwareVersionDetailPreferenceController.java index 277024576a..daa10d2696 100644 --- a/src/com/android/settings/deviceinfo/firmwareversion/FirmwareVersionDialogController.java +++ b/src/com/android/settings/deviceinfo/firmwareversion/FirmwareVersionDetailPreferenceController.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 The Android Open Source Project + * Copyright (C) 2019 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. @@ -22,40 +22,65 @@ import android.os.Build; import android.os.SystemClock; import android.os.UserHandle; import android.os.UserManager; -import androidx.annotation.VisibleForTesting; +import android.text.TextUtils; import android.util.Log; -import android.view.View; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; import com.android.settings.R; +import com.android.settings.Utils; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.slices.Sliceable; import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; -public class FirmwareVersionDialogController implements View.OnClickListener { +public class FirmwareVersionDetailPreferenceController extends BasePreferenceController { private static final String TAG = "firmwareDialogCtrl"; private static final int DELAY_TIMER_MILLIS = 500; private static final int ACTIVITY_TRIGGER_COUNT = 3; - @VisibleForTesting - static final int FIRMWARE_VERSION_VALUE_ID = R.id.firmware_version_value; - @VisibleForTesting - static final int FIRMWARE_VERSION_LABEL_ID = R.id.firmware_version_label; - - private final FirmwareVersionDialogFragment mDialog; - private final Context mContext; private final UserManager mUserManager; private final long[] mHits = new long[ACTIVITY_TRIGGER_COUNT]; private RestrictedLockUtils.EnforcedAdmin mFunDisallowedAdmin; private boolean mFunDisallowedBySystem; - public FirmwareVersionDialogController(FirmwareVersionDialogFragment dialog) { - mDialog = dialog; - mContext = dialog.getContext(); + public FirmwareVersionDetailPreferenceController(Context context, String key) { + super(context, key); mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); + initializeAdminPermissions(); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } + + @Override + public boolean useDynamicSliceSummary() { + return true; + } + + @Override + public boolean isSliceable() { + return true; } @Override - public void onClick(View v) { + public CharSequence getSummary() { + return Build.VERSION.RELEASE; + } + + @Override + public boolean handlePreferenceTreeClick(Preference preference) { + if (!TextUtils.equals(preference.getKey(), getPreferenceKey())) { + return false; + } + if (Utils.isMonkeyRunning()) { + return false; + } arrayCopy(); mHits[mHits.length - 1] = SystemClock.uptimeMillis(); if (mHits[0] >= (SystemClock.uptimeMillis() - DELAY_TIMER_MILLIS)) { @@ -65,7 +90,7 @@ public class FirmwareVersionDialogController implements View.OnClickListener { mFunDisallowedAdmin); } Log.d(TAG, "Sorry, no fun for you!"); - return; + return true; } final Intent intent = new Intent(Intent.ACTION_MAIN) @@ -77,21 +102,7 @@ public class FirmwareVersionDialogController implements View.OnClickListener { Log.e(TAG, "Unable to start activity " + intent.toString()); } } - } - - /** - * Populates the Android version field in the dialog and registers click listeners. - */ - public void initialize() { - initializeAdminPermissions(); - registerClickListeners(); - - mDialog.setText(FIRMWARE_VERSION_VALUE_ID, Build.VERSION.RELEASE); - } - - private void registerClickListeners() { - mDialog.registerClickListener(FIRMWARE_VERSION_LABEL_ID, this /* listener */); - mDialog.registerClickListener(FIRMWARE_VERSION_VALUE_ID, this /* listener */); + return true; } /** @@ -104,9 +115,15 @@ public class FirmwareVersionDialogController implements View.OnClickListener { @VisibleForTesting void initializeAdminPermissions() { - mFunDisallowedAdmin = RestrictedLockUtils.checkIfRestrictionEnforced( + mFunDisallowedAdmin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced( mContext, UserManager.DISALLOW_FUN, UserHandle.myUserId()); - mFunDisallowedBySystem = RestrictedLockUtils.hasBaseUserRestriction( + mFunDisallowedBySystem = RestrictedLockUtilsInternal.hasBaseUserRestriction( mContext, UserManager.DISALLOW_FUN, UserHandle.myUserId()); } + + @Override + public void copy() { + Sliceable.setCopyContent(mContext, getSummary(), + mContext.getText(R.string.firmware_version)); + } } diff --git a/src/com/android/settings/deviceinfo/firmwareversion/FirmwareVersionDialogFragment.java b/src/com/android/settings/deviceinfo/firmwareversion/FirmwareVersionDialogFragment.java deleted file mode 100644 index 0087444605..0000000000 --- a/src/com/android/settings/deviceinfo/firmwareversion/FirmwareVersionDialogFragment.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (C) 2017 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.firmwareversion; - -import android.app.AlertDialog; -import android.app.Dialog; -import android.app.Fragment; -import android.app.FragmentManager; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.TextView; - -import com.android.internal.logging.nano.MetricsProto; -import com.android.settings.R; -import com.android.settings.core.instrumentation.InstrumentedDialogFragment; - -public class FirmwareVersionDialogFragment extends InstrumentedDialogFragment { - - private static final String TAG = "firmwareVersionDialog"; - - private View mRootView; - - public static void show(Fragment host) { - final FragmentManager manager = host.getChildFragmentManager(); - if (manager.findFragmentByTag(TAG) == null) { - final FirmwareVersionDialogFragment dialog = new FirmwareVersionDialogFragment(); - dialog.show(manager, TAG); - } - } - - @Override - public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DIALOG_FIRMWARE_VERSION; - } - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()) - .setTitle(R.string.firmware_title) - .setPositiveButton(android.R.string.ok, null /* listener */); - - mRootView = LayoutInflater.from(getActivity()).inflate( - R.layout.dialog_firmware_version, null /* parent */); - - initializeControllers(); - - return builder.setView(mRootView).create(); - } - - public void setText(int viewId, CharSequence text) { - final TextView view = mRootView.findViewById(viewId); - if (view != null) { - view.setText(text); - } - } - - public void removeSettingFromScreen(int viewId) { - final View view = mRootView.findViewById(viewId); - if (view != null) { - view.setVisibility(View.GONE); - } - } - - public void registerClickListener(int viewId, View.OnClickListener listener) { - final View view = mRootView.findViewById(viewId); - if (view != null) { - view.setOnClickListener(listener); - } - } - - private void initializeControllers() { - new FirmwareVersionDialogController(this).initialize(); - new SecurityPatchLevelDialogController(this).initialize(); - new BasebandVersionDialogController(this).initialize(); - new KernelVersionDialogController(this).initialize(); - new BuildNumberDialogController(this).initialize(); - } -} diff --git a/src/com/android/settings/deviceinfo/firmwareversion/FirmwareVersionPreferenceController.java b/src/com/android/settings/deviceinfo/firmwareversion/FirmwareVersionPreferenceController.java index 785ec595b0..41d9566760 100644 --- a/src/com/android/settings/deviceinfo/firmwareversion/FirmwareVersionPreferenceController.java +++ b/src/com/android/settings/deviceinfo/firmwareversion/FirmwareVersionPreferenceController.java @@ -16,55 +16,24 @@ package com.android.settings.deviceinfo.firmwareversion; -import android.app.Fragment; import android.content.Context; import android.os.Build; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; -import android.text.TextUtils; -import com.android.settings.core.PreferenceControllerMixin; -import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settings.core.BasePreferenceController; -public class FirmwareVersionPreferenceController extends AbstractPreferenceController implements - PreferenceControllerMixin { +public class FirmwareVersionPreferenceController extends BasePreferenceController { - private final static String FIRMWARE_VERSION_KEY = "firmware_version"; - - private final Fragment mFragment; - - public FirmwareVersionPreferenceController(Context context, Fragment fragment) { - super(context); - - mFragment = fragment; - } - - @Override - public boolean isAvailable() { - return true; + public FirmwareVersionPreferenceController(Context context, String key) { + super(context, key); } @Override - public void displayPreference(PreferenceScreen screen) { - super.displayPreference(screen); - final Preference pref = screen.findPreference(getPreferenceKey()); - if (pref != null) { - pref.setSummary(Build.VERSION.RELEASE); - } + public int getAvailabilityStatus() { + return AVAILABLE_UNSEARCHABLE; } @Override - public String getPreferenceKey() { - return FIRMWARE_VERSION_KEY; - } - - @Override - public boolean handlePreferenceTreeClick(Preference preference) { - if (!TextUtils.equals(preference.getKey(), getPreferenceKey())) { - return false; - } - - FirmwareVersionDialogFragment.show(mFragment); - return true; + public CharSequence getSummary() { + return Build.VERSION.RELEASE; } } diff --git a/src/com/android/settings/deviceinfo/firmwareversion/FirmwareVersionSettings.java b/src/com/android/settings/deviceinfo/firmwareversion/FirmwareVersionSettings.java new file mode 100644 index 0000000000..90c3b56876 --- /dev/null +++ b/src/com/android/settings/deviceinfo/firmwareversion/FirmwareVersionSettings.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2019 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.firmwareversion; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.provider.SearchIndexableResource; + +import com.android.settings.R; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; +import com.android.settingslib.search.SearchIndexable; + +import java.util.ArrayList; +import java.util.List; + +@SearchIndexable +public class FirmwareVersionSettings extends DashboardFragment { + + @Override + protected int getPreferenceScreenResId() { + return R.xml.firmware_version; + } + + @Override + protected String getLogTag() { + return "FirmwareVersionSettings"; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.DIALOG_FIRMWARE_VERSION; + } + + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, + boolean enabled) { + final ArrayList<SearchIndexableResource> result = new ArrayList<>(); + + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.firmware_version; + result.add(sir); + return result; + } + + }; +} diff --git a/src/com/android/settings/deviceinfo/firmwareversion/KernelVersionDialogController.java b/src/com/android/settings/deviceinfo/firmwareversion/KernelVersionDialogController.java deleted file mode 100644 index 0d816d563e..0000000000 --- a/src/com/android/settings/deviceinfo/firmwareversion/KernelVersionDialogController.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) 2017 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.firmwareversion; - -import androidx.annotation.VisibleForTesting; - -import com.android.settings.R; -import com.android.settingslib.DeviceInfoUtils; - -public class KernelVersionDialogController { - - @VisibleForTesting - static int KERNEL_VERSION_VALUE_ID = R.id.kernel_version_value; - - private final FirmwareVersionDialogFragment mDialog; - - public KernelVersionDialogController(FirmwareVersionDialogFragment dialog) { - mDialog = dialog; - } - - /** - * Updates kernel version to the dialog. - */ - public void initialize() { - mDialog.setText(KERNEL_VERSION_VALUE_ID, - DeviceInfoUtils.getFormattedKernelVersion(mDialog.getContext())); - } -} diff --git a/src/com/android/settings/deviceinfo/firmwareversion/KernelVersionPreferenceController.java b/src/com/android/settings/deviceinfo/firmwareversion/KernelVersionPreferenceController.java new file mode 100644 index 0000000000..0500c89371 --- /dev/null +++ b/src/com/android/settings/deviceinfo/firmwareversion/KernelVersionPreferenceController.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2019 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.firmwareversion; + +import android.content.Context; + +import com.android.settings.core.BasePreferenceController; +import com.android.settingslib.DeviceInfoUtils; + +public class KernelVersionPreferenceController extends BasePreferenceController { + + public KernelVersionPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } + + @Override + public CharSequence getSummary() { + return DeviceInfoUtils.getFormattedKernelVersion(mContext); + } +} diff --git a/src/com/android/settings/deviceinfo/firmwareversion/MainlineModuleVersionPreferenceController.java b/src/com/android/settings/deviceinfo/firmwareversion/MainlineModuleVersionPreferenceController.java new file mode 100644 index 0000000000..ff9352a7d4 --- /dev/null +++ b/src/com/android/settings/deviceinfo/firmwareversion/MainlineModuleVersionPreferenceController.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2019 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.firmwareversion; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; + +import com.android.settings.core.BasePreferenceController; + +public class MainlineModuleVersionPreferenceController extends BasePreferenceController { + + private static final String TAG = "MainlineModuleControl"; + + @VisibleForTesting + static final Intent MODULE_UPDATE_INTENT = + new Intent("android.settings.MODULE_UPDATE_SETTINGS"); + private final PackageManager mPackageManager; + + private String mModuleVersion; + + public MainlineModuleVersionPreferenceController(Context context, String key) { + super(context, key); + mPackageManager = mContext.getPackageManager(); + initModules(); + } + + @Override + public int getAvailabilityStatus() { + return !TextUtils.isEmpty(mModuleVersion) ? AVAILABLE : UNSUPPORTED_ON_DEVICE; + } + + private void initModules() { + final String moduleProvider = mContext.getString( + com.android.internal.R.string.config_defaultModuleMetadataProvider); + if (!TextUtils.isEmpty(moduleProvider)) { + try { + mModuleVersion = + mPackageManager.getPackageInfo(moduleProvider, 0 /* flags */).versionName; + return; + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "Failed to get mainline version.", e); + mModuleVersion = null; + } + } + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + + // Confirm MODULE_UPDATE_INTENT is handleable, and set it to Preference. + final ResolveInfo resolved = + mPackageManager.resolveActivity(MODULE_UPDATE_INTENT, 0 /* flags */); + if (resolved != null) { + preference.setIntent(MODULE_UPDATE_INTENT); + } else { + preference.setIntent(null); + } + } + + @Override + public CharSequence getSummary() { + return mModuleVersion; + } +} diff --git a/src/com/android/settings/deviceinfo/firmwareversion/SecurityPatchLevelDialogController.java b/src/com/android/settings/deviceinfo/firmwareversion/SecurityPatchLevelDialogController.java deleted file mode 100644 index cf8ee61090..0000000000 --- a/src/com/android/settings/deviceinfo/firmwareversion/SecurityPatchLevelDialogController.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (C) 2017 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.firmwareversion; - -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import androidx.annotation.VisibleForTesting; -import android.text.TextUtils; -import android.util.Log; -import android.view.View; - -import com.android.settings.R; -import com.android.settingslib.DeviceInfoUtils; -import com.android.settingslib.wrapper.PackageManagerWrapper; - -public class SecurityPatchLevelDialogController implements View.OnClickListener { - - private static final String TAG = "SecurityPatchCtrl"; - private static final Uri INTENT_URI_DATA = Uri.parse( - "https://source.android.com/security/bulletin/"); - - @VisibleForTesting - static final int SECURITY_PATCH_VALUE_ID = R.id.security_patch_level_value; - @VisibleForTesting - static final int SECURITY_PATCH_LABEL_ID = R.id.security_patch_level_label; - - private final FirmwareVersionDialogFragment mDialog; - private final Context mContext; - private final PackageManagerWrapper mPackageManager; - private final String mCurrentPatch; - - public SecurityPatchLevelDialogController(FirmwareVersionDialogFragment dialog) { - mDialog = dialog; - mContext = dialog.getContext(); - mPackageManager = new PackageManagerWrapper(mContext.getPackageManager()); - mCurrentPatch = DeviceInfoUtils.getSecurityPatch(); - } - - @Override - public void onClick(View v) { - final Intent intent = new Intent(); - intent.setAction(Intent.ACTION_VIEW); - intent.setData(INTENT_URI_DATA); - if (mPackageManager.queryIntentActivities(intent, 0).isEmpty()) { - // Don't send out the intent to stop crash - Log.w(TAG, "Stop click action on " + SECURITY_PATCH_VALUE_ID + ": " - + "queryIntentActivities() returns empty"); - return; - } - - mContext.startActivity(intent); - } - - /** - * Populates the security patch level field in the dialog and registers click listeners. - */ - public void initialize() { - if (TextUtils.isEmpty(mCurrentPatch)) { - mDialog.removeSettingFromScreen(SECURITY_PATCH_LABEL_ID); - mDialog.removeSettingFromScreen(SECURITY_PATCH_VALUE_ID); - return; - } - registerListeners(); - mDialog.setText(SECURITY_PATCH_VALUE_ID, mCurrentPatch); - } - - private void registerListeners() { - mDialog.registerClickListener(SECURITY_PATCH_LABEL_ID, this /* listener */); - mDialog.registerClickListener(SECURITY_PATCH_VALUE_ID, this /* listener */); - } -} diff --git a/src/com/android/settings/deviceinfo/firmwareversion/SecurityPatchLevelPreferenceController.java b/src/com/android/settings/deviceinfo/firmwareversion/SecurityPatchLevelPreferenceController.java new file mode 100644 index 0000000000..1df78a8081 --- /dev/null +++ b/src/com/android/settings/deviceinfo/firmwareversion/SecurityPatchLevelPreferenceController.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2019 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.firmwareversion; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.text.TextUtils; +import android.util.Log; + +import androidx.preference.Preference; + +import com.android.settings.core.BasePreferenceController; +import com.android.settingslib.DeviceInfoUtils; + +public class SecurityPatchLevelPreferenceController extends BasePreferenceController { + + private static final String TAG = "SecurityPatchCtrl"; + private static final Uri INTENT_URI_DATA = Uri.parse( + "https://source.android.com/security/bulletin/"); + + private final PackageManager mPackageManager; + private final String mCurrentPatch; + + public SecurityPatchLevelPreferenceController(Context context, String key) { + super(context, key); + mPackageManager = mContext.getPackageManager(); + mCurrentPatch = DeviceInfoUtils.getSecurityPatch(); + } + + @Override + public int getAvailabilityStatus() { + return !TextUtils.isEmpty(mCurrentPatch) + ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; + } + + @Override + public CharSequence getSummary() { + return mCurrentPatch; + } + + @Override + public boolean handlePreferenceTreeClick(Preference preference) { + if (!TextUtils.equals(preference.getKey(), getPreferenceKey())) { + return false; + } + + final Intent intent = new Intent(); + intent.setAction(Intent.ACTION_VIEW); + intent.setData(INTENT_URI_DATA); + if (mPackageManager.queryIntentActivities(intent, 0).isEmpty()) { + // Don't send out the intent to stop crash + Log.w(TAG, "queryIntentActivities() returns empty"); + return true; + } + + mContext.startActivity(intent); + return true; + } +} diff --git a/src/com/android/settings/deviceinfo/firmwareversion/SimpleBuildNumberPreferenceController.java b/src/com/android/settings/deviceinfo/firmwareversion/SimpleBuildNumberPreferenceController.java new file mode 100644 index 0000000000..53f5ff9aa9 --- /dev/null +++ b/src/com/android/settings/deviceinfo/firmwareversion/SimpleBuildNumberPreferenceController.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2019 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.firmwareversion; + +import android.content.Context; +import android.os.Build; +import android.text.BidiFormatter; + +import com.android.settings.core.BasePreferenceController; + +public class SimpleBuildNumberPreferenceController extends BasePreferenceController { + + public SimpleBuildNumberPreferenceController(Context context, + String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE_UNSEARCHABLE; + } + + @Override + public CharSequence getSummary() { + return BidiFormatter.getInstance().unicodeWrap(Build.DISPLAY); + } +} diff --git a/src/com/android/settings/deviceinfo/hardwareinfo/DeviceModelPreferenceController.java b/src/com/android/settings/deviceinfo/hardwareinfo/DeviceModelPreferenceController.java new file mode 100644 index 0000000000..93dd826dbc --- /dev/null +++ b/src/com/android/settings/deviceinfo/hardwareinfo/DeviceModelPreferenceController.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2019 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.hardwareinfo; + +import android.content.Context; + +import com.android.settings.deviceinfo.HardwareInfoPreferenceController; + +public class DeviceModelPreferenceController extends HardwareInfoPreferenceController { + + public DeviceModelPreferenceController(Context context, String key) { + super(context, key); + } + + @Override + public int getAvailabilityStatus() { + final int availability = super.getAvailabilityStatus(); + if (availability == AVAILABLE_UNSEARCHABLE) { + return AVAILABLE; + } + return availability; + } + + @Override + public boolean isSliceable() { + return true; + } + + @Override + public CharSequence getSummary() { + return HardwareInfoPreferenceController.getDeviceModel(); + } + + @Override + public boolean useDynamicSliceSummary() { + return true; + } +} diff --git a/src/com/android/settings/deviceinfo/hardwareinfo/HardwareInfoFragment.java b/src/com/android/settings/deviceinfo/hardwareinfo/HardwareInfoFragment.java new file mode 100644 index 0000000000..40e73efe88 --- /dev/null +++ b/src/com/android/settings/deviceinfo/hardwareinfo/HardwareInfoFragment.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2019 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.hardwareinfo; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.provider.SearchIndexableResource; + +import com.android.settings.R; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; +import com.android.settingslib.search.SearchIndexable; + +import java.util.ArrayList; +import java.util.List; + + +@SearchIndexable +public class HardwareInfoFragment extends DashboardFragment { + + public static final String TAG = "HardwareInfo"; + + @Override + public int getMetricsCategory() { + return SettingsEnums.DIALOG_SETTINGS_HARDWARE_INFO; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.hardware_info; + } + + @Override + protected String getLogTag() { + return TAG; + } + + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, + boolean enabled) { + final ArrayList<SearchIndexableResource> result = new ArrayList<>(); + + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.hardware_info; + result.add(sir); + return result; + } + + @Override + protected boolean isPageSearchEnabled(Context context) { + return context.getResources().getBoolean(R.bool.config_show_device_model); + } + }; +} diff --git a/src/com/android/settings/deviceinfo/hardwareinfo/HardwareRevisionPreferenceController.java b/src/com/android/settings/deviceinfo/hardwareinfo/HardwareRevisionPreferenceController.java new file mode 100644 index 0000000000..e7f642316b --- /dev/null +++ b/src/com/android/settings/deviceinfo/hardwareinfo/HardwareRevisionPreferenceController.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2019 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.hardwareinfo; + +import android.content.Context; +import android.os.SystemProperties; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.slices.Sliceable; + +public class HardwareRevisionPreferenceController extends BasePreferenceController { + + public HardwareRevisionPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public int getAvailabilityStatus() { + return mContext.getResources().getBoolean(R.bool.config_show_device_model) + ? AVAILABLE : UNSUPPORTED_ON_DEVICE; + } + + @Override + public boolean useDynamicSliceSummary() { + return true; + } + + @Override + public boolean isSliceable() { + return true; + } + + @Override + public boolean isCopyableSlice() { + return true; + } + + @Override + public void copy() { + Sliceable.setCopyContent(mContext, getSummary(), + mContext.getText(R.string.hardware_revision)); + } + + @Override + public CharSequence getSummary() { + return SystemProperties.get("ro.boot.hardware.revision"); + } +} diff --git a/src/com/android/settings/deviceinfo/hardwareinfo/SerialNumberPreferenceController.java b/src/com/android/settings/deviceinfo/hardwareinfo/SerialNumberPreferenceController.java new file mode 100644 index 0000000000..7ef1313ef4 --- /dev/null +++ b/src/com/android/settings/deviceinfo/hardwareinfo/SerialNumberPreferenceController.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2019 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.hardwareinfo; + +import android.content.Context; +import android.os.Build; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.slices.Sliceable; + +public class SerialNumberPreferenceController extends BasePreferenceController { + + public SerialNumberPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public int getAvailabilityStatus() { + return mContext.getResources().getBoolean(R.bool.config_show_device_model) + ? AVAILABLE : UNSUPPORTED_ON_DEVICE; + } + + @Override + public boolean isSliceable() { + return true; + } + + @Override + public boolean isCopyableSlice() { + return true; + } + + @Override + public boolean useDynamicSliceSummary() { + return true; + } + + @Override + public void copy() { + Sliceable.setCopyContent(mContext, getSummary(), + mContext.getText(R.string.status_serial_number)); + } + + @Override + public CharSequence getSummary() { + return Build.getSerial(); + } +} diff --git a/src/com/android/settings/deviceinfo/imei/ImeiInfoDialogController.java b/src/com/android/settings/deviceinfo/imei/ImeiInfoDialogController.java index 71da4a3d5d..5c3772bac8 100644 --- a/src/com/android/settings/deviceinfo/imei/ImeiInfoDialogController.java +++ b/src/com/android/settings/deviceinfo/imei/ImeiInfoDialogController.java @@ -18,8 +18,6 @@ package com.android.settings.deviceinfo.imei; import android.content.Context; import android.content.res.Resources; -import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; @@ -28,14 +26,18 @@ import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextUtils; import android.text.style.TtsSpan; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; import com.android.internal.telephony.PhoneConstants; import com.android.settings.R; -import java.util.List; - public class ImeiInfoDialogController { + private static final String TAG = "ImeiInfoDialog"; + @VisibleForTesting static final int ID_PRL_VERSION_VALUE = R.id.prl_version_value; private static final int ID_MIN_NUMBER_LABEL = R.id.min_number_label; @@ -53,6 +55,9 @@ public class ImeiInfoDialogController { static final int ID_GSM_SETTINGS = R.id.gsm_settings; private static CharSequence getTextAsDigits(CharSequence text) { + if (TextUtils.isEmpty(text)) { + return ""; + } if (TextUtils.isDigitsOnly(text)) { final Spannable spannable = new SpannableStringBuilder(text); final TtsSpan span = new TtsSpan.DigitsBuilder(text.toString()).build(); @@ -71,15 +76,27 @@ public class ImeiInfoDialogController { mDialog = dialog; mSlotId = slotId; final Context context = dialog.getContext(); - mTelephonyManager = (TelephonyManager) context.getSystemService( - Context.TELEPHONY_SERVICE); - mSubscriptionInfo = getSubscriptionInfo(context, slotId); + mSubscriptionInfo = context.getSystemService(SubscriptionManager.class) + .getActiveSubscriptionInfoForSimSlotIndex(slotId); + TelephonyManager tm = context.getSystemService(TelephonyManager.class); + if (mSubscriptionInfo != null) { + mTelephonyManager = context.getSystemService(TelephonyManager.class) + .createForSubscriptionId(mSubscriptionInfo.getSubscriptionId()); + } else if(isValidSlotIndex(slotId, tm)) { + mTelephonyManager = tm; + } else { + mTelephonyManager = null; + } } /** * Sets IMEI/MEID information based on whether the device is CDMA or GSM. */ public void populateImeiInfo() { + if (mTelephonyManager == null) { + Log.w(TAG, "TelephonyManager for this slot is null. Invalid slot? id=" + mSlotId); + return; + } if (mTelephonyManager.getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA) { updateDialogForCdmaPhone(); } else { @@ -90,9 +107,10 @@ public class ImeiInfoDialogController { private void updateDialogForCdmaPhone() { final Resources res = mDialog.getContext().getResources(); mDialog.setText(ID_MEID_NUMBER_VALUE, getMeid()); - mDialog.setText(ID_MIN_NUMBER_VALUE, - mSubscriptionInfo != null ? mTelephonyManager.getCdmaMin( - mSubscriptionInfo.getSubscriptionId()) : ""); + // MIN needs to read from SIM. So if no SIM, we should not show MIN on UI + mDialog.setText(ID_MIN_NUMBER_VALUE, mSubscriptionInfo != null + ? mTelephonyManager.getCdmaMin(mSubscriptionInfo.getSubscriptionId()) + : ""); if (res.getBoolean(R.bool.config_msid_enable)) { mDialog.setText(ID_MIN_NUMBER_LABEL, @@ -121,19 +139,10 @@ public class ImeiInfoDialogController { mDialog.removeViewFromScreen(ID_CDMA_SETTINGS); } - private SubscriptionInfo getSubscriptionInfo(Context context, int slotId) { - final List<SubscriptionInfo> subscriptionInfoList = SubscriptionManager.from(context) - .getActiveSubscriptionInfoList(); - if (subscriptionInfoList == null) { - return null; - } - - return subscriptionInfoList.get(slotId); - } - @VisibleForTesting String getCdmaPrlVersion() { - return mTelephonyManager.getCdmaPrlVersion(); + // PRL needs to read from SIM. So if no SIM, return empty + return mSubscriptionInfo != null ? mTelephonyManager.getCdmaPrlVersion() : ""; } @VisibleForTesting @@ -146,4 +155,9 @@ public class ImeiInfoDialogController { String getMeid() { return mTelephonyManager.getMeid(mSlotId); } + + @VisibleForTesting + private boolean isValidSlotIndex(int slotIndex, TelephonyManager telephonyManager) { + return slotIndex >= 0 && slotIndex < telephonyManager.getPhoneCount(); + } } diff --git a/src/com/android/settings/deviceinfo/imei/ImeiInfoDialogFragment.java b/src/com/android/settings/deviceinfo/imei/ImeiInfoDialogFragment.java index 3cc90e4829..b2f083f001 100644 --- a/src/com/android/settings/deviceinfo/imei/ImeiInfoDialogFragment.java +++ b/src/com/android/settings/deviceinfo/imei/ImeiInfoDialogFragment.java @@ -16,19 +16,20 @@ package com.android.settings.deviceinfo.imei; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.Fragment; -import android.app.FragmentManager; +import android.app.settings.SettingsEnums; import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.widget.TextView; -import com.android.internal.logging.nano.MetricsProto; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; + import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; @@ -56,7 +57,7 @@ public class ImeiInfoDialogFragment extends InstrumentedDialogFragment { @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DIALOG_IMEI_INFO; + return SettingsEnums.DIALOG_IMEI_INFO; } @Override diff --git a/src/com/android/settings/deviceinfo/imei/ImeiInfoPreferenceController.java b/src/com/android/settings/deviceinfo/imei/ImeiInfoPreferenceController.java index d673a28027..3459b5ca84 100644 --- a/src/com/android/settings/deviceinfo/imei/ImeiInfoPreferenceController.java +++ b/src/com/android/settings/deviceinfo/imei/ImeiInfoPreferenceController.java @@ -18,16 +18,21 @@ package com.android.settings.deviceinfo.imei; import static android.telephony.TelephonyManager.PHONE_TYPE_CDMA; -import android.app.Fragment; import android.content.Context; +import android.os.UserManager; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; + import androidx.annotation.VisibleForTesting; +import androidx.fragment.app.Fragment; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.telephony.TelephonyManager; import com.android.settings.R; -import com.android.settings.core.PreferenceControllerMixin; -import com.android.settingslib.deviceinfo.AbstractSimStatusImeiInfoPreferenceController; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.slices.Sliceable; +import com.android.settingslib.Utils; import java.util.ArrayList; import java.util.List; @@ -35,39 +40,30 @@ import java.util.List; /** * Controller that manages preference for single and multi sim devices. */ -public class ImeiInfoPreferenceController extends - AbstractSimStatusImeiInfoPreferenceController implements PreferenceControllerMixin { - - private static final String KEY_IMEI_INFO = "imei_info"; +public class ImeiInfoPreferenceController extends BasePreferenceController { private final boolean mIsMultiSim; private final TelephonyManager mTelephonyManager; private final List<Preference> mPreferenceList = new ArrayList<>(); - private final Fragment mFragment; - - public ImeiInfoPreferenceController(Context context, Fragment fragment) { - super(context); + private Fragment mFragment; - mFragment = fragment; + public ImeiInfoPreferenceController(Context context, String key) { + super(context, key); mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); mIsMultiSim = mTelephonyManager.getPhoneCount() > 1; } - @Override - public String getPreferenceKey() { - return KEY_IMEI_INFO; + public void setHost(Fragment fragment) { + mFragment = fragment; } @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); final Preference preference = screen.findPreference(getPreferenceKey()); - if (!isAvailable() || preference == null || !preference.isVisible()) { - return; - } mPreferenceList.add(preference); - updatePreference(preference, 0 /* sim slot */); + updatePreference(preference, 0 /* simSlot */); final int imeiPreferenceOrder = preference.getOrder(); // Add additional preferences for each sim in the device @@ -75,7 +71,7 @@ public class ImeiInfoPreferenceController extends simSlotNumber++) { final Preference multiSimPreference = createNewPreference(screen.getContext()); multiSimPreference.setOrder(imeiPreferenceOrder + simSlotNumber); - multiSimPreference.setKey(KEY_IMEI_INFO + simSlotNumber); + multiSimPreference.setKey(getPreferenceKey() + simSlotNumber); screen.addPreference(multiSimPreference); mPreferenceList.add(multiSimPreference); updatePreference(multiSimPreference, simSlotNumber); @@ -83,6 +79,29 @@ public class ImeiInfoPreferenceController extends } @Override + public void updateState(Preference preference) { + if (preference == null) { + return; + } + int size = mPreferenceList.size(); + for (int i = 0; i < size; i++) { + Preference pref = mPreferenceList.get(i); + updatePreference(pref, i); + } + } + + @Override + public CharSequence getSummary() { + return getSummary(0); + } + + private CharSequence getSummary(int simSlot) { + final int phoneType = getPhoneType(simSlot); + return phoneType == PHONE_TYPE_CDMA ? mTelephonyManager.getMeid(simSlot) + : mTelephonyManager.getImei(simSlot); + } + + @Override public boolean handlePreferenceTreeClick(Preference preference) { final int simSlot = mPreferenceList.indexOf(preference); if (simSlot == -1) { @@ -93,16 +112,35 @@ public class ImeiInfoPreferenceController extends return true; } + @Override + public int getAvailabilityStatus() { + return mContext.getSystemService(UserManager.class).isAdminUser() + && !Utils.isWifiOnly(mContext) ? AVAILABLE : UNSUPPORTED_ON_DEVICE; + } + + @Override + public boolean isSliceable() { + return true; + } + + @Override + public boolean isCopyableSlice() { + return true; + } + + @Override + public boolean useDynamicSliceSummary() { + return true; + } + + @Override + public void copy() { + Sliceable.setCopyContent(mContext, getSummary(0), getTitle(0)); + } + private void updatePreference(Preference preference, int simSlot) { - final int phoneType = mTelephonyManager.getPhoneType(); - if (phoneType == PHONE_TYPE_CDMA) { - preference.setTitle(getTitleForCdmaPhone(simSlot)); - preference.setSummary(getMeid(simSlot)); - } else { - // GSM phone - preference.setTitle(getTitleForGsmPhone(simSlot)); - preference.setSummary(mTelephonyManager.getImei(simSlot)); - } + preference.setTitle(getTitle(simSlot)); + preference.setSummary(getSummary(simSlot)); } private CharSequence getTitleForGsmPhone(int simSlot) { @@ -115,9 +153,17 @@ public class ImeiInfoPreferenceController extends : mContext.getString(R.string.status_meid_number); } - @VisibleForTesting - String getMeid(int simSlot) { - return mTelephonyManager.getMeid(simSlot); + private CharSequence getTitle(int simSlot) { + final int phoneType = getPhoneType(simSlot); + return phoneType == PHONE_TYPE_CDMA ? getTitleForCdmaPhone(simSlot) + : getTitleForGsmPhone(simSlot); + } + + private int getPhoneType(int slotIndex) { + SubscriptionInfo subInfo = SubscriptionManager.from(mContext) + .getActiveSubscriptionInfoForSimSlotIndex(slotIndex); + return mTelephonyManager.getCurrentPhoneType(subInfo != null ? subInfo.getSubscriptionId() + : SubscriptionManager.DEFAULT_SUBSCRIPTION_ID); } @VisibleForTesting diff --git a/src/com/android/settings/search/DeviceIndexFeatureProviderImpl.java b/src/com/android/settings/deviceinfo/legal/CopyrightPreferenceController.java index 087ecf83db..68e51f40df 100644 --- a/src/com/android/settings/search/DeviceIndexFeatureProviderImpl.java +++ b/src/com/android/settings/deviceinfo/legal/CopyrightPreferenceController.java @@ -12,28 +12,21 @@ * permissions and limitations under the License. */ -package com.android.settings.search; +package com.android.settings.deviceinfo.legal; import android.content.Context; -import android.net.Uri; +import android.content.Intent; -import java.util.List; +public class CopyrightPreferenceController extends LegalPreferenceController { -public class DeviceIndexFeatureProviderImpl implements DeviceIndexFeatureProvider { + private static final Intent INTENT = new Intent("android.settings.COPYRIGHT"); - @Override - public boolean isIndexingEnabled() { - return false; - } - - @Override - public void index(Context context, CharSequence title, Uri sliceUri, Uri launchUri, - List<String> keywords) { - // Not enabled by default. + public CopyrightPreferenceController(Context context, String key) { + super(context, key); } @Override - public void clearIndex(Context context) { - // Not enabled by default. + protected Intent getIntent() { + return INTENT; } } diff --git a/src/com/android/settings/deviceinfo/legal/LegalPreferenceController.java b/src/com/android/settings/deviceinfo/legal/LegalPreferenceController.java new file mode 100644 index 0000000000..0e164747b6 --- /dev/null +++ b/src/com/android/settings/deviceinfo/legal/LegalPreferenceController.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2018 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.legal; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; + +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.core.BasePreferenceController; + +import java.util.List; + + +public abstract class LegalPreferenceController extends BasePreferenceController { + private final PackageManager mPackageManager; + private Preference mPreference; + + public LegalPreferenceController(Context context, String key) { + super(context, key); + mPackageManager = mContext.getPackageManager(); + } + + @Override + public int getAvailabilityStatus() { + if (findMatchingSpecificActivity() != null) { + return AVAILABLE; + } else { + return UNSUPPORTED_ON_DEVICE; + } + } + + @Override + public void displayPreference(PreferenceScreen screen) { + mPreference = screen.findPreference(getPreferenceKey()); + super.displayPreference(screen); + + if (getAvailabilityStatus() == AVAILABLE) { + replacePreferenceIntent(); + } + } + + protected abstract Intent getIntent(); + + private ResolveInfo findMatchingSpecificActivity() { + final Intent intent = getIntent(); + if (intent == null) { + return null; + } + + // Find the activity that is in the system image + final List<ResolveInfo> list = mPackageManager.queryIntentActivities(intent, 0); + if (list == null) { + return null; + } + + for (ResolveInfo resolveInfo : list) { + if ((resolveInfo.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) + != 0) { + return resolveInfo; + } + } + + // Did not find a matching activity + return null; + } + + private void replacePreferenceIntent() { + final ResolveInfo resolveInfo = findMatchingSpecificActivity(); + if (resolveInfo == null) { + return; + } + + // Replace the intent with this specific activity + mPreference.setIntent(new Intent().setClassName( + resolveInfo.activityInfo.packageName, + resolveInfo.activityInfo.name)); + + mPreference.setTitle(resolveInfo.loadLabel(mPackageManager)); + } +} diff --git a/src/com/android/settings/deviceinfo/legal/LicensePreferenceController.java b/src/com/android/settings/deviceinfo/legal/LicensePreferenceController.java new file mode 100644 index 0000000000..67af15b4fc --- /dev/null +++ b/src/com/android/settings/deviceinfo/legal/LicensePreferenceController.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2018 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.legal; + +import android.content.Context; +import android.content.Intent; + +public class LicensePreferenceController extends LegalPreferenceController { + + private static final Intent INTENT = new Intent("android.settings.LICENSE"); + + public LicensePreferenceController(Context context, String key) { + super(context, key); + } + + @Override + protected Intent getIntent() { + return INTENT; + } +} diff --git a/src/com/android/settings/deviceinfo/legal/ModuleLicensePreference.java b/src/com/android/settings/deviceinfo/legal/ModuleLicensePreference.java new file mode 100644 index 0000000000..e012275798 --- /dev/null +++ b/src/com/android/settings/deviceinfo/legal/ModuleLicensePreference.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2019 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.legal; + +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ModuleInfo; +import android.util.Log; +import android.widget.Toast; + +import androidx.preference.Preference; + +import com.android.settings.R; + +/** + * Preference in a list that represents a mainline module that has a licenses file. + */ +public class ModuleLicensePreference extends Preference { + private static final String TAG = "ModuleLicensePreference"; + private final ModuleInfo mModule; + + public ModuleLicensePreference(Context context, ModuleInfo module) { + super(context); + mModule = module; + setKey(module.getPackageName()); + setTitle(module.getName()); + } + + @Override + protected void onClick() { + // Kick off external viewer due to WebView security restrictions (Settings cannot use + // WebView because it is UID 1000). + Intent intent = new Intent(Intent.ACTION_VIEW) + .setDataAndType( + ModuleLicenseProvider.getUriForPackage(mModule.getPackageName()), + ModuleLicenseProvider.LICENSE_FILE_MIME_TYPE) + .putExtra(Intent.EXTRA_TITLE, mModule.getName()) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + .addCategory(Intent.CATEGORY_DEFAULT) + .setPackage("com.android.htmlviewer"); + try { + getContext().startActivity(intent); + } catch (ActivityNotFoundException e) { + Log.e(TAG, "Failed to find viewer", e); + showError(); + } + } + + private void showError() { + Toast.makeText( + getContext(), R.string.settings_license_activity_unavailable, Toast.LENGTH_LONG) + .show(); + } +} diff --git a/src/com/android/settings/deviceinfo/legal/ModuleLicenseProvider.java b/src/com/android/settings/deviceinfo/legal/ModuleLicenseProvider.java new file mode 100644 index 0000000000..6731c695ab --- /dev/null +++ b/src/com/android/settings/deviceinfo/legal/ModuleLicenseProvider.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2019 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.legal; + +import android.content.ContentProvider; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.res.AssetManager; +import android.database.Cursor; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; +import androidx.core.util.Preconditions; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.List; +import java.util.zip.GZIPInputStream; + +public class ModuleLicenseProvider extends ContentProvider { + private static final String TAG = "ModuleLicenseProvider"; + + public static final String AUTHORITY = "com.android.settings.module_licenses"; + static final String GZIPPED_LICENSE_FILE_NAME = "NOTICE.html.gz"; + static final String LICENSE_FILE_NAME = "NOTICE.html"; + static final String LICENSE_FILE_MIME_TYPE = "text/html"; + static final String PREFS_NAME = "ModuleLicenseProvider"; + + @Override + public boolean onCreate() { + return true; + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + throw new UnsupportedOperationException(); + } + + @Override + public String getType(Uri uri) { + checkUri(getContext(), uri); + return LICENSE_FILE_MIME_TYPE; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + throw new UnsupportedOperationException(); + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException(); + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException(); + } + + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode) { + final Context context = getContext(); + checkUri(context, uri); + Preconditions.checkArgument("r".equals(mode), "Read is the only supported mode"); + + try { + String packageName = uri.getPathSegments().get(0); + File cachedFile = getCachedHtmlFile(context, packageName); + if (isCachedHtmlFileOutdated(context, packageName)) { + try (InputStream in = new GZIPInputStream( + getPackageAssetManager(context.getPackageManager(), packageName) + .open(GZIPPED_LICENSE_FILE_NAME))) { + File directory = getCachedFileDirectory(context, packageName); + if (!directory.exists()) { + directory.mkdir(); + } + Files.copy(in, cachedFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + // Now that the file is saved, write the package's version code to shared prefs + SharedPreferences.Editor editor = getPrefs(context).edit(); + editor.putLong( + packageName, + getPackageInfo(context, packageName).getLongVersionCode()) + .commit(); + } + return ParcelFileDescriptor.open(cachedFile, ParcelFileDescriptor.MODE_READ_ONLY); + } catch (PackageManager.NameNotFoundException e) { + Log.wtf(TAG, "checkUri should have already caught this error", e); + } catch (IOException e) { + Log.e(TAG, "Could not open file descriptor", e); + } + return null; + } + + /** + * Returns true if the cached file for the given package is outdated. A cached file is + * outdated if one of the following are true: + * 1. the shared prefs does not contain a version code for this package + * 2. The version code does not match the package's version code + * 3. There is no file or the file is empty. + */ + @VisibleForTesting + static boolean isCachedHtmlFileOutdated(Context context, String packageName) + throws PackageManager.NameNotFoundException { + SharedPreferences prefs = getPrefs(context); + File file = getCachedHtmlFile(context, packageName); + return !prefs.contains(packageName) + || prefs.getLong(packageName, 0L) + != getPackageInfo(context, packageName).getLongVersionCode() + || !file.exists() || file.length() == 0; + } + + static AssetManager getPackageAssetManager(PackageManager packageManager, String packageName) + throws PackageManager.NameNotFoundException { + return packageManager.getResourcesForApplication( + packageManager.getPackageInfo(packageName, PackageManager.MATCH_APEX) + .applicationInfo) + .getAssets(); + } + + static Uri getUriForPackage(String packageName) { + return new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(AUTHORITY) + .appendPath(packageName) + .appendPath(LICENSE_FILE_NAME) + .build(); + } + + private static void checkUri(Context context, Uri uri) { + List<String> pathSegments = uri.getPathSegments(); + // A URI is valid iff it: + // 1. is a content URI + // 2. uses the correct authority + // 3. has exactly 2 segments and the last one is NOTICE.html + // 4. (checked below) first path segment is the package name of a module + if (!ContentResolver.SCHEME_CONTENT.equals(uri.getScheme()) + || !AUTHORITY.equals(uri.getAuthority()) + || pathSegments == null + || pathSegments.size() != 2 + || !LICENSE_FILE_NAME.equals(pathSegments.get(1))) { + throw new IllegalArgumentException(uri + "is not a valid URI"); + } + // Grab the first path segment, which is the package name of the module and make sure that + // there's actually a module for that package. getModuleInfo will throw if it does not + // exist. + try { + context.getPackageManager().getModuleInfo(pathSegments.get(0), 0 /* flags */); + } catch (PackageManager.NameNotFoundException e) { + throw new IllegalArgumentException(uri + "is not a valid URI", e); + } + } + + private static File getCachedFileDirectory(Context context, String packageName) { + return new File(context.getCacheDir(), packageName); + } + + private static File getCachedHtmlFile(Context context, String packageName) { + return new File(context.getCacheDir() + "/" + packageName, LICENSE_FILE_NAME); + } + + private static PackageInfo getPackageInfo(Context context, String packageName) + throws PackageManager.NameNotFoundException { + return context.getPackageManager().getPackageInfo(packageName, PackageManager.MATCH_APEX); + } + + private static SharedPreferences getPrefs(Context context) { + return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + } +} diff --git a/src/com/android/settings/deviceinfo/legal/ModuleLicensesDashboard.java b/src/com/android/settings/deviceinfo/legal/ModuleLicensesDashboard.java new file mode 100644 index 0000000000..f74b68f923 --- /dev/null +++ b/src/com/android/settings/deviceinfo/legal/ModuleLicensesDashboard.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2019 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.legal; + +import android.app.settings.SettingsEnums; + +import com.android.settings.R; +import com.android.settings.dashboard.DashboardFragment; + +public class ModuleLicensesDashboard extends DashboardFragment { + private static final String TAG = "ModuleLicensesDashboard"; + + @Override + public int getMetricsCategory() { + return SettingsEnums.MODULE_LICENSES_DASHBOARD; + } + + @Override + protected String getLogTag() { + return TAG; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.module_licenses; + } + + @Override + public int getHelpResource() { + return 0; + } +} diff --git a/src/com/android/settings/deviceinfo/legal/ModuleLicensesListPreferenceController.java b/src/com/android/settings/deviceinfo/legal/ModuleLicensesListPreferenceController.java new file mode 100644 index 0000000000..9faff85607 --- /dev/null +++ b/src/com/android/settings/deviceinfo/legal/ModuleLicensesListPreferenceController.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2019 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.legal; + +import android.content.Context; +import android.content.pm.ModuleInfo; +import android.content.pm.PackageManager; + +import com.android.settings.core.BasePreferenceController; + +import java.util.List; + +public class ModuleLicensesListPreferenceController extends BasePreferenceController { + public ModuleLicensesListPreferenceController(Context context, + String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public int getAvailabilityStatus() { + PackageManager packageManager = mContext.getPackageManager(); + List<ModuleInfo> modules = packageManager.getInstalledModules(0 /* flags */); + return modules.stream().anyMatch(new ModuleLicensesPreferenceController.Predicate(mContext)) + ? AVAILABLE + : CONDITIONALLY_UNAVAILABLE; + } +} diff --git a/src/com/android/settings/deviceinfo/legal/ModuleLicensesPreferenceController.java b/src/com/android/settings/deviceinfo/legal/ModuleLicensesPreferenceController.java new file mode 100644 index 0000000000..dd5edbb0f6 --- /dev/null +++ b/src/com/android/settings/deviceinfo/legal/ModuleLicensesPreferenceController.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2019 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.legal; + +import android.content.Context; +import android.content.pm.ModuleInfo; +import android.content.pm.PackageManager; + +import androidx.preference.PreferenceGroup; +import androidx.preference.PreferenceScreen; + +import com.android.internal.util.ArrayUtils; +import com.android.settings.core.BasePreferenceController; + +import java.io.IOException; +import java.util.Comparator; +import java.util.List; + +public class ModuleLicensesPreferenceController extends BasePreferenceController { + public ModuleLicensesPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE_UNSEARCHABLE; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + + PackageManager packageManager = mContext.getPackageManager(); + List<ModuleInfo> modules = packageManager.getInstalledModules(0 /* flags */); + PreferenceGroup group = screen.findPreference(getPreferenceKey()); + modules.stream() + .sorted(Comparator.comparing(o -> o.getName().toString())) + .filter(new Predicate(mContext)) + .forEach(module -> + group.addPreference( + new ModuleLicensePreference(group.getContext(), module))); + } + + static class Predicate implements java.util.function.Predicate<ModuleInfo> { + private final Context mContext; + + public Predicate(Context context) { + mContext = context; + } + @Override + public boolean test(ModuleInfo module) { + try { + return ArrayUtils.contains( + ModuleLicenseProvider.getPackageAssetManager( + mContext.getPackageManager(), + module.getPackageName()) + .list(""), + ModuleLicenseProvider.GZIPPED_LICENSE_FILE_NAME); + } catch (IOException | PackageManager.NameNotFoundException e) { + return false; + } + } + } +} diff --git a/src/com/android/settings/deviceinfo/legal/TermsPreferenceController.java b/src/com/android/settings/deviceinfo/legal/TermsPreferenceController.java new file mode 100644 index 0000000000..bccc44579b --- /dev/null +++ b/src/com/android/settings/deviceinfo/legal/TermsPreferenceController.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2018 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.legal; + +import android.content.Context; +import android.content.Intent; + +public class TermsPreferenceController extends LegalPreferenceController { + + private static final Intent INTENT = new Intent("android.settings.TERMS"); + + public TermsPreferenceController(Context context, String key) { + super(context, key); + } + + @Override + protected Intent getIntent() { + return INTENT; + } +} diff --git a/src/com/android/settings/deviceinfo/legal/WallpaperAttributionsPreferenceController.java b/src/com/android/settings/deviceinfo/legal/WallpaperAttributionsPreferenceController.java new file mode 100644 index 0000000000..caa5afcdc5 --- /dev/null +++ b/src/com/android/settings/deviceinfo/legal/WallpaperAttributionsPreferenceController.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2018 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.legal; + +import android.content.Context; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; + +public class WallpaperAttributionsPreferenceController extends BasePreferenceController { + + public WallpaperAttributionsPreferenceController(Context context, String key) { + super(context, key); + } + + @Override + public int getAvailabilityStatus() { + return mContext.getResources().getBoolean(R.bool.config_show_wallpaper_attribution) + ? AVAILABLE + : UNSUPPORTED_ON_DEVICE; + } +} diff --git a/src/com/android/settings/deviceinfo/legal/WebViewLicensePreferenceController.java b/src/com/android/settings/deviceinfo/legal/WebViewLicensePreferenceController.java new file mode 100644 index 0000000000..9d8b3f9502 --- /dev/null +++ b/src/com/android/settings/deviceinfo/legal/WebViewLicensePreferenceController.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2018 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.legal; + +import android.content.Context; +import android.content.Intent; + +public class WebViewLicensePreferenceController extends LegalPreferenceController { + + private static final Intent INTENT = new Intent("android.settings.WEBVIEW_LICENSE"); + + public WebViewLicensePreferenceController(Context context, String key) { + super(context, key); + } + + @Override + protected Intent getIntent() { + return INTENT; + } +} diff --git a/src/com/android/settings/deviceinfo/simstatus/SimStatusDialogController.java b/src/com/android/settings/deviceinfo/simstatus/SimStatusDialogController.java index 2a682d205b..3fafd14f62 100644 --- a/src/com/android/settings/deviceinfo/simstatus/SimStatusDialogController.java +++ b/src/com/android/settings/deviceinfo/simstatus/SimStatusDialogController.java @@ -19,19 +19,17 @@ package com.android.settings.deviceinfo.simstatus; import static android.content.Context.CARRIER_CONFIG_SERVICE; import static android.content.Context.EUICC_SERVICE; import static android.content.Context.TELEPHONY_SERVICE; +import static android.content.Context.TELEPHONY_SUBSCRIPTION_SERVICE; import android.Manifest; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.pm.PackageManager; import android.content.res.Resources; import android.os.Bundle; import android.os.PersistableBundle; import android.os.UserHandle; -import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; import android.telephony.CarrierConfigManager; import android.telephony.CellBroadcastMessage; import android.telephony.PhoneStateListener; @@ -39,22 +37,24 @@ import android.telephony.ServiceState; import android.telephony.SignalStrength; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; +import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener; import android.telephony.TelephonyManager; import android.telephony.euicc.EuiccManager; import android.text.BidiFormatter; import android.text.TextDirectionHeuristics; import android.text.TextUtils; -import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; import com.android.settings.R; import com.android.settingslib.DeviceInfoUtils; +import com.android.settingslib.Utils; import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnPause; import com.android.settingslib.core.lifecycle.events.OnResume; -import java.util.List; - public class SimStatusDialogController implements LifecycleObserver, OnResume, OnPause { private final static String TAG = "SimStatusDialogCtrl"; @@ -98,9 +98,21 @@ public class SimStatusDialogController implements LifecycleObserver, OnResume, O "com.android.cellbroadcastreceiver.GET_LATEST_CB_AREA_INFO"; private final static String CELL_BROADCAST_RECEIVER_APP = "com.android.cellbroadcastreceiver"; + private final OnSubscriptionsChangedListener mOnSubscriptionsChangedListener = + new OnSubscriptionsChangedListener() { + @Override + public void onSubscriptionsChanged() { + mSubscriptionInfo = mSubscriptionManager.getActiveSubscriptionInfo( + mSubscriptionInfo.getSubscriptionId()); + updateNetworkProvider(); + } + }; + + private SubscriptionInfo mSubscriptionInfo; + private final SimStatusDialogFragment mDialog; - private final SubscriptionInfo mSubscriptionInfo; private final TelephonyManager mTelephonyManager; + private final SubscriptionManager mSubscriptionManager; private final CarrierConfigManager mCarrierConfigManager; private final EuiccManager mEuiccManager; private final Resources mRes; @@ -134,11 +146,10 @@ public class SimStatusDialogController implements LifecycleObserver, OnResume, O mDialog = dialog; mContext = dialog.getContext(); mSubscriptionInfo = getPhoneSubscriptionInfo(slotId); - mTelephonyManager = (TelephonyManager) mContext.getSystemService( - TELEPHONY_SERVICE); - mCarrierConfigManager = (CarrierConfigManager) mContext.getSystemService( - CARRIER_CONFIG_SERVICE); - mEuiccManager = (EuiccManager) mContext.getSystemService(EUICC_SERVICE); + mTelephonyManager = mContext.getSystemService(TelephonyManager.class); + mCarrierConfigManager = mContext.getSystemService(CarrierConfigManager.class); + mEuiccManager = mContext.getSystemService(EuiccManager.class); + mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class); mRes = mContext.getResources(); @@ -155,9 +166,9 @@ public class SimStatusDialogController implements LifecycleObserver, OnResume, O } mPhoneStateListener = getPhoneStateListener(); + updateNetworkProvider(); final ServiceState serviceState = getCurrentServiceState(); - updateNetworkProvider(serviceState); updatePhoneNumber(); updateLatestAreaInfo(); updateServiceState(serviceState); @@ -174,10 +185,12 @@ public class SimStatusDialogController implements LifecycleObserver, OnResume, O return; } - mTelephonyManager.listen(mPhoneStateListener, + mTelephonyManager.createForSubscriptionId(mSubscriptionInfo.getSubscriptionId()) + .listen(mPhoneStateListener, PhoneStateListener.LISTEN_DATA_CONNECTION_STATE | PhoneStateListener.LISTEN_SIGNAL_STRENGTHS | PhoneStateListener.LISTEN_SERVICE_STATE); + mSubscriptionManager.addOnSubscriptionsChangedListener(mOnSubscriptionsChangedListener); if (mShowLatestAreaInfo) { mContext.registerReceiver(mAreaInfoReceiver, @@ -197,16 +210,19 @@ public class SimStatusDialogController implements LifecycleObserver, OnResume, O return; } - mTelephonyManager.listen(mPhoneStateListener, - PhoneStateListener.LISTEN_NONE); + mSubscriptionManager.removeOnSubscriptionsChangedListener(mOnSubscriptionsChangedListener); + mTelephonyManager.createForSubscriptionId(mSubscriptionInfo.getSubscriptionId()) + .listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE); if (mShowLatestAreaInfo) { mContext.unregisterReceiver(mAreaInfoReceiver); } } - private void updateNetworkProvider(ServiceState serviceState) { - mDialog.setText(NETWORK_PROVIDER_VALUE_ID, serviceState.getOperatorAlphaLong()); + private void updateNetworkProvider() { + final CharSequence carrierName = + mSubscriptionInfo != null ? mSubscriptionInfo.getCarrierName() : null; + mDialog.setText(NETWORK_PROVIDER_VALUE_ID, carrierName); } private void updatePhoneNumber() { @@ -252,8 +268,8 @@ public class SimStatusDialogController implements LifecycleObserver, OnResume, O } private void updateServiceState(ServiceState serviceState) { - final int state = serviceState.getState(); - if (state == ServiceState.STATE_OUT_OF_SERVICE || state == ServiceState.STATE_POWER_OFF) { + final int state = Utils.getCombinedServiceState(serviceState); + if (!Utils.isInService(serviceState)) { resetSignalStrength(); } @@ -281,6 +297,9 @@ public class SimStatusDialogController implements LifecycleObserver, OnResume, O } private void updateSignalStrength(SignalStrength signalStrength) { + if (signalStrength == null) { + return; + } final int subscriptionId = mSubscriptionInfo.getSubscriptionId(); final PersistableBundle carrierConfig = mCarrierConfigManager.getConfigForSubId(subscriptionId); @@ -296,10 +315,8 @@ public class SimStatusDialogController implements LifecycleObserver, OnResume, O return; } - final int state = getCurrentServiceState().getState(); - - if ((ServiceState.STATE_OUT_OF_SERVICE == state) || - (ServiceState.STATE_POWER_OFF == state)) { + ServiceState serviceState = getCurrentServiceState(); + if (serviceState == null || !Utils.isInService(serviceState)) { return; } @@ -383,7 +400,11 @@ public class SimStatusDialogController implements LifecycleObserver, OnResume, O } private void updateEid() { - mDialog.setText(EID_INFO_VALUE_ID, mEuiccManager.getEid()); + if (mEuiccManager.isEnabled()) { + mDialog.setText(EID_INFO_VALUE_ID, mEuiccManager.getEid()); + } else { + mDialog.removeSettingFromScreen(EID_INFO_VALUE_ID); + } } private void updateImsRegistrationState() { @@ -403,13 +424,7 @@ public class SimStatusDialogController implements LifecycleObserver, OnResume, O } private SubscriptionInfo getPhoneSubscriptionInfo(int slotId) { - final List<SubscriptionInfo> subscriptionInfoList = SubscriptionManager.from( - mContext).getActiveSubscriptionInfoList(); - if (subscriptionInfoList != null && subscriptionInfoList.size() > slotId) { - return subscriptionInfoList.get(slotId); - } else { - return null; - } + return SubscriptionManager.from(mContext).getActiveSubscriptionInfoForSimSlotIndex(slotId); } @VisibleForTesting @@ -418,20 +433,17 @@ public class SimStatusDialogController implements LifecycleObserver, OnResume, O mSubscriptionInfo.getSubscriptionId()); } - @VisibleForTesting - int getDbm(SignalStrength signalStrength) { + private int getDbm(SignalStrength signalStrength) { return signalStrength.getDbm(); } - @VisibleForTesting - int getAsuLevel(SignalStrength signalStrength) { + private int getAsuLevel(SignalStrength signalStrength) { return signalStrength.getAsuLevel(); } @VisibleForTesting PhoneStateListener getPhoneStateListener() { - return new PhoneStateListener( - mSubscriptionInfo.getSubscriptionId()) { + return new PhoneStateListener() { @Override public void onDataConnectionStateChanged(int state) { updateDataState(state); @@ -445,7 +457,7 @@ public class SimStatusDialogController implements LifecycleObserver, OnResume, O @Override public void onServiceStateChanged(ServiceState serviceState) { - updateNetworkProvider(serviceState); + updateNetworkProvider(); updateServiceState(serviceState); updateRoamingStatus(serviceState); } diff --git a/src/com/android/settings/deviceinfo/simstatus/SimStatusDialogFragment.java b/src/com/android/settings/deviceinfo/simstatus/SimStatusDialogFragment.java index a15cb81e01..d2d563f3b8 100644 --- a/src/com/android/settings/deviceinfo/simstatus/SimStatusDialogFragment.java +++ b/src/com/android/settings/deviceinfo/simstatus/SimStatusDialogFragment.java @@ -16,17 +16,18 @@ package com.android.settings.deviceinfo.simstatus; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.Fragment; -import android.app.FragmentManager; +import android.app.settings.SettingsEnums; import android.os.Bundle; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.widget.TextView; -import com.android.internal.logging.nano.MetricsProto; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; + import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; @@ -42,7 +43,7 @@ public class SimStatusDialogFragment extends InstrumentedDialogFragment { @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DIALOG_SIM_STATUS; + return SettingsEnums.DIALOG_SIM_STATUS; } public static void show(Fragment host, int slotId, String dialogTitle) { diff --git a/src/com/android/settings/deviceinfo/simstatus/SimStatusPreferenceController.java b/src/com/android/settings/deviceinfo/simstatus/SimStatusPreferenceController.java index 3f714712a0..ed01e22724 100644 --- a/src/com/android/settings/deviceinfo/simstatus/SimStatusPreferenceController.java +++ b/src/com/android/settings/deviceinfo/simstatus/SimStatusPreferenceController.java @@ -16,15 +16,16 @@ package com.android.settings.deviceinfo.simstatus; -import android.app.Fragment; import android.content.Context; -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; +import androidx.annotation.VisibleForTesting; +import androidx.fragment.app.Fragment; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.deviceinfo.AbstractSimStatusImeiInfoPreferenceController; @@ -106,7 +107,7 @@ public class SimStatusPreferenceController extends private CharSequence getCarrierName(int simSlot) { final List<SubscriptionInfo> subscriptionInfoList = - mSubscriptionManager.getActiveSubscriptionInfoList(); + mSubscriptionManager.getActiveSubscriptionInfoList(true); if (subscriptionInfoList != null) { for (SubscriptionInfo info : subscriptionInfoList) { if (info.getSimSlotIndex() == simSlot) { diff --git a/src/com/android/settings/deviceinfo/storage/AutomaticStorageManagementSwitchPreferenceController.java b/src/com/android/settings/deviceinfo/storage/AutomaticStorageManagementSwitchPreferenceController.java index 92078036c1..50027f2187 100644 --- a/src/com/android/settings/deviceinfo/storage/AutomaticStorageManagementSwitchPreferenceController.java +++ b/src/com/android/settings/deviceinfo/storage/AutomaticStorageManagementSwitchPreferenceController.java @@ -17,58 +17,56 @@ package com.android.settings.deviceinfo.storage; import android.app.ActivityManager; -import android.app.FragmentManager; +import android.app.settings.SettingsEnums; import android.content.Context; import android.os.SystemProperties; import android.provider.Settings; + import androidx.annotation.VisibleForTesting; +import androidx.fragment.app.FragmentManager; import androidx.preference.PreferenceScreen; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.settings.core.PreferenceControllerMixin; +import com.android.settings.core.BasePreferenceController; import com.android.settings.deletionhelper.ActivationWarningFragment; +import com.android.settings.overlay.FeatureFactory; import com.android.settings.widget.MasterSwitchController; import com.android.settings.widget.MasterSwitchPreference; import com.android.settings.widget.SwitchWidgetController; import com.android.settingslib.Utils; -import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnResume; public class AutomaticStorageManagementSwitchPreferenceController extends - AbstractPreferenceController implements PreferenceControllerMixin, LifecycleObserver, - OnResume, SwitchWidgetController.OnSwitchChangeListener { - private static final String KEY_TOGGLE_ASM = "toggle_asm"; + BasePreferenceController implements LifecycleObserver, OnResume, + SwitchWidgetController.OnSwitchChangeListener { @VisibleForTesting static final String STORAGE_MANAGER_ENABLED_BY_DEFAULT_PROPERTY = "ro.storage_manager.enabled"; - + private final MetricsFeatureProvider mMetricsFeatureProvider; private MasterSwitchPreference mSwitch; private MasterSwitchController mSwitchController; - private final MetricsFeatureProvider mMetricsFeatureProvider; - private final FragmentManager mFragmentManager; + private FragmentManager mFragmentManager; - public AutomaticStorageManagementSwitchPreferenceController(Context context, - MetricsFeatureProvider metricsFeatureProvider, FragmentManager fragmentManager) { - super(context); - mMetricsFeatureProvider = metricsFeatureProvider; + public AutomaticStorageManagementSwitchPreferenceController(Context context, String key) { + super(context, key); + mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider(); + } + + public AutomaticStorageManagementSwitchPreferenceController setFragmentManager( + FragmentManager fragmentManager) { mFragmentManager = fragmentManager; + return this; } @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - mSwitch = (MasterSwitchPreference) screen.findPreference(KEY_TOGGLE_ASM); - } - - @Override - public boolean isAvailable() { - return !ActivityManager.isLowRamDeviceStatic(); + mSwitch = screen.findPreference(getPreferenceKey()); } @Override - public String getPreferenceKey() { - return KEY_TOGGLE_ASM; + public int getAvailabilityStatus() { + return !ActivityManager.isLowRamDeviceStatic() ? AVAILABLE : UNSUPPORTED_ON_DEVICE; } @Override @@ -88,7 +86,7 @@ public class AutomaticStorageManagementSwitchPreferenceController extends @Override public boolean onSwitchToggled(boolean isChecked) { mMetricsFeatureProvider.action(mContext, - MetricsEvent.ACTION_TOGGLE_STORAGE_MANAGER, isChecked); + SettingsEnums.ACTION_TOGGLE_STORAGE_MANAGER, isChecked); Settings.Secure.putInt(mContext.getContentResolver(), Settings.Secure.AUTOMATIC_STORAGE_MANAGER_ENABLED, isChecked ? 1 : 0); @@ -103,11 +101,11 @@ public class AutomaticStorageManagementSwitchPreferenceController extends != 0; // Show warning if it is disabled by default and turning it on or if it was disabled by // policy and we're turning it on. - if ((isChecked && (!storageManagerEnabledByDefault || storageManagerDisabledByPolicy))) { + if (isChecked && (!storageManagerEnabledByDefault || storageManagerDisabledByPolicy)) { ActivationWarningFragment fragment = ActivationWarningFragment.newInstance(); fragment.show(mFragmentManager, ActivationWarningFragment.TAG); } return true; } -} +}
\ No newline at end of file diff --git a/src/com/android/settings/deviceinfo/storage/CachedStorageValuesHelper.java b/src/com/android/settings/deviceinfo/storage/CachedStorageValuesHelper.java index f07af5e56e..e6d9a724d9 100644 --- a/src/com/android/settings/deviceinfo/storage/CachedStorageValuesHelper.java +++ b/src/com/android/settings/deviceinfo/storage/CachedStorageValuesHelper.java @@ -19,9 +19,10 @@ package com.android.settings.deviceinfo.storage; import android.content.Context; import android.content.SharedPreferences; import android.provider.Settings; -import androidx.annotation.VisibleForTesting; import android.util.SparseArray; +import androidx.annotation.VisibleForTesting; + import com.android.settingslib.applications.StorageStatsSource; import com.android.settingslib.deviceinfo.PrivateStorageInfo; diff --git a/src/com/android/settings/deviceinfo/storage/SecondaryUserController.java b/src/com/android/settings/deviceinfo/storage/SecondaryUserController.java index 1eccb67f54..c78d108ab0 100644 --- a/src/com/android/settings/deviceinfo/storage/SecondaryUserController.java +++ b/src/com/android/settings/deviceinfo/storage/SecondaryUserController.java @@ -20,12 +20,13 @@ import android.content.Context; import android.content.pm.UserInfo; import android.graphics.drawable.Drawable; import android.os.UserManager; +import android.util.SparseArray; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.preference.PreferenceGroup; import androidx.preference.PreferenceScreen; -import android.util.SparseArray; import com.android.settings.Utils; import com.android.settings.core.PreferenceControllerMixin; @@ -48,8 +49,10 @@ public class SecondaryUserController extends AbstractPreferenceController implem private static final int USER_PROFILE_INSERTION_LOCATION = 6; private static final int SIZE_NOT_SET = -1; - private @NonNull UserInfo mUser; - private @Nullable StorageItemPreference mStoragePreference; + private @NonNull + UserInfo mUser; + private @Nullable + StorageItemPreference mStoragePreference; private Drawable mUserIcon; private long mSize; private long mTotalSizeBytes; @@ -57,7 +60,8 @@ public class SecondaryUserController extends AbstractPreferenceController implem /** * Adds the appropriate controllers to a controller list for handling all secondary users on * a device. - * @param context Context for initializing the preference controllers. + * + * @param context Context for initializing the preference controllers. * @param userManager UserManagerWrapper for figuring out which controllers to add. */ public static List<AbstractPreferenceController> getSecondaryUserControllers( @@ -90,8 +94,9 @@ public class SecondaryUserController extends AbstractPreferenceController implem /** * Constructor for a given secondary user. + * * @param context Context to initialize the underlying {@link AbstractPreferenceController}. - * @param info {@link UserInfo} for the secondary user which this controllers covers. + * @param info {@link UserInfo} for the secondary user which this controllers covers. */ @VisibleForTesting SecondaryUserController(Context context, @NonNull UserInfo info) { @@ -106,7 +111,7 @@ public class SecondaryUserController extends AbstractPreferenceController implem mStoragePreference = new StorageItemPreference(screen.getContext()); PreferenceGroup group = - (PreferenceGroup) screen.findPreference(TARGET_PREFERENCE_GROUP_KEY); + screen.findPreference(TARGET_PREFERENCE_GROUP_KEY); mStoragePreference.setTitle(mUser.name); mStoragePreference.setKey(PREFERENCE_KEY_BASE + mUser.id); if (mSize != SIZE_NOT_SET) { @@ -139,6 +144,7 @@ public class SecondaryUserController extends AbstractPreferenceController implem /** * Sets the size for the preference. + * * @param size Size in bytes. */ public void setSize(long size) { @@ -150,6 +156,7 @@ public class SecondaryUserController extends AbstractPreferenceController implem /** * Sets the total size for the preference for the progress bar. + * * @param totalSizeBytes Total size in bytes. */ public void setTotalSize(long totalSizeBytes) { @@ -184,8 +191,7 @@ public class SecondaryUserController extends AbstractPreferenceController implem @Override public void displayPreference(PreferenceScreen screen) { - PreferenceGroup group = - (PreferenceGroup) screen.findPreference(TARGET_PREFERENCE_GROUP_KEY); + final PreferenceGroup group = screen.findPreference(TARGET_PREFERENCE_GROUP_KEY); if (group == null) { return; } diff --git a/src/com/android/settings/deviceinfo/storage/StorageAsyncLoader.java b/src/com/android/settings/deviceinfo/storage/StorageAsyncLoader.java index affcbc9c1a..9d10a1daf0 100644 --- a/src/com/android/settings/deviceinfo/storage/StorageAsyncLoader.java +++ b/src/com/android/settings/deviceinfo/storage/StorageAsyncLoader.java @@ -23,6 +23,7 @@ import static android.content.pm.ApplicationInfo.CATEGORY_VIDEO; import android.content.Context; import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.UserInfo; import android.os.UserHandle; @@ -32,8 +33,7 @@ import android.util.Log; import android.util.SparseArray; import com.android.settingslib.applications.StorageStatsSource; -import com.android.settingslib.utils.AsyncLoader; -import com.android.settingslib.wrapper.PackageManagerWrapper; +import com.android.settingslib.utils.AsyncLoaderCompat; import java.io.IOException; import java.util.Collections; @@ -45,17 +45,17 @@ import java.util.List; * users */ public class StorageAsyncLoader - extends AsyncLoader<SparseArray<StorageAsyncLoader.AppsStorageResult>> { + extends AsyncLoaderCompat<SparseArray<StorageAsyncLoader.AppsStorageResult>> { private UserManager mUserManager; private static final String TAG = "StorageAsyncLoader"; private String mUuid; private StorageStatsSource mStatsManager; - private PackageManagerWrapper mPackageManager; + private PackageManager mPackageManager; private ArraySet<String> mSeenPackages; public StorageAsyncLoader(Context context, UserManager userManager, - String uuid, StorageStatsSource source, PackageManagerWrapper pm) { + String uuid, StorageStatsSource source, PackageManager pm) { super(context); mUserManager = userManager; mUuid = uuid; diff --git a/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceController.java b/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceController.java index 6ae4a6f0b2..525a887cb8 100644 --- a/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceController.java +++ b/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceController.java @@ -16,7 +16,7 @@ package com.android.settings.deviceinfo.storage; -import android.app.Fragment; +import android.app.settings.SettingsEnums; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; @@ -26,13 +26,14 @@ import android.net.TrafficStats; import android.os.Bundle; import android.os.UserHandle; import android.os.storage.VolumeInfo; +import android.util.Log; +import android.util.SparseArray; + import androidx.annotation.VisibleForTesting; +import androidx.fragment.app.Fragment; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.util.Log; -import android.util.SparseArray; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.Settings; import com.android.settings.applications.manageapplications.ManageApplications; @@ -76,7 +77,7 @@ public class StorageItemPreferenceController extends AbstractPreferenceControlle static final String FILES_KEY = "pref_files"; private final Fragment mFragment; - private final MetricsFeatureProvider mMetricsFeatureProvider; + private final MetricsFeatureProvider mMetricsFeatureProvider; private final StorageVolumeProvider mSvp; private VolumeInfo mVolume; private int mUserId; @@ -154,7 +155,7 @@ public class StorageItemPreferenceController extends AbstractPreferenceControlle case FILES_KEY: intent = getFilesIntent(); FeatureFactory.getFactory(mContext).getMetricsFeatureProvider().action( - mContext, MetricsEvent.STORAGE_FILES); + mContext, SettingsEnums.STORAGE_FILES); break; case SYSTEM_KEY: final SystemInfoFragment dialog = new SystemInfoFragment(); @@ -224,7 +225,7 @@ public class StorageItemPreferenceController extends AbstractPreferenceControlle private static Drawable applyTint(Context context, Drawable icon) { TypedArray array = - context.obtainStyledAttributes(new int[] {android.R.attr.colorControlNormal}); + context.obtainStyledAttributes(new int[]{android.R.attr.colorControlNormal}); icon = icon.mutate(); icon.setTint(array.getColor(0, 0)); array.recycle(); @@ -234,13 +235,13 @@ public class StorageItemPreferenceController extends AbstractPreferenceControlle @Override public void displayPreference(PreferenceScreen screen) { mScreen = screen; - mPhotoPreference = (StorageItemPreference) screen.findPreference(PHOTO_KEY); - mAudioPreference = (StorageItemPreference) screen.findPreference(AUDIO_KEY); - mGamePreference = (StorageItemPreference) screen.findPreference(GAME_KEY); - mMoviesPreference = (StorageItemPreference) screen.findPreference(MOVIES_KEY); - mAppPreference = (StorageItemPreference) screen.findPreference(OTHER_APPS_KEY); - mSystemPreference = (StorageItemPreference) screen.findPreference(SYSTEM_KEY); - mFilePreference = (StorageItemPreference) screen.findPreference(FILES_KEY); + mPhotoPreference = screen.findPreference(PHOTO_KEY); + mAudioPreference = screen.findPreference(AUDIO_KEY); + mGamePreference = screen.findPreference(GAME_KEY); + mMoviesPreference = screen.findPreference(MOVIES_KEY); + mAppPreference = screen.findPreference(OTHER_APPS_KEY); + mSystemPreference = screen.findPreference(SYSTEM_KEY); + mFilePreference = screen.findPreference(FILES_KEY); setFilesPreferenceVisibility(); } @@ -321,7 +322,7 @@ public class StorageItemPreferenceController extends AbstractPreferenceControlle ManageApplications.STORAGE_TYPE_PHOTOS_VIDEOS); return new SubSettingLauncher(mContext) .setDestination(ManageApplications.class.getName()) - .setTitle(R.string.storage_photos_videos) + .setTitleRes(R.string.storage_photos_videos) .setArguments(args) .setSourceMetricsCategory(mMetricsFeatureProvider.getMetricsCategory(mFragment)) .toIntent(); @@ -340,7 +341,7 @@ public class StorageItemPreferenceController extends AbstractPreferenceControlle args.putInt(ManageApplications.EXTRA_STORAGE_TYPE, ManageApplications.STORAGE_TYPE_MUSIC); return new SubSettingLauncher(mContext) .setDestination(ManageApplications.class.getName()) - .setTitle(R.string.storage_music_audio) + .setTitleRes(R.string.storage_music_audio) .setArguments(args) .setSourceMetricsCategory(mMetricsFeatureProvider.getMetricsCategory(mFragment)) .toIntent(); @@ -357,7 +358,7 @@ public class StorageItemPreferenceController extends AbstractPreferenceControlle args.putString(ManageApplications.EXTRA_VOLUME_NAME, mVolume.getDescription()); return new SubSettingLauncher(mContext) .setDestination(ManageApplications.class.getName()) - .setTitle(R.string.apps_storage) + .setTitleRes(R.string.apps_storage) .setArguments(args) .setSourceMetricsCategory(mMetricsFeatureProvider.getMetricsCategory(mFragment)) .toIntent(); @@ -369,7 +370,7 @@ public class StorageItemPreferenceController extends AbstractPreferenceControlle Settings.GamesStorageActivity.class.getName()); return new SubSettingLauncher(mContext) .setDestination(ManageApplications.class.getName()) - .setTitle(R.string.game_storage_settings) + .setTitleRes(R.string.game_storage_settings) .setArguments(args) .setSourceMetricsCategory(mMetricsFeatureProvider.getMetricsCategory(mFragment)) .toIntent(); @@ -381,7 +382,7 @@ public class StorageItemPreferenceController extends AbstractPreferenceControlle Settings.MoviesStorageActivity.class.getName()); return new SubSettingLauncher(mContext) .setDestination(ManageApplications.class.getName()) - .setTitle(R.string.storage_movies_tv) + .setTitleRes(R.string.storage_movies_tv) .setArguments(args) .setSourceMetricsCategory(mMetricsFeatureProvider.getMetricsCategory(mFragment)) .toIntent(); @@ -402,10 +403,14 @@ public class StorageItemPreferenceController extends AbstractPreferenceControlle try { final int userId = intent.getIntExtra(Intent.EXTRA_USER_ID, -1); + // b/33117269: Note that launchIntent may launch activity in different task which set + // different launchMode (e.g. Files), using startActivityForesult to set task as + // source task, and set requestCode as 0 means don't care about returnCode currently. if (userId == -1) { - mFragment.startActivity(intent); + mFragment.startActivityForResult(intent, 0 /* requestCode not used */); } else { - mFragment.getActivity().startActivityAsUser(intent, new UserHandle(userId)); + mFragment.getActivity().startActivityForResultAsUser(intent, + 0 /* requestCode not used */, new UserHandle(userId)); } } catch (ActivityNotFoundException e) { Log.w(TAG, "No activity found for " + intent); diff --git a/src/com/android/settings/deviceinfo/storage/StorageSummaryDonutPreference.java b/src/com/android/settings/deviceinfo/storage/StorageSummaryDonutPreference.java index 813261f50b..cf50596026 100644 --- a/src/com/android/settings/deviceinfo/storage/StorageSummaryDonutPreference.java +++ b/src/com/android/settings/deviceinfo/storage/StorageSummaryDonutPreference.java @@ -16,32 +16,24 @@ package com.android.settings.deviceinfo.storage; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; import android.graphics.Typeface; import android.os.storage.StorageManager; -import android.provider.Settings; -import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; -import android.text.SpannableString; -import android.text.Spanned; import android.text.TextPaint; -import android.text.TextUtils; import android.text.style.StyleSpan; import android.util.AttributeSet; -import android.util.MathUtils; import android.view.View; import android.widget.Button; -import android.widget.TextView; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + import com.android.settings.R; import com.android.settings.overlay.FeatureFactory; import com.android.settings.widget.DonutView; -import java.text.NumberFormat; -import java.util.Locale; - /** * StorageSummaryDonutPreference is a preference which summarizes the used and remaining storage left * on a given storage volume. It is visualized with a donut graphing the % used. @@ -89,7 +81,7 @@ public class StorageSummaryDonutPreference extends Preference implements View.On if (v != null && R.id.deletion_helper_button == v.getId()) { Context context = getContext(); FeatureFactory.getFactory(context).getMetricsFeatureProvider().action( - context, MetricsEvent.STORAGE_FREE_UP_SPACE_NOW); + context, SettingsEnums.STORAGE_FREE_UP_SPACE_NOW); Intent intent = new Intent(StorageManager.ACTION_MANAGE_STORAGE); getContext().startActivity(intent); } diff --git a/src/com/android/settings/deviceinfo/storage/StorageSummaryDonutPreferenceController.java b/src/com/android/settings/deviceinfo/storage/StorageSummaryDonutPreferenceController.java index f4c33be5ae..d450a2aeaa 100644 --- a/src/com/android/settings/deviceinfo/storage/StorageSummaryDonutPreferenceController.java +++ b/src/com/android/settings/deviceinfo/storage/StorageSummaryDonutPreferenceController.java @@ -18,11 +18,12 @@ package com.android.settings.deviceinfo.storage; import android.content.Context; import android.os.storage.VolumeInfo; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; import android.text.TextUtils; import android.text.format.Formatter; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.core.AbstractPreferenceController; @@ -42,9 +43,22 @@ public class StorageSummaryDonutPreferenceController extends AbstractPreferenceC super(context); } + /** + * Converts a used storage amount to a formatted text. + * + * @param usedBytes used bytes of storage + * @return a formatted text. + */ + public static CharSequence convertUsedBytesToFormattedText(Context context, long usedBytes) { + final Formatter.BytesResult result = Formatter.formatBytes(context.getResources(), + usedBytes, 0); + return TextUtils.expandTemplate(context.getText(R.string.storage_size_large_alternate), + result.value, result.units); + } + @Override public void displayPreference(PreferenceScreen screen) { - mSummary = (StorageSummaryDonutPreference) screen.findPreference("pref_summary"); + mSummary = screen.findPreference("pref_summary"); mSummary.setEnabled(true); } @@ -52,11 +66,7 @@ public class StorageSummaryDonutPreferenceController extends AbstractPreferenceC public void updateState(Preference preference) { super.updateState(preference); StorageSummaryDonutPreference summary = (StorageSummaryDonutPreference) preference; - final Formatter.BytesResult result = Formatter.formatBytes(mContext.getResources(), - mUsedBytes, 0); - summary.setTitle(TextUtils.expandTemplate( - mContext.getText(R.string.storage_size_large_alternate), result.value, - result.units)); + summary.setTitle(convertUsedBytesToFormattedText(mContext, mUsedBytes)); summary.setSummary(mContext.getString(R.string.storage_volume_total, Formatter.formatShortFileSize(mContext, mTotalBytes))); summary.setPercent(mUsedBytes, mTotalBytes); @@ -82,7 +92,8 @@ public class StorageSummaryDonutPreferenceController extends AbstractPreferenceC /** * Updates the state of the donut preference for the next update. - * @param used Total number of used bytes on the summarized volume. + * + * @param used Total number of used bytes on the summarized volume. * @param total Total number of bytes on the summarized volume. */ public void updateBytes(long used, long total) { @@ -93,6 +104,7 @@ public class StorageSummaryDonutPreferenceController extends AbstractPreferenceC /** * Updates the state of the donut preference for the next update using volume to summarize. + * * @param volume VolumeInfo to use to populate the informayion. */ public void updateSizes(StorageVolumeProvider svp, VolumeInfo volume) { diff --git a/src/com/android/settings/deviceinfo/storage/UserIconLoader.java b/src/com/android/settings/deviceinfo/storage/UserIconLoader.java index d1c29dfe38..8273e684ca 100644 --- a/src/com/android/settings/deviceinfo/storage/UserIconLoader.java +++ b/src/com/android/settings/deviceinfo/storage/UserIconLoader.java @@ -20,17 +20,16 @@ import android.content.Context; import android.content.pm.UserInfo; import android.graphics.drawable.Drawable; import android.os.UserManager; -import android.util.Log; import android.util.SparseArray; import com.android.internal.util.Preconditions; import com.android.settings.Utils; -import com.android.settingslib.utils.AsyncLoader; +import com.android.settingslib.utils.AsyncLoaderCompat; /** * Fetches a user icon as a loader using a given icon loading lambda. */ -public class UserIconLoader extends AsyncLoader<SparseArray<Drawable>> { +public class UserIconLoader extends AsyncLoaderCompat<SparseArray<Drawable>> { private FetchUserIconTask mTask; /** diff --git a/src/com/android/settings/deviceinfo/storage/UserProfileController.java b/src/com/android/settings/deviceinfo/storage/UserProfileController.java index 1778d13f32..a2e19db235 100644 --- a/src/com/android/settings/deviceinfo/storage/UserProfileController.java +++ b/src/com/android/settings/deviceinfo/storage/UserProfileController.java @@ -16,16 +16,17 @@ package com.android.settings.deviceinfo.storage; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.pm.UserInfo; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.storage.VolumeInfo; +import android.util.SparseArray; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.util.SparseArray; -import com.android.internal.logging.nano.MetricsProto; import com.android.internal.util.Preconditions; import com.android.settings.Utils; import com.android.settings.core.PreferenceControllerMixin; @@ -82,8 +83,8 @@ public class UserProfileController extends AbstractPreferenceController implemen new SubSettingLauncher(mContext) .setDestination(StorageProfileFragment.class.getName()) .setArguments(args) - .setTitle(mUser.name) - .setSourceMetricsCategory(MetricsProto.MetricsEvent.DEVICEINFO_STORAGE) + .setTitleText(mUser.name) + .setSourceMetricsCategory(SettingsEnums.DEVICEINFO_STORAGE) .launch(); return true; } @@ -130,7 +131,7 @@ public class UserProfileController extends AbstractPreferenceController implemen private static Drawable applyTint(Context context, Drawable icon) { icon = icon.mutate(); - icon.setTint(Utils.getColorAttr(context, android.R.attr.colorControlNormal)); + icon.setTintList(Utils.getColorAttr(context, android.R.attr.colorControlNormal)); return icon; } diff --git a/src/com/android/settings/deviceinfo/storage/VolumeSizesLoader.java b/src/com/android/settings/deviceinfo/storage/VolumeSizesLoader.java index a9b047c58b..d95befaa3a 100644 --- a/src/com/android/settings/deviceinfo/storage/VolumeSizesLoader.java +++ b/src/com/android/settings/deviceinfo/storage/VolumeSizesLoader.java @@ -19,15 +19,16 @@ package com.android.settings.deviceinfo.storage; import android.app.usage.StorageStatsManager; import android.content.Context; import android.os.storage.VolumeInfo; + import androidx.annotation.VisibleForTesting; import com.android.settingslib.deviceinfo.PrivateStorageInfo; import com.android.settingslib.deviceinfo.StorageVolumeProvider; -import com.android.settingslib.utils.AsyncLoader; +import com.android.settingslib.utils.AsyncLoaderCompat; import java.io.IOException; -public class VolumeSizesLoader extends AsyncLoader<PrivateStorageInfo> { +public class VolumeSizesLoader extends AsyncLoaderCompat<PrivateStorageInfo> { private StorageVolumeProvider mVolumeProvider; private StorageStatsManager mStats; private VolumeInfo mVolume; diff --git a/src/com/android/settings/display/AdaptiveSleepDetailPreferenceController.java b/src/com/android/settings/display/AdaptiveSleepDetailPreferenceController.java new file mode 100644 index 0000000000..5ce7be5b43 --- /dev/null +++ b/src/com/android/settings/display/AdaptiveSleepDetailPreferenceController.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2019 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.display; + +import android.content.Context; + +import androidx.preference.Preference; + +public class AdaptiveSleepDetailPreferenceController extends AdaptiveSleepPreferenceController { + public AdaptiveSleepDetailPreferenceController(Context context, String key) { + super(context, key); + } + + @Override + @AvailabilityStatus + public int getAvailabilityStatus() { + return mContext.getResources().getBoolean( + com.android.internal.R.bool.config_adaptive_sleep_available) + ? AVAILABLE + : UNSUPPORTED_ON_DEVICE; + } + + @Override + public boolean isSliceable() { + return true; + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + preference.setEnabled(super.hasSufficientPermissions); + } +} diff --git a/src/com/android/settings/display/AdaptiveSleepPreferenceController.java b/src/com/android/settings/display/AdaptiveSleepPreferenceController.java new file mode 100644 index 0000000000..6b91792800 --- /dev/null +++ b/src/com/android/settings/display/AdaptiveSleepPreferenceController.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2019 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.display; + +import static android.provider.Settings.System.ADAPTIVE_SLEEP; + +import android.Manifest; +import android.content.Context; +import android.content.pm.PackageManager; +import android.provider.Settings; + +import com.android.settings.R; +import com.android.settings.core.TogglePreferenceController; + + +public class AdaptiveSleepPreferenceController extends TogglePreferenceController { + + private final String SYSTEM_KEY = ADAPTIVE_SLEEP; + private final int DEFAULT_VALUE = 0; + + final boolean hasSufficientPermissions; + + public AdaptiveSleepPreferenceController(Context context, String key) { + super(context, key); + + final PackageManager packageManager = mContext.getPackageManager(); + final String attentionPackage = packageManager.getAttentionServicePackageName(); + hasSufficientPermissions = attentionPackage != null && packageManager.checkPermission( + Manifest.permission.CAMERA, attentionPackage) == PackageManager.PERMISSION_GRANTED; + } + + @Override + public boolean isChecked() { + return hasSufficientPermissions && Settings.System.getInt(mContext.getContentResolver(), + SYSTEM_KEY, DEFAULT_VALUE) != DEFAULT_VALUE; + } + + + @Override + public boolean setChecked(boolean isChecked) { + Settings.System.putInt(mContext.getContentResolver(), SYSTEM_KEY, + isChecked ? 1 : DEFAULT_VALUE); + return true; + } + + @Override + @AvailabilityStatus + public int getAvailabilityStatus() { + return mContext.getResources().getBoolean( + com.android.internal.R.bool.config_adaptive_sleep_available) + ? AVAILABLE_UNSEARCHABLE + : UNSUPPORTED_ON_DEVICE; + } + + @Override + public CharSequence getSummary() { + return mContext.getText(isChecked() + ? R.string.adaptive_sleep_summary_on + : R.string.adaptive_sleep_summary_off); + } +} diff --git a/src/com/android/settings/display/AdaptiveSleepSettings.java b/src/com/android/settings/display/AdaptiveSleepSettings.java new file mode 100644 index 0000000000..4c17a67b71 --- /dev/null +++ b/src/com/android/settings/display/AdaptiveSleepSettings.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2019 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.display; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.os.Bundle; +import android.provider.SearchIndexableResource; + +import com.android.settings.R; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settingslib.search.SearchIndexable; +import com.android.settingslib.widget.FooterPreference; + +import java.util.Arrays; +import java.util.List; + +@SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC) +public class AdaptiveSleepSettings extends DashboardFragment { + + private static final String TAG = "AdaptiveSleepSettings"; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + final FooterPreference footerPreference = + mFooterPreferenceMixin.createFooterPreference(); + footerPreference.setIcon(R.drawable.ic_privacy_shield_24dp); + footerPreference.setTitle(R.string.adaptive_sleep_privacy); + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.adaptive_sleep_detail; + } + + @Override + protected String getLogTag() { + return TAG; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.SETTINGS_ADAPTIVE_SLEEP; + } + + @Override + public int getHelpResource() { + return R.string.help_url_adaptive_sleep; + } + + public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex( + Context context, boolean enabled) { + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.adaptive_sleep_detail; + return Arrays.asList(sir); + } + }; +} diff --git a/src/com/android/settings/display/AmbientDisplayAlwaysOnPreferenceController.java b/src/com/android/settings/display/AmbientDisplayAlwaysOnPreferenceController.java index e91ddbece9..6a9e9fc2f7 100644 --- a/src/com/android/settings/display/AmbientDisplayAlwaysOnPreferenceController.java +++ b/src/com/android/settings/display/AmbientDisplayAlwaysOnPreferenceController.java @@ -16,17 +16,12 @@ package com.android.settings.display; import android.content.Context; -import android.content.Intent; +import android.hardware.display.AmbientDisplayConfiguration; import android.os.UserHandle; import android.provider.Settings; import android.text.TextUtils; -import com.android.internal.hardware.AmbientDisplayConfiguration; -import com.android.settings.R; import com.android.settings.core.TogglePreferenceController; -import com.android.settings.search.DatabaseIndexingUtils; -import com.android.settings.search.InlineSwitchPayload; -import com.android.settings.search.ResultPayload; public class AmbientDisplayAlwaysOnPreferenceController extends TogglePreferenceController { @@ -48,10 +43,7 @@ public class AmbientDisplayAlwaysOnPreferenceController extends TogglePreference @Override public int getAvailabilityStatus() { - if (mConfig == null) { - mConfig = new AmbientDisplayConfiguration(mContext); - } - return isAvailable(mConfig) ? AVAILABLE : UNSUPPORTED_ON_DEVICE; + return isAvailable(getConfig()) ? AVAILABLE : UNSUPPORTED_ON_DEVICE; } @Override @@ -61,7 +53,7 @@ public class AmbientDisplayAlwaysOnPreferenceController extends TogglePreference @Override public boolean isChecked() { - return mConfig.alwaysOnEnabled(MY_USER); + return getConfig().alwaysOnEnabled(MY_USER); } @Override @@ -87,26 +79,14 @@ public class AmbientDisplayAlwaysOnPreferenceController extends TogglePreference return this; } - public static boolean isAlwaysOnEnabled(AmbientDisplayConfiguration config) { - return config.alwaysOnEnabled(MY_USER); - } - public static boolean isAvailable(AmbientDisplayConfiguration config) { return config.alwaysOnAvailableForUser(MY_USER); } - public static boolean accessibilityInversionEnabled(AmbientDisplayConfiguration config) { - return config.accessibilityInversionEnabled(MY_USER); - } - - @Override - public ResultPayload getResultPayload() { - final Intent intent = DatabaseIndexingUtils.buildSearchResultPageIntent(mContext, - AmbientDisplaySettings.class.getName(), getPreferenceKey(), - mContext.getString(R.string.ambient_display_screen_title)); - - return new InlineSwitchPayload(Settings.Secure.DOZE_ALWAYS_ON, - ResultPayload.SettingsSource.SECURE, ON /* onValue */, intent, isAvailable(), - ON /* defaultValue */); + private AmbientDisplayConfiguration getConfig() { + if (mConfig == null) { + mConfig = new AmbientDisplayConfiguration(mContext); + } + return mConfig; } } diff --git a/src/com/android/settings/display/AmbientDisplayNotificationsPreferenceController.java b/src/com/android/settings/display/AmbientDisplayNotificationsPreferenceController.java index 0da5dbeb76..daaf7b15f1 100644 --- a/src/com/android/settings/display/AmbientDisplayNotificationsPreferenceController.java +++ b/src/com/android/settings/display/AmbientDisplayNotificationsPreferenceController.java @@ -15,23 +15,18 @@ package com.android.settings.display; import static android.provider.Settings.Secure.DOZE_ENABLED; -import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_AMBIENT_DISPLAY; - +import android.app.settings.SettingsEnums; import android.content.Context; -import android.content.Intent; +import android.hardware.display.AmbientDisplayConfiguration; import android.os.UserHandle; import android.provider.Settings; +import android.text.TextUtils; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; -import android.text.TextUtils; -import com.android.internal.hardware.AmbientDisplayConfiguration; -import com.android.settings.R; import com.android.settings.core.TogglePreferenceController; import com.android.settings.overlay.FeatureFactory; -import com.android.settings.search.DatabaseIndexingUtils; -import com.android.settings.search.InlineSwitchPayload; -import com.android.settings.search.ResultPayload; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; public class AmbientDisplayNotificationsPreferenceController extends @@ -66,14 +61,14 @@ public class AmbientDisplayNotificationsPreferenceController extends @Override public boolean handlePreferenceTreeClick(Preference preference) { if (KEY_AMBIENT_DISPLAY_NOTIFICATIONS.equals(preference.getKey())) { - mMetricsFeatureProvider.action(mContext, ACTION_AMBIENT_DISPLAY); + mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_AMBIENT_DISPLAY); } return false; } @Override public boolean isChecked() { - return mConfig.pulseOnNotificationEnabled(MY_USER); + return getAmbientConfig().pulseOnNotificationEnabled(MY_USER); } @Override @@ -84,10 +79,8 @@ public class AmbientDisplayNotificationsPreferenceController extends @Override public int getAvailabilityStatus() { - if (mConfig == null) { - mConfig = new AmbientDisplayConfiguration(mContext); - } - return mConfig.pulseOnNotificationAvailable() ? AVAILABLE : UNSUPPORTED_ON_DEVICE; + return getAmbientConfig().pulseOnNotificationAvailable() + ? AVAILABLE : UNSUPPORTED_ON_DEVICE; } @Override @@ -95,15 +88,11 @@ public class AmbientDisplayNotificationsPreferenceController extends return TextUtils.equals(getPreferenceKey(), "ambient_display_notification"); } - @Override - //TODO (b/69808376): Remove result payload - public ResultPayload getResultPayload() { - final Intent intent = DatabaseIndexingUtils.buildSearchResultPageIntent(mContext, - AmbientDisplaySettings.class.getName(), KEY_AMBIENT_DISPLAY_NOTIFICATIONS, - mContext.getString(R.string.ambient_display_screen_title)); - - return new InlineSwitchPayload(Settings.Secure.DOZE_ENABLED, - ResultPayload.SettingsSource.SECURE, ON /* onValue */, intent, isAvailable(), - ON /* defaultValue */); + private AmbientDisplayConfiguration getAmbientConfig() { + if (mConfig == null) { + mConfig = new AmbientDisplayConfiguration(mContext); + } + + return mConfig; } } diff --git a/src/com/android/settings/display/AmbientDisplayPreferenceController.java b/src/com/android/settings/display/AmbientDisplayPreferenceController.java deleted file mode 100644 index f429b0dfe5..0000000000 --- a/src/com/android/settings/display/AmbientDisplayPreferenceController.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2017 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.display; - -import android.content.Context; -import android.os.UserHandle; -import androidx.preference.Preference; - -import com.android.internal.hardware.AmbientDisplayConfiguration; -import com.android.settings.R; -import com.android.settings.core.PreferenceControllerMixin; -import com.android.settingslib.core.AbstractPreferenceController; - -public class AmbientDisplayPreferenceController extends AbstractPreferenceController implements - PreferenceControllerMixin { - - private static final int MY_USER_ID = UserHandle.myUserId(); - - private final AmbientDisplayConfiguration mConfig; - private final String mKey; - - public AmbientDisplayPreferenceController(Context context, AmbientDisplayConfiguration config, - String key) { - super(context); - mConfig = config; - mKey = key; - } - - @Override - public boolean isAvailable() { - return mConfig.available(); - } - - @Override - public void updateState(Preference preference) { - super.updateState(preference); - if (mConfig.alwaysOnEnabled(MY_USER_ID)) { - preference.setSummary(R.string.ambient_display_screen_summary_always_on); - } else if (mConfig.pulseOnNotificationEnabled(MY_USER_ID)) { - preference.setSummary(R.string.ambient_display_screen_summary_notifications); - } else if (mConfig.enabled(MY_USER_ID)) { - preference.setSummary(R.string.switch_on_text); - } else { - preference.setSummary(R.string.switch_off_text); - } - } - - @Override - public String getPreferenceKey() { - return mKey; - } -} diff --git a/src/com/android/settings/display/AppGridView.java b/src/com/android/settings/display/AppGridView.java index 0027537528..cda14458d0 100644 --- a/src/com/android/settings/display/AppGridView.java +++ b/src/com/android/settings/display/AppGridView.java @@ -22,7 +22,6 @@ import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.graphics.drawable.Drawable; import android.os.UserHandle; -import androidx.annotation.VisibleForTesting; import android.util.AttributeSet; import android.util.IconDrawableFactory; import android.view.View; @@ -31,6 +30,8 @@ import android.widget.ArrayAdapter; import android.widget.GridView; import android.widget.ImageView; +import androidx.annotation.VisibleForTesting; + import com.android.settings.R; import java.util.ArrayList; @@ -39,26 +40,28 @@ import java.util.List; public class AppGridView extends GridView { public AppGridView(Context context) { - this(context, null); + super(context); + init(context); } public AppGridView(Context context, AttributeSet attrs) { - this(context, attrs, 0); + super(context, attrs); + init(context); } public AppGridView(Context context, AttributeSet attrs, int defStyleAttr) { - this(context, attrs, defStyleAttr, 0); + super(context, attrs, defStyleAttr); + init(context); } public AppGridView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleResId) { super(context, attrs, defStyleAttr, defStyleResId); - setNumColumns(AUTO_FIT); + init(context); - final int columnWidth = getResources().getDimensionPixelSize( - R.dimen.screen_zoom_preview_app_icon_width); - setColumnWidth(columnWidth); + } + private void init(Context context) { setAdapter(new AppsAdapter(context, R.layout.screen_zoom_preview_app_icon, android.R.id.text1, android.R.id.icon1)); } @@ -105,6 +108,7 @@ public class AppGridView extends GridView { } private void loadAllApps() { + final int needAppCount = 6; final Intent mainIntent = new Intent(Intent.ACTION_MAIN, null); mainIntent.addCategory(Intent.CATEGORY_LAUNCHER); @@ -117,6 +121,9 @@ public class AppGridView extends GridView { if (label != null) { results.add(new ActivityEntry(info, label.toString(), iconFactory)); } + if (results.size() >= needAppCount) { + break; + } } Collections.sort(results); diff --git a/src/com/android/settings/display/AutoBrightnessDetailPreferenceController.java b/src/com/android/settings/display/AutoBrightnessDetailPreferenceController.java new file mode 100644 index 0000000000..a04ddbf553 --- /dev/null +++ b/src/com/android/settings/display/AutoBrightnessDetailPreferenceController.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2019 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.display; + +import android.content.Context; + +public class AutoBrightnessDetailPreferenceController extends AutoBrightnessPreferenceController { + + public AutoBrightnessDetailPreferenceController(Context context, String key) { + super(context, key); + } + + @Override + @AvailabilityStatus + public int getAvailabilityStatus() { + return mContext.getResources().getBoolean( + com.android.internal.R.bool.config_automatic_brightness_available) + ? AVAILABLE : UNSUPPORTED_ON_DEVICE; + } + + @Override + public boolean isSliceable() { + return true; + } +} diff --git a/src/com/android/settings/display/AutoBrightnessPreferenceController.java b/src/com/android/settings/display/AutoBrightnessPreferenceController.java index 7c93d28590..0018d84868 100644 --- a/src/com/android/settings/display/AutoBrightnessPreferenceController.java +++ b/src/com/android/settings/display/AutoBrightnessPreferenceController.java @@ -13,21 +13,15 @@ */ package com.android.settings.display; +import static android.provider.Settings.System.SCREEN_BRIGHTNESS_MODE; +import static android.provider.Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC; +import static android.provider.Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL; + import android.content.Context; -import android.content.Intent; import android.provider.Settings; -import android.text.TextUtils; -import com.android.settings.DisplaySettings; -import com.android.settings.core.TogglePreferenceController; -import com.android.settings.search.DatabaseIndexingUtils; -import com.android.settings.search.InlineSwitchPayload; -import com.android.settings.search.ResultPayload; import com.android.settings.R; - -import static android.provider.Settings.System.SCREEN_BRIGHTNESS_MODE; -import static android.provider.Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC; -import static android.provider.Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL; +import com.android.settings.core.TogglePreferenceController; public class AutoBrightnessPreferenceController extends TogglePreferenceController { @@ -57,24 +51,14 @@ public class AutoBrightnessPreferenceController extends TogglePreferenceControll public int getAvailabilityStatus() { return mContext.getResources().getBoolean( com.android.internal.R.bool.config_automatic_brightness_available) - ? AVAILABLE + ? AVAILABLE_UNSEARCHABLE : UNSUPPORTED_ON_DEVICE; } @Override - public boolean isSliceable() { - return TextUtils.equals(getPreferenceKey(), "auto_brightness"); - } - - @Override - public ResultPayload getResultPayload() { - // TODO remove result payload - final Intent intent = DatabaseIndexingUtils.buildSearchResultPageIntent(mContext, - DisplaySettings.class.getName(), getPreferenceKey(), - mContext.getString(R.string.display_settings)); - - return new InlineSwitchPayload(SYSTEM_KEY, - ResultPayload.SettingsSource.SYSTEM, SCREEN_BRIGHTNESS_MODE_AUTOMATIC, intent, - isAvailable(), DEFAULT_VALUE); + public CharSequence getSummary() { + return mContext.getText(isChecked() + ? R.string.auto_brightness_summary_on + : R.string.auto_brightness_summary_off); } -}
\ No newline at end of file +} diff --git a/src/com/android/settings/display/AutoBrightnessSettings.java b/src/com/android/settings/display/AutoBrightnessSettings.java index 778acf6880..b0ab6b0f12 100644 --- a/src/com/android/settings/display/AutoBrightnessSettings.java +++ b/src/com/android/settings/display/AutoBrightnessSettings.java @@ -16,18 +16,20 @@ package com.android.settings.display; +import android.app.settings.SettingsEnums; import android.content.Context; import android.os.Bundle; import android.provider.SearchIndexableResource; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settingslib.search.SearchIndexable; import java.util.Arrays; import java.util.List; +@SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC) public class AutoBrightnessSettings extends DashboardFragment { private static final String TAG = "AutoBrightnessSettings"; @@ -51,7 +53,7 @@ public class AutoBrightnessSettings extends DashboardFragment { @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.SETTINGS_AUTO_BRIGHTNESS; + return SettingsEnums.SETTINGS_AUTO_BRIGHTNESS; } @Override diff --git a/src/com/android/settings/display/AutoRotatePreferenceController.java b/src/com/android/settings/display/AutoRotatePreferenceController.java index 6bdf75aebf..f444d8ad61 100644 --- a/src/com/android/settings/display/AutoRotatePreferenceController.java +++ b/src/com/android/settings/display/AutoRotatePreferenceController.java @@ -13,11 +13,12 @@ */ package com.android.settings.display; +import android.app.settings.SettingsEnums; import android.content.Context; -import androidx.preference.Preference; import android.text.TextUtils; -import com.android.internal.logging.nano.MetricsProto; +import androidx.preference.Preference; + import com.android.internal.view.RotationPolicy; import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.core.TogglePreferenceController; @@ -88,7 +89,7 @@ public class AutoRotatePreferenceController extends TogglePreferenceController i @Override public boolean setChecked(boolean isChecked) { final boolean isLocked = !isChecked; - mMetricsFeatureProvider.action(mContext, MetricsProto.MetricsEvent.ACTION_ROTATION_LOCK, + mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_ROTATION_LOCK, isLocked); RotationPolicy.setRotationLock(mContext, isLocked); return true; diff --git a/src/com/android/settings/display/BatteryPercentagePreferenceController.java b/src/com/android/settings/display/BatteryPercentagePreferenceController.java index 8ef8425bc5..e1bbbfc58f 100644 --- a/src/com/android/settings/display/BatteryPercentagePreferenceController.java +++ b/src/com/android/settings/display/BatteryPercentagePreferenceController.java @@ -15,39 +15,34 @@ */ package com.android.settings.display; +import static android.provider.Settings.System.SHOW_BATTERY_PERCENT; + import android.content.Context; import android.provider.Settings; + import androidx.preference.Preference; import androidx.preference.SwitchPreference; import com.android.internal.R; +import com.android.settings.core.BasePreferenceController; import com.android.settings.core.PreferenceControllerMixin; -import com.android.settingslib.core.AbstractPreferenceController; - -import static android.provider.Settings.System.SHOW_BATTERY_PERCENT; /** * A controller to manage the switch for showing battery percentage in the status bar. */ -public class BatteryPercentagePreferenceController extends AbstractPreferenceController implements +public class BatteryPercentagePreferenceController extends BasePreferenceController implements PreferenceControllerMixin, Preference.OnPreferenceChangeListener { - private static final String KEY_BATTERY_PERCENTAGE = "battery_percentage"; - - public BatteryPercentagePreferenceController(Context context) { - super(context); - } - - @Override - public boolean isAvailable() { - return mContext.getResources() - .getBoolean(R.bool.config_battery_percentage_setting_available); + public BatteryPercentagePreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); } @Override - public String getPreferenceKey() { - return KEY_BATTERY_PERCENTAGE; + public int getAvailabilityStatus() { + return mContext.getResources().getBoolean( + R.bool.config_battery_percentage_setting_available) ? AVAILABLE + : UNSUPPORTED_ON_DEVICE; } @Override diff --git a/src/com/android/settings/display/BrightnessLevelPreferenceController.java b/src/com/android/settings/display/BrightnessLevelPreferenceController.java index 07882d8488..dc24ed0188 100644 --- a/src/com/android/settings/display/BrightnessLevelPreferenceController.java +++ b/src/com/android/settings/display/BrightnessLevelPreferenceController.java @@ -28,10 +28,11 @@ import android.os.ServiceManager; import android.provider.Settings; import android.provider.Settings.System; import android.service.vr.IVrManager; +import android.util.Log; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.util.Log; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.core.AbstractPreferenceController; diff --git a/src/com/android/settings/display/CameraGesturePreferenceController.java b/src/com/android/settings/display/CameraGesturePreferenceController.java index 9b1b2c296c..8e72a55227 100644 --- a/src/com/android/settings/display/CameraGesturePreferenceController.java +++ b/src/com/android/settings/display/CameraGesturePreferenceController.java @@ -13,17 +13,18 @@ */ package com.android.settings.display; +import static android.provider.Settings.Secure.CAMERA_GESTURE_DISABLED; + import android.content.Context; import android.os.SystemProperties; import android.provider.Settings; -import androidx.preference.SwitchPreference; + import androidx.preference.Preference; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.core.AbstractPreferenceController; -import static android.provider.Settings.Secure.CAMERA_GESTURE_DISABLED; - public class CameraGesturePreferenceController extends AbstractPreferenceController implements PreferenceControllerMixin, Preference.OnPreferenceChangeListener { diff --git a/src/com/android/settings/display/ColorModePreferenceController.java b/src/com/android/settings/display/ColorModePreferenceController.java index ebb4370098..234cc98859 100644 --- a/src/com/android/settings/display/ColorModePreferenceController.java +++ b/src/com/android/settings/display/ColorModePreferenceController.java @@ -14,86 +14,49 @@ package com.android.settings.display; import android.content.Context; -import android.os.IBinder; -import android.os.Parcel; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.util.Log; +import android.hardware.display.ColorDisplayManager; + +import androidx.annotation.VisibleForTesting; -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.app.ColorDisplayController; import com.android.settings.R; import com.android.settings.core.BasePreferenceController; public class ColorModePreferenceController extends BasePreferenceController { - private static final String TAG = "ColorModePreference"; - private static final String KEY_COLOR_MODE = "color_mode"; - - private static final int SURFACE_FLINGER_TRANSACTION_QUERY_WIDE_COLOR = 1024; - private final ConfigurationWrapper mConfigWrapper; - private ColorDisplayController mColorDisplayController; + private ColorDisplayManager mColorDisplayManager; - public ColorModePreferenceController(Context context) { - super(context, KEY_COLOR_MODE); - mConfigWrapper = new ConfigurationWrapper(); + public ColorModePreferenceController(Context context, String key) { + super(context, key); } @Override public int getAvailabilityStatus() { - return mConfigWrapper.isScreenWideColorGamut() - && !getColorDisplayController().getAccessibilityTransformActivated() ? - AVAILABLE : DISABLED_FOR_USER; + return mContext.getSystemService(ColorDisplayManager.class) + .isDeviceColorManaged() + && !ColorDisplayManager.areAccessibilityTransformsEnabled(mContext) ? + AVAILABLE_UNSEARCHABLE : DISABLED_FOR_USER; } @Override public CharSequence getSummary() { - final int colorMode = getColorDisplayController().getColorMode(); - if (colorMode == ColorDisplayController.COLOR_MODE_AUTOMATIC) { + final int colorMode = getColorDisplayManager().getColorMode(); + if (colorMode == ColorDisplayManager.COLOR_MODE_AUTOMATIC) { return mContext.getText(R.string.color_mode_option_automatic); } - if (colorMode == ColorDisplayController.COLOR_MODE_SATURATED) { + if (colorMode == ColorDisplayManager.COLOR_MODE_SATURATED) { return mContext.getText(R.string.color_mode_option_saturated); } - if (colorMode == ColorDisplayController.COLOR_MODE_BOOSTED) { + if (colorMode == ColorDisplayManager.COLOR_MODE_BOOSTED) { return mContext.getText(R.string.color_mode_option_boosted); } return mContext.getText(R.string.color_mode_option_natural); } @VisibleForTesting - ColorDisplayController getColorDisplayController() { - if (mColorDisplayController == null) { - mColorDisplayController = new ColorDisplayController(mContext); - } - return mColorDisplayController; - } - - @VisibleForTesting - static class ConfigurationWrapper { - private final IBinder mSurfaceFlinger; - - ConfigurationWrapper() { - mSurfaceFlinger = ServiceManager.getService("SurfaceFlinger"); - } - - boolean isScreenWideColorGamut() { - if (mSurfaceFlinger != null) { - final Parcel data = Parcel.obtain(); - final Parcel reply = Parcel.obtain(); - data.writeInterfaceToken("android.ui.ISurfaceComposer"); - try { - mSurfaceFlinger.transact(SURFACE_FLINGER_TRANSACTION_QUERY_WIDE_COLOR, - data, reply, 0); - return reply.readBoolean(); - } catch (RemoteException ex) { - Log.e(TAG, "Failed to query wide color support", ex); - } finally { - data.recycle(); - reply.recycle(); - } - } - return false; + ColorDisplayManager getColorDisplayManager() { + if (mColorDisplayManager == null) { + mColorDisplayManager = mContext.getSystemService(ColorDisplayManager.class); } + return mColorDisplayManager; } } diff --git a/src/com/android/settings/display/ColorModePreferenceFragment.java b/src/com/android/settings/display/ColorModePreferenceFragment.java index ab17c72051..c28de731ff 100644 --- a/src/com/android/settings/display/ColorModePreferenceFragment.java +++ b/src/com/android/settings/display/ColorModePreferenceFragment.java @@ -13,25 +13,36 @@ */ package com.android.settings.display; +import android.app.settings.SettingsEnums; +import android.content.ContentResolver; import android.content.Context; +import android.database.ContentObserver; import android.graphics.drawable.Drawable; +import android.hardware.display.ColorDisplayManager; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.provider.SearchIndexableResource; + +import android.provider.Settings.Secure; import androidx.annotation.VisibleForTesting; import androidx.preference.PreferenceScreen; -import com.android.internal.app.ColorDisplayController; -import com.android.internal.logging.nano.MetricsProto; - -import com.android.settings.applications.LayoutPreference; import com.android.settings.R; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; import com.android.settings.widget.RadioButtonPickerFragment; +import com.android.settingslib.search.SearchIndexable; import com.android.settingslib.widget.CandidateInfo; +import com.android.settingslib.widget.LayoutPreference; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; @SuppressWarnings("WeakerAccess") -public class ColorModePreferenceFragment extends RadioButtonPickerFragment - implements ColorDisplayController.Callback { +@SearchIndexable +public class ColorModePreferenceFragment extends RadioButtonPickerFragment { @VisibleForTesting static final String KEY_COLOR_MODE_NATURAL = "color_mode_natural"; @@ -42,21 +53,41 @@ public class ColorModePreferenceFragment extends RadioButtonPickerFragment @VisibleForTesting static final String KEY_COLOR_MODE_AUTOMATIC = "color_mode_automatic"; - private ColorDisplayController mController; + private ContentObserver mContentObserver; + private ColorDisplayManager mColorDisplayManager; @Override public void onAttach(Context context) { super.onAttach(context); - mController = new ColorDisplayController(context); - mController.setListener(this); + + mColorDisplayManager = context.getSystemService(ColorDisplayManager.class); + + final ContentResolver cr = context.getContentResolver(); + mContentObserver = new ContentObserver(new Handler(Looper.getMainLooper())) { + @Override + public void onChange(boolean selfChange, Uri uri) { + super.onChange(selfChange, uri); + if (ColorDisplayManager.areAccessibilityTransformsEnabled(getContext())) { + // Color modes are not configurable when Accessibility transforms are enabled. + // Close this fragment in that case. + getActivity().finish(); + } + } + }; + cr.registerContentObserver( + Secure.getUriFor(Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED), + false /* notifyForDescendants */, mContentObserver, mUserId); + cr.registerContentObserver( + Secure.getUriFor(Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED), + false /* notifyForDescendants */, mContentObserver, mUserId); } @Override public void onDetach() { super.onDetach(); - if (mController != null) { - mController.setListener(null); - mController = null; + if (mContentObserver != null) { + getContext().getContentResolver().unregisterContentObserver(mContentObserver); + mContentObserver = null; } } @@ -84,22 +115,22 @@ public class ColorModePreferenceFragment extends RadioButtonPickerFragment final int[] availableColorModes = c.getResources().getIntArray( com.android.internal.R.array.config_availableColorModes); - List<ColorModeCandidateInfo> candidates = new ArrayList<ColorModeCandidateInfo>(); + List<ColorModeCandidateInfo> candidates = new ArrayList<>(); if (availableColorModes != null) { for (int colorMode : availableColorModes) { - if (colorMode == ColorDisplayController.COLOR_MODE_NATURAL) { + if (colorMode == ColorDisplayManager.COLOR_MODE_NATURAL) { candidates.add(new ColorModeCandidateInfo( c.getText(R.string.color_mode_option_natural), KEY_COLOR_MODE_NATURAL, true /* enabled */)); - } else if (colorMode == ColorDisplayController.COLOR_MODE_BOOSTED) { + } else if (colorMode == ColorDisplayManager.COLOR_MODE_BOOSTED) { candidates.add(new ColorModeCandidateInfo( c.getText(R.string.color_mode_option_boosted), KEY_COLOR_MODE_BOOSTED, true /* enabled */)); - } else if (colorMode == ColorDisplayController.COLOR_MODE_SATURATED) { + } else if (colorMode == ColorDisplayManager.COLOR_MODE_SATURATED) { candidates.add(new ColorModeCandidateInfo( c.getText(R.string.color_mode_option_saturated), KEY_COLOR_MODE_SATURATED, true /* enabled */)); - } else if (colorMode == ColorDisplayController.COLOR_MODE_AUTOMATIC) { + } else if (colorMode == ColorDisplayManager.COLOR_MODE_AUTOMATIC) { candidates.add(new ColorModeCandidateInfo( c.getText(R.string.color_mode_option_automatic), KEY_COLOR_MODE_AUTOMATIC, true /* enabled */)); @@ -111,12 +142,12 @@ public class ColorModePreferenceFragment extends RadioButtonPickerFragment @Override protected String getDefaultKey() { - final int colorMode = mController.getColorMode(); - if (colorMode == ColorDisplayController.COLOR_MODE_AUTOMATIC) { + final int colorMode = mColorDisplayManager.getColorMode(); + if (colorMode == ColorDisplayManager.COLOR_MODE_AUTOMATIC) { return KEY_COLOR_MODE_AUTOMATIC; - } else if (colorMode == ColorDisplayController.COLOR_MODE_SATURATED) { + } else if (colorMode == ColorDisplayManager.COLOR_MODE_SATURATED) { return KEY_COLOR_MODE_SATURATED; - } else if (colorMode == ColorDisplayController.COLOR_MODE_BOOSTED) { + } else if (colorMode == ColorDisplayManager.COLOR_MODE_BOOSTED) { return KEY_COLOR_MODE_BOOSTED; } return KEY_COLOR_MODE_NATURAL; @@ -126,16 +157,16 @@ public class ColorModePreferenceFragment extends RadioButtonPickerFragment protected boolean setDefaultKey(String key) { switch (key) { case KEY_COLOR_MODE_NATURAL: - mController.setColorMode(ColorDisplayController.COLOR_MODE_NATURAL); + mColorDisplayManager.setColorMode(ColorDisplayManager.COLOR_MODE_NATURAL); break; case KEY_COLOR_MODE_BOOSTED: - mController.setColorMode(ColorDisplayController.COLOR_MODE_BOOSTED); + mColorDisplayManager.setColorMode(ColorDisplayManager.COLOR_MODE_BOOSTED); break; case KEY_COLOR_MODE_SATURATED: - mController.setColorMode(ColorDisplayController.COLOR_MODE_SATURATED); + mColorDisplayManager.setColorMode(ColorDisplayManager.COLOR_MODE_SATURATED); break; case KEY_COLOR_MODE_AUTOMATIC: - mController.setColorMode(ColorDisplayController.COLOR_MODE_AUTOMATIC); + mColorDisplayManager.setColorMode(ColorDisplayManager.COLOR_MODE_AUTOMATIC); break; } return true; @@ -143,7 +174,7 @@ public class ColorModePreferenceFragment extends RadioButtonPickerFragment @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.COLOR_MODE_SETTINGS; + return SettingsEnums.COLOR_MODE_SETTINGS; } @VisibleForTesting @@ -173,12 +204,14 @@ public class ColorModePreferenceFragment extends RadioButtonPickerFragment } } - @Override - public void onAccessibilityTransformChanged(boolean state) { - // Color modes are no not configurable when Accessibility transforms are enabled. Close - // this fragment in that case. - if (state) { - getActivity().onBackPressed(); - } - } + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex( + Context context, boolean enabled) { + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.color_mode_settings; + return Arrays.asList(sir); + } + }; } diff --git a/src/com/android/settings/display/ConversationMessageView.java b/src/com/android/settings/display/ConversationMessageView.java index b054444fe5..22e1f20550 100644 --- a/src/com/android/settings/display/ConversationMessageView.java +++ b/src/com/android/settings/display/ConversationMessageView.java @@ -29,6 +29,7 @@ import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.TextView; + import com.android.settings.R; /** diff --git a/src/com/android/settings/display/DarkUIInfoDialogFragment.java b/src/com/android/settings/display/DarkUIInfoDialogFragment.java new file mode 100644 index 0000000000..ce517e8db2 --- /dev/null +++ b/src/com/android/settings/display/DarkUIInfoDialogFragment.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2019 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.display; + +import android.app.Dialog; +import android.app.UiModeManager; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.provider.Settings; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; + +import com.android.settings.R; +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; + +public class DarkUIInfoDialogFragment extends InstrumentedDialogFragment + implements DialogInterface.OnClickListener{ + + @Override + public int getMetricsCategory() { + return SettingsEnums.DIALOG_DARK_UI_INFO; + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + Context context = getContext(); + AlertDialog.Builder dialog = new AlertDialog.Builder(context); + LayoutInflater inflater = LayoutInflater.from(dialog.getContext()); + View titleView = inflater.inflate(R.layout.settings_dialog_title, null); + ((ImageView) titleView.findViewById(R.id.settings_icon)) + .setImageDrawable(context.getDrawable(R.drawable.dark_theme)); + ((TextView) titleView.findViewById(R.id.settings_title)).setText(R.string.dark_ui_mode); + + dialog.setCustomTitle(titleView) + .setMessage(R.string.dark_ui_settings_dark_summary) + .setPositiveButton( + R.string.dark_ui_settings_dialog_acknowledge, + this); + return dialog.create(); + } + + @Override + public void onDismiss(@NonNull DialogInterface dialog) { + enableDarkTheme(); + super.onDismiss(dialog); + } + + @Override + public void onClick(DialogInterface dialogInterface, int i) { + // We have to manually dismiss the dialog because changing night mode causes it to + // recreate itself. + dialogInterface.dismiss(); + enableDarkTheme(); + } + + private void enableDarkTheme() { + final Context context = getContext(); + if (context != null) { + Settings.Secure.putInt(context.getContentResolver(), + Settings.Secure.DARK_MODE_DIALOG_SEEN, + DarkUIPreferenceController.DIALOG_SEEN); + context.getSystemService(UiModeManager.class) + .setNightMode(UiModeManager.MODE_NIGHT_YES); + } + } +} diff --git a/src/com/android/settings/display/DarkUIPreferenceController.java b/src/com/android/settings/display/DarkUIPreferenceController.java new file mode 100644 index 0000000000..d3d30b5022 --- /dev/null +++ b/src/com/android/settings/display/DarkUIPreferenceController.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2018 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.display; + +import android.app.UiModeManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.PowerManager; +import android.provider.Settings; + +import androidx.annotation.VisibleForTesting; +import androidx.fragment.app.Fragment; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreference; + +import com.android.settings.R; +import com.android.settings.core.TogglePreferenceController; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.core.lifecycle.events.OnStop; + +public class DarkUIPreferenceController extends TogglePreferenceController implements + LifecycleObserver, OnStart, OnStop { + + public static final String DARK_MODE_PREFS = "dark_mode_prefs"; + public static final String PREF_DARK_MODE_DIALOG_SEEN = "dark_mode_dialog_seen"; + public static final int DIALOG_SEEN = 1; + + @VisibleForTesting + SwitchPreference mPreference; + + private UiModeManager mUiModeManager; + private PowerManager mPowerManager; + private Context mContext; + + private Fragment mFragment; + + private BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + updateEnabledStateIfNeeded(); + } + }; + + public DarkUIPreferenceController(Context context, String key) { + super(context, key); + mContext = context; + mUiModeManager = context.getSystemService(UiModeManager.class); + mPowerManager = context.getSystemService(PowerManager.class); + } + + @Override + public boolean isChecked() { + return mUiModeManager.getNightMode() == UiModeManager.MODE_NIGHT_YES; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + updateEnabledStateIfNeeded(); + } + + @Override + public boolean setChecked(boolean isChecked) { + final boolean dialogSeen = + Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.DARK_MODE_DIALOG_SEEN, 0) == DIALOG_SEEN; + if (!dialogSeen && isChecked) { + showDarkModeDialog(); + return false; + } + mUiModeManager.setNightMode(isChecked + ? UiModeManager.MODE_NIGHT_YES + : UiModeManager.MODE_NIGHT_NO); + return true; + } + + private void showDarkModeDialog() { + final DarkUIInfoDialogFragment frag = new DarkUIInfoDialogFragment(); + if (mFragment.getFragmentManager() != null) { + frag.show(mFragment.getFragmentManager(), getClass().getName()); + } + } + + @VisibleForTesting + void updateEnabledStateIfNeeded() { + if (mPreference == null) { + return; + } + boolean isBatterySaver = isPowerSaveMode(); + mPreference.setEnabled(!isBatterySaver); + if (isBatterySaver) { + int stringId = mUiModeManager.getNightMode() == UiModeManager.MODE_NIGHT_YES + ? R.string.dark_ui_mode_disabled_summary_dark_theme_on + : R.string.dark_ui_mode_disabled_summary_dark_theme_off; + mPreference.setSummary(mContext.getString(stringId)); + } else { + mPreference.setSummary(null); + } + } + + @VisibleForTesting + boolean isPowerSaveMode() { + return mPowerManager.isPowerSaveMode(); + } + + + @VisibleForTesting + void setUiModeManager(UiModeManager uiModeManager) { + mUiModeManager = uiModeManager; + } + + public void setParentFragment(Fragment fragment) { + mFragment = fragment; + } + + @Override + public void onStart() { + mContext.registerReceiver(mReceiver, + new IntentFilter(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED)); + } + + @Override + public void onStop() { + mContext.unregisterReceiver(mReceiver); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } +} diff --git a/src/com/android/settings/display/DensityPreference.java b/src/com/android/settings/display/DensityPreference.java index 581a3ee444..61c57294bf 100644 --- a/src/com/android/settings/display/DensityPreference.java +++ b/src/com/android/settings/display/DensityPreference.java @@ -26,13 +26,13 @@ import android.view.View; import android.widget.EditText; import com.android.settings.R; -import com.android.settingslib.CustomEditTextPreference; import com.android.settings.Utils; +import com.android.settingslib.CustomEditTextPreferenceCompat; import com.android.settingslib.display.DisplayDensityUtils; import java.text.NumberFormat; -public class DensityPreference extends CustomEditTextPreference { +public class DensityPreference extends CustomEditTextPreferenceCompat { private static final String TAG = "DensityPreference"; public DensityPreference(Context context, AttributeSet attrs) { diff --git a/src/com/android/settings/display/DisplayWhiteBalancePreferenceController.java b/src/com/android/settings/display/DisplayWhiteBalancePreferenceController.java new file mode 100644 index 0000000000..6fc0b0e10e --- /dev/null +++ b/src/com/android/settings/display/DisplayWhiteBalancePreferenceController.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2018 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.display; + +import android.app.ActivityManager; +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.hardware.display.ColorDisplayManager; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.os.UserHandle; +import android.provider.Settings.Secure; +import android.provider.Settings.System; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.core.lifecycle.events.OnStop; + +import com.android.settings.core.TogglePreferenceController; + +public class DisplayWhiteBalancePreferenceController extends TogglePreferenceController + implements LifecycleObserver, OnStart, OnStop { + + private ColorDisplayManager mColorDisplayManager; + @VisibleForTesting + ContentObserver mContentObserver; + private Preference mPreference; + + public DisplayWhiteBalancePreferenceController(Context context, String key) { + super(context, key); + } + + @Override + public int getAvailabilityStatus() { + return getColorDisplayManager().isDisplayWhiteBalanceAvailable(mContext) ? + AVAILABLE : DISABLED_FOR_USER; + } + + @Override + public boolean isChecked() { + return getColorDisplayManager().isDisplayWhiteBalanceEnabled(); + } + + @Override + public boolean setChecked(boolean isChecked) { + return getColorDisplayManager().setDisplayWhiteBalanceEnabled(isChecked); + } + + @Override + public void onStart() { + if (!isAvailable()) { + return; + } + + final ContentResolver cr = mContext.getContentResolver(); + mContentObserver = new ContentObserver(new Handler(Looper.getMainLooper())) { + @Override + public void onChange(boolean selfChange, Uri uri) { + super.onChange(selfChange, uri); + updateVisibility(); + } + }; + cr.registerContentObserver( + Secure.getUriFor(Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED), + false /* notifyForDescendants */, mContentObserver, + ActivityManager.getCurrentUser()); + cr.registerContentObserver( + Secure.getUriFor(Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED), + false /* notifyForDescendants */, mContentObserver, + ActivityManager.getCurrentUser()); + cr.registerContentObserver( + System.getUriFor(System.DISPLAY_COLOR_MODE), + false /* notifyForDescendants */, mContentObserver, + ActivityManager.getCurrentUser()); + + updateVisibility(); + } + + @Override + public void onStop() { + if (mContentObserver != null) { + mContext.getContentResolver().unregisterContentObserver(mContentObserver); + mContentObserver = null; + } + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + } + + @VisibleForTesting + ColorDisplayManager getColorDisplayManager() { + if (mColorDisplayManager == null) { + mColorDisplayManager = mContext.getSystemService(ColorDisplayManager.class); + } + return mColorDisplayManager; + } + + @VisibleForTesting + void updateVisibility() { + if (mPreference != null) { + ColorDisplayManager cdm = getColorDisplayManager(); + + // Display white balance is only valid in linear light space. COLOR_MODE_SATURATED + // implies unmanaged color mode, and hence unknown color processing conditions. + // We also disallow display white balance when color accessibility features are enabled. + mPreference.setVisible(cdm.getColorMode() != ColorDisplayManager.COLOR_MODE_SATURATED && + !cdm.areAccessibilityTransformsEnabled(mContext)); + } + } +} diff --git a/src/com/android/settings/display/FontSizePreferenceController.java b/src/com/android/settings/display/FontSizePreferenceController.java index 3d6b6b41d3..680e378cc4 100644 --- a/src/com/android/settings/display/FontSizePreferenceController.java +++ b/src/com/android/settings/display/FontSizePreferenceController.java @@ -18,7 +18,6 @@ import android.content.res.Resources; import android.provider.Settings; import com.android.settings.R; -import com.android.settings.accessibility.ToggleFontSizePreferenceFragment; import com.android.settings.core.BasePreferenceController; public class FontSizePreferenceController extends BasePreferenceController { @@ -29,7 +28,7 @@ public class FontSizePreferenceController extends BasePreferenceController { @Override public int getAvailabilityStatus() { - return AVAILABLE; + return AVAILABLE_UNSEARCHABLE; } @Override diff --git a/src/com/android/settings/accessibility/FontSizePreferenceFragmentForSetupWizard.java b/src/com/android/settings/display/FontSizePreferenceFragmentForSetupWizard.java index 8dfb2b4992..04b6dc33f4 100644 --- a/src/com/android/settings/accessibility/FontSizePreferenceFragmentForSetupWizard.java +++ b/src/com/android/settings/display/FontSizePreferenceFragmentForSetupWizard.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2018 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. @@ -14,23 +14,23 @@ * limitations under the License. */ -package com.android.settings.accessibility; +package com.android.settings.display; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import android.app.settings.SettingsEnums; public class FontSizePreferenceFragmentForSetupWizard extends ToggleFontSizePreferenceFragment { @Override public int getMetricsCategory() { - return MetricsEvent.SUW_ACCESSIBILITY_FONT_SIZE; + return SettingsEnums.SUW_ACCESSIBILITY_FONT_SIZE; } @Override public void onStop() { // Log the final choice in value if it's different from the previous value. if (mCurrentIndex != mInitialIndex) { - mMetricsFeatureProvider.action(getContext(), MetricsEvent.SUW_ACCESSIBILITY_FONT_SIZE, + mMetricsFeatureProvider.action(getContext(), SettingsEnums.SUW_ACCESSIBILITY_FONT_SIZE, mCurrentIndex); } diff --git a/src/com/android/settings/display/LiftToWakePreferenceController.java b/src/com/android/settings/display/LiftToWakePreferenceController.java index 42feda2c96..d12b7c4c15 100644 --- a/src/com/android/settings/display/LiftToWakePreferenceController.java +++ b/src/com/android/settings/display/LiftToWakePreferenceController.java @@ -13,18 +13,19 @@ */ package com.android.settings.display; +import static android.provider.Settings.Secure.WAKE_GESTURE_ENABLED; + import android.content.Context; import android.hardware.Sensor; import android.hardware.SensorManager; import android.provider.Settings; -import androidx.preference.SwitchPreference; + import androidx.preference.Preference; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.core.AbstractPreferenceController; -import static android.provider.Settings.Secure.WAKE_GESTURE_ENABLED; - public class LiftToWakePreferenceController extends AbstractPreferenceController implements PreferenceControllerMixin, Preference.OnPreferenceChangeListener { diff --git a/src/com/android/settings/display/NightDisplayActivationPreferenceController.java b/src/com/android/settings/display/NightDisplayActivationPreferenceController.java index ffc73860c2..5bb70fc4e1 100644 --- a/src/com/android/settings/display/NightDisplayActivationPreferenceController.java +++ b/src/com/android/settings/display/NightDisplayActivationPreferenceController.java @@ -17,20 +17,22 @@ package com.android.settings.display; import android.content.Context; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; +import android.hardware.display.ColorDisplayManager; import android.text.TextUtils; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; -import com.android.internal.app.ColorDisplayController; + +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + import com.android.settings.R; -import com.android.settings.applications.LayoutPreference; import com.android.settings.core.TogglePreferenceController; +import com.android.settingslib.widget.LayoutPreference; public class NightDisplayActivationPreferenceController extends TogglePreferenceController { - private ColorDisplayController mController; + private ColorDisplayManager mColorDisplayManager; private NightDisplayTimeFormatter mTimeFormatter; private Button mTurnOffButton; private Button mTurnOnButton; @@ -38,20 +40,22 @@ public class NightDisplayActivationPreferenceController extends TogglePreference private final OnClickListener mListener = new OnClickListener() { @Override public void onClick(View v) { - mController.setActivated(!mController.isActivated()); + mColorDisplayManager.setNightDisplayActivated(!mColorDisplayManager.isNightDisplayActivated()); updateStateInternal(); } }; public NightDisplayActivationPreferenceController(Context context, String key) { super(context, key); - mController = new ColorDisplayController(context); + + mColorDisplayManager = context.getSystemService(ColorDisplayManager.class); mTimeFormatter = new NightDisplayTimeFormatter(context); } @Override public int getAvailabilityStatus() { - return ColorDisplayController.isAvailable(mContext) ? AVAILABLE : UNSUPPORTED_ON_DEVICE; + return ColorDisplayManager.isNightDisplayAvailable(mContext) ? AVAILABLE + : UNSUPPORTED_ON_DEVICE; } @Override @@ -63,8 +67,7 @@ public class NightDisplayActivationPreferenceController extends TogglePreference public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - final LayoutPreference preference = (LayoutPreference) screen.findPreference( - getPreferenceKey()); + final LayoutPreference preference = screen.findPreference(getPreferenceKey()); mTurnOnButton = preference.findViewById(R.id.night_display_turn_on_button); mTurnOnButton.setOnClickListener(mListener); mTurnOffButton = preference.findViewById(R.id.night_display_turn_off_button); @@ -80,17 +83,17 @@ public class NightDisplayActivationPreferenceController extends TogglePreference @Override public boolean isChecked() { - return mController.isActivated(); + return mColorDisplayManager.isNightDisplayActivated(); } @Override public boolean setChecked(boolean isChecked) { - return mController.setActivated(isChecked); + return mColorDisplayManager.setNightDisplayActivated(isChecked); } @Override public CharSequence getSummary() { - return mTimeFormatter.getAutoModeTimeSummary(mContext, mController); + return mTimeFormatter.getAutoModeSummary(mContext, mColorDisplayManager); } private void updateStateInternal() { @@ -98,18 +101,18 @@ public class NightDisplayActivationPreferenceController extends TogglePreference return; } - final boolean isActivated = mController.isActivated(); - final int autoMode = mController.getAutoMode(); + final boolean isActivated = mColorDisplayManager.isNightDisplayActivated(); + final int autoMode = mColorDisplayManager.getNightDisplayAutoMode(); String buttonText; - if (autoMode == ColorDisplayController.AUTO_MODE_CUSTOM) { + if (autoMode == ColorDisplayManager.AUTO_MODE_CUSTOM_TIME) { buttonText = mContext.getString(isActivated ? R.string.night_display_activation_off_custom : R.string.night_display_activation_on_custom, mTimeFormatter.getFormattedTimeString(isActivated - ? mController.getCustomStartTime() - : mController.getCustomEndTime())); - } else if (autoMode == ColorDisplayController.AUTO_MODE_TWILIGHT) { + ? mColorDisplayManager.getNightDisplayCustomStartTime() + : mColorDisplayManager.getNightDisplayCustomEndTime())); + } else if (autoMode == ColorDisplayManager.AUTO_MODE_TWILIGHT) { buttonText = mContext.getString(isActivated ? R.string.night_display_activation_off_twilight : R.string.night_display_activation_on_twilight); diff --git a/src/com/android/settings/display/NightDisplayAutoModePreferenceController.java b/src/com/android/settings/display/NightDisplayAutoModePreferenceController.java index bb42c857a7..121b062a0e 100644 --- a/src/com/android/settings/display/NightDisplayAutoModePreferenceController.java +++ b/src/com/android/settings/display/NightDisplayAutoModePreferenceController.java @@ -17,10 +17,12 @@ package com.android.settings.display; import android.content.Context; +import android.hardware.display.ColorDisplayManager; + import androidx.preference.DropDownPreference; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import com.android.internal.app.ColorDisplayController; + import com.android.settings.R; import com.android.settings.core.BasePreferenceController; @@ -28,23 +30,24 @@ public class NightDisplayAutoModePreferenceController extends BasePreferenceCont implements Preference.OnPreferenceChangeListener { private DropDownPreference mPreference; - private ColorDisplayController mController; + private ColorDisplayManager mColorDisplayManager; public NightDisplayAutoModePreferenceController(Context context, String key) { super(context, key); - mController = new ColorDisplayController(context); + mColorDisplayManager = context.getSystemService(ColorDisplayManager.class); } @Override public int getAvailabilityStatus() { - return ColorDisplayController.isAvailable(mContext) ? AVAILABLE : UNSUPPORTED_ON_DEVICE; + return ColorDisplayManager.isNightDisplayAvailable(mContext) ? AVAILABLE + : UNSUPPORTED_ON_DEVICE; } @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - mPreference = (DropDownPreference) screen.findPreference(getPreferenceKey()); + mPreference = screen.findPreference(getPreferenceKey()); mPreference.setEntries(new CharSequence[]{ mContext.getString(R.string.night_display_auto_mode_never), @@ -52,19 +55,19 @@ public class NightDisplayAutoModePreferenceController extends BasePreferenceCont mContext.getString(R.string.night_display_auto_mode_twilight) }); mPreference.setEntryValues(new CharSequence[]{ - String.valueOf(ColorDisplayController.AUTO_MODE_DISABLED), - String.valueOf(ColorDisplayController.AUTO_MODE_CUSTOM), - String.valueOf(ColorDisplayController.AUTO_MODE_TWILIGHT) + String.valueOf(ColorDisplayManager.AUTO_MODE_DISABLED), + String.valueOf(ColorDisplayManager.AUTO_MODE_CUSTOM_TIME), + String.valueOf(ColorDisplayManager.AUTO_MODE_TWILIGHT) }); } @Override public final void updateState(Preference preference) { - mPreference.setValue(String.valueOf(mController.getAutoMode())); + mPreference.setValue(String.valueOf(mColorDisplayManager.getNightDisplayAutoMode())); } @Override public final boolean onPreferenceChange(Preference preference, Object newValue) { - return mController.setAutoMode(Integer.parseInt((String) newValue)); + return mColorDisplayManager.setNightDisplayAutoMode(Integer.parseInt((String) newValue)); } } diff --git a/src/com/android/settings/display/NightDisplayCustomEndTimePreferenceController.java b/src/com/android/settings/display/NightDisplayCustomEndTimePreferenceController.java index 1eebb6be5a..0ebbeae4cc 100644 --- a/src/com/android/settings/display/NightDisplayCustomEndTimePreferenceController.java +++ b/src/com/android/settings/display/NightDisplayCustomEndTimePreferenceController.java @@ -17,31 +17,34 @@ package com.android.settings.display; import android.content.Context; +import android.hardware.display.ColorDisplayManager; import androidx.preference.Preference; -import com.android.internal.app.ColorDisplayController; import com.android.settings.core.BasePreferenceController; public class NightDisplayCustomEndTimePreferenceController extends BasePreferenceController { - private ColorDisplayController mController; + private ColorDisplayManager mColorDisplayManager; private NightDisplayTimeFormatter mTimeFormatter; public NightDisplayCustomEndTimePreferenceController(Context context, String key) { super(context, key); - mController = new ColorDisplayController(context); + mColorDisplayManager = context.getSystemService(ColorDisplayManager.class); mTimeFormatter = new NightDisplayTimeFormatter(context); } @Override public int getAvailabilityStatus() { - return ColorDisplayController.isAvailable(mContext) ? AVAILABLE : UNSUPPORTED_ON_DEVICE; + return ColorDisplayManager.isNightDisplayAvailable(mContext) ? AVAILABLE + : UNSUPPORTED_ON_DEVICE; } @Override public final void updateState(Preference preference) { - preference.setVisible(mController.getAutoMode() == ColorDisplayController.AUTO_MODE_CUSTOM); + preference + .setVisible(mColorDisplayManager.getNightDisplayAutoMode() + == ColorDisplayManager.AUTO_MODE_CUSTOM_TIME); preference.setSummary(mTimeFormatter.getFormattedTimeString( - mController.getCustomEndTime())); + mColorDisplayManager.getNightDisplayCustomEndTime())); } } diff --git a/src/com/android/settings/display/NightDisplayCustomStartTimePreferenceController.java b/src/com/android/settings/display/NightDisplayCustomStartTimePreferenceController.java index 19297b82f3..a2a85ef292 100644 --- a/src/com/android/settings/display/NightDisplayCustomStartTimePreferenceController.java +++ b/src/com/android/settings/display/NightDisplayCustomStartTimePreferenceController.java @@ -17,31 +17,34 @@ package com.android.settings.display; import android.content.Context; +import android.hardware.display.ColorDisplayManager; import androidx.preference.Preference; -import com.android.internal.app.ColorDisplayController; import com.android.settings.core.BasePreferenceController; public class NightDisplayCustomStartTimePreferenceController extends BasePreferenceController { - private ColorDisplayController mController; + private ColorDisplayManager mColorDisplayManager; private NightDisplayTimeFormatter mTimeFormatter; public NightDisplayCustomStartTimePreferenceController(Context context, String key) { super(context, key); - mController = new ColorDisplayController(context); + mColorDisplayManager = context.getSystemService(ColorDisplayManager.class); mTimeFormatter = new NightDisplayTimeFormatter(context); } @Override public int getAvailabilityStatus() { - return ColorDisplayController.isAvailable(mContext) ? AVAILABLE : UNSUPPORTED_ON_DEVICE; + return ColorDisplayManager.isNightDisplayAvailable(mContext) ? AVAILABLE + : UNSUPPORTED_ON_DEVICE; } @Override public final void updateState(Preference preference) { - preference.setVisible(mController.getAutoMode() == ColorDisplayController.AUTO_MODE_CUSTOM); + preference + .setVisible(mColorDisplayManager.getNightDisplayAutoMode() + == ColorDisplayManager.AUTO_MODE_CUSTOM_TIME); preference.setSummary(mTimeFormatter.getFormattedTimeString( - mController.getCustomStartTime())); + mColorDisplayManager.getNightDisplayCustomStartTime())); } } diff --git a/src/com/android/settings/display/NightDisplayFooterPreferenceController.java b/src/com/android/settings/display/NightDisplayFooterPreferenceController.java index 2075cfca32..194f006859 100644 --- a/src/com/android/settings/display/NightDisplayFooterPreferenceController.java +++ b/src/com/android/settings/display/NightDisplayFooterPreferenceController.java @@ -17,8 +17,10 @@ package com.android.settings.display; import android.content.Context; +import android.hardware.display.ColorDisplayManager; + import androidx.preference.Preference; -import com.android.internal.app.ColorDisplayController; + import com.android.settings.R; import com.android.settings.core.BasePreferenceController; import com.android.settingslib.widget.FooterPreference; @@ -31,7 +33,8 @@ public class NightDisplayFooterPreferenceController extends BasePreferenceContro @Override public int getAvailabilityStatus() { - return ColorDisplayController.isAvailable(mContext) ? AVAILABLE : UNSUPPORTED_ON_DEVICE; + return ColorDisplayManager.isNightDisplayAvailable(mContext) ? AVAILABLE + : UNSUPPORTED_ON_DEVICE; } @Override diff --git a/src/com/android/settings/display/NightDisplayIntensityPreferenceController.java b/src/com/android/settings/display/NightDisplayIntensityPreferenceController.java index 9f215d9b59..c8447cb17c 100644 --- a/src/com/android/settings/display/NightDisplayIntensityPreferenceController.java +++ b/src/com/android/settings/display/NightDisplayIntensityPreferenceController.java @@ -17,28 +17,29 @@ package com.android.settings.display; import android.content.Context; +import android.hardware.display.ColorDisplayManager; +import android.text.TextUtils; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.text.TextUtils; -import com.android.internal.app.ColorDisplayController; import com.android.settings.core.SliderPreferenceController; import com.android.settings.widget.SeekBarPreference; public class NightDisplayIntensityPreferenceController extends SliderPreferenceController { - private ColorDisplayController mController; + private ColorDisplayManager mColorDisplayManager; public NightDisplayIntensityPreferenceController(Context context, String key) { super(context, key); - mController = new ColorDisplayController(context); + mColorDisplayManager = context.getSystemService(ColorDisplayManager.class); } @Override public int getAvailabilityStatus() { - if (!ColorDisplayController.isAvailable(mContext)) { + if (!ColorDisplayManager.isNightDisplayAvailable(mContext)) { return UNSUPPORTED_ON_DEVICE; - } else if (!mController.isActivated()) { + } else if (!mColorDisplayManager.isNightDisplayActivated()) { return DISABLED_DEPENDENT_SETTING; } return AVAILABLE; @@ -52,31 +53,37 @@ public class NightDisplayIntensityPreferenceController extends SliderPreferenceC @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - final SeekBarPreference preference = (SeekBarPreference) screen.findPreference( - getPreferenceKey()); + final SeekBarPreference preference = screen.findPreference(getPreferenceKey()); preference.setContinuousUpdates(true); - preference.setMax(getMaxSteps()); + preference.setMax(getMax()); + preference.setMin(getMin()); } @Override public final void updateState(Preference preference) { super.updateState(preference); - preference.setEnabled(mController.isActivated()); + preference.setEnabled(mColorDisplayManager.isNightDisplayActivated()); } @Override public int getSliderPosition() { - return convertTemperature(mController.getColorTemperature()); + return convertTemperature(mColorDisplayManager.getNightDisplayColorTemperature()); } @Override public boolean setSliderPosition(int position) { - return mController.setColorTemperature(convertTemperature(position)); + return mColorDisplayManager.setNightDisplayColorTemperature(convertTemperature(position)); + } + + @Override + public int getMax() { + return convertTemperature(ColorDisplayManager.getMinimumColorTemperature(mContext)); } @Override - public int getMaxSteps() { - return convertTemperature(mController.getMinimumColorTemperature()); + public int getMin() { + // The min should always be 0 in this case. + return 0; } /** @@ -85,6 +92,6 @@ public class NightDisplayIntensityPreferenceController extends SliderPreferenceC * adjustment status of the input. */ private int convertTemperature(int temperature) { - return mController.getMaximumColorTemperature() - temperature; + return ColorDisplayManager.getMaximumColorTemperature(mContext) - temperature; } }
\ No newline at end of file diff --git a/src/com/android/settings/display/NightDisplayPreference.java b/src/com/android/settings/display/NightDisplayPreference.java index a1608d75e0..7020c49cec 100644 --- a/src/com/android/settings/display/NightDisplayPreference.java +++ b/src/com/android/settings/display/NightDisplayPreference.java @@ -15,23 +15,26 @@ package com.android.settings.display; import android.content.Context; -import androidx.preference.SwitchPreference; +import android.hardware.display.ColorDisplayManager; +import android.hardware.display.NightDisplayListener; import android.util.AttributeSet; -import com.android.internal.app.ColorDisplayController; +import androidx.preference.SwitchPreference; import java.time.LocalTime; public class NightDisplayPreference extends SwitchPreference - implements ColorDisplayController.Callback { + implements NightDisplayListener.Callback { - private ColorDisplayController mController; + private ColorDisplayManager mColorDisplayManager; + private NightDisplayListener mNightDisplayListener; private NightDisplayTimeFormatter mTimeFormatter; public NightDisplayPreference(Context context, AttributeSet attrs) { super(context, attrs); - mController = new ColorDisplayController(context); + mColorDisplayManager = context.getSystemService(ColorDisplayManager.class); + mNightDisplayListener = new NightDisplayListener(context); mTimeFormatter = new NightDisplayTimeFormatter(context); } @@ -40,7 +43,7 @@ public class NightDisplayPreference extends SwitchPreference super.onAttached(); // Listen for changes only while attached. - mController.setListener(this); + mNightDisplayListener.setCallback(this); // Update the summary since the state may have changed while not attached. updateSummary(); @@ -51,7 +54,7 @@ public class NightDisplayPreference extends SwitchPreference super.onDetached(); // Stop listening for state changes. - mController.setListener(null); + mNightDisplayListener.setCallback(null); } @Override @@ -75,6 +78,6 @@ public class NightDisplayPreference extends SwitchPreference } private void updateSummary() { - setSummary(mTimeFormatter.getAutoModeTimeSummary(getContext(), mController)); + setSummary(mTimeFormatter.getAutoModeTimeSummary(getContext(), mColorDisplayManager)); } } diff --git a/src/com/android/settings/display/NightDisplayPreferenceController.java b/src/com/android/settings/display/NightDisplayPreferenceController.java index 0bbab935f8..6891d65dd9 100644 --- a/src/com/android/settings/display/NightDisplayPreferenceController.java +++ b/src/com/android/settings/display/NightDisplayPreferenceController.java @@ -14,11 +14,11 @@ package com.android.settings.display; import android.content.Context; +import android.hardware.display.ColorDisplayManager; -import com.android.internal.app.ColorDisplayController; +import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.core.AbstractPreferenceController; -import com.android.settings.R; public class NightDisplayPreferenceController extends AbstractPreferenceController implements PreferenceControllerMixin { @@ -36,13 +36,13 @@ public class NightDisplayPreferenceController extends AbstractPreferenceControll if (!isEnabled) { return true; } - final ColorDisplayController controller = new ColorDisplayController(context); - return controller.getAutoMode() != ColorDisplayController.AUTO_MODE_DISABLED; + final ColorDisplayManager manager = context.getSystemService(ColorDisplayManager.class); + return manager.getNightDisplayAutoMode() != ColorDisplayManager.AUTO_MODE_DISABLED; } @Override public boolean isAvailable() { - return ColorDisplayController.isAvailable(mContext); + return ColorDisplayManager.isNightDisplayAvailable(mContext); } @Override diff --git a/src/com/android/settings/display/NightDisplaySettings.java b/src/com/android/settings/display/NightDisplaySettings.java index 848ddd51ae..6441d71650 100644 --- a/src/com/android/settings/display/NightDisplaySettings.java +++ b/src/com/android/settings/display/NightDisplaySettings.java @@ -18,18 +18,21 @@ package com.android.settings.display; import android.app.Dialog; import android.app.TimePickerDialog; +import android.app.settings.SettingsEnums; import android.content.Context; +import android.hardware.display.ColorDisplayManager; +import android.hardware.display.NightDisplayListener; import android.os.Bundle; import android.provider.SearchIndexableResource; + import androidx.preference.Preference; -import com.android.internal.app.ColorDisplayController; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.search.SearchIndexable; import java.time.LocalTime; import java.util.ArrayList; @@ -38,22 +41,25 @@ import java.util.List; /** * Settings screen for Night display. */ +@SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC) public class NightDisplaySettings extends DashboardFragment - implements ColorDisplayController.Callback, Indexable { + implements NightDisplayListener.Callback { private static final String TAG = "NightDisplaySettings"; private static final int DIALOG_START_TIME = 0; private static final int DIALOG_END_TIME = 1; - private ColorDisplayController mController; + private ColorDisplayManager mColorDisplayManager; + private NightDisplayListener mNightDisplayListener; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); final Context context = getContext(); - mController = new ColorDisplayController(context); + mColorDisplayManager = context.getSystemService(ColorDisplayManager.class); + mNightDisplayListener = new NightDisplayListener(context); } @Override @@ -61,7 +67,7 @@ public class NightDisplaySettings extends DashboardFragment super.onStart(); // Listen for changes only while visible. - mController.setListener(this); + mNightDisplayListener.setCallback(this); } @Override @@ -69,7 +75,7 @@ public class NightDisplaySettings extends DashboardFragment super.onStop(); // Stop listening for state changes. - mController.setListener(null); + mNightDisplayListener.setCallback(null); } @Override @@ -89,9 +95,9 @@ public class NightDisplaySettings extends DashboardFragment if (dialogId == DIALOG_START_TIME || dialogId == DIALOG_END_TIME) { final LocalTime initialTime; if (dialogId == DIALOG_START_TIME) { - initialTime = mController.getCustomStartTime(); + initialTime = mColorDisplayManager.getNightDisplayCustomStartTime(); } else { - initialTime = mController.getCustomEndTime(); + initialTime = mColorDisplayManager.getNightDisplayCustomEndTime(); } final Context context = getContext(); @@ -99,9 +105,9 @@ public class NightDisplaySettings extends DashboardFragment return new TimePickerDialog(context, (view, hourOfDay, minute) -> { final LocalTime time = LocalTime.of(hourOfDay, minute); if (dialogId == DIALOG_START_TIME) { - mController.setCustomStartTime(time); + mColorDisplayManager.setNightDisplayCustomStartTime(time); } else { - mController.setCustomEndTime(time); + mColorDisplayManager.setNightDisplayCustomEndTime(time); } }, initialTime.getHour(), initialTime.getMinute(), use24HourFormat); } @@ -112,9 +118,9 @@ public class NightDisplaySettings extends DashboardFragment public int getDialogMetricsCategory(int dialogId) { switch (dialogId) { case DIALOG_START_TIME: - return MetricsEvent.DIALOG_NIGHT_DISPLAY_SET_START_TIME; + return SettingsEnums.DIALOG_NIGHT_DISPLAY_SET_START_TIME; case DIALOG_END_TIME: - return MetricsEvent.DIALOG_NIGHT_DISPLAY_SET_END_TIME; + return SettingsEnums.DIALOG_NIGHT_DISPLAY_SET_END_TIME; default: return 0; } @@ -157,7 +163,7 @@ public class NightDisplaySettings extends DashboardFragment @Override public int getMetricsCategory() { - return MetricsEvent.NIGHT_DISPLAY_SETTINGS; + return SettingsEnums.NIGHT_DISPLAY_SETTINGS; } @Override @@ -175,7 +181,7 @@ public class NightDisplaySettings extends DashboardFragment return buildPreferenceControllers(context); } - private static List <AbstractPreferenceController> buildPreferenceControllers(Context context) { + private static List<AbstractPreferenceController> buildPreferenceControllers(Context context) { final List<AbstractPreferenceController> controllers = new ArrayList<>(1); controllers.add(new NightDisplayFooterPreferenceController(context)); return controllers; @@ -195,12 +201,12 @@ public class NightDisplaySettings extends DashboardFragment @Override protected boolean isPageSearchEnabled(Context context) { - return ColorDisplayController.isAvailable(context); + return ColorDisplayManager.isNightDisplayAvailable(context); } @Override public List<AbstractPreferenceController> createPreferenceControllers( - Context context) { + Context context) { return buildPreferenceControllers(context); } }; diff --git a/src/com/android/settings/display/NightDisplayTimeFormatter.java b/src/com/android/settings/display/NightDisplayTimeFormatter.java index 48a1994cee..1449ac1224 100644 --- a/src/com/android/settings/display/NightDisplayTimeFormatter.java +++ b/src/com/android/settings/display/NightDisplayTimeFormatter.java @@ -18,7 +18,7 @@ package com.android.settings.display; import android.content.Context; -import com.android.internal.app.ColorDisplayController; +import android.hardware.display.ColorDisplayManager; import com.android.settings.R; import java.text.DateFormat; @@ -45,24 +45,25 @@ public class NightDisplayTimeFormatter { return mTimeFormatter.format(c.getTime()); } - public String getAutoModeTimeSummary(Context context, ColorDisplayController controller) { - final int summaryFormatResId = controller.isActivated() ? R.string.night_display_summary_on - : R.string.night_display_summary_off; - return context.getString(summaryFormatResId, getAutoModeSummary(context, controller)); + public String getAutoModeTimeSummary(Context context, ColorDisplayManager manager) { + final int summaryFormatResId = + manager.isNightDisplayActivated() ? R.string.night_display_summary_on + : R.string.night_display_summary_off; + return context.getString(summaryFormatResId, getAutoModeSummary(context, manager)); } - private String getAutoModeSummary(Context context, ColorDisplayController controller) { - final boolean isActivated = controller.isActivated(); - final int autoMode = controller.getAutoMode(); - if (autoMode == ColorDisplayController.AUTO_MODE_CUSTOM) { + public String getAutoModeSummary(Context context, ColorDisplayManager manager) { + final boolean isActivated = manager.isNightDisplayActivated(); + final int autoMode = manager.getNightDisplayAutoMode(); + if (autoMode == ColorDisplayManager.AUTO_MODE_CUSTOM_TIME) { if (isActivated) { return context.getString(R.string.night_display_summary_on_auto_mode_custom, - getFormattedTimeString(controller.getCustomEndTime())); + getFormattedTimeString(manager.getNightDisplayCustomEndTime())); } else { return context.getString(R.string.night_display_summary_off_auto_mode_custom, - getFormattedTimeString(controller.getCustomStartTime())); + getFormattedTimeString(manager.getNightDisplayCustomStartTime())); } - } else if (autoMode == ColorDisplayController.AUTO_MODE_TWILIGHT) { + } else if (autoMode == ColorDisplayManager.AUTO_MODE_TWILIGHT) { return context.getString(isActivated ? R.string.night_display_summary_on_auto_mode_twilight : R.string.night_display_summary_off_auto_mode_twilight); diff --git a/src/com/android/settings/display/NightModePreferenceController.java b/src/com/android/settings/display/NightModePreferenceController.java index 41620ea470..a4d96f63b0 100644 --- a/src/com/android/settings/display/NightModePreferenceController.java +++ b/src/com/android/settings/display/NightModePreferenceController.java @@ -17,10 +17,11 @@ import static android.content.Context.UI_MODE_SERVICE; import android.app.UiModeManager; import android.content.Context; +import android.util.Log; + import androidx.preference.ListPreference; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.util.Log; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.core.AbstractPreferenceController; @@ -51,8 +52,7 @@ public class NightModePreferenceController extends AbstractPreferenceController setVisible(screen, KEY_NIGHT_MODE, false /* visible */); return; } - ListPreference mNightModePreference = (ListPreference) screen.findPreference( - KEY_NIGHT_MODE); + final ListPreference mNightModePreference = screen.findPreference(KEY_NIGHT_MODE); if (mNightModePreference != null) { final UiModeManager uiManager = (UiModeManager) mContext.getSystemService(UI_MODE_SERVICE); diff --git a/src/com/android/settings/PreviewPagerAdapter.java b/src/com/android/settings/display/PreviewPagerAdapter.java index b98ffcb1a1..018be326d1 100644 --- a/src/com/android/settings/PreviewPagerAdapter.java +++ b/src/com/android/settings/display/PreviewPagerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2018 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. @@ -13,24 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.settings; +package com.android.settings.display; import android.animation.Animator; import android.animation.Animator.AnimatorListener; import android.content.Context; import android.content.res.Configuration; -import androidx.viewpager.widget.PagerAdapter; -import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewStub; -import android.view.ViewStub.OnInflateListener; import android.view.animation.AccelerateInterpolator; import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; import android.widget.FrameLayout; import android.widget.LinearLayout; -import android.widget.ScrollView; + +import androidx.viewpager.widget.PagerAdapter; /** * A PagerAdapter used by PreviewSeekBarPreferenceFragment that for showing multiple preview screen @@ -69,23 +67,20 @@ public class PreviewPagerAdapter extends PagerAdapter { mPreviewFrames[p].setLayoutParams(new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT)); - + mPreviewFrames[p].setClipToPadding(true); + mPreviewFrames[p].setClipChildren(true); for (int j = 0; j < configurations.length; ++j) { // Create a new configuration for the specified value. It won't // have any theme set, so manually apply the current theme. final Context configContext = context.createConfigurationContext(configurations[j]); configContext.getTheme().setTo(context.getTheme()); - final LayoutInflater configInflater = LayoutInflater.from(configContext); final ViewStub sampleViewStub = new ViewStub(configContext); sampleViewStub.setLayoutResource(previewSampleResIds[i]); final int fi = i, fj = j; - sampleViewStub.setOnInflateListener(new OnInflateListener() { - @Override - public void onInflate(ViewStub stub, View inflated) { - inflated.setVisibility(stub.getVisibility()); - mViewStubInflated[fi][fj] = true; - } + sampleViewStub.setOnInflateListener((stub, inflated) -> { + inflated.setVisibility(stub.getVisibility()); + mViewStubInflated[fi][fj] = true; }); mPreviewFrames[p].addView(sampleViewStub); @@ -94,7 +89,7 @@ public class PreviewPagerAdapter extends PagerAdapter { } @Override - public void destroyItem (ViewGroup container, int position, Object object) { + public void destroyItem(ViewGroup container, int position, Object object) { container.removeView((View) object); } @@ -164,29 +159,29 @@ public class PreviewPagerAdapter extends PagerAdapter { if (visibility == View.VISIBLE) { // Fade in animation. view.animate() - .alpha(alpha) - .setInterpolator(FADE_IN_INTERPOLATOR) - .setDuration(CROSS_FADE_DURATION_MS) - .setListener(new PreviewFrameAnimatorListener()) - .withStartAction(new Runnable() { - @Override - public void run() { - view.setVisibility(visibility); - } - }); + .alpha(alpha) + .setInterpolator(FADE_IN_INTERPOLATOR) + .setDuration(CROSS_FADE_DURATION_MS) + .setListener(new PreviewFrameAnimatorListener()) + .withStartAction(new Runnable() { + @Override + public void run() { + view.setVisibility(visibility); + } + }); } else { // Fade out animation. view.animate() - .alpha(alpha) - .setInterpolator(FADE_OUT_INTERPOLATOR) - .setDuration(CROSS_FADE_DURATION_MS) - .setListener(new PreviewFrameAnimatorListener()) - .withEndAction(new Runnable() { - @Override - public void run() { - view.setVisibility(visibility); - } - }); + .alpha(alpha) + .setInterpolator(FADE_OUT_INTERPOLATOR) + .setDuration(CROSS_FADE_DURATION_MS) + .setListener(new PreviewFrameAnimatorListener()) + .withEndAction(new Runnable() { + @Override + public void run() { + view.setVisibility(visibility); + } + }); } } } diff --git a/src/com/android/settings/PreviewSeekBarPreferenceFragment.java b/src/com/android/settings/display/PreviewSeekBarPreferenceFragment.java index e031edb26c..bb9e3d7102 100644 --- a/src/com/android/settings/PreviewSeekBarPreferenceFragment.java +++ b/src/com/android/settings/display/PreviewSeekBarPreferenceFragment.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 The Android Open Source Project + * Copyright (C) 2018 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. @@ -14,22 +14,24 @@ * limitations under the License. */ -package com.android.settings; +package com.android.settings.display; import android.content.Context; import android.content.res.Configuration; import android.os.Bundle; -import androidx.viewpager.widget.ViewPager; -import androidx.viewpager.widget.ViewPager.OnPageChangeListener; import android.view.LayoutInflater; import android.view.View; -import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; import android.widget.SeekBar; import android.widget.SeekBar.OnSeekBarChangeListener; import android.widget.TextView; +import androidx.viewpager.widget.ViewPager; +import androidx.viewpager.widget.ViewPager.OnPageChangeListener; + +import com.android.settings.R; +import com.android.settings.SettingsPreferenceFragment; import com.android.settings.widget.DotsPageIndicator; import com.android.settings.widget.LabeledSeekBar; @@ -47,12 +49,6 @@ public abstract class PreviewSeekBarPreferenceFragment extends SettingsPreferenc /** Index of the entry corresponding to current value of the settings. */ protected int mCurrentIndex; - /** Resource id of the layout for this preference fragment. */ - protected int mActivityLayoutResId; - - /** Resource id of the layout that defines the contents inside preview screen. */ - protected int[] mPreviewSampleResIds; - private ViewPager mPreviewPager; private PreviewPagerAdapter mPreviewPagerAdapter; private DotsPageIndicator mPageIndicator; @@ -81,12 +77,7 @@ public abstract class PreviewSeekBarPreferenceFragment extends SettingsPreferenc @Override public void onStopTrackingTouch(SeekBar seekBar) { if (mPreviewPagerAdapter.isAnimating()) { - mPreviewPagerAdapter.setAnimationEndAction(new Runnable() { - @Override - public void run() { - commit(); - } - }); + mPreviewPagerAdapter.setAnimationEndAction(() -> commit()); } else { commit(); } @@ -96,44 +87,38 @@ public abstract class PreviewSeekBarPreferenceFragment extends SettingsPreferenc @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + Bundle savedInstanceState) { final View root = super.onCreateView(inflater, container, savedInstanceState); - final ViewGroup listContainer = (ViewGroup) root.findViewById(android.R.id.list_container); + final ViewGroup listContainer = root.findViewById(android.R.id.list_container); listContainer.removeAllViews(); - final View content = inflater.inflate(mActivityLayoutResId, listContainer, false); + final View content = inflater.inflate(getActivityLayoutResId(), listContainer, false); listContainer.addView(content); - mLabel = (TextView) content.findViewById(R.id.current_label); + mLabel = content.findViewById(R.id.current_label); // The maximum SeekBar value always needs to be non-zero. If there's // only one available value, we'll handle this by disabling the // seek bar. final int max = Math.max(1, mEntries.length - 1); - mSeekBar = (LabeledSeekBar) content.findViewById(R.id.seek_bar); + mSeekBar = content.findViewById(R.id.seek_bar); mSeekBar.setLabels(mEntries); mSeekBar.setMax(max); mSmaller = content.findViewById(R.id.smaller); - mSmaller.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - final int progress = mSeekBar.getProgress(); - if (progress > 0) { - mSeekBar.setProgress(progress - 1, true); - } + mSmaller.setOnClickListener(v -> { + final int progress = mSeekBar.getProgress(); + if (progress > 0) { + mSeekBar.setProgress(progress - 1, true); } }); mLarger = content.findViewById(R.id.larger); - mLarger.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - final int progress = mSeekBar.getProgress(); - if (progress < mSeekBar.getMax()) { - mSeekBar.setProgress(progress + 1, true); - } + mLarger.setOnClickListener(v -> { + final int progress = mSeekBar.getProgress(); + if (progress < mSeekBar.getMax()) { + mSeekBar.setProgress(progress + 1, true); } }); @@ -151,15 +136,16 @@ public abstract class PreviewSeekBarPreferenceFragment extends SettingsPreferenc configurations[i] = createConfig(origConfig, i); } - mPreviewPager = (ViewPager) content.findViewById(R.id.preview_pager); + final int[] previews = getPreviewSampleResIds(); + mPreviewPager = content.findViewById(R.id.preview_pager); mPreviewPagerAdapter = new PreviewPagerAdapter(context, isLayoutRtl, - mPreviewSampleResIds, configurations); + previews, configurations); mPreviewPager.setAdapter(mPreviewPagerAdapter); - mPreviewPager.setCurrentItem(isLayoutRtl ? mPreviewSampleResIds.length - 1 : 0); + mPreviewPager.setCurrentItem(isLayoutRtl ? previews.length - 1 : 0); mPreviewPager.addOnPageChangeListener(mPreviewPageChangeListener); - mPageIndicator = (DotsPageIndicator) content.findViewById(R.id.page_indicator); - if (mPreviewSampleResIds.length > 1) { + mPageIndicator = content.findViewById(R.id.page_indicator); + if (previews.length > 1) { mPageIndicator.setViewPager(mPreviewPager); mPageIndicator.setVisibility(View.VISIBLE); mPageIndicator.setOnPageChangeListener(mPageIndicatorPageChangeListener); @@ -186,6 +172,12 @@ public abstract class PreviewSeekBarPreferenceFragment extends SettingsPreferenc mSeekBar.setOnSeekBarChangeListener(null); } + /** Resource id of the layout for this preference fragment. */ + protected abstract int getActivityLayoutResId(); + + /** Resource id of the layout that defines the contents inside preview screen. */ + protected abstract int[] getPreviewSampleResIds(); + /** * Creates new configuration based on the current position of the SeekBar. */ @@ -209,8 +201,8 @@ public abstract class PreviewSeekBarPreferenceFragment extends SettingsPreferenc private void setPagerIndicatorContentDescription(int position) { mPageIndicator.setContentDescription( - getPrefContext().getString(R.string.preview_page_indicator_content_description, - position + 1, mPreviewSampleResIds.length)); + getString(R.string.preview_page_indicator_content_description, + position + 1, getPreviewSampleResIds().length)); } private OnPageChangeListener mPreviewPageChangeListener = new OnPageChangeListener() { diff --git a/src/com/android/settings/display/ScreenSaverPreferenceController.java b/src/com/android/settings/display/ScreenSaverPreferenceController.java index ae44bf3e18..c1b0b4e9eb 100644 --- a/src/com/android/settings/display/ScreenSaverPreferenceController.java +++ b/src/com/android/settings/display/ScreenSaverPreferenceController.java @@ -14,6 +14,7 @@ package com.android.settings.display; import android.content.Context; + import androidx.preference.Preference; import com.android.settings.core.PreferenceControllerMixin; diff --git a/src/com/android/settings/display/ScreenZoomPreference.java b/src/com/android/settings/display/ScreenZoomPreference.java index 8451a2d4e3..f47d7da438 100644 --- a/src/com/android/settings/display/ScreenZoomPreference.java +++ b/src/com/android/settings/display/ScreenZoomPreference.java @@ -17,11 +17,12 @@ package com.android.settings.display; import android.content.Context; -import androidx.core.content.res.TypedArrayUtils; -import androidx.preference.Preference; import android.text.TextUtils; import android.util.AttributeSet; +import androidx.core.content.res.TypedArrayUtils; +import androidx.preference.Preference; + import com.android.settingslib.display.DisplayDensityUtils; /** diff --git a/src/com/android/settings/display/ScreenZoomPreferenceFragmentForSetupWizard.java b/src/com/android/settings/display/ScreenZoomPreferenceFragmentForSetupWizard.java index 82cb58d3cd..cfa7b5d428 100644 --- a/src/com/android/settings/display/ScreenZoomPreferenceFragmentForSetupWizard.java +++ b/src/com/android/settings/display/ScreenZoomPreferenceFragmentForSetupWizard.java @@ -16,14 +16,13 @@ package com.android.settings.display; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import android.app.settings.SettingsEnums; -public class ScreenZoomPreferenceFragmentForSetupWizard - extends ScreenZoomSettings { +public class ScreenZoomPreferenceFragmentForSetupWizard extends ScreenZoomSettings { @Override public int getMetricsCategory() { - return MetricsEvent.SUW_ACCESSIBILITY_DISPLAY_SIZE; + return SettingsEnums.SUW_ACCESSIBILITY_DISPLAY_SIZE; } @Override @@ -31,7 +30,7 @@ public class ScreenZoomPreferenceFragmentForSetupWizard // Log the final choice in value if it's different from the previous value. if (mCurrentIndex != mInitialIndex) { mMetricsFeatureProvider.action( - getContext(), MetricsEvent.SUW_ACCESSIBILITY_DISPLAY_SIZE, mCurrentIndex); + getContext(), SettingsEnums.SUW_ACCESSIBILITY_DISPLAY_SIZE, mCurrentIndex); } super.onStop(); diff --git a/src/com/android/settings/display/ScreenZoomSettings.java b/src/com/android/settings/display/ScreenZoomSettings.java index 6b5216e7ab..ad6c68533e 100644 --- a/src/com/android/settings/display/ScreenZoomSettings.java +++ b/src/com/android/settings/display/ScreenZoomSettings.java @@ -17,19 +17,19 @@ package com.android.settings.display; import android.annotation.Nullable; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.os.Bundle; import android.view.Display; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.settings.PreviewSeekBarPreferenceFragment; import com.android.settings.R; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; import com.android.settings.search.SearchIndexableRaw; import com.android.settingslib.display.DisplayDensityUtils; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.List; @@ -37,21 +37,31 @@ import java.util.List; /** * Preference fragment used to control screen zoom. */ -public class ScreenZoomSettings extends PreviewSeekBarPreferenceFragment implements Indexable { +@SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC) +public class ScreenZoomSettings extends PreviewSeekBarPreferenceFragment { private int mDefaultDensity; private int[] mValues; @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); + protected int getActivityLayoutResId() { + return R.layout.screen_zoom_activity; + } - mActivityLayoutResId = R.layout.screen_zoom_activity; + @Override + protected int[] getPreviewSampleResIds() { + return getContext().getResources().getBoolean( + R.bool.config_enable_extra_screen_zoom_preview) + ? new int[]{ + R.layout.screen_zoom_preview_1, + R.layout.screen_zoom_preview_2, + R.layout.screen_zoom_preview_settings} + : new int[]{R.layout.screen_zoom_preview_1}; + } - // This should be replaced once the final preview sample screen is in place. - mPreviewSampleResIds = new int[] {R.layout.screen_zoom_preview_1, - R.layout.screen_zoom_preview_2, - R.layout.screen_zoom_preview_settings}; + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); final DisplayDensityUtils density = new DisplayDensityUtils(getContext()); @@ -61,8 +71,8 @@ public class ScreenZoomSettings extends PreviewSeekBarPreferenceFragment impleme // connect to the window manager service. Just use the current // density and don't let the user change anything. final int densityDpi = getResources().getDisplayMetrics().densityDpi; - mValues = new int[] {densityDpi}; - mEntries = new String[] {getString(DisplayDensityUtils.SUMMARY_DEFAULT)}; + mValues = new int[]{densityDpi}; + mEntries = new String[]{getString(DisplayDensityUtils.SUMMARY_DEFAULT)}; mInitialIndex = 0; mDefaultDensity = densityDpi; } else { @@ -103,11 +113,11 @@ public class ScreenZoomSettings extends PreviewSeekBarPreferenceFragment impleme @Override public int getMetricsCategory() { - return MetricsEvent.DISPLAY_SCREEN_ZOOM; + return SettingsEnums.DISPLAY_SCREEN_ZOOM; } /** Index provider used to expose this fragment in search. */ - public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = new BaseSearchIndexProvider() { @Override public List<SearchIndexableRaw> getRawDataToIndex(Context context, diff --git a/src/com/android/settings/display/ShowOperatorNamePreferenceController.java b/src/com/android/settings/display/ShowOperatorNamePreferenceController.java index cdb89fb4e1..40eaad330d 100644 --- a/src/com/android/settings/display/ShowOperatorNamePreferenceController.java +++ b/src/com/android/settings/display/ShowOperatorNamePreferenceController.java @@ -15,8 +15,9 @@ package com.android.settings.display; import android.content.Context; import android.provider.Settings; -import androidx.preference.SwitchPreference; + import androidx.preference.Preference; +import androidx.preference.SwitchPreference; import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; diff --git a/src/com/android/settings/display/SystemUiThemePreferenceController.java b/src/com/android/settings/display/SystemUiThemePreferenceController.java deleted file mode 100644 index 3419ece9dd..0000000000 --- a/src/com/android/settings/display/SystemUiThemePreferenceController.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (C) 2018 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.display; - -import static android.provider.Settings.Secure.THEME_MODE; - -import android.content.Context; -import android.provider.Settings; -import androidx.preference.ListPreference; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; -import android.util.FeatureFlagUtils; - -import com.android.settings.core.BasePreferenceController; -import com.android.settings.core.PreferenceControllerMixin; -import com.android.settingslib.core.AbstractPreferenceController; - -/** - * Setting where user can pick if SystemUI will be light, dark or try to match - * the wallpaper colors. - */ -public class SystemUiThemePreferenceController extends BasePreferenceController - implements Preference.OnPreferenceChangeListener { - - private ListPreference mSystemUiThemePref; - - public SystemUiThemePreferenceController(Context context, String preferenceKey) { - super(context, preferenceKey); - } - - @Override - public int getAvailabilityStatus() { - boolean enabled = FeatureFlagUtils.isEnabled(mContext, "settings_systemui_theme"); - return enabled ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; - } - - @Override - public void displayPreference(PreferenceScreen screen) { - super.displayPreference(screen); - mSystemUiThemePref = (ListPreference) screen.findPreference(getPreferenceKey()); - int value = Settings.Secure.getInt(mContext.getContentResolver(), THEME_MODE, 0); - mSystemUiThemePref.setValue(Integer.toString(value)); - } - - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - int value = Integer.parseInt((String) newValue); - Settings.Secure.putInt(mContext.getContentResolver(), THEME_MODE, value); - refreshSummary(preference); - return true; - } - - @Override - public CharSequence getSummary() { - int value = Settings.Secure.getInt(mContext.getContentResolver(), THEME_MODE, 0); - int index = mSystemUiThemePref.findIndexOfValue(Integer.toString(value)); - return mSystemUiThemePref.getEntries()[index]; - } -} diff --git a/src/com/android/settings/display/TapToWakePreferenceController.java b/src/com/android/settings/display/TapToWakePreferenceController.java index ff149ba838..5c2d97530d 100644 --- a/src/com/android/settings/display/TapToWakePreferenceController.java +++ b/src/com/android/settings/display/TapToWakePreferenceController.java @@ -15,8 +15,9 @@ package com.android.settings.display; import android.content.Context; import android.provider.Settings; -import androidx.preference.SwitchPreference; + import androidx.preference.Preference; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.core.AbstractPreferenceController; diff --git a/src/com/android/settings/display/ThemePreferenceController.java b/src/com/android/settings/display/ThemePreferenceController.java index 2cdf7e21c3..1dbf87f698 100644 --- a/src/com/android/settings/display/ThemePreferenceController.java +++ b/src/com/android/settings/display/ThemePreferenceController.java @@ -13,23 +13,26 @@ */ package com.android.settings.display; + +import android.app.settings.SettingsEnums; import android.content.Context; +import android.content.om.IOverlayManager; +import android.content.om.OverlayInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; +import android.text.TextUtils; + import androidx.annotation.VisibleForTesting; import androidx.preference.ListPreference; import androidx.preference.Preference; -import android.text.TextUtils; import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.overlay.FeatureFactory; -import com.android.settings.wrapper.OverlayManagerWrapper; -import com.android.settings.wrapper.OverlayManagerWrapper.OverlayInfo; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; @@ -37,24 +40,22 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; -import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_THEME; - public class ThemePreferenceController extends AbstractPreferenceController implements PreferenceControllerMixin, Preference.OnPreferenceChangeListener { private static final String KEY_THEME = "theme"; private final MetricsFeatureProvider mMetricsFeatureProvider; - private final OverlayManagerWrapper mOverlayService; + private final IOverlayManager mOverlayService; private final PackageManager mPackageManager; public ThemePreferenceController(Context context) { - this(context, ServiceManager.getService(Context.OVERLAY_SERVICE) != null - ? new OverlayManagerWrapper() : null); + this(context, IOverlayManager.Stub + .asInterface(ServiceManager.getService(Context.OVERLAY_SERVICE))); } @VisibleForTesting - ThemePreferenceController(Context context, OverlayManagerWrapper overlayManager) { + ThemePreferenceController(Context context, IOverlayManager overlayManager) { super(context); mOverlayService = overlayManager; mPackageManager = context.getPackageManager(); @@ -69,7 +70,7 @@ public class ThemePreferenceController extends AbstractPreferenceController impl @Override public boolean handlePreferenceTreeClick(Preference preference) { if (KEY_THEME.equals(preference.getKey())) { - mMetricsFeatureProvider.action(mContext, ACTION_THEME); + mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_THEME); } return false; } @@ -77,7 +78,7 @@ public class ThemePreferenceController extends AbstractPreferenceController impl @Override public void updateState(Preference preference) { ListPreference pref = (ListPreference) preference; - String[] pkgs = getAvailableThemes(); + String[] pkgs = getAvailableThemes(false /* currentThemeOnly */); CharSequence[] labels = new CharSequence[pkgs.length]; for (int i = 0; i < pkgs.length; i++) { try { @@ -109,11 +110,15 @@ public class ThemePreferenceController extends AbstractPreferenceController impl @Override public boolean onPreferenceChange(Preference preference, Object newValue) { - String current = getTheme(); + String current = getCurrentTheme(); if (Objects.equals(newValue, current)) { return true; } - mOverlayService.setEnabledExclusiveInCategory((String) newValue, UserHandle.myUserId()); + try { + mOverlayService.setEnabledExclusiveInCategory((String) newValue, UserHandle.myUserId()); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } return true; } @@ -129,39 +134,43 @@ public class ThemePreferenceController extends AbstractPreferenceController impl } } - private String getTheme() { - List<OverlayInfo> infos = mOverlayService.getOverlayInfosForTarget("android", - UserHandle.myUserId()); - for (int i = 0, size = infos.size(); i < size; i++) { - if (infos.get(i).isEnabled() && isTheme(infos.get(i))) { - return infos.get(i).packageName; - } - } - return null; - } - @Override public boolean isAvailable() { if (mOverlayService == null) return false; - String[] themes = getAvailableThemes(); + String[] themes = getAvailableThemes(false /* currentThemeOnly */); return themes != null && themes.length > 1; } @VisibleForTesting String getCurrentTheme() { - return getTheme(); + String[] themePackages = getAvailableThemes(true /* currentThemeOnly */); + return themePackages.length < 1 ? null : themePackages[0]; } @VisibleForTesting - String[] getAvailableThemes() { - List<OverlayInfo> infos = mOverlayService.getOverlayInfosForTarget("android", - UserHandle.myUserId()); - List<String> pkgs = new ArrayList<>(infos.size()); - for (int i = 0, size = infos.size(); i < size; i++) { - if (isTheme(infos.get(i))) { - pkgs.add(infos.get(i).packageName); + String[] getAvailableThemes(boolean currentThemeOnly) { + List<OverlayInfo> infos; + List<String> pkgs; + try { + infos = mOverlayService.getOverlayInfosForTarget("android", UserHandle.myUserId()); + pkgs = new ArrayList<>(infos.size()); + for (int i = 0, size = infos.size(); i < size; i++) { + if (isTheme(infos.get(i))) { + if (infos.get(i).isEnabled() && currentThemeOnly) { + return new String[] {infos.get(i).packageName}; + } else { + pkgs.add(infos.get(i).packageName); + } + } } + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + + // Current enabled theme is not found. + if (currentThemeOnly) { + return new String[0]; } return pkgs.toArray(new String[pkgs.size()]); } diff --git a/src/com/android/settings/TimeoutListPreference.java b/src/com/android/settings/display/TimeoutListPreference.java index 4b0fd9d045..f9a731d32e 100644 --- a/src/com/android/settings/TimeoutListPreference.java +++ b/src/com/android/settings/display/TimeoutListPreference.java @@ -14,23 +14,29 @@ * limitations under the License. */ -package com.android.settings; +package com.android.settings.display; + +import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; -import android.app.AlertDialog; import android.app.Dialog; import android.app.admin.DevicePolicyManager; import android.content.Context; import android.content.DialogInterface; import android.util.AttributeSet; +import android.util.Log; import android.view.View; +import androidx.appcompat.app.AlertDialog.Builder; + +import com.android.settings.R; +import com.android.settings.RestrictedListPreference; import com.android.settingslib.RestrictedLockUtils; import java.util.ArrayList; -import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; public class TimeoutListPreference extends RestrictedListPreference { + private static final String TAG = "TimeoutListPreference"; private EnforcedAdmin mAdmin; private final CharSequence[] mInitialEntries; private final CharSequence[] mInitialValues; @@ -42,7 +48,7 @@ public class TimeoutListPreference extends RestrictedListPreference { } @Override - protected void onPrepareDialogBuilder(AlertDialog.Builder builder, + protected void onPrepareDialogBuilder(Builder builder, DialogInterface.OnClickListener listener) { super.onPrepareDialogBuilder(builder, listener); if (mAdmin != null) { @@ -115,10 +121,11 @@ public class TimeoutListPreference extends RestrictedListPreference { // If the last one happens to be the same as the max timeout, select that setValue(String.valueOf(maxTimeout)); } else { - // There will be no highlighted selection since nothing in the list matches - // maxTimeout. The user can still select anything less than maxTimeout. - // TODO: maybe append maxTimeout to the list and mark selected. + // The selected time out value is longer than the max timeout allowed by the admin. + // Select the largest value from the list by default. + Log.w(TAG, "Default to longest timeout. Value disabled by admin:" + userPreference); + setValue(revisedValues.get(revisedValues.size() - 1).toString()); } } } -}
\ No newline at end of file +} diff --git a/src/com/android/settings/display/TimeoutPreferenceController.java b/src/com/android/settings/display/TimeoutPreferenceController.java index b2a638ecde..60b7e24b85 100644 --- a/src/com/android/settings/display/TimeoutPreferenceController.java +++ b/src/com/android/settings/display/TimeoutPreferenceController.java @@ -20,14 +20,15 @@ import android.content.Context; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; -import androidx.preference.Preference; import android.util.Log; +import androidx.preference.Preference; + import com.android.settings.R; -import com.android.settings.TimeoutListPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; +import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.core.AbstractPreferenceController; public class TimeoutPreferenceController extends AbstractPreferenceController implements @@ -65,17 +66,18 @@ public class TimeoutPreferenceController extends AbstractPreferenceController im (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE); if (dpm != null) { final RestrictedLockUtils.EnforcedAdmin admin = - RestrictedLockUtils.checkIfMaximumTimeToLockIsSet(mContext); + RestrictedLockUtilsInternal.checkIfMaximumTimeToLockIsSet(mContext); final long maxTimeout = dpm.getMaximumTimeToLock(null /* admin */, UserHandle.myUserId()); timeoutListPreference.removeUnusableTimeouts(maxTimeout, admin); } - updateTimeoutPreferenceDescription(timeoutListPreference, currentTimeout); + updateTimeoutPreferenceDescription(timeoutListPreference, + Long.parseLong(timeoutListPreference.getValue())); - EnforcedAdmin admin = RestrictedLockUtils.checkIfRestrictionEnforced( - mContext, UserManager.DISALLOW_CONFIG_SCREEN_TIMEOUT, - UserHandle.myUserId()); - if(admin != null) { + final EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced( + mContext, UserManager.DISALLOW_CONFIG_SCREEN_TIMEOUT, + UserHandle.myUserId()); + if (admin != null) { timeoutListPreference.removeUnusableTimeouts(0/* disable all*/, admin); } } diff --git a/src/com/android/settings/accessibility/ToggleFontSizePreferenceFragment.java b/src/com/android/settings/display/ToggleFontSizePreferenceFragment.java index 8c15e5ba63..49177a0b8b 100644 --- a/src/com/android/settings/accessibility/ToggleFontSizePreferenceFragment.java +++ b/src/com/android/settings/display/ToggleFontSizePreferenceFragment.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 The Android Open Source Project + * Copyright (C) 2018 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. @@ -14,33 +14,49 @@ * limitations under the License. */ -package com.android.settings.accessibility; +package com.android.settings.display; import android.annotation.Nullable; +import android.app.settings.SettingsEnums; import android.content.ContentResolver; +import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.os.Bundle; import android.provider.Settings; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.settings.PreviewSeekBarPreferenceFragment; + import com.android.settings.R; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; +import com.android.settings.search.SearchIndexableRaw; +import com.android.settingslib.search.SearchIndexable; + +import java.util.ArrayList; +import java.util.List; /** * Preference fragment used to control font size. */ +@SearchIndexable public class ToggleFontSizePreferenceFragment extends PreviewSeekBarPreferenceFragment { private float[] mValues; @Override + protected int getActivityLayoutResId() { + return R.layout.font_size_activity; + } + + @Override + protected int[] getPreviewSampleResIds() { + return new int[]{R.layout.font_size_preview}; + } + + @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - mActivityLayoutResId = R.layout.font_size_activity; - mPreviewSampleResIds = new int[]{R.layout.font_size_preview}; - - Resources res = getContext().getResources(); + final Resources res = getContext().getResources(); final ContentResolver resolver = getContext().getContentResolver(); // Mark the appropriate item in the preferences list. mEntries = res.getStringArray(R.array.entries_font_size); @@ -80,23 +96,39 @@ public class ToggleFontSizePreferenceFragment extends PreviewSeekBarPreferenceFr @Override public int getMetricsCategory() { - return MetricsEvent.ACCESSIBILITY_FONT_SIZE; + return SettingsEnums.ACCESSIBILITY_FONT_SIZE; } /** - * Utility function that returns the index in a string array with which the represented value is - * the closest to a given float value. + * Utility function that returns the index in a string array with which the represented value is + * the closest to a given float value. */ public static int fontSizeValueToIndex(float val, String[] indices) { float lastVal = Float.parseFloat(indices[0]); - for (int i=1; i<indices.length; i++) { + for (int i = 1; i < indices.length; i++) { float thisVal = Float.parseFloat(indices[i]); - if (val < (lastVal + (thisVal-lastVal)*.5f)) { - return i-1; + if (val < (lastVal + (thisVal - lastVal) * .5f)) { + return i - 1; } lastVal = thisVal; } - return indices.length-1; + return indices.length - 1; } + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableRaw> getRawDataToIndex(Context context, + boolean enabled) { + final ArrayList<SearchIndexableRaw> result = new ArrayList<>(); + final SearchIndexableRaw data = new SearchIndexableRaw(context); + data.title = context.getString(R.string.title_font_size); + data.screenTitle = context.getString(R.string.title_font_size); + data.key = "font_size_setting_screen"; + data.keywords = context.getString(R.string.keywords_display_font_size); + result.add(data); + return result; + } + }; + } diff --git a/src/com/android/settings/display/TopLevelDisplayPreferenceController.java b/src/com/android/settings/display/TopLevelDisplayPreferenceController.java new file mode 100644 index 0000000000..fbaea93d57 --- /dev/null +++ b/src/com/android/settings/display/TopLevelDisplayPreferenceController.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2018 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.display; + +import android.content.Context; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; + +public class TopLevelDisplayPreferenceController extends BasePreferenceController { + + public TopLevelDisplayPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public int getAvailabilityStatus() { + return mContext.getResources().getBoolean(R.bool.config_show_top_level_display) + ? AVAILABLE + : UNSUPPORTED_ON_DEVICE; + } + + @Override + public CharSequence getSummary() { + final WallpaperPreferenceController controller = + new WallpaperPreferenceController(mContext, "dummy_key"); + if (controller.isAvailable()) { + return mContext.getText( + controller.areStylesAvailable() + ? R.string.display_dashboard_summary_with_style + : R.string.display_dashboard_summary); + } else { + return mContext.getText(R.string.display_dashboard_nowallpaper_summary); + } + } +} diff --git a/src/com/android/settings/display/VrDisplayPreferenceController.java b/src/com/android/settings/display/VrDisplayPreferenceController.java index 9ff2861dae..6fcb5b0bc1 100644 --- a/src/com/android/settings/display/VrDisplayPreferenceController.java +++ b/src/com/android/settings/display/VrDisplayPreferenceController.java @@ -17,6 +17,7 @@ import android.app.ActivityManager; import android.content.Context; import android.content.pm.PackageManager; import android.provider.Settings; + import androidx.preference.Preference; import com.android.settings.R; diff --git a/src/com/android/settings/display/VrDisplayPreferencePicker.java b/src/com/android/settings/display/VrDisplayPreferencePicker.java index 5f99be9388..4612779ba5 100644 --- a/src/com/android/settings/display/VrDisplayPreferencePicker.java +++ b/src/com/android/settings/display/VrDisplayPreferencePicker.java @@ -16,12 +16,12 @@ package com.android.settings.display; +import android.app.settings.SettingsEnums; import android.content.Context; import android.graphics.drawable.Drawable; import android.provider.Settings; import android.text.TextUtils; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.widget.RadioButtonPickerFragment; import com.android.settingslib.widget.CandidateInfo; @@ -40,7 +40,7 @@ public class VrDisplayPreferencePicker extends RadioButtonPickerFragment { @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.VR_DISPLAY_PREFERENCE; + return SettingsEnums.VR_DISPLAY_PREFERENCE; } @Override diff --git a/src/com/android/settings/display/WallpaperPreferenceController.java b/src/com/android/settings/display/WallpaperPreferenceController.java index 3c0e9476d8..3ba39e9072 100644 --- a/src/com/android/settings/display/WallpaperPreferenceController.java +++ b/src/com/android/settings/display/WallpaperPreferenceController.java @@ -13,61 +13,77 @@ */ package com.android.settings.display; +import static android.os.UserManager.DISALLOW_SET_WALLPAPER; + import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.os.UserHandle; -import androidx.preference.Preference; import android.text.TextUtils; import android.util.Log; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + import com.android.settings.R; -import com.android.settings.core.PreferenceControllerMixin; -import com.android.settingslib.RestrictedLockUtils; +import com.android.settings.core.BasePreferenceController; +import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.RestrictedPreference; -import com.android.settingslib.core.AbstractPreferenceController; - -import static android.os.UserManager.DISALLOW_SET_WALLPAPER; import java.util.List; -public class WallpaperPreferenceController extends AbstractPreferenceController implements - PreferenceControllerMixin { - +public class WallpaperPreferenceController extends BasePreferenceController { private static final String TAG = "WallpaperPrefController"; - public static final String KEY_WALLPAPER = "wallpaper"; - private final String mWallpaperPackage; private final String mWallpaperClass; + private final String mStylesAndWallpaperClass; - public WallpaperPreferenceController(Context context) { - super(context); + public WallpaperPreferenceController(Context context, String key) { + super(context, key); mWallpaperPackage = mContext.getString(R.string.config_wallpaper_picker_package); mWallpaperClass = mContext.getString(R.string.config_wallpaper_picker_class); + mStylesAndWallpaperClass = + mContext.getString(R.string.config_styles_and_wallpaper_picker_class); } @Override - public boolean isAvailable() { - if (TextUtils.isEmpty(mWallpaperPackage) || TextUtils.isEmpty(mWallpaperClass)) { - Log.e(TAG, "No Wallpaper picker specified!"); - return false; + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + Preference preference = screen.findPreference(getPreferenceKey()); + preference.setTitle(getTitle()); + } + + public String getTitle() { + return mContext.getString(areStylesAvailable() + ? R.string.style_and_wallpaper_settings_title : R.string.wallpaper_settings_title); + } + + public ComponentName getComponentName() { + return new ComponentName(mWallpaperPackage, + areStylesAvailable() ? mStylesAndWallpaperClass : mWallpaperClass); + } + + public String getKeywords() { + StringBuilder sb = new StringBuilder(mContext.getString(R.string.keywords_wallpaper)); + if (areStylesAvailable()) { + // TODO(b/130759285): Create a new string keywords_styles_and_wallpaper + sb.append(", ").append(mContext.getString(R.string.theme_customization_category)) + .append(", ").append(mContext.getString(R.string.keywords_dark_ui_mode)); } - final ComponentName componentName = - new ComponentName(mWallpaperPackage, mWallpaperClass); - final PackageManager pm = mContext.getPackageManager(); - final Intent intent = new Intent(); - intent.setComponent(componentName); - final List<ResolveInfo> resolveInfos = - pm.queryIntentActivities(intent, 0 /* flags */); - return resolveInfos != null && resolveInfos.size() != 0; + return sb.toString(); } @Override - public String getPreferenceKey() { - return KEY_WALLPAPER; + public int getAvailabilityStatus() { + if (TextUtils.isEmpty(mWallpaperPackage) || TextUtils.isEmpty(mWallpaperClass)) { + Log.e(TAG, "No Wallpaper picker specified!"); + return UNSUPPORTED_ON_DEVICE; + } + return canResolveWallpaperComponent(mWallpaperClass) + ? AVAILABLE_UNSEARCHABLE : CONDITIONALLY_UNAVAILABLE; } @Override @@ -75,11 +91,34 @@ public class WallpaperPreferenceController extends AbstractPreferenceController disablePreferenceIfManaged((RestrictedPreference) preference); } + @Override + public boolean handlePreferenceTreeClick(Preference preference) { + if (getPreferenceKey().equals(preference.getKey())) { + preference.getContext().startActivity(new Intent().setComponent(getComponentName())); + return true; + } + return super.handlePreferenceTreeClick(preference); + } + + /** Returns whether Styles & Wallpaper is enabled and available. */ + public boolean areStylesAvailable() { + return !TextUtils.isEmpty(mStylesAndWallpaperClass) + && canResolveWallpaperComponent(mStylesAndWallpaperClass); + } + + private boolean canResolveWallpaperComponent(String className) { + final ComponentName componentName = new ComponentName(mWallpaperPackage, className); + final PackageManager pm = mContext.getPackageManager(); + final Intent intent = new Intent().setComponent(componentName); + final List<ResolveInfo> resolveInfos = pm.queryIntentActivities(intent, 0 /* flags */); + return resolveInfos != null && !resolveInfos.isEmpty(); + } + private void disablePreferenceIfManaged(RestrictedPreference pref) { final String restriction = DISALLOW_SET_WALLPAPER; if (pref != null) { pref.setDisabledByAdmin(null); - if (RestrictedLockUtils.hasBaseUserRestriction(mContext, + if (RestrictedLockUtilsInternal.hasBaseUserRestriction(mContext, restriction, UserHandle.myUserId())) { pref.setEnabled(false); } else { diff --git a/src/com/android/settings/dream/CurrentDreamPicker.java b/src/com/android/settings/dream/CurrentDreamPicker.java index 3ebce1c1d5..3134e79ffd 100644 --- a/src/com/android/settings/dream/CurrentDreamPicker.java +++ b/src/com/android/settings/dream/CurrentDreamPicker.java @@ -16,15 +16,17 @@ package com.android.settings.dream; +import android.app.settings.SettingsEnums; import android.content.ComponentName; import android.content.Context; import android.graphics.drawable.Drawable; -import com.android.internal.logging.nano.MetricsProto; + import com.android.settings.R; import com.android.settings.widget.RadioButtonPickerFragment; import com.android.settingslib.dream.DreamBackend; import com.android.settingslib.dream.DreamBackend.DreamInfo; import com.android.settingslib.widget.CandidateInfo; + import java.util.HashMap; import java.util.List; import java.util.Map; @@ -48,7 +50,7 @@ public final class CurrentDreamPicker extends RadioButtonPickerFragment { @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DREAM; + return SettingsEnums.DREAM; } @Override diff --git a/src/com/android/settings/dream/CurrentDreamPreferenceController.java b/src/com/android/settings/dream/CurrentDreamPreferenceController.java index 957852b695..fee4826c5a 100644 --- a/src/com/android/settings/dream/CurrentDreamPreferenceController.java +++ b/src/com/android/settings/dream/CurrentDreamPreferenceController.java @@ -17,48 +17,48 @@ package com.android.settings.dream; import android.content.Context; + import androidx.preference.Preference; -import com.android.settings.core.PreferenceControllerMixin; + +import com.android.settings.core.BasePreferenceController; import com.android.settings.widget.GearPreference; -import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.dream.DreamBackend; import com.android.settingslib.dream.DreamBackend.DreamInfo; + import java.util.Optional; -public class CurrentDreamPreferenceController extends AbstractPreferenceController implements - PreferenceControllerMixin { +public class CurrentDreamPreferenceController extends BasePreferenceController { + private final DreamBackend mBackend; - private final static String TAG = "CurrentDreamPreferenceController"; - private final static String CURRENT_SCREENSAVER = "current_screensaver"; - public CurrentDreamPreferenceController(Context context) { - super(context); + public CurrentDreamPreferenceController(Context context, String key) { + super(context, key); mBackend = DreamBackend.getInstance(context); } @Override - public boolean isAvailable() { - return mBackend.getDreamInfos().size() > 0; - } - - @Override - public String getPreferenceKey() { - return CURRENT_SCREENSAVER; + public int getAvailabilityStatus() { + return mBackend.getDreamInfos().size() > 0 ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; } @Override public void updateState(Preference preference) { super.updateState(preference); - - preference.setSummary(mBackend.getActiveDreamName()); setGearClickListenerForPreference(preference); } + @Override + public CharSequence getSummary() { + return mBackend.getActiveDreamName(); + } + private void setGearClickListenerForPreference(Preference preference) { - if (!(preference instanceof GearPreference)) return; + if (!(preference instanceof GearPreference)) { + return; + } - GearPreference gearPreference = (GearPreference)preference; - Optional<DreamInfo> info = getActiveDreamInfo(); + final GearPreference gearPreference = (GearPreference) preference; + final Optional<DreamInfo> info = getActiveDreamInfo(); if (!info.isPresent() || info.get().settingsComponentName == null) { gearPreference.setOnGearClickListener(null); return; @@ -67,7 +67,7 @@ public class CurrentDreamPreferenceController extends AbstractPreferenceControll } private void launchScreenSaverSettings() { - Optional<DreamInfo> info = getActiveDreamInfo(); + final Optional<DreamInfo> info = getActiveDreamInfo(); if (!info.isPresent()) return; mBackend.launchSettings(mContext, info.get()); } diff --git a/src/com/android/settings/dream/DreamSettings.java b/src/com/android/settings/dream/DreamSettings.java index 73762bd7cd..c36970e9bc 100644 --- a/src/com/android/settings/dream/DreamSettings.java +++ b/src/com/android/settings/dream/DreamSettings.java @@ -21,22 +21,25 @@ import static com.android.settingslib.dream.DreamBackend.NEVER; import static com.android.settingslib.dream.DreamBackend.WHILE_CHARGING; import static com.android.settingslib.dream.DreamBackend.WHILE_DOCKED; +import android.app.settings.SettingsEnums; import android.content.Context; import android.provider.SearchIndexableResource; + import androidx.annotation.VisibleForTesting; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.dream.DreamBackend; import com.android.settingslib.dream.DreamBackend.WhenToDream; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +@SearchIndexable public class DreamSettings extends DashboardFragment { private static final String TAG = "DreamSettings"; @@ -90,7 +93,7 @@ public class DreamSettings extends DashboardFragment { @Override public int getMetricsCategory() { - return MetricsEvent.DREAM; + return SettingsEnums.DREAM; } @Override @@ -129,7 +132,6 @@ public class DreamSettings extends DashboardFragment { private static List<AbstractPreferenceController> buildPreferenceControllers(Context context) { List<AbstractPreferenceController> controllers = new ArrayList<>(); - controllers.add(new CurrentDreamPreferenceController(context)); controllers.add(new WhenToDreamPreferenceController(context)); controllers.add(new StartNowPreferenceController(context)); return controllers; diff --git a/src/com/android/settings/dream/StartNowPreferenceController.java b/src/com/android/settings/dream/StartNowPreferenceController.java index 0a6c1b6746..0541bca891 100644 --- a/src/com/android/settings/dream/StartNowPreferenceController.java +++ b/src/com/android/settings/dream/StartNowPreferenceController.java @@ -17,14 +17,16 @@ package com.android.settings.dream; import android.content.Context; +import android.widget.Button; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.widget.Button; + import com.android.settings.R; -import com.android.settings.applications.LayoutPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.dream.DreamBackend; +import com.android.settingslib.widget.LayoutPreference; public class StartNowPreferenceController extends AbstractPreferenceController implements PreferenceControllerMixin { @@ -52,8 +54,8 @@ public class StartNowPreferenceController extends AbstractPreferenceController i public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - LayoutPreference pref = (LayoutPreference) screen.findPreference(getPreferenceKey()); - Button startButton = (Button)pref.findViewById(R.id.dream_start_now_button); + LayoutPreference pref = screen.findPreference(getPreferenceKey()); + Button startButton = pref.findViewById(R.id.dream_start_now_button); startButton.setOnClickListener(v -> mBackend.startDreaming()); } @@ -61,7 +63,7 @@ public class StartNowPreferenceController extends AbstractPreferenceController i public void updateState(Preference preference) { super.updateState(preference); - Button startButton = (Button)((LayoutPreference)preference) + Button startButton = ((LayoutPreference) preference) .findViewById(R.id.dream_start_now_button); startButton.setEnabled(mBackend.getWhenToDreamSetting() != DreamBackend.NEVER); } diff --git a/src/com/android/settings/dream/WhenToDreamPicker.java b/src/com/android/settings/dream/WhenToDreamPicker.java index f6f89a89f8..1c5e25ebac 100644 --- a/src/com/android/settings/dream/WhenToDreamPicker.java +++ b/src/com/android/settings/dream/WhenToDreamPicker.java @@ -16,13 +16,15 @@ package com.android.settings.dream; +import android.app.settings.SettingsEnums; import android.content.Context; import android.graphics.drawable.Drawable; -import com.android.internal.logging.nano.MetricsProto; + import com.android.settings.R; import com.android.settings.widget.RadioButtonPickerFragment; import com.android.settingslib.dream.DreamBackend; import com.android.settingslib.widget.CandidateInfo; + import java.util.ArrayList; import java.util.List; @@ -45,7 +47,7 @@ public class WhenToDreamPicker extends RadioButtonPickerFragment { @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DREAM; + return SettingsEnums.DREAM; } @Override diff --git a/src/com/android/settings/dream/WhenToDreamPreferenceController.java b/src/com/android/settings/dream/WhenToDreamPreferenceController.java index 702812e7fd..4108e85053 100644 --- a/src/com/android/settings/dream/WhenToDreamPreferenceController.java +++ b/src/com/android/settings/dream/WhenToDreamPreferenceController.java @@ -17,7 +17,9 @@ package com.android.settings.dream; import android.content.Context; + import androidx.preference.Preference; + import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.dream.DreamBackend; diff --git a/src/com/android/settings/enterprise/ActionDisabledByAdminDialog.java b/src/com/android/settings/enterprise/ActionDisabledByAdminDialog.java index 104b216b60..717c5bcd75 100644 --- a/src/com/android/settings/enterprise/ActionDisabledByAdminDialog.java +++ b/src/com/android/settings/enterprise/ActionDisabledByAdminDialog.java @@ -53,12 +53,22 @@ public class ActionDisabledByAdminDialog extends Activity @androidx.annotation.VisibleForTesting EnforcedAdmin getAdminDetailsFromIntent(Intent intent) { - final EnforcedAdmin admin = new EnforcedAdmin(null, UserHandle.myUserId()); + final EnforcedAdmin admin = new EnforcedAdmin(null, UserHandle.of(UserHandle.myUserId())); if (intent == null) { return admin; } admin.component = intent.getParcelableExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN); - admin.userId = intent.getIntExtra(Intent.EXTRA_USER_ID, UserHandle.myUserId()); + + if (intent.hasExtra(Intent.EXTRA_USER)) { + admin.user = intent.getParcelableExtra(Intent.EXTRA_USER); + } else { + int userId = intent.getIntExtra(Intent.EXTRA_USER_ID, UserHandle.myUserId()); + if (userId == UserHandle.USER_NULL) { + admin.user = null; + } else { + admin.user = UserHandle.of(userId); + } + } return admin; } diff --git a/src/com/android/settings/enterprise/ActionDisabledByAdminDialogHelper.java b/src/com/android/settings/enterprise/ActionDisabledByAdminDialogHelper.java index 026b79f0fd..5599a94a21 100644 --- a/src/com/android/settings/enterprise/ActionDisabledByAdminDialogHelper.java +++ b/src/com/android/settings/enterprise/ActionDisabledByAdminDialogHelper.java @@ -16,17 +16,19 @@ package com.android.settings.enterprise; +import android.annotation.NonNull; +import android.annotation.UserIdInt; import android.app.Activity; -import android.app.AlertDialog; import android.app.admin.DevicePolicyManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.os.Process; import android.os.UserHandle; import android.os.UserManager; -import androidx.annotation.VisibleForTesting; import android.util.IconDrawableFactory; import android.view.LayoutInflater; import android.view.View; @@ -34,12 +36,16 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; -import com.android.settings.DeviceAdminAdd; +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; + import com.android.settings.R; import com.android.settings.Settings; import com.android.settings.Utils; +import com.android.settings.applications.specialaccess.deviceadmin.DeviceAdminAdd; import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; +import com.android.settingslib.RestrictedLockUtilsInternal; import java.util.Objects; @@ -49,7 +55,7 @@ import java.util.Objects; public class ActionDisabledByAdminDialogHelper { private static final String TAG = ActionDisabledByAdminDialogHelper.class.getName(); - private EnforcedAdmin mEnforcedAdmin; + @VisibleForTesting EnforcedAdmin mEnforcedAdmin; private ViewGroup mDialogView; private String mRestriction = null; private Activity mActivity; @@ -58,24 +64,44 @@ public class ActionDisabledByAdminDialogHelper { mActivity = activity; } + private @UserIdInt int getEnforcementAdminUserId(@NonNull EnforcedAdmin admin) { + if (admin.user == null) { + return UserHandle.USER_NULL; + } else { + return admin.user.getIdentifier(); + } + } + + private @UserIdInt int getEnforcementAdminUserId() { + return getEnforcementAdminUserId(mEnforcedAdmin); + } + public AlertDialog.Builder prepareDialogBuilder(String restriction, EnforcedAdmin enforcedAdmin) { mEnforcedAdmin = enforcedAdmin; mRestriction = restriction; - final AlertDialog.Builder builder = new AlertDialog.Builder(mActivity); mDialogView = (ViewGroup) LayoutInflater.from(builder.getContext()).inflate( R.layout.admin_support_details_dialog, null); - initializeDialogViews(mDialogView, mEnforcedAdmin.component, mEnforcedAdmin.userId, + initializeDialogViews(mDialogView, mEnforcedAdmin.component, getEnforcementAdminUserId(), mRestriction); - return builder - .setPositiveButton(R.string.okay, null) - .setNeutralButton(R.string.learn_more, - (dialog, which) -> { - showAdminPolicies(mEnforcedAdmin, mActivity); - mActivity.finish(); - }) - .setView(mDialogView); + builder.setPositiveButton(R.string.okay, null).setView(mDialogView); + maybeSetLearnMoreButton(builder); + return builder; + } + + @VisibleForTesting + void maybeSetLearnMoreButton(AlertDialog.Builder builder) { + // The "Learn more" button appears only if the restriction is enforced by an admin in the + // same profile group. Otherwise the admin package and its policies are not accessible to + // the current user. + final UserManager um = UserManager.get(mActivity.getApplicationContext()); + if (um.isSameProfileGroup(getEnforcementAdminUserId(mEnforcedAdmin), um.getUserHandle())) { + builder.setNeutralButton(R.string.learn_more, (dialog, which) -> { + showAdminPolicies(mEnforcedAdmin, mActivity); + mActivity.finish(); + }); + } } public void updateDialog(String restriction, EnforcedAdmin admin) { @@ -84,7 +110,7 @@ public class ActionDisabledByAdminDialogHelper { } mEnforcedAdmin = admin; mRestriction = restriction; - initializeDialogViews(mDialogView, mEnforcedAdmin.component, mEnforcedAdmin.userId, + initializeDialogViews(mDialogView, mEnforcedAdmin.component, getEnforcementAdminUserId(), mRestriction); } @@ -93,20 +119,36 @@ public class ActionDisabledByAdminDialogHelper { if (admin == null) { return; } - if (!RestrictedLockUtils.isAdminInCurrentUserOrProfile(mActivity, admin) + ImageView supportIconView = root.requireViewById(R.id.admin_support_icon); + if (!RestrictedLockUtilsInternal.isAdminInCurrentUserOrProfile(mActivity, admin) || !RestrictedLockUtils.isCurrentUserOrProfile(mActivity, userId)) { admin = null; + + supportIconView.setImageDrawable( + mActivity.getDrawable(com.android.internal.R.drawable.ic_info)); + + TypedArray ta = mActivity.obtainStyledAttributes(new int[]{android.R.attr.colorAccent}); + supportIconView.setImageTintList(ColorStateList.valueOf(ta.getColor(0, 0))); + ta.recycle(); } else { final Drawable badgedIcon = Utils.getBadgedIcon( IconDrawableFactory.newInstance(mActivity), mActivity.getPackageManager(), admin.getPackageName(), userId); - ((ImageView) root.findViewById(R.id.admin_support_icon)).setImageDrawable(badgedIcon); + supportIconView.setImageDrawable(badgedIcon); } setAdminSupportTitle(root, restriction); - setAdminSupportDetails(mActivity, root, new EnforcedAdmin(admin, userId)); + + final UserHandle user; + if (userId == UserHandle.USER_NULL) { + user = null; + } else { + user = UserHandle.of(userId); + } + + setAdminSupportDetails(mActivity, root, new EnforcedAdmin(admin, user)); } @VisibleForTesting @@ -135,9 +177,6 @@ public class ActionDisabledByAdminDialogHelper { case DevicePolicyManager.POLICY_DISABLE_SCREEN_CAPTURE: titleView.setText(R.string.disabled_by_policy_title_screen_capture); break; - case DevicePolicyManager.POLICY_MANDATORY_BACKUPS: - titleView.setText(R.string.disabled_by_policy_title_turn_off_backups); - break; case DevicePolicyManager.POLICY_SUSPEND_PACKAGES: titleView.setText(R.string.disabled_by_policy_title_suspend_packages); break; @@ -153,20 +192,21 @@ public class ActionDisabledByAdminDialogHelper { if (enforcedAdmin == null || enforcedAdmin.component == null) { return; } + final DevicePolicyManager dpm = (DevicePolicyManager) activity.getSystemService( Context.DEVICE_POLICY_SERVICE); - if (!RestrictedLockUtils.isAdminInCurrentUserOrProfile(activity, + if (!RestrictedLockUtilsInternal.isAdminInCurrentUserOrProfile(activity, enforcedAdmin.component) || !RestrictedLockUtils.isCurrentUserOrProfile( - activity, enforcedAdmin.userId)) { + activity, getEnforcementAdminUserId(enforcedAdmin))) { enforcedAdmin.component = null; } else { - if (enforcedAdmin.userId == UserHandle.USER_NULL) { - enforcedAdmin.userId = UserHandle.myUserId(); + if (enforcedAdmin.user == null) { + enforcedAdmin.user = UserHandle.of(UserHandle.myUserId()); } CharSequence supportMessage = null; if (UserHandle.isSameApp(Process.myUid(), Process.SYSTEM_UID)) { - supportMessage = dpm.getShortSupportMessageForUser( - enforcedAdmin.component, enforcedAdmin.userId); + supportMessage = dpm.getShortSupportMessageForUser(enforcedAdmin.component, + getEnforcementAdminUserId(enforcedAdmin)); } if (supportMessage != null) { final TextView textView = root.findViewById(R.id.admin_support_msg); @@ -184,8 +224,7 @@ public class ActionDisabledByAdminDialogHelper { enforcedAdmin.component); intent.putExtra(DeviceAdminAdd.EXTRA_CALLED_FROM_SUPPORT_DIALOG, true); // DeviceAdminAdd class may need to run as managed profile. - activity.startActivityAsUser(intent, - new UserHandle(enforcedAdmin.userId)); + activity.startActivityAsUser(intent, enforcedAdmin.user); } else { intent.setClass(activity, Settings.DeviceAdminSettingsActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); diff --git a/src/com/android/settings/enterprise/AdminActionPreferenceControllerBase.java b/src/com/android/settings/enterprise/AdminActionPreferenceControllerBase.java index 520c436662..1c7ff620c3 100644 --- a/src/com/android/settings/enterprise/AdminActionPreferenceControllerBase.java +++ b/src/com/android/settings/enterprise/AdminActionPreferenceControllerBase.java @@ -15,9 +15,10 @@ package com.android.settings.enterprise; import android.content.Context; -import androidx.preference.Preference; import android.text.format.DateUtils; +import androidx.preference.Preference; + import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.overlay.FeatureFactory; diff --git a/src/com/android/settings/enterprise/AdminGrantedPermissionsPreferenceControllerBase.java b/src/com/android/settings/enterprise/AdminGrantedPermissionsPreferenceControllerBase.java index 93ed1027c2..86d08cb674 100644 --- a/src/com/android/settings/enterprise/AdminGrantedPermissionsPreferenceControllerBase.java +++ b/src/com/android/settings/enterprise/AdminGrantedPermissionsPreferenceControllerBase.java @@ -16,6 +16,7 @@ package com.android.settings.enterprise; import android.content.Context; + import androidx.preference.Preference; import com.android.settings.R; diff --git a/src/com/android/settings/enterprise/AlwaysOnVpnCurrentUserPreferenceController.java b/src/com/android/settings/enterprise/AlwaysOnVpnCurrentUserPreferenceController.java index 7b6d5df3fd..696561bad8 100644 --- a/src/com/android/settings/enterprise/AlwaysOnVpnCurrentUserPreferenceController.java +++ b/src/com/android/settings/enterprise/AlwaysOnVpnCurrentUserPreferenceController.java @@ -14,6 +14,7 @@ package com.android.settings.enterprise; import android.content.Context; + import androidx.preference.Preference; import com.android.settings.R; diff --git a/src/com/android/settings/enterprise/ApplicationListFragment.java b/src/com/android/settings/enterprise/ApplicationListFragment.java index 1b41f0396e..17de9f892a 100644 --- a/src/com/android/settings/enterprise/ApplicationListFragment.java +++ b/src/com/android/settings/enterprise/ApplicationListFragment.java @@ -17,9 +17,9 @@ package com.android.settings.enterprise; import android.Manifest; +import android.app.settings.SettingsEnums; import android.content.Context; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.applications.ApplicationFeatureProvider; import com.android.settings.dashboard.DashboardFragment; @@ -73,7 +73,7 @@ public abstract class ApplicationListFragment extends DashboardFragment @Override public int getMetricsCategory() { - return MetricsEvent.ENTERPRISE_PRIVACY_PERMISSIONS; + return SettingsEnums.ENTERPRISE_PRIVACY_PERMISSIONS; } } @@ -102,7 +102,7 @@ public abstract class ApplicationListFragment extends DashboardFragment @Override public int getMetricsCategory() { - return MetricsEvent.ENTERPRISE_PRIVACY_INSTALLED_APPS; + return SettingsEnums.ENTERPRISE_PRIVACY_INSTALLED_APPS; } @Override diff --git a/src/com/android/settings/enterprise/ApplicationListPreferenceController.java b/src/com/android/settings/enterprise/ApplicationListPreferenceController.java index 07a39e3a90..271981858b 100644 --- a/src/com/android/settings/enterprise/ApplicationListPreferenceController.java +++ b/src/com/android/settings/enterprise/ApplicationListPreferenceController.java @@ -18,16 +18,17 @@ package com.android.settings.enterprise; import android.content.Context; import android.content.pm.PackageManager; +import android.util.IconDrawableFactory; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.util.IconDrawableFactory; import com.android.settings.SettingsPreferenceFragment; import com.android.settings.applications.ApplicationFeatureProvider; import com.android.settings.applications.UserAppInfo; import com.android.settings.core.PreferenceControllerMixin; -import com.android.settings.widget.AppPreference; import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.widget.apppreference.AppPreference; import java.util.List; @@ -79,6 +80,7 @@ public class ApplicationListPreferenceController extends AbstractPreferenceContr /** * Simple interface for building application list within { + * * @link ApplicationListPreferenceController} */ public interface ApplicationListBuilder { diff --git a/src/com/android/settings/enterprise/BackupsEnabledPreferenceController.java b/src/com/android/settings/enterprise/BackupsEnabledPreferenceController.java deleted file mode 100644 index b24f8dcc89..0000000000 --- a/src/com/android/settings/enterprise/BackupsEnabledPreferenceController.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) 2018 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.enterprise; - -import android.content.Context; - -import com.android.settings.core.BasePreferenceController; -import com.android.settings.overlay.FeatureFactory; - -public class BackupsEnabledPreferenceController extends BasePreferenceController { - - private static final String KEY_BACKUPS_ENABLED = "backups_enabled"; - private final EnterprisePrivacyFeatureProvider mFeatureProvider; - - public BackupsEnabledPreferenceController(Context context) { - super(context, KEY_BACKUPS_ENABLED); - mFeatureProvider = FeatureFactory.getFactory(context) - .getEnterprisePrivacyFeatureProvider(context); - } - - @Override - public int getAvailabilityStatus() { - return mFeatureProvider.areBackupsMandatory() ? AVAILABLE : DISABLED_FOR_USER; - } -} - diff --git a/src/com/android/settings/enterprise/CaCertsCurrentUserPreferenceController.java b/src/com/android/settings/enterprise/CaCertsCurrentUserPreferenceController.java index 6e81482e10..45170b3842 100644 --- a/src/com/android/settings/enterprise/CaCertsCurrentUserPreferenceController.java +++ b/src/com/android/settings/enterprise/CaCertsCurrentUserPreferenceController.java @@ -15,6 +15,7 @@ package com.android.settings.enterprise; import android.content.Context; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; diff --git a/src/com/android/settings/enterprise/CaCertsManagedProfilePreferenceController.java b/src/com/android/settings/enterprise/CaCertsManagedProfilePreferenceController.java index 2d3f41a6d4..1bfe74724f 100644 --- a/src/com/android/settings/enterprise/CaCertsManagedProfilePreferenceController.java +++ b/src/com/android/settings/enterprise/CaCertsManagedProfilePreferenceController.java @@ -15,6 +15,7 @@ package com.android.settings.enterprise; import android.content.Context; + import androidx.annotation.VisibleForTesting; public class CaCertsManagedProfilePreferenceController extends CaCertsPreferenceControllerBase { diff --git a/src/com/android/settings/enterprise/CaCertsPreferenceControllerBase.java b/src/com/android/settings/enterprise/CaCertsPreferenceControllerBase.java index 4220ccc546..c7dde5cc46 100644 --- a/src/com/android/settings/enterprise/CaCertsPreferenceControllerBase.java +++ b/src/com/android/settings/enterprise/CaCertsPreferenceControllerBase.java @@ -15,6 +15,7 @@ package com.android.settings.enterprise; import android.content.Context; + import androidx.preference.Preference; import com.android.settings.R; diff --git a/src/com/android/settings/enterprise/EnterpriseInstalledPackagesPreferenceController.java b/src/com/android/settings/enterprise/EnterpriseInstalledPackagesPreferenceController.java index 9e8574fcfb..9bd4279db3 100644 --- a/src/com/android/settings/enterprise/EnterpriseInstalledPackagesPreferenceController.java +++ b/src/com/android/settings/enterprise/EnterpriseInstalledPackagesPreferenceController.java @@ -14,6 +14,7 @@ package com.android.settings.enterprise; import android.content.Context; + import androidx.preference.Preference; import com.android.settings.R; diff --git a/src/com/android/settings/enterprise/EnterprisePrivacyFeatureProvider.java b/src/com/android/settings/enterprise/EnterprisePrivacyFeatureProvider.java index 51d125d5c2..46f9b71f23 100644 --- a/src/com/android/settings/enterprise/EnterprisePrivacyFeatureProvider.java +++ b/src/com/android/settings/enterprise/EnterprisePrivacyFeatureProvider.java @@ -125,8 +125,16 @@ public interface EnterprisePrivacyFeatureProvider { */ int getNumberOfActiveDeviceAdminsForCurrentUserAndManagedProfile(); - /* - * Returns whether backups are mandatory. + /** + * Returns {@code true} if it is possilbe to resolve an Intent to launch the "Your work policy + * info" page provided by the active Device Owner or Profile Owner app if it exists, {@code + * false} otherwise. + */ + boolean hasWorkPolicyInfo(); + + /** + * Launches the Device Owner or Profile Owner's activity that displays the "Your work policy + * info" page. Returns {@code true} if the activity has indeed been launched. */ - boolean areBackupsMandatory(); + boolean showWorkPolicyInfo(); } diff --git a/src/com/android/settings/enterprise/EnterprisePrivacyFeatureProviderImpl.java b/src/com/android/settings/enterprise/EnterprisePrivacyFeatureProviderImpl.java index ab985f0526..675795e754 100644 --- a/src/com/android/settings/enterprise/EnterprisePrivacyFeatureProviderImpl.java +++ b/src/com/android/settings/enterprise/EnterprisePrivacyFeatureProviderImpl.java @@ -21,6 +21,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.content.pm.UserInfo; import android.content.res.Resources; import android.net.ConnectivityManager; @@ -33,7 +34,6 @@ import android.view.View; import com.android.settings.R; import com.android.settings.vpn2.VpnUtils; -import com.android.settingslib.wrapper.PackageManagerWrapper; import java.util.Date; import java.util.List; @@ -42,7 +42,7 @@ public class EnterprisePrivacyFeatureProviderImpl implements EnterprisePrivacyFe private final Context mContext; private final DevicePolicyManager mDpm; - private final PackageManagerWrapper mPm; + private final PackageManager mPm; private final UserManager mUm; private final ConnectivityManager mCm; private final Resources mResources; @@ -50,7 +50,7 @@ public class EnterprisePrivacyFeatureProviderImpl implements EnterprisePrivacyFe private static final int MY_USER_ID = UserHandle.myUserId(); public EnterprisePrivacyFeatureProviderImpl(Context context, DevicePolicyManager dpm, - PackageManagerWrapper pm, UserManager um, ConnectivityManager cm, + PackageManager pm, UserManager um, ConnectivityManager cm, Resources resources) { mContext = context.getApplicationContext(); mDpm = dpm; @@ -62,19 +62,7 @@ public class EnterprisePrivacyFeatureProviderImpl implements EnterprisePrivacyFe @Override public boolean hasDeviceOwner() { - if (!mPm.hasSystemFeature(PackageManager.FEATURE_DEVICE_ADMIN)) { - return false; - } - return mDpm.getDeviceOwnerComponentOnAnyUser() != null; - } - - private int getManagedProfileUserId() { - for (final UserInfo userInfo : mUm.getProfiles(MY_USER_ID)) { - if (userInfo.isManagedProfile()) { - return userInfo.id; - } - } - return UserHandle.USER_NULL; + return getDeviceOwnerComponent() != null; } @Override @@ -194,7 +182,7 @@ public class EnterprisePrivacyFeatureProviderImpl implements EnterprisePrivacyFe } try { return mPm.getApplicationInfoAsUser(packageName, 0 /* flags */, MY_USER_ID) - .loadLabel(mPm.getPackageManager()).toString(); + .loadLabel(mPm).toString(); } catch (PackageManager.NameNotFoundException e) { return null; } @@ -236,8 +224,93 @@ public class EnterprisePrivacyFeatureProviderImpl implements EnterprisePrivacyFe } @Override - public boolean areBackupsMandatory() { - return null != mDpm.getMandatoryBackupTransport(); + public boolean hasWorkPolicyInfo() { + return (getWorkPolicyInfoIntentDO() != null) || (getWorkPolicyInfoIntentPO() != null); + } + + @Override + public boolean showWorkPolicyInfo() { + Intent intent = getWorkPolicyInfoIntentDO(); + if (intent != null) { + mContext.startActivity(intent); + return true; + } + + intent = getWorkPolicyInfoIntentPO(); + final UserInfo userInfo = getManagedProfileUserInfo(); + if (intent != null && userInfo != null) { + mContext.startActivityAsUser(intent, userInfo.getUserHandle()); + return true; + } + + return false; + } + + private ComponentName getDeviceOwnerComponent() { + if (!mPm.hasSystemFeature(PackageManager.FEATURE_DEVICE_ADMIN)) { + return null; + } + return mDpm.getDeviceOwnerComponentOnAnyUser(); + } + + private UserInfo getManagedProfileUserInfo() { + for (final UserInfo userInfo : mUm.getProfiles(MY_USER_ID)) { + if (userInfo.isManagedProfile()) { + return userInfo; + } + } + return null; + } + + private int getManagedProfileUserId() { + final UserInfo userInfo = getManagedProfileUserInfo(); + if (userInfo != null) { + return userInfo.id; + } + return UserHandle.USER_NULL; + } + + private Intent getWorkPolicyInfoIntentDO() { + final ComponentName ownerComponent = getDeviceOwnerComponent(); + if (ownerComponent == null) { + return null; + } + + // Only search for the required action in the Device Owner's package + final Intent intent = + new Intent(mResources.getString(R.string.config_work_policy_info_intent_action)) + .setPackage(ownerComponent.getPackageName()) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + final List<ResolveInfo> activities = mPm.queryIntentActivities(intent, 0); + if (activities.size() != 0) { + return intent; + } + + return null; + } + + private Intent getWorkPolicyInfoIntentPO() { + final int userId = getManagedProfileUserId(); + if (userId == UserHandle.USER_NULL) { + return null; + } + + final ComponentName ownerComponent = mDpm.getProfileOwnerAsUser(userId); + if (ownerComponent == null) { + return null; + } + + // Only search for the required action in the Profile Owner's package + final Intent intent = + new Intent(mResources.getString(R.string.config_work_policy_info_intent_action)) + .setPackage(ownerComponent.getPackageName()) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + final List<ResolveInfo> activities = mPm.queryIntentActivitiesAsUser(intent, 0, userId); + if (activities.size() != 0) { + return intent; + } + + return null; } protected static class EnterprisePrivacySpan extends ClickableSpan { diff --git a/src/com/android/settings/enterprise/EnterprisePrivacyPreferenceController.java b/src/com/android/settings/enterprise/EnterprisePrivacyPreferenceController.java index e0c287df9e..b07eb91114 100644 --- a/src/com/android/settings/enterprise/EnterprisePrivacyPreferenceController.java +++ b/src/com/android/settings/enterprise/EnterprisePrivacyPreferenceController.java @@ -14,6 +14,7 @@ package com.android.settings.enterprise; import android.content.Context; + import androidx.preference.Preference; import com.android.settings.R; diff --git a/src/com/android/settings/enterprise/EnterprisePrivacySettings.java b/src/com/android/settings/enterprise/EnterprisePrivacySettings.java index 92ae38df94..c5beb86191 100644 --- a/src/com/android/settings/enterprise/EnterprisePrivacySettings.java +++ b/src/com/android/settings/enterprise/EnterprisePrivacySettings.java @@ -16,28 +16,30 @@ package com.android.settings.enterprise; +import android.app.settings.SettingsEnums; import android.content.Context; import android.provider.SearchIndexableResource; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.overlay.FeatureFactory; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.widget.PreferenceCategoryController; import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +@SearchIndexable public class EnterprisePrivacySettings extends DashboardFragment { static final String TAG = "EnterprisePrivacySettings"; @Override public int getMetricsCategory() { - return MetricsEvent.ENTERPRISE_PRIVACY_SETTINGS; + return SettingsEnums.ENTERPRISE_PRIVACY_SETTINGS; } @Override @@ -82,7 +84,6 @@ public class EnterprisePrivacySettings extends DashboardFragment { exposureChangesCategoryControllers.add(new CaCertsCurrentUserPreferenceController(context)); exposureChangesCategoryControllers.add(new CaCertsManagedProfilePreferenceController( context)); - exposureChangesCategoryControllers.add(new BackupsEnabledPreferenceController(context)); controllers.addAll(exposureChangesCategoryControllers); controllers.add(new PreferenceCategoryController(context, "exposure_changes_category") .setChildren(exposureChangesCategoryControllers)); diff --git a/src/com/android/settings/enterprise/EnterpriseSetDefaultAppsListFragment.java b/src/com/android/settings/enterprise/EnterpriseSetDefaultAppsListFragment.java index 1788c333e4..67bf68786a 100644 --- a/src/com/android/settings/enterprise/EnterpriseSetDefaultAppsListFragment.java +++ b/src/com/android/settings/enterprise/EnterpriseSetDefaultAppsListFragment.java @@ -16,9 +16,9 @@ package com.android.settings.enterprise; +import android.app.settings.SettingsEnums; import android.content.Context; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; import com.android.settingslib.core.AbstractPreferenceController; @@ -34,7 +34,7 @@ public class EnterpriseSetDefaultAppsListFragment extends DashboardFragment { @Override public int getMetricsCategory() { - return MetricsEvent.ENTERPRISE_PRIVACY_DEFAULT_APPS; + return SettingsEnums.ENTERPRISE_PRIVACY_DEFAULT_APPS; } @Override diff --git a/src/com/android/settings/enterprise/EnterpriseSetDefaultAppsListPreferenceController.java b/src/com/android/settings/enterprise/EnterpriseSetDefaultAppsListPreferenceController.java index 876d706d4a..e3136f4a5c 100644 --- a/src/com/android/settings/enterprise/EnterpriseSetDefaultAppsListPreferenceController.java +++ b/src/com/android/settings/enterprise/EnterpriseSetDefaultAppsListPreferenceController.java @@ -21,6 +21,7 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.UserInfo; import android.os.UserHandle; + import androidx.preference.Preference; import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceGroup; diff --git a/src/com/android/settings/enterprise/EnterpriseSetDefaultAppsPreferenceController.java b/src/com/android/settings/enterprise/EnterpriseSetDefaultAppsPreferenceController.java index 63a552d0ac..b2f9459d8a 100644 --- a/src/com/android/settings/enterprise/EnterpriseSetDefaultAppsPreferenceController.java +++ b/src/com/android/settings/enterprise/EnterpriseSetDefaultAppsPreferenceController.java @@ -16,6 +16,7 @@ package com.android.settings.enterprise; import android.content.Context; import android.os.UserHandle; + import androidx.preference.Preference; import com.android.settings.R; diff --git a/src/com/android/settings/enterprise/FailedPasswordWipePreferenceControllerBase.java b/src/com/android/settings/enterprise/FailedPasswordWipePreferenceControllerBase.java index 2a2a94e704..322589ea3f 100644 --- a/src/com/android/settings/enterprise/FailedPasswordWipePreferenceControllerBase.java +++ b/src/com/android/settings/enterprise/FailedPasswordWipePreferenceControllerBase.java @@ -15,6 +15,7 @@ package com.android.settings.enterprise; import android.content.Context; + import androidx.preference.Preference; import com.android.settings.R; diff --git a/src/com/android/settings/enterprise/ImePreferenceController.java b/src/com/android/settings/enterprise/ImePreferenceController.java index fa4e7aa1a9..51a24a2762 100644 --- a/src/com/android/settings/enterprise/ImePreferenceController.java +++ b/src/com/android/settings/enterprise/ImePreferenceController.java @@ -15,6 +15,7 @@ package com.android.settings.enterprise; import android.content.Context; + import androidx.preference.Preference; import com.android.settings.R; diff --git a/src/com/android/settings/enterprise/ManageDeviceAdminPreferenceController.java b/src/com/android/settings/enterprise/ManageDeviceAdminPreferenceController.java index e4f2e76f7d..1b88e38583 100644 --- a/src/com/android/settings/enterprise/ManageDeviceAdminPreferenceController.java +++ b/src/com/android/settings/enterprise/ManageDeviceAdminPreferenceController.java @@ -14,42 +14,37 @@ package com.android.settings.enterprise; import android.content.Context; -import androidx.preference.Preference; import com.android.settings.R; -import com.android.settings.core.PreferenceControllerMixin; +import com.android.settings.core.BasePreferenceController; import com.android.settings.overlay.FeatureFactory; -import com.android.settingslib.core.AbstractPreferenceController; -public class ManageDeviceAdminPreferenceController extends AbstractPreferenceController implements - PreferenceControllerMixin { - private static final String KEY_MANAGE_DEVICE_ADMIN = "manage_device_admin"; +public class ManageDeviceAdminPreferenceController extends BasePreferenceController { + private final EnterprisePrivacyFeatureProvider mFeatureProvider; - public ManageDeviceAdminPreferenceController(Context context) { - super(context); + public ManageDeviceAdminPreferenceController(Context context, String key) { + super(context, key); mFeatureProvider = FeatureFactory.getFactory(context) .getEnterprisePrivacyFeatureProvider(context); } @Override - public void updateState(Preference preference) { + public CharSequence getSummary() { final int activeAdmins = mFeatureProvider.getNumberOfActiveDeviceAdminsForCurrentUserAndManagedProfile(); - preference.setSummary(activeAdmins == 0 + return activeAdmins == 0 ? mContext.getResources().getString(R.string.number_of_device_admins_none) : mContext.getResources().getQuantityString(R.plurals.number_of_device_admins, - activeAdmins, activeAdmins)); + activeAdmins, activeAdmins); } @Override - public boolean isAvailable() { - return mContext.getResources().getBoolean(R.bool.config_show_manage_device_admin); + public int getAvailabilityStatus() { + return mContext.getResources().getBoolean(R.bool.config_show_manage_device_admin) + ? AVAILABLE_UNSEARCHABLE + : UNSUPPORTED_ON_DEVICE; } - @Override - public String getPreferenceKey() { - return KEY_MANAGE_DEVICE_ADMIN; - } } diff --git a/src/com/android/settings/fingerprint/FingerprintEnrollBase.java b/src/com/android/settings/fingerprint/FingerprintEnrollBase.java deleted file mode 100644 index 5a148d3343..0000000000 --- a/src/com/android/settings/fingerprint/FingerprintEnrollBase.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * 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.fingerprint; - -import android.annotation.Nullable; -import android.content.Intent; -import android.content.res.Resources; -import android.graphics.Color; -import android.os.Bundle; -import android.os.UserHandle; -import android.text.TextUtils; -import android.view.View; -import android.widget.Button; -import android.widget.TextView; - -import com.android.settings.R; -import com.android.settings.SetupWizardUtils; -import com.android.settings.core.InstrumentedActivity; -import com.android.settings.password.ChooseLockSettingsHelper; -import com.android.setupwizardlib.GlifLayout; - -/** - * Base activity for all fingerprint enrollment steps. - */ -public abstract class FingerprintEnrollBase extends InstrumentedActivity - implements View.OnClickListener { - public 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; - protected int mUserId; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - mToken = getIntent().getByteArrayExtra( - ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN); - if (savedInstanceState != null && mToken == null) { - mToken = savedInstanceState.getByteArray( - ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN); - } - mUserId = getIntent().getIntExtra(Intent.EXTRA_USER_ID, UserHandle.myUserId()); - } - - @Override - protected void onApplyThemeResource(Resources.Theme theme, int resid, boolean first) { - resid = SetupWizardUtils.getTheme(getIntent()); - super.onApplyThemeResource(theme, resid, first); - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.putByteArray(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, mToken); - } - - @Override - protected void onPostCreate(@Nullable Bundle savedInstanceState) { - super.onPostCreate(savedInstanceState); - initViews(); - } - - protected void initViews() { - getWindow().setStatusBarColor(Color.TRANSPARENT); - Button nextButton = getNextButton(); - if (nextButton != null) { - nextButton.setOnClickListener(this); - } - } - - protected GlifLayout getLayout() { - return (GlifLayout) findViewById(R.id.setup_wizard_layout); - } - - protected void setHeaderText(int resId, boolean force) { - TextView layoutTitle = getLayout().getHeaderTextView(); - CharSequence previousTitle = layoutTitle.getText(); - CharSequence title = getText(resId); - if (previousTitle != title || force) { - if (!TextUtils.isEmpty(previousTitle)) { - layoutTitle.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); - } - getLayout().setHeaderText(title); - setTitle(title); - } - } - - protected void setHeaderText(int resId) { - setHeaderText(resId, false /* force */); - } - - protected Button getNextButton() { - return (Button) findViewById(R.id.next_button); - } - - @Override - public void onClick(View v) { - if (v == getNextButton()) { - onNextButtonClick(); - } - } - - protected void onNextButtonClick() { - } - - protected Intent getEnrollingIntent() { - Intent intent = new Intent(); - intent.setClassName("com.android.settings", FingerprintEnrollEnrolling.class.getName()); - intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, mToken); - if (mUserId != UserHandle.USER_NULL) { - intent.putExtra(Intent.EXTRA_USER_ID, mUserId); - } - return intent; - } -} diff --git a/src/com/android/settings/fingerprint/FingerprintEnrollFinish.java b/src/com/android/settings/fingerprint/FingerprintEnrollFinish.java deleted file mode 100644 index 4bd438c797..0000000000 --- a/src/com/android/settings/fingerprint/FingerprintEnrollFinish.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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.fingerprint; - -import android.content.Intent; -import android.hardware.fingerprint.FingerprintManager; -import android.os.Bundle; -import android.view.View; -import android.widget.Button; - -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.settings.R; -import com.android.settings.Utils; - -/** - * Activity which concludes fingerprint enrollment. - */ -public class FingerprintEnrollFinish extends FingerprintEnrollBase { - - private static final int REQUEST_ADD_ANOTHER = 1; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.fingerprint_enroll_finish); - setHeaderText(R.string.security_settings_fingerprint_enroll_finish_title); - } - - @Override - protected void onResume() { - super.onResume(); - - Button addButton = (Button) findViewById(R.id.add_another_button); - - final FingerprintManager fpm = Utils.getFingerprintManagerOrNull(this); - boolean hideAddAnother = false; - if (fpm != null) { - int enrolled = fpm.getEnrolledFingerprints(mUserId).size(); - int max = getResources().getInteger( - com.android.internal.R.integer.config_fingerprintMaxTemplatesPerUser); - hideAddAnother = enrolled >= max; - } - if (hideAddAnother) { - // Don't show "Add" button if too many fingerprints already added - addButton.setVisibility(View.INVISIBLE); - } else { - addButton.setOnClickListener(this); - } - } - - @Override - protected void onNextButtonClick() { - setResult(RESULT_FINISHED); - finish(); - } - - @Override - public void onClick(View v) { - if (v.getId() == R.id.add_another_button) { - startActivityForResult(getEnrollingIntent(), REQUEST_ADD_ANOTHER); - } - super.onClick(v); - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == REQUEST_ADD_ANOTHER && resultCode != RESULT_CANCELED) { - setResult(resultCode, data); - finish(); - } else { - super.onActivityResult(requestCode, resultCode, data); - } - } - - @Override - public int getMetricsCategory() { - return MetricsEvent.FINGERPRINT_ENROLL_FINISH; - } -} diff --git a/src/com/android/settings/fingerprint/FingerprintEnrollIntroduction.java b/src/com/android/settings/fingerprint/FingerprintEnrollIntroduction.java deleted file mode 100644 index dd831219ad..0000000000 --- a/src/com/android/settings/fingerprint/FingerprintEnrollIntroduction.java +++ /dev/null @@ -1,233 +0,0 @@ -/* - * 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.fingerprint; - -import android.app.admin.DevicePolicyManager; -import android.content.ActivityNotFoundException; -import android.content.Intent; -import android.hardware.fingerprint.FingerprintManager; -import android.os.Bundle; -import android.os.UserHandle; -import android.os.UserManager; -import android.util.Log; -import android.view.View; -import android.widget.Button; -import android.widget.TextView; - -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.settings.R; -import com.android.settings.Utils; -import com.android.settings.password.ChooseLockGeneric; -import com.android.settings.password.ChooseLockSettingsHelper; -import com.android.settingslib.HelpUtils; -import com.android.settingslib.RestrictedLockUtils; -import com.android.setupwizardlib.span.LinkSpan; - -/** - * Onboarding activity for fingerprint enrollment. - */ -public class FingerprintEnrollIntroduction extends FingerprintEnrollBase - implements View.OnClickListener, LinkSpan.OnClickListener { - - private static final String TAG = "FingerprintIntro"; - - protected static final int CHOOSE_LOCK_GENERIC_REQUEST = 1; - protected static final int FINGERPRINT_FIND_SENSOR_REQUEST = 2; - protected static final int LEARN_MORE_REQUEST = 3; - - private UserManager mUserManager; - private boolean mHasPassword; - private boolean mFingerprintUnlockDisabledByAdmin; - private TextView mErrorText; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - mFingerprintUnlockDisabledByAdmin = RestrictedLockUtils.checkIfKeyguardFeaturesDisabled( - this, DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT, mUserId) != null; - - setContentView(R.layout.fingerprint_enroll_introduction); - if (mFingerprintUnlockDisabledByAdmin) { - setHeaderText(R.string - .security_settings_fingerprint_enroll_introduction_title_unlock_disabled); - } else { - setHeaderText(R.string.security_settings_fingerprint_enroll_introduction_title); - } - - Button cancelButton = (Button) findViewById(R.id.fingerprint_cancel_button); - cancelButton.setOnClickListener(this); - - mErrorText = (TextView) findViewById(R.id.error_text); - - mUserManager = UserManager.get(this); - updatePasswordQuality(); - } - - @Override - protected void onResume() { - super.onResume(); - - final FingerprintManager fingerprintManager = Utils.getFingerprintManagerOrNull(this); - int errorMsg = 0; - if (fingerprintManager != null) { - final int max = getResources().getInteger( - com.android.internal.R.integer.config_fingerprintMaxTemplatesPerUser); - final int numEnrolledFingerprints = - fingerprintManager.getEnrolledFingerprints(mUserId).size(); - if (numEnrolledFingerprints >= max) { - errorMsg = R.string.fingerprint_intro_error_max; - } - } else { - errorMsg = R.string.fingerprint_intro_error_unknown; - } - if (errorMsg == 0) { - mErrorText.setText(null); - getNextButton().setVisibility(View.VISIBLE); - } else { - mErrorText.setText(errorMsg); - getNextButton().setVisibility(View.GONE); - } - } - - private void updatePasswordQuality() { - final int passwordQuality = new ChooseLockSettingsHelper(this).utils() - .getActivePasswordQuality(mUserManager.getCredentialOwnerProfile(mUserId)); - mHasPassword = passwordQuality != DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED; - } - - @Override - protected Button getNextButton() { - return (Button) findViewById(R.id.fingerprint_next_button); - } - - @Override - protected void onNextButtonClick() { - if (!mHasPassword) { - // No fingerprints registered, launch into enrollment wizard. - launchChooseLock(); - } else { - // Lock thingy is already set up, launch directly into find sensor step from wizard. - launchFindSensor(null); - } - } - - private void launchChooseLock() { - Intent intent = getChooseLockIntent(); - long challenge = Utils.getFingerprintManagerOrNull(this).preEnroll(); - intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment.MINIMUM_QUALITY_KEY, - DevicePolicyManager.PASSWORD_QUALITY_SOMETHING); - intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment.HIDE_DISABLED_PREFS, true); - intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_HAS_CHALLENGE, true); - intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE, challenge); - intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FINGERPRINT, true); - if (mUserId != UserHandle.USER_NULL) { - intent.putExtra(Intent.EXTRA_USER_ID, mUserId); - } - startActivityForResult(intent, CHOOSE_LOCK_GENERIC_REQUEST); - } - - private void launchFindSensor(byte[] token) { - Intent intent = getFindSensorIntent(); - if (token != null) { - intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token); - } - if (mUserId != UserHandle.USER_NULL) { - intent.putExtra(Intent.EXTRA_USER_ID, mUserId); - } - startActivityForResult(intent, FINGERPRINT_FIND_SENSOR_REQUEST); - } - - protected Intent getChooseLockIntent() { - return new Intent(this, ChooseLockGeneric.class); - } - - protected Intent getFindSensorIntent() { - return new Intent(this, FingerprintEnrollFindSensor.class); - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - final boolean isResultFinished = resultCode == RESULT_FINISHED; - if (requestCode == FINGERPRINT_FIND_SENSOR_REQUEST) { - if (isResultFinished || resultCode == RESULT_SKIP) { - final int result = isResultFinished ? RESULT_OK : RESULT_SKIP; - setResult(result, data); - finish(); - return; - } - } else if (requestCode == CHOOSE_LOCK_GENERIC_REQUEST) { - if (isResultFinished) { - updatePasswordQuality(); - byte[] token = data.getByteArrayExtra( - ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN); - launchFindSensor(token); - return; - } - } else if (requestCode == LEARN_MORE_REQUEST) { - overridePendingTransition(R.anim.suw_slide_back_in, R.anim.suw_slide_back_out); - } - super.onActivityResult(requestCode, resultCode, data); - } - - @Override - public void onClick(View v) { - if (v.getId() == R.id.fingerprint_cancel_button) { - onCancelButtonClick(); - } else { - super.onClick(v); - } - } - - @Override - public int getMetricsCategory() { - return MetricsEvent.FINGERPRINT_ENROLL_INTRO; - } - - protected void onCancelButtonClick() { - finish(); - } - - @Override - protected void initViews() { - super.initViews(); - - TextView description = (TextView) findViewById(R.id.description_text); - if (mFingerprintUnlockDisabledByAdmin) { - description.setText(R.string - .security_settings_fingerprint_enroll_introduction_message_unlock_disabled); - } - } - - @Override - public void onClick(LinkSpan span) { - if ("url".equals(span.getId())) { - String url = getString(R.string.help_url_fingerprint); - Intent intent = HelpUtils.getHelpIntent(this, url, getClass().getName()); - if (intent == null) { - Log.w(TAG, "Null help intent."); - return; - } - try { - // This needs to be startActivityForResult even though we do not care about the - // actual result because the help app needs to know about who invoked it. - startActivityForResult(intent, LEARN_MORE_REQUEST); - } catch (ActivityNotFoundException e) { - Log.w(TAG, "Activity was not found for intent, " + e); - } - } - } -} diff --git a/src/com/android/settings/fingerprint/FingerprintUiHelper.java b/src/com/android/settings/fingerprint/FingerprintUiHelper.java deleted file mode 100644 index 4a67ecd5c0..0000000000 --- a/src/com/android/settings/fingerprint/FingerprintUiHelper.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * 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.fingerprint; - -import android.hardware.fingerprint.FingerprintManager; -import android.os.CancellationSignal; -import android.view.View; -import android.widget.ImageView; -import android.widget.TextView; - -import com.android.settings.R; -import com.android.settings.Utils; - -/** - * Small helper class to manage text/icon around fingerprint authentication UI. - */ -public class FingerprintUiHelper extends FingerprintManager.AuthenticationCallback { - - private static final long ERROR_TIMEOUT = 1300; - - private ImageView mIcon; - private TextView mErrorTextView; - private CancellationSignal mCancellationSignal; - private int mUserId; - - private Callback mCallback; - private FingerprintManager mFingerprintManager; - - public FingerprintUiHelper(ImageView icon, TextView errorTextView, Callback callback, - int userId) { - mFingerprintManager = Utils.getFingerprintManagerOrNull(icon.getContext()); - mIcon = icon; - mErrorTextView = errorTextView; - mCallback = callback; - mUserId = userId; - } - - public void startListening() { - if (mFingerprintManager != null && mFingerprintManager.isHardwareDetected() - && mFingerprintManager.getEnrolledFingerprints(mUserId).size() > 0) { - mCancellationSignal = new CancellationSignal(); - mFingerprintManager.setActiveUser(mUserId); - mFingerprintManager.authenticate( - null, mCancellationSignal, 0 /* flags */, this, null, mUserId); - setFingerprintIconVisibility(true); - mIcon.setImageResource(R.drawable.ic_fingerprint); - } - } - - public void stopListening() { - if (mCancellationSignal != null) { - mCancellationSignal.cancel(); - mCancellationSignal = null; - } - } - - public boolean isListening() { - return mCancellationSignal != null && !mCancellationSignal.isCanceled(); - } - - private void setFingerprintIconVisibility(boolean visible) { - mIcon.setVisibility(visible ? View.VISIBLE : View.GONE); - mCallback.onFingerprintIconVisibilityChanged(visible); - } - - @Override - public void onAuthenticationError(int errMsgId, CharSequence errString) { - if (errMsgId == FingerprintManager.FINGERPRINT_ERROR_CANCELED) { - // Only happens if we get preempted by another activity. Ignored. - return; - } - showError(errString); - setFingerprintIconVisibility(false); - } - - @Override - public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) { - showError(helpString); - } - - @Override - public void onAuthenticationFailed() { - showError(mIcon.getResources().getString( - R.string.fingerprint_not_recognized)); - } - - @Override - public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) { - mIcon.setImageResource(R.drawable.ic_fingerprint_success); - mCallback.onAuthenticated(); - } - - private void showError(CharSequence error) { - if (!isListening()) { - return; - } - - mIcon.setImageResource(R.drawable.ic_fingerprint_error); - mErrorTextView.setText(error); - mErrorTextView.removeCallbacks(mResetErrorTextRunnable); - mErrorTextView.postDelayed(mResetErrorTextRunnable, ERROR_TIMEOUT); - } - - private Runnable mResetErrorTextRunnable = new Runnable() { - @Override - public void run() { - mErrorTextView.setText(""); - mIcon.setImageResource(R.drawable.ic_fingerprint); - } - }; - - public interface Callback { - void onAuthenticated(); - void onFingerprintIconVisibilityChanged(boolean visible); - } -} diff --git a/src/com/android/settings/flashlight/FlashlightHandleActivity.java b/src/com/android/settings/flashlight/FlashlightHandleActivity.java new file mode 100644 index 0000000000..0c50f91c89 --- /dev/null +++ b/src/com/android/settings/flashlight/FlashlightHandleActivity.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2019 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.flashlight; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.provider.Settings; + +import com.android.settings.R; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; +import com.android.settings.search.SearchIndexableRaw; +import com.android.settingslib.search.SearchIndexable; + +import java.util.ArrayList; +import java.util.List; + +/** + * Headless activity that toggles flashlight state when launched. + */ +@SearchIndexable(forTarget = SearchIndexable.MOBILE) +public class FlashlightHandleActivity extends Activity implements Indexable { + + public static final String EXTRA_FALLBACK_TO_HOMEPAGE = "fallback_to_homepage"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // Do nothing meaningful in this activity. + // The sole purpose of this activity is to provide a place to index flashlight + // into Settings search. + + // Caller's choice: fallback to homepage, or just exit? + if (getIntent().getBooleanExtra(EXTRA_FALLBACK_TO_HOMEPAGE, false)) { + startActivity(new Intent(Settings.ACTION_SETTINGS)); + } + finish(); + } + + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + + @Override + public List<SearchIndexableRaw> getRawDataToIndex(Context context, + boolean enabled) { + + final List<SearchIndexableRaw> result = new ArrayList<>(); + + SearchIndexableRaw data = new SearchIndexableRaw(context); + data.title = context.getString(R.string.power_flashlight); + data.screenTitle = context.getString(R.string.power_flashlight); + data.keywords = context.getString(R.string.keywords_flashlight); + data.intentTargetPackage = context.getPackageName(); + data.intentTargetClass = FlashlightHandleActivity.class.getName(); + data.intentAction = Intent.ACTION_MAIN; + data.key = "flashlight"; + result.add(data); + + return result; + } + }; +} diff --git a/src/com/android/settings/flashlight/FlashlightSlice.java b/src/com/android/settings/flashlight/FlashlightSlice.java new file mode 100644 index 0000000000..a2c4561c1d --- /dev/null +++ b/src/com/android/settings/flashlight/FlashlightSlice.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2018 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.flashlight; + +import static android.app.slice.Slice.EXTRA_TOGGLE_STATE; + +import static androidx.slice.builders.ListBuilder.ICON_IMAGE; + +import android.annotation.ColorInt; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraManager; +import android.net.Uri; +import android.provider.Settings; +import android.provider.Settings.Secure; +import android.util.Log; + +import androidx.core.graphics.drawable.IconCompat; +import androidx.slice.Slice; +import androidx.slice.builders.ListBuilder; +import androidx.slice.builders.ListBuilder.RowBuilder; +import androidx.slice.builders.SliceAction; + +import com.android.settings.R; +import com.android.settings.Utils; +import com.android.settings.slices.CustomSliceRegistry; +import com.android.settings.slices.CustomSliceable; + + +/** + * Utility class to build a Flashlight Slice, and handle all associated actions. + */ +public class FlashlightSlice implements CustomSliceable { + + private static final String TAG = "FlashlightSlice"; + + /** + * Action broadcasting a change on whether flashlight is on or off. + */ + private static final String ACTION_FLASHLIGHT_CHANGED = + "com.android.settings.flashlight.action.FLASHLIGHT_CHANGED"; + + private final Context mContext; + + public FlashlightSlice(Context context) { + mContext = context; + } + + @Override + public Slice getSlice() { + if (!isFlashlightAvailable(mContext)) { + return null; + } + final PendingIntent toggleAction = getBroadcastIntent(mContext); + @ColorInt final int color = Utils.getColorAccentDefaultColor(mContext); + final IconCompat icon = + IconCompat.createWithResource(mContext, R.drawable.ic_signal_flashlight); + return new ListBuilder(mContext, CustomSliceRegistry.FLASHLIGHT_SLICE_URI, + ListBuilder.INFINITY) + .setAccentColor(color) + .addRow(new RowBuilder() + .setTitle(mContext.getText(R.string.power_flashlight)) + .setTitleItem(icon, ICON_IMAGE) + .setPrimaryAction( + SliceAction.createToggle(toggleAction, null, + isFlashlightEnabled(mContext)))) + .build(); + } + + @Override + public Uri getUri() { + return CustomSliceRegistry.FLASHLIGHT_SLICE_URI; + } + + @Override + public IntentFilter getIntentFilter() { + return new IntentFilter(ACTION_FLASHLIGHT_CHANGED); + } + + @Override + public void onNotifyChange(Intent intent) { + try { + final String cameraId = getCameraId(mContext); + if (cameraId != null) { + final boolean state = intent.getBooleanExtra( + EXTRA_TOGGLE_STATE, isFlashlightEnabled(mContext)); + final CameraManager cameraManager = mContext.getSystemService(CameraManager.class); + cameraManager.setTorchMode(cameraId, state); + } + } catch (CameraAccessException e) { + Log.e(TAG, "Camera couldn't set torch mode.", e); + } + mContext.getContentResolver().notifyChange(CustomSliceRegistry.FLASHLIGHT_SLICE_URI, null); + } + + @Override + public Intent getIntent() { + return null; + } + + private static String getCameraId(Context context) throws CameraAccessException { + final CameraManager cameraManager = context.getSystemService(CameraManager.class); + final String[] ids = cameraManager.getCameraIdList(); + for (String id : ids) { + CameraCharacteristics c = cameraManager.getCameraCharacteristics(id); + Boolean flashAvailable = c.get(CameraCharacteristics.FLASH_INFO_AVAILABLE); + Integer lensFacing = c.get(CameraCharacteristics.LENS_FACING); + if (flashAvailable != null && flashAvailable + && lensFacing != null && lensFacing == CameraCharacteristics.LENS_FACING_BACK) { + return id; + } + } + return null; + } + + + private static boolean isFlashlightAvailable(Context context) { + return Settings.Secure.getInt( + context.getContentResolver(), Secure.FLASHLIGHT_AVAILABLE, 0) == 1; + } + + private static boolean isFlashlightEnabled(Context context) { + return Settings.Secure.getInt( + context.getContentResolver(), Secure.FLASHLIGHT_ENABLED, 0) == 1; + } +} diff --git a/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java b/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java index badcb53547..e8d5f3330f 100644 --- a/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java +++ b/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java @@ -19,38 +19,31 @@ package com.android.settings.fuelgauge; import android.annotation.UserIdInt; import android.app.Activity; import android.app.ActivityManager; -import android.app.LoaderManager; -import android.app.admin.DevicePolicyManager; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; -import android.content.Loader; import android.content.pm.PackageManager; import android.os.BatteryStats; import android.os.Bundle; import android.os.UserHandle; -import android.os.UserManager; -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; import android.text.TextUtils; import android.util.Log; import android.view.View; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; + import com.android.internal.os.BatterySipper; import com.android.internal.os.BatteryStatsHelper; import com.android.internal.util.ArrayUtils; import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.Utils; -import com.android.settings.applications.LayoutPreference; +import com.android.settings.applications.appinfo.AppButtonsPreferenceController; +import com.android.settings.applications.appinfo.ButtonActionDialogFragment; import com.android.settings.core.InstrumentedPreferenceFragment; import com.android.settings.core.SubSettingLauncher; import com.android.settings.dashboard.DashboardFragment; -import com.android.settings.fuelgauge.anomaly.Anomaly; -import com.android.settings.fuelgauge.anomaly.AnomalyDialogFragment; -import com.android.settings.fuelgauge.anomaly.AnomalyLoader; -import com.android.settings.fuelgauge.anomaly.AnomalySummaryPreferenceController; -import com.android.settings.fuelgauge.anomaly.AnomalyUtils; import com.android.settings.fuelgauge.batterytip.BatteryTipPreferenceController; import com.android.settings.fuelgauge.batterytip.tips.BatteryTip; import com.android.settings.widget.EntityHeaderController; @@ -58,6 +51,7 @@ import com.android.settingslib.applications.AppUtils; import com.android.settingslib.applications.ApplicationsState; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.utils.StringUtil; +import com.android.settingslib.widget.LayoutPreference; import java.util.ArrayList; import java.util.List; @@ -70,8 +64,6 @@ import java.util.List; */ public class AdvancedPowerUsageDetail extends DashboardFragment implements ButtonActionDialogFragment.AppButtonsDialogListener, - AnomalyDialogFragment.AnomalyDialogListener, - LoaderManager.LoaderCallbacks<List<Anomaly>>, BatteryTipPreferenceController.BatteryTipListener { public static final String TAG = "AdvancedPowerDetail"; @@ -83,7 +75,6 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements public static final String EXTRA_ICON_ID = "extra_icon_id"; public static final String EXTRA_POWER_USAGE_PERCENT = "extra_power_usage_percent"; public static final String EXTRA_POWER_USAGE_AMOUNT = "extra_power_usage_amount"; - public static final String EXTRA_ANOMALY_LIST = "extra_anomaly_list"; private static final String KEY_PREF_FOREGROUND = "app_usage_foreground"; private static final String KEY_PREF_BACKGROUND = "app_usage_background"; @@ -92,8 +83,6 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements private static final int REQUEST_UNINSTALL = 0; private static final int REQUEST_REMOVE_DEVICE_ADMIN = 1; - private static final int ANOMALY_LOADER = 0; - @VisibleForTesting LayoutPreference mHeaderPreference; @VisibleForTesting @@ -107,21 +96,15 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements Preference mForegroundPreference; @VisibleForTesting Preference mBackgroundPreference; - @VisibleForTesting - AnomalySummaryPreferenceController mAnomalySummaryPreferenceController; private AppButtonsPreferenceController mAppButtonsPreferenceController; private BackgroundActivityPreferenceController mBackgroundActivityPreferenceController; - private DevicePolicyManager mDpm; - private UserManager mUserManager; - private PackageManager mPackageManager; - private List<Anomaly> mAnomalies; private String mPackageName; @VisibleForTesting static void startBatteryDetailPage(Activity caller, BatteryUtils batteryUtils, InstrumentedPreferenceFragment fragment, BatteryStatsHelper helper, int which, - BatteryEntry entry, String usagePercent, List<Anomaly> anomalies) { + BatteryEntry entry, String usagePercent) { // Initialize mStats if necessary. helper.getStats(); @@ -152,11 +135,10 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements args.putLong(EXTRA_FOREGROUND_TIME, foregroundTimeMs); args.putString(EXTRA_POWER_USAGE_PERCENT, usagePercent); args.putInt(EXTRA_POWER_USAGE_AMOUNT, (int) sipper.totalPowerMah); - args.putParcelableList(EXTRA_ANOMALY_LIST, anomalies); new SubSettingLauncher(caller) .setDestination(AdvancedPowerUsageDetail.class.getName()) - .setTitle(R.string.battery_details_title) + .setTitleRes(R.string.battery_details_title) .setArguments(args) .setSourceMetricsCategory(fragment.getMetricsCategory()) .setUserHandle(new UserHandle(getUserIdToLaunchAdvancePowerUsageDetail(sipper))) @@ -173,9 +155,9 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements public static void startBatteryDetailPage(Activity caller, InstrumentedPreferenceFragment fragment, BatteryStatsHelper helper, int which, - BatteryEntry entry, String usagePercent, List<Anomaly> anomalies) { + BatteryEntry entry, String usagePercent) { startBatteryDetailPage(caller, BatteryUtils.getInstance(caller), fragment, helper, which, - entry, usagePercent, anomalies); + entry, usagePercent); } public static void startBatteryDetailPage(Activity caller, @@ -187,12 +169,12 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements try { args.putInt(EXTRA_UID, packageManager.getPackageUid(packageName, 0 /* no flag */)); } catch (PackageManager.NameNotFoundException e) { - Log.e(TAG, "Cannot find package: " + packageName, e); + Log.w(TAG, "Cannot find package: " + packageName, e); } new SubSettingLauncher(caller) .setDestination(AdvancedPowerUsageDetail.class.getName()) - .setTitle(R.string.battery_details_title) + .setTitleRes(R.string.battery_details_title) .setArguments(args) .setSourceMetricsCategory(fragment.getMetricsCategory()) .launch(); @@ -203,9 +185,6 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements super.onAttach(activity); mState = ApplicationsState.getInstance(getActivity().getApplication()); - mDpm = (DevicePolicyManager) activity.getSystemService(Context.DEVICE_POLICY_SERVICE); - mUserManager = (UserManager) activity.getSystemService(Context.USER_SERVICE); - mPackageManager = activity.getPackageManager(); mBatteryUtils = BatteryUtils.getInstance(getContext()); } @@ -214,15 +193,12 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements super.onCreate(icicle); mPackageName = getArguments().getString(EXTRA_PACKAGE_NAME); - mAnomalySummaryPreferenceController = new AnomalySummaryPreferenceController( - (SettingsActivity) getActivity(), this); mForegroundPreference = findPreference(KEY_PREF_FOREGROUND); mBackgroundPreference = findPreference(KEY_PREF_BACKGROUND); mHeaderPreference = (LayoutPreference) findPreference(KEY_PREF_HEADER); if (mPackageName != null) { mAppEntry = mState.getEntry(mPackageName, UserHandle.myUserId()); - initAnomalyInfo(); } } @@ -235,23 +211,13 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements } @VisibleForTesting - void initAnomalyInfo() { - mAnomalies = getArguments().getParcelableArrayList(EXTRA_ANOMALY_LIST); - if (mAnomalies == null) { - getLoaderManager().initLoader(ANOMALY_LOADER, Bundle.EMPTY, this); - } else if (mAnomalies != null) { - mAnomalySummaryPreferenceController.updateAnomalySummaryPreference(mAnomalies); - } - } - - @VisibleForTesting void initHeader() { final View appSnippet = mHeaderPreference.findViewById(R.id.entity_header); final Activity context = getActivity(); final Bundle bundle = getArguments(); EntityHeaderController controller = EntityHeaderController .newInstance(context, this, appSnippet) - .setRecyclerView(getListView(), getLifecycle()) + .setRecyclerView(getListView(), getSettingsLifecycle()) .setButtonActions(EntityHeaderController.ActionType.ACTION_NONE, EntityHeaderController.ActionType.ACTION_NONE); @@ -269,10 +235,7 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements controller.setLabel(mAppEntry); controller.setIcon(mAppEntry); boolean isInstantApp = AppUtils.isInstant(mAppEntry.info); - CharSequence summary = isInstantApp - ? null : getString(Utils.getInstallationStatus(mAppEntry.info)); controller.setIsInstantApp(AppUtils.isInstant(mAppEntry.info)); - controller.setSummary(summary); } controller.done(context, true /* rebindActions */); @@ -285,8 +248,6 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements final long foregroundTimeMs = bundle.getLong(EXTRA_FOREGROUND_TIME); final long backgroundTimeMs = bundle.getLong(EXTRA_BACKGROUND_TIME); - final String usagePercent = bundle.getString(EXTRA_POWER_USAGE_PERCENT); - final int powerMah = bundle.getInt(EXTRA_POWER_USAGE_AMOUNT); mForegroundPreference.setSummary( TextUtils.expandTemplate(getText(R.string.battery_used_for), StringUtil.formatElapsedTime(context, foregroundTimeMs, false))); @@ -296,17 +257,8 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements } @Override - public boolean onPreferenceTreeClick(Preference preference) { - if (TextUtils.equals(preference.getKey(), AnomalySummaryPreferenceController.ANOMALY_KEY)) { - mAnomalySummaryPreferenceController.onPreferenceTreeClick(preference); - return true; - } - return super.onPreferenceTreeClick(preference); - } - - @Override public int getMetricsCategory() { - return MetricsEvent.FUELGAUGE_POWER_USAGE_DETAIL; + return SettingsEnums.FUELGAUGE_POWER_USAGE_DETAIL; } @Override @@ -332,8 +284,8 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements controllers.add(new BatteryOptimizationPreferenceController( (SettingsActivity) getActivity(), this, packageName)); mAppButtonsPreferenceController = new AppButtonsPreferenceController( - (SettingsActivity) getActivity(), this, getLifecycle(), packageName, mState, mDpm, - mUserManager, mPackageManager, REQUEST_UNINSTALL, REQUEST_REMOVE_DEVICE_ADMIN); + (SettingsActivity) getActivity(), this, getSettingsLifecycle(), packageName, mState, + REQUEST_UNINSTALL, REQUEST_REMOVE_DEVICE_ADMIN); controllers.add(mAppButtonsPreferenceController); return controllers; @@ -355,29 +307,6 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements } @Override - public void onAnomalyHandled(Anomaly anomaly) { - mAnomalySummaryPreferenceController.hideHighUsagePreference(); - } - - @Override - public Loader<List<Anomaly>> onCreateLoader(int id, Bundle args) { - return new AnomalyLoader(getContext(), mPackageName); - } - - @Override - public void onLoadFinished(Loader<List<Anomaly>> loader, List<Anomaly> data) { - final AnomalyUtils anomalyUtils = AnomalyUtils.getInstance(getContext()); - anomalyUtils.logAnomalies(mMetricsFeatureProvider, data, - MetricsEvent.FUELGAUGE_POWER_USAGE_DETAIL); - mAnomalySummaryPreferenceController.updateAnomalySummaryPreference(data); - } - - @Override - public void onLoaderReset(Loader<List<Anomaly>> loader) { - - } - - @Override public void onBatteryTipHandled(BatteryTip batteryTip) { mBackgroundActivityPreferenceController.updateSummary( findPreference(mBackgroundActivityPreferenceController.getPreferenceKey())); diff --git a/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetailActivity.java b/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetailActivity.java new file mode 100644 index 0000000000..03ddde5636 --- /dev/null +++ b/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetailActivity.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2019 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.fuelgauge; + +import static com.android.settings.fuelgauge.AdvancedPowerUsageDetail.EXTRA_PACKAGE_NAME; +import static com.android.settings.fuelgauge.AdvancedPowerUsageDetail.EXTRA_POWER_USAGE_PERCENT; +import static com.android.settings.fuelgauge.AdvancedPowerUsageDetail.EXTRA_UID; + +import android.app.settings.SettingsEnums; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; + +import androidx.appcompat.app.AppCompatActivity; + +import com.android.settings.core.SubSettingLauncher; +import com.android.settings.fuelgauge.AdvancedPowerUsageDetail; +import com.android.settings.R; +import com.android.settings.Utils; + +/** + * Trampoline activity for launching the {@link AdvancedPowerUsageDetail} fragment. + */ +public class AdvancedPowerUsageDetailActivity extends AppCompatActivity { + + private static final String TAG = "AdvancedPowerDetailActivity"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + final Intent intent = getIntent(); + final Uri data = intent == null ? null : intent.getData(); + final String packageName = data == null ? null : data.getSchemeSpecificPart(); + if (packageName != null) { + final Bundle args = new Bundle(4); + final PackageManager packageManager = getPackageManager(); + args.putString(EXTRA_PACKAGE_NAME, packageName); + args.putString(EXTRA_POWER_USAGE_PERCENT, Utils.formatPercentage(0)); + + if (intent.getBooleanExtra("request_ignore_background_restriction", false)) { + args.putString(":settings:fragment_args_key", "background_activity"); + } + + try { + args.putInt(EXTRA_UID, packageManager.getPackageUid(packageName, 0 /* no flag */)); + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "Cannot find package: " + packageName, e); + } + + new SubSettingLauncher(this) + .setDestination(AdvancedPowerUsageDetail.class.getName()) + .setTitleRes(R.string.battery_details_title) + .setArguments(args) + .setSourceMetricsCategory(SettingsEnums.APPLICATIONS_INSTALLED_APP_DETAILS) + .launch(); + } + + finish(); + } +} diff --git a/src/com/android/settings/fuelgauge/AutoRestrictionPreferenceController.java b/src/com/android/settings/fuelgauge/AutoRestrictionPreferenceController.java index 7527db3acb..26fd398515 100644 --- a/src/com/android/settings/fuelgauge/AutoRestrictionPreferenceController.java +++ b/src/com/android/settings/fuelgauge/AutoRestrictionPreferenceController.java @@ -17,8 +17,9 @@ package com.android.settings.fuelgauge; import android.content.Context; import android.provider.Settings; -import androidx.preference.SwitchPreference; + import androidx.preference.Preference; +import androidx.preference.SwitchPreference; import com.android.settings.core.BasePreferenceController; import com.android.settings.overlay.FeatureFactory; diff --git a/src/com/android/settings/fuelgauge/BackgroundActivityPreferenceController.java b/src/com/android/settings/fuelgauge/BackgroundActivityPreferenceController.java index cd384f1fa2..4a5ce7db90 100644 --- a/src/com/android/settings/fuelgauge/BackgroundActivityPreferenceController.java +++ b/src/com/android/settings/fuelgauge/BackgroundActivityPreferenceController.java @@ -18,6 +18,7 @@ import android.app.AppOpsManager; import android.app.admin.DevicePolicyManager; import android.content.Context; import android.os.UserManager; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; @@ -30,6 +31,7 @@ import com.android.settings.fuelgauge.batterytip.BatteryTipDialogFragment; import com.android.settings.fuelgauge.batterytip.tips.BatteryTip; import com.android.settings.fuelgauge.batterytip.tips.RestrictAppTip; import com.android.settings.fuelgauge.batterytip.tips.UnrestrictAppTip; +import com.android.settingslib.RestrictedPreference; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.fuelgauge.PowerWhitelistBackend; @@ -75,6 +77,11 @@ public class BackgroundActivityPreferenceController extends AbstractPreferenceCo @Override public void updateState(Preference preference) { + final RestrictedPreference restrictedPreference = (RestrictedPreference) preference; + if (restrictedPreference.isDisabledByAdmin()) { + // If disabled, let RestrictedPreference handle it and do nothing here + return; + } final int mode = mAppOpsManager .checkOpNoThrow(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, mUid, mTargetPackage); final boolean whitelisted = mPowerWhitelistBackend.isWhitelisted(mTargetPackage); diff --git a/src/com/android/settings/fuelgauge/BatteryAppListPreferenceController.java b/src/com/android/settings/fuelgauge/BatteryAppListPreferenceController.java index 83b0984c7e..7741a979a9 100644 --- a/src/com/android/settings/fuelgauge/BatteryAppListPreferenceController.java +++ b/src/com/android/settings/fuelgauge/BatteryAppListPreferenceController.java @@ -27,27 +27,26 @@ import android.os.Message; import android.os.Process; import android.os.UserHandle; import android.os.UserManager; -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; -import androidx.preference.PreferenceGroup; -import androidx.preference.PreferenceScreen; import android.text.TextUtils; import android.text.format.DateUtils; import android.util.ArrayMap; -import android.util.FeatureFlagUtils; import android.util.Log; import android.util.SparseArray; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceGroup; +import androidx.preference.PreferenceScreen; + import com.android.internal.os.BatterySipper; import com.android.internal.os.BatterySipper.DrainType; import com.android.internal.os.BatteryStatsHelper; import com.android.internal.os.PowerProfile; import com.android.settings.R; import com.android.settings.SettingsActivity; -import com.android.settings.core.FeatureFlags; import com.android.settings.core.InstrumentedPreferenceFragment; import com.android.settings.core.PreferenceControllerMixin; -import com.android.settings.fuelgauge.anomaly.Anomaly; +import com.android.settingslib.applications.AppUtils; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.core.lifecycle.LifecycleObserver; @@ -65,7 +64,7 @@ public class BatteryAppListPreferenceController extends AbstractPreferenceContro implements PreferenceControllerMixin, LifecycleObserver, OnPause, OnDestroy { @VisibleForTesting static final boolean USE_FAKE_DATA = false; - private static final int MAX_ITEMS_TO_LIST = USE_FAKE_DATA ? 30 : 10; + private static final int MAX_ITEMS_TO_LIST = USE_FAKE_DATA ? 30 : 20; private static final int MIN_AVERAGE_POWER_THRESHOLD_MILLI_AMP = 10; private static final int STATS_TYPE = BatteryStats.STATS_SINCE_CHARGED; @@ -80,7 +79,6 @@ public class BatteryAppListPreferenceController extends AbstractPreferenceContro private SettingsActivity mActivity; private InstrumentedPreferenceFragment mFragment; private Context mPrefContext; - SparseArray<List<Anomaly>> mAnomalySparseArray; private Handler mHandler = new Handler(Looper.getMainLooper()) { @Override @@ -145,7 +143,7 @@ public class BatteryAppListPreferenceController extends AbstractPreferenceContro public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); mPrefContext = screen.getContext(); - mAppListGroup = (PreferenceGroup) screen.findPreference(mPreferenceKey); + mAppListGroup = screen.findPreference(mPreferenceKey); } @Override @@ -163,30 +161,13 @@ public class BatteryAppListPreferenceController extends AbstractPreferenceContro if (preference instanceof PowerGaugePreference) { PowerGaugePreference pgp = (PowerGaugePreference) preference; BatteryEntry entry = pgp.getInfo(); - AdvancedPowerUsageDetail.startBatteryDetailPage(mActivity, - mFragment, mBatteryStatsHelper, STATS_TYPE, entry, pgp.getPercent(), - mAnomalySparseArray != null ? mAnomalySparseArray.get(entry.sipper.getUid()) - : null); + AdvancedPowerUsageDetail.startBatteryDetailPage(mActivity, mBatteryUtils, + mFragment, mBatteryStatsHelper, STATS_TYPE, entry, pgp.getPercent()); return true; } return false; } - public void refreshAnomalyIcon(final SparseArray<List<Anomaly>> anomalySparseArray) { - if (!isAvailable()) { - return; - } - mAnomalySparseArray = anomalySparseArray; - for (int i = 0, size = anomalySparseArray.size(); i < size; i++) { - final String key = extractKeyFromUid(anomalySparseArray.keyAt(i)); - final PowerGaugePreference pref = (PowerGaugePreference) mAppListGroup.findPreference( - key); - if (pref != null) { - pref.shouldShowAnomalyIcon(true); - } - } - } - public void refreshAppListGroup(BatteryStatsHelper statsHelper, boolean showAllApps) { if (!isAvailable()) { return; @@ -371,9 +352,10 @@ public class BatteryAppListPreferenceController extends AbstractPreferenceContro @VisibleForTesting boolean shouldHideSipper(BatterySipper sipper) { - // Don't show over-counted and unaccounted in any condition + // Don't show over-counted, unaccounted and hidden system module in any condition return sipper.drainType == BatterySipper.DrainType.OVERCOUNTED - || sipper.drainType == BatterySipper.DrainType.UNACCOUNTED; + || sipper.drainType == BatterySipper.DrainType.UNACCOUNTED + || mBatteryUtils.isHiddenSystemModule(sipper); } @VisibleForTesting diff --git a/src/com/android/settings/fuelgauge/BatteryBroadcastReceiver.java b/src/com/android/settings/fuelgauge/BatteryBroadcastReceiver.java index f07389de53..026668905a 100644 --- a/src/com/android/settings/fuelgauge/BatteryBroadcastReceiver.java +++ b/src/com/android/settings/fuelgauge/BatteryBroadcastReceiver.java @@ -21,11 +21,12 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.PowerManager; + import androidx.annotation.IntDef; import androidx.annotation.VisibleForTesting; import com.android.settings.Utils; -import com.android.settings.fuelgauge.batterytip.tips.BatteryTip; +import com.android.settings.homepage.contextualcards.slices.BatteryFixSlice; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -115,6 +116,6 @@ public class BatteryBroadcastReceiver extends BroadcastReceiver { mBatteryListener.onBatteryChanged(BatteryUpdateType.BATTERY_SAVER); } } + BatteryFixSlice.updateBatteryTipAvailabilityCache(mContext); } - }
\ No newline at end of file diff --git a/src/com/android/settings/fuelgauge/BatteryCellParser.java b/src/com/android/settings/fuelgauge/BatteryCellParser.java deleted file mode 100644 index 2b398778ca..0000000000 --- a/src/com/android/settings/fuelgauge/BatteryCellParser.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file - * except in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the - * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package com.android.settings.fuelgauge; - -import android.os.BatteryStats.HistoryItem; -import android.telephony.ServiceState; -import android.util.SparseIntArray; -import com.android.settings.Utils; -import com.android.settings.fuelgauge.BatteryActiveView.BatteryActiveProvider; - -public class BatteryCellParser implements BatteryInfo.BatteryDataParser, BatteryActiveProvider { - - private final SparseIntArray mData = new SparseIntArray(); - - private int mLastValue; - private long mLength; - private long mLastTime; - - protected int getValue(HistoryItem rec) { - int bin; - if (((rec.states & HistoryItem.STATE_PHONE_STATE_MASK) - >> HistoryItem.STATE_PHONE_STATE_SHIFT) - == ServiceState.STATE_POWER_OFF) { - bin = 0; - } else if ((rec.states & HistoryItem.STATE_PHONE_SCANNING_FLAG) != 0) { - bin = 1; - } else { - bin = (rec.states & HistoryItem.STATE_PHONE_SIGNAL_STRENGTH_MASK) - >> HistoryItem.STATE_PHONE_SIGNAL_STRENGTH_SHIFT; - bin += 2; - } - return bin; - } - - @Override - public void onParsingStarted(long startTime, long endTime) { - mLength = endTime - startTime; - } - - @Override - public void onDataPoint(long time, HistoryItem record) { - int value = getValue(record); - if (value != mLastValue) { - mData.put((int) time, value); - mLastValue = value; - } - mLastTime = time; - } - - @Override - public void onDataGap() { - if (mLastValue != 0) { - mData.put((int) mLastTime, 0); - mLastValue = 0; - } - } - - @Override - public void onParsingDone() { - if (mLastValue != 0) { - mData.put((int) mLastTime, 0); - mLastValue = 0; - } - } - - @Override - public long getPeriod() { - return mLength; - } - - @Override - public boolean hasData() { - return mData.size() > 1; - } - - @Override - public SparseIntArray getColorArray() { - SparseIntArray ret = new SparseIntArray(); - for (int i = 0; i < mData.size(); i++) { - ret.put(mData.keyAt(i), getColor(mData.valueAt(i))); - } - return ret; - } - - private int getColor(int i) { - return Utils.BADNESS_COLORS[i]; - } -} diff --git a/src/com/android/settings/fuelgauge/BatteryEntry.java b/src/com/android/settings/fuelgauge/BatteryEntry.java index a93d522315..38ae2b2df7 100644 --- a/src/com/android/settings/fuelgauge/BatteryEntry.java +++ b/src/com/android/settings/fuelgauge/BatteryEntry.java @@ -145,7 +145,7 @@ public class BatteryEntry { break; case CELL: name = context.getResources().getString(R.string.power_cell); - iconId = R.drawable.ic_settings_cell_standby; + iconId = R.drawable.ic_cellular_1_bar; break; case PHONE: name = context.getResources().getString(R.string.power_phone); @@ -157,7 +157,7 @@ public class BatteryEntry { break; case BLUETOOTH: name = context.getResources().getString(R.string.power_bluetooth); - iconId = R.drawable.ic_settings_bluetooth; + iconId = com.android.internal.R.drawable.ic_settings_bluetooth; break; case SCREEN: name = context.getResources().getString(R.string.power_screen); @@ -199,11 +199,11 @@ public class BatteryEntry { } break; case UNACCOUNTED: name = context.getResources().getString(R.string.power_unaccounted); - iconId = R.drawable.ic_power_system; + iconId = R.drawable.ic_android; break; case OVERCOUNTED: name = context.getResources().getString(R.string.power_overcounted); - iconId = R.drawable.ic_power_system; + iconId = R.drawable.ic_android; break; case CAMERA: name = context.getResources().getString(R.string.power_camera); diff --git a/src/com/android/settings/fuelgauge/BatteryFlagParser.java b/src/com/android/settings/fuelgauge/BatteryFlagParser.java index d4f3fb20fd..e16d5e7633 100644 --- a/src/com/android/settings/fuelgauge/BatteryFlagParser.java +++ b/src/com/android/settings/fuelgauge/BatteryFlagParser.java @@ -17,6 +17,7 @@ package com.android.settings.fuelgauge; import android.os.BatteryStats.HistoryItem; import android.util.SparseBooleanArray; import android.util.SparseIntArray; + import com.android.settings.fuelgauge.BatteryActiveView.BatteryActiveProvider; public class BatteryFlagParser implements BatteryInfo.BatteryDataParser, BatteryActiveProvider { diff --git a/src/com/android/settings/fuelgauge/BatteryHeaderPreferenceController.java b/src/com/android/settings/fuelgauge/BatteryHeaderPreferenceController.java index 7038fade34..3064d4f2ba 100644 --- a/src/com/android/settings/fuelgauge/BatteryHeaderPreferenceController.java +++ b/src/com/android/settings/fuelgauge/BatteryHeaderPreferenceController.java @@ -21,26 +21,30 @@ import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.icu.text.NumberFormat; import android.os.BatteryManager; +import android.os.PowerManager; +import android.text.TextUtils; +import android.widget.TextView; + import androidx.annotation.VisibleForTesting; -import androidx.preference.PreferenceFragment; +import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceScreen; -import android.widget.TextView; import com.android.settings.R; -import com.android.settings.applications.LayoutPreference; +import com.android.settings.core.BasePreferenceController; import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.widget.EntityHeaderController; import com.android.settingslib.Utils; -import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.widget.LayoutPreference; /** * Controller that update the battery header view */ -public class BatteryHeaderPreferenceController extends AbstractPreferenceController +public class BatteryHeaderPreferenceController extends BasePreferenceController implements PreferenceControllerMixin, LifecycleObserver, OnStart { @VisibleForTesting static final String KEY_BATTERY_HEADER = "battery_header"; @@ -54,28 +58,35 @@ public class BatteryHeaderPreferenceController extends AbstractPreferenceControl @VisibleForTesting TextView mSummary2; - private final Activity mActivity; - private final PreferenceFragment mHost; - private final Lifecycle mLifecycle; + private Activity mActivity; + private PreferenceFragmentCompat mHost; + private Lifecycle mLifecycle; + private final PowerManager mPowerManager; private LayoutPreference mBatteryLayoutPref; - public BatteryHeaderPreferenceController(Context context, Activity activity, - PreferenceFragment host, Lifecycle lifecycle) { - super(context); + public BatteryHeaderPreferenceController(Context context, String key) { + super(context, key); + mPowerManager = context.getSystemService(PowerManager.class); + } + + public void setActivity(Activity activity) { mActivity = activity; - mHost = host; + } + + public void setFragment(PreferenceFragmentCompat fragment) { + mHost = fragment; + } + + public void setLifecycle(Lifecycle lifecycle) { mLifecycle = lifecycle; - if (mLifecycle != null) { - mLifecycle.addObserver(this); - } } @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - mBatteryLayoutPref = (LayoutPreference) screen.findPreference(KEY_BATTERY_HEADER); - mBatteryMeterView = (BatteryMeterView) mBatteryLayoutPref + mBatteryLayoutPref = screen.findPreference(getPreferenceKey()); + mBatteryMeterView = mBatteryLayoutPref .findViewById(R.id.battery_header_icon); mBatteryPercentText = mBatteryLayoutPref.findViewById(R.id.battery_percent); mSummary1 = mBatteryLayoutPref.findViewById(R.id.summary1); @@ -85,13 +96,8 @@ public class BatteryHeaderPreferenceController extends AbstractPreferenceControl } @Override - public boolean isAvailable() { - return true; - } - - @Override - public String getPreferenceKey() { - return KEY_BATTERY_HEADER; + public int getAvailabilityStatus() { + return AVAILABLE_UNSEARCHABLE; } @Override @@ -103,7 +109,7 @@ public class BatteryHeaderPreferenceController extends AbstractPreferenceControl } public void updateHeaderPreference(BatteryInfo info) { - mBatteryPercentText.setText(Utils.formatPercentage(info.batteryLevel)); + mBatteryPercentText.setText(formatBatteryPercentageText(info.batteryLevel)); if (info.remainingLabel == null) { mSummary1.setText(info.statusLabel); } else { @@ -115,6 +121,7 @@ public class BatteryHeaderPreferenceController extends AbstractPreferenceControl mBatteryMeterView.setBatteryLevel(info.batteryLevel); mBatteryMeterView.setCharging(!info.discharging); + mBatteryMeterView.setPowerSave(mPowerManager.isPowerSaveMode()); } public void quickUpdateHeaderPreference() { @@ -127,10 +134,12 @@ public class BatteryHeaderPreferenceController extends AbstractPreferenceControl // Set battery level and charging status mBatteryMeterView.setBatteryLevel(batteryLevel); mBatteryMeterView.setCharging(!discharging); - mBatteryPercentText.setText(Utils.formatPercentage(batteryLevel)); + mBatteryMeterView.setPowerSave(mPowerManager.isPowerSaveMode()); + mBatteryPercentText.setText(formatBatteryPercentageText(batteryLevel)); + } - // clear all the summaries - mSummary1.setText(""); - mSummary2.setText(""); + private CharSequence formatBatteryPercentageText(int batteryLevel) { + return TextUtils.expandTemplate(mContext.getText(R.string.battery_header_title_alternate), + NumberFormat.getIntegerInstance().format(batteryLevel)); } } diff --git a/src/com/android/settings/fuelgauge/BatteryHistoryChart.java b/src/com/android/settings/fuelgauge/BatteryHistoryChart.java deleted file mode 100644 index 5003254bf1..0000000000 --- a/src/com/android/settings/fuelgauge/BatteryHistoryChart.java +++ /dev/null @@ -1,1366 +0,0 @@ -/* - * Copyright (C) 2010 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.fuelgauge; - -import android.content.Context; -import android.content.Intent; -import android.content.res.ColorStateList; -import android.content.res.TypedArray; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.DashPathEffect; -import android.graphics.Paint; -import android.graphics.Path; -import android.graphics.Typeface; -import android.os.BatteryStats; -import android.os.BatteryStats.HistoryItem; -import android.os.SystemClock; -import android.telephony.ServiceState; -import android.text.TextPaint; -import android.text.format.DateFormat; -import android.text.format.Formatter; -import android.util.AttributeSet; -import android.util.Log; -import android.util.TimeUtils; -import android.util.TypedValue; -import android.view.View; - -import com.android.settings.R; -import com.android.settings.Utils; - -import libcore.icu.LocaleData; - -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Locale; - -public class BatteryHistoryChart extends View { - static final boolean DEBUG = false; - static final String TAG = "BatteryHistoryChart"; - - static final int CHART_DATA_X_MASK = 0x0000ffff; - static final int CHART_DATA_BIN_MASK = 0xffff0000; - static final int CHART_DATA_BIN_SHIFT = 16; - - static class ChartData { - int[] mColors; - Paint[] mPaints; - - int mNumTicks; - int[] mTicks; - int mLastBin; - - void setColors(int[] colors) { - mColors = colors; - mPaints = new Paint[colors.length]; - for (int i=0; i<colors.length; i++) { - mPaints[i] = new Paint(); - mPaints[i].setColor(colors[i]); - mPaints[i].setStyle(Paint.Style.FILL); - } - } - - void init(int width) { - if (width > 0) { - mTicks = new int[width*2]; - } else { - mTicks = null; - } - mNumTicks = 0; - mLastBin = 0; - } - - void addTick(int x, int bin) { - if (bin != mLastBin && mNumTicks < mTicks.length) { - mTicks[mNumTicks] = (x&CHART_DATA_X_MASK) | (bin<<CHART_DATA_BIN_SHIFT); - mNumTicks++; - mLastBin = bin; - } - } - - void finish(int width) { - if (mLastBin != 0) { - addTick(width, 0); - } - } - - void draw(Canvas canvas, int top, int height) { - int lastBin=0, lastX=0; - int bottom = top + height; - for (int i=0; i<mNumTicks; i++) { - int tick = mTicks[i]; - int x = tick&CHART_DATA_X_MASK; - int bin = (tick&CHART_DATA_BIN_MASK) >> CHART_DATA_BIN_SHIFT; - if (lastBin != 0) { - canvas.drawRect(lastX, top, x, bottom, mPaints[lastBin]); - } - lastBin = bin; - lastX = x; - } - - } - } - - static final int SANS = 1; - static final int SERIF = 2; - static final int MONOSPACE = 3; - - // First value if for phone off; first value is "scanning"; following values - // are battery stats signal strength buckets. - static final int NUM_PHONE_SIGNALS = 7; - - final Paint mBatteryBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - final Paint mBatteryGoodPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - final Paint mBatteryWarnPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - final Paint mBatteryCriticalPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - final Paint mTimeRemainPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - final Paint mChargingPaint = new Paint(); - final Paint mScreenOnPaint = new Paint(); - final Paint mGpsOnPaint = new Paint(); - final Paint mFlashlightOnPaint = new Paint(); - final Paint mCameraOnPaint = new Paint(); - final Paint mWifiRunningPaint = new Paint(); - final Paint mCpuRunningPaint = new Paint(); - final Paint mDateLinePaint = new Paint(); - final ChartData mPhoneSignalChart = new ChartData(); - final TextPaint mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); - final TextPaint mHeaderTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); - final Paint mDebugRectPaint = new Paint(); - - final Path mBatLevelPath = new Path(); - final Path mBatGoodPath = new Path(); - final Path mBatWarnPath = new Path(); - final Path mBatCriticalPath = new Path(); - final Path mTimeRemainPath = new Path(); - final Path mChargingPath = new Path(); - final Path mScreenOnPath = new Path(); - final Path mGpsOnPath = new Path(); - final Path mFlashlightOnPath = new Path(); - final Path mCameraOnPath = new Path(); - final Path mWifiRunningPath = new Path(); - final Path mCpuRunningPath = new Path(); - final Path mDateLinePath = new Path(); - - BatteryStats mStats; - Intent mBatteryBroadcast; - long mStatsPeriod; - String mMaxPercentLabelString; - String mMinPercentLabelString; - String mDurationString; - String mChargeDurationString; - String mDrainString; - String mChargingLabel; - String mScreenOnLabel; - String mGpsOnLabel; - String mCameraOnLabel; - String mFlashlightOnLabel; - String mWifiRunningLabel; - String mCpuRunningLabel; - String mPhoneSignalLabel; - - BatteryInfo mInfo; - - int mChartMinHeight; - int mHeaderHeight; - - int mBatteryWarnLevel; - int mBatteryCriticalLevel; - - int mTextAscent; - int mTextDescent; - int mHeaderTextAscent; - int mHeaderTextDescent; - int mMaxPercentLabelStringWidth; - int mMinPercentLabelStringWidth; - int mDurationStringWidth; - int mChargeLabelStringWidth; - int mChargeDurationStringWidth; - int mDrainStringWidth; - - boolean mLargeMode; - - int mLastWidth = -1; - int mLastHeight = -1; - - int mLineWidth; - int mThinLineWidth; - int mChargingOffset; - int mScreenOnOffset; - int mGpsOnOffset; - int mFlashlightOnOffset; - int mCameraOnOffset; - int mWifiRunningOffset; - int mCpuRunningOffset; - int mPhoneSignalOffset; - int mLevelOffset; - int mLevelTop; - int mLevelBottom; - int mLevelLeft; - int mLevelRight; - - int mNumHist; - long mHistStart; - long mHistDataEnd; - long mHistEnd; - long mStartWallTime; - long mEndDataWallTime; - long mEndWallTime; - int mBatLow; - int mBatHigh; - boolean mHaveWifi; - boolean mHaveGps; - boolean mHavePhoneSignal; - boolean mHaveCamera; - boolean mHaveFlashlight; - - final ArrayList<TimeLabel> mTimeLabels = new ArrayList<TimeLabel>(); - final ArrayList<DateLabel> mDateLabels = new ArrayList<DateLabel>(); - - Bitmap mBitmap; - Canvas mCanvas; - - static class TextAttrs { - ColorStateList textColor = null; - int textSize = 15; - int typefaceIndex = -1; - int styleIndex = -1; - - void retrieve(Context context, TypedArray from, int index) { - TypedArray appearance = null; - int ap = from.getResourceId(index, -1); - if (ap != -1) { - appearance = context.obtainStyledAttributes(ap, - com.android.internal.R.styleable.TextAppearance); - } - if (appearance != null) { - int n = appearance.getIndexCount(); - for (int i = 0; i < n; i++) { - int attr = appearance.getIndex(i); - - switch (attr) { - case com.android.internal.R.styleable.TextAppearance_textColor: - textColor = appearance.getColorStateList(attr); - break; - - case com.android.internal.R.styleable.TextAppearance_textSize: - textSize = appearance.getDimensionPixelSize(attr, textSize); - break; - - case com.android.internal.R.styleable.TextAppearance_typeface: - typefaceIndex = appearance.getInt(attr, -1); - break; - - case com.android.internal.R.styleable.TextAppearance_textStyle: - styleIndex = appearance.getInt(attr, -1); - break; - } - } - - appearance.recycle(); - } - } - - void apply(Context context, TextPaint paint) { - paint.density = context.getResources().getDisplayMetrics().density; - paint.setCompatibilityScaling( - context.getResources().getCompatibilityInfo().applicationScale); - - paint.setColor(textColor.getDefaultColor()); - paint.setTextSize(textSize); - - Typeface tf = null; - switch (typefaceIndex) { - case SANS: - tf = Typeface.SANS_SERIF; - break; - - case SERIF: - tf = Typeface.SERIF; - break; - - case MONOSPACE: - tf = Typeface.MONOSPACE; - break; - } - - setTypeface(paint, tf, styleIndex); - } - - public void setTypeface(TextPaint paint, Typeface tf, int style) { - if (style > 0) { - if (tf == null) { - tf = Typeface.defaultFromStyle(style); - } else { - tf = Typeface.create(tf, style); - } - - paint.setTypeface(tf); - // now compute what (if any) algorithmic styling is needed - int typefaceStyle = tf != null ? tf.getStyle() : 0; - int need = style & ~typefaceStyle; - paint.setFakeBoldText((need & Typeface.BOLD) != 0); - paint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0); - } else { - paint.setFakeBoldText(false); - paint.setTextSkewX(0); - paint.setTypeface(tf); - } - } - } - - static class TimeLabel { - final int x; - final String label; - final int width; - - TimeLabel(TextPaint paint, int x, Calendar cal, boolean use24hr) { - this.x = x; - final String bestFormat = DateFormat.getBestDateTimePattern( - Locale.getDefault(), use24hr ? "km" : "ha"); - label = DateFormat.format(bestFormat, cal).toString(); - width = (int)paint.measureText(label); - } - } - - static class DateLabel { - final int x; - final String label; - final int width; - - DateLabel(TextPaint paint, int x, Calendar cal, boolean dayFirst) { - this.x = x; - final String bestFormat = DateFormat.getBestDateTimePattern( - Locale.getDefault(), dayFirst ? "dM" : "Md"); - label = DateFormat.format(bestFormat, cal).toString(); - width = (int)paint.measureText(label); - } - } - - public BatteryHistoryChart(Context context, AttributeSet attrs) { - super(context, attrs); - - if (DEBUG) Log.d(TAG, "New BatteryHistoryChart!"); - - mBatteryWarnLevel = mContext.getResources().getInteger( - com.android.internal.R.integer.config_lowBatteryWarningLevel); - mBatteryCriticalLevel = mContext.getResources().getInteger( - com.android.internal.R.integer.config_criticalBatteryWarningLevel); - - mThinLineWidth = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, - 2, getResources().getDisplayMetrics()); - - int accentColor = Utils.getColorAccent(mContext); - mBatteryBackgroundPaint.setColor(accentColor); - mBatteryBackgroundPaint.setStyle(Paint.Style.FILL); - mBatteryGoodPaint.setARGB(128, 0, 128, 0); - mBatteryGoodPaint.setStyle(Paint.Style.STROKE); - mBatteryWarnPaint.setARGB(128, 128, 128, 0); - mBatteryWarnPaint.setStyle(Paint.Style.STROKE); - mBatteryCriticalPaint.setARGB(192, 128, 0, 0); - mBatteryCriticalPaint.setStyle(Paint.Style.STROKE); - mTimeRemainPaint.setColor(0xFFCED7BB); - mTimeRemainPaint.setStyle(Paint.Style.FILL); - mChargingPaint.setStyle(Paint.Style.STROKE); - mScreenOnPaint.setStyle(Paint.Style.STROKE); - mGpsOnPaint.setStyle(Paint.Style.STROKE); - mCameraOnPaint.setStyle(Paint.Style.STROKE); - mFlashlightOnPaint.setStyle(Paint.Style.STROKE); - mWifiRunningPaint.setStyle(Paint.Style.STROKE); - mCpuRunningPaint.setStyle(Paint.Style.STROKE); - mPhoneSignalChart.setColors(com.android.settings.Utils.BADNESS_COLORS); - mDebugRectPaint.setARGB(255, 255, 0, 0); - mDebugRectPaint.setStyle(Paint.Style.STROKE); - mScreenOnPaint.setColor(accentColor); - mGpsOnPaint.setColor(accentColor); - mCameraOnPaint.setColor(accentColor); - mFlashlightOnPaint.setColor(accentColor); - mWifiRunningPaint.setColor(accentColor); - mCpuRunningPaint.setColor(accentColor); - mChargingPaint.setColor(accentColor); - - TypedArray a = - context.obtainStyledAttributes( - attrs, R.styleable.BatteryHistoryChart, 0, 0); - - final TextAttrs mainTextAttrs = new TextAttrs(); - final TextAttrs headTextAttrs = new TextAttrs(); - mainTextAttrs.retrieve(context, a, R.styleable.BatteryHistoryChart_android_textAppearance); - headTextAttrs.retrieve(context, a, R.styleable.BatteryHistoryChart_headerAppearance); - - int shadowcolor = 0; - float dx=0, dy=0, r=0; - - int n = a.getIndexCount(); - for (int i = 0; i < n; i++) { - int attr = a.getIndex(i); - - switch (attr) { - case R.styleable.BatteryHistoryChart_android_shadowColor: - shadowcolor = a.getInt(attr, 0); - break; - - case R.styleable.BatteryHistoryChart_android_shadowDx: - dx = a.getFloat(attr, 0); - break; - - case R.styleable.BatteryHistoryChart_android_shadowDy: - dy = a.getFloat(attr, 0); - break; - - case R.styleable.BatteryHistoryChart_android_shadowRadius: - r = a.getFloat(attr, 0); - break; - - case R.styleable.BatteryHistoryChart_android_textColor: - mainTextAttrs.textColor = a.getColorStateList(attr); - headTextAttrs.textColor = a.getColorStateList(attr); - break; - - case R.styleable.BatteryHistoryChart_android_textSize: - mainTextAttrs.textSize = a.getDimensionPixelSize(attr, mainTextAttrs.textSize); - headTextAttrs.textSize = a.getDimensionPixelSize(attr, headTextAttrs.textSize); - break; - - case R.styleable.BatteryHistoryChart_android_typeface: - mainTextAttrs.typefaceIndex = a.getInt(attr, mainTextAttrs.typefaceIndex); - headTextAttrs.typefaceIndex = a.getInt(attr, headTextAttrs.typefaceIndex); - break; - - case R.styleable.BatteryHistoryChart_android_textStyle: - mainTextAttrs.styleIndex = a.getInt(attr, mainTextAttrs.styleIndex); - headTextAttrs.styleIndex = a.getInt(attr, headTextAttrs.styleIndex); - break; - - case R.styleable.BatteryHistoryChart_barPrimaryColor: - mBatteryBackgroundPaint.setColor(a.getInt(attr, 0)); - mScreenOnPaint.setColor(a.getInt(attr, 0)); - mGpsOnPaint.setColor(a.getInt(attr, 0)); - mCameraOnPaint.setColor(a.getInt(attr, 0)); - mFlashlightOnPaint.setColor(a.getInt(attr, 0)); - mWifiRunningPaint.setColor(a.getInt(attr, 0)); - mCpuRunningPaint.setColor(a.getInt(attr, 0)); - mChargingPaint.setColor(a.getInt(attr, 0)); - break; - - case R.styleable.BatteryHistoryChart_barPredictionColor: - mTimeRemainPaint.setColor(a.getInt(attr, 0)); - break; - - case R.styleable.BatteryHistoryChart_chartMinHeight: - mChartMinHeight = a.getDimensionPixelSize(attr, 0); - break; - } - } - - a.recycle(); - - mainTextAttrs.apply(context, mTextPaint); - headTextAttrs.apply(context, mHeaderTextPaint); - - mDateLinePaint.set(mTextPaint); - mDateLinePaint.setStyle(Paint.Style.STROKE); - int hairlineWidth = mThinLineWidth/2; - if (hairlineWidth < 1) { - hairlineWidth = 1; - } - mDateLinePaint.setStrokeWidth(hairlineWidth); - mDateLinePaint.setPathEffect(new DashPathEffect(new float[] { - mThinLineWidth * 2, mThinLineWidth * 2 }, 0)); - - if (shadowcolor != 0) { - mTextPaint.setShadowLayer(r, dx, dy, shadowcolor); - mHeaderTextPaint.setShadowLayer(r, dx, dy, shadowcolor); - } - } - - void setStats(BatteryStats stats, Intent broadcast) { - mStats = stats; - mBatteryBroadcast = broadcast; - - if (DEBUG) Log.d(TAG, "Setting stats..."); - - final long elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000; - - long uSecTime = mStats.computeBatteryRealtime(elapsedRealtimeUs, - BatteryStats.STATS_SINCE_CHARGED); - mStatsPeriod = uSecTime; - mChargingLabel = getContext().getString(R.string.battery_stats_charging_label); - mScreenOnLabel = getContext().getString(R.string.battery_stats_screen_on_label); - mGpsOnLabel = getContext().getString(R.string.battery_stats_gps_on_label); - mCameraOnLabel = getContext().getString(R.string.battery_stats_camera_on_label); - mFlashlightOnLabel = getContext().getString(R.string.battery_stats_flashlight_on_label); - mWifiRunningLabel = getContext().getString(R.string.battery_stats_wifi_running_label); - mCpuRunningLabel = getContext().getString(R.string.battery_stats_wake_lock_label); - mPhoneSignalLabel = getContext().getString(R.string.battery_stats_phone_signal_label); - - mMaxPercentLabelString = Utils.formatPercentage(100); - mMinPercentLabelString = Utils.formatPercentage(0); - BatteryInfo.getBatteryInfo(getContext(), info -> { - mInfo = info; - mDrainString = ""; - mChargeDurationString = ""; - setContentDescription(mInfo.chargeLabel); - - int pos = 0; - int lastInteresting = 0; - byte lastLevel = -1; - mBatLow = 0; - mBatHigh = 100; - mStartWallTime = 0; - mEndDataWallTime = 0; - mEndWallTime = 0; - mHistStart = 0; - mHistEnd = 0; - long lastWallTime = 0; - long lastRealtime = 0; - int aggrStates = 0; - int aggrStates2 = 0; - boolean first = true; - if (stats.startIteratingHistoryLocked()) { - final HistoryItem rec = new HistoryItem(); - while (stats.getNextHistoryLocked(rec)) { - pos++; - if (first) { - first = false; - mHistStart = rec.time; - } - if (rec.cmd == HistoryItem.CMD_CURRENT_TIME - || rec.cmd == HistoryItem.CMD_RESET) { - // If there is a ridiculously large jump in time, then we won't be - // able to create a good chart with that data, so just ignore the - // times we got before and pretend like our data extends back from - // the time we have now. - // Also, if we are getting a time change and we are less than 5 minutes - // since the start of the history real time, then also use this new - // time to compute the base time, since whatever time we had before is - // pretty much just noise. - if (rec.currentTime > (lastWallTime+(180*24*60*60*1000L)) - || rec.time < (mHistStart+(5*60*1000L))) { - mStartWallTime = 0; - } - lastWallTime = rec.currentTime; - lastRealtime = rec.time; - if (mStartWallTime == 0) { - mStartWallTime = lastWallTime - (lastRealtime-mHistStart); - } - } - if (rec.isDeltaData()) { - if (rec.batteryLevel != lastLevel || pos == 1) { - lastLevel = rec.batteryLevel; - } - lastInteresting = pos; - mHistDataEnd = rec.time; - aggrStates |= rec.states; - aggrStates2 |= rec.states2; - } - } - } - mHistEnd = mHistDataEnd + (mInfo.remainingTimeUs/1000); - mEndDataWallTime = lastWallTime + mHistDataEnd - lastRealtime; - mEndWallTime = mEndDataWallTime + (mInfo.remainingTimeUs/1000); - mNumHist = lastInteresting; - mHaveGps = (aggrStates&HistoryItem.STATE_GPS_ON_FLAG) != 0; - mHaveFlashlight = (aggrStates2&HistoryItem.STATE2_FLASHLIGHT_FLAG) != 0; - mHaveCamera = (aggrStates2&HistoryItem.STATE2_CAMERA_FLAG) != 0; - mHaveWifi = (aggrStates2&HistoryItem.STATE2_WIFI_RUNNING_FLAG) != 0 - || (aggrStates&(HistoryItem.STATE_WIFI_FULL_LOCK_FLAG - |HistoryItem.STATE_WIFI_MULTICAST_ON_FLAG - |HistoryItem.STATE_WIFI_SCAN_FLAG)) != 0; - if (!com.android.settingslib.Utils.isWifiOnly(getContext())) { - mHavePhoneSignal = true; - } - if (mHistEnd <= mHistStart) mHistEnd = mHistStart+1; - }, mStats, false /* shortString */); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - mMaxPercentLabelStringWidth = (int)mTextPaint.measureText(mMaxPercentLabelString); - mMinPercentLabelStringWidth = (int)mTextPaint.measureText(mMinPercentLabelString); - mDrainStringWidth = (int)mHeaderTextPaint.measureText(mDrainString); - mChargeLabelStringWidth = (int) mHeaderTextPaint.measureText( - mInfo.chargeLabel.toString()); - mChargeDurationStringWidth = (int)mHeaderTextPaint.measureText(mChargeDurationString); - mTextAscent = (int)mTextPaint.ascent(); - mTextDescent = (int)mTextPaint.descent(); - mHeaderTextAscent = (int)mHeaderTextPaint.ascent(); - mHeaderTextDescent = (int)mHeaderTextPaint.descent(); - int headerTextHeight = mHeaderTextDescent - mHeaderTextAscent; - mHeaderHeight = headerTextHeight*2 - mTextAscent; - setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), - getDefaultSize(mChartMinHeight+mHeaderHeight, heightMeasureSpec)); - } - - void finishPaths(int w, int h, int levelh, int startX, int y, Path curLevelPath, - int lastX, boolean lastCharging, boolean lastScreenOn, boolean lastGpsOn, - boolean lastFlashlightOn, boolean lastCameraOn, boolean lastWifiRunning, - boolean lastCpuRunning, Path lastPath) { - if (curLevelPath != null) { - if (lastX >= 0 && lastX < w) { - if (lastPath != null) { - lastPath.lineTo(w, y); - } - curLevelPath.lineTo(w, y); - } - curLevelPath.lineTo(w, mLevelTop+levelh); - curLevelPath.lineTo(startX, mLevelTop+levelh); - curLevelPath.close(); - } - - if (lastCharging) { - mChargingPath.lineTo(w, h-mChargingOffset); - } - if (lastScreenOn) { - mScreenOnPath.lineTo(w, h-mScreenOnOffset); - } - if (lastGpsOn) { - mGpsOnPath.lineTo(w, h-mGpsOnOffset); - } - if (lastFlashlightOn) { - mFlashlightOnPath.lineTo(w, h-mFlashlightOnOffset); - } - if (lastCameraOn) { - mCameraOnPath.lineTo(w, h-mCameraOnOffset); - } - if (lastWifiRunning) { - mWifiRunningPath.lineTo(w, h-mWifiRunningOffset); - } - if (lastCpuRunning) { - mCpuRunningPath.lineTo(w, h - mCpuRunningOffset); - } - if (mHavePhoneSignal) { - mPhoneSignalChart.finish(w); - } - } - - private boolean is24Hour() { - return DateFormat.is24HourFormat(getContext()); - } - - private boolean isDayFirst() { - final String value = LocaleData.get(getResources().getConfiguration().locale) - .getDateFormat(java.text.DateFormat.SHORT); - return value.indexOf('M') > value.indexOf('d'); - } - - @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) { - super.onSizeChanged(w, h, oldw, oldh); - - if (DEBUG) Log.d(TAG, "onSizeChanged: " + oldw + "x" + oldh + " to " + w + "x" + h); - - if (mLastWidth == w && mLastHeight == h) { - return; - } - - if (mLastWidth == 0 || mLastHeight == 0) { - return; - } - - if (DEBUG) Log.d(TAG, "Rebuilding chart for: " + w + "x" + h); - - mLastWidth = w; - mLastHeight = h; - mBitmap = null; - mCanvas = null; - - int textHeight = mTextDescent - mTextAscent; - if (h > ((textHeight*10)+mChartMinHeight)) { - mLargeMode = true; - if (h > (textHeight*15)) { - // Plenty of room for the chart. - mLineWidth = textHeight/2; - } else { - // Compress lines to make more room for chart. - mLineWidth = textHeight/3; - } - } else { - mLargeMode = false; - mLineWidth = mThinLineWidth; - } - if (mLineWidth <= 0) mLineWidth = 1; - - mLevelTop = mHeaderHeight; - mLevelLeft = mMaxPercentLabelStringWidth + mThinLineWidth*3; - mLevelRight = w; - int levelWidth = mLevelRight-mLevelLeft; - - mTextPaint.setStrokeWidth(mThinLineWidth); - mBatteryGoodPaint.setStrokeWidth(mThinLineWidth); - mBatteryWarnPaint.setStrokeWidth(mThinLineWidth); - mBatteryCriticalPaint.setStrokeWidth(mThinLineWidth); - mChargingPaint.setStrokeWidth(mLineWidth); - mScreenOnPaint.setStrokeWidth(mLineWidth); - mGpsOnPaint.setStrokeWidth(mLineWidth); - mCameraOnPaint.setStrokeWidth(mLineWidth); - mFlashlightOnPaint.setStrokeWidth(mLineWidth); - mWifiRunningPaint.setStrokeWidth(mLineWidth); - mCpuRunningPaint.setStrokeWidth(mLineWidth); - mDebugRectPaint.setStrokeWidth(1); - - int fullBarOffset = textHeight + mLineWidth; - - if (mLargeMode) { - mChargingOffset = mLineWidth; - mScreenOnOffset = mChargingOffset + fullBarOffset; - mCpuRunningOffset = mScreenOnOffset + fullBarOffset; - mWifiRunningOffset = mCpuRunningOffset + fullBarOffset; - mGpsOnOffset = mWifiRunningOffset + (mHaveWifi ? fullBarOffset : 0); - mFlashlightOnOffset = mGpsOnOffset + (mHaveGps ? fullBarOffset : 0); - mCameraOnOffset = mFlashlightOnOffset + (mHaveFlashlight ? fullBarOffset : 0); - mPhoneSignalOffset = mCameraOnOffset + (mHaveCamera ? fullBarOffset : 0); - mLevelOffset = mPhoneSignalOffset + (mHavePhoneSignal ? fullBarOffset : 0) - + mLineWidth*2 + mLineWidth/2; - if (mHavePhoneSignal) { - mPhoneSignalChart.init(w); - } - } else { - mScreenOnOffset = mGpsOnOffset = mCameraOnOffset = mFlashlightOnOffset = - mWifiRunningOffset = mCpuRunningOffset = mChargingOffset = - mPhoneSignalOffset = 0; - mLevelOffset = fullBarOffset + mThinLineWidth*4; - if (mHavePhoneSignal) { - mPhoneSignalChart.init(0); - } - } - - mBatLevelPath.reset(); - mBatGoodPath.reset(); - mBatWarnPath.reset(); - mTimeRemainPath.reset(); - mBatCriticalPath.reset(); - mScreenOnPath.reset(); - mGpsOnPath.reset(); - mFlashlightOnPath.reset(); - mCameraOnPath.reset(); - mWifiRunningPath.reset(); - mCpuRunningPath.reset(); - mChargingPath.reset(); - - mTimeLabels.clear(); - mDateLabels.clear(); - - final long walltimeStart = mStartWallTime; - final long walltimeChange = mEndWallTime > walltimeStart - ? (mEndWallTime-walltimeStart) : 1; - long curWalltime = mStartWallTime; - long lastRealtime = 0; - - final int batLow = mBatLow; - final int batChange = mBatHigh-mBatLow; - - final int levelh = h - mLevelOffset - mLevelTop; - mLevelBottom = mLevelTop + levelh; - - int x = mLevelLeft, y = 0, startX = mLevelLeft, lastX = -1, lastY = -1; - int i = 0; - Path curLevelPath = null; - Path lastLinePath = null; - boolean lastCharging = false, lastScreenOn = false, lastGpsOn = false; - boolean lastFlashlightOn = false, lastCameraOn = false; - boolean lastWifiRunning = false, lastWifiSupplRunning = false, lastCpuRunning = false; - int lastWifiSupplState = BatteryStats.WIFI_SUPPL_STATE_INVALID; - final int N = mNumHist; - if (mEndDataWallTime > mStartWallTime && mStats.startIteratingHistoryLocked()) { - final HistoryItem rec = new HistoryItem(); - while (mStats.getNextHistoryLocked(rec) && i < N) { - if (rec.isDeltaData()) { - curWalltime += rec.time-lastRealtime; - lastRealtime = rec.time; - x = mLevelLeft + (int)(((curWalltime-walltimeStart)*levelWidth)/walltimeChange); - if (x < 0) { - x = 0; - } - if (false) { - StringBuilder sb = new StringBuilder(128); - sb.append("walloff="); - TimeUtils.formatDuration(curWalltime - walltimeStart, sb); - sb.append(" wallchange="); - TimeUtils.formatDuration(walltimeChange, sb); - sb.append(" x="); - sb.append(x); - Log.d("foo", sb.toString()); - } - y = mLevelTop + levelh - ((rec.batteryLevel-batLow)*(levelh-1))/batChange; - - if (lastX != x) { - // We have moved by at least a pixel. - if (lastY != y) { - // Don't plot changes within a pixel. - Path path; - byte value = rec.batteryLevel; - if (value <= mBatteryCriticalLevel) path = mBatCriticalPath; - else if (value <= mBatteryWarnLevel) path = mBatWarnPath; - else path = null; //mBatGoodPath; - - if (path != lastLinePath) { - if (lastLinePath != null) { - lastLinePath.lineTo(x, y); - } - if (path != null) { - path.moveTo(x, y); - } - lastLinePath = path; - } else if (path != null) { - path.lineTo(x, y); - } - - if (curLevelPath == null) { - curLevelPath = mBatLevelPath; - curLevelPath.moveTo(x, y); - startX = x; - } else { - curLevelPath.lineTo(x, y); - } - lastX = x; - lastY = y; - } - } - - if (mLargeMode) { - final boolean charging = - (rec.states&HistoryItem.STATE_BATTERY_PLUGGED_FLAG) != 0; - if (charging != lastCharging) { - if (charging) { - mChargingPath.moveTo(x, h-mChargingOffset); - } else { - mChargingPath.lineTo(x, h-mChargingOffset); - } - lastCharging = charging; - } - - final boolean screenOn = - (rec.states&HistoryItem.STATE_SCREEN_ON_FLAG) != 0; - if (screenOn != lastScreenOn) { - if (screenOn) { - mScreenOnPath.moveTo(x, h-mScreenOnOffset); - } else { - mScreenOnPath.lineTo(x, h-mScreenOnOffset); - } - lastScreenOn = screenOn; - } - - final boolean gpsOn = - (rec.states&HistoryItem.STATE_GPS_ON_FLAG) != 0; - if (gpsOn != lastGpsOn) { - if (gpsOn) { - mGpsOnPath.moveTo(x, h-mGpsOnOffset); - } else { - mGpsOnPath.lineTo(x, h-mGpsOnOffset); - } - lastGpsOn = gpsOn; - } - - final boolean flashlightOn = - (rec.states2&HistoryItem.STATE2_FLASHLIGHT_FLAG) != 0; - if (flashlightOn != lastFlashlightOn) { - if (flashlightOn) { - mFlashlightOnPath.moveTo(x, h-mFlashlightOnOffset); - } else { - mFlashlightOnPath.lineTo(x, h-mFlashlightOnOffset); - } - lastFlashlightOn = flashlightOn; - } - - final boolean cameraOn = - (rec.states2&HistoryItem.STATE2_CAMERA_FLAG) != 0; - if (cameraOn != lastCameraOn) { - if (cameraOn) { - mCameraOnPath.moveTo(x, h-mCameraOnOffset); - } else { - mCameraOnPath.lineTo(x, h-mCameraOnOffset); - } - lastCameraOn = cameraOn; - } - - final int wifiSupplState = - ((rec.states2&HistoryItem.STATE2_WIFI_SUPPL_STATE_MASK) - >> HistoryItem.STATE2_WIFI_SUPPL_STATE_SHIFT); - boolean wifiRunning; - if (lastWifiSupplState != wifiSupplState) { - lastWifiSupplState = wifiSupplState; - switch (wifiSupplState) { - case BatteryStats.WIFI_SUPPL_STATE_DISCONNECTED: - case BatteryStats.WIFI_SUPPL_STATE_DORMANT: - case BatteryStats.WIFI_SUPPL_STATE_INACTIVE: - case BatteryStats.WIFI_SUPPL_STATE_INTERFACE_DISABLED: - case BatteryStats.WIFI_SUPPL_STATE_INVALID: - case BatteryStats.WIFI_SUPPL_STATE_UNINITIALIZED: - wifiRunning = lastWifiSupplRunning = false; - break; - default: - wifiRunning = lastWifiSupplRunning = true; - break; - } - } else { - wifiRunning = lastWifiSupplRunning; - } - if ((rec.states&(HistoryItem.STATE_WIFI_FULL_LOCK_FLAG - |HistoryItem.STATE_WIFI_MULTICAST_ON_FLAG - |HistoryItem.STATE_WIFI_SCAN_FLAG)) != 0) { - wifiRunning = true; - } - if (wifiRunning != lastWifiRunning) { - if (wifiRunning) { - mWifiRunningPath.moveTo(x, h-mWifiRunningOffset); - } else { - mWifiRunningPath.lineTo(x, h-mWifiRunningOffset); - } - lastWifiRunning = wifiRunning; - } - - final boolean cpuRunning = - (rec.states&HistoryItem.STATE_CPU_RUNNING_FLAG) != 0; - if (cpuRunning != lastCpuRunning) { - if (cpuRunning) { - mCpuRunningPath.moveTo(x, h - mCpuRunningOffset); - } else { - mCpuRunningPath.lineTo(x, h - mCpuRunningOffset); - } - lastCpuRunning = cpuRunning; - } - - if (mLargeMode && mHavePhoneSignal) { - int bin; - if (((rec.states&HistoryItem.STATE_PHONE_STATE_MASK) - >> HistoryItem.STATE_PHONE_STATE_SHIFT) - == ServiceState.STATE_POWER_OFF) { - bin = 0; - } else if ((rec.states&HistoryItem.STATE_PHONE_SCANNING_FLAG) != 0) { - bin = 1; - } else { - bin = (rec.states&HistoryItem.STATE_PHONE_SIGNAL_STRENGTH_MASK) - >> HistoryItem.STATE_PHONE_SIGNAL_STRENGTH_SHIFT; - bin += 2; - } - mPhoneSignalChart.addTick(x, bin); - } - } - - } else { - long lastWalltime = curWalltime; - if (rec.cmd == HistoryItem.CMD_CURRENT_TIME - || rec.cmd == HistoryItem.CMD_RESET) { - if (rec.currentTime >= mStartWallTime) { - curWalltime = rec.currentTime; - } else { - curWalltime = mStartWallTime + (rec.time-mHistStart); - } - lastRealtime = rec.time; - } - - if (rec.cmd != HistoryItem.CMD_OVERFLOW - && (rec.cmd != HistoryItem.CMD_CURRENT_TIME - || Math.abs(lastWalltime-curWalltime) > (60*60*1000))) { - if (curLevelPath != null) { - finishPaths(x+1, h, levelh, startX, lastY, curLevelPath, lastX, - lastCharging, lastScreenOn, lastGpsOn, lastFlashlightOn, - lastCameraOn, lastWifiRunning, lastCpuRunning, lastLinePath); - lastX = lastY = -1; - curLevelPath = null; - lastLinePath = null; - lastCharging = lastScreenOn = lastGpsOn = lastFlashlightOn = - lastCameraOn = lastCpuRunning = false; - } - } - } - - i++; - } - mStats.finishIteratingHistoryLocked(); - } - - if (lastY < 0 || lastX < 0) { - // Didn't get any data... - x = lastX = mLevelLeft; - y = lastY = mLevelTop + levelh - ((mInfo.batteryLevel -batLow)*(levelh-1))/batChange; - Path path; - byte value = (byte)mInfo.batteryLevel; - if (value <= mBatteryCriticalLevel) path = mBatCriticalPath; - else if (value <= mBatteryWarnLevel) path = mBatWarnPath; - else path = null; //mBatGoodPath; - if (path != null) { - path.moveTo(x, y); - lastLinePath = path; - } - mBatLevelPath.moveTo(x, y); - curLevelPath = mBatLevelPath; - x = w; - } else { - // Figure out where the actual data ends on the screen. - x = mLevelLeft + (int)(((mEndDataWallTime-walltimeStart)*levelWidth)/walltimeChange); - if (x < 0) { - x = 0; - } - } - - finishPaths(x, h, levelh, startX, lastY, curLevelPath, lastX, - lastCharging, lastScreenOn, lastGpsOn, lastFlashlightOn, lastCameraOn, - lastWifiRunning, lastCpuRunning, lastLinePath); - - if (x < w) { - // If we reserved room for the remaining time, create a final path to draw - // that part of the UI. - mTimeRemainPath.moveTo(x, lastY); - int fullY = mLevelTop + levelh - ((100-batLow)*(levelh-1))/batChange; - int emptyY = mLevelTop + levelh - ((0-batLow)*(levelh-1))/batChange; - if (mInfo.discharging) { - mTimeRemainPath.lineTo(mLevelRight, emptyY); - } else { - mTimeRemainPath.lineTo(mLevelRight, fullY); - mTimeRemainPath.lineTo(mLevelRight, emptyY); - } - mTimeRemainPath.lineTo(x, emptyY); - mTimeRemainPath.close(); - } - - if (mStartWallTime > 0 && mEndWallTime > mStartWallTime) { - // Create the time labels at the bottom. - boolean is24hr = is24Hour(); - Calendar calStart = Calendar.getInstance(); - calStart.setTimeInMillis(mStartWallTime); - calStart.set(Calendar.MILLISECOND, 0); - calStart.set(Calendar.SECOND, 0); - calStart.set(Calendar.MINUTE, 0); - long startRoundTime = calStart.getTimeInMillis(); - if (startRoundTime < mStartWallTime) { - calStart.set(Calendar.HOUR_OF_DAY, calStart.get(Calendar.HOUR_OF_DAY)+1); - startRoundTime = calStart.getTimeInMillis(); - } - Calendar calEnd = Calendar.getInstance(); - calEnd.setTimeInMillis(mEndWallTime); - calEnd.set(Calendar.MILLISECOND, 0); - calEnd.set(Calendar.SECOND, 0); - calEnd.set(Calendar.MINUTE, 0); - long endRoundTime = calEnd.getTimeInMillis(); - if (startRoundTime < endRoundTime) { - addTimeLabel(calStart, mLevelLeft, mLevelRight, is24hr); - Calendar calMid = Calendar.getInstance(); - calMid.setTimeInMillis(mStartWallTime+((mEndWallTime-mStartWallTime)/2)); - calMid.set(Calendar.MILLISECOND, 0); - calMid.set(Calendar.SECOND, 0); - calMid.set(Calendar.MINUTE, 0); - long calMidMillis = calMid.getTimeInMillis(); - if (calMidMillis > startRoundTime && calMidMillis < endRoundTime) { - addTimeLabel(calMid, mLevelLeft, mLevelRight, is24hr); - } - addTimeLabel(calEnd, mLevelLeft, mLevelRight, is24hr); - } - - // Create the date labels if the chart includes multiple days - if (calStart.get(Calendar.DAY_OF_YEAR) != calEnd.get(Calendar.DAY_OF_YEAR) || - calStart.get(Calendar.YEAR) != calEnd.get(Calendar.YEAR)) { - boolean isDayFirst = isDayFirst(); - calStart.set(Calendar.HOUR_OF_DAY, 0); - startRoundTime = calStart.getTimeInMillis(); - if (startRoundTime < mStartWallTime) { - calStart.set(Calendar.DAY_OF_YEAR, calStart.get(Calendar.DAY_OF_YEAR) + 1); - startRoundTime = calStart.getTimeInMillis(); - } - calEnd.set(Calendar.HOUR_OF_DAY, 0); - endRoundTime = calEnd.getTimeInMillis(); - if (startRoundTime < endRoundTime) { - addDateLabel(calStart, mLevelLeft, mLevelRight, isDayFirst); - Calendar calMid = Calendar.getInstance(); - - // The middle between two beginnings of days can be anywhere between -1 to 13 - // after the beginning of the "median" day. - calMid.setTimeInMillis(startRoundTime + ((endRoundTime - startRoundTime) / 2) - + 2 * 60 * 60 * 1000); - calMid.set(Calendar.HOUR_OF_DAY, 0); - calMid.set(Calendar.MINUTE, 0); - long calMidMillis = calMid.getTimeInMillis(); - if (calMidMillis > startRoundTime && calMidMillis < endRoundTime) { - addDateLabel(calMid, mLevelLeft, mLevelRight, isDayFirst); - } - } - addDateLabel(calEnd, mLevelLeft, mLevelRight, isDayFirst); - } - } - - if (mTimeLabels.size() < 2) { - // If there are fewer than 2 time labels, then they are useless. Just - // show an axis label giving the entire duration. - mDurationString = Formatter.formatShortElapsedTime(getContext(), - mEndWallTime - mStartWallTime); - mDurationStringWidth = (int)mTextPaint.measureText(mDurationString); - } else { - mDurationString = null; - mDurationStringWidth = 0; - } - } - - void addTimeLabel(Calendar cal, int levelLeft, int levelRight, boolean is24hr) { - final long walltimeStart = mStartWallTime; - final long walltimeChange = mEndWallTime-walltimeStart; - mTimeLabels.add(new TimeLabel(mTextPaint, - levelLeft + (int)(((cal.getTimeInMillis()-walltimeStart)*(levelRight-levelLeft)) - / walltimeChange), - cal, is24hr)); - } - - void addDateLabel(Calendar cal, int levelLeft, int levelRight, boolean isDayFirst) { - final long walltimeStart = mStartWallTime; - final long walltimeChange = mEndWallTime-walltimeStart; - mDateLabels.add(new DateLabel(mTextPaint, - levelLeft + (int)(((cal.getTimeInMillis()-walltimeStart)*(levelRight-levelLeft)) - / walltimeChange), - cal, isDayFirst)); - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - - final int width = getWidth(); - final int height = getHeight(); - - //buildBitmap(width, height); - - if (DEBUG) Log.d(TAG, "onDraw: " + width + "x" + height); - //canvas.drawBitmap(mBitmap, 0, 0, null); - drawChart(canvas, width, height); - } - - void buildBitmap(int width, int height) { - if (mBitmap != null && width == mBitmap.getWidth() && height == mBitmap.getHeight()) { - return; - } - - if (DEBUG) Log.d(TAG, "buildBitmap: " + width + "x" + height); - - mBitmap = Bitmap.createBitmap(getResources().getDisplayMetrics(), width, height, - Bitmap.Config.ARGB_8888); - mCanvas = new Canvas(mBitmap); - drawChart(mCanvas, width, height); - } - - void drawChart(Canvas canvas, int width, int height) { - final boolean layoutRtl = isLayoutRtl(); - final int textStartX = layoutRtl ? width : 0; - final int textEndX = layoutRtl ? 0 : width; - final Paint.Align textAlignLeft = layoutRtl ? Paint.Align.RIGHT : Paint.Align.LEFT; - final Paint.Align textAlignRight = layoutRtl ? Paint.Align.LEFT : Paint.Align.RIGHT; - - if (DEBUG) { - canvas.drawRect(1, 1, width, height, mDebugRectPaint); - } - - if (DEBUG) Log.d(TAG, "Drawing level path."); - canvas.drawPath(mBatLevelPath, mBatteryBackgroundPaint); - if (!mTimeRemainPath.isEmpty()) { - if (DEBUG) Log.d(TAG, "Drawing time remain path."); - canvas.drawPath(mTimeRemainPath, mTimeRemainPaint); - } - if (mTimeLabels.size() > 1) { - int y = mLevelBottom - mTextAscent + (mThinLineWidth*4); - int ytick = mLevelBottom+mThinLineWidth+(mThinLineWidth/2); - mTextPaint.setTextAlign(Paint.Align.LEFT); - int lastX = 0; - for (int i=0; i<mTimeLabels.size(); i++) { - TimeLabel label = mTimeLabels.get(i); - if (i == 0) { - int x = label.x - label.width/2; - if (x < 0) { - x = 0; - } - if (DEBUG) Log.d(TAG, "Drawing left label: " + label.label + " @ " + x); - canvas.drawText(label.label, x, y, mTextPaint); - canvas.drawLine(label.x, ytick, label.x, ytick+mThinLineWidth, mTextPaint); - lastX = x + label.width; - } else if (i < (mTimeLabels.size()-1)) { - int x = label.x - label.width/2; - if (x < (lastX+mTextAscent)) { - continue; - } - TimeLabel nextLabel = mTimeLabels.get(i+1); - if (x > (width-nextLabel.width-mTextAscent)) { - continue; - } - if (DEBUG) Log.d(TAG, "Drawing middle label: " + label.label + " @ " + x); - canvas.drawText(label.label, x, y, mTextPaint); - canvas.drawLine(label.x, ytick, label.x, ytick + mThinLineWidth, mTextPaint); - lastX = x + label.width; - } else { - int x = label.x - label.width/2; - if ((x+label.width) >= width) { - x = width-1-label.width; - } - if (DEBUG) Log.d(TAG, "Drawing right label: " + label.label + " @ " + x); - canvas.drawText(label.label, x, y, mTextPaint); - canvas.drawLine(label.x, ytick, label.x, ytick+mThinLineWidth, mTextPaint); - } - } - } else if (mDurationString != null) { - int y = mLevelBottom - mTextAscent + (mThinLineWidth*4); - mTextPaint.setTextAlign(Paint.Align.LEFT); - canvas.drawText(mDurationString, - mLevelLeft + (mLevelRight-mLevelLeft)/2 - mDurationStringWidth/2, - y, mTextPaint); - } - - int headerTop = -mHeaderTextAscent + (mHeaderTextDescent-mHeaderTextAscent)/3; - mHeaderTextPaint.setTextAlign(textAlignLeft); - if (DEBUG) Log.d(TAG, "Drawing charge label string: " + mInfo.chargeLabel); - canvas.drawText(mInfo.chargeLabel.toString(), textStartX, headerTop, - mHeaderTextPaint); - int stringHalfWidth = mChargeDurationStringWidth / 2; - if (layoutRtl) stringHalfWidth = -stringHalfWidth; - int headerCenter = ((width-mChargeDurationStringWidth-mDrainStringWidth)/2) - + (layoutRtl ? mDrainStringWidth : mChargeLabelStringWidth); - if (DEBUG) Log.d(TAG, "Drawing charge duration string: " + mChargeDurationString); - canvas.drawText(mChargeDurationString, headerCenter - stringHalfWidth, headerTop, - mHeaderTextPaint); - mHeaderTextPaint.setTextAlign(textAlignRight); - if (DEBUG) Log.d(TAG, "Drawing drain string: " + mDrainString); - canvas.drawText(mDrainString, textEndX, headerTop, mHeaderTextPaint); - - if (!mBatGoodPath.isEmpty()) { - if (DEBUG) Log.d(TAG, "Drawing good battery path"); - canvas.drawPath(mBatGoodPath, mBatteryGoodPaint); - } - if (!mBatWarnPath.isEmpty()) { - if (DEBUG) Log.d(TAG, "Drawing warn battery path"); - canvas.drawPath(mBatWarnPath, mBatteryWarnPaint); - } - if (!mBatCriticalPath.isEmpty()) { - if (DEBUG) Log.d(TAG, "Drawing critical battery path"); - canvas.drawPath(mBatCriticalPath, mBatteryCriticalPaint); - } - if (mHavePhoneSignal) { - if (DEBUG) Log.d(TAG, "Drawing phone signal path"); - int top = height-mPhoneSignalOffset - (mLineWidth/2); - mPhoneSignalChart.draw(canvas, top, mLineWidth); - } - if (!mScreenOnPath.isEmpty()) { - if (DEBUG) Log.d(TAG, "Drawing screen on path"); - canvas.drawPath(mScreenOnPath, mScreenOnPaint); - } - if (!mChargingPath.isEmpty()) { - if (DEBUG) Log.d(TAG, "Drawing charging path"); - canvas.drawPath(mChargingPath, mChargingPaint); - } - if (mHaveGps) { - if (!mGpsOnPath.isEmpty()) { - if (DEBUG) Log.d(TAG, "Drawing gps path"); - canvas.drawPath(mGpsOnPath, mGpsOnPaint); - } - } - if (mHaveFlashlight) { - if (!mFlashlightOnPath.isEmpty()) { - if (DEBUG) Log.d(TAG, "Drawing flashlight path"); - canvas.drawPath(mFlashlightOnPath, mFlashlightOnPaint); - } - } - if (mHaveCamera) { - if (!mCameraOnPath.isEmpty()) { - if (DEBUG) Log.d(TAG, "Drawing camera path"); - canvas.drawPath(mCameraOnPath, mCameraOnPaint); - } - } - if (mHaveWifi) { - if (!mWifiRunningPath.isEmpty()) { - if (DEBUG) Log.d(TAG, "Drawing wifi path"); - canvas.drawPath(mWifiRunningPath, mWifiRunningPaint); - } - } - if (!mCpuRunningPath.isEmpty()) { - if (DEBUG) Log.d(TAG, "Drawing running path"); - canvas.drawPath(mCpuRunningPath, mCpuRunningPaint); - } - - if (mLargeMode) { - if (DEBUG) Log.d(TAG, "Drawing large mode labels"); - Paint.Align align = mTextPaint.getTextAlign(); - mTextPaint.setTextAlign(textAlignLeft); // large-mode labels always aligned to start - if (mHavePhoneSignal) { - canvas.drawText(mPhoneSignalLabel, textStartX, - height - mPhoneSignalOffset - mTextDescent, mTextPaint); - } - if (mHaveGps) { - canvas.drawText(mGpsOnLabel, textStartX, - height - mGpsOnOffset - mTextDescent, mTextPaint); - } - if (mHaveFlashlight) { - canvas.drawText(mFlashlightOnLabel, textStartX, - height - mFlashlightOnOffset - mTextDescent, mTextPaint); - } - if (mHaveCamera) { - canvas.drawText(mCameraOnLabel, textStartX, - height - mCameraOnOffset - mTextDescent, mTextPaint); - } - if (mHaveWifi) { - canvas.drawText(mWifiRunningLabel, textStartX, - height - mWifiRunningOffset - mTextDescent, mTextPaint); - } - canvas.drawText(mCpuRunningLabel, textStartX, - height - mCpuRunningOffset - mTextDescent, mTextPaint); - canvas.drawText(mChargingLabel, textStartX, - height - mChargingOffset - mTextDescent, mTextPaint); - canvas.drawText(mScreenOnLabel, textStartX, - height - mScreenOnOffset - mTextDescent, mTextPaint); - mTextPaint.setTextAlign(align); - } - - canvas.drawLine(mLevelLeft-mThinLineWidth, mLevelTop, mLevelLeft-mThinLineWidth, - mLevelBottom+(mThinLineWidth/2), mTextPaint); - if (mLargeMode) { - for (int i=0; i<10; i++) { - int y = mLevelTop + mThinLineWidth/2 + ((mLevelBottom-mLevelTop)*i)/10; - canvas.drawLine(mLevelLeft-mThinLineWidth*2-mThinLineWidth/2, y, - mLevelLeft-mThinLineWidth-mThinLineWidth/2, y, mTextPaint); - } - } - if (DEBUG) Log.d(TAG, "Drawing max percent, origw=" + mMaxPercentLabelStringWidth - + ", noww=" + (int)mTextPaint.measureText(mMaxPercentLabelString)); - canvas.drawText(mMaxPercentLabelString, 0, mLevelTop, mTextPaint); - canvas.drawText(mMinPercentLabelString, - mMaxPercentLabelStringWidth-mMinPercentLabelStringWidth, - mLevelBottom - mThinLineWidth, mTextPaint); - canvas.drawLine(mLevelLeft/2, mLevelBottom+mThinLineWidth, width, - mLevelBottom+mThinLineWidth, mTextPaint); - - if (mDateLabels.size() > 0) { - int ytop = mLevelTop + mTextAscent; - int ybottom = mLevelBottom; - int lastLeft = mLevelRight; - mTextPaint.setTextAlign(Paint.Align.LEFT); - for (int i=mDateLabels.size()-1; i>=0; i--) { - DateLabel label = mDateLabels.get(i); - int left = label.x - mThinLineWidth; - int x = label.x + mThinLineWidth*2; - if ((x+label.width) >= lastLeft) { - x = label.x - mThinLineWidth*2 - label.width; - left = x - mThinLineWidth; - if (left >= lastLeft) { - // okay we give up. - continue; - } - } - if (left < mLevelLeft) { - // Won't fit on left, give up. - continue; - } - mDateLinePath.reset(); - mDateLinePath.moveTo(label.x, ytop); - mDateLinePath.lineTo(label.x, ybottom); - canvas.drawPath(mDateLinePath, mDateLinePaint); - canvas.drawText(label.label, x, ytop - mTextAscent, mTextPaint); - } - } - } -} diff --git a/src/com/android/settings/fuelgauge/BatteryHistoryDetail.java b/src/com/android/settings/fuelgauge/BatteryHistoryDetail.java deleted file mode 100644 index 3661467887..0000000000 --- a/src/com/android/settings/fuelgauge/BatteryHistoryDetail.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright (C) 2009 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.fuelgauge; - -import android.content.Intent; -import android.os.BatteryStats; -import android.os.BatteryStats.HistoryItem; -import android.os.Bundle; -import android.util.TypedValue; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.internal.os.BatteryStatsHelper; -import com.android.settings.R; -import com.android.settings.SettingsPreferenceFragment; -import com.android.settings.fuelgauge.BatteryActiveView.BatteryActiveProvider; -import com.android.settings.graph.UsageView; - -public class BatteryHistoryDetail extends SettingsPreferenceFragment { - public static final String EXTRA_STATS = "stats"; - public static final String EXTRA_BROADCAST = "broadcast"; - public static final String BATTERY_HISTORY_FILE = "tmp_bat_history.bin"; - - private BatteryStats mStats; - private Intent mBatteryBroadcast; - - private BatteryFlagParser mChargingParser; - private BatteryFlagParser mScreenOn; - private BatteryFlagParser mGpsParser; - private BatteryFlagParser mFlashlightParser; - private BatteryFlagParser mCameraParser; - private BatteryWifiParser mWifiParser; - private BatteryFlagParser mCpuParser; - private BatteryCellParser mPhoneParser; - - @Override - public void onCreate(Bundle icicle) { - super.onCreate(icicle); - String histFile = getArguments().getString(EXTRA_STATS); - mStats = BatteryStatsHelper.statsFromFile(getActivity(), histFile); - mBatteryBroadcast = getArguments().getParcelable(EXTRA_BROADCAST); - - TypedValue value = new TypedValue(); - getContext().getTheme().resolveAttribute(android.R.attr.colorAccent, value, true); - int accentColor = getContext().getColor(value.resourceId); - - mChargingParser = new BatteryFlagParser(accentColor, false, - HistoryItem.STATE_BATTERY_PLUGGED_FLAG); - mScreenOn = new BatteryFlagParser(accentColor, false, - HistoryItem.STATE_SCREEN_ON_FLAG); - mGpsParser = new BatteryFlagParser(accentColor, false, - HistoryItem.STATE_GPS_ON_FLAG); - mFlashlightParser = new BatteryFlagParser(accentColor, true, - HistoryItem.STATE2_FLASHLIGHT_FLAG); - mCameraParser = new BatteryFlagParser(accentColor, true, - HistoryItem.STATE2_CAMERA_FLAG); - mWifiParser = new BatteryWifiParser(accentColor); - mCpuParser = new BatteryFlagParser(accentColor, false, - HistoryItem.STATE_CPU_RUNNING_FLAG); - mPhoneParser = new BatteryCellParser(); - setHasOptionsMenu(true); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - return inflater.inflate(R.layout.battery_history_detail, container, false); - } - - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - updateEverything(); - } - - private void updateEverything() { - BatteryInfo.getBatteryInfo(getContext(), info -> { - final View view = getView(); - info.bindHistory((UsageView) view.findViewById(R.id.battery_usage), mChargingParser, - mScreenOn, mGpsParser, mFlashlightParser, mCameraParser, mWifiParser, - mCpuParser, mPhoneParser); - ((TextView) view.findViewById(R.id.charge)).setText(info.batteryPercentString); - ((TextView) view.findViewById(R.id.estimation)).setText(info.remainingLabel); - - bindData(mChargingParser, R.string.battery_stats_charging_label, R.id.charging_group); - bindData(mScreenOn, R.string.battery_stats_screen_on_label, R.id.screen_on_group); - bindData(mGpsParser, R.string.battery_stats_gps_on_label, R.id.gps_group); - bindData(mFlashlightParser, R.string.battery_stats_flashlight_on_label, - R.id.flashlight_group); - bindData(mCameraParser, R.string.battery_stats_camera_on_label, R.id.camera_group); - bindData(mWifiParser, R.string.battery_stats_wifi_running_label, R.id.wifi_group); - bindData(mCpuParser, R.string.battery_stats_wake_lock_label, R.id.cpu_group); - bindData(mPhoneParser, R.string.battery_stats_phone_signal_label, - R.id.cell_network_group); - }, mStats, false /* shortString */); - } - - private void bindData(BatteryActiveProvider provider, int label, int groupId) { - View group = getView().findViewById(groupId); - group.setVisibility(provider.hasData() ? View.VISIBLE : View.GONE); - ((TextView) group.findViewById(android.R.id.title)).setText(label); - ((BatteryActiveView) group.findViewById(R.id.battery_active)).setProvider(provider); - } - - @Override - public int getMetricsCategory() { - return MetricsEvent.FUELGAUGE_BATTERY_HISTORY_DETAIL; - } -} diff --git a/src/com/android/settings/fuelgauge/BatteryHistoryPreference.java b/src/com/android/settings/fuelgauge/BatteryHistoryPreference.java index 6ef09570b6..faca9fbcb6 100644 --- a/src/com/android/settings/fuelgauge/BatteryHistoryPreference.java +++ b/src/com/android/settings/fuelgauge/BatteryHistoryPreference.java @@ -17,15 +17,17 @@ package com.android.settings.fuelgauge; import android.content.Context; -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; import android.util.AttributeSet; import android.view.View; import android.widget.TextView; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + import com.android.internal.os.BatteryStatsHelper; import com.android.settings.R; -import com.android.settings.graph.UsageView; +import com.android.settings.widget.UsageView; /** * Custom preference for displaying power consumption as a bar and an icon on the left for the @@ -52,7 +54,7 @@ public class BatteryHistoryPreference extends Preference { BatteryInfo.getBatteryInfo(getContext(), info -> { mBatteryInfo = info; notifyChanged(); - }, batteryStats.getStats(), false); + }, batteryStats, false); } public void setBottomSummary(CharSequence text) { diff --git a/src/com/android/settings/fuelgauge/BatteryInfo.java b/src/com/android/settings/fuelgauge/BatteryInfo.java index 9afaabad39..34a8b916d5 100644 --- a/src/com/android/settings/fuelgauge/BatteryInfo.java +++ b/src/com/android/settings/fuelgauge/BatteryInfo.java @@ -24,15 +24,18 @@ import android.os.BatteryStats; import android.os.BatteryStats.HistoryItem; import android.os.Bundle; import android.os.SystemClock; -import androidx.annotation.WorkerThread; import android.text.format.Formatter; import android.util.SparseIntArray; +import androidx.annotation.WorkerThread; + import com.android.internal.os.BatteryStatsHelper; import com.android.settings.Utils; -import com.android.settings.graph.UsageView; import com.android.settings.overlay.FeatureFactory; +import com.android.settings.widget.UsageView; import com.android.settingslib.R; +import com.android.settingslib.fuelgauge.Estimate; +import com.android.settingslib.fuelgauge.EstimateKt; import com.android.settingslib.utils.PowerUtil; import com.android.settingslib.utils.StringUtil; @@ -43,9 +46,10 @@ public class BatteryInfo { public int batteryLevel; public boolean discharging = true; public long remainingTimeUs = 0; - public long averageTimeToDischarge = Estimate.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN; + public long averageTimeToDischarge = EstimateKt.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN; public String batteryPercentString; public String statusLabel; + public String suggestionLabel; private boolean mCharging; private BatteryStats mStats; private static final String LOG_TAG = "BatteryInfo"; @@ -129,66 +133,25 @@ public class BatteryInfo { remaining = context.getString(R.string.remaining_length_format, Formatter.formatShortElapsedTime(context, remainingTimeUs / 1000)); } - view.setBottomLabels(new CharSequence[]{timeString, remaining}); + view.setBottomLabels(new CharSequence[] {timeString, remaining}); } public static void getBatteryInfo(final Context context, final Callback callback) { - BatteryInfo.getBatteryInfo(context, callback, false /* shortString */); + BatteryInfo.getBatteryInfo(context, callback, null /* statsHelper */, + false /* shortString */); } public static void getBatteryInfo(final Context context, final Callback callback, boolean shortString) { - final long startTime = System.currentTimeMillis(); - BatteryStatsHelper statsHelper = new BatteryStatsHelper(context, true); - statsHelper.create((Bundle) null); - BatteryUtils.logRuntime(LOG_TAG, "time to make batteryStatsHelper", startTime); - BatteryInfo.getBatteryInfo(context, callback, statsHelper, shortString); - } - - public static void getBatteryInfo(final Context context, final Callback callback, - BatteryStatsHelper statsHelper, boolean shortString) { - final long startTime = System.currentTimeMillis(); - BatteryStats stats = statsHelper.getStats(); - BatteryUtils.logRuntime(LOG_TAG, "time for getStats", startTime); - getBatteryInfo(context, callback, stats, shortString); + BatteryInfo.getBatteryInfo(context, callback, null /* statsHelper */, shortString); } public static void getBatteryInfo(final Context context, final Callback callback, - BatteryStats stats, boolean shortString) { + final BatteryStatsHelper statsHelper, boolean shortString) { new AsyncTask<Void, Void, BatteryInfo>() { @Override protected BatteryInfo doInBackground(Void... params) { - final long startTime = System.currentTimeMillis(); - PowerUsageFeatureProvider provider = - FeatureFactory.getFactory(context).getPowerUsageFeatureProvider(context); - final long elapsedRealtimeUs = - PowerUtil.convertMsToUs(SystemClock.elapsedRealtime()); - - Intent batteryBroadcast = context.registerReceiver(null, - new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); - // 0 means we are discharging, anything else means charging - boolean discharging = - batteryBroadcast.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) == 0; - - if (discharging && provider != null - && provider.isEnhancedBatteryPredictionEnabled(context)) { - Estimate estimate = provider.getEnhancedBatteryPrediction(context); - if(estimate != null) { - BatteryUtils - .logRuntime(LOG_TAG, "time for enhanced BatteryInfo", startTime); - return BatteryInfo.getBatteryInfo(context, batteryBroadcast, stats, - estimate, elapsedRealtimeUs, shortString); - } - } - long prediction = discharging - ? stats.computeBatteryTimeRemaining(elapsedRealtimeUs) : 0; - Estimate estimate = new Estimate( - PowerUtil.convertUsToMs(prediction), - false, /* isBasedOnUsage */ - Estimate.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN); - BatteryUtils.logRuntime(LOG_TAG, "time for regular BatteryInfo", startTime); - return BatteryInfo.getBatteryInfo(context, batteryBroadcast, stats, - estimate, elapsedRealtimeUs, shortString); + return getBatteryInfo(context, statsHelper, shortString); } @Override @@ -200,13 +163,61 @@ public class BatteryInfo { }.execute(); } + public static BatteryInfo getBatteryInfo(final Context context, + final BatteryStatsHelper statsHelper, boolean shortString) { + final BatteryStats stats; + final long batteryStatsTime = System.currentTimeMillis(); + if (statsHelper == null) { + final BatteryStatsHelper localStatsHelper = new BatteryStatsHelper(context, + true); + localStatsHelper.create((Bundle) null); + stats = localStatsHelper.getStats(); + } else { + stats = statsHelper.getStats(); + } + BatteryUtils.logRuntime(LOG_TAG, "time for getStats", batteryStatsTime); + + final long startTime = System.currentTimeMillis(); + PowerUsageFeatureProvider provider = + FeatureFactory.getFactory(context).getPowerUsageFeatureProvider(context); + final long elapsedRealtimeUs = + PowerUtil.convertMsToUs(SystemClock.elapsedRealtime()); + + final Intent batteryBroadcast = context.registerReceiver(null, + new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); + // 0 means we are discharging, anything else means charging + final boolean discharging = + batteryBroadcast.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) == 0; + + if (discharging && provider != null + && provider.isEnhancedBatteryPredictionEnabled(context)) { + Estimate estimate = provider.getEnhancedBatteryPrediction(context); + if (estimate != null) { + Estimate.storeCachedEstimate(context, estimate); + BatteryUtils + .logRuntime(LOG_TAG, "time for enhanced BatteryInfo", startTime); + return BatteryInfo.getBatteryInfo(context, batteryBroadcast, stats, + estimate, elapsedRealtimeUs, shortString); + } + } + final long prediction = discharging + ? stats.computeBatteryTimeRemaining(elapsedRealtimeUs) : 0; + final Estimate estimate = new Estimate( + PowerUtil.convertUsToMs(prediction), + false, /* isBasedOnUsage */ + EstimateKt.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN); + BatteryUtils.logRuntime(LOG_TAG, "time for regular BatteryInfo", startTime); + return BatteryInfo.getBatteryInfo(context, batteryBroadcast, stats, + estimate, elapsedRealtimeUs, shortString); + } + @WorkerThread public static BatteryInfo getBatteryInfoOld(Context context, Intent batteryBroadcast, BatteryStats stats, long elapsedRealtimeUs, boolean shortString) { Estimate estimate = new Estimate( PowerUtil.convertUsToMs(stats.computeBatteryTimeRemaining(elapsedRealtimeUs)), false, - Estimate.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN); + EstimateKt.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN); return getBatteryInfo(context, batteryBroadcast, stats, estimate, elapsedRealtimeUs, shortString); } @@ -220,7 +231,7 @@ public class BatteryInfo { info.batteryLevel = Utils.getBatteryLevel(batteryBroadcast); info.batteryPercentString = Utils.formatPercentage(info.batteryLevel); info.mCharging = batteryBroadcast.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0; - info.averageTimeToDischarge = estimate.averageDischargeTime; + info.averageTimeToDischarge = estimate.getAverageDischargeTime(); final Resources resources = context.getResources(); info.statusLabel = Utils.getBatteryStatus(resources, batteryBroadcast); @@ -240,6 +251,7 @@ public class BatteryInfo { final int status = batteryBroadcast.getIntExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_UNKNOWN); info.discharging = false; + info.suggestionLabel = null; if (chargeTime > 0 && status != BatteryManager.BATTERY_STATUS_FULL) { info.remainingTimeUs = chargeTime; CharSequence timeString = StringUtil.formatElapsedTime(context, @@ -260,23 +272,26 @@ public class BatteryInfo { private static void updateBatteryInfoDischarging(Context context, boolean shortString, Estimate estimate, BatteryInfo info) { - final long drainTimeUs = PowerUtil.convertMsToUs(estimate.estimateMillis); + final long drainTimeUs = PowerUtil.convertMsToUs(estimate.getEstimateMillis()); if (drainTimeUs > 0) { info.remainingTimeUs = drainTimeUs; info.remainingLabel = PowerUtil.getBatteryRemainingStringFormatted( context, PowerUtil.convertUsToMs(drainTimeUs), null /* percentageString */, - estimate.isBasedOnUsage && !shortString + estimate.isBasedOnUsage() && !shortString ); info.chargeLabel = PowerUtil.getBatteryRemainingStringFormatted( context, PowerUtil.convertUsToMs(drainTimeUs), info.batteryPercentString, - estimate.isBasedOnUsage && !shortString + estimate.isBasedOnUsage() && !shortString ); + info.suggestionLabel = PowerUtil.getBatteryTipStringFormatted( + context, PowerUtil.convertUsToMs(drainTimeUs)); } else { info.remainingLabel = null; + info.suggestionLabel = null; info.chargeLabel = info.batteryPercentString; } } diff --git a/src/com/android/settings/fuelgauge/BatteryInfoLoader.java b/src/com/android/settings/fuelgauge/BatteryInfoLoader.java index c60f42314f..cd87612ea7 100644 --- a/src/com/android/settings/fuelgauge/BatteryInfoLoader.java +++ b/src/com/android/settings/fuelgauge/BatteryInfoLoader.java @@ -17,17 +17,17 @@ package com.android.settings.fuelgauge; import android.content.Context; -import com.android.internal.os.BatteryStatsHelper; -import com.android.settingslib.utils.AsyncLoader; +import androidx.annotation.VisibleForTesting; -import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.os.BatteryStatsHelper; +import com.android.settingslib.utils.AsyncLoaderCompat; /** * Loader that can be used by classes to load BatteryInfo in a background thread. This loader will * automatically grab enhanced battery estimates if available or fall back to the system estimate * when not available. */ -public class BatteryInfoLoader extends AsyncLoader<BatteryInfo>{ +public class BatteryInfoLoader extends AsyncLoaderCompat<BatteryInfo>{ BatteryStatsHelper mStatsHelper; private static final String LOG_TAG = "BatteryInfoLoader"; diff --git a/src/com/android/settings/fuelgauge/BatteryMeterView.java b/src/com/android/settings/fuelgauge/BatteryMeterView.java index 0ad3724c90..dc30c28ace 100644 --- a/src/com/android/settings/fuelgauge/BatteryMeterView.java +++ b/src/com/android/settings/fuelgauge/BatteryMeterView.java @@ -18,17 +18,17 @@ package com.android.settings.fuelgauge; import android.annotation.Nullable; import android.content.Context; -import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; -import androidx.annotation.VisibleForTesting; import android.util.AttributeSet; import android.widget.ImageView; +import androidx.annotation.VisibleForTesting; + import com.android.settings.R; import com.android.settings.Utils; -import com.android.settingslib.graph.BatteryMeterDrawableBase; +import com.android.settingslib.graph.ThemedBatteryDrawable; public class BatteryMeterView extends ImageView { @VisibleForTesting @@ -37,6 +37,8 @@ public class BatteryMeterView extends ImageView { ColorFilter mErrorColorFilter; @VisibleForTesting ColorFilter mAccentColorFilter; + @VisibleForTesting + ColorFilter mForegroundColorFilter; public BatteryMeterView(Context context) { this(context, null, 0); @@ -51,25 +53,30 @@ public class BatteryMeterView extends ImageView { final int frameColor = context.getColor(R.color.meter_background_color); mAccentColorFilter = new PorterDuffColorFilter( - Utils.getColorAttr(context, android.R.attr.colorAccent), PorterDuff.Mode.SRC_IN); + Utils.getColorAttrDefaultColor(context, android.R.attr.colorAccent), + PorterDuff.Mode.SRC); mErrorColorFilter = new PorterDuffColorFilter( context.getColor(R.color.battery_icon_color_error), PorterDuff.Mode.SRC_IN); - + mForegroundColorFilter =new PorterDuffColorFilter( + Utils.getColorAttrDefaultColor(context, android.R.attr.colorForeground), + PorterDuff.Mode.SRC); mDrawable = new BatteryMeterDrawable(context, frameColor); - mDrawable.setShowPercent(false); - mDrawable.setBatteryColorFilter(mAccentColorFilter); - mDrawable.setWarningColorFilter( - new PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN)); + mDrawable.setColorFilter(mAccentColorFilter); setImageDrawable(mDrawable); } public void setBatteryLevel(int level) { mDrawable.setBatteryLevel(level); - if (level < mDrawable.getCriticalLevel()) { - mDrawable.setBatteryColorFilter(mErrorColorFilter); - } else { - mDrawable.setBatteryColorFilter(mAccentColorFilter); - } + updateColorFilter(); + } + + public void setPowerSave(boolean powerSave) { + mDrawable.setPowerSaveEnabled(powerSave); + updateColorFilter(); + } + + public boolean getPowerSave() { + return mDrawable.getPowerSaveEnabled(); } public int getBatteryLevel() { @@ -85,7 +92,19 @@ public class BatteryMeterView extends ImageView { return mDrawable.getCharging(); } - public static class BatteryMeterDrawable extends BatteryMeterDrawableBase { + private void updateColorFilter() { + final boolean powerSaveEnabled = mDrawable.getPowerSaveEnabled(); + final int level = mDrawable.getBatteryLevel(); + if (powerSaveEnabled) { + mDrawable.setColorFilter(mForegroundColorFilter); + } else if (level < mDrawable.getCriticalLevel()) { + mDrawable.setColorFilter(mErrorColorFilter); + } else { + mDrawable.setColorFilter(mAccentColorFilter); + } + } + + public static class BatteryMeterDrawable extends ThemedBatteryDrawable { private final int mIntrinsicWidth; private final int mIntrinsicHeight; @@ -107,16 +126,5 @@ public class BatteryMeterView extends ImageView { public int getIntrinsicHeight() { return mIntrinsicHeight; } - - public void setWarningColorFilter(@Nullable ColorFilter colorFilter) { - mWarningTextPaint.setColorFilter(colorFilter); - } - - public void setBatteryColorFilter(@Nullable ColorFilter colorFilter) { - mFramePaint.setColorFilter(colorFilter); - mBatteryPaint.setColorFilter(colorFilter); - mBoltPaint.setColorFilter(colorFilter); - } } - } diff --git a/src/com/android/settings/fuelgauge/BatteryOptimizationPreferenceController.java b/src/com/android/settings/fuelgauge/BatteryOptimizationPreferenceController.java index b010998bef..7e003074ca 100644 --- a/src/com/android/settings/fuelgauge/BatteryOptimizationPreferenceController.java +++ b/src/com/android/settings/fuelgauge/BatteryOptimizationPreferenceController.java @@ -15,6 +15,7 @@ package com.android.settings.fuelgauge; import android.os.Bundle; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; @@ -89,7 +90,7 @@ public class BatteryOptimizationPreferenceController extends AbstractPreferenceC new SubSettingLauncher(mSettingsActivity) .setDestination(ManageApplications.class.getName()) .setArguments(args) - .setTitle(R.string.high_power_apps) + .setTitleRes(R.string.high_power_apps) .setSourceMetricsCategory(mFragment.getMetricsCategory()) .launch(); diff --git a/src/com/android/settings/fuelgauge/BatterySaverController.java b/src/com/android/settings/fuelgauge/BatterySaverController.java index 8b32a9cca1..acb5e32569 100644 --- a/src/com/android/settings/fuelgauge/BatterySaverController.java +++ b/src/com/android/settings/fuelgauge/BatterySaverController.java @@ -15,24 +15,25 @@ */ package com.android.settings.fuelgauge; +import android.content.ContentResolver; import android.content.Context; import android.database.ContentObserver; import android.os.Handler; import android.os.Looper; import android.os.PowerManager; import android.provider.Settings; -import androidx.annotation.VisibleForTesting; +import android.provider.Settings.Global; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; import com.android.settings.R; import com.android.settings.Utils; import com.android.settings.core.BasePreferenceController; -import com.android.settings.dashboard.conditional.BatterySaverCondition; -import com.android.settings.dashboard.conditional.ConditionManager; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnStart; import com.android.settingslib.core.lifecycle.events.OnStop; +import com.android.settingslib.fuelgauge.BatterySaverUtils; public class BatterySaverController extends BasePreferenceController implements LifecycleObserver, OnStart, OnStop, BatterySaverReceiver.BatterySaverListener { @@ -47,11 +48,12 @@ public class BatterySaverController extends BasePreferenceController mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); mBatteryStateChangeReceiver = new BatterySaverReceiver(context); mBatteryStateChangeReceiver.setBatterySaverListener(this); + BatterySaverUtils.revertScheduleToNoneIfNeeded(context); } @Override public int getAvailabilityStatus() { - return AVAILABLE; + return AVAILABLE_UNSEARCHABLE; } @Override @@ -81,23 +83,25 @@ public class BatterySaverController extends BasePreferenceController mBatteryStateChangeReceiver.setListening(false); } - @VisibleForTesting - void refreshConditionManager() { - ConditionManager.get(mContext).getCondition(BatterySaverCondition.class).refreshState(); - } - @Override public CharSequence getSummary() { + final ContentResolver resolver = mContext.getContentResolver(); final boolean isPowerSaveOn = mPowerManager.isPowerSaveMode(); - final int percent = Settings.Global.getInt(mContext.getContentResolver(), + final int percent = Settings.Global.getInt(resolver, Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL, 0); + final int mode = Settings.Global.getInt(resolver, + Global.AUTOMATIC_POWER_SAVE_MODE, PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE); if (isPowerSaveOn) { return mContext.getString(R.string.battery_saver_on_summary); - } else if (percent != 0) { - return mContext.getString(R.string.battery_saver_off_scheduled_summary, - Utils.formatPercentage(percent)); + } else if (mode == PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE) { + if (percent != 0) { + return mContext.getString(R.string.battery_saver_off_scheduled_summary, + Utils.formatPercentage(percent)); + } else { + return mContext.getString(R.string.battery_saver_off_summary); + } } else { - return mContext.getString(R.string.battery_saver_off_summary); + return mContext.getString(R.string.battery_saver_auto_routine); } } diff --git a/src/com/android/settings/fuelgauge/BatterySaverDrawable.java b/src/com/android/settings/fuelgauge/BatterySaverDrawable.java index 0d3008a0e3..ce2936177a 100644 --- a/src/com/android/settings/fuelgauge/BatterySaverDrawable.java +++ b/src/com/android/settings/fuelgauge/BatterySaverDrawable.java @@ -37,7 +37,7 @@ public class BatterySaverDrawable extends BatteryMeterDrawableBase { setPowerSave(true); setCharging(false); setPowerSaveAsColorError(false); - final int tintColor = Utils.getColorAttr(context, android.R.attr.colorAccent); + final int tintColor = Utils.getColorAttrDefaultColor(context, android.R.attr.colorAccent); setColorFilter(new PorterDuffColorFilter(tintColor, PorterDuff.Mode.SRC_IN)); } -}
\ No newline at end of file +} diff --git a/src/com/android/settings/fuelgauge/BatteryStatsHelperLoader.java b/src/com/android/settings/fuelgauge/BatteryStatsHelperLoader.java index addd30945c..5de83d3f42 100644 --- a/src/com/android/settings/fuelgauge/BatteryStatsHelperLoader.java +++ b/src/com/android/settings/fuelgauge/BatteryStatsHelperLoader.java @@ -17,18 +17,17 @@ package com.android.settings.fuelgauge; import android.content.Context; -import android.os.BatteryStats; -import android.os.Bundle; import android.os.UserManager; + import androidx.annotation.VisibleForTesting; import com.android.internal.os.BatteryStatsHelper; -import com.android.settingslib.utils.AsyncLoader; +import com.android.settingslib.utils.AsyncLoaderCompat; /** * Loader to get new {@link BatteryStatsHelper} in the background */ -public class BatteryStatsHelperLoader extends AsyncLoader<BatteryStatsHelper> { +public class BatteryStatsHelperLoader extends AsyncLoaderCompat<BatteryStatsHelper> { @VisibleForTesting UserManager mUserManager; @VisibleForTesting diff --git a/src/com/android/settings/fuelgauge/BatteryUtils.java b/src/com/android/settings/fuelgauge/BatteryUtils.java index 8540f0fd69..03387f9ef8 100644 --- a/src/com/android/settings/fuelgauge/BatteryUtils.java +++ b/src/com/android/settings/fuelgauge/BatteryUtils.java @@ -30,28 +30,34 @@ import android.os.Process; import android.os.SystemClock; import android.os.UserHandle; import android.os.UserManager; +import android.text.format.DateUtils; +import android.util.Log; +import android.util.SparseLongArray; + import androidx.annotation.IntDef; import androidx.annotation.Nullable; -import androidx.annotation.StringRes; import androidx.annotation.VisibleForTesting; import androidx.annotation.WorkerThread; -import android.text.format.DateUtils; -import android.util.Log; -import android.util.SparseLongArray; import com.android.internal.os.BatterySipper; import com.android.internal.os.BatteryStatsHelper; import com.android.internal.util.ArrayUtils; -import com.android.settings.R; -import com.android.settings.fuelgauge.anomaly.Anomaly; +import com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper; import com.android.settings.fuelgauge.batterytip.AnomalyInfo; +import com.android.settings.fuelgauge.batterytip.BatteryDatabaseManager; import com.android.settings.fuelgauge.batterytip.StatsManagerConfig; import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.applications.AppUtils; +import com.android.settingslib.fuelgauge.Estimate; +import com.android.settingslib.fuelgauge.EstimateKt; import com.android.settingslib.fuelgauge.PowerWhitelistBackend; import com.android.settingslib.utils.PowerUtil; +import com.android.settingslib.utils.ThreadUtils; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.time.Duration; +import java.time.Instant; import java.util.Collections; import java.util.Comparator; import java.util.List; @@ -101,8 +107,8 @@ public class BatteryUtils { mContext = context; mPackageManager = context.getPackageManager(); mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); - mPowerUsageFeatureProvider = FeatureFactory.getFactory( - context).getPowerUsageFeatureProvider(context); + mPowerUsageFeatureProvider = FeatureFactory.getFactory(context) + .getPowerUsageFeatureProvider(context); } public long getProcessTimeMs(@StatusType int type, @Nullable BatteryStats.Uid uid, @@ -184,8 +190,10 @@ public class BatteryUtils { && sipper.drainType != BatterySipper.DrainType.UNACCOUNTED && sipper.drainType != BatterySipper.DrainType.BLUETOOTH && sipper.drainType != BatterySipper.DrainType.WIFI - && sipper.drainType != BatterySipper.DrainType.IDLE) { - // Don't add it if it is overcounted, unaccounted, wifi, bluetooth, or screen + && sipper.drainType != BatterySipper.DrainType.IDLE + && !isHiddenSystemModule(sipper)) { + // Don't add it if it is overcounted, unaccounted, wifi, bluetooth, screen + // or hidden system modules proportionalSmearPowerMah += sipper.totalPowerMah; } } @@ -248,7 +256,27 @@ public class BatteryUtils { || drainType == BatterySipper.DrainType.WIFI || (sipper.totalPowerMah * SECONDS_IN_HOUR) < MIN_POWER_THRESHOLD_MILLI_AMP || mPowerUsageFeatureProvider.isTypeService(sipper) - || mPowerUsageFeatureProvider.isTypeSystem(sipper); + || mPowerUsageFeatureProvider.isTypeSystem(sipper) + || isHiddenSystemModule(sipper); + } + + /** + * Return {@code true} if one of packages in {@code sipper} is hidden system modules + */ + public boolean isHiddenSystemModule(BatterySipper sipper) { + if (sipper.uidObj == null) { + return false; + } + sipper.mPackages = mPackageManager.getPackagesForUid(sipper.getUid()); + if (sipper.mPackages != null) { + for (int i = 0, length = sipper.mPackages.length; i < length; i++) { + if (AppUtils.isHiddenSystemModule(mContext, sipper.mPackages[i])) { + return true; + } + } + } + + return false; } /** @@ -391,20 +419,6 @@ public class BatteryUtils { } } - @StringRes - public int getSummaryResIdFromAnomalyType(@Anomaly.AnomalyType int type) { - switch (type) { - case Anomaly.AnomalyType.WAKE_LOCK: - return R.string.battery_abnormal_wakelock_summary; - case Anomaly.AnomalyType.WAKEUP_ALARM: - return R.string.battery_abnormal_wakeup_alarm_summary; - case Anomaly.AnomalyType.BLUETOOTH_SCAN: - return R.string.battery_abnormal_location_summary; - default: - throw new IllegalArgumentException("Incorrect anomaly type: " + type); - } - } - public void setForceAppStandby(int uid, String packageName, int mode) { final boolean isPreOApp = isPreOApp(packageName); @@ -414,6 +428,18 @@ public class BatteryUtils { } // Control whether app could run jobs in the background mAppOpsManager.setMode(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, uid, packageName, mode); + + ThreadUtils.postOnBackgroundThread(() -> { + final BatteryDatabaseManager batteryDatabaseManager = BatteryDatabaseManager + .getInstance(mContext); + if (mode == AppOpsManager.MODE_IGNORED) { + batteryDatabaseManager.insertAction(AnomalyDatabaseHelper.ActionType.RESTRICTION, + uid, packageName, System.currentTimeMillis()); + } else if (mode == AppOpsManager.MODE_ALLOWED) { + batteryDatabaseManager.deleteAction(AnomalyDatabaseHelper.ActionType.RESTRICTION, + uid, packageName); + } + }); } public boolean isForceAppStandbyEnabled(int uid, String packageName) { @@ -421,6 +447,16 @@ public class BatteryUtils { packageName) == AppOpsManager.MODE_IGNORED; } + public boolean clearForceAppStandby(String packageName) { + final int uid = getPackageUid(packageName); + if (uid != UID_NULL && isForceAppStandbyEnabled(uid, packageName)) { + setForceAppStandby(uid, packageName, AppOpsManager.MODE_ALLOWED); + return true; + } else { + return false; + } + } + public void initBatteryStatsHelper(BatteryStatsHelper statsHelper, Bundle bundle, UserManager userManager) { statsHelper.create(bundle); @@ -439,17 +475,14 @@ public class BatteryUtils { SystemClock.elapsedRealtime()); final BatteryStats stats = statsHelper.getStats(); BatteryInfo batteryInfo; + Estimate estimate = getEnhancedEstimate(); - final Estimate estimate; - // Get enhanced prediction if available - if (mPowerUsageFeatureProvider != null && - mPowerUsageFeatureProvider.isEnhancedBatteryPredictionEnabled(mContext)) { - estimate = mPowerUsageFeatureProvider.getEnhancedBatteryPrediction(mContext); - } else { + // couldn't get estimate from cache or provider, use fallback + if (estimate == null) { estimate = new Estimate( PowerUtil.convertUsToMs(stats.computeBatteryTimeRemaining(elapsedRealtimeUs)), false /* isBasedOnUsage */, - Estimate.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN); + EstimateKt.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN); } BatteryUtils.logRuntime(tag, "BatteryInfoLoader post query", startTime); @@ -460,6 +493,23 @@ public class BatteryUtils { return batteryInfo; } + @VisibleForTesting + Estimate getEnhancedEstimate() { + Estimate estimate = null; + // Get enhanced prediction if available + if (Duration.between(Estimate.getLastCacheUpdateTime(mContext), Instant.now()) + .compareTo(Duration.ofSeconds(10)) < 0) { + estimate = Estimate.getCachedEstimateIfAvailable(mContext); + } else if (mPowerUsageFeatureProvider != null && + mPowerUsageFeatureProvider.isEnhancedBatteryPredictionEnabled(mContext)) { + estimate = mPowerUsageFeatureProvider.getEnhancedBatteryPrediction(mContext); + if (estimate != null) { + Estimate.storeCachedEstimate(mContext, estimate); + } + } + return estimate; + } + /** * Find the {@link BatterySipper} with the corresponding {@link BatterySipper.DrainType} */ diff --git a/src/com/android/settings/fuelgauge/DebugEstimatesLoader.java b/src/com/android/settings/fuelgauge/DebugEstimatesLoader.java index 784902fca7..c8dbb59a4b 100644 --- a/src/com/android/settings/fuelgauge/DebugEstimatesLoader.java +++ b/src/com/android/settings/fuelgauge/DebugEstimatesLoader.java @@ -20,14 +20,18 @@ import android.content.Intent; import android.content.IntentFilter; import android.os.BatteryStats; import android.os.SystemClock; + import com.android.internal.os.BatteryStatsHelper; import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.fuelgauge.Estimate; +import com.android.settingslib.fuelgauge.EstimateKt; +import com.android.settingslib.utils.AsyncLoaderCompat; import com.android.settingslib.utils.PowerUtil; -import com.android.settingslib.utils.AsyncLoader; + import java.util.ArrayList; import java.util.List; -public class DebugEstimatesLoader extends AsyncLoader<List<BatteryInfo>> { +public class DebugEstimatesLoader extends AsyncLoaderCompat<List<BatteryInfo>> { private BatteryStatsHelper mStatsHelper; public DebugEstimatesLoader(Context context, BatteryStatsHelper statsHelper) { @@ -58,7 +62,7 @@ public class DebugEstimatesLoader extends AsyncLoader<List<BatteryInfo>> { Estimate estimate = powerUsageFeatureProvider.getEnhancedBatteryPrediction(context); if (estimate == null) { - estimate = new Estimate(0, false, Estimate.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN); + estimate = new Estimate(0, false, EstimateKt.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN); } BatteryInfo newInfo = BatteryInfo.getBatteryInfo(getContext(), batteryBroadcast, stats, estimate, elapsedRealtimeUs, false); diff --git a/src/com/android/settings/fuelgauge/Estimate.java b/src/com/android/settings/fuelgauge/Estimate.java deleted file mode 100644 index f59bbf15ac..0000000000 --- a/src/com/android/settings/fuelgauge/Estimate.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.android.settings.fuelgauge; - -public class Estimate { - - // Value to indicate averageTimeToDischarge could not be obtained - public static final int AVERAGE_TIME_TO_DISCHARGE_UNKNOWN = -1; - - public final long estimateMillis; - public final boolean isBasedOnUsage; - public final long averageDischargeTime; - - public Estimate(long estimateMillis, boolean isBasedOnUsage, - long averageDischargeTime) { - this.estimateMillis = estimateMillis; - this.isBasedOnUsage = isBasedOnUsage; - this.averageDischargeTime = averageDischargeTime; - } -} diff --git a/src/com/android/settings/fuelgauge/HighPowerDetail.java b/src/com/android/settings/fuelgauge/HighPowerDetail.java index 7dfa0befa7..6448d9a226 100644 --- a/src/com/android/settings/fuelgauge/HighPowerDetail.java +++ b/src/com/android/settings/fuelgauge/HighPowerDetail.java @@ -16,10 +16,9 @@ package com.android.settings.fuelgauge; -import android.app.AlertDialog; import android.app.AppOpsManager; import android.app.Dialog; -import android.app.Fragment; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; @@ -30,8 +29,10 @@ import android.view.View; import android.widget.Checkable; import android.widget.TextView; -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.logging.nano.MetricsProto; +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; + import com.android.settings.R; import com.android.settings.applications.AppInfoBase; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; @@ -61,7 +62,7 @@ public class HighPowerDetail extends InstrumentedDialogFragment implements OnCli @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DIALOG_HIGH_POWER_DETAILS; + return SettingsEnums.DIALOG_HIGH_POWER_DETAILS; } @Override @@ -152,8 +153,8 @@ public class HighPowerDetail extends InstrumentedDialogFragment implements OnCli @VisibleForTesting static void logSpecialPermissionChange(boolean whitelist, String packageName, Context context) { - int logCategory = whitelist ? MetricsProto.MetricsEvent.APP_SPECIAL_PERMISSION_BATTERY_DENY - : MetricsProto.MetricsEvent.APP_SPECIAL_PERMISSION_BATTERY_ALLOW; + int logCategory = whitelist ? SettingsEnums.APP_SPECIAL_PERMISSION_BATTERY_DENY + : SettingsEnums.APP_SPECIAL_PERMISSION_BATTERY_ALLOW; FeatureFactory.getFactory(context).getMetricsFeatureProvider().action(context, logCategory, packageName); } @@ -172,10 +173,18 @@ public class HighPowerDetail extends InstrumentedDialogFragment implements OnCli } public static CharSequence getSummary(Context context, String pkg) { - PowerWhitelistBackend powerWhitelist = PowerWhitelistBackend.getInstance(context); - return context.getString(powerWhitelist.isSysWhitelisted(pkg) ? R.string.high_power_system - : powerWhitelist.isWhitelisted(pkg) ? R.string.high_power_on - : R.string.high_power_off); + return getSummary(context, PowerWhitelistBackend.getInstance(context), pkg); + } + + @VisibleForTesting + static CharSequence getSummary(Context context, PowerWhitelistBackend powerWhitelist, + String pkg) { + return context.getString( + powerWhitelist.isSysWhitelisted(pkg) || powerWhitelist.isDefaultActiveApp(pkg) + ? R.string.high_power_system + : powerWhitelist.isWhitelisted(pkg) + ? R.string.high_power_on + : R.string.high_power_off); } public static void show(Fragment caller, int uid, String packageName, int requestCode) { diff --git a/src/com/android/settings/fuelgauge/InactiveApps.java b/src/com/android/settings/fuelgauge/InactiveApps.java index f93cffc1f4..fdfcaabef9 100644 --- a/src/com/android/settings/fuelgauge/InactiveApps.java +++ b/src/com/android/settings/fuelgauge/InactiveApps.java @@ -23,6 +23,7 @@ import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_NEVER; import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_RARE; import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_WORKING_SET; +import android.app.settings.SettingsEnums; import android.app.usage.UsageStatsManager; import android.content.Context; import android.content.Intent; @@ -30,15 +31,13 @@ import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Resources; import android.os.Bundle; + import androidx.preference.ListPreference; import androidx.preference.Preference; -import androidx.preference.Preference.OnPreferenceClickListener; import androidx.preference.PreferenceGroup; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; -import com.android.settings.widget.RadioButtonPreference; import java.util.List; @@ -59,7 +58,7 @@ public class InactiveApps extends SettingsPreferenceFragment @Override public int getMetricsCategory() { - return MetricsEvent.FUELGAUGE_INACTIVE_APPS; + return SettingsEnums.FUELGAUGE_INACTIVE_APPS; } @Override diff --git a/src/com/android/settings/fuelgauge/PowerGaugePreference.java b/src/com/android/settings/fuelgauge/PowerGaugePreference.java index 95ff45c922..8cac2b2412 100644 --- a/src/com/android/settings/fuelgauge/PowerGaugePreference.java +++ b/src/com/android/settings/fuelgauge/PowerGaugePreference.java @@ -17,15 +17,15 @@ package com.android.settings.fuelgauge; import android.content.Context; -import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; -import androidx.preference.PreferenceViewHolder; import android.util.AttributeSet; import android.widget.TextView; +import androidx.preference.PreferenceViewHolder; + import com.android.settings.R; import com.android.settings.Utils; -import com.android.settings.widget.AppPreference; +import com.android.settingslib.widget.apppreference.AppPreference; /** * Custom preference for displaying battery usage info as a bar and an icon on @@ -57,7 +57,9 @@ public class PowerGaugePreference extends AppPreference { private PowerGaugePreference(Context context, AttributeSet attrs, Drawable icon, CharSequence contentDescription, BatteryEntry info) { super(context, attrs); - setIcon(icon != null ? icon : new ColorDrawable(0)); + if (icon != null) { + setIcon(icon); + } setWidgetLayoutResource(R.layout.preference_widget_summary); mInfo = info; mContentDescription = contentDescription; diff --git a/src/com/android/settings/fuelgauge/PowerUsageAdvanced.java b/src/com/android/settings/fuelgauge/PowerUsageAdvanced.java index b73245733e..669d7ed0b7 100644 --- a/src/com/android/settings/fuelgauge/PowerUsageAdvanced.java +++ b/src/com/android/settings/fuelgauge/PowerUsageAdvanced.java @@ -15,28 +15,31 @@ package com.android.settings.fuelgauge; import static com.android.settings.fuelgauge.BatteryBroadcastReceiver.BatteryUpdateType; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.BatteryManager; import android.os.Bundle; import android.provider.SearchIndexableResource; -import androidx.annotation.VisibleForTesting; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; -import com.android.internal.logging.nano.MetricsProto; +import androidx.annotation.VisibleForTesting; + import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.overlay.FeatureFactory; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +@SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC) public class PowerUsageAdvanced extends PowerUsageBase { private static final String TAG = "AdvancedBatteryUsage"; private static final String KEY_BATTERY_GRAPH = "battery_graph"; @@ -78,7 +81,7 @@ public class PowerUsageAdvanced extends PowerUsageBase { @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.FUELGAUGE_BATTERY_HISTORY_DETAIL; + return SettingsEnums.FUELGAUGE_BATTERY_HISTORY_DETAIL; } @Override @@ -105,7 +108,7 @@ public class PowerUsageAdvanced extends PowerUsageBase { mShowAllApps = !mShowAllApps; item.setTitle(mShowAllApps ? R.string.hide_extra_apps : R.string.show_all_apps); mMetricsFeatureProvider.action(getContext(), - MetricsProto.MetricsEvent.ACTION_SETTINGS_MENU_BATTERY_APPS_TOGGLE, + SettingsEnums.ACTION_SETTINGS_MENU_BATTERY_APPS_TOGGLE, mShowAllApps); restartBatteryStatsLoader(BatteryUpdateType.MANUAL); return true; @@ -132,7 +135,7 @@ public class PowerUsageAdvanced extends PowerUsageBase { final List<AbstractPreferenceController> controllers = new ArrayList<>(); mBatteryAppListPreferenceController = new BatteryAppListPreferenceController(context, - KEY_APP_LIST, getLifecycle(), (SettingsActivity) getActivity(), this); + KEY_APP_LIST, getSettingsLifecycle(), (SettingsActivity) getActivity(), this); controllers.add(mBatteryAppListPreferenceController); return controllers; diff --git a/src/com/android/settings/fuelgauge/PowerUsageAnomalyDetails.java b/src/com/android/settings/fuelgauge/PowerUsageAnomalyDetails.java deleted file mode 100644 index cd34a477a6..0000000000 --- a/src/com/android/settings/fuelgauge/PowerUsageAnomalyDetails.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright (C) 2017 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.fuelgauge; - -import android.content.Context; -import android.content.pm.PackageManager; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.os.UserHandle; -import androidx.preference.Preference; -import androidx.preference.PreferenceGroup; -import android.util.IconDrawableFactory; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.logging.nano.MetricsProto; -import com.android.settings.R; -import com.android.settings.SettingsActivity; -import com.android.settings.Utils; -import com.android.settings.core.InstrumentedPreferenceFragment; -import com.android.settings.core.SubSettingLauncher; -import com.android.settings.dashboard.DashboardFragment; -import com.android.settings.fuelgauge.anomaly.Anomaly; -import com.android.settings.fuelgauge.anomaly.AnomalyDialogFragment; -import com.android.settings.fuelgauge.anomaly.AnomalyPreference; -import com.android.settingslib.core.AbstractPreferenceController; - -import java.util.List; - -/** - * Fragment to show a list of anomaly apps, where user could handle these anomalies - */ -public class PowerUsageAnomalyDetails extends DashboardFragment implements - AnomalyDialogFragment.AnomalyDialogListener { - - public static final String TAG = "PowerAbnormalUsageDetail"; - @VisibleForTesting - static final String EXTRA_ANOMALY_LIST = "anomaly_list"; - private static final int REQUEST_ANOMALY_ACTION = 0; - private static final String KEY_PREF_ANOMALY_LIST = "app_abnormal_list"; - - @VisibleForTesting - List<Anomaly> mAnomalies; - @VisibleForTesting - PreferenceGroup mAbnormalListGroup; - @VisibleForTesting - PackageManager mPackageManager; - @VisibleForTesting - BatteryUtils mBatteryUtils; - @VisibleForTesting - IconDrawableFactory mIconDrawableFactory; - - public static void startBatteryAbnormalPage(SettingsActivity caller, - InstrumentedPreferenceFragment fragment, List<Anomaly> anomalies) { - Bundle args = new Bundle(); - args.putParcelableList(EXTRA_ANOMALY_LIST, anomalies); - - new SubSettingLauncher(caller) - .setDestination(PowerUsageAnomalyDetails.class.getName()) - .setTitle(R.string.battery_abnormal_details_title) - .setArguments(args) - .setSourceMetricsCategory(fragment.getMetricsCategory()) - .launch(); - } - - @Override - public void onCreate(Bundle icicle) { - super.onCreate(icicle); - final Context context = getContext(); - - mAnomalies = getArguments().getParcelableArrayList(EXTRA_ANOMALY_LIST); - mAbnormalListGroup = (PreferenceGroup) findPreference(KEY_PREF_ANOMALY_LIST); - mPackageManager = context.getPackageManager(); - mIconDrawableFactory = IconDrawableFactory.newInstance(context); - mBatteryUtils = BatteryUtils.getInstance(context); - } - - @Override - public void onResume() { - super.onResume(); - - refreshUi(); - } - - @Override - public boolean onPreferenceTreeClick(Preference preference) { - if (preference instanceof AnomalyPreference) { - AnomalyPreference anomalyPreference = (AnomalyPreference) preference; - final Anomaly anomaly = anomalyPreference.getAnomaly(); - - AnomalyDialogFragment dialogFragment = AnomalyDialogFragment.newInstance(anomaly, - MetricsProto.MetricsEvent.FUELGAUGE_ANOMALY_DETAIL); - dialogFragment.setTargetFragment(this, REQUEST_ANOMALY_ACTION); - dialogFragment.show(getFragmentManager(), TAG); - - return true; - } - - return super.onPreferenceTreeClick(preference); - } - - @Override - protected String getLogTag() { - return TAG; - } - - @Override - protected int getPreferenceScreenResId() { - return R.xml.power_abnormal_detail; - } - - @Override - protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { - return null; - } - - @Override - public int getMetricsCategory() { - return MetricsProto.MetricsEvent.FUELGAUGE_ANOMALY_DETAIL; - } - - void refreshUi() { - mAbnormalListGroup.removeAll(); - for (int i = 0, size = mAnomalies.size(); i < size; i++) { - final Anomaly anomaly = mAnomalies.get(i); - Preference pref = new AnomalyPreference(getPrefContext(), anomaly); - pref.setSummary(mBatteryUtils.getSummaryResIdFromAnomalyType(anomaly.type)); - Drawable icon = getBadgedIcon(anomaly.packageName, UserHandle.getUserId(anomaly.uid)); - if (icon != null) { - pref.setIcon(icon); - } - - mAbnormalListGroup.addPreference(pref); - } - } - - @Override - public void onAnomalyHandled(Anomaly anomaly) { - mAnomalies.remove(anomaly); - refreshUi(); - } - - @VisibleForTesting - Drawable getBadgedIcon(String packageName, int userId) { - return Utils.getBadgedIcon(mIconDrawableFactory, mPackageManager, packageName, userId); - } -} diff --git a/src/com/android/settings/fuelgauge/PowerUsageBase.java b/src/com/android/settings/fuelgauge/PowerUsageBase.java index aa029191a8..ec73b1b57a 100644 --- a/src/com/android/settings/fuelgauge/PowerUsageBase.java +++ b/src/com/android/settings/fuelgauge/PowerUsageBase.java @@ -15,17 +15,18 @@ */ package com.android.settings.fuelgauge; -import static com.android.settings.fuelgauge.BatteryBroadcastReceiver.*; +import static com.android.settings.fuelgauge.BatteryBroadcastReceiver.BatteryUpdateType; import android.app.Activity; -import android.app.LoaderManager; import android.content.Context; -import android.content.Loader; import android.os.Bundle; import android.os.UserManager; -import androidx.annotation.VisibleForTesting; import android.view.Menu; +import androidx.annotation.VisibleForTesting; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.Loader; + import com.android.internal.os.BatteryStatsHelper; import com.android.settings.dashboard.DashboardFragment; @@ -71,8 +72,6 @@ public abstract class PowerUsageBase extends DashboardFragment { @Override public void onResume() { super.onResume(); - - BatteryStatsHelper.dropFile(getActivity(), BatteryHistoryDetail.BATTERY_HISTORY_FILE); mBatteryBroadcastReceiver.register(); } diff --git a/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java b/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java index a6474fb21d..4f292dddac 100644 --- a/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java +++ b/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java @@ -21,6 +21,7 @@ import android.content.Intent; import android.util.SparseIntArray; import com.android.internal.os.BatterySipper; +import com.android.settingslib.fuelgauge.Estimate; /** * Feature Provider used in power usage diff --git a/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java b/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java index b76aef087c..ab71c97e14 100644 --- a/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java +++ b/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java @@ -24,6 +24,7 @@ import android.util.SparseIntArray; import com.android.internal.os.BatterySipper; import com.android.internal.util.ArrayUtils; +import com.android.settingslib.fuelgauge.Estimate; public class PowerUsageFeatureProviderImpl implements PowerUsageFeatureProvider { diff --git a/src/com/android/settings/fuelgauge/PowerUsageSummary.java b/src/com/android/settings/fuelgauge/PowerUsageSummary.java index 2332c91d99..880255b212 100644 --- a/src/com/android/settings/fuelgauge/PowerUsageSummary.java +++ b/src/com/android/settings/fuelgauge/PowerUsageSummary.java @@ -18,18 +18,18 @@ package com.android.settings.fuelgauge; import static com.android.settings.fuelgauge.BatteryBroadcastReceiver.BatteryUpdateType; -import android.app.Activity; -import android.app.LoaderManager; -import android.app.LoaderManager.LoaderCallbacks; +import android.app.settings.SettingsEnums; +import android.content.ContentResolver; import android.content.Context; -import android.content.Loader; +import android.database.ContentObserver; +import android.net.Uri; import android.os.BatteryStats; import android.os.Bundle; +import android.os.Handler; import android.provider.SearchIndexableResource; -import androidx.annotation.VisibleForTesting; -import android.text.BidiFormatter; +import android.provider.Settings; +import android.provider.Settings.Global; import android.text.format.Formatter; -import android.util.SparseArray; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -37,27 +37,26 @@ import android.view.View; import android.view.View.OnLongClickListener; import android.widget.TextView; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.annotation.VisibleForTesting; +import androidx.loader.app.LoaderManager; +import androidx.loader.app.LoaderManager.LoaderCallbacks; +import androidx.loader.content.Loader; + import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.Utils; -import com.android.settings.applications.LayoutPreference; import com.android.settings.core.SubSettingLauncher; -import com.android.settings.dashboard.SummaryLoader; -import com.android.settings.display.BatteryPercentagePreferenceController; -import com.android.settings.fuelgauge.anomaly.Anomaly; -import com.android.settings.fuelgauge.anomaly.AnomalyDetectionPolicy; import com.android.settings.fuelgauge.batterytip.BatteryTipLoader; import com.android.settings.fuelgauge.batterytip.BatteryTipPreferenceController; import com.android.settings.fuelgauge.batterytip.tips.BatteryTip; import com.android.settings.overlay.FeatureFactory; import com.android.settings.search.BaseSearchIndexProvider; -import com.android.settingslib.core.AbstractPreferenceController; -import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.fuelgauge.EstimateKt; +import com.android.settingslib.search.SearchIndexable; import com.android.settingslib.utils.PowerUtil; import com.android.settingslib.utils.StringUtil; +import com.android.settingslib.widget.LayoutPreference; -import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -65,6 +64,7 @@ import java.util.List; * Displays a list of apps and subsystems that consume power, ordered by how much power was * consumed since the last time it was unplugged. */ +@SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC) public class PowerUsageSummary extends PowerUsageBase implements OnLongClickListener, BatteryTipPreferenceController.BatteryTipListener { @@ -72,7 +72,6 @@ public class PowerUsageSummary extends PowerUsageBase implements OnLongClickList private static final boolean DEBUG = false; private static final String KEY_BATTERY_HEADER = "battery_header"; - private static final String KEY_BATTERY_TIP = "battery_tip"; private static final String KEY_SCREEN_USAGE = "screen_usage"; private static final String KEY_TIME_SINCE_LAST_FULL_CHARGE = "last_full_charge"; @@ -101,11 +100,6 @@ public class PowerUsageSummary extends PowerUsageBase implements OnLongClickList @VisibleForTesting BatteryInfo mBatteryInfo; - /** - * SparseArray that maps uid to {@link Anomaly}, so we could find {@link Anomaly} by uid - */ - @VisibleForTesting - SparseArray<List<Anomaly>> mAnomalySparseArray; @VisibleForTesting BatteryHeaderPreferenceController mBatteryHeaderPreferenceController; @VisibleForTesting @@ -115,6 +109,14 @@ public class PowerUsageSummary extends PowerUsageBase implements OnLongClickList private int mStatsType = BatteryStats.STATS_SINCE_CHARGED; @VisibleForTesting + final ContentObserver mSettingsObserver = new ContentObserver(new Handler()) { + @Override + public void onChange(boolean selfChange, Uri uri) { + restartBatteryInfoLoader(); + } + }; + + @VisibleForTesting LoaderManager.LoaderCallbacks<BatteryInfo> mBatteryInfoLoaderCallbacks = new LoaderManager.LoaderCallbacks<BatteryInfo>() { @@ -156,9 +158,9 @@ public class PowerUsageSummary extends PowerUsageBase implements OnLongClickList protected void updateViews(List<BatteryInfo> batteryInfos) { final BatteryMeterView batteryView = mBatteryLayoutPref - .findViewById(R.id.battery_header_icon); + .findViewById(R.id.battery_header_icon); final TextView percentRemaining = - mBatteryLayoutPref.findViewById(R.id.battery_percent); + mBatteryLayoutPref.findViewById(R.id.battery_percent); final TextView summary1 = mBatteryLayoutPref.findViewById(R.id.summary1); final TextView summary2 = mBatteryLayoutPref.findViewById(R.id.summary2); BatteryInfo oldInfo = batteryInfos.get(0); @@ -169,13 +171,13 @@ public class PowerUsageSummary extends PowerUsageBase implements OnLongClickList // can sometimes say 0 time remaining because battery stats requires the phone // be unplugged for a period of time before being willing ot make an estimate. summary1.setText(mPowerFeatureProvider.getOldEstimateDebugString( - Formatter.formatShortElapsedTime(getContext(), - PowerUtil.convertUsToMs(oldInfo.remainingTimeUs)))); + Formatter.formatShortElapsedTime(getContext(), + PowerUtil.convertUsToMs(oldInfo.remainingTimeUs)))); // for this one we can just set the string directly summary2.setText(mPowerFeatureProvider.getEnhancedEstimateDebugString( - Formatter.formatShortElapsedTime(getContext(), - PowerUtil.convertUsToMs(newInfo.remainingTimeUs)))); + Formatter.formatShortElapsedTime(getContext(), + PowerUtil.convertUsToMs(newInfo.remainingTimeUs)))); batteryView.setBatteryLevel(oldInfo.batteryLevel); batteryView.setCharging(!oldInfo.discharging); @@ -202,6 +204,22 @@ public class PowerUsageSummary extends PowerUsageBase implements OnLongClickList }; @Override + public void onAttach(Context context) { + super.onAttach(context); + final SettingsActivity activity = (SettingsActivity) getActivity(); + + mBatteryHeaderPreferenceController = use(BatteryHeaderPreferenceController.class); + mBatteryHeaderPreferenceController.setActivity(activity); + mBatteryHeaderPreferenceController.setFragment(this); + mBatteryHeaderPreferenceController.setLifecycle(getSettingsLifecycle()); + + mBatteryTipPreferenceController = use(BatteryTipPreferenceController.class); + mBatteryTipPreferenceController.setActivity(activity); + mBatteryTipPreferenceController.setFragment(this); + mBatteryTipPreferenceController.setBatteryTipListener(this::onBatteryTipHandled); + } + + @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setAnimationAllowed(true); @@ -214,7 +232,6 @@ public class PowerUsageSummary extends PowerUsageBase implements OnLongClickList KEY_TIME_SINCE_LAST_FULL_CHARGE); mFooterPreferenceMixin.createFooterPreference().setTitle(R.string.battery_footer_summary); mBatteryUtils = BatteryUtils.getInstance(getContext()); - mAnomalySparseArray = new SparseArray<>(); restartBatteryInfoLoader(); mBatteryTipPreferenceController.restoreInstanceState(icicle); @@ -222,8 +239,23 @@ public class PowerUsageSummary extends PowerUsageBase implements OnLongClickList } @Override + public void onResume() { + super.onResume(); + getContentResolver().registerContentObserver( + Global.getUriFor(Global.BATTERY_ESTIMATES_LAST_UPDATE_TIME), + false, + mSettingsObserver); + } + + @Override + public void onPause() { + getContentResolver().unregisterContentObserver(mSettingsObserver); + super.onPause(); + } + + @Override public int getMetricsCategory() { - return MetricsEvent.FUELGAUGE_POWER_USAGE_SUMMARY_V2; + return SettingsEnums.FUELGAUGE_POWER_USAGE_SUMMARY_V2; } @Override @@ -237,22 +269,6 @@ public class PowerUsageSummary extends PowerUsageBase implements OnLongClickList } @Override - protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { - final Lifecycle lifecycle = getLifecycle(); - final SettingsActivity activity = (SettingsActivity) getActivity(); - final List<AbstractPreferenceController> controllers = new ArrayList<>(); - mBatteryHeaderPreferenceController = new BatteryHeaderPreferenceController( - context, activity, this /* host */, lifecycle); - controllers.add(mBatteryHeaderPreferenceController); - mBatteryTipPreferenceController = new BatteryTipPreferenceController(context, - KEY_BATTERY_TIP, (SettingsActivity) getActivity(), this /* fragment */, this /* - BatteryTipListener */); - controllers.add(mBatteryTipPreferenceController); - controllers.add(new BatteryPercentagePreferenceController(context)); - return controllers; - } - - @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { if (DEBUG) { menu.add(Menu.NONE, MENU_STATS_TYPE, Menu.NONE, R.string.menu_stats_total) @@ -285,7 +301,7 @@ public class PowerUsageSummary extends PowerUsageBase implements OnLongClickList new SubSettingLauncher(getContext()) .setDestination(PowerUsageAdvanced.class.getName()) .setSourceMetricsCategory(getMetricsCategory()) - .setTitle(R.string.advanced_battery_title) + .setTitleRes(R.string.advanced_battery_title) .launch(); return true; default: @@ -325,14 +341,9 @@ public class PowerUsageSummary extends PowerUsageBase implements OnLongClickList } @VisibleForTesting - AnomalyDetectionPolicy getAnomalyDetectionPolicy() { - return new AnomalyDetectionPolicy(getContext()); - } - - @VisibleForTesting void updateLastFullChargePreference() { if (mBatteryInfo != null && mBatteryInfo.averageTimeToDischarge - != Estimate.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN) { + != EstimateKt.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN) { mLastFullChargePref.setTitle(R.string.battery_full_charge_last); mLastFullChargePref.setSubtitle( StringUtil.formatElapsedTime(getContext(), mBatteryInfo.averageTimeToDischarge, @@ -366,18 +377,10 @@ public class PowerUsageSummary extends PowerUsageBase implements OnLongClickList } @VisibleForTesting - void updateAnomalySparseArray(List<Anomaly> anomalies) { - mAnomalySparseArray.clear(); - for (final Anomaly anomaly : anomalies) { - if (mAnomalySparseArray.get(anomaly.uid) == null) { - mAnomalySparseArray.append(anomaly.uid, new ArrayList<>()); - } - mAnomalySparseArray.get(anomaly.uid).add(anomaly); - } - } - - @VisibleForTesting void restartBatteryInfoLoader() { + if (getContext() == null) { + return; + } getLoaderManager().restartLoader(BATTERY_INFO_LOADER, Bundle.EMPTY, mBatteryInfoLoaderCallbacks); if (mPowerFeatureProvider.isEstimateDebugEnabled()) { @@ -416,48 +419,6 @@ public class PowerUsageSummary extends PowerUsageBase implements OnLongClickList restartBatteryTipLoader(); } - private static class SummaryProvider implements SummaryLoader.SummaryProvider { - private final Context mContext; - private final SummaryLoader mLoader; - private final BatteryBroadcastReceiver mBatteryBroadcastReceiver; - - private SummaryProvider(Context context, SummaryLoader loader) { - mContext = context; - mLoader = loader; - mBatteryBroadcastReceiver = new BatteryBroadcastReceiver(mContext); - mBatteryBroadcastReceiver.setBatteryChangedListener(type -> { - BatteryInfo.getBatteryInfo(mContext, new BatteryInfo.Callback() { - @Override - public void onBatteryInfoLoaded(BatteryInfo info) { - mLoader.setSummary(SummaryProvider.this, getDashboardLabel(mContext, info)); - } - }, true /* shortString */); - }); - } - - @Override - public void setListening(boolean listening) { - if (listening) { - mBatteryBroadcastReceiver.register(); - } else { - mBatteryBroadcastReceiver.unRegister(); - } - } - } - - @VisibleForTesting - static CharSequence getDashboardLabel(Context context, BatteryInfo info) { - CharSequence label; - final BidiFormatter formatter = BidiFormatter.getInstance(); - if (info.remainingLabel == null) { - label = info.batteryPercentString; - } else { - label = context.getString(R.string.power_remaining_settings_home_page, - formatter.unicodeWrap(info.batteryPercentString), - formatter.unicodeWrap(info.remainingLabel)); - } - return label; - } public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = new BaseSearchIndexProvider() { @@ -468,27 +429,5 @@ public class PowerUsageSummary extends PowerUsageBase implements OnLongClickList sir.xmlResId = R.xml.power_usage_summary; return Collections.singletonList(sir); } - - @Override - public List<String> getNonIndexableKeys(Context context) { - List<String> niks = super.getNonIndexableKeys(context); - - final BatteryPercentagePreferenceController controller = - new BatteryPercentagePreferenceController(context); - if (!controller.isAvailable()) { - niks.add(controller.getPreferenceKey()); - } - niks.add(KEY_BATTERY_SAVER_SUMMARY); - return niks; - } }; - - public static final SummaryLoader.SummaryProviderFactory SUMMARY_PROVIDER_FACTORY - = new SummaryLoader.SummaryProviderFactory() { - @Override - public SummaryLoader.SummaryProvider createSummaryProvider(Activity activity, - SummaryLoader summaryLoader) { - return new SummaryProvider(activity, summaryLoader); - } - }; } diff --git a/src/com/android/settings/fuelgauge/RestrictAppPreferenceController.java b/src/com/android/settings/fuelgauge/RestrictAppPreferenceController.java index f282154b66..d3c1d54e1a 100644 --- a/src/com/android/settings/fuelgauge/RestrictAppPreferenceController.java +++ b/src/com/android/settings/fuelgauge/RestrictAppPreferenceController.java @@ -19,13 +19,12 @@ package com.android.settings.fuelgauge; import android.app.AppOpsManager; import android.content.Context; -import android.os.UserHandle; import android.os.UserManager; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import com.android.settings.R; -import com.android.settings.SettingsActivity; import com.android.settings.core.BasePreferenceController; import com.android.settings.core.InstrumentedPreferenceFragment; import com.android.settings.fuelgauge.batterytip.AppInfo; diff --git a/src/com/android/settings/fuelgauge/RestrictedAppDetails.java b/src/com/android/settings/fuelgauge/RestrictedAppDetails.java index b89824bf08..6722b4a35a 100644 --- a/src/com/android/settings/fuelgauge/RestrictedAppDetails.java +++ b/src/com/android/settings/fuelgauge/RestrictedAppDetails.java @@ -16,24 +16,29 @@ package com.android.settings.fuelgauge; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.os.Bundle; import android.os.UserHandle; +import android.util.IconDrawableFactory; +import android.util.Log; +import android.util.SparseLongArray; + +import androidx.annotation.VisibleForTesting; import androidx.preference.CheckBoxPreference; import androidx.preference.Preference; import androidx.preference.PreferenceGroup; -import android.util.IconDrawableFactory; -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.Utils; import com.android.settings.core.InstrumentedPreferenceFragment; import com.android.settings.core.SubSettingLauncher; import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper; import com.android.settings.fuelgauge.batterytip.AppInfo; +import com.android.settings.fuelgauge.batterytip.BatteryDatabaseManager; import com.android.settings.fuelgauge.batterytip.BatteryTipDialogFragment; import com.android.settings.fuelgauge.batterytip.BatteryTipPreferenceController; import com.android.settings.fuelgauge.batterytip.tips.BatteryTip; @@ -41,7 +46,8 @@ import com.android.settings.fuelgauge.batterytip.tips.RestrictAppTip; import com.android.settings.fuelgauge.batterytip.tips.UnrestrictAppTip; import com.android.settings.widget.AppCheckBoxPreference; import com.android.settingslib.core.AbstractPreferenceController; -import com.android.settingslib.widget.FooterPreferenceMixin; +import com.android.settingslib.utils.StringUtil; +import com.android.settingslib.widget.FooterPreferenceMixinCompat; import java.util.List; @@ -56,6 +62,7 @@ public class RestrictedAppDetails extends DashboardFragment implements @VisibleForTesting static final String EXTRA_APP_INFO_LIST = "app_info_list"; private static final String KEY_PREF_RESTRICTED_APP_LIST = "restrict_app_list"; + private static final long TIME_NULL = -1; @VisibleForTesting List<AppInfo> mAppInfos; @@ -67,8 +74,10 @@ public class RestrictedAppDetails extends DashboardFragment implements BatteryUtils mBatteryUtils; @VisibleForTesting PackageManager mPackageManager; - private final FooterPreferenceMixin mFooterPreferenceMixin = - new FooterPreferenceMixin(this, getLifecycle()); + @VisibleForTesting + BatteryDatabaseManager mBatteryDatabaseManager; + private final FooterPreferenceMixinCompat mFooterPreferenceMixin = + new FooterPreferenceMixinCompat(this, getSettingsLifecycle()); public static void startRestrictedAppDetails(InstrumentedPreferenceFragment fragment, List<AppInfo> appInfos) { @@ -78,7 +87,7 @@ public class RestrictedAppDetails extends DashboardFragment implements new SubSettingLauncher(fragment.getContext()) .setDestination(RestrictedAppDetails.class.getName()) .setArguments(args) - .setTitle(R.string.restricted_app_title) + .setTitleRes(R.string.restricted_app_title) .setSourceMetricsCategory(fragment.getMetricsCategory()) .launch(); } @@ -95,6 +104,7 @@ public class RestrictedAppDetails extends DashboardFragment implements mPackageManager = context.getPackageManager(); mIconDrawableFactory = IconDrawableFactory.newInstance(context); mBatteryUtils = BatteryUtils.getInstance(context); + mBatteryDatabaseManager = BatteryDatabaseManager.getInstance(context); refreshUi(); } @@ -122,7 +132,7 @@ public class RestrictedAppDetails extends DashboardFragment implements @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.FUELGAUGE_RESTRICTED_APP_DETAILS; + return SettingsEnums.FUELGAUGE_RESTRICTED_APP_DETAILS; } @Override @@ -134,6 +144,9 @@ public class RestrictedAppDetails extends DashboardFragment implements void refreshUi() { mRestrictedAppListGroup.removeAll(); final Context context = getPrefContext(); + final SparseLongArray timestampArray = mBatteryDatabaseManager + .queryActionTime(AnomalyDatabaseHelper.ActionType.RESTRICTION); + final long now = System.currentTimeMillis(); for (int i = 0, size = mAppInfos.size(); i < size; i++) { final CheckBoxPreference checkBoxPreference = new AppCheckBoxPreference(context); @@ -157,9 +170,16 @@ public class RestrictedAppDetails extends DashboardFragment implements return false; }); + + final long timestamp = timestampArray.get(appInfo.uid, TIME_NULL); + if (timestamp != TIME_NULL) { + checkBoxPreference.setSummary(getString(R.string.restricted_app_time_summary, + StringUtil.formatRelativeTime(context, now - timestamp, false))); + } + final CharSequence test = checkBoxPreference.getSummaryOn(); mRestrictedAppListGroup.addPreference(checkBoxPreference); } catch (PackageManager.NameNotFoundException e) { - e.printStackTrace(); + Log.e(TAG, "Can't find package: " + appInfo.packageName); } } } diff --git a/src/com/android/settings/fuelgauge/SmartBatteryPreferenceController.java b/src/com/android/settings/fuelgauge/SmartBatteryPreferenceController.java index a3afd459ae..af49f3d954 100644 --- a/src/com/android/settings/fuelgauge/SmartBatteryPreferenceController.java +++ b/src/com/android/settings/fuelgauge/SmartBatteryPreferenceController.java @@ -19,10 +19,11 @@ package com.android.settings.fuelgauge; import android.content.Context; import android.provider.Settings; -import androidx.preference.SwitchPreference; -import androidx.preference.Preference; import android.text.TextUtils; +import androidx.preference.Preference; +import androidx.preference.SwitchPreference; + import com.android.settings.core.BasePreferenceController; import com.android.settings.overlay.FeatureFactory; diff --git a/src/com/android/settings/fuelgauge/SmartBatterySettings.java b/src/com/android/settings/fuelgauge/SmartBatterySettings.java index d954e83320..5fddbac665 100644 --- a/src/com/android/settings/fuelgauge/SmartBatterySettings.java +++ b/src/com/android/settings/fuelgauge/SmartBatterySettings.java @@ -16,17 +16,18 @@ package com.android.settings.fuelgauge; +import android.app.settings.SettingsEnums; import android.content.Context; import android.os.Bundle; import android.provider.SearchIndexableResource; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.core.InstrumentedPreferenceFragment; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.Arrays; @@ -35,6 +36,7 @@ import java.util.List; /** * Fragment to show smart battery and restricted app controls */ +@SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC) public class SmartBatterySettings extends DashboardFragment { public static final String TAG = "SmartBatterySettings"; @@ -46,7 +48,7 @@ public class SmartBatterySettings extends DashboardFragment { @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.FUELGAUGE_SMART_BATTERY; + return SettingsEnums.FUELGAUGE_SMART_BATTERY; } @Override diff --git a/src/com/android/settings/fuelgauge/TopLevelBatteryPreferenceController.java b/src/com/android/settings/fuelgauge/TopLevelBatteryPreferenceController.java new file mode 100644 index 0000000000..c4da6701f0 --- /dev/null +++ b/src/com/android/settings/fuelgauge/TopLevelBatteryPreferenceController.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2018 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.fuelgauge; + +import android.content.Context; +import android.text.BidiFormatter; + +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.core.lifecycle.events.OnStop; + +public class TopLevelBatteryPreferenceController extends BasePreferenceController implements + LifecycleObserver, OnStart, OnStop { + + private final BatteryBroadcastReceiver mBatteryBroadcastReceiver; + private Preference mPreference; + private BatteryInfo mBatteryInfo; + + public TopLevelBatteryPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + mBatteryBroadcastReceiver = new BatteryBroadcastReceiver(mContext); + mBatteryBroadcastReceiver.setBatteryChangedListener(type -> { + BatteryInfo.getBatteryInfo(mContext, info -> { + mBatteryInfo = info; + updateState(mPreference); + }, true /* shortString */); + }); + } + + @Override + public int getAvailabilityStatus() { + return mContext.getResources().getBoolean(R.bool.config_show_top_level_battery) + ? AVAILABLE_UNSEARCHABLE + : UNSUPPORTED_ON_DEVICE; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + } + + @Override + public void onStart() { + mBatteryBroadcastReceiver.register(); + } + + @Override + public void onStop() { + mBatteryBroadcastReceiver.unRegister(); + } + + @Override + public CharSequence getSummary() { + return getDashboardLabel(mContext, mBatteryInfo); + } + + static CharSequence getDashboardLabel(Context context, BatteryInfo info) { + if (info == null || context == null) { + return null; + } + CharSequence label; + final BidiFormatter formatter = BidiFormatter.getInstance(); + if (!info.discharging && info.chargeLabel != null) { + label = info.chargeLabel; + } else if (info.remainingLabel == null) { + label = info.batteryPercentString; + } else { + label = context.getString(R.string.power_remaining_settings_home_page, + formatter.unicodeWrap(info.batteryPercentString), + formatter.unicodeWrap(info.remainingLabel)); + } + return label; + } +} diff --git a/src/com/android/settings/fuelgauge/anomaly/Anomaly.java b/src/com/android/settings/fuelgauge/anomaly/Anomaly.java deleted file mode 100644 index edbc14f5fb..0000000000 --- a/src/com/android/settings/fuelgauge/anomaly/Anomaly.java +++ /dev/null @@ -1,246 +0,0 @@ -/* - * Copyright (C) 2017 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.fuelgauge.anomaly; - -import android.os.Parcel; -import android.os.Parcelable; -import androidx.annotation.IntDef; -import android.text.TextUtils; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.Objects; - -/** - * Data that represents an app has been detected as anomaly. It contains - * - * 1. Basic information of the app(i.e. uid, package name) - * 2. Type of anomaly - * 3. Data that has been detected as anomaly(i.e wakelock time) - */ -public class Anomaly implements Parcelable { - @Retention(RetentionPolicy.SOURCE) - @IntDef({AnomalyType.WAKE_LOCK, - AnomalyType.WAKEUP_ALARM, - AnomalyType.BLUETOOTH_SCAN}) - public @interface AnomalyType { - int WAKE_LOCK = 0; - int WAKEUP_ALARM = 1; - int BLUETOOTH_SCAN = 2; - } - - @Retention(RetentionPolicy.SOURCE) - @IntDef({AnomalyActionType.FORCE_STOP, - AnomalyActionType.BACKGROUND_CHECK, - AnomalyActionType.LOCATION_CHECK, - AnomalyActionType.STOP_AND_BACKGROUND_CHECK}) - public @interface AnomalyActionType { - int FORCE_STOP = 0; - int BACKGROUND_CHECK = 1; - int LOCATION_CHECK = 2; - int STOP_AND_BACKGROUND_CHECK = 3; - } - - @AnomalyType - public static final int[] ANOMALY_TYPE_LIST = { - AnomalyType.WAKE_LOCK, - AnomalyType.WAKEUP_ALARM, - AnomalyType.BLUETOOTH_SCAN}; - - /** - * Type of this this anomaly - */ - public final int type; - public final int uid; - public final int targetSdkVersion; - public final long wakelockTimeMs; - public final long bluetoothScanningTimeMs; - public final int wakeupAlarmCount; - /** - * {@code true} if background restriction is enabled - * - * @see android.app.AppOpsManager.OP_RUN_IN_BACKGROUND - */ - public final boolean backgroundRestrictionEnabled; - /** - * Display name of this anomaly, usually it is the app name - */ - public final CharSequence displayName; - public final String packageName; - - private Anomaly(Builder builder) { - type = builder.mType; - uid = builder.mUid; - displayName = builder.mDisplayName; - packageName = builder.mPackageName; - wakelockTimeMs = builder.mWakeLockTimeMs; - targetSdkVersion = builder.mTargetSdkVersion; - backgroundRestrictionEnabled = builder.mBgRestrictionEnabled; - bluetoothScanningTimeMs = builder.mBluetoothScanningTimeMs; - wakeupAlarmCount = builder.mWakeupAlarmCount; - } - - private Anomaly(Parcel in) { - type = in.readInt(); - uid = in.readInt(); - displayName = in.readCharSequence(); - packageName = in.readString(); - wakelockTimeMs = in.readLong(); - targetSdkVersion = in.readInt(); - backgroundRestrictionEnabled = in.readBoolean(); - wakeupAlarmCount = in.readInt(); - bluetoothScanningTimeMs = in.readLong(); - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(type); - dest.writeInt(uid); - dest.writeCharSequence(displayName); - dest.writeString(packageName); - dest.writeLong(wakelockTimeMs); - dest.writeInt(targetSdkVersion); - dest.writeBoolean(backgroundRestrictionEnabled); - dest.writeInt(wakeupAlarmCount); - dest.writeLong(bluetoothScanningTimeMs); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (!(obj instanceof Anomaly)) { - return false; - } - - Anomaly other = (Anomaly) obj; - return type == other.type - && uid == other.uid - && wakelockTimeMs == other.wakelockTimeMs - && TextUtils.equals(displayName, other.displayName) - && TextUtils.equals(packageName, other.packageName) - && targetSdkVersion == other.targetSdkVersion - && backgroundRestrictionEnabled == other.backgroundRestrictionEnabled - && wakeupAlarmCount == other.wakeupAlarmCount - && bluetoothScanningTimeMs == other.bluetoothScanningTimeMs; - } - - @Override - public int hashCode() { - return Objects.hash(type, uid, displayName, packageName, wakelockTimeMs, targetSdkVersion, - backgroundRestrictionEnabled, wakeupAlarmCount, bluetoothScanningTimeMs); - } - - @Override - public String toString() { - return "type=" + toAnomalyTypeText(type) + " uid=" + uid + " package=" + packageName + - " displayName=" + displayName + " wakelockTimeMs=" + wakelockTimeMs + - " wakeupAlarmCount=" + wakeupAlarmCount + " bluetoothTimeMs=" - + bluetoothScanningTimeMs; - } - - private String toAnomalyTypeText(@AnomalyType int type) { - switch (type) { - case AnomalyType.WAKEUP_ALARM: - return "wakeupAlarm"; - case AnomalyType.WAKE_LOCK: - return "wakelock"; - case AnomalyType.BLUETOOTH_SCAN: - return "unoptimizedBluetoothScan"; - } - - return ""; - } - - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { - public Anomaly createFromParcel(Parcel in) { - return new Anomaly(in); - } - - public Anomaly[] newArray(int size) { - return new Anomaly[size]; - } - }; - - public static final class Builder { - @AnomalyType - private int mType; - private int mUid; - private int mTargetSdkVersion; - private CharSequence mDisplayName; - private String mPackageName; - private long mWakeLockTimeMs; - private boolean mBgRestrictionEnabled; - private int mWakeupAlarmCount; - private long mBluetoothScanningTimeMs; - - public Builder setType(@AnomalyType int type) { - mType = type; - return this; - } - - public Builder setUid(int uid) { - mUid = uid; - return this; - } - - public Builder setDisplayName(CharSequence displayName) { - mDisplayName = displayName; - return this; - } - - public Builder setPackageName(String packageName) { - mPackageName = packageName; - return this; - } - - public Builder setWakeLockTimeMs(long wakeLockTimeMs) { - mWakeLockTimeMs = wakeLockTimeMs; - return this; - } - - public Builder setTargetSdkVersion(int targetSdkVersion) { - mTargetSdkVersion = targetSdkVersion; - return this; - } - - public Builder setBackgroundRestrictionEnabled(boolean bgRestrictionEnabled) { - mBgRestrictionEnabled = bgRestrictionEnabled; - return this; - } - - public Builder setWakeupAlarmCount(int wakeupAlarmCount) { - mWakeupAlarmCount = wakeupAlarmCount; - return this; - } - - public Builder setBluetoothScanningTimeMs(long bluetoothScanningTimeMs) { - mBluetoothScanningTimeMs = bluetoothScanningTimeMs; - return this; - } - - public Anomaly build() { - return new Anomaly(this); - } - } -} diff --git a/src/com/android/settings/fuelgauge/anomaly/AnomalyDetectionPolicy.java b/src/com/android/settings/fuelgauge/anomaly/AnomalyDetectionPolicy.java deleted file mode 100644 index 55d972157b..0000000000 --- a/src/com/android/settings/fuelgauge/anomaly/AnomalyDetectionPolicy.java +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright (C) 2017 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.fuelgauge.anomaly; - -import android.content.Context; -import android.net.Uri; -import android.provider.Settings; -import androidx.annotation.VisibleForTesting; -import android.text.format.DateUtils; -import android.util.KeyValueListParser; -import android.util.Log; - -import java.util.Arrays; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * Class to store the policy for anomaly detection, which comes from - * {@link android.provider.Settings.Global} - */ -public class AnomalyDetectionPolicy { - public static final String TAG = "AnomalyDetectionPolicy"; - - @VisibleForTesting - static final String KEY_ANOMALY_DETECTION_ENABLED = "anomaly_detection_enabled"; - @VisibleForTesting - static final String KEY_WAKELOCK_DETECTION_ENABLED = "wakelock_enabled"; - @VisibleForTesting - static final String KEY_WAKEUP_ALARM_DETECTION_ENABLED = "wakeup_alarm_enabled"; - @VisibleForTesting - static final String KEY_BLUETOOTH_SCAN_DETECTION_ENABLED = "bluetooth_scan_enabled"; - @VisibleForTesting - static final String KEY_WAKELOCK_THRESHOLD = "wakelock_threshold"; - @VisibleForTesting - static final String KEY_WAKEUP_ALARM_THRESHOLD = "wakeup_alarm_threshold"; - @VisibleForTesting - static final String KEY_WAKEUP_BLACKLISTED_TAGS = "wakeup_blacklisted_tags"; - @VisibleForTesting - static final String KEY_BLUETOOTH_SCAN_THRESHOLD = "bluetooth_scan_threshold"; - - /** - * {@code true} if general anomaly detection is enabled - * - * @see Settings.Global#ANOMALY_DETECTION_CONSTANTS - * @see #KEY_ANOMALY_DETECTION_ENABLED - */ - final boolean anomalyDetectionEnabled; - - /** - * {@code true} if wakelock anomaly detection is enabled - * - * @see Settings.Global#ANOMALY_DETECTION_CONSTANTS - * @see #KEY_WAKELOCK_DETECTION_ENABLED - */ - final boolean wakeLockDetectionEnabled; - - /** - * {@code true} if wakeup alarm detection is enabled - * - * @see Settings.Global#ANOMALY_DETECTION_CONSTANTS - * @see #KEY_WAKEUP_ALARM_DETECTION_ENABLED - */ - final boolean wakeupAlarmDetectionEnabled; - - /** - * {@code true} if bluetooth scanning detection is enabled - * - * @see Settings.Global#ANOMALY_DETECTION_CONSTANTS - * @see #KEY_BLUETOOTH_SCAN_THRESHOLD - */ - final boolean bluetoothScanDetectionEnabled; - - /** - * Threshold for wakelock time in milli seconds - * - * @see Settings.Global#ANOMALY_DETECTION_CONSTANTS - * @see #KEY_WAKELOCK_THRESHOLD - */ - public final long wakeLockThreshold; - - /** - * Threshold for wakeup alarm count per hour - * - * @see Settings.Global#ANOMALY_DETECTION_CONSTANTS - * @see #KEY_WAKEUP_ALARM_THRESHOLD - */ - public final long wakeupAlarmThreshold; - - /** - * Array of blacklisted wakeups, by tag. - * - * @see Settings.Global#ANOMALY_DETECTION_CONSTANTS - * @see #KEY_WAKEUP_BLACKLISTED_TAGS - */ - public final Set<String> wakeupBlacklistedTags; - - /** - * Threshold for bluetooth unoptimized scanning time in milli seconds - * - * @see Settings.Global#ANOMALY_DETECTION_CONSTANTS - * @see #KEY_BLUETOOTH_SCAN_THRESHOLD - */ - public final long bluetoothScanThreshold; - - private final KeyValueListParser mParser; - - public AnomalyDetectionPolicy(Context context) { - mParser = new KeyValueListParser(','); - final String value = Settings.Global.getString(context.getContentResolver(), - Settings.Global.ANOMALY_DETECTION_CONSTANTS); - - try { - mParser.setString(value); - } catch (IllegalArgumentException e) { - Log.e(TAG, "Bad anomaly detection constants"); - } - - anomalyDetectionEnabled = - mParser.getBoolean(KEY_ANOMALY_DETECTION_ENABLED, false); - wakeLockDetectionEnabled = - mParser.getBoolean(KEY_WAKELOCK_DETECTION_ENABLED,false); - wakeupAlarmDetectionEnabled = - mParser.getBoolean(KEY_WAKEUP_ALARM_DETECTION_ENABLED,false); - bluetoothScanDetectionEnabled = mParser.getBoolean( - KEY_BLUETOOTH_SCAN_DETECTION_ENABLED, false); - wakeLockThreshold = mParser.getLong(KEY_WAKELOCK_THRESHOLD, - DateUtils.HOUR_IN_MILLIS); - wakeupAlarmThreshold = mParser.getLong(KEY_WAKEUP_ALARM_THRESHOLD, 10); - wakeupBlacklistedTags = parseStringSet(KEY_WAKEUP_BLACKLISTED_TAGS, null); - bluetoothScanThreshold = mParser.getLong(KEY_BLUETOOTH_SCAN_THRESHOLD, - 30 * DateUtils.MINUTE_IN_MILLIS); - } - - public boolean isAnomalyDetectionEnabled() { - return anomalyDetectionEnabled; - } - - public boolean isAnomalyDetectorEnabled(@Anomaly.AnomalyType int type) { - switch (type) { - case Anomaly.AnomalyType.WAKE_LOCK: - return wakeLockDetectionEnabled; - case Anomaly.AnomalyType.WAKEUP_ALARM: - return wakeupAlarmDetectionEnabled; - case Anomaly.AnomalyType.BLUETOOTH_SCAN: - return bluetoothScanDetectionEnabled; - default: - return false; // Disabled when no this type - } - } - - private Set<String> parseStringSet(final String key, final Set<String> defaultSet) { - final String value = mParser.getString(key, null); - if (value != null) { - return Arrays.stream(value.split(":")) - .map(String::trim).map(Uri::decode).collect(Collectors.toSet()); - } else { - return defaultSet; - } - } -} diff --git a/src/com/android/settings/fuelgauge/anomaly/AnomalyDialogFragment.java b/src/com/android/settings/fuelgauge/anomaly/AnomalyDialogFragment.java deleted file mode 100644 index 918f98af50..0000000000 --- a/src/com/android/settings/fuelgauge/anomaly/AnomalyDialogFragment.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright (C) 2017 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.fuelgauge.anomaly; - -import android.app.AlertDialog; -import android.app.Dialog; -import android.content.Context; -import android.content.DialogInterface; -import android.os.Bundle; -import androidx.annotation.VisibleForTesting; - -import com.android.internal.logging.nano.MetricsProto; -import com.android.settings.R; -import com.android.settings.core.instrumentation.InstrumentedDialogFragment; -import com.android.settings.fuelgauge.anomaly.action.AnomalyAction; - -/** - * Dialog Fragment to show action dialog for each anomaly - */ -public class AnomalyDialogFragment extends InstrumentedDialogFragment implements - DialogInterface.OnClickListener { - - private static final String ARG_ANOMALY = "anomaly"; - private static final String ARG_METRICS_KEY = "metrics_key"; - - @VisibleForTesting - Anomaly mAnomaly; - @VisibleForTesting - AnomalyUtils mAnomalyUtils; - - /** - * Listener to give the control back to target fragment - */ - public interface AnomalyDialogListener { - /** - * This method is invoked once anomaly is handled, then target fragment could do - * extra work. One example is that fragment could remove the anomaly preference - * since it has been handled - * - * @param anomaly that has been handled - */ - void onAnomalyHandled(Anomaly anomaly); - } - - public static AnomalyDialogFragment newInstance(Anomaly anomaly, int metricsKey) { - AnomalyDialogFragment dialogFragment = new AnomalyDialogFragment(); - - Bundle args = new Bundle(2); - args.putParcelable(ARG_ANOMALY, anomaly); - args.putInt(ARG_METRICS_KEY, metricsKey); - dialogFragment.setArguments(args); - - return dialogFragment; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - initAnomalyUtils(); - } - - @VisibleForTesting - void initAnomalyUtils() { - mAnomalyUtils = AnomalyUtils.getInstance(getContext()); - } - - @Override - public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DIALOG_HANDLE_ANOMALY; - } - - @Override - public void onClick(DialogInterface dialog, int which) { - final AnomalyDialogListener lsn = (AnomalyDialogListener) getTargetFragment(); - if (lsn == null) { - return; - } - - final AnomalyAction anomalyAction = mAnomalyUtils.getAnomalyAction(mAnomaly); - final int metricsKey = getArguments().getInt(ARG_METRICS_KEY); - - anomalyAction.handlePositiveAction(mAnomaly, metricsKey); - lsn.onAnomalyHandled(mAnomaly); - } - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - final Bundle bundle = getArguments(); - final Context context = getContext(); - final AnomalyUtils anomalyUtils = AnomalyUtils.getInstance(context); - - mAnomaly = bundle.getParcelable(ARG_ANOMALY); - anomalyUtils.logAnomaly(mMetricsFeatureProvider, mAnomaly, - MetricsProto.MetricsEvent.DIALOG_HANDLE_ANOMALY); - - final AnomalyAction anomalyAction = mAnomalyUtils.getAnomalyAction(mAnomaly); - switch (anomalyAction.getActionType()) { - case Anomaly.AnomalyActionType.FORCE_STOP: - return new AlertDialog.Builder(context) - .setTitle(R.string.dialog_stop_title) - .setMessage(getString(mAnomaly.type == Anomaly.AnomalyType.WAKE_LOCK - ? R.string.dialog_stop_message - : R.string.dialog_stop_message_wakeup_alarm, mAnomaly.displayName)) - .setPositiveButton(R.string.dialog_stop_ok, this) - .setNegativeButton(R.string.dlg_cancel, null) - .create(); - case Anomaly.AnomalyActionType.STOP_AND_BACKGROUND_CHECK: - return new AlertDialog.Builder(context) - .setTitle(R.string.dialog_background_check_title) - .setMessage(getString(R.string.dialog_background_check_message, - mAnomaly.displayName)) - .setPositiveButton(R.string.dialog_background_check_ok, this) - .setNegativeButton(R.string.dlg_cancel, null) - .create(); - case Anomaly.AnomalyActionType.LOCATION_CHECK: - return new AlertDialog.Builder(context) - .setTitle(R.string.dialog_location_title) - .setMessage(getString(R.string.dialog_location_message, - mAnomaly.displayName)) - .setPositiveButton(R.string.dialog_location_ok, this) - .setNegativeButton(R.string.dlg_cancel, null) - .create(); - default: - throw new IllegalArgumentException("unknown type " + mAnomaly.type); - } - } - -} diff --git a/src/com/android/settings/fuelgauge/anomaly/AnomalyLoader.java b/src/com/android/settings/fuelgauge/anomaly/AnomalyLoader.java deleted file mode 100644 index fbd7dedf05..0000000000 --- a/src/com/android/settings/fuelgauge/anomaly/AnomalyLoader.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright (C) 2017 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.fuelgauge.anomaly; - -import android.content.Context; -import android.content.pm.PackageManager; -import android.os.BatteryStats; -import android.os.Bundle; -import android.os.UserManager; -import androidx.annotation.VisibleForTesting; -import android.util.Log; - -import com.android.internal.os.BatteryStatsHelper; -import com.android.internal.util.ArrayUtils; -import com.android.settingslib.utils.AsyncLoader; - -import java.io.FileDescriptor; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.List; - -/** - * Loader to compute which apps are anomaly and return a anomaly list. It will return - * an empty list if there is no anomaly. - */ -public class AnomalyLoader extends AsyncLoader<List<Anomaly>> { - private static final String TAG = "AnomalyLoader"; - - private static final boolean USE_FAKE_DATA = false; - private BatteryStatsHelper mBatteryStatsHelper; - private String mPackageName; - private UserManager mUserManager; - @VisibleForTesting - AnomalyUtils mAnomalyUtils; - @VisibleForTesting - AnomalyDetectionPolicy mPolicy; - - /** - * Create {@link AnomalyLoader} that runs anomaly check for all apps. - */ - public AnomalyLoader(Context context, BatteryStatsHelper batteryStatsHelper) { - this(context, batteryStatsHelper, null, new AnomalyDetectionPolicy(context)); - - } - - /** - * Create {@link AnomalyLoader} with {@code packageName}, so this loader will only - * detect anomalies related to {@code packageName}, or check all apps if {@code packageName} - * is {@code null}. - * - * This constructor will create {@link BatteryStatsHelper} in background thread. - * - * @param packageName if set, only finds anomalies for this package. If {@code null}, - * detects all anomalies of this type. - */ - public AnomalyLoader(Context context, String packageName) { - this(context, null, packageName, new AnomalyDetectionPolicy(context)); - } - - @VisibleForTesting - AnomalyLoader(Context context, BatteryStatsHelper batteryStatsHelper, - String packageName, AnomalyDetectionPolicy policy) { - super(context); - mBatteryStatsHelper = batteryStatsHelper; - mPackageName = packageName; - mAnomalyUtils = AnomalyUtils.getInstance(context); - mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE); - mPolicy = policy; - } - - @Override - protected void onDiscardResult(List<Anomaly> result) { - } - - @Override - public List<Anomaly> loadInBackground() { - if (USE_FAKE_DATA) { - return generateFakeData(); - } - if (mBatteryStatsHelper == null) { - mBatteryStatsHelper = new BatteryStatsHelper(getContext()); - mBatteryStatsHelper.create((Bundle) null); - mBatteryStatsHelper.refreshStats(BatteryStats.STATS_SINCE_CHARGED, - mUserManager.getUserProfiles()); - } - - return mAnomalyUtils.detectAnomalies(mBatteryStatsHelper, mPolicy, mPackageName); - } - - @VisibleForTesting - List<Anomaly> generateFakeData() { - final List<Anomaly> anomalies = new ArrayList<>(); - final Context context = getContext(); - final String packageName = "com.android.settings"; - final CharSequence displayName = "Settings"; - try { - final int uid = context.getPackageManager().getPackageUid(packageName, 0); - - anomalies.add(new Anomaly.Builder() - .setUid(uid) - .setType(Anomaly.AnomalyType.WAKE_LOCK) - .setPackageName(packageName) - .setDisplayName(displayName) - .build()); - anomalies.add(new Anomaly.Builder() - .setUid(uid) - .setType(Anomaly.AnomalyType.WAKEUP_ALARM) - .setPackageName(packageName) - .setDisplayName(displayName) - .build()); - anomalies.add(new Anomaly.Builder() - .setUid(uid) - .setType(Anomaly.AnomalyType.BLUETOOTH_SCAN) - .setPackageName(packageName) - .setDisplayName(displayName) - .build()); - } catch (PackageManager.NameNotFoundException e) { - Log.e(TAG, "Cannot find package by name: " + packageName, e); - } - return anomalies; - } - -} diff --git a/src/com/android/settings/fuelgauge/anomaly/AnomalyPreference.java b/src/com/android/settings/fuelgauge/anomaly/AnomalyPreference.java deleted file mode 100644 index a54b02ec3d..0000000000 --- a/src/com/android/settings/fuelgauge/anomaly/AnomalyPreference.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.android.settings.fuelgauge.anomaly; - -import android.content.Context; -import androidx.preference.Preference; - -import com.android.settings.R; - -/** - * Preference that stores {@link Anomaly} - */ -public class AnomalyPreference extends Preference { - private Anomaly mAnomaly; - - public AnomalyPreference(Context context, Anomaly anomaly) { - super(context); - mAnomaly = anomaly; - - if (anomaly != null) { - setTitle(anomaly.displayName); - } - } - - public Anomaly getAnomaly() { - return mAnomaly; - } - -} diff --git a/src/com/android/settings/fuelgauge/anomaly/AnomalySummaryPreferenceController.java b/src/com/android/settings/fuelgauge/anomaly/AnomalySummaryPreferenceController.java deleted file mode 100644 index dd90d89949..0000000000 --- a/src/com/android/settings/fuelgauge/anomaly/AnomalySummaryPreferenceController.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (C) 2017 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.fuelgauge.anomaly; - -import android.content.Context; -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; - -import com.android.settings.R; -import com.android.settings.SettingsActivity; -import com.android.settings.core.InstrumentedPreferenceFragment; -import com.android.settings.fuelgauge.BatteryUtils; -import com.android.settings.fuelgauge.PowerUsageAnomalyDetails; - -import java.util.List; - -/** - * Manager that responsible for updating high usage preference and handling preference click. - */ -public class AnomalySummaryPreferenceController { - private static final String TAG = "HighUsagePreferenceController"; - - public static final String ANOMALY_KEY = "high_usage"; - - private static final int REQUEST_ANOMALY_ACTION = 0; - private InstrumentedPreferenceFragment mFragment; - @VisibleForTesting - Preference mAnomalyPreference; - @VisibleForTesting - List<Anomaly> mAnomalies; - @VisibleForTesting - BatteryUtils mBatteryUtils; - private SettingsActivity mSettingsActivity; - - /** - * Metrics key about fragment that create this controller - * - * @see com.android.internal.logging.nano.MetricsProto.MetricsEvent - */ - private int mMetricsKey; - - public AnomalySummaryPreferenceController(SettingsActivity activity, - InstrumentedPreferenceFragment fragment) { - mFragment = fragment; - mSettingsActivity = activity; - mAnomalyPreference = mFragment.getPreferenceScreen().findPreference(ANOMALY_KEY); - mMetricsKey = fragment.getMetricsCategory(); - mBatteryUtils = BatteryUtils.getInstance(activity.getApplicationContext()); - hideHighUsagePreference(); - } - - public boolean onPreferenceTreeClick(Preference preference) { - if (mAnomalies != null && ANOMALY_KEY.equals(preference.getKey())) { - if (mAnomalies.size() == 1) { - final Anomaly anomaly = mAnomalies.get(0); - AnomalyDialogFragment dialogFragment = AnomalyDialogFragment.newInstance(anomaly, - mMetricsKey); - dialogFragment.setTargetFragment(mFragment, REQUEST_ANOMALY_ACTION); - dialogFragment.show(mFragment.getFragmentManager(), TAG); - } else { - PowerUsageAnomalyDetails.startBatteryAbnormalPage(mSettingsActivity, mFragment, - mAnomalies); - } - return true; - } - return false; - } - - /** - * Update anomaly preference based on {@code anomalies}, also store a reference - * of {@paramref anomalies}, which would be used in {@link #onPreferenceTreeClick(Preference)} - * - * @param anomalies used to update the summary, this method will store a reference of it - */ - public void updateAnomalySummaryPreference(List<Anomaly> anomalies) { - final Context context = mFragment.getContext(); - mAnomalies = anomalies; - - if (!mAnomalies.isEmpty()) { - mAnomalyPreference.setVisible(true); - final int count = mAnomalies.size(); - final String title = context.getResources().getQuantityString( - R.plurals.power_high_usage_title, count, mAnomalies.get(0).displayName); - final String summary = count > 1 ? - context.getString(R.string.battery_abnormal_apps_summary, count) - : context.getString( - mBatteryUtils.getSummaryResIdFromAnomalyType(mAnomalies.get(0).type)); - - mAnomalyPreference.setTitle(title); - mAnomalyPreference.setSummary(summary); - } else { - mAnomalyPreference.setVisible(false); - } - } - - public void hideHighUsagePreference() { - mAnomalyPreference.setVisible(false); - } -} diff --git a/src/com/android/settings/fuelgauge/anomaly/AnomalyUtils.java b/src/com/android/settings/fuelgauge/anomaly/AnomalyUtils.java deleted file mode 100644 index 2926547237..0000000000 --- a/src/com/android/settings/fuelgauge/anomaly/AnomalyUtils.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright (C) 2017 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.fuelgauge.anomaly; - -import android.content.Context; -import android.os.Build; -import androidx.annotation.VisibleForTesting; -import android.util.Pair; -import android.util.SparseIntArray; - -import com.android.internal.logging.nano.MetricsProto; -import com.android.internal.os.BatteryStatsHelper; -import com.android.settings.fuelgauge.anomaly.action.AnomalyAction; -import com.android.settings.fuelgauge.anomaly.action.ForceStopAction; -import com.android.settings.fuelgauge.anomaly.action.LocationCheckAction; -import com.android.settings.fuelgauge.anomaly.action.StopAndBackgroundCheckAction; -import com.android.settings.fuelgauge.anomaly.checker.AnomalyDetector; -import com.android.settings.fuelgauge.anomaly.checker.BluetoothScanAnomalyDetector; -import com.android.settings.fuelgauge.anomaly.checker.WakeLockAnomalyDetector; -import com.android.settings.fuelgauge.anomaly.checker.WakeupAlarmAnomalyDetector; -import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; - -import java.util.ArrayList; -import java.util.List; - -/** - * Utility class for anomaly detection - */ -public class AnomalyUtils { - private Context mContext; - private static AnomalyUtils sInstance; - - private static final SparseIntArray mMetricArray; - static { - mMetricArray = new SparseIntArray(); - mMetricArray.append(Anomaly.AnomalyType.WAKE_LOCK, - MetricsProto.MetricsEvent.ANOMALY_TYPE_WAKELOCK); - mMetricArray.append(Anomaly.AnomalyType.WAKEUP_ALARM, - MetricsProto.MetricsEvent.ANOMALY_TYPE_WAKEUP_ALARM); - mMetricArray.append(Anomaly.AnomalyType.BLUETOOTH_SCAN, - MetricsProto.MetricsEvent.ANOMALY_TYPE_UNOPTIMIZED_BT); - } - - @VisibleForTesting - AnomalyUtils(Context context) { - mContext = context.getApplicationContext(); - } - - public static AnomalyUtils getInstance(Context context) { - if (sInstance == null) { - sInstance = new AnomalyUtils(context); - } - return sInstance; - } - - /** - * Return the corresponding {@link AnomalyAction} according to - * {@link com.android.settings.fuelgauge.anomaly.Anomaly} - * - * @return corresponding {@link AnomalyAction}, or null if cannot find it. - */ - public AnomalyAction getAnomalyAction(Anomaly anomaly) { - switch (anomaly.type) { - case Anomaly.AnomalyType.WAKE_LOCK: - return new ForceStopAction(mContext); - case Anomaly.AnomalyType.WAKEUP_ALARM: - if (anomaly.targetSdkVersion >= Build.VERSION_CODES.O - || (anomaly.targetSdkVersion < Build.VERSION_CODES.O - && anomaly.backgroundRestrictionEnabled)) { - return new ForceStopAction(mContext); - } else { - return new StopAndBackgroundCheckAction(mContext); - } - case Anomaly.AnomalyType.BLUETOOTH_SCAN: - return new LocationCheckAction(mContext); - default: - return null; - } - } - - /** - * Return the corresponding {@link AnomalyDetector} according to - * {@link com.android.settings.fuelgauge.anomaly.Anomaly.AnomalyType} - * - * @return corresponding {@link AnomalyDetector}, or null if cannot find it. - */ - public AnomalyDetector getAnomalyDetector(@Anomaly.AnomalyType int anomalyType) { - switch (anomalyType) { - case Anomaly.AnomalyType.WAKE_LOCK: - return new WakeLockAnomalyDetector(mContext); - case Anomaly.AnomalyType.WAKEUP_ALARM: - return new WakeupAlarmAnomalyDetector(mContext); - case Anomaly.AnomalyType.BLUETOOTH_SCAN: - return new BluetoothScanAnomalyDetector(mContext); - default: - return null; - } - } - - /** - * Detect whether application with {@code targetPackageName} has anomaly. When - * {@code targetPackageName} is null, start detection among all the applications. - * - * @param batteryStatsHelper contains battery stats, used to detect anomaly - * @param policy contains configuration about anomaly check - * @param targetPackageName represents the app need to be detected - * @return the list of anomalies - */ - public List<Anomaly> detectAnomalies(BatteryStatsHelper batteryStatsHelper, - AnomalyDetectionPolicy policy, String targetPackageName) { - final List<Anomaly> anomalies = new ArrayList<>(); - for (@Anomaly.AnomalyType int type : Anomaly.ANOMALY_TYPE_LIST) { - if (policy.isAnomalyDetectorEnabled(type)) { - anomalies.addAll(getAnomalyDetector(type).detectAnomalies( - batteryStatsHelper, targetPackageName)); - } - } - - return anomalies; - } - - /** - * Log the list of {@link Anomaly} using {@link MetricsFeatureProvider}, which contains - * anomaly type, package name, field_context, field_action_type - * - * @param provider provider to do the logging - * @param anomalies contains the data to log - * @param contextId which page invoke this logging - * @see #logAnomaly(MetricsFeatureProvider, Anomaly, int) - */ - public void logAnomalies(MetricsFeatureProvider provider, List<Anomaly> anomalies, - int contextId) { - for (int i = 0, size = anomalies.size(); i < size; i++) { - logAnomaly(provider, anomalies.get(i), contextId); - } - } - - /** - * Log the {@link Anomaly} using {@link MetricsFeatureProvider}, which contains - * anomaly type, package name, field_context, field_action_type - * - * @param provider provider to do the logging - * @param anomaly contains the data to log - * @param contextId which page invoke this logging - * @see #logAnomalies(MetricsFeatureProvider, List, int) - */ - public void logAnomaly(MetricsFeatureProvider provider, Anomaly anomaly, int contextId) { - provider.action( - mContext, - mMetricArray.get(anomaly.type, MetricsProto.MetricsEvent.VIEW_UNKNOWN), - anomaly.packageName, - Pair.create(MetricsProto.MetricsEvent.FIELD_CONTEXT, contextId), - Pair.create(MetricsProto.MetricsEvent.FIELD_ANOMALY_ACTION_TYPE, - getAnomalyAction(anomaly).getActionType())); - } - -} diff --git a/src/com/android/settings/fuelgauge/anomaly/action/AnomalyAction.java b/src/com/android/settings/fuelgauge/anomaly/action/AnomalyAction.java deleted file mode 100644 index d7de5a7ac2..0000000000 --- a/src/com/android/settings/fuelgauge/anomaly/action/AnomalyAction.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (C) 2017 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.fuelgauge.anomaly.action; - -import android.content.Context; -import android.util.Pair; - -import com.android.internal.logging.nano.MetricsProto; -import com.android.settings.fuelgauge.anomaly.Anomaly; -import com.android.settings.overlay.FeatureFactory; -import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; - -/** - * Abstract class for anomaly action, which is triggered if we need to handle the anomaly - */ -public abstract class AnomalyAction { - protected Context mContext; - protected int mActionMetricKey; - - private MetricsFeatureProvider mMetricsFeatureProvider; - - public AnomalyAction(Context context) { - mContext = context; - mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider(); - } - - /** - * handle the action when user clicks positive button - * - * @param anomaly about the app that we need to handle - * @param contextMetricsKey key for the page that invokes the action - * @see com.android.internal.logging.nano.MetricsProto - */ - public void handlePositiveAction(Anomaly anomaly, int contextMetricsKey) { - mMetricsFeatureProvider.action(mContext, mActionMetricKey, anomaly.packageName, - Pair.create(MetricsProto.MetricsEvent.FIELD_CONTEXT, contextMetricsKey)); - } - - /** - * Check whether the action is active for {@code anomaly} - * - * @param anomaly about the app that we need to handle - * @return {@code true} if action is active, otherwise return {@code false} - */ - public abstract boolean isActionActive(Anomaly anomaly); - - @Anomaly.AnomalyActionType - public abstract int getActionType(); -} diff --git a/src/com/android/settings/fuelgauge/anomaly/action/BackgroundCheckAction.java b/src/com/android/settings/fuelgauge/anomaly/action/BackgroundCheckAction.java deleted file mode 100644 index e3666d8f14..0000000000 --- a/src/com/android/settings/fuelgauge/anomaly/action/BackgroundCheckAction.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (C) 2017 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.fuelgauge.anomaly.action; - -import android.app.AppOpsManager; -import android.content.Context; -import android.os.Build; -import androidx.annotation.VisibleForTesting; - -import com.android.internal.logging.nano.MetricsProto; -import com.android.settings.fuelgauge.BatteryUtils; -import com.android.settings.fuelgauge.anomaly.Anomaly; - -/** - * Background check action for anomaly app, which means to stop app running in the background - */ -public class BackgroundCheckAction extends AnomalyAction { - - private AppOpsManager mAppOpsManager; - @VisibleForTesting - BatteryUtils mBatteryUtils; - - public BackgroundCheckAction(Context context) { - super(context); - mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); - mActionMetricKey = MetricsProto.MetricsEvent.ACTION_APP_BACKGROUND_CHECK; - mBatteryUtils = BatteryUtils.getInstance(context); - } - - @Override - public void handlePositiveAction(Anomaly anomaly, int contextMetricsKey) { - super.handlePositiveAction(anomaly, contextMetricsKey); - if (anomaly.targetSdkVersion < Build.VERSION_CODES.O) { - mAppOpsManager.setMode(AppOpsManager.OP_RUN_IN_BACKGROUND, anomaly.uid, - anomaly.packageName, - AppOpsManager.MODE_IGNORED); - } - } - - @Override - public boolean isActionActive(Anomaly anomaly) { - return !mBatteryUtils.isBackgroundRestrictionEnabled(anomaly.targetSdkVersion, anomaly.uid, - anomaly.packageName); - } - - @Override - public int getActionType() { - return Anomaly.AnomalyActionType.BACKGROUND_CHECK; - } -} diff --git a/src/com/android/settings/fuelgauge/anomaly/action/ForceStopAction.java b/src/com/android/settings/fuelgauge/anomaly/action/ForceStopAction.java deleted file mode 100644 index fb7306a175..0000000000 --- a/src/com/android/settings/fuelgauge/anomaly/action/ForceStopAction.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (C) 2017 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.fuelgauge.anomaly.action; - -import android.app.ActivityManager; -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.util.Log; - -import com.android.internal.logging.nano.MetricsProto; -import com.android.settings.fuelgauge.anomaly.Anomaly; - -/** - * Force stop action for anomaly app, which means to stop the app which causes anomaly - */ -public class ForceStopAction extends AnomalyAction { - private static final String TAG = "ForceStopAction"; - - private ActivityManager mActivityManager; - private PackageManager mPackageManager; - - public ForceStopAction(Context context) { - super(context); - mActivityManager = (ActivityManager) context.getSystemService( - Context.ACTIVITY_SERVICE); - mPackageManager = context.getPackageManager(); - mActionMetricKey = MetricsProto.MetricsEvent.ACTION_APP_FORCE_STOP; - } - - @Override - public void handlePositiveAction(Anomaly anomaly, int contextMetricsKey) { - super.handlePositiveAction(anomaly, contextMetricsKey); - - mActivityManager.forceStopPackage(anomaly.packageName); - } - - @Override - public boolean isActionActive(Anomaly anomaly) { - try { - ApplicationInfo info = mPackageManager.getApplicationInfo(anomaly.packageName, - PackageManager.GET_META_DATA); - return (info.flags & ApplicationInfo.FLAG_STOPPED) == 0; - } catch (PackageManager.NameNotFoundException e) { - Log.e(TAG, "Cannot find info for app: " + anomaly.packageName); - } - return false; - } - - @Override - public int getActionType() { - return Anomaly.AnomalyActionType.FORCE_STOP; - } -} diff --git a/src/com/android/settings/fuelgauge/anomaly/action/LocationCheckAction.java b/src/com/android/settings/fuelgauge/anomaly/action/LocationCheckAction.java deleted file mode 100644 index dcf7e46fda..0000000000 --- a/src/com/android/settings/fuelgauge/anomaly/action/LocationCheckAction.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (C) 2017 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.fuelgauge.anomaly.action; - -import android.Manifest; -import android.content.Context; -import android.content.pm.permission.RuntimePermissionPresenter; -import androidx.annotation.VisibleForTesting; -import androidx.core.content.PermissionChecker; - -import com.android.internal.logging.nano.MetricsProto; -import com.android.settings.fuelgauge.anomaly.Anomaly; - -/** - * Location action for anomaly app, which means to turn off location permission for this app - */ -public class LocationCheckAction extends AnomalyAction { - - private static final String TAG = "LocationCheckAction"; - - private final RuntimePermissionPresenter mRuntimePermissionPresenter; - - public LocationCheckAction(Context context) { - this(context, RuntimePermissionPresenter.getInstance(context)); - } - - @VisibleForTesting - LocationCheckAction(Context context, RuntimePermissionPresenter runtimePermissionPresenter) { - super(context); - mRuntimePermissionPresenter = runtimePermissionPresenter; - mActionMetricKey = MetricsProto.MetricsEvent.ACTION_APP_LOCATION_CHECK; - } - - @Override - public void handlePositiveAction(Anomaly anomaly, int contextMetricsKey) { - super.handlePositiveAction(anomaly, contextMetricsKey); - mRuntimePermissionPresenter.revokeRuntimePermission(anomaly.packageName, - Manifest.permission.ACCESS_COARSE_LOCATION); - mRuntimePermissionPresenter.revokeRuntimePermission(anomaly.packageName, - Manifest.permission.ACCESS_FINE_LOCATION); - } - - @Override - public boolean isActionActive(Anomaly anomaly) { - return isPermissionGranted(anomaly, Manifest.permission.ACCESS_COARSE_LOCATION) - || isPermissionGranted(anomaly, Manifest.permission.ACCESS_FINE_LOCATION); - } - - @Override - public int getActionType() { - return Anomaly.AnomalyActionType.LOCATION_CHECK; - } - - private boolean isPermissionGranted(Anomaly anomaly, String permission) { - return PermissionChecker.checkPermission(mContext, permission, -1, anomaly.uid, - anomaly.packageName) == PermissionChecker.PERMISSION_GRANTED; - } -} diff --git a/src/com/android/settings/fuelgauge/anomaly/action/StopAndBackgroundCheckAction.java b/src/com/android/settings/fuelgauge/anomaly/action/StopAndBackgroundCheckAction.java deleted file mode 100644 index b5fd20b5ca..0000000000 --- a/src/com/android/settings/fuelgauge/anomaly/action/StopAndBackgroundCheckAction.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2017 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.fuelgauge.anomaly.action; - -import android.content.Context; -import androidx.annotation.VisibleForTesting; - -import com.android.internal.logging.nano.MetricsProto; -import com.android.settings.fuelgauge.anomaly.Anomaly; - -/** - * Force stop and background check action for anomaly app, this action will - * 1. Force stop the app - * 2. Turn on background check - */ -public class StopAndBackgroundCheckAction extends AnomalyAction { - @VisibleForTesting - ForceStopAction mForceStopAction; - @VisibleForTesting - BackgroundCheckAction mBackgroundCheckAction; - - public StopAndBackgroundCheckAction(Context context) { - this(context, new ForceStopAction(context), new BackgroundCheckAction(context)); - mActionMetricKey = MetricsProto.MetricsEvent.ACTION_APP_STOP_AND_BACKGROUND_CHECK; - } - - @VisibleForTesting - StopAndBackgroundCheckAction(Context context, ForceStopAction forceStopAction, - BackgroundCheckAction backgroundCheckAction) { - super(context); - mForceStopAction = forceStopAction; - mBackgroundCheckAction = backgroundCheckAction; - } - - @Override - public void handlePositiveAction(Anomaly anomaly, int metricsKey) { - super.handlePositiveAction(anomaly, metricsKey); - mForceStopAction.handlePositiveAction(anomaly, metricsKey); - mBackgroundCheckAction.handlePositiveAction(anomaly, metricsKey); - } - - @Override - public boolean isActionActive(Anomaly anomaly) { - return mForceStopAction.isActionActive(anomaly) - && mBackgroundCheckAction.isActionActive(anomaly); - } - - @Override - public int getActionType() { - return Anomaly.AnomalyActionType.STOP_AND_BACKGROUND_CHECK; - } -} diff --git a/src/com/android/settings/fuelgauge/anomaly/checker/AnomalyDetector.java b/src/com/android/settings/fuelgauge/anomaly/checker/AnomalyDetector.java deleted file mode 100644 index 1921bef4e1..0000000000 --- a/src/com/android/settings/fuelgauge/anomaly/checker/AnomalyDetector.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2017 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.fuelgauge.anomaly.checker; - -import android.annotation.Nullable; - -import com.android.internal.os.BatteryStatsHelper; -import com.android.settings.fuelgauge.anomaly.Anomaly; - -import java.util.List; - -public interface AnomalyDetector { - /** - * Detect whether there is anomaly among all the applications in the device - * - * @param batteryStatsHelper used to detect the anomaly - * @return anomaly list - */ - List<Anomaly> detectAnomalies(BatteryStatsHelper batteryStatsHelper); - - /** - * Detect whether application with {@code targetPackageName} has anomaly. When - * {@code targetPackageName} is null, start detection among all the applications. - * - * @param batteryStatsHelper used to detect the anomaly - * @param targetPackageName represents the app need to be detected - * @return anomaly list - */ - List<Anomaly> detectAnomalies(BatteryStatsHelper batteryStatsHelper, - @Nullable String targetPackageName); -} diff --git a/src/com/android/settings/fuelgauge/anomaly/checker/BluetoothScanAnomalyDetector.java b/src/com/android/settings/fuelgauge/anomaly/checker/BluetoothScanAnomalyDetector.java deleted file mode 100644 index 5cb188a220..0000000000 --- a/src/com/android/settings/fuelgauge/anomaly/checker/BluetoothScanAnomalyDetector.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright (C) 2017 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.fuelgauge.anomaly.checker; - -import android.content.Context; -import android.os.BatteryStats; -import android.os.SystemClock; -import androidx.annotation.VisibleForTesting; -import android.text.format.DateUtils; -import android.util.ArrayMap; - -import com.android.internal.os.BatterySipper; -import com.android.internal.os.BatteryStatsHelper; -import com.android.settings.Utils; -import com.android.settings.fuelgauge.BatteryUtils; -import com.android.settings.fuelgauge.anomaly.Anomaly; -import com.android.settings.fuelgauge.anomaly.AnomalyDetectionPolicy; -import com.android.settings.fuelgauge.anomaly.AnomalyUtils; -import com.android.settings.fuelgauge.anomaly.action.AnomalyAction; - -import java.util.ArrayList; -import java.util.List; - -/** - * Check whether apps have unoptimized bluetooth scanning in the background - */ -public class BluetoothScanAnomalyDetector implements AnomalyDetector { - private static final String TAG = "BluetoothScanAnomalyDetector"; - @VisibleForTesting - BatteryUtils mBatteryUtils; - private long mBluetoothScanningThreshold; - private Context mContext; - private AnomalyUtils mAnomalyUtils; - - public BluetoothScanAnomalyDetector(Context context) { - this(context, new AnomalyDetectionPolicy(context), AnomalyUtils.getInstance(context)); - } - - @VisibleForTesting - BluetoothScanAnomalyDetector(Context context, AnomalyDetectionPolicy policy, - AnomalyUtils anomalyUtils) { - mContext = context; - mBatteryUtils = BatteryUtils.getInstance(context); - mBluetoothScanningThreshold = policy.bluetoothScanThreshold; - mAnomalyUtils = anomalyUtils; - } - - @Override - public List<Anomaly> detectAnomalies(BatteryStatsHelper batteryStatsHelper) { - // Detect all apps if targetPackageName is null - return detectAnomalies(batteryStatsHelper, null /* targetPackageName */); - } - - @Override - public List<Anomaly> detectAnomalies(BatteryStatsHelper batteryStatsHelper, - String targetPackageName) { - final List<BatterySipper> batterySippers = batteryStatsHelper.getUsageList(); - final List<Anomaly> anomalies = new ArrayList<>(); - final int targetUid = mBatteryUtils.getPackageUid(targetPackageName); - final long elapsedRealtimeMs = SystemClock.elapsedRealtime(); - - for (int i = 0, size = batterySippers.size(); i < size; i++) { - final BatterySipper sipper = batterySippers.get(i); - final BatteryStats.Uid uid = sipper.uidObj; - if (uid == null - || mBatteryUtils.shouldHideSipper(sipper) - || (targetUid != BatteryUtils.UID_NULL && targetUid != uid.getUid())) { - continue; - } - - final long bluetoothTimeMs = getBluetoothUnoptimizedBgTimeMs(uid, elapsedRealtimeMs); - if (bluetoothTimeMs > mBluetoothScanningThreshold) { - final String packageName = mBatteryUtils.getPackageName(uid.getUid()); - final CharSequence displayName = Utils.getApplicationLabel(mContext, - packageName); - - Anomaly anomaly = new Anomaly.Builder() - .setUid(uid.getUid()) - .setType(Anomaly.AnomalyType.BLUETOOTH_SCAN) - .setDisplayName(displayName) - .setPackageName(packageName) - .setBluetoothScanningTimeMs(bluetoothTimeMs) - .build(); - - if (mAnomalyUtils.getAnomalyAction(anomaly).isActionActive(anomaly)) { - anomalies.add(anomaly); - } - } - } - - return anomalies; - } - - @VisibleForTesting - public long getBluetoothUnoptimizedBgTimeMs(BatteryStats.Uid uid, long elapsedRealtimeMs) { - BatteryStats.Timer timer = uid.getBluetoothUnoptimizedScanBackgroundTimer(); - - return timer != null ? timer.getTotalDurationMsLocked(elapsedRealtimeMs) : 0; - } - -} diff --git a/src/com/android/settings/fuelgauge/anomaly/checker/WakeLockAnomalyDetector.java b/src/com/android/settings/fuelgauge/anomaly/checker/WakeLockAnomalyDetector.java deleted file mode 100644 index 73d092517a..0000000000 --- a/src/com/android/settings/fuelgauge/anomaly/checker/WakeLockAnomalyDetector.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright (C) 2017 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.fuelgauge.anomaly.checker; - -import android.content.Context; -import android.content.pm.PackageManager; -import android.os.BatteryStats; -import android.os.SystemClock; -import androidx.annotation.VisibleForTesting; - -import com.android.internal.os.BatterySipper; -import com.android.internal.os.BatteryStatsHelper; -import com.android.settings.Utils; -import com.android.settings.fuelgauge.BatteryUtils; -import com.android.settings.fuelgauge.anomaly.Anomaly; -import com.android.settings.fuelgauge.anomaly.AnomalyDetectionPolicy; -import com.android.settings.fuelgauge.anomaly.AnomalyUtils; - -import java.util.ArrayList; -import java.util.List; - -/** - * Check whether apps holding wakelock too long - */ -public class WakeLockAnomalyDetector implements AnomalyDetector { - private static final String TAG = "WakeLockAnomalyChecker"; - @VisibleForTesting - BatteryUtils mBatteryUtils; - @VisibleForTesting - long mWakeLockThresholdMs; - private PackageManager mPackageManager; - private Context mContext; - private AnomalyUtils mAnomalyUtils; - - public WakeLockAnomalyDetector(Context context) { - this(context, new AnomalyDetectionPolicy(context), AnomalyUtils.getInstance(context)); - } - - @VisibleForTesting - WakeLockAnomalyDetector(Context context, AnomalyDetectionPolicy policy, - AnomalyUtils anomalyUtils) { - mContext = context; - mPackageManager = context.getPackageManager(); - mBatteryUtils = BatteryUtils.getInstance(context); - mAnomalyUtils = anomalyUtils; - mWakeLockThresholdMs = policy.wakeLockThreshold; - } - - @Override - public List<Anomaly> detectAnomalies(BatteryStatsHelper batteryStatsHelper) { - // Detect all apps if targetPackageName is null - return detectAnomalies(batteryStatsHelper, null /* targetPackageName */); - } - - @Override - public List<Anomaly> detectAnomalies(BatteryStatsHelper batteryStatsHelper, - String targetPackageName) { - final List<BatterySipper> batterySippers = batteryStatsHelper.getUsageList(); - final List<Anomaly> anomalies = new ArrayList<>(); - final long rawRealtime = SystemClock.elapsedRealtime(); - final int targetUid = mBatteryUtils.getPackageUid(targetPackageName); - - // Check the app one by one - for (int i = 0, size = batterySippers.size(); i < size; i++) { - final BatterySipper sipper = batterySippers.get(i); - final BatteryStats.Uid uid = sipper.uidObj; - if (uid == null - || mBatteryUtils.shouldHideSipper(sipper) - || (targetUid != BatteryUtils.UID_NULL && targetUid != uid.getUid())) { - continue; - } - - final long currentDurationMs = getCurrentDurationMs(uid, rawRealtime); - final long backgroundDurationMs = getBackgroundTotalDurationMs(uid, rawRealtime); - - if (backgroundDurationMs > mWakeLockThresholdMs && currentDurationMs != 0) { - final String packageName = mBatteryUtils.getPackageName(uid.getUid()); - final CharSequence displayName = Utils.getApplicationLabel(mContext, - packageName); - - Anomaly anomaly = new Anomaly.Builder() - .setUid(uid.getUid()) - .setType(Anomaly.AnomalyType.WAKE_LOCK) - .setDisplayName(displayName) - .setPackageName(packageName) - .setWakeLockTimeMs(backgroundDurationMs) - .build(); - - if (mAnomalyUtils.getAnomalyAction(anomaly).isActionActive(anomaly)) { - anomalies.add(anomaly); - } - } - } - return anomalies; - } - - @VisibleForTesting - long getCurrentDurationMs(BatteryStats.Uid uid, long elapsedRealtimeMs) { - BatteryStats.Timer timer = uid.getAggregatedPartialWakelockTimer(); - - return timer != null ? timer.getCurrentDurationMsLocked(elapsedRealtimeMs) : 0; - } - - @VisibleForTesting - long getBackgroundTotalDurationMs(BatteryStats.Uid uid, long elapsedRealtimeMs) { - BatteryStats.Timer timer = uid.getAggregatedPartialWakelockTimer(); - BatteryStats.Timer subTimer = timer != null ? timer.getSubTimer() : null; - - return subTimer != null ? subTimer.getTotalDurationMsLocked(elapsedRealtimeMs) : 0; - } -} diff --git a/src/com/android/settings/fuelgauge/anomaly/checker/WakeupAlarmAnomalyDetector.java b/src/com/android/settings/fuelgauge/anomaly/checker/WakeupAlarmAnomalyDetector.java deleted file mode 100644 index f891b51486..0000000000 --- a/src/com/android/settings/fuelgauge/anomaly/checker/WakeupAlarmAnomalyDetector.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright (C) 2017 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.fuelgauge.anomaly.checker; - -import android.content.Context; -import android.os.BatteryStats; -import androidx.annotation.VisibleForTesting; -import android.text.format.DateUtils; -import android.util.ArrayMap; -import android.util.ArraySet; -import android.util.Log; - -import com.android.internal.os.BatterySipper; -import com.android.internal.os.BatteryStatsHelper; -import com.android.settings.Utils; -import com.android.settings.fuelgauge.BatteryUtils; -import com.android.settings.fuelgauge.anomaly.Anomaly; -import com.android.settings.fuelgauge.anomaly.AnomalyDetectionPolicy; -import com.android.settings.fuelgauge.anomaly.AnomalyUtils; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * Check whether apps has too many wakeup alarms - */ -public class WakeupAlarmAnomalyDetector implements AnomalyDetector { - private static final String TAG = "WakeupAlarmAnomalyDetector"; - @VisibleForTesting - BatteryUtils mBatteryUtils; - private long mWakeupAlarmThreshold; - private Set<String> mWakeupBlacklistedTags; - private Context mContext; - private AnomalyUtils mAnomalyUtils; - - public WakeupAlarmAnomalyDetector(Context context) { - this(context, new AnomalyDetectionPolicy(context), AnomalyUtils.getInstance(context)); - } - - @VisibleForTesting - WakeupAlarmAnomalyDetector(Context context, AnomalyDetectionPolicy policy, - AnomalyUtils anomalyUtils) { - mContext = context; - mBatteryUtils = BatteryUtils.getInstance(context); - mAnomalyUtils = anomalyUtils; - mWakeupAlarmThreshold = policy.wakeupAlarmThreshold; - mWakeupBlacklistedTags = policy.wakeupBlacklistedTags; - } - - @Override - public List<Anomaly> detectAnomalies(BatteryStatsHelper batteryStatsHelper) { - // Detect all apps if targetPackageName is null - return detectAnomalies(batteryStatsHelper, null /* targetPackageName */); - } - - @Override - public List<Anomaly> detectAnomalies(BatteryStatsHelper batteryStatsHelper, - String targetPackageName) { - final List<BatterySipper> batterySippers = batteryStatsHelper.getUsageList(); - final List<Anomaly> anomalies = new ArrayList<>(); - final double totalRunningHours = mBatteryUtils.calculateRunningTimeBasedOnStatsType( - batteryStatsHelper, BatteryStats.STATS_SINCE_CHARGED) - / (double) DateUtils.HOUR_IN_MILLIS; - final int targetUid = mBatteryUtils.getPackageUid(targetPackageName); - - if (totalRunningHours >= 1) { - for (int i = 0, size = batterySippers.size(); i < size; i++) { - final BatterySipper sipper = batterySippers.get(i); - final BatteryStats.Uid uid = sipper.uidObj; - if (uid == null - || mBatteryUtils.shouldHideSipper(sipper) - || (targetUid != BatteryUtils.UID_NULL && targetUid != uid.getUid())) { - continue; - } - - final int wakeupAlarmCount = (int) (getWakeupAlarmCountFromUid(uid) - / totalRunningHours); - if (wakeupAlarmCount > mWakeupAlarmThreshold) { - final String packageName = mBatteryUtils.getPackageName(uid.getUid()); - final CharSequence displayName = Utils.getApplicationLabel(mContext, - packageName); - final int targetSdkVersion = mBatteryUtils.getTargetSdkVersion(packageName); - - Anomaly anomaly = new Anomaly.Builder() - .setUid(uid.getUid()) - .setType(Anomaly.AnomalyType.WAKEUP_ALARM) - .setDisplayName(displayName) - .setPackageName(packageName) - .setTargetSdkVersion(targetSdkVersion) - .setBackgroundRestrictionEnabled( - mBatteryUtils.isBackgroundRestrictionEnabled(targetSdkVersion, - uid.getUid(), packageName)) - .setWakeupAlarmCount(wakeupAlarmCount) - .build(); - - if (mAnomalyUtils.getAnomalyAction(anomaly).isActionActive(anomaly)) { - anomalies.add(anomaly); - } - } - } - } - - return anomalies; - } - - @VisibleForTesting - int getWakeupAlarmCountFromUid(BatteryStats.Uid uid) { - int wakeups = 0; - final ArrayMap<String, ? extends BatteryStats.Uid.Pkg> packageStats - = uid.getPackageStats(); - for (int ipkg = packageStats.size() - 1; ipkg >= 0; ipkg--) { - final BatteryStats.Uid.Pkg ps = packageStats.valueAt(ipkg); - final ArrayMap<String, ? extends BatteryStats.Counter> alarms = - ps.getWakeupAlarmStats(); - for (Map.Entry<String, ? extends BatteryStats.Counter> alarm : alarms.entrySet()) { - if (mWakeupBlacklistedTags != null - && mWakeupBlacklistedTags.contains(alarm.getKey())) { - continue; - } - int count = alarm.getValue().getCountLocked(BatteryStats.STATS_SINCE_CHARGED); - wakeups += count; - } - } - - return wakeups; - } - -} diff --git a/src/com/android/settings/fuelgauge/batterysaver/AutoBatterySaverPreferenceController.java b/src/com/android/settings/fuelgauge/batterysaver/AutoBatterySaverPreferenceController.java deleted file mode 100644 index 06c05c075d..0000000000 --- a/src/com/android/settings/fuelgauge/batterysaver/AutoBatterySaverPreferenceController.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (C) 2018 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.fuelgauge.batterysaver; - -import android.content.Context; -import android.provider.Settings; -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; - -import com.android.settings.core.TogglePreferenceController; -import com.android.settingslib.fuelgauge.BatterySaverUtils; - -/** - * Controller that update whether to turn on battery saver automatically - */ -public class AutoBatterySaverPreferenceController extends TogglePreferenceController implements - Preference.OnPreferenceChangeListener { - - /** - * Default value for {@link Settings.Global#LOW_POWER_MODE_TRIGGER_LEVEL}. - */ - static final int DEFAULT_TRIGGER_LEVEL = 0; - - /** - * The default value to set to {@link Settings.Global#LOW_POWER_MODE_TRIGGER_LEVEL} when the - * user enables battery saver. - */ - private final int mDefaultTriggerLevelForOn; - - @VisibleForTesting - static final String KEY_AUTO_BATTERY_SAVER = "auto_battery_saver"; - - public AutoBatterySaverPreferenceController(Context context) { - super(context, KEY_AUTO_BATTERY_SAVER); - mDefaultTriggerLevelForOn = mContext.getResources().getInteger( - com.android.internal.R.integer.config_lowBatteryWarningLevel); - } - - @Override - public int getAvailabilityStatus() { - return AVAILABLE; - } - - @Override - public boolean isChecked() { - return Settings.Global.getInt(mContext.getContentResolver(), - Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL, DEFAULT_TRIGGER_LEVEL) != 0; - } - - @Override - public boolean setChecked(boolean isChecked) { - BatterySaverUtils.setAutoBatterySaverTriggerLevel(mContext, - isChecked ? mDefaultTriggerLevelForOn : 0); - return true; - } -} diff --git a/src/com/android/settings/fuelgauge/batterysaver/AutoBatterySeekBarPreferenceController.java b/src/com/android/settings/fuelgauge/batterysaver/AutoBatterySeekBarPreferenceController.java deleted file mode 100644 index b082eeb52f..0000000000 --- a/src/com/android/settings/fuelgauge/batterysaver/AutoBatterySeekBarPreferenceController.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright (C) 2018 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.fuelgauge.batterysaver; - -import android.content.ContentResolver; -import android.content.Context; -import android.database.ContentObserver; -import android.net.Uri; -import android.os.Handler; -import android.os.Looper; -import android.provider.Settings; -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; -import android.util.Log; -import android.view.accessibility.AccessibilityNodeInfo; - -import com.android.settings.R; -import com.android.settings.Utils; -import com.android.settings.core.BasePreferenceController; -import com.android.settings.widget.SeekBarPreference; -import com.android.settingslib.core.lifecycle.Lifecycle; -import com.android.settingslib.core.lifecycle.LifecycleObserver; -import com.android.settingslib.core.lifecycle.events.OnStart; -import com.android.settingslib.core.lifecycle.events.OnStop; - -/** - * Controller that update the battery saver seekbar - */ -public class AutoBatterySeekBarPreferenceController extends BasePreferenceController implements - LifecycleObserver, OnStart, OnStop, SeekBarPreference.OnPreferenceChangeListener { - private static final String TAG = "AutoBatterySeekBarPreferenceController"; - @VisibleForTesting - static final String KEY_AUTO_BATTERY_SEEK_BAR = "battery_saver_seek_bar"; - private SeekBarPreference mPreference; - private AutoBatterySaverSettingObserver mContentObserver; - - public AutoBatterySeekBarPreferenceController(Context context, Lifecycle lifecycle) { - super(context, KEY_AUTO_BATTERY_SEEK_BAR); - mContentObserver = new AutoBatterySaverSettingObserver(new Handler(Looper.getMainLooper())); - if (lifecycle != null) { - lifecycle.addObserver(this); - } - } - - @Override - public void displayPreference(PreferenceScreen screen) { - super.displayPreference(screen); - mPreference = (SeekBarPreference) screen.findPreference( - KEY_AUTO_BATTERY_SEEK_BAR); - mPreference.setContinuousUpdates(true); - mPreference.setAccessibilityRangeInfoType( - AccessibilityNodeInfo.RangeInfo.RANGE_TYPE_PERCENT); - updatePreference(mPreference); - } - - @Override - public int getAvailabilityStatus() { - return AVAILABLE; - } - - @Override - public void updateState(Preference preference) { - super.updateState(preference); - updatePreference(preference); - } - - @Override - public void onStart() { - mContentObserver.registerContentObserver(); - } - - @Override - public void onStop() { - mContentObserver.unRegisterContentObserver(); - } - - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - final int progress = (int) newValue; - Settings.Global.putInt(mContext.getContentResolver(), - Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL, progress); - return true; - } - - @VisibleForTesting - void updatePreference(Preference preference) { - final ContentResolver contentResolver = mContext.getContentResolver(); - - // Override the max value with LOW_POWER_MODE_TRIGGER_LEVEL_MAX, if set. - final int maxLevel = Settings.Global.getInt(contentResolver, - Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL_MAX, 0); - if (maxLevel > 0) { - if (!(preference instanceof SeekBarPreference)) { - Log.e(TAG, "Unexpected preference class: " + preference.getClass()); - } else { - final SeekBarPreference seekBarPreference = (SeekBarPreference) preference; - if (maxLevel < seekBarPreference.getMin()) { - Log.e(TAG, "LOW_POWER_MODE_TRIGGER_LEVEL_MAX too low; ignored."); - } else { - seekBarPreference.setMax(maxLevel); - } - } - } - - // Set the current value. - final int level = Settings.Global.getInt(contentResolver, - Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL, - AutoBatterySaverPreferenceController.DEFAULT_TRIGGER_LEVEL); - if (level == 0) { - preference.setVisible(false); - } else { - preference.setVisible(true); - preference.setTitle(mContext.getString(R.string.battery_saver_seekbar_title, - Utils.formatPercentage(level))); - SeekBarPreference seekBarPreference = (SeekBarPreference) preference; - seekBarPreference.setProgress(level); - seekBarPreference.setSeekBarContentDescription( - mContext.getString(R.string.battery_saver_turn_on_automatically_title)); - } - } - - /** - * Observer that listens to change from {@link Settings.Global#LOW_POWER_MODE_TRIGGER_LEVEL} - */ - private final class AutoBatterySaverSettingObserver extends ContentObserver { - private final Uri mUri = Settings.Global.getUriFor( - Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL); - private final ContentResolver mContentResolver; - - public AutoBatterySaverSettingObserver(Handler handler) { - super(handler); - mContentResolver = mContext.getContentResolver(); - } - - public void registerContentObserver() { - mContentResolver.registerContentObserver(mUri, false, this); - } - - public void unRegisterContentObserver() { - mContentResolver.unregisterContentObserver(this); - } - - @Override - public void onChange(boolean selfChange, Uri uri, int userId) { - if (mUri.equals(uri)) { - updatePreference(mPreference); - } - } - } -} diff --git a/src/com/android/settings/fuelgauge/batterysaver/BatterySaverButtonPreferenceController.java b/src/com/android/settings/fuelgauge/batterysaver/BatterySaverButtonPreferenceController.java index 0618b44bdf..bb12d1ad47 100644 --- a/src/com/android/settings/fuelgauge/batterysaver/BatterySaverButtonPreferenceController.java +++ b/src/com/android/settings/fuelgauge/batterysaver/BatterySaverButtonPreferenceController.java @@ -18,6 +18,7 @@ package com.android.settings.fuelgauge.batterysaver; import android.content.Context; import android.os.PowerManager; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; @@ -71,7 +72,7 @@ public class BatterySaverButtonPreferenceController extends @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - mPreference = (TwoStateButtonPreference) screen.findPreference(getPreferenceKey()); + mPreference = screen.findPreference(getPreferenceKey()); } @Override diff --git a/src/com/android/settings/fuelgauge/batterysaver/BatterySaverSchedulePreferenceController.java b/src/com/android/settings/fuelgauge/batterysaver/BatterySaverSchedulePreferenceController.java new file mode 100644 index 0000000000..0b129ef401 --- /dev/null +++ b/src/com/android/settings/fuelgauge/batterysaver/BatterySaverSchedulePreferenceController.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2018 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.fuelgauge.batterysaver; + +import android.content.ContentResolver; +import android.content.Context; +import android.os.PowerManager; +import android.provider.Settings; +import android.provider.Settings.Global; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.Utils; +import com.android.settings.core.BasePreferenceController; +import com.android.settingslib.fuelgauge.BatterySaverUtils; + +/** + * Simple controller to navigate users to the scheduling page from + * "Settings > Battery > Battery Saver". Also updates the summary for preference based on + * the currently selected settings. + */ +public class BatterySaverSchedulePreferenceController extends BasePreferenceController { + + @VisibleForTesting + Preference mBatterySaverSchedulePreference; + public static final String KEY_BATTERY_SAVER_SCHEDULE = "battery_saver_schedule"; + + + public BatterySaverSchedulePreferenceController(Context context) { + super(context, KEY_BATTERY_SAVER_SCHEDULE); + BatterySaverUtils.revertScheduleToNoneIfNeeded(context); + } + + @Override + public String getPreferenceKey() { + return KEY_BATTERY_SAVER_SCHEDULE; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mBatterySaverSchedulePreference = screen.findPreference(KEY_BATTERY_SAVER_SCHEDULE); + } + + @Override + public CharSequence getSummary() { + final ContentResolver resolver = mContext.getContentResolver(); + final int mode = Settings.Global.getInt(resolver, Global.AUTOMATIC_POWER_SAVE_MODE, + PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE); + if (mode == PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE) { + final int threshold = + Settings.Global.getInt(resolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, 0); + if (threshold <= 0) { + return mContext.getText(R.string.battery_saver_auto_no_schedule); + } else { + return mContext.getString(R.string.battery_saver_auto_percentage_summary, + Utils.formatPercentage(threshold)); + } + } else { + return mContext.getText(R.string.battery_saver_auto_routine); + } + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } +} diff --git a/src/com/android/settings/fuelgauge/batterysaver/BatterySaverScheduleRadioButtonsController.java b/src/com/android/settings/fuelgauge/batterysaver/BatterySaverScheduleRadioButtonsController.java new file mode 100644 index 0000000000..2cf2b6df7e --- /dev/null +++ b/src/com/android/settings/fuelgauge/batterysaver/BatterySaverScheduleRadioButtonsController.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2018 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.fuelgauge.batterysaver; + +import android.content.ContentResolver; +import android.content.Context; +import android.os.Bundle; +import android.os.PowerManager; +import android.provider.Settings; +import android.provider.Settings.Global; +import android.text.TextUtils; +import com.android.settingslib.fuelgauge.BatterySaverUtils; + +/** + * Responds to user actions in the Settings > Battery > Set a Schedule Screen + * + * Note that this is not a preference controller since that screen does not inherit from + * DashboardFragment. + * + * Will call the appropriate power manager APIs and modify the correct settings to enable + * users to control their automatic battery saver toggling preferences. + * See {@link Settings.Global#AUTOMATIC_POWER_SAVE_MODE} for more details. + */ +public class BatterySaverScheduleRadioButtonsController { + + public static final String KEY_NO_SCHEDULE = "key_battery_saver_no_schedule"; + public static final String KEY_ROUTINE = "key_battery_saver_routine"; + public static final String KEY_PERCENTAGE = "key_battery_saver_percentage"; + public static final int TRIGGER_LEVEL_MIN = 5; + + private Context mContext; + private BatterySaverScheduleSeekBarController mSeekBarController; + + public BatterySaverScheduleRadioButtonsController(Context context, + BatterySaverScheduleSeekBarController seekbar) { + mContext = context; + mSeekBarController = seekbar; + } + + public String getDefaultKey() { + final ContentResolver resolver = mContext.getContentResolver(); + // Note: this can also be obtained via PowerManager.getPowerSaveModeTrigger() + final int mode = Settings.Global.getInt(resolver, Global.AUTOMATIC_POWER_SAVE_MODE, + PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE); + // if mode is "dynamic" we are in routine mode, percentage with non-zero threshold is + // percentage mode, otherwise it is no schedule mode + if (mode == PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE) { + final int threshold = + Settings.Global.getInt(resolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, 0); + if (threshold <= 0) { + return KEY_NO_SCHEDULE; + } + return KEY_PERCENTAGE; + } + return KEY_ROUTINE; + } + + public boolean setDefaultKey(String key) { + if (key == null) { + return false; + } + + final ContentResolver resolver = mContext.getContentResolver(); + int mode = PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE; + int triggerLevel = 0; + final Bundle confirmationExtras = new Bundle(3); + switch (key) { + case KEY_NO_SCHEDULE: + break; + case KEY_PERCENTAGE: + triggerLevel = TRIGGER_LEVEL_MIN; + confirmationExtras.putBoolean(BatterySaverUtils.EXTRA_CONFIRM_TEXT_ONLY, true); + confirmationExtras.putInt(BatterySaverUtils.EXTRA_POWER_SAVE_MODE_TRIGGER, + PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE); + confirmationExtras.putInt(BatterySaverUtils.EXTRA_POWER_SAVE_MODE_TRIGGER_LEVEL, + triggerLevel); + break; + case KEY_ROUTINE: + mode = PowerManager.POWER_SAVE_MODE_TRIGGER_DYNAMIC; + confirmationExtras.putBoolean(BatterySaverUtils.EXTRA_CONFIRM_TEXT_ONLY, true); + confirmationExtras.putInt(BatterySaverUtils.EXTRA_POWER_SAVE_MODE_TRIGGER, + PowerManager.POWER_SAVE_MODE_TRIGGER_DYNAMIC); + break; + default: + throw new IllegalStateException( + "Not a valid key for " + this.getClass().getSimpleName()); + } + + if (!TextUtils.equals(key, KEY_NO_SCHEDULE) + && BatterySaverUtils.maybeShowBatterySaverConfirmation( + mContext, confirmationExtras)) { + // reset this if we need to show the confirmation message + mode = PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE; + triggerLevel = 0; + } + // Trigger level is intentionally left alone when going between dynamic and percentage modes + // so that a users percentage based schedule is preserved when they toggle between the two. + Settings.Global.putInt(resolver, Global.AUTOMATIC_POWER_SAVE_MODE, mode); + if (mode != PowerManager.POWER_SAVE_MODE_TRIGGER_DYNAMIC) { + Settings.Global.putInt(resolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, triggerLevel); + } + mSeekBarController.updateSeekBar(); + return true; + } +} diff --git a/src/com/android/settings/fuelgauge/batterysaver/BatterySaverScheduleSeekBarController.java b/src/com/android/settings/fuelgauge/batterysaver/BatterySaverScheduleSeekBarController.java new file mode 100644 index 0000000000..5442e7d02c --- /dev/null +++ b/src/com/android/settings/fuelgauge/batterysaver/BatterySaverScheduleSeekBarController.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2018 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.fuelgauge.batterysaver; + +import android.content.ContentResolver; +import android.content.Context; +import android.os.PowerManager; +import android.provider.Settings; +import android.provider.Settings.Global; + +import androidx.preference.Preference; +import androidx.preference.Preference.OnPreferenceChangeListener; +import androidx.preference.PreferenceScreen; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.settings.R; +import com.android.settings.Utils; +import com.android.settings.widget.SeekBarPreference; + +/** + * Responds to user actions in the Settings > Battery > Set a Schedule Screen for the seekbar. + * Note that this seekbar is only visible when the radio button selected is "Percentage". + * + * Note that this is not a preference controller since that screen does not inherit from + * DashboardFragment. + * + * Will call the appropriate power manager APIs and modify the correct settings to enable + * users to control their automatic battery saver toggling preferences. + * See {@link Settings.Global#AUTOMATIC_POWER_SAVE_MODE} for more details. + */ +public class BatterySaverScheduleSeekBarController implements + OnPreferenceChangeListener { + + public static final int MAX_SEEKBAR_VALUE = 15; + public static final int MIN_SEEKBAR_VALUE = 1; + public static final String KEY_BATTERY_SAVER_SEEK_BAR = "battery_saver_seek_bar"; + + @VisibleForTesting + public SeekBarPreference mSeekBarPreference; + private Context mContext; + + public BatterySaverScheduleSeekBarController(Context context) { + mContext = context; + mSeekBarPreference = new SeekBarPreference(context); + mSeekBarPreference.setOnPreferenceChangeListener(this); + mSeekBarPreference.setContinuousUpdates(true); + mSeekBarPreference.setMax(MAX_SEEKBAR_VALUE); + mSeekBarPreference.setMin(MIN_SEEKBAR_VALUE); + mSeekBarPreference.setKey(KEY_BATTERY_SAVER_SEEK_BAR); + updateSeekBar(); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + // The nits are in intervals of 5% + final int percentage = ((Integer) newValue) * 5; + Settings.Global.putInt(mContext.getContentResolver(), Global.LOW_POWER_MODE_TRIGGER_LEVEL, + percentage); + preference.setTitle(mContext.getString( + R.string.battery_saver_seekbar_title, Utils.formatPercentage(percentage))); + return true; + } + + public void updateSeekBar() { + final ContentResolver resolver = mContext.getContentResolver(); + // Note: this can also be obtained via PowerManager.getPowerSaveModeTrigger() + final int mode = Settings.Global.getInt(resolver, Global.AUTOMATIC_POWER_SAVE_MODE, + PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE); + // if mode is "dynamic" we are in routine mode, percentage with non-zero threshold is + // percentage mode, otherwise it is no schedule mode + if (mode == PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE) { + final int threshold = + Settings.Global.getInt(resolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, 0); + if (threshold <= 0) { + mSeekBarPreference.setVisible(false); + } else { + final int currentSeekbarValue = Math.max(threshold / 5, MIN_SEEKBAR_VALUE); + mSeekBarPreference.setVisible(true); + mSeekBarPreference.setProgress(currentSeekbarValue); + mSeekBarPreference.setTitle(mContext.getString( + R.string.battery_saver_seekbar_title, + Utils.formatPercentage(currentSeekbarValue * 5))); + } + } else { + mSeekBarPreference.setVisible(false); + } + } + + /** + * Adds the seekbar to the end of the provided preference screen + * + * @param screen The preference screen to add the seekbar to + */ + public void addToScreen(PreferenceScreen screen) { + // makes sure it gets added after the preferences if called due to first time battery + // saver message + mSeekBarPreference.setOrder(100); + screen.addPreference(mSeekBarPreference); + } +} diff --git a/src/com/android/settings/fuelgauge/batterysaver/BatterySaverScheduleSettings.java b/src/com/android/settings/fuelgauge/batterysaver/BatterySaverScheduleSettings.java new file mode 100644 index 0000000000..31ee278eaf --- /dev/null +++ b/src/com/android/settings/fuelgauge/batterysaver/BatterySaverScheduleSettings.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2018 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.fuelgauge.batterysaver; + +import android.content.Context; +import android.database.ContentObserver; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.provider.Settings; +import android.text.TextUtils; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.widget.RadioButtonPickerFragment; +import com.android.settings.widget.RadioButtonPreference; +import com.android.settingslib.fuelgauge.BatterySaverUtils; +import com.android.settingslib.widget.CandidateInfo; + +import com.google.common.collect.Lists; + +import java.util.List; + +/** + * Fragment that allows users to customize their automatic battery saver mode settings. + * + * Location: Settings > Battery > Battery Saver > Set a Schedule + * See {@link BatterySaverSchedulePreferenceController} for the controller that manages navigation + * to this screen from "Settings > Battery > Battery Saver" and the summary. + * See {@link BatterySaverScheduleRadioButtonsController} & + * {@link BatterySaverScheduleSeekBarController} for the controller that manages user + * interactions in this screen. + */ +public class BatterySaverScheduleSettings extends RadioButtonPickerFragment { + + public BatterySaverScheduleRadioButtonsController mRadioButtonController; + @VisibleForTesting + Context mContext; + private BatterySaverScheduleSeekBarController mSeekBarController; + + @VisibleForTesting + final ContentObserver mSettingsObserver = new ContentObserver(new Handler()) { + @Override + public void onChange(boolean selfChange, Uri uri) { + getPreferenceScreen().removeAll(); + updateCandidates(); + } + }; + + @Override + protected int getPreferenceScreenResId() { + return R.xml.battery_saver_schedule_settings; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + mSeekBarController = new BatterySaverScheduleSeekBarController(context); + mRadioButtonController = new BatterySaverScheduleRadioButtonsController( + context, mSeekBarController); + mContext = context; + } + + @Override + public void onResume() { + super.onResume(); + mContext.getContentResolver().registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.LOW_POWER_WARNING_ACKNOWLEDGED), + false, + mSettingsObserver); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + setDivider(new ColorDrawable(Color.TRANSPARENT)); + setDividerHeight(0); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + public void onPause() { + mContext.getContentResolver().unregisterContentObserver(mSettingsObserver); + super.onPause(); + } + + @Override + protected List<? extends CandidateInfo> getCandidates() { + Context context = getContext(); + List<CandidateInfo> candidates = Lists.newArrayList(); + String routineProviderApp = getContext().getResources() + .getString(com.android.internal.R.string.config_batterySaverScheduleProvider); + candidates.add(new BatterySaverScheduleCandidateInfo( + context.getText(R.string.battery_saver_auto_no_schedule), + /* summary */ null, + BatterySaverScheduleRadioButtonsController.KEY_NO_SCHEDULE, + /* enabled */ true)); + // only add routine option if an app has been specified + if (!TextUtils.isEmpty(routineProviderApp)) { + candidates.add(new BatterySaverScheduleCandidateInfo( + context.getText(R.string.battery_saver_auto_routine), + context.getText(R.string.battery_saver_auto_routine_summary), + BatterySaverScheduleRadioButtonsController.KEY_ROUTINE, + /* enabled */ true)); + } else { + // Make sure routine is not selected if no provider app is configured + BatterySaverUtils.revertScheduleToNoneIfNeeded(context); + } + candidates.add(new BatterySaverScheduleCandidateInfo( + context.getText(R.string.battery_saver_auto_percentage), + /* summary */ null, + BatterySaverScheduleRadioButtonsController.KEY_PERCENTAGE, + /* enabled */ true)); + + return candidates; + } + + @Override + public void bindPreferenceExtra(RadioButtonPreference pref, String key, CandidateInfo info, + String defaultKey, String systemDefaultKey) { + final BatterySaverScheduleCandidateInfo candidateInfo = + (BatterySaverScheduleCandidateInfo) info; + final CharSequence summary = candidateInfo.getSummary(); + if (summary != null) { + pref.setSummary(summary); + pref.setAppendixVisibility(View.GONE); + } + } + + @Override + protected void addStaticPreferences(PreferenceScreen screen) { + mSeekBarController.updateSeekBar(); + mSeekBarController.addToScreen(screen); + } + + @Override + protected String getDefaultKey() { + return mRadioButtonController.getDefaultKey(); + } + + @Override + protected boolean setDefaultKey(String key) { + return mRadioButtonController.setDefaultKey(key); + } + + @Override + public int getMetricsCategory() { + return 0; + } + + static class BatterySaverScheduleCandidateInfo extends CandidateInfo { + + private final CharSequence mLabel; + private final CharSequence mSummary; + private final String mKey; + + BatterySaverScheduleCandidateInfo(CharSequence label, CharSequence summary, String key, + boolean enabled) { + super(enabled); + mLabel = label; + mKey = key; + mSummary = summary; + } + + @Override + public CharSequence loadLabel() { + return mLabel; + } + + @Override + public Drawable loadIcon() { + return null; + } + + @Override + public String getKey() { + return mKey; + } + + public CharSequence getSummary() { + return mSummary; + } + } +}
\ No newline at end of file diff --git a/src/com/android/settings/fuelgauge/batterysaver/BatterySaverSettings.java b/src/com/android/settings/fuelgauge/batterysaver/BatterySaverSettings.java index 26f9e17121..be23f72027 100644 --- a/src/com/android/settings/fuelgauge/batterysaver/BatterySaverSettings.java +++ b/src/com/android/settings/fuelgauge/batterysaver/BatterySaverSettings.java @@ -16,36 +16,52 @@ package com.android.settings.fuelgauge.batterysaver; +import android.app.settings.SettingsEnums; import android.content.Context; import android.os.Bundle; import android.provider.SearchIndexableResource; +import android.text.Annotation; +import android.text.Spannable; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.TextPaint; +import android.text.TextUtils; +import android.text.style.URLSpan; +import android.view.View; + +import androidx.annotation.VisibleForTesting; +import androidx.fragment.app.Fragment; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; -import com.android.settingslib.core.AbstractPreferenceController; -import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.HelpUtils; +import com.android.settingslib.search.SearchIndexable; +import com.android.settingslib.widget.FooterPreference; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * Battery saver settings page */ +@SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC) public class BatterySaverSettings extends DashboardFragment { private static final String TAG = "BatterySaverSettings"; + public static final String KEY_FOOTER_PREFERENCE = "footer_preference"; + private SpannableStringBuilder mFooterText; + private String mHelpUri; @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); + public void onStart() { + super.onStart(); + setupFooter(); } @Override public int getMetricsCategory() { - return MetricsEvent.FUELGAUGE_BATTERY_SAVER; + return SettingsEnums.FUELGAUGE_BATTERY_SAVER; } @Override @@ -59,23 +75,10 @@ public class BatterySaverSettings extends DashboardFragment { } @Override - protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { - return buildPreferenceControllers(context, getLifecycle()); - } - - @Override public int getHelpResource() { return R.string.help_url_battery_saver_settings; } - private static List<AbstractPreferenceController> buildPreferenceControllers( - Context context, Lifecycle lifecycle) { - final List<AbstractPreferenceController> controllers = new ArrayList<>(); - controllers.add(new AutoBatterySaverPreferenceController(context)); - controllers.add(new AutoBatterySeekBarPreferenceController(context, lifecycle)); - return controllers; - } - /** * For Search. */ @@ -88,11 +91,83 @@ public class BatterySaverSettings extends DashboardFragment { sir.xmlResId = R.xml.battery_saver_settings; return Arrays.asList(sir); } + }; - @Override - public List<AbstractPreferenceController> createPreferenceControllers( - Context context) { - return buildPreferenceControllers(context, null); + // Updates the footer for this page. + @VisibleForTesting + void setupFooter() { + mFooterText = new SpannableStringBuilder(getText( + com.android.internal.R.string.battery_saver_description_with_learn_more)); + mHelpUri = getString(R.string.help_url_battery_saver_settings); + if (!TextUtils.isEmpty(mHelpUri)) { + addHelpLink(); + } + } + + // Changes the text to include a learn more link if possible. + @VisibleForTesting + void addHelpLink() { + FooterPreference pref = getPreferenceScreen().findPreference(KEY_FOOTER_PREFERENCE); + if (pref != null) { + SupportPageLearnMoreSpan.linkify(mFooterText, this, mHelpUri); + pref.setTitle(mFooterText); + } + } + + /** + * A {@link URLSpan} that opens a support page when clicked + */ + public static class SupportPageLearnMoreSpan extends URLSpan { + + + private static final String ANNOTATION_URL = "url"; + private final Fragment mFragment; + private final String mUriString; + + public SupportPageLearnMoreSpan(Fragment fragment, String uriString) { + // sets the url to empty string so we can prevent any other span processing from + // from clearing things we need in this string. + super(""); + mFragment = fragment; + mUriString = uriString; + } + + @Override + public void onClick(View widget) { + if (mFragment != null) { + // launch the support page + mFragment.startActivityForResult(HelpUtils.getHelpIntent(mFragment.getContext(), + mUriString, ""), 0); + } + } + + @Override + public void updateDrawState(TextPaint ds) { + super.updateDrawState(ds); + // remove underline + ds.setUnderlineText(false); + } + + /** + * This method takes a string and turns it into a url span that will launch a support page + * @param msg The text to turn into a link + * @param fragment The fragment which contains this span + * @param uriString The URI string of the help article to open when clicked + * @return A CharSequence containing the original text content as a url + */ + public static CharSequence linkify(Spannable msg, Fragment fragment, String uriString) { + Annotation[] spans = msg.getSpans(0, msg.length(), Annotation.class); + for (Annotation annotation : spans) { + int start = msg.getSpanStart(annotation); + int end = msg.getSpanEnd(annotation); + if (ANNOTATION_URL.equals(annotation.getValue())) { + SupportPageLearnMoreSpan link = + new SupportPageLearnMoreSpan(fragment, uriString); + msg.removeSpan(annotation); + msg.setSpan(link, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } - }; + } + return msg; + } + } } diff --git a/src/com/android/settings/fuelgauge/batterysaver/BatterySaverStickyPreferenceController.java b/src/com/android/settings/fuelgauge/batterysaver/BatterySaverStickyPreferenceController.java new file mode 100644 index 0000000000..7d4bdac074 --- /dev/null +++ b/src/com/android/settings/fuelgauge/batterysaver/BatterySaverStickyPreferenceController.java @@ -0,0 +1,62 @@ +package com.android.settings.fuelgauge.batterysaver; + +import android.content.Context; +import android.icu.text.NumberFormat; +import android.provider.Settings; +import android.provider.Settings.Global; +import androidx.preference.Preference; +import androidx.preference.SwitchPreference; +import com.android.settings.R; +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settings.core.TogglePreferenceController; + +public class BatterySaverStickyPreferenceController extends TogglePreferenceController implements + PreferenceControllerMixin, Preference.OnPreferenceChangeListener { + + private Context mContext; + + public BatterySaverStickyPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + mContext = context; + } + + @Override + public boolean isChecked() { + return Settings.Global.getInt(mContext.getContentResolver(), + Global.LOW_POWER_MODE_STICKY_AUTO_DISABLE_ENABLED, 1) == 1; + } + + @Override + public boolean setChecked(boolean isChecked) { + Settings.Global.putInt(mContext.getContentResolver(), + Global.LOW_POWER_MODE_STICKY_AUTO_DISABLE_ENABLED, + isChecked ? 1 : 0); + return true; + } + + @Override + protected void refreshSummary(Preference preference) { + super.refreshSummary(preference); + final double stickyShutoffLevel = Settings.Global.getInt( + mContext.getContentResolver(), Global.LOW_POWER_MODE_STICKY_AUTO_DISABLE_LEVEL, 90); + final String percentage = NumberFormat + .getPercentInstance() + .format(stickyShutoffLevel / 100.0); + preference.setSummary( + mContext.getString(R.string.battery_saver_sticky_description_new, percentage)); + } + + @Override + public void updateState(Preference preference) { + int setting = Settings.Global.getInt(mContext.getContentResolver(), + Global.LOW_POWER_MODE_STICKY_AUTO_DISABLE_ENABLED, 1); + + ((SwitchPreference) preference).setChecked(setting == 1); + refreshSummary(preference); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } +} diff --git a/src/com/android/settings/fuelgauge/batterytip/AnomalyCleanupJobService.java b/src/com/android/settings/fuelgauge/batterytip/AnomalyCleanupJobService.java index 331572ba61..bb73142b9b 100644 --- a/src/com/android/settings/fuelgauge/batterytip/AnomalyCleanupJobService.java +++ b/src/com/android/settings/fuelgauge/batterytip/AnomalyCleanupJobService.java @@ -22,9 +22,10 @@ import android.app.job.JobScheduler; import android.app.job.JobService; import android.content.ComponentName; import android.content.Context; -import androidx.annotation.VisibleForTesting; import android.util.Log; +import androidx.annotation.VisibleForTesting; + import com.android.settings.R; import com.android.settingslib.utils.ThreadUtils; diff --git a/src/com/android/settings/fuelgauge/batterytip/AnomalyConfigJobService.java b/src/com/android/settings/fuelgauge/batterytip/AnomalyConfigJobService.java index 44169f0bfe..ad02c3a75b 100644 --- a/src/com/android/settings/fuelgauge/batterytip/AnomalyConfigJobService.java +++ b/src/com/android/settings/fuelgauge/batterytip/AnomalyConfigJobService.java @@ -25,11 +25,12 @@ import android.content.ComponentName; import android.content.Context; import android.content.SharedPreferences; import android.provider.Settings; -import androidx.annotation.VisibleForTesting; import android.text.TextUtils; import android.util.Base64; import android.util.Log; +import androidx.annotation.VisibleForTesting; + import com.android.settings.R; import com.android.settingslib.utils.ThreadUtils; diff --git a/src/com/android/settings/fuelgauge/batterytip/AnomalyConfigReceiver.java b/src/com/android/settings/fuelgauge/batterytip/AnomalyConfigReceiver.java index 8e22447022..369e61382d 100644 --- a/src/com/android/settings/fuelgauge/batterytip/AnomalyConfigReceiver.java +++ b/src/com/android/settings/fuelgauge/batterytip/AnomalyConfigReceiver.java @@ -16,15 +16,12 @@ package com.android.settings.fuelgauge.batterytip; -import android.app.PendingIntent; import android.app.StatsManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.util.Log; -import com.android.internal.annotations.VisibleForTesting; - /** * Receive broadcast when {@link StatsManager} restart, then check the anomaly config and * prepare info for {@link StatsManager} diff --git a/src/com/android/settings/fuelgauge/batterytip/AnomalyDatabaseHelper.java b/src/com/android/settings/fuelgauge/batterytip/AnomalyDatabaseHelper.java index 404d8d7a88..349a419ea2 100644 --- a/src/com/android/settings/fuelgauge/batterytip/AnomalyDatabaseHelper.java +++ b/src/com/android/settings/fuelgauge/batterytip/AnomalyDatabaseHelper.java @@ -19,10 +19,9 @@ package com.android.settings.fuelgauge.batterytip; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; -import androidx.annotation.IntDef; import android.util.Log; -import com.android.settings.fuelgauge.anomaly.Anomaly; +import androidx.annotation.IntDef; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -34,7 +33,7 @@ public class AnomalyDatabaseHelper extends SQLiteOpenHelper { private static final String TAG = "BatteryDatabaseHelper"; private static final String DATABASE_NAME = "battery_settings.db"; - private static final int DATABASE_VERSION = 4; + private static final int DATABASE_VERSION = 5; @Retention(RetentionPolicy.SOURCE) @IntDef({State.NEW, @@ -46,8 +45,15 @@ public class AnomalyDatabaseHelper extends SQLiteOpenHelper { int AUTO_HANDLED = 2; } + @Retention(RetentionPolicy.SOURCE) + @IntDef({ActionType.RESTRICTION}) + public @interface ActionType { + int RESTRICTION = 0; + } + public interface Tables { String TABLE_ANOMALY = "anomaly"; + String TABLE_ACTION = "action"; } public interface AnomalyColumns { @@ -61,7 +67,7 @@ public class AnomalyDatabaseHelper extends SQLiteOpenHelper { String UID = "uid"; /** * The type of the anomaly app - * @see Anomaly.AnomalyType + * @see StatsManagerConfig.AnomalyType */ String ANOMALY_TYPE = "anomaly_type"; /** @@ -92,6 +98,42 @@ public class AnomalyDatabaseHelper extends SQLiteOpenHelper { + AnomalyColumns.ANOMALY_STATE + "," + AnomalyColumns.TIME_STAMP_MS + ")" + ")"; + + public interface ActionColumns { + /** + * The package name of an app been performed an action + */ + String PACKAGE_NAME = "package_name"; + /** + * The uid of an app been performed an action + */ + String UID = "uid"; + /** + * The type of user action + * @see ActionType + */ + String ACTION_TYPE = "action_type"; + /** + * The time when action been performed + */ + String TIME_STAMP_MS = "time_stamp_ms"; + } + + private static final String CREATE_ACTION_TABLE = + "CREATE TABLE " + Tables.TABLE_ACTION + + "(" + + ActionColumns.UID + + " INTEGER NOT NULL, " + + ActionColumns.PACKAGE_NAME + + " TEXT, " + + ActionColumns.ACTION_TYPE + + " INTEGER NOT NULL, " + + ActionColumns.TIME_STAMP_MS + + " INTEGER NOT NULL, " + + " PRIMARY KEY (" + ActionColumns.ACTION_TYPE + "," + ActionColumns.UID + "," + + ActionColumns.PACKAGE_NAME + ")" + + ")"; + private static AnomalyDatabaseHelper sSingleton; public static synchronized AnomalyDatabaseHelper getInstance(Context context) { @@ -110,11 +152,6 @@ public class AnomalyDatabaseHelper extends SQLiteOpenHelper { bootstrapDB(db); } - private void bootstrapDB(SQLiteDatabase db) { - db.execSQL(CREATE_ANOMALY_TABLE); - Log.i(TAG, "Bootstrapped database"); - } - @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion < DATABASE_VERSION) { @@ -138,7 +175,14 @@ public class AnomalyDatabaseHelper extends SQLiteOpenHelper { bootstrapDB(db); } + private void bootstrapDB(SQLiteDatabase db) { + db.execSQL(CREATE_ANOMALY_TABLE); + db.execSQL(CREATE_ACTION_TABLE); + Log.i(TAG, "Bootstrapped database"); + } + private void dropTables(SQLiteDatabase db) { db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_ANOMALY); + db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_ACTION); } } diff --git a/src/com/android/settings/fuelgauge/batterytip/AnomalyDetectionJobService.java b/src/com/android/settings/fuelgauge/batterytip/AnomalyDetectionJobService.java index ad63fd21ad..8b572347e7 100644 --- a/src/com/android/settings/fuelgauge/batterytip/AnomalyDetectionJobService.java +++ b/src/com/android/settings/fuelgauge/batterytip/AnomalyDetectionJobService.java @@ -26,6 +26,7 @@ import android.app.job.JobParameters; import android.app.job.JobScheduler; import android.app.job.JobService; import android.app.job.JobWorkItem; +import android.app.settings.SettingsEnums; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; @@ -34,12 +35,11 @@ import android.os.Bundle; import android.os.StatsDimensionsValue; import android.os.UserManager; import android.provider.Settings; +import android.util.Log; + import androidx.annotation.GuardedBy; import androidx.annotation.VisibleForTesting; -import android.util.Log; -import android.util.Pair; -import com.android.internal.logging.nano.MetricsProto; import com.android.internal.util.ArrayUtils; import com.android.settings.R; import com.android.settings.fuelgauge.BatteryUtils; @@ -145,20 +145,18 @@ public class AnomalyDetectionJobService extends JobService { final int uid = extractUidFromStatsDimensionsValue(intentDimsValue); final boolean autoFeatureOn = powerUsageFeatureProvider.isSmartBatterySupported() ? Settings.Global.getInt(contentResolver, - Settings.Global.ADAPTIVE_BATTERY_MANAGEMENT_ENABLED, ON) == ON + Settings.Global.ADAPTIVE_BATTERY_MANAGEMENT_ENABLED, ON) == ON : Settings.Global.getInt(contentResolver, Settings.Global.APP_AUTO_RESTRICTION_ENABLED, ON) == ON; final String packageName = batteryUtils.getPackageName(uid); final long versionCode = batteryUtils.getAppLongVersionCode(packageName); - + final String versionedPackage = packageName + "/" + versionCode; if (batteryUtils.shouldHideAnomaly(powerWhitelistBackend, uid, anomalyInfo)) { - metricsFeatureProvider.action(context, - MetricsProto.MetricsEvent.ACTION_ANOMALY_IGNORED, - packageName, - Pair.create(MetricsProto.MetricsEvent.FIELD_CONTEXT, - anomalyInfo.anomalyType), - Pair.create(MetricsProto.MetricsEvent.FIELD_APP_VERSION_CODE, - versionCode)); + metricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, + SettingsEnums.ACTION_ANOMALY_IGNORED, + SettingsEnums.PAGE_UNKNOWN, + versionedPackage, + anomalyInfo.anomalyType); } else { if (autoFeatureOn && anomalyInfo.autoRestriction) { // Auto restrict this app @@ -172,13 +170,11 @@ public class AnomalyDetectionJobService extends JobService { AnomalyDatabaseHelper.State.NEW, timeMs); } - metricsFeatureProvider.action(context, - MetricsProto.MetricsEvent.ACTION_ANOMALY_TRIGGERED, - packageName, - Pair.create(MetricsProto.MetricsEvent.FIELD_ANOMALY_TYPE, - anomalyInfo.anomalyType), - Pair.create(MetricsProto.MetricsEvent.FIELD_APP_VERSION_CODE, - versionCode)); + metricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, + SettingsEnums.ACTION_ANOMALY_TRIGGERED, + SettingsEnums.PAGE_UNKNOWN, + versionedPackage, + anomalyInfo.anomalyType); } } catch (NullPointerException | IndexOutOfBoundsException e) { diff --git a/src/com/android/settings/fuelgauge/batterytip/AnomalyInfo.java b/src/com/android/settings/fuelgauge/batterytip/AnomalyInfo.java index 063cfec062..fc15706510 100644 --- a/src/com/android/settings/fuelgauge/batterytip/AnomalyInfo.java +++ b/src/com/android/settings/fuelgauge/batterytip/AnomalyInfo.java @@ -20,7 +20,7 @@ import android.util.KeyValueListParser; import android.util.Log; /** - * Model class to parse and store anomaly info from westworld + * Model class to parse and store anomaly info from statsd. */ public class AnomalyInfo { private static final String TAG = "AnomalyInfo"; @@ -38,4 +38,4 @@ public class AnomalyInfo { autoRestriction = parser.getBoolean(KEY_AUTO_RESTRICTION, false); } -}
\ No newline at end of file +} diff --git a/src/com/android/settings/fuelgauge/batterytip/AppInfo.java b/src/com/android/settings/fuelgauge/batterytip/AppInfo.java index 4128f9dc48..e79b874519 100644 --- a/src/com/android/settings/fuelgauge/batterytip/AppInfo.java +++ b/src/com/android/settings/fuelgauge/batterytip/AppInfo.java @@ -18,11 +18,10 @@ package com.android.settings.fuelgauge.batterytip; import android.os.Parcel; import android.os.Parcelable; -import androidx.annotation.VisibleForTesting; import android.text.TextUtils; import android.util.ArraySet; -import com.android.settings.fuelgauge.anomaly.Anomaly; +import androidx.annotation.VisibleForTesting; import java.util.Objects; @@ -33,7 +32,7 @@ public class AppInfo implements Comparable<AppInfo>, Parcelable { public final String packageName; /** * Anomaly type of the app - * @see Anomaly.AnomalyType + * @see StatsManagerConfig.AnomalyType */ public final ArraySet<Integer> anomalyTypes; public final long screenOnTimeMs; diff --git a/src/com/android/settings/fuelgauge/batterytip/BatteryDatabaseManager.java b/src/com/android/settings/fuelgauge/batterytip/BatteryDatabaseManager.java index 772660a69d..bb6999949c 100644 --- a/src/com/android/settings/fuelgauge/batterytip/BatteryDatabaseManager.java +++ b/src/com/android/settings/fuelgauge/batterytip/BatteryDatabaseManager.java @@ -17,16 +17,14 @@ package com.android.settings.fuelgauge.batterytip; import static android.database.sqlite.SQLiteDatabase.CONFLICT_IGNORE; +import static android.database.sqlite.SQLiteDatabase.CONFLICT_REPLACE; -import static com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper.AnomalyColumns - .ANOMALY_STATE; -import static com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper.AnomalyColumns - .ANOMALY_TYPE; -import static com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper.AnomalyColumns - .PACKAGE_NAME; -import static com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper.AnomalyColumns - .TIME_STAMP_MS; +import static com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper.AnomalyColumns.ANOMALY_STATE; +import static com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper.AnomalyColumns.ANOMALY_TYPE; +import static com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper.AnomalyColumns.PACKAGE_NAME; +import static com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper.AnomalyColumns.TIME_STAMP_MS; import static com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper.AnomalyColumns.UID; +import static com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper.Tables.TABLE_ACTION; import static com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper.Tables.TABLE_ANOMALY; import android.content.ContentValues; @@ -35,6 +33,11 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.text.TextUtils; import android.util.ArrayMap; +import android.util.SparseLongArray; + +import androidx.annotation.VisibleForTesting; + +import com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper.ActionColumns; import java.util.ArrayList; import java.util.Collections; @@ -56,13 +59,18 @@ public class BatteryDatabaseManager { mDatabaseHelper = AnomalyDatabaseHelper.getInstance(context); } - public static BatteryDatabaseManager getInstance(Context context) { + public static synchronized BatteryDatabaseManager getInstance(Context context) { if (sSingleton == null) { sSingleton = new BatteryDatabaseManager(context); } return sSingleton; } + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + public static void setUpForTest(BatteryDatabaseManager batteryDatabaseManager) { + sSingleton = batteryDatabaseManager; + } + /** * Insert an anomaly log to database. * @@ -76,15 +84,15 @@ public class BatteryDatabaseManager { public synchronized boolean insertAnomaly(int uid, String packageName, int type, int anomalyState, long timestampMs) { - try (SQLiteDatabase db = mDatabaseHelper.getWritableDatabase()) { - ContentValues values = new ContentValues(); - values.put(UID, uid); - values.put(PACKAGE_NAME, packageName); - values.put(ANOMALY_TYPE, type); - values.put(ANOMALY_STATE, anomalyState); - values.put(TIME_STAMP_MS, timestampMs); - return db.insertWithOnConflict(TABLE_ANOMALY, null, values, CONFLICT_IGNORE) != -1; - } + final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(UID, uid); + values.put(PACKAGE_NAME, packageName); + values.put(ANOMALY_TYPE, type); + values.put(ANOMALY_STATE, anomalyState); + values.put(TIME_STAMP_MS, timestampMs); + + return db.insertWithOnConflict(TABLE_ANOMALY, null, values, CONFLICT_IGNORE) != -1; } /** @@ -92,43 +100,41 @@ public class BatteryDatabaseManager { */ public synchronized List<AppInfo> queryAllAnomalies(long timestampMsAfter, int state) { final List<AppInfo> appInfos = new ArrayList<>(); - try (SQLiteDatabase db = mDatabaseHelper.getReadableDatabase()) { - final String[] projection = {PACKAGE_NAME, ANOMALY_TYPE, UID}; - final String orderBy = AnomalyDatabaseHelper.AnomalyColumns.TIME_STAMP_MS + " DESC"; - final Map<Integer, AppInfo.Builder> mAppInfoBuilders = new ArrayMap<>(); - final String selection = TIME_STAMP_MS + " > ? AND " + ANOMALY_STATE + " = ? "; - final String[] selectionArgs = new String[]{String.valueOf(timestampMsAfter), - String.valueOf(state)}; - - try (Cursor cursor = db.query(TABLE_ANOMALY, projection, selection, selectionArgs, - null /* groupBy */, null /* having */, orderBy)) { - while (cursor.moveToNext()) { - final int uid = cursor.getInt(cursor.getColumnIndex(UID)); - if (!mAppInfoBuilders.containsKey(uid)) { - final AppInfo.Builder builder = new AppInfo.Builder() - .setUid(uid) - .setPackageName( - cursor.getString(cursor.getColumnIndex(PACKAGE_NAME))); - mAppInfoBuilders.put(uid, builder); - } - mAppInfoBuilders.get(uid).addAnomalyType( - cursor.getInt(cursor.getColumnIndex(ANOMALY_TYPE))); + final SQLiteDatabase db = mDatabaseHelper.getReadableDatabase(); + final String[] projection = {PACKAGE_NAME, ANOMALY_TYPE, UID}; + final String orderBy = AnomalyDatabaseHelper.AnomalyColumns.TIME_STAMP_MS + " DESC"; + final Map<Integer, AppInfo.Builder> mAppInfoBuilders = new ArrayMap<>(); + final String selection = TIME_STAMP_MS + " > ? AND " + ANOMALY_STATE + " = ? "; + final String[] selectionArgs = new String[]{String.valueOf(timestampMsAfter), + String.valueOf(state)}; + + try (Cursor cursor = db.query(TABLE_ANOMALY, projection, selection, selectionArgs, + null /* groupBy */, null /* having */, orderBy)) { + while (cursor.moveToNext()) { + final int uid = cursor.getInt(cursor.getColumnIndex(UID)); + if (!mAppInfoBuilders.containsKey(uid)) { + final AppInfo.Builder builder = new AppInfo.Builder() + .setUid(uid) + .setPackageName( + cursor.getString(cursor.getColumnIndex(PACKAGE_NAME))); + mAppInfoBuilders.put(uid, builder); } + mAppInfoBuilders.get(uid).addAnomalyType( + cursor.getInt(cursor.getColumnIndex(ANOMALY_TYPE))); } + } - for (Integer uid : mAppInfoBuilders.keySet()) { - appInfos.add(mAppInfoBuilders.get(uid).build()); - } + for (Integer uid : mAppInfoBuilders.keySet()) { + appInfos.add(mAppInfoBuilders.get(uid).build()); } return appInfos; } public synchronized void deleteAllAnomaliesBeforeTimeStamp(long timestampMs) { - try (SQLiteDatabase db = mDatabaseHelper.getWritableDatabase()) { - db.delete(TABLE_ANOMALY, TIME_STAMP_MS + " < ?", - new String[]{String.valueOf(timestampMs)}); - } + final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase(); + db.delete(TABLE_ANOMALY, TIME_STAMP_MS + " < ?", + new String[]{String.valueOf(timestampMs)}); } /** @@ -144,12 +150,71 @@ public class BatteryDatabaseManager { for (int i = 0; i < size; i++) { whereArgs[i] = appInfos.get(i).packageName; } - try (SQLiteDatabase db = mDatabaseHelper.getWritableDatabase()) { - final ContentValues values = new ContentValues(); - values.put(ANOMALY_STATE, state); - db.update(TABLE_ANOMALY, values, PACKAGE_NAME + " IN (" + TextUtils.join(",", - Collections.nCopies(appInfos.size(), "?")) + ")", whereArgs); + + final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase(); + final ContentValues values = new ContentValues(); + values.put(ANOMALY_STATE, state); + db.update(TABLE_ANOMALY, values, PACKAGE_NAME + " IN (" + TextUtils.join(",", + Collections.nCopies(appInfos.size(), "?")) + ")", whereArgs); + } + } + + /** + * Query latest timestamps when an app has been performed action {@code type} + * + * @param type of action been performed + * @return {@link SparseLongArray} where key is uid and value is timestamp + */ + public synchronized SparseLongArray queryActionTime( + @AnomalyDatabaseHelper.ActionType int type) { + final SparseLongArray timeStamps = new SparseLongArray(); + final SQLiteDatabase db = mDatabaseHelper.getReadableDatabase(); + final String[] projection = {ActionColumns.UID, ActionColumns.TIME_STAMP_MS}; + final String selection = ActionColumns.ACTION_TYPE + " = ? "; + final String[] selectionArgs = new String[]{String.valueOf(type)}; + + try (Cursor cursor = db.query(TABLE_ACTION, projection, selection, selectionArgs, + null /* groupBy */, null /* having */, null /* orderBy */)) { + final int uidIndex = cursor.getColumnIndex(ActionColumns.UID); + final int timestampIndex = cursor.getColumnIndex(ActionColumns.TIME_STAMP_MS); + + while (cursor.moveToNext()) { + final int uid = cursor.getInt(uidIndex); + final long timeStamp = cursor.getLong(timestampIndex); + timeStamps.append(uid, timeStamp); } } + + return timeStamps; + } + + /** + * Insert an action, or update it if already existed + */ + public synchronized boolean insertAction(@AnomalyDatabaseHelper.ActionType int type, + int uid, String packageName, long timestampMs) { + final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase(); + final ContentValues values = new ContentValues(); + values.put(ActionColumns.UID, uid); + values.put(ActionColumns.PACKAGE_NAME, packageName); + values.put(ActionColumns.ACTION_TYPE, type); + values.put(ActionColumns.TIME_STAMP_MS, timestampMs); + + return db.insertWithOnConflict(TABLE_ACTION, null, values, CONFLICT_REPLACE) != -1; + } + + /** + * Remove an action + */ + public synchronized boolean deleteAction(@AnomalyDatabaseHelper.ActionType int type, + int uid, String packageName) { + SQLiteDatabase db = mDatabaseHelper.getWritableDatabase(); + final String where = + ActionColumns.ACTION_TYPE + " = ? AND " + ActionColumns.UID + " = ? AND " + + ActionColumns.PACKAGE_NAME + " = ? "; + final String[] whereArgs = new String[]{String.valueOf(type), String.valueOf(uid), + String.valueOf(packageName)}; + + return db.delete(TABLE_ACTION, where, whereArgs) != 0; } } diff --git a/src/com/android/settings/fuelgauge/batterytip/BatteryManagerPreferenceController.java b/src/com/android/settings/fuelgauge/batterytip/BatteryManagerPreferenceController.java index 8edc45e338..9565036bfc 100644 --- a/src/com/android/settings/fuelgauge/batterytip/BatteryManagerPreferenceController.java +++ b/src/com/android/settings/fuelgauge/batterytip/BatteryManagerPreferenceController.java @@ -20,6 +20,7 @@ import android.app.AppOpsManager; import android.content.Context; import android.os.UserManager; import android.provider.Settings; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; @@ -48,7 +49,7 @@ public class BatteryManagerPreferenceController extends BasePreferenceController @Override public int getAvailabilityStatus() { - return AVAILABLE; + return AVAILABLE_UNSEARCHABLE; } @Override diff --git a/src/com/android/settings/fuelgauge/batterytip/BatteryTipDialogFragment.java b/src/com/android/settings/fuelgauge/batterytip/BatteryTipDialogFragment.java index 5cd52775ec..58038cd4bb 100644 --- a/src/com/android/settings/fuelgauge/batterytip/BatteryTipDialogFragment.java +++ b/src/com/android/settings/fuelgauge/batterytip/BatteryTipDialogFragment.java @@ -16,31 +16,29 @@ package com.android.settings.fuelgauge.batterytip; -import android.app.AlertDialog; import android.app.Dialog; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; +import android.view.LayoutInflater; + import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import android.view.LayoutInflater; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.Utils; import com.android.settings.core.InstrumentedPreferenceFragment; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; -import com.android.settings.fuelgauge.Estimate; import com.android.settings.fuelgauge.batterytip.BatteryTipPreferenceController.BatteryTipListener; import com.android.settings.fuelgauge.batterytip.actions.BatteryTipAction; import com.android.settings.fuelgauge.batterytip.tips.BatteryTip; import com.android.settings.fuelgauge.batterytip.tips.HighUsageTip; import com.android.settings.fuelgauge.batterytip.tips.RestrictAppTip; -import com.android.settings.fuelgauge.batterytip.tips.SummaryTip; import com.android.settings.fuelgauge.batterytip.tips.UnrestrictAppTip; -import com.android.settingslib.utils.StringUtil; import java.util.List; @@ -147,7 +145,7 @@ public class BatteryTipDialogFragment extends InstrumentedDialogFragment impleme @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.FUELGAUGE_BATTERY_TIP_DIALOG; + return SettingsEnums.FUELGAUGE_BATTERY_TIP_DIALOG; } @Override diff --git a/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoader.java b/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoader.java index c445687b20..a1fb076e64 100644 --- a/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoader.java +++ b/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoader.java @@ -17,22 +17,23 @@ package com.android.settings.fuelgauge.batterytip; import android.content.Context; + import androidx.annotation.VisibleForTesting; import com.android.internal.os.BatteryStatsHelper; import com.android.settings.fuelgauge.BatteryInfo; import com.android.settings.fuelgauge.BatteryUtils; -import com.android.settings.fuelgauge.Estimate; import com.android.settings.fuelgauge.batterytip.detectors.EarlyWarningDetector; import com.android.settings.fuelgauge.batterytip.detectors.HighUsageDetector; import com.android.settings.fuelgauge.batterytip.detectors.LowBatteryDetector; -import com.android.settings.fuelgauge.batterytip.detectors.SmartBatteryDetector; import com.android.settings.fuelgauge.batterytip.detectors.RestrictAppDetector; +import com.android.settings.fuelgauge.batterytip.detectors.SmartBatteryDetector; import com.android.settings.fuelgauge.batterytip.detectors.SummaryDetector; import com.android.settings.fuelgauge.batterytip.tips.BatteryTip; import com.android.settings.fuelgauge.batterytip.tips.LowBatteryTip; import com.android.settings.fuelgauge.batterytip.tips.SummaryTip; -import com.android.settingslib.utils.AsyncLoader; +import com.android.settingslib.fuelgauge.EstimateKt; +import com.android.settingslib.utils.AsyncLoaderCompat; import java.util.ArrayList; import java.util.Collections; @@ -42,7 +43,7 @@ import java.util.List; * Loader to compute and return a battery tip list. It will always return a full length list even * though some tips may have state {@code BaseBatteryTip.StateType.INVISIBLE}. */ -public class BatteryTipLoader extends AsyncLoader<List<BatteryTip>> { +public class BatteryTipLoader extends AsyncLoaderCompat<List<BatteryTip>> { private static final String TAG = "BatteryTipLoader"; private static final boolean USE_FAKE_DATA = false; @@ -86,7 +87,7 @@ public class BatteryTipLoader extends AsyncLoader<List<BatteryTip>> { private List<BatteryTip> getFakeData() { final List<BatteryTip> tips = new ArrayList<>(); tips.add(new SummaryTip(BatteryTip.StateType.NEW, - Estimate.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN)); + EstimateKt.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN)); tips.add(new LowBatteryTip(BatteryTip.StateType.NEW, false /* powerSaveModeOn */, "Fake data")); diff --git a/src/com/android/settings/fuelgauge/batterytip/BatteryTipPolicy.java b/src/com/android/settings/fuelgauge/batterytip/BatteryTipPolicy.java index 42e723d239..487adf859d 100644 --- a/src/com/android/settings/fuelgauge/batterytip/BatteryTipPolicy.java +++ b/src/com/android/settings/fuelgauge/batterytip/BatteryTipPolicy.java @@ -18,12 +18,12 @@ package com.android.settings.fuelgauge.batterytip; import android.content.Context; import android.provider.Settings; -import androidx.annotation.VisibleForTesting; import android.util.KeyValueListParser; import android.util.Log; +import androidx.annotation.VisibleForTesting; + import java.time.Duration; -import java.util.concurrent.TimeUnit; /** * Class to store the policy for battery tips, which comes from @@ -40,6 +40,7 @@ public class BatteryTipPolicy { private static final String KEY_HIGH_USAGE_PERIOD_MS = "high_usage_period_ms"; private static final String KEY_HIGH_USAGE_BATTERY_DRAINING = "high_usage_battery_draining"; private static final String KEY_APP_RESTRICTION_ENABLED = "app_restriction_enabled"; + private static final String KEY_APP_RESTRICTION_ACTIVE_HOUR = "app_restriction_active_hour"; private static final String KEY_REDUCED_BATTERY_ENABLED = "reduced_battery_enabled"; private static final String KEY_REDUCED_BATTERY_PERCENT = "reduced_battery_percent"; private static final String KEY_LOW_BATTERY_ENABLED = "low_battery_enabled"; @@ -119,6 +120,15 @@ public class BatteryTipPolicy { public final boolean appRestrictionEnabled; /** + * Period(hour) to show anomaly apps. If it is 24 hours, it means only show anomaly apps + * happened in last 24 hours. + * + * @see Settings.Global#BATTERY_TIP_CONSTANTS + * @see #KEY_APP_RESTRICTION_ACTIVE_HOUR + */ + public final int appRestrictionActiveHour; + + /** * {@code true} if reduced battery tip is enabled * * @see Settings.Global#BATTERY_TIP_CONSTANTS @@ -228,6 +238,7 @@ public class BatteryTipPolicy { Duration.ofHours(2).toMillis()); highUsageBatteryDraining = mParser.getInt(KEY_HIGH_USAGE_BATTERY_DRAINING, 25); appRestrictionEnabled = mParser.getBoolean(KEY_APP_RESTRICTION_ENABLED, true); + appRestrictionActiveHour = mParser.getInt(KEY_APP_RESTRICTION_ACTIVE_HOUR, 24); reducedBatteryEnabled = mParser.getBoolean(KEY_REDUCED_BATTERY_ENABLED, false); reducedBatteryPercent = mParser.getInt(KEY_REDUCED_BATTERY_PERCENT, 50); lowBatteryEnabled = mParser.getBoolean(KEY_LOW_BATTERY_ENABLED, true); diff --git a/src/com/android/settings/fuelgauge/batterytip/BatteryTipPreferenceController.java b/src/com/android/settings/fuelgauge/batterytip/BatteryTipPreferenceController.java index 21fc132259..d615db6db1 100644 --- a/src/com/android/settings/fuelgauge/batterytip/BatteryTipPreferenceController.java +++ b/src/com/android/settings/fuelgauge/batterytip/BatteryTipPreferenceController.java @@ -18,23 +18,22 @@ package com.android.settings.fuelgauge.batterytip; import android.content.Context; import android.os.Bundle; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; -import androidx.preference.PreferenceGroup; import androidx.preference.PreferenceScreen; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.SettingsActivity; import com.android.settings.core.BasePreferenceController; import com.android.settings.core.InstrumentedPreferenceFragment; -import com.android.settings.fuelgauge.Estimate; import com.android.settings.fuelgauge.batterytip.actions.BatteryTipAction; import com.android.settings.fuelgauge.batterytip.tips.BatteryTip; import com.android.settings.fuelgauge.batterytip.tips.SummaryTip; import com.android.settings.overlay.FeatureFactory; +import com.android.settings.widget.CardPreference; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; +import com.android.settingslib.fuelgauge.EstimateKt; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -43,6 +42,9 @@ import java.util.Map; * Controller in charge of the battery tip group */ public class BatteryTipPreferenceController extends BasePreferenceController { + + public static final String PREF_NAME = "battery_tip"; + private static final String TAG = "BatteryTipPreferenceController"; private static final int REQUEST_ANOMALY_ACTION = 0; private static final String KEY_BATTERY_TIPS = "key_battery_tips"; @@ -54,42 +56,45 @@ public class BatteryTipPreferenceController extends BasePreferenceController { private MetricsFeatureProvider mMetricsFeatureProvider; private boolean mNeedUpdate; @VisibleForTesting - PreferenceGroup mPreferenceGroup; + CardPreference mCardPreference; @VisibleForTesting Context mPrefContext; InstrumentedPreferenceFragment mFragment; public BatteryTipPreferenceController(Context context, String preferenceKey) { - this(context, preferenceKey, null, null, null); - } - - public BatteryTipPreferenceController(Context context, String preferenceKey, - SettingsActivity settingsActivity, InstrumentedPreferenceFragment fragment, - BatteryTipListener batteryTipListener) { super(context, preferenceKey); - mBatteryTipListener = batteryTipListener; mBatteryTipMap = new HashMap<>(); - mFragment = fragment; - mSettingsActivity = settingsActivity; mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider(); mNeedUpdate = true; } + public void setActivity(SettingsActivity activity) { + mSettingsActivity = activity; + } + + public void setFragment(InstrumentedPreferenceFragment fragment) { + mFragment = fragment; + } + + public void setBatteryTipListener(BatteryTipListener lsn) { + mBatteryTipListener = lsn; + } + @Override public int getAvailabilityStatus() { - return AVAILABLE; + return AVAILABLE_UNSEARCHABLE; } @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); mPrefContext = screen.getContext(); - mPreferenceGroup = (PreferenceGroup) screen.findPreference(getPreferenceKey()); + mCardPreference = screen.findPreference(getPreferenceKey()); // Add summary tip in advance to avoid UI flakiness final SummaryTip summaryTip = new SummaryTip(BatteryTip.StateType.NEW, - Estimate.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN); - mPreferenceGroup.addPreference(summaryTip.buildPreference(mPrefContext)); + EstimateKt.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN); + summaryTip.updatePreference(mCardPreference); } public void updateBatteryTips(List<BatteryTip> batteryTips) { @@ -105,13 +110,12 @@ public class BatteryTipPreferenceController extends BasePreferenceController { } } - mPreferenceGroup.removeAll(); for (int i = 0, size = batteryTips.size(); i < size; i++) { final BatteryTip batteryTip = mBatteryTips.get(i); + batteryTip.sanityCheck(mContext); if (batteryTip.getState() != BatteryTip.StateType.INVISIBLE) { - final Preference preference = batteryTip.buildPreference(mPrefContext); - mBatteryTipMap.put(preference.getKey(), batteryTip); - mPreferenceGroup.addPreference(preference); + batteryTip.updatePreference(mCardPreference); + mBatteryTipMap.put(mCardPreference.getKey(), batteryTip); batteryTip.log(mContext, mMetricsFeatureProvider); mNeedUpdate = batteryTip.needUpdate(); break; diff --git a/src/com/android/settings/fuelgauge/batterytip/BatteryTipUtils.java b/src/com/android/settings/fuelgauge/batterytip/BatteryTipUtils.java index da2d81f1ff..ed06cce59d 100644 --- a/src/com/android/settings/fuelgauge/batterytip/BatteryTipUtils.java +++ b/src/com/android/settings/fuelgauge/batterytip/BatteryTipUtils.java @@ -23,6 +23,7 @@ import android.content.Context; import android.content.Intent; import android.os.UserHandle; import android.os.UserManager; + import androidx.annotation.NonNull; import com.android.internal.util.CollectionUtils; @@ -35,6 +36,8 @@ import com.android.settings.fuelgauge.batterytip.actions.OpenRestrictAppFragment import com.android.settings.fuelgauge.batterytip.actions.RestrictAppAction; import com.android.settings.fuelgauge.batterytip.actions.SmartBatteryAction; import com.android.settings.fuelgauge.batterytip.actions.UnrestrictAppAction; +import com.android.settings.fuelgauge.batterytip.tips.AppLabelPredicate; +import com.android.settings.fuelgauge.batterytip.tips.AppRestrictionPredicate; import com.android.settings.fuelgauge.batterytip.tips.BatteryTip; import com.android.settings.fuelgauge.batterytip.tips.RestrictAppTip; import com.android.settings.fuelgauge.batterytip.tips.UnrestrictAppTip; @@ -125,4 +128,17 @@ public class BatteryTipUtils { statsManager.setBroadcastSubscriber(pendingIntent, StatsManagerConfig.ANOMALY_CONFIG_KEY, StatsManagerConfig.SUBSCRIBER_ID); } + + /** + * Detect and return anomaly apps after {@code timeAfterMs} + */ + public static List<AppInfo> detectAnomalies(Context context, long timeAfterMs) { + final List<AppInfo> highUsageApps = BatteryDatabaseManager.getInstance(context) + .queryAllAnomalies(timeAfterMs, AnomalyDatabaseHelper.State.NEW); + // Remove it if it doesn't have label or been restricted + highUsageApps.removeIf(AppLabelPredicate.getInstance(context) + .or(AppRestrictionPredicate.getInstance(context))); + + return highUsageApps; + } } diff --git a/src/com/android/settings/fuelgauge/batterytip/HighUsageAdapter.java b/src/com/android/settings/fuelgauge/batterytip/HighUsageAdapter.java index b1866b9d22..cd79ea04f2 100644 --- a/src/com/android/settings/fuelgauge/batterytip/HighUsageAdapter.java +++ b/src/com/android/settings/fuelgauge/batterytip/HighUsageAdapter.java @@ -19,7 +19,6 @@ package com.android.settings.fuelgauge.batterytip; import android.content.Context; import android.content.pm.PackageManager; import android.os.UserHandle; -import androidx.recyclerview.widget.RecyclerView; import android.util.IconDrawableFactory; import android.view.LayoutInflater; import android.view.View; @@ -27,10 +26,12 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; +import androidx.recyclerview.widget.RecyclerView; + import com.android.settings.R; import com.android.settings.Utils; - import com.android.settingslib.utils.StringUtil; + import java.util.List; /** @@ -78,9 +79,6 @@ public class HighUsageAdapter extends RecyclerView.Adapter<HighUsageAdapter.View Utils.getBadgedIcon(mIconDrawableFactory, mPackageManager, app.packageName, UserHandle.getUserId(app.uid))); holder.appName.setText(Utils.getApplicationLabel(mContext, app.packageName)); - if (app.screenOnTimeMs != 0) { - holder.appTime.setText(StringUtil.formatElapsedTime(mContext, app.screenOnTimeMs, false)); - } } @Override diff --git a/src/com/android/settings/fuelgauge/batterytip/StatsManagerConfig.java b/src/com/android/settings/fuelgauge/batterytip/StatsManagerConfig.java index d7bde786c9..153aa40869 100644 --- a/src/com/android/settings/fuelgauge/batterytip/StatsManagerConfig.java +++ b/src/com/android/settings/fuelgauge/batterytip/StatsManagerConfig.java @@ -18,9 +18,6 @@ package com.android.settings.fuelgauge.batterytip; import androidx.annotation.IntDef; -import com.google.common.hash.HashFunction; -import com.google.common.hash.Hashing; - import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -30,7 +27,7 @@ import java.lang.annotation.RetentionPolicy; public class StatsManagerConfig { /** * The key that represents the anomaly config. - * This value is used in {@link android.app.StatsManager#addConfiguration(long, byte[])} + * This value is used in {@link android.app.StatsManager#addConfig(long, byte[])} */ public static final long ANOMALY_CONFIG_KEY = 1; diff --git a/src/com/android/settings/fuelgauge/batterytip/actions/BatterySaverAction.java b/src/com/android/settings/fuelgauge/batterytip/actions/BatterySaverAction.java index 6eff47a6b4..0a92215aea 100644 --- a/src/com/android/settings/fuelgauge/batterytip/actions/BatterySaverAction.java +++ b/src/com/android/settings/fuelgauge/batterytip/actions/BatterySaverAction.java @@ -16,9 +16,9 @@ package com.android.settings.fuelgauge.batterytip.actions; +import android.app.settings.SettingsEnums; import android.content.Context; -import com.android.internal.logging.nano.MetricsProto; import com.android.settingslib.fuelgauge.BatterySaverUtils; public class BatterySaverAction extends BatteryTipAction { @@ -33,6 +33,6 @@ public class BatterySaverAction extends BatteryTipAction { public void handlePositiveAction(int metricsKey) { BatterySaverUtils.setPowerSaveMode(mContext, true, /*needFirstTimeWarning*/ true); mMetricsFeatureProvider.action(mContext, - MetricsProto.MetricsEvent.ACTION_TIP_TURN_ON_BATTERY_SAVER, metricsKey); + SettingsEnums.ACTION_TIP_TURN_ON_BATTERY_SAVER, metricsKey); } } diff --git a/src/com/android/settings/fuelgauge/batterytip/actions/OpenBatterySaverAction.java b/src/com/android/settings/fuelgauge/batterytip/actions/OpenBatterySaverAction.java index 9f9a99d0f5..1bc2ebfe56 100644 --- a/src/com/android/settings/fuelgauge/batterytip/actions/OpenBatterySaverAction.java +++ b/src/com/android/settings/fuelgauge/batterytip/actions/OpenBatterySaverAction.java @@ -16,20 +16,11 @@ package com.android.settings.fuelgauge.batterytip.actions; +import android.app.settings.SettingsEnums; import android.content.Context; -import com.android.internal.logging.nano.MetricsProto; -import com.android.settings.R; -import com.android.settings.SettingsActivity; -import com.android.settings.core.InstrumentedPreferenceFragment; import com.android.settings.core.SubSettingLauncher; -import com.android.settings.fuelgauge.BatteryUtils; -import com.android.settings.fuelgauge.RestrictedAppDetails; import com.android.settings.fuelgauge.batterysaver.BatterySaverSettings; -import com.android.settings.fuelgauge.batterytip.AppInfo; -import com.android.settings.fuelgauge.batterytip.tips.RestrictAppTip; - -import java.util.List; /** * @@ -47,7 +38,7 @@ public class OpenBatterySaverAction extends BatteryTipAction { @Override public void handlePositiveAction(int metricsKey) { mMetricsFeatureProvider.action(mContext, - MetricsProto.MetricsEvent.ACTION_TIP_OPEN_BATTERY_SAVER_PAGE, metricsKey); + SettingsEnums.ACTION_TIP_OPEN_BATTERY_SAVER_PAGE, metricsKey); new SubSettingLauncher(mContext) .setDestination(BatterySaverSettings.class.getName()) .setSourceMetricsCategory(metricsKey) diff --git a/src/com/android/settings/fuelgauge/batterytip/actions/OpenRestrictAppFragmentAction.java b/src/com/android/settings/fuelgauge/batterytip/actions/OpenRestrictAppFragmentAction.java index 9d35615840..f0d9f28e8d 100644 --- a/src/com/android/settings/fuelgauge/batterytip/actions/OpenRestrictAppFragmentAction.java +++ b/src/com/android/settings/fuelgauge/batterytip/actions/OpenRestrictAppFragmentAction.java @@ -16,9 +16,10 @@ package com.android.settings.fuelgauge.batterytip.actions; +import android.app.settings.SettingsEnums; + import androidx.annotation.VisibleForTesting; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.core.InstrumentedPreferenceFragment; import com.android.settings.fuelgauge.RestrictedAppDetails; import com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper; @@ -52,7 +53,7 @@ public class OpenRestrictAppFragmentAction extends BatteryTipAction { @Override public void handlePositiveAction(int metricsKey) { mMetricsFeatureProvider.action(mContext, - MetricsProto.MetricsEvent.ACTION_TIP_OPEN_APP_RESTRICTION_PAGE, metricsKey); + SettingsEnums.ACTION_TIP_OPEN_APP_RESTRICTION_PAGE, metricsKey); final List<AppInfo> mAppInfos = mRestrictAppTip.getRestrictAppList(); RestrictedAppDetails.startRestrictedAppDetails(mFragment, mAppInfos); diff --git a/src/com/android/settings/fuelgauge/batterytip/actions/RestrictAppAction.java b/src/com/android/settings/fuelgauge/batterytip/actions/RestrictAppAction.java index 59b5c15b05..33ac5df4a4 100644 --- a/src/com/android/settings/fuelgauge/batterytip/actions/RestrictAppAction.java +++ b/src/com/android/settings/fuelgauge/batterytip/actions/RestrictAppAction.java @@ -17,11 +17,11 @@ package com.android.settings.fuelgauge.batterytip.actions; import android.app.AppOpsManager; +import android.app.settings.SettingsEnums; import android.content.Context; + import androidx.annotation.VisibleForTesting; -import android.util.Pair; -import com.android.internal.logging.nano.MetricsProto; import com.android.internal.util.CollectionUtils; import com.android.settings.fuelgauge.BatteryUtils; import com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper; @@ -63,15 +63,18 @@ public class RestrictAppAction extends BatteryTipAction { AppOpsManager.MODE_IGNORED); if (CollectionUtils.isEmpty(appInfo.anomalyTypes)) { // Only log context if there is no anomaly type - mMetricsFeatureProvider.action(mContext, - MetricsProto.MetricsEvent.ACTION_TIP_RESTRICT_APP, packageName, - Pair.create(MetricsProto.MetricsEvent.FIELD_CONTEXT, metricsKey)); + mMetricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, + SettingsEnums.ACTION_TIP_RESTRICT_APP, + metricsKey, + packageName, + 0); } else { for (int type : appInfo.anomalyTypes) { - mMetricsFeatureProvider.action(mContext, - MetricsProto.MetricsEvent.ACTION_TIP_RESTRICT_APP, packageName, - Pair.create(MetricsProto.MetricsEvent.FIELD_CONTEXT, metricsKey), - Pair.create(MetricsProto.MetricsEvent.FIELD_ANOMALY_TYPE, type)); + mMetricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, + SettingsEnums.ACTION_TIP_RESTRICT_APP, + metricsKey, + packageName, + type); } } } diff --git a/src/com/android/settings/fuelgauge/batterytip/actions/SmartBatteryAction.java b/src/com/android/settings/fuelgauge/batterytip/actions/SmartBatteryAction.java index 48925917c0..f6b9c94dfd 100644 --- a/src/com/android/settings/fuelgauge/batterytip/actions/SmartBatteryAction.java +++ b/src/com/android/settings/fuelgauge/batterytip/actions/SmartBatteryAction.java @@ -16,9 +16,10 @@ package com.android.settings.fuelgauge.batterytip.actions; -import android.app.Fragment; +import android.app.settings.SettingsEnums; + +import androidx.fragment.app.Fragment; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.core.SubSettingLauncher; @@ -41,13 +42,13 @@ public class SmartBatteryAction extends BatteryTipAction { @Override public void handlePositiveAction(int metricsKey) { mMetricsFeatureProvider.action(mContext, - MetricsProto.MetricsEvent.ACTION_TIP_OPEN_SMART_BATTERY, metricsKey); + SettingsEnums.ACTION_TIP_OPEN_SMART_BATTERY, metricsKey); new SubSettingLauncher(mSettingsActivity) .setSourceMetricsCategory(mFragment instanceof Instrumentable ? ((Instrumentable) mFragment).getMetricsCategory() : Instrumentable.METRICS_CATEGORY_UNKNOWN) .setDestination(SmartBatterySettings.class.getName()) - .setTitle(R.string.smart_battery_manager_title) + .setTitleRes(R.string.smart_battery_manager_title) .launch(); } diff --git a/src/com/android/settings/fuelgauge/batterytip/actions/UnrestrictAppAction.java b/src/com/android/settings/fuelgauge/batterytip/actions/UnrestrictAppAction.java index ea40107d62..0ef2c0f13b 100644 --- a/src/com/android/settings/fuelgauge/batterytip/actions/UnrestrictAppAction.java +++ b/src/com/android/settings/fuelgauge/batterytip/actions/UnrestrictAppAction.java @@ -17,11 +17,11 @@ package com.android.settings.fuelgauge.batterytip.actions; import android.app.AppOpsManager; +import android.app.settings.SettingsEnums; import android.content.Context; + import androidx.annotation.VisibleForTesting; -import android.util.Pair; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.fuelgauge.BatteryUtils; import com.android.settings.fuelgauge.batterytip.AppInfo; import com.android.settings.fuelgauge.batterytip.tips.UnrestrictAppTip; @@ -49,8 +49,11 @@ public class UnrestrictAppAction extends BatteryTipAction { // Clear force app standby, then app can run in the background mBatteryUtils.setForceAppStandby(appInfo.uid, appInfo.packageName, AppOpsManager.MODE_ALLOWED); - mMetricsFeatureProvider.action(mContext, - MetricsProto.MetricsEvent.ACTION_TIP_UNRESTRICT_APP, appInfo.packageName, - Pair.create(MetricsProto.MetricsEvent.FIELD_CONTEXT, metricsKey)); + mMetricsFeatureProvider.action( + SettingsEnums.PAGE_UNKNOWN, + SettingsEnums.ACTION_TIP_UNRESTRICT_APP, + metricsKey, + appInfo.packageName, + 0); } } diff --git a/src/com/android/settings/fuelgauge/batterytip/detectors/HighUsageDetector.java b/src/com/android/settings/fuelgauge/batterytip/detectors/HighUsageDetector.java index 664a8ca202..067046ca85 100644 --- a/src/com/android/settings/fuelgauge/batterytip/detectors/HighUsageDetector.java +++ b/src/com/android/settings/fuelgauge/batterytip/detectors/HighUsageDetector.java @@ -16,17 +16,19 @@ package com.android.settings.fuelgauge.batterytip.detectors; +import static com.android.settings.Utils.SETTINGS_PACKAGE_NAME; + import android.content.Context; import android.os.BatteryStats; + import androidx.annotation.VisibleForTesting; -import android.text.format.DateUtils; import com.android.internal.os.BatterySipper; import com.android.internal.os.BatteryStatsHelper; +import com.android.settings.fuelgauge.BatteryInfo; import com.android.settings.fuelgauge.BatteryUtils; -import com.android.settings.fuelgauge.batterytip.BatteryTipPolicy; import com.android.settings.fuelgauge.batterytip.AppInfo; -import com.android.settings.fuelgauge.BatteryInfo; +import com.android.settings.fuelgauge.batterytip.BatteryTipPolicy; import com.android.settings.fuelgauge.batterytip.HighUsageDataParser; import com.android.settings.fuelgauge.batterytip.tips.BatteryTip; import com.android.settings.fuelgauge.batterytip.tips.HighUsageTip; @@ -69,35 +71,42 @@ public class HighUsageDetector implements BatteryTipDetector { if (mPolicy.highUsageEnabled && mDischarging) { parseBatteryData(); if (mDataParser.isDeviceHeavilyUsed() || mPolicy.testHighUsageTip) { - final List<BatterySipper> batterySippers = mBatteryStatsHelper.getUsageList(); - for (int i = 0, size = batterySippers.size(); i < size; i++) { - final BatterySipper batterySipper = batterySippers.get(i); - if (!mBatteryUtils.shouldHideSipper(batterySipper)) { - final long foregroundTimeMs = mBatteryUtils.getProcessTimeMs( - BatteryUtils.StatusType.FOREGROUND, batterySipper.uidObj, - BatteryStats.STATS_SINCE_CHARGED); - if (foregroundTimeMs >= DateUtils.MINUTE_IN_MILLIS) { - mHighUsageAppList.add(new AppInfo.Builder() - .setUid(batterySipper.getUid()) - .setPackageName( - mBatteryUtils.getPackageName(batterySipper.getUid())) - .setScreenOnTimeMs(foregroundTimeMs) - .build()); - } + final BatteryStats batteryStats = mBatteryStatsHelper.getStats(); + final List<BatterySipper> batterySippers + = new ArrayList<>(mBatteryStatsHelper.getUsageList()); + final double totalPower = mBatteryStatsHelper.getTotalPower(); + final int dischargeAmount = batteryStats != null + ? batteryStats.getDischargeAmount(BatteryStats.STATS_SINCE_CHARGED) + : 0; + + Collections.sort(batterySippers, + (sipper1, sipper2) -> Double.compare(sipper2.totalSmearedPowerMah, + sipper1.totalSmearedPowerMah)); + for (BatterySipper batterySipper : batterySippers) { + final double percent = mBatteryUtils.calculateBatteryPercent( + batterySipper.totalSmearedPowerMah, totalPower, 0, dischargeAmount); + if ((percent + 0.5f < 1f) || mBatteryUtils.shouldHideSipper(batterySipper)) { + // Don't show it if we should hide or usage percentage is lower than 1% + continue; + } + mHighUsageAppList.add(new AppInfo.Builder() + .setUid(batterySipper.getUid()) + .setPackageName( + mBatteryUtils.getPackageName(batterySipper.getUid())) + .build()); + if (mHighUsageAppList.size() >= mPolicy.highUsageAppCount) { + break; } + } // When in test mode, add an app if necessary if (mPolicy.testHighUsageTip && mHighUsageAppList.isEmpty()) { mHighUsageAppList.add(new AppInfo.Builder() - .setPackageName("com.android.settings") + .setPackageName(SETTINGS_PACKAGE_NAME) .setScreenOnTimeMs(TimeUnit.HOURS.toMillis(3)) .build()); } - - Collections.sort(mHighUsageAppList, Collections.reverseOrder()); - mHighUsageAppList = mHighUsageAppList.subList(0, - Math.min(mPolicy.highUsageAppCount, mHighUsageAppList.size())); } } diff --git a/src/com/android/settings/fuelgauge/batterytip/detectors/LowBatteryDetector.java b/src/com/android/settings/fuelgauge/batterytip/detectors/LowBatteryDetector.java index c3f9b07eaa..ca9141d0e7 100644 --- a/src/com/android/settings/fuelgauge/batterytip/detectors/LowBatteryDetector.java +++ b/src/com/android/settings/fuelgauge/batterytip/detectors/LowBatteryDetector.java @@ -47,7 +47,7 @@ public class LowBatteryDetector implements BatteryTipDetector { public BatteryTip detect() { final boolean powerSaveModeOn = mPowerManager.isPowerSaveMode(); final boolean lowBattery = mBatteryInfo.batteryLevel <= mWarningLevel - || (mBatteryInfo.discharging + || (mBatteryInfo.discharging && mBatteryInfo.remainingTimeUs != 0 && mBatteryInfo.remainingTimeUs < TimeUnit.HOURS.toMicros(mPolicy.lowBatteryHour)); int state = BatteryTip.StateType.INVISIBLE; @@ -62,6 +62,6 @@ public class LowBatteryDetector implements BatteryTipDetector { } return new LowBatteryTip( - state, powerSaveModeOn, mBatteryInfo.remainingLabel); + state, powerSaveModeOn, mBatteryInfo.suggestionLabel); } } diff --git a/src/com/android/settings/fuelgauge/batterytip/detectors/RestrictAppDetector.java b/src/com/android/settings/fuelgauge/batterytip/detectors/RestrictAppDetector.java index 3ff0989d08..70ae0ec9ea 100644 --- a/src/com/android/settings/fuelgauge/batterytip/detectors/RestrictAppDetector.java +++ b/src/com/android/settings/fuelgauge/batterytip/detectors/RestrictAppDetector.java @@ -16,14 +16,17 @@ package com.android.settings.fuelgauge.batterytip.detectors; +import static com.android.settings.Utils.SETTINGS_PACKAGE_NAME; + import android.content.Context; + import androidx.annotation.VisibleForTesting; -import android.text.format.DateUtils; import com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper; import com.android.settings.fuelgauge.batterytip.AppInfo; import com.android.settings.fuelgauge.batterytip.BatteryDatabaseManager; import com.android.settings.fuelgauge.batterytip.BatteryTipPolicy; +import com.android.settings.fuelgauge.batterytip.BatteryTipUtils; import com.android.settings.fuelgauge.batterytip.tips.AppLabelPredicate; import com.android.settings.fuelgauge.batterytip.tips.AppRestrictionPredicate; import com.android.settings.fuelgauge.batterytip.tips.BatteryTip; @@ -31,6 +34,7 @@ import com.android.settings.fuelgauge.batterytip.tips.RestrictAppTip; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeUnit; /** * Detector whether to show summary tip. This detector should be executed as the last @@ -51,8 +55,8 @@ public class RestrictAppDetector implements BatteryTipDetector { mContext = context; mPolicy = policy; mBatteryDatabaseManager = BatteryDatabaseManager.getInstance(context); - mAppRestrictionPredicate = new AppRestrictionPredicate(context); - mAppLabelPredicate = new AppLabelPredicate(context); + mAppRestrictionPredicate = AppRestrictionPredicate.getInstance(context); + mAppLabelPredicate = AppLabelPredicate.getInstance(context); } @Override @@ -61,12 +65,10 @@ public class RestrictAppDetector implements BatteryTipDetector { return getFakeData(); } if (mPolicy.appRestrictionEnabled) { - // TODO(b/72385333): hook up the query timestamp to server side - final long oneDayBeforeMs = System.currentTimeMillis() - DateUtils.DAY_IN_MILLIS; - final List<AppInfo> highUsageApps = mBatteryDatabaseManager.queryAllAnomalies( - oneDayBeforeMs, AnomalyDatabaseHelper.State.NEW); - // Remove it if it doesn't have label or been restricted - highUsageApps.removeIf(mAppLabelPredicate.or(mAppRestrictionPredicate)); + final long oneDayBeforeMs = System.currentTimeMillis() + - TimeUnit.HOURS.toMillis(mPolicy.appRestrictionActiveHour); + final List<AppInfo> highUsageApps = BatteryTipUtils.detectAnomalies(mContext, + oneDayBeforeMs); if (!highUsageApps.isEmpty()) { // If there are new anomalies, show them return new RestrictAppTip(BatteryTip.StateType.NEW, highUsageApps); @@ -87,7 +89,7 @@ public class RestrictAppDetector implements BatteryTipDetector { private BatteryTip getFakeData() { final List<AppInfo> highUsageApps = new ArrayList<>(); highUsageApps.add(new AppInfo.Builder() - .setPackageName("com.android.settings") + .setPackageName(SETTINGS_PACKAGE_NAME) .build()); return new RestrictAppTip(BatteryTip.StateType.NEW, highUsageApps); } diff --git a/src/com/android/settings/fuelgauge/batterytip/tips/AppLabelPredicate.java b/src/com/android/settings/fuelgauge/batterytip/tips/AppLabelPredicate.java index 13a2452bb2..1444b12b4e 100644 --- a/src/com/android/settings/fuelgauge/batterytip/tips/AppLabelPredicate.java +++ b/src/com/android/settings/fuelgauge/batterytip/tips/AppLabelPredicate.java @@ -16,7 +16,6 @@ package com.android.settings.fuelgauge.batterytip.tips; -import android.app.AppOpsManager; import android.content.Context; import com.android.settings.Utils; @@ -28,12 +27,20 @@ import java.util.function.Predicate; * {@link Predicate} for {@link AppInfo} to check whether it has label */ public class AppLabelPredicate implements Predicate<AppInfo> { + + private static AppLabelPredicate sInstance; private Context mContext; - private AppOpsManager mAppOpsManager; - public AppLabelPredicate(Context context) { + public static AppLabelPredicate getInstance(Context context) { + if (sInstance == null) { + sInstance = new AppLabelPredicate(context.getApplicationContext()); + } + + return sInstance; + } + + private AppLabelPredicate(Context context) { mContext = context; - mAppOpsManager = context.getSystemService(AppOpsManager.class); } @Override diff --git a/src/com/android/settings/fuelgauge/batterytip/tips/AppRestrictionPredicate.java b/src/com/android/settings/fuelgauge/batterytip/tips/AppRestrictionPredicate.java index 21bdbf1081..43a4d900e2 100644 --- a/src/com/android/settings/fuelgauge/batterytip/tips/AppRestrictionPredicate.java +++ b/src/com/android/settings/fuelgauge/batterytip/tips/AppRestrictionPredicate.java @@ -19,7 +19,6 @@ package com.android.settings.fuelgauge.batterytip.tips; import android.app.AppOpsManager; import android.content.Context; -import com.android.settings.Utils; import com.android.settings.fuelgauge.batterytip.AppInfo; import java.util.function.Predicate; @@ -28,9 +27,19 @@ import java.util.function.Predicate; * {@link Predicate} for {@link AppInfo} to check whether it is restricted. */ public class AppRestrictionPredicate implements Predicate<AppInfo> { + + private static AppRestrictionPredicate sInstance; private AppOpsManager mAppOpsManager; - public AppRestrictionPredicate(Context context) { + public static AppRestrictionPredicate getInstance(Context context) { + if (sInstance == null) { + sInstance = new AppRestrictionPredicate(context.getApplicationContext()); + } + + return sInstance; + } + + private AppRestrictionPredicate(Context context) { mAppOpsManager = context.getSystemService(AppOpsManager.class); } diff --git a/src/com/android/settings/fuelgauge/batterytip/tips/BatteryTip.java b/src/com/android/settings/fuelgauge/batterytip/tips/BatteryTip.java index b1a0d7c6b2..ebc493967e 100644 --- a/src/com/android/settings/fuelgauge/batterytip/tips/BatteryTip.java +++ b/src/com/android/settings/fuelgauge/batterytip/tips/BatteryTip.java @@ -17,13 +17,16 @@ package com.android.settings.fuelgauge.batterytip.tips; import android.content.Context; +import android.content.res.ColorStateList; import android.os.Parcel; import android.os.Parcelable; +import android.util.SparseIntArray; +import android.view.View; + import androidx.annotation.IdRes; import androidx.annotation.IntDef; import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; -import android.util.SparseIntArray; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; @@ -133,18 +136,32 @@ public abstract class BatteryTip implements Comparable<BatteryTip>, Parcelable { public abstract void updateState(BatteryTip tip); /** + * Check whether data is still make sense. If not, try recover. + * @param context used to do sanity check + */ + public void sanityCheck(Context context) { + // do nothing + } + + /** * Log the battery tip */ public abstract void log(Context context, MetricsFeatureProvider metricsFeatureProvider); - public Preference buildPreference(Context context) { - Preference preference = new Preference(context); - - preference.setKey(getKey()); + public void updatePreference(Preference preference) { + final Context context = preference.getContext(); preference.setTitle(getTitle(context)); preference.setSummary(getSummary(context)); preference.setIcon(getIconId()); - return preference; + @IdRes int iconTintColorId = getIconTintColorId(); + if (iconTintColorId != View.NO_ID) { + preference.getIcon().setTint(context.getColor(iconTintColorId)); + } + } + + /** Returns the color resid for tinting {@link #getIconId()} or {@link View#NO_ID} if none. */ + public @IdRes int getIconTintColorId() { + return View.NO_ID; } public boolean shouldShowDialog() { diff --git a/src/com/android/settings/fuelgauge/batterytip/tips/EarlyWarningTip.java b/src/com/android/settings/fuelgauge/batterytip/tips/EarlyWarningTip.java index 28065fd8db..0c2bb03b97 100644 --- a/src/com/android/settings/fuelgauge/batterytip/tips/EarlyWarningTip.java +++ b/src/com/android/settings/fuelgauge/batterytip/tips/EarlyWarningTip.java @@ -16,10 +16,11 @@ package com.android.settings.fuelgauge.batterytip.tips; +import android.app.settings.SettingsEnums; import android.content.Context; +import android.content.res.ColorStateList; import android.os.Parcel; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; @@ -63,6 +64,13 @@ public class EarlyWarningTip extends BatteryTip { } @Override + public int getIconTintColorId() { + return mState == StateType.HANDLED + ? R.color.battery_maybe_color_light + : R.color.battery_bad_color_light; + } + + @Override public void updateState(BatteryTip tip) { final EarlyWarningTip earlyWarningTip = (EarlyWarningTip) tip; if (earlyWarningTip.mState == StateType.NEW) { @@ -79,7 +87,7 @@ public class EarlyWarningTip extends BatteryTip { @Override public void log(Context context, MetricsFeatureProvider metricsFeatureProvider) { - metricsFeatureProvider.action(context, MetricsProto.MetricsEvent.ACTION_EARLY_WARNING_TIP, + metricsFeatureProvider.action(context, SettingsEnums.ACTION_EARLY_WARNING_TIP, mState); } diff --git a/src/com/android/settings/fuelgauge/batterytip/tips/HighUsageTip.java b/src/com/android/settings/fuelgauge/batterytip/tips/HighUsageTip.java index 2b8ad231c1..0a7b1ab35b 100644 --- a/src/com/android/settings/fuelgauge/batterytip/tips/HighUsageTip.java +++ b/src/com/android/settings/fuelgauge/batterytip/tips/HighUsageTip.java @@ -16,15 +16,15 @@ package com.android.settings.fuelgauge.batterytip.tips; +import android.app.settings.SettingsEnums; import android.content.Context; import android.os.Parcel; import android.os.Parcelable; + import androidx.annotation.VisibleForTesting; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.fuelgauge.batterytip.AppInfo; - import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; import java.util.List; @@ -81,12 +81,12 @@ public class HighUsageTip extends BatteryTip { @Override public void log(Context context, MetricsFeatureProvider metricsFeatureProvider) { - metricsFeatureProvider.action(context, MetricsProto.MetricsEvent.ACTION_HIGH_USAGE_TIP, + metricsFeatureProvider.action(context, SettingsEnums.ACTION_HIGH_USAGE_TIP, mState); for (int i = 0, size = mHighUsageAppList.size(); i < size; i++) { final AppInfo appInfo = mHighUsageAppList.get(i); metricsFeatureProvider.action(context, - MetricsProto.MetricsEvent.ACTION_HIGH_USAGE_TIP_LIST, + SettingsEnums.ACTION_HIGH_USAGE_TIP_LIST, appInfo.packageName); } } diff --git a/src/com/android/settings/fuelgauge/batterytip/tips/LowBatteryTip.java b/src/com/android/settings/fuelgauge/batterytip/tips/LowBatteryTip.java index b48a7dd468..d7acdd29fc 100644 --- a/src/com/android/settings/fuelgauge/batterytip/tips/LowBatteryTip.java +++ b/src/com/android/settings/fuelgauge/batterytip/tips/LowBatteryTip.java @@ -16,11 +16,11 @@ package com.android.settings.fuelgauge.batterytip.tips; +import android.app.settings.SettingsEnums; import android.content.Context; import android.os.Parcel; import android.os.Parcelable; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; @@ -55,7 +55,7 @@ public class LowBatteryTip extends EarlyWarningTip { @Override public void log(Context context, MetricsFeatureProvider metricsFeatureProvider) { - metricsFeatureProvider.action(context, MetricsProto.MetricsEvent.ACTION_LOW_BATTERY_TIP, + metricsFeatureProvider.action(context, SettingsEnums.ACTION_LOW_BATTERY_TIP, mState); } diff --git a/src/com/android/settings/fuelgauge/batterytip/tips/RestrictAppTip.java b/src/com/android/settings/fuelgauge/batterytip/tips/RestrictAppTip.java index c2e45b19c4..1bd760b6da 100644 --- a/src/com/android/settings/fuelgauge/batterytip/tips/RestrictAppTip.java +++ b/src/com/android/settings/fuelgauge/batterytip/tips/RestrictAppTip.java @@ -16,15 +16,14 @@ package com.android.settings.fuelgauge.batterytip.tips; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.res.Resources; import android.icu.text.ListFormatter; import android.os.Parcel; -import android.text.TextUtils; -import android.util.Pair; -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.logging.nano.MetricsProto; +import androidx.annotation.VisibleForTesting; + import com.android.settings.R; import com.android.settings.Utils; import com.android.settings.fuelgauge.batterytip.AppInfo; @@ -108,19 +107,30 @@ public class RestrictAppTip extends BatteryTip { } @Override + public void sanityCheck(Context context) { + super.sanityCheck(context); + + // Set it invisible if there is no valid app + mRestrictAppList.removeIf(AppLabelPredicate.getInstance(context)); + if (mRestrictAppList.isEmpty()) { + mState = StateType.INVISIBLE; + } + } + + @Override public void log(Context context, MetricsFeatureProvider metricsFeatureProvider) { - metricsFeatureProvider.action(context, MetricsProto.MetricsEvent.ACTION_APP_RESTRICTION_TIP, + metricsFeatureProvider.action(context, SettingsEnums.ACTION_APP_RESTRICTION_TIP, mState); if (mState == StateType.NEW) { for (int i = 0, size = mRestrictAppList.size(); i < size; i++) { final AppInfo appInfo = mRestrictAppList.get(i); for (Integer anomalyType : appInfo.anomalyTypes) { - metricsFeatureProvider.action(context, - MetricsProto.MetricsEvent.ACTION_APP_RESTRICTION_TIP_LIST, + metricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, + SettingsEnums.ACTION_APP_RESTRICTION_TIP_LIST, + SettingsEnums.PAGE_UNKNOWN, appInfo.packageName, - Pair.create(MetricsProto.MetricsEvent.FIELD_ANOMALY_TYPE, anomalyType)); + anomalyType); } - } } } diff --git a/src/com/android/settings/fuelgauge/batterytip/tips/SmartBatteryTip.java b/src/com/android/settings/fuelgauge/batterytip/tips/SmartBatteryTip.java index 68c7d70be5..106766ff92 100644 --- a/src/com/android/settings/fuelgauge/batterytip/tips/SmartBatteryTip.java +++ b/src/com/android/settings/fuelgauge/batterytip/tips/SmartBatteryTip.java @@ -16,11 +16,10 @@ package com.android.settings.fuelgauge.batterytip.tips; +import android.app.settings.SettingsEnums; import android.content.Context; import android.os.Parcel; -import android.provider.Settings; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; @@ -59,7 +58,7 @@ public class SmartBatteryTip extends BatteryTip { @Override public void log(Context context, MetricsFeatureProvider metricsFeatureProvider) { - metricsFeatureProvider.action(context, MetricsProto.MetricsEvent.ACTION_SMART_BATTERY_TIP, + metricsFeatureProvider.action(context, SettingsEnums.ACTION_SMART_BATTERY_TIP, mState); } diff --git a/src/com/android/settings/fuelgauge/batterytip/tips/SummaryTip.java b/src/com/android/settings/fuelgauge/batterytip/tips/SummaryTip.java index 8d0624aa96..37122d8e82 100644 --- a/src/com/android/settings/fuelgauge/batterytip/tips/SummaryTip.java +++ b/src/com/android/settings/fuelgauge/batterytip/tips/SummaryTip.java @@ -16,12 +16,14 @@ package com.android.settings.fuelgauge.batterytip.tips; +import android.app.settings.SettingsEnums; import android.content.Context; +import android.content.res.ColorStateList; import android.os.Parcel; import android.os.Parcelable; + import androidx.annotation.VisibleForTesting; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; @@ -58,6 +60,11 @@ public class SummaryTip extends BatteryTip { } @Override + public int getIconTintColorId() { + return R.color.battery_good_color_light; + } + + @Override public void updateState(BatteryTip tip) { mState = tip.mState; } @@ -70,7 +77,7 @@ public class SummaryTip extends BatteryTip { @Override public void log(Context context, MetricsFeatureProvider metricsFeatureProvider) { - metricsFeatureProvider.action(context, MetricsProto.MetricsEvent.ACTION_SUMMARY_TIP, + metricsFeatureProvider.action(context, SettingsEnums.ACTION_SUMMARY_TIP, mState); } diff --git a/src/com/android/settings/fuelgauge/batterytip/tips/UnrestrictAppTip.java b/src/com/android/settings/fuelgauge/batterytip/tips/UnrestrictAppTip.java index 18ab3c2c05..a0e470f5c4 100644 --- a/src/com/android/settings/fuelgauge/batterytip/tips/UnrestrictAppTip.java +++ b/src/com/android/settings/fuelgauge/batterytip/tips/UnrestrictAppTip.java @@ -19,7 +19,8 @@ package com.android.settings.fuelgauge.batterytip.tips; import android.content.Context; import android.os.Parcel; -import com.android.internal.annotations.VisibleForTesting; +import androidx.annotation.VisibleForTesting; + import com.android.settings.fuelgauge.batterytip.AppInfo; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; diff --git a/src/com/android/settings/gestures/AssistGestureFeatureProviderImpl.java b/src/com/android/settings/gestures/AssistGestureFeatureProviderImpl.java index 187b1cabb4..11c6b22539 100644 --- a/src/com/android/settings/gestures/AssistGestureFeatureProviderImpl.java +++ b/src/com/android/settings/gestures/AssistGestureFeatureProviderImpl.java @@ -18,7 +18,6 @@ package com.android.settings.gestures; import android.content.Context; -import com.android.settings.R; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; diff --git a/src/com/android/settings/gestures/AssistGestureSettings.java b/src/com/android/settings/gestures/AssistGestureSettings.java index df2dd93a54..b67fd65928 100644 --- a/src/com/android/settings/gestures/AssistGestureSettings.java +++ b/src/com/android/settings/gestures/AssistGestureSettings.java @@ -16,28 +16,30 @@ package com.android.settings.gestures; +import android.app.settings.SettingsEnums; import android.content.Context; import android.provider.SearchIndexableResource; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.overlay.FeatureFactory; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +@SearchIndexable public class AssistGestureSettings extends DashboardFragment { private static final String TAG = "AssistGesture"; @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.SETTINGS_ASSIST_GESTURE; + return SettingsEnums.SETTINGS_ASSIST_GESTURE; } @Override @@ -52,7 +54,7 @@ public class AssistGestureSettings extends DashboardFragment { @Override protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { - return buildPreferenceControllers(context, getLifecycle()); + return buildPreferenceControllers(context, getSettingsLifecycle()); } private static List<AbstractPreferenceController> buildPreferenceControllers(Context context, diff --git a/src/com/android/settings/gestures/AssistGestureSettingsPreferenceController.java b/src/com/android/settings/gestures/AssistGestureSettingsPreferenceController.java index 6e318d65b2..729962db62 100644 --- a/src/com/android/settings/gestures/AssistGestureSettingsPreferenceController.java +++ b/src/com/android/settings/gestures/AssistGestureSettingsPreferenceController.java @@ -20,17 +20,14 @@ import static android.provider.Settings.Secure.ASSIST_GESTURE_ENABLED; import static android.provider.Settings.Secure.ASSIST_GESTURE_SILENCE_ALERTS_ENABLED; import android.content.Context; -import android.content.Intent; import android.provider.Settings; + +import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import com.android.internal.annotations.VisibleForTesting; import com.android.settings.R; import com.android.settings.overlay.FeatureFactory; -import com.android.settings.search.DatabaseIndexingUtils; -import com.android.settings.search.InlineSwitchPayload; -import com.android.settings.search.ResultPayload; public class AssistGestureSettingsPreferenceController extends GesturePreferenceController { @@ -41,7 +38,6 @@ public class AssistGestureSettingsPreferenceController extends GesturePreference private static final int ON = 1; private static final int OFF = 0; - private final String mAssistGesturePrefKey; private final AssistGestureFeatureProvider mFeatureProvider; private boolean mWasAvailable; @@ -51,12 +47,10 @@ public class AssistGestureSettingsPreferenceController extends GesturePreference @VisibleForTesting boolean mAssistOnly; - public AssistGestureSettingsPreferenceController(Context context, - String key) { + public AssistGestureSettingsPreferenceController(Context context, String key) { super(context, key); mFeatureProvider = FeatureFactory.getFactory(context).getAssistGestureFeatureProvider(); mWasAvailable = isAvailable(); - mAssistGesturePrefKey = key; } @Override @@ -137,15 +131,4 @@ public class AssistGestureSettingsPreferenceController extends GesturePreference public boolean isChecked() { return Settings.Secure.getInt(mContext.getContentResolver(), SECURE_KEY_ASSIST, OFF) == ON; } - - @Override - //TODO (b/69808376): Remove result payload - public ResultPayload getResultPayload() { - final Intent intent = DatabaseIndexingUtils.buildSearchResultPageIntent(mContext, - AssistGestureSettings.class.getName(), mAssistGesturePrefKey, - mContext.getString(R.string.display_settings)); - - return new InlineSwitchPayload(SECURE_KEY_ASSIST, ResultPayload.SettingsSource.SECURE, - ON /* onValue */, intent, isAvailable(), ON /* defaultValue */); - } } diff --git a/src/com/android/settings/gestures/DoubleTapPowerPreferenceController.java b/src/com/android/settings/gestures/DoubleTapPowerPreferenceController.java index 4433dd6e29..751bb798ab 100644 --- a/src/com/android/settings/gestures/DoubleTapPowerPreferenceController.java +++ b/src/com/android/settings/gestures/DoubleTapPowerPreferenceController.java @@ -19,16 +19,11 @@ package com.android.settings.gestures; import static android.provider.Settings.Secure.CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED; import android.content.Context; -import android.content.Intent; import android.content.SharedPreferences; import android.provider.Settings; -import androidx.annotation.VisibleForTesting; import android.text.TextUtils; -import com.android.settings.R; -import com.android.settings.search.DatabaseIndexingUtils; -import com.android.settings.search.InlineSwitchPayload; -import com.android.settings.search.ResultPayload; +import androidx.annotation.VisibleForTesting; public class DoubleTapPowerPreferenceController extends GesturePreferenceController { @@ -38,13 +33,11 @@ public class DoubleTapPowerPreferenceController extends GesturePreferenceControl static final int OFF = 1; private static final String PREF_KEY_VIDEO = "gesture_double_tap_power_video"; - private final String mDoubleTapPowerKey; private final String SECURE_KEY = CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED; public DoubleTapPowerPreferenceController(Context context, String key) { super(context, key); - mDoubleTapPowerKey = key; } public static boolean isSuggestionComplete(Context context, SharedPreferences prefs) { @@ -84,15 +77,4 @@ public class DoubleTapPowerPreferenceController extends GesturePreferenceControl return Settings.Secure.putInt(mContext.getContentResolver(), SECURE_KEY, isChecked ? ON : OFF); } - - @Override - //TODO (b/69808376): Remove result payload - public ResultPayload getResultPayload() { - final Intent intent = DatabaseIndexingUtils.buildSearchResultPageIntent(mContext, - DoubleTapPowerSettings.class.getName(), mDoubleTapPowerKey, - mContext.getString(R.string.display_settings)); - - return new InlineSwitchPayload(SECURE_KEY, ResultPayload.SettingsSource.SECURE, - ON /* onValue */, intent, isAvailable(), ON /* defaultValue */); - } } diff --git a/src/com/android/settings/gestures/DoubleTapPowerSettings.java b/src/com/android/settings/gestures/DoubleTapPowerSettings.java index 6eec6cd31c..acabdbc679 100644 --- a/src/com/android/settings/gestures/DoubleTapPowerSettings.java +++ b/src/com/android/settings/gestures/DoubleTapPowerSettings.java @@ -16,20 +16,22 @@ package com.android.settings.gestures; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.SharedPreferences; import android.provider.SearchIndexableResource; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider; import com.android.settings.overlay.FeatureFactory; import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settingslib.search.SearchIndexable; import java.util.Arrays; import java.util.List; +@SearchIndexable public class DoubleTapPowerSettings extends DashboardFragment { private static final String TAG = "DoubleTapPower"; @@ -48,7 +50,7 @@ public class DoubleTapPowerSettings extends DashboardFragment { @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.SETTINGS_GESTURE_DOUBLE_TAP_POWER; + return SettingsEnums.SETTINGS_GESTURE_DOUBLE_TAP_POWER; } @Override diff --git a/src/com/android/settings/gestures/DoubleTapScreenPreferenceController.java b/src/com/android/settings/gestures/DoubleTapScreenPreferenceController.java index f1c2ed30c2..4abf09af12 100644 --- a/src/com/android/settings/gestures/DoubleTapScreenPreferenceController.java +++ b/src/com/android/settings/gestures/DoubleTapScreenPreferenceController.java @@ -16,23 +16,17 @@ package com.android.settings.gestures; +import static android.provider.Settings.Secure.DOZE_DOUBLE_TAP_GESTURE; + import android.annotation.UserIdInt; import android.content.Context; -import android.content.Intent; import android.content.SharedPreferences; +import android.hardware.display.AmbientDisplayConfiguration; import android.os.UserHandle; import android.provider.Settings; -import androidx.preference.Preference; -import androidx.annotation.VisibleForTesting; import android.text.TextUtils; -import com.android.internal.hardware.AmbientDisplayConfiguration; -import com.android.settings.R; -import com.android.settings.search.DatabaseIndexingUtils; -import com.android.settings.search.InlineSwitchPayload; -import com.android.settings.search.ResultPayload; - -import static android.provider.Settings.Secure.DOZE_PULSE_ON_DOUBLE_TAP; +import androidx.annotation.VisibleForTesting; public class DoubleTapScreenPreferenceController extends GesturePreferenceController { @@ -40,9 +34,8 @@ public class DoubleTapScreenPreferenceController extends GesturePreferenceContro private final int OFF = 0; private static final String PREF_KEY_VIDEO = "gesture_double_tap_screen_video"; - private final String mDoubleTapScreenPrefKey; - private final String SECURE_KEY = DOZE_PULSE_ON_DOUBLE_TAP; + private final String SECURE_KEY = DOZE_DOUBLE_TAP_GESTURE; private AmbientDisplayConfiguration mAmbientConfig; @UserIdInt @@ -51,7 +44,6 @@ public class DoubleTapScreenPreferenceController extends GesturePreferenceContro public DoubleTapScreenPreferenceController(Context context, String key) { super(context, key); mUserId = UserHandle.myUserId(); - mDoubleTapScreenPrefKey = key; } public DoubleTapScreenPreferenceController setConfig(AmbientDisplayConfiguration config) { @@ -66,26 +58,17 @@ public class DoubleTapScreenPreferenceController extends GesturePreferenceContro @VisibleForTesting static boolean isSuggestionComplete(AmbientDisplayConfiguration config, SharedPreferences prefs) { - return !config.pulseOnDoubleTapAvailable() + return !config.doubleTapSensorAvailable() || prefs.getBoolean(DoubleTapScreenSettings.PREF_KEY_SUGGESTION_COMPLETE, false); } @Override public int getAvailabilityStatus() { - if (mAmbientConfig == null) { - mAmbientConfig = new AmbientDisplayConfiguration(mContext); - } - // No hardware support for Double Tap - if (!mAmbientConfig.doubleTapSensorAvailable()) { + if (!getAmbientConfig().doubleTapSensorAvailable()) { return UNSUPPORTED_ON_DEVICE; } - // Can't change Double Tap when AOD is enabled. - if (!mAmbientConfig.ambientDisplayAvailable()) { - return DISABLED_DEPENDENT_SETTING; - } - return AVAILABLE; } @@ -107,22 +90,13 @@ public class DoubleTapScreenPreferenceController extends GesturePreferenceContro @Override public boolean isChecked() { - return mAmbientConfig.pulseOnDoubleTapEnabled(mUserId); - } - - @Override - //TODO (b/69808376): Remove result payload - public ResultPayload getResultPayload() { - final Intent intent = DatabaseIndexingUtils.buildSearchResultPageIntent(mContext, - DoubleTapScreenSettings.class.getName(), mDoubleTapScreenPrefKey, - mContext.getString(R.string.display_settings)); - - return new InlineSwitchPayload(SECURE_KEY, ResultPayload.SettingsSource.SECURE, - ON /* onValue */, intent, isAvailable(), ON /* defaultValue */); + return getAmbientConfig().doubleTapGestureEnabled(mUserId); } - @Override - protected boolean canHandleClicks() { - return !mAmbientConfig.alwaysOnEnabled(mUserId); + private AmbientDisplayConfiguration getAmbientConfig() { + if (mAmbientConfig == null) { + mAmbientConfig = new AmbientDisplayConfiguration(mContext); + } + return mAmbientConfig; } -}
\ No newline at end of file +} diff --git a/src/com/android/settings/gestures/DoubleTapScreenSettings.java b/src/com/android/settings/gestures/DoubleTapScreenSettings.java index 29e0a1a4b7..300ce487a3 100644 --- a/src/com/android/settings/gestures/DoubleTapScreenSettings.java +++ b/src/com/android/settings/gestures/DoubleTapScreenSettings.java @@ -16,23 +16,23 @@ package com.android.settings.gestures; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.SharedPreferences; -import android.os.UserHandle; +import android.hardware.display.AmbientDisplayConfiguration; import android.provider.SearchIndexableResource; -import com.android.internal.hardware.AmbientDisplayConfiguration; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider; import com.android.settings.overlay.FeatureFactory; import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settingslib.search.SearchIndexable; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; +@SearchIndexable public class DoubleTapScreenSettings extends DashboardFragment { private static final String TAG = "DoubleTapScreen"; @@ -54,7 +54,7 @@ public class DoubleTapScreenSettings extends DashboardFragment { @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.SETTINGS_GESTURE_DOUBLE_TAP_SCREEN; + return SettingsEnums.SETTINGS_GESTURE_DOUBLE_TAP_SCREEN; } @Override diff --git a/src/com/android/settings/gestures/DoubleTwistGestureSettings.java b/src/com/android/settings/gestures/DoubleTwistGestureSettings.java index a3f37f0821..34819299b0 100644 --- a/src/com/android/settings/gestures/DoubleTwistGestureSettings.java +++ b/src/com/android/settings/gestures/DoubleTwistGestureSettings.java @@ -16,20 +16,22 @@ package com.android.settings.gestures; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.SharedPreferences; import android.provider.SearchIndexableResource; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider; import com.android.settings.overlay.FeatureFactory; import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settingslib.search.SearchIndexable; import java.util.Arrays; import java.util.List; +@SearchIndexable public class DoubleTwistGestureSettings extends DashboardFragment { private static final String TAG = "DoubleTwistGesture"; @@ -48,7 +50,7 @@ public class DoubleTwistGestureSettings extends DashboardFragment { @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.SETTINGS_GESTURE_DOUBLE_TWIST; + return SettingsEnums.SETTINGS_GESTURE_DOUBLE_TWIST; } @Override diff --git a/src/com/android/settings/gestures/DoubleTwistPreferenceController.java b/src/com/android/settings/gestures/DoubleTwistPreferenceController.java index 6a72dc4e4d..d4c63c0a76 100644 --- a/src/com/android/settings/gestures/DoubleTwistPreferenceController.java +++ b/src/com/android/settings/gestures/DoubleTwistPreferenceController.java @@ -24,9 +24,10 @@ import android.hardware.SensorManager; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; -import androidx.annotation.VisibleForTesting; import android.text.TextUtils; +import androidx.annotation.VisibleForTesting; + import com.android.settings.R; import com.android.settings.Utils; diff --git a/src/com/android/settings/gestures/GestureNavigationBackSensitivityDialog.java b/src/com/android/settings/gestures/GestureNavigationBackSensitivityDialog.java new file mode 100644 index 0000000000..e4f25ebb37 --- /dev/null +++ b/src/com/android/settings/gestures/GestureNavigationBackSensitivityDialog.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2019 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.gestures; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.om.IOverlayManager; +import android.os.Bundle; +import android.os.ServiceManager; +import android.view.View; +import android.widget.SeekBar; + +import com.android.settings.R; +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; + +/** + * Dialog to set the back gesture's sensitivity in Gesture navigation mode. + */ +public class GestureNavigationBackSensitivityDialog extends InstrumentedDialogFragment { + private static final String TAG = "GestureNavigationBackSensitivityDialog"; + private static final String KEY_BACK_SENSITIVITY = "back_sensitivity"; + + public static void show(SystemNavigationGestureSettings parent, int sensitivity) { + if (!parent.isAdded()) { + return; + } + + final GestureNavigationBackSensitivityDialog dialog = + new GestureNavigationBackSensitivityDialog(); + final Bundle bundle = new Bundle(); + bundle.putInt(KEY_BACK_SENSITIVITY, sensitivity); + dialog.setArguments(bundle); + dialog.setTargetFragment(parent, 0); + dialog.show(parent.getFragmentManager(), TAG); + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.SETTINGS_GESTURE_NAV_BACK_SENSITIVITY_DLG; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final View view = getActivity().getLayoutInflater().inflate( + R.layout.dialog_back_gesture_sensitivity, null); + final SeekBar seekBar = view.findViewById(R.id.back_sensitivity_seekbar); + seekBar.setProgress(getArguments().getInt(KEY_BACK_SENSITIVITY)); + return new AlertDialog.Builder(getContext()) + .setTitle(R.string.back_sensitivity_dialog_title) + .setMessage(R.string.back_sensitivity_dialog_message) + .setView(view) + .setPositiveButton(R.string.okay, (dialog, which) -> { + int sensitivity = seekBar.getProgress(); + getArguments().putInt(KEY_BACK_SENSITIVITY, sensitivity); + SystemNavigationGestureSettings.setBackSensitivity(getActivity(), + getOverlayManager(), sensitivity); + }) + .create(); + } + + private IOverlayManager getOverlayManager() { + return IOverlayManager.Stub.asInterface(ServiceManager.getService(Context.OVERLAY_SERVICE)); + } +}
\ No newline at end of file diff --git a/src/com/android/settings/gestures/GestureNavigationNotAvailableDialog.java b/src/com/android/settings/gestures/GestureNavigationNotAvailableDialog.java new file mode 100644 index 0000000000..6e8b4142d8 --- /dev/null +++ b/src/com/android/settings/gestures/GestureNavigationNotAvailableDialog.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2019 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.gestures; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.os.Bundle; + +import com.android.settings.R; +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; + +/** + * Dialog to notify user that gesture navigation is not available because of unsupported launcher. + */ +public class GestureNavigationNotAvailableDialog extends InstrumentedDialogFragment { + private static final String TAG = "GestureNavigationNotAvailableDialog"; + + public static void show(SystemNavigationGestureSettings parent) { + if (!parent.isAdded()) { + return; + } + + final GestureNavigationNotAvailableDialog dialog = + new GestureNavigationNotAvailableDialog(); + dialog.setTargetFragment(parent, 0); + dialog.show(parent.getFragmentManager(), TAG); + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.SETTINGS_GESTURE_NAV_NOT_AVAILABLE_DLG; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Context context = getActivity(); + final String defaultHomeAppName = SystemNavigationPreferenceController + .getDefaultHomeAppName(context); + final String message = getString(R.string.gesture_not_supported_dialog_message, + defaultHomeAppName); + return new AlertDialog.Builder(context) + .setMessage(message) + .setPositiveButton(R.string.okay, null) + .create(); + } +}
\ No newline at end of file diff --git a/src/com/android/settings/gestures/GesturePreferenceController.java b/src/com/android/settings/gestures/GesturePreferenceController.java index 780325f085..d7b386ada8 100644 --- a/src/com/android/settings/gestures/GesturePreferenceController.java +++ b/src/com/android/settings/gestures/GesturePreferenceController.java @@ -18,10 +18,10 @@ package com.android.settings.gestures; import android.content.Context; import android.os.Bundle; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import androidx.preference.TwoStatePreference; import com.android.settings.R; import com.android.settings.core.TogglePreferenceController; @@ -51,7 +51,7 @@ public abstract class GesturePreferenceController extends TogglePreferenceContro public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); if (isAvailable()) { - mVideoPreference = (VideoPreference) screen.findPreference(getVideoPrefKey()); + mVideoPreference = screen.findPreference(getVideoPrefKey()); } } diff --git a/src/com/android/settings/gestures/GestureSettings.java b/src/com/android/settings/gestures/GestureSettings.java index 27172d95b1..db402cc04d 100644 --- a/src/com/android/settings/gestures/GestureSettings.java +++ b/src/com/android/settings/gestures/GestureSettings.java @@ -16,42 +16,29 @@ package com.android.settings.gestures; +import android.app.settings.SettingsEnums; import android.content.Context; -import android.os.UserHandle; +import android.hardware.display.AmbientDisplayConfiguration; import android.provider.SearchIndexableResource; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.android.internal.hardware.AmbientDisplayConfiguration; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.search.BaseSearchIndexProvider; -import com.android.settingslib.core.AbstractPreferenceController; -import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.search.SearchIndexable; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; +@SearchIndexable public class GestureSettings extends DashboardFragment { private static final String TAG = "GestureSettings"; - private static final String KEY_ASSIST = "gesture_assist_input_summary"; - private static final String KEY_SWIPE_DOWN = "gesture_swipe_down_fingerprint_input_summary"; - private static final String KEY_DOUBLE_TAP_POWER = "gesture_double_tap_power_input_summary"; - private static final String KEY_DOUBLE_TWIST = "gesture_double_twist_input_summary"; - private static final String KEY_DOUBLE_TAP_SCREEN = "gesture_double_tap_screen_input_summary"; - private static final String KEY_PICK_UP = "gesture_pick_up_input_summary"; - private static final String KEY_PREVENT_RINGING = "gesture_prevent_ringing_summary"; - private static final String KEY_SWIPE_UP = "gesture_swipe_up_input_summary"; - private AmbientDisplayConfiguration mAmbientDisplayConfig; @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.SETTINGS_GESTURES; + return SettingsEnums.SETTINGS_GESTURES; } @Override @@ -90,19 +77,10 @@ public class GestureSettings extends DashboardFragment { } @Override - public List<String> getNonIndexableKeys(Context context) { - List<String> keys = super.getNonIndexableKeys(context); - // Duplicates in summary and details pages. - keys.add(KEY_ASSIST); - keys.add(KEY_SWIPE_DOWN); - keys.add(KEY_DOUBLE_TAP_POWER); - keys.add(KEY_DOUBLE_TWIST); - keys.add(KEY_SWIPE_UP); - keys.add(KEY_DOUBLE_TAP_SCREEN); - keys.add(KEY_PICK_UP); - keys.add(KEY_PREVENT_RINGING); - - return keys; + protected boolean isPageSearchEnabled(Context context) { + // All rows in this screen can lead to a different page, so suppress everything + // from this page to remove duplicates. + return false; } }; } diff --git a/src/com/android/settings/gestures/GesturesSettingPreferenceController.java b/src/com/android/settings/gestures/GesturesSettingPreferenceController.java index 652e151818..98eddffb8f 100644 --- a/src/com/android/settings/gestures/GesturesSettingPreferenceController.java +++ b/src/com/android/settings/gestures/GesturesSettingPreferenceController.java @@ -18,10 +18,11 @@ package com.android.settings.gestures; import android.content.ContentResolver; import android.content.Context; +import android.hardware.display.AmbientDisplayConfiguration; import android.provider.Settings; + import androidx.annotation.NonNull; -import com.android.internal.hardware.AmbientDisplayConfiguration; import com.android.settings.R; import com.android.settings.core.BasePreferenceController; import com.android.settings.overlay.FeatureFactory; @@ -75,7 +76,7 @@ public class GesturesSettingPreferenceController extends BasePreferenceControlle .setConfig(ambientDisplayConfiguration)); controllers.add(new DoubleTapScreenPreferenceController(context, FAKE_PREF_KEY) .setConfig(ambientDisplayConfiguration)); - controllers.add(new PreventRingingPreferenceController(context, FAKE_PREF_KEY)); + controllers.add(new PreventRingingParentPreferenceController(context, FAKE_PREF_KEY)); return controllers; } @@ -100,4 +101,4 @@ public class GesturesSettingPreferenceController extends BasePreferenceControlle } return mContext.getText(R.string.language_input_gesture_summary_off); } -}
\ No newline at end of file +} diff --git a/src/com/android/settings/gestures/GlobalActionsPanelPreferenceController.java b/src/com/android/settings/gestures/GlobalActionsPanelPreferenceController.java new file mode 100644 index 0000000000..b980499ef9 --- /dev/null +++ b/src/com/android/settings/gestures/GlobalActionsPanelPreferenceController.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2019 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.gestures; + +import android.content.Context; +import android.provider.Settings; +import android.text.TextUtils; + +import com.android.internal.annotations.VisibleForTesting; + +public class GlobalActionsPanelPreferenceController extends GesturePreferenceController { + private static final String PREF_KEY_VIDEO = "global_actions_panel_video"; + + @VisibleForTesting + protected static final String ENABLED_SETTING = Settings.Secure.GLOBAL_ACTIONS_PANEL_ENABLED; + @VisibleForTesting + protected static final String AVAILABLE_SETTING = + Settings.Secure.GLOBAL_ACTIONS_PANEL_AVAILABLE; + + @VisibleForTesting + protected static final String TOGGLE_KEY = "gesture_global_actions_panel_switch"; + + public GlobalActionsPanelPreferenceController(Context context, String key) { + super(context, key); + } + + @Override + public int getAvailabilityStatus() { + int enabled = Settings.Secure.getInt(mContext.getContentResolver(), AVAILABLE_SETTING, 0); + return enabled == 1 ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; + } + + @Override + public boolean setChecked(boolean isChecked) { + return Settings.Secure.putInt(mContext.getContentResolver(), ENABLED_SETTING, + isChecked ? 1 : 0); + } + + @Override + protected String getVideoPrefKey() { + return PREF_KEY_VIDEO; + } + + @Override + public boolean isSliceable() { + return TextUtils.equals(getPreferenceKey(), TOGGLE_KEY); + } + + @Override + public boolean isChecked() { + int enabled = Settings.Secure.getInt(mContext.getContentResolver(), ENABLED_SETTING, 0); + return enabled == 1; + } +} diff --git a/src/com/android/settings/gestures/GlobalActionsPanelSettings.java b/src/com/android/settings/gestures/GlobalActionsPanelSettings.java new file mode 100644 index 0000000000..fe9a9e80cb --- /dev/null +++ b/src/com/android/settings/gestures/GlobalActionsPanelSettings.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2019 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.gestures; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.provider.SearchIndexableResource; + +import com.android.settings.R; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settingslib.search.SearchIndexable; + +import java.util.Arrays; +import java.util.List; + +@SearchIndexable +public class GlobalActionsPanelSettings extends DashboardFragment { + + private static final String TAG = "GlobalActionsPanelSettings"; + + @Override + public int getMetricsCategory() { + return SettingsEnums.GLOBAL_ACTIONS_PANEL_SETTINGS; + } + + @Override + protected String getLogTag() { + return TAG; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.global_actions_panel_settings; + } + + public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex( + Context context, boolean enabled) { + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.global_actions_panel_settings; + return Arrays.asList(sir); + } + }; +} diff --git a/src/com/android/settings/gestures/PickupGesturePreferenceController.java b/src/com/android/settings/gestures/PickupGesturePreferenceController.java index 6d01e52e84..0738a51f0f 100644 --- a/src/com/android/settings/gestures/PickupGesturePreferenceController.java +++ b/src/com/android/settings/gestures/PickupGesturePreferenceController.java @@ -16,32 +16,25 @@ package com.android.settings.gestures; -import static android.provider.Settings.Secure.DOZE_PULSE_ON_PICK_UP; +import static android.provider.Settings.Secure.DOZE_PICK_UP_GESTURE; import android.annotation.UserIdInt; import android.content.Context; -import android.content.Intent; import android.content.SharedPreferences; +import android.hardware.display.AmbientDisplayConfiguration; import android.os.UserHandle; import android.provider.Settings; -import androidx.annotation.VisibleForTesting; import android.text.TextUtils; -import com.android.internal.hardware.AmbientDisplayConfiguration; -import com.android.settings.R; -import com.android.settings.search.DatabaseIndexingUtils; -import com.android.settings.search.InlineSwitchPayload; -import com.android.settings.search.ResultPayload; - public class PickupGesturePreferenceController extends GesturePreferenceController { - private final int ON = 1; - private final int OFF = 0; + private static final int ON = 1; + private static final int OFF = 0; private static final String PREF_KEY_VIDEO = "gesture_pick_up_video"; private final String mPickUpPrefKey; - private final String SECURE_KEY = DOZE_PULSE_ON_PICK_UP; + private final String SECURE_KEY = DOZE_PICK_UP_GESTURE; private AmbientDisplayConfiguration mAmbientConfig; @UserIdInt @@ -61,25 +54,16 @@ public class PickupGesturePreferenceController extends GesturePreferenceControll public static boolean isSuggestionComplete(Context context, SharedPreferences prefs) { AmbientDisplayConfiguration ambientConfig = new AmbientDisplayConfiguration(context); return prefs.getBoolean(PickupGestureSettings.PREF_KEY_SUGGESTION_COMPLETE, false) - || !ambientConfig.pulseOnPickupAvailable(); + || !ambientConfig.dozePickupSensorAvailable(); } @Override public int getAvailabilityStatus() { - if (mAmbientConfig == null) { - mAmbientConfig = new AmbientDisplayConfiguration(mContext); - } - // No hardware support for Pickup Gesture - if (!mAmbientConfig.dozePulsePickupSensorAvailable()) { + if (!getAmbientConfig().dozePickupSensorAvailable()) { return UNSUPPORTED_ON_DEVICE; } - // Can't change Pickup Gesture when AOD is enabled. - if (!mAmbientConfig.ambientDisplayAvailable()) { - return DISABLED_DEPENDENT_SETTING; - } - return AVAILABLE; } @@ -95,7 +79,7 @@ public class PickupGesturePreferenceController extends GesturePreferenceControll @Override public boolean isChecked() { - return mAmbientConfig.pulseOnPickupEnabled(mUserId); + return getAmbientConfig().pickupGestureEnabled(mUserId); } @Override @@ -109,23 +93,11 @@ public class PickupGesturePreferenceController extends GesturePreferenceControll isChecked ? ON : OFF); } - @Override - public boolean canHandleClicks() { - return pulseOnPickupCanBeModified(); - } - - @Override - public ResultPayload getResultPayload() { - final Intent intent = DatabaseIndexingUtils.buildSearchResultPageIntent(mContext, - PickupGestureSettings.class.getName(), mPickUpPrefKey, - mContext.getString(R.string.display_settings)); - - return new InlineSwitchPayload(SECURE_KEY, ResultPayload.SettingsSource.SECURE, - ON /* onValue */, intent, isAvailable(), ON /* defaultValue */); - } + private AmbientDisplayConfiguration getAmbientConfig() { + if (mAmbientConfig == null) { + mAmbientConfig = new AmbientDisplayConfiguration(mContext); + } - @VisibleForTesting - boolean pulseOnPickupCanBeModified() { - return mAmbientConfig.pulseOnPickupCanBeModified(mUserId); + return mAmbientConfig; } } diff --git a/src/com/android/settings/gestures/PickupGestureSettings.java b/src/com/android/settings/gestures/PickupGestureSettings.java index 8f4e7b3f1f..f1cc3f05ed 100644 --- a/src/com/android/settings/gestures/PickupGestureSettings.java +++ b/src/com/android/settings/gestures/PickupGestureSettings.java @@ -16,21 +16,23 @@ package com.android.settings.gestures; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.SharedPreferences; +import android.hardware.display.AmbientDisplayConfiguration; import android.provider.SearchIndexableResource; -import com.android.internal.hardware.AmbientDisplayConfiguration; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider; import com.android.settings.overlay.FeatureFactory; import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settingslib.search.SearchIndexable; import java.util.Arrays; import java.util.List; +@SearchIndexable public class PickupGestureSettings extends DashboardFragment { private static final String TAG = "PickupGestureSettings"; @@ -52,7 +54,7 @@ public class PickupGestureSettings extends DashboardFragment { @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.SETTINGS_GESTURE_PICKUP; + return SettingsEnums.SETTINGS_GESTURE_PICKUP; } @Override diff --git a/src/com/android/settings/gestures/PreventRingingGesturePreferenceController.java b/src/com/android/settings/gestures/PreventRingingGesturePreferenceController.java new file mode 100644 index 0000000000..04f7e476c2 --- /dev/null +++ b/src/com/android/settings/gestures/PreventRingingGesturePreferenceController.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2018 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.gestures; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.provider.Settings; + +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceScreen; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.settings.R; +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settings.widget.RadioButtonPreference; +import com.android.settings.widget.VideoPreference; +import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnCreate; +import com.android.settingslib.core.lifecycle.events.OnPause; +import com.android.settingslib.core.lifecycle.events.OnResume; +import com.android.settingslib.core.lifecycle.events.OnSaveInstanceState; + +public class PreventRingingGesturePreferenceController extends AbstractPreferenceController + implements RadioButtonPreference.OnClickListener, LifecycleObserver, OnSaveInstanceState, + OnResume, OnPause, OnCreate, PreferenceControllerMixin { + + @VisibleForTesting + static final String KEY_VIBRATE = "prevent_ringing_option_vibrate"; + + @VisibleForTesting + static final String KEY_MUTE = "prevent_ringing_option_mute"; + + private final String KEY_VIDEO_PAUSED = "key_video_paused"; + private final String PREF_KEY_VIDEO = "gesture_prevent_ringing_video"; + private final String KEY = "gesture_prevent_ringing_category"; + private final Context mContext; + + private VideoPreference mVideoPreference; + private boolean mVideoPaused; + + @VisibleForTesting + PreferenceCategory mPreferenceCategory; + @VisibleForTesting + RadioButtonPreference mVibratePref; + @VisibleForTesting + RadioButtonPreference mMutePref; + + private SettingObserver mSettingObserver; + + public PreventRingingGesturePreferenceController(Context context, Lifecycle lifecycle) { + super(context); + mContext = context; + + if (lifecycle != null) { + lifecycle.addObserver(this); + } + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + if (!isAvailable()) { + return; + } + mPreferenceCategory = screen.findPreference(getPreferenceKey()); + mVibratePref = makeRadioPreference(KEY_VIBRATE, R.string.prevent_ringing_option_vibrate); + mMutePref = makeRadioPreference(KEY_MUTE, R.string.prevent_ringing_option_mute); + + if (mPreferenceCategory != null) { + mSettingObserver = new SettingObserver(mPreferenceCategory); + } + + mVideoPreference = screen.findPreference(getVideoPrefKey()); + } + + @Override + public boolean isAvailable() { + return mContext.getResources().getBoolean( + com.android.internal.R.bool.config_volumeHushGestureEnabled); + } + + @Override + public String getPreferenceKey() { + return KEY; + } + + public String getVideoPrefKey() { + return PREF_KEY_VIDEO; + } + + @Override + public void onSaveInstanceState(Bundle outState) { + outState.putBoolean(KEY_VIDEO_PAUSED, mVideoPaused); + } + + @Override + public void onRadioButtonClicked(RadioButtonPreference preference) { + int preventRingingSetting = keyToSetting(preference.getKey()); + if (preventRingingSetting != Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.VOLUME_HUSH_GESTURE, Settings.Secure.VOLUME_HUSH_VIBRATE)) { + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.VOLUME_HUSH_GESTURE, preventRingingSetting); + } + } + + @Override + public void updateState(Preference preference) { + int preventRingingSetting = Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.VOLUME_HUSH_GESTURE, Settings.Secure.VOLUME_HUSH_VIBRATE); + final boolean isVibrate = preventRingingSetting == Settings.Secure.VOLUME_HUSH_VIBRATE; + final boolean isMute = preventRingingSetting == Settings.Secure.VOLUME_HUSH_MUTE; + if (mVibratePref != null && mVibratePref.isChecked() != isVibrate) { + mVibratePref.setChecked(isVibrate); + } + if (mMutePref != null && mMutePref.isChecked() != isMute) { + mMutePref.setChecked(isMute); + } + + if (preventRingingSetting == Settings.Secure.VOLUME_HUSH_OFF) { + mVibratePref.setEnabled(false); + mMutePref.setEnabled(false); + } else { + mVibratePref.setEnabled(true); + mMutePref.setEnabled(true); + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + if (savedInstanceState != null) { + mVideoPaused = savedInstanceState.getBoolean(KEY_VIDEO_PAUSED, false); + } + } + + @Override + public void onResume() { + if (mSettingObserver != null) { + mSettingObserver.register(mContext.getContentResolver()); + mSettingObserver.onChange(false, null); + } + + if (mVideoPreference != null) { + mVideoPreference.onViewVisible(mVideoPaused); + } + } + + @Override + public void onPause() { + if (mSettingObserver != null) { + mSettingObserver.unregister(mContext.getContentResolver()); + } + + if (mVideoPreference != null) { + mVideoPaused = mVideoPreference.isVideoPaused(); + mVideoPreference.onViewInvisible(); + } + } + + private int keyToSetting(String key) { + switch (key) { + case KEY_MUTE: + return Settings.Secure.VOLUME_HUSH_MUTE; + case KEY_VIBRATE: + return Settings.Secure.VOLUME_HUSH_VIBRATE; + default: + return Settings.Secure.VOLUME_HUSH_OFF; + } + } + + private RadioButtonPreference makeRadioPreference(String key, int titleId) { + RadioButtonPreference pref = new RadioButtonPreference(mPreferenceCategory.getContext()); + pref.setKey(key); + pref.setTitle(titleId); + pref.setOnClickListener(this); + mPreferenceCategory.addPreference(pref); + return pref; + } + + private class SettingObserver extends ContentObserver { + private final Uri VOLUME_HUSH_GESTURE = Settings.Secure.getUriFor( + Settings.Secure.VOLUME_HUSH_GESTURE); + + private final Preference mPreference; + + public SettingObserver(Preference preference) { + super(new Handler()); + mPreference = preference; + } + + public void register(ContentResolver cr) { + cr.registerContentObserver(VOLUME_HUSH_GESTURE, false, this); + } + + public void unregister(ContentResolver cr) { + cr.unregisterContentObserver(this); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + super.onChange(selfChange, uri); + if (uri == null || VOLUME_HUSH_GESTURE.equals(uri)) { + updateState(mPreference); + } + } + } +} diff --git a/src/com/android/settings/gestures/PreventRingingGestureSettings.java b/src/com/android/settings/gestures/PreventRingingGestureSettings.java index 241e5c0bf5..47cd3ea6ed 100644 --- a/src/com/android/settings/gestures/PreventRingingGestureSettings.java +++ b/src/com/android/settings/gestures/PreventRingingGestureSettings.java @@ -16,25 +16,25 @@ package com.android.settings.gestures; +import android.app.settings.SettingsEnums; import android.content.Context; -import android.os.UserHandle; import android.provider.SearchIndexableResource; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +@SearchIndexable public class PreventRingingGestureSettings extends DashboardFragment { private static final String TAG = "RingingGestureSettings"; - private static final String KEY_PREVENT_RINGING = "gesture_prevent_ringing"; @Override public void onAttach(Context context) { @@ -42,8 +42,21 @@ public class PreventRingingGestureSettings extends DashboardFragment { } @Override + protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { + return buildPreferenceControllers(context, getSettingsLifecycle()); + } + + private static List<AbstractPreferenceController> buildPreferenceControllers(Context context, + Lifecycle lifecycle) { + List<AbstractPreferenceController> controllers = new ArrayList<>(); + controllers.add(new PreventRingingGesturePreferenceController(context, lifecycle)); + controllers.add(new PreventRingingSwitchPreferenceController(context)); + return controllers; + } + + @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.SETTINGS_PREVENT_RINGING; + return SettingsEnums.SETTINGS_PREVENT_RINGING; } @Override @@ -70,6 +83,12 @@ public class PreventRingingGestureSettings extends DashboardFragment { sir.xmlResId = R.xml.prevent_ringing_gesture_settings; return Arrays.asList(sir); } + + @Override + public List<AbstractPreferenceController> createPreferenceControllers( + Context context) { + return buildPreferenceControllers(context, null); + } }; } diff --git a/src/com/android/settings/gestures/PreventRingingParentPreferenceController.java b/src/com/android/settings/gestures/PreventRingingParentPreferenceController.java new file mode 100644 index 0000000000..afb3431526 --- /dev/null +++ b/src/com/android/settings/gestures/PreventRingingParentPreferenceController.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2018 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.gestures; + +import static android.provider.Settings.Secure.VOLUME_HUSH_GESTURE; +import static android.provider.Settings.Secure.VOLUME_HUSH_MUTE; +import static android.provider.Settings.Secure.VOLUME_HUSH_VIBRATE; + +import android.content.Context; +import android.provider.Settings; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; + +public class PreventRingingParentPreferenceController extends BasePreferenceController { + + final String SECURE_KEY = VOLUME_HUSH_GESTURE; + + public PreventRingingParentPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public int getAvailabilityStatus() { + return mContext.getResources().getBoolean( + com.android.internal.R.bool.config_volumeHushGestureEnabled) + ? AVAILABLE_UNSEARCHABLE : UNSUPPORTED_ON_DEVICE; + } + + @Override + public CharSequence getSummary() { + int value = Settings.Secure.getInt( + mContext.getContentResolver(), SECURE_KEY, VOLUME_HUSH_VIBRATE); + int summary; + switch (value) { + case VOLUME_HUSH_VIBRATE: + summary = R.string.prevent_ringing_option_vibrate_summary; + break; + case VOLUME_HUSH_MUTE: + summary = R.string.prevent_ringing_option_mute_summary; + break; + default: + summary = R.string.prevent_ringing_option_none_summary; + } + return mContext.getText(summary); + } +} diff --git a/src/com/android/settings/gestures/PreventRingingPreferenceController.java b/src/com/android/settings/gestures/PreventRingingPreferenceController.java deleted file mode 100644 index c6bc3aa8f2..0000000000 --- a/src/com/android/settings/gestures/PreventRingingPreferenceController.java +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright (C) 2018 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.gestures; - -import static android.provider.Settings.Secure.VOLUME_HUSH_GESTURE; -import static android.provider.Settings.Secure.VOLUME_HUSH_MUTE; -import static android.provider.Settings.Secure.VOLUME_HUSH_OFF; -import static android.provider.Settings.Secure.VOLUME_HUSH_VIBRATE; - -import android.content.Context; -import android.os.Bundle; -import android.provider.Settings; -import androidx.annotation.VisibleForTesting; -import androidx.preference.ListPreference; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; - -import com.android.settings.R; -import com.android.settings.core.BasePreferenceController; -import com.android.settings.core.PreferenceControllerMixin; -import com.android.settings.widget.VideoPreference; -import com.android.settingslib.core.lifecycle.LifecycleObserver; -import com.android.settingslib.core.lifecycle.events.OnCreate; -import com.android.settingslib.core.lifecycle.events.OnPause; -import com.android.settingslib.core.lifecycle.events.OnResume; -import com.android.settingslib.core.lifecycle.events.OnSaveInstanceState; - -public class PreventRingingPreferenceController extends BasePreferenceController - implements PreferenceControllerMixin, Preference.OnPreferenceChangeListener, - LifecycleObserver, OnResume, OnPause, OnCreate, OnSaveInstanceState { - - private static final String PREF_KEY_VIDEO = "gesture_prevent_ringing_video"; - @VisibleForTesting - static final String KEY_VIDEO_PAUSED = "key_video_paused"; - - private VideoPreference mVideoPreference; - @VisibleForTesting - boolean mVideoPaused; - - private final String SECURE_KEY = VOLUME_HUSH_GESTURE; - - public PreventRingingPreferenceController(Context context, String key) { - super(context, key); - } - - @Override - public int getAvailabilityStatus() { - return mContext.getResources().getBoolean( - com.android.internal.R.bool.config_volumeHushGestureEnabled) - ? AVAILABLE : UNSUPPORTED_ON_DEVICE; - } - - @Override - public void displayPreference(PreferenceScreen screen) { - super.displayPreference(screen); - if (isAvailable()) { - mVideoPreference = (VideoPreference) screen.findPreference(getVideoPrefKey()); - } - } - - @Override - public void updateState(Preference preference) { - super.updateState(preference); - if (preference != null) { - if (preference instanceof ListPreference) { - ListPreference pref = (ListPreference) preference; - int value = Settings.Secure.getInt( - mContext.getContentResolver(), SECURE_KEY, VOLUME_HUSH_VIBRATE); - switch (value) { - case VOLUME_HUSH_VIBRATE: - pref.setValue(String.valueOf(value)); - break; - case VOLUME_HUSH_MUTE: - pref.setValue(String.valueOf(value)); - break; - default: - pref.setValue(String.valueOf(VOLUME_HUSH_OFF)); - } - } - } - } - - @Override - public CharSequence getSummary() { - int value = Settings.Secure.getInt( - mContext.getContentResolver(), SECURE_KEY, VOLUME_HUSH_VIBRATE); - int summary; - switch (value) { - case VOLUME_HUSH_VIBRATE: - summary = R.string.prevent_ringing_option_vibrate_summary; - break; - case VOLUME_HUSH_MUTE: - summary = R.string.prevent_ringing_option_mute_summary; - break; - default: - summary = R.string.prevent_ringing_option_none_summary; - } - return mContext.getString(summary); - } - - @Override - public void onCreate(Bundle savedInstanceState) { - if (savedInstanceState != null) { - mVideoPaused = savedInstanceState.getBoolean(KEY_VIDEO_PAUSED, false); - } - } - - @Override - public void onSaveInstanceState(Bundle outState) { - outState.putBoolean(KEY_VIDEO_PAUSED, mVideoPaused); - } - - @Override - public void onPause() { - if (mVideoPreference != null) { - mVideoPaused = mVideoPreference.isVideoPaused(); - mVideoPreference.onViewInvisible(); - } - } - - @Override - public void onResume() { - if (mVideoPreference != null) { - mVideoPreference.onViewVisible(mVideoPaused); - } - } - - protected String getVideoPrefKey() { - return PREF_KEY_VIDEO; - } - - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - int value = Integer.parseInt((String) newValue); - Settings.Secure.putInt(mContext.getContentResolver(), SECURE_KEY, value); - preference.setSummary(getSummary()); - return true; - } -} diff --git a/src/com/android/settings/gestures/PreventRingingSwitchPreferenceController.java b/src/com/android/settings/gestures/PreventRingingSwitchPreferenceController.java new file mode 100644 index 0000000000..9545939255 --- /dev/null +++ b/src/com/android/settings/gestures/PreventRingingSwitchPreferenceController.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2019 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.gestures; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.provider.Settings; +import android.widget.Switch; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settings.widget.SwitchBar; +import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.widget.LayoutPreference; + +public class PreventRingingSwitchPreferenceController extends AbstractPreferenceController + implements PreferenceControllerMixin, SwitchBar.OnSwitchChangeListener { + + private static final String KEY = "gesture_prevent_ringing_switch"; + private final Context mContext; + private SettingObserver mSettingObserver; + + @VisibleForTesting + SwitchBar mSwitch; + + public PreventRingingSwitchPreferenceController(Context context) { + super(context); + mContext = context; + } + + @Override + public String getPreferenceKey() { + return KEY; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + if (isAvailable()) { + LayoutPreference pref = screen.findPreference(getPreferenceKey()); + if (pref != null) { + mSettingObserver = new SettingObserver(pref); + pref.setOnPreferenceClickListener(preference -> { + int preventRinging = Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.VOLUME_HUSH_GESTURE, + Settings.Secure.VOLUME_HUSH_VIBRATE); + boolean isChecked = preventRinging != Settings.Secure.VOLUME_HUSH_OFF; + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.VOLUME_HUSH_GESTURE, isChecked + ? Settings.Secure.VOLUME_HUSH_OFF + : Settings.Secure.VOLUME_HUSH_VIBRATE); + return true; + }); + mSwitch = pref.findViewById(R.id.switch_bar); + if (mSwitch != null) { + mSwitch.addOnSwitchChangeListener(this); + mSwitch.show(); + } + } + } + } + + public void setChecked(boolean isChecked) { + if (mSwitch != null) { + mSwitch.setChecked(isChecked); + } + } + + @Override + public void updateState(Preference preference) { + int preventRingingSetting = Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.VOLUME_HUSH_GESTURE, Settings.Secure.VOLUME_HUSH_VIBRATE); + setChecked(preventRingingSetting != Settings.Secure.VOLUME_HUSH_OFF); + } + + @Override + public boolean isAvailable() { + return mContext.getResources().getBoolean( + com.android.internal.R.bool.config_volumeHushGestureEnabled); + } + + @Override + public void onSwitchChanged(Switch switchView, boolean isChecked) { + final int preventRingingSetting = Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.VOLUME_HUSH_GESTURE, Settings.Secure.VOLUME_HUSH_VIBRATE); + final int newRingingSetting = preventRingingSetting == Settings.Secure.VOLUME_HUSH_OFF + ? Settings.Secure.VOLUME_HUSH_VIBRATE + : preventRingingSetting; + + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.VOLUME_HUSH_GESTURE, isChecked + ? newRingingSetting + : Settings.Secure.VOLUME_HUSH_OFF); + } + + private class SettingObserver extends ContentObserver { + private final Uri VOLUME_HUSH_GESTURE = Settings.Secure.getUriFor( + Settings.Secure.VOLUME_HUSH_GESTURE); + + private final Preference mPreference; + + public SettingObserver(Preference preference) { + super(new Handler()); + mPreference = preference; + } + + public void register(ContentResolver cr) { + cr.registerContentObserver(VOLUME_HUSH_GESTURE, false, this); + } + + public void unregister(ContentResolver cr) { + cr.unregisterContentObserver(this); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + super.onChange(selfChange, uri); + if (uri == null || VOLUME_HUSH_GESTURE.equals(uri)) { + updateState(mPreference); + } + } + } +} diff --git a/src/com/android/settings/gestures/SwipeToNotificationSettings.java b/src/com/android/settings/gestures/SwipeToNotificationSettings.java index c18289c1a9..230cdda27c 100644 --- a/src/com/android/settings/gestures/SwipeToNotificationSettings.java +++ b/src/com/android/settings/gestures/SwipeToNotificationSettings.java @@ -16,20 +16,22 @@ package com.android.settings.gestures; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.SharedPreferences; import android.provider.SearchIndexableResource; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider; import com.android.settings.overlay.FeatureFactory; import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settingslib.search.SearchIndexable; import java.util.Arrays; import java.util.List; +@SearchIndexable public class SwipeToNotificationSettings extends DashboardFragment { private static final String TAG = "SwipeToNotifSettings"; @@ -48,7 +50,7 @@ public class SwipeToNotificationSettings extends DashboardFragment { @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.SETTINGS_GESTURE_SWIPE_TO_NOTIFICATION; + return SettingsEnums.SETTINGS_GESTURE_SWIPE_TO_NOTIFICATION; } @Override @@ -70,5 +72,10 @@ public class SwipeToNotificationSettings extends DashboardFragment { sir.xmlResId = R.xml.swipe_to_notification_settings; return Arrays.asList(sir); } + + @Override + protected boolean isPageSearchEnabled(Context context) { + return SwipeToNotificationPreferenceController.isAvailable(context); + } }; } diff --git a/src/com/android/settings/gestures/SwipeUpPreferenceController.java b/src/com/android/settings/gestures/SwipeUpPreferenceController.java deleted file mode 100644 index 331a5efbaf..0000000000 --- a/src/com/android/settings/gestures/SwipeUpPreferenceController.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (C) 2018 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.gestures; - -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.os.UserManager; -import android.provider.Settings; -import androidx.annotation.VisibleForTesting; -import android.text.TextUtils; - -import com.android.internal.R; - -public class SwipeUpPreferenceController extends GesturePreferenceController { - - private final int ON = 1; - private final int OFF = 0; - - private static final String ACTION_QUICKSTEP = "android.intent.action.QUICKSTEP_SERVICE"; - private static final String PREF_KEY_VIDEO = "gesture_swipe_up_video"; - private final UserManager mUserManager; - - public SwipeUpPreferenceController(Context context, String key) { - super(context, key); - mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE); - } - - static boolean isGestureAvailable(Context context) { - if (!context.getResources().getBoolean(R.bool.config_swipe_up_gesture_setting_available)) { - return false; - } - - final ComponentName recentsComponentName = ComponentName.unflattenFromString( - context.getString(R.string.config_recentsComponentName)); - final Intent quickStepIntent = new Intent(ACTION_QUICKSTEP) - .setPackage(recentsComponentName.getPackageName()); - if (context.getPackageManager().resolveService(quickStepIntent, - PackageManager.MATCH_SYSTEM_ONLY) == null) { - return false; - } - return true; - } - - @Override - public int getAvailabilityStatus() { - return isGestureAvailable(mContext) ? AVAILABLE : UNSUPPORTED_ON_DEVICE; - } - - @Override - public boolean isSliceable() { - return TextUtils.equals(getPreferenceKey(), "gesture_swipe_up"); - } - - @Override - protected String getVideoPrefKey() { - return PREF_KEY_VIDEO; - } - - @Override - public boolean setChecked(boolean isChecked) { - setSwipeUpPreference(mContext, mUserManager, isChecked ? ON : OFF); - return true; - } - - public static void setSwipeUpPreference(Context context, UserManager userManager, - int enabled) { - Settings.Secure.putInt(context.getContentResolver(), - Settings.Secure.SWIPE_UP_TO_SWITCH_APPS_ENABLED, enabled); - } - - @Override - public boolean isChecked() { - final int defaultValue = mContext.getResources() - .getBoolean(R.bool.config_swipe_up_gesture_default) ? ON : OFF; - final int swipeUpEnabled = Settings.Secure.getInt(mContext.getContentResolver(), - Settings.Secure.SWIPE_UP_TO_SWITCH_APPS_ENABLED, defaultValue); - return swipeUpEnabled != OFF; - } -} diff --git a/src/com/android/settings/gestures/SystemNavigationGestureSettings.java b/src/com/android/settings/gestures/SystemNavigationGestureSettings.java new file mode 100644 index 0000000000..b3d090d75f --- /dev/null +++ b/src/com/android/settings/gestures/SystemNavigationGestureSettings.java @@ -0,0 +1,391 @@ +/* + * Copyright (C) 2018 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.gestures; + +import static android.os.UserHandle.USER_CURRENT; +import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_2BUTTON_OVERLAY; +import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY; +import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY; + +import static com.android.settings.widget.RadioButtonPreferenceWithExtraWidget.EXTRA_WIDGET_VISIBILITY_GONE; +import static com.android.settings.widget.RadioButtonPreferenceWithExtraWidget.EXTRA_WIDGET_VISIBILITY_INFO; +import static com.android.settings.widget.RadioButtonPreferenceWithExtraWidget.EXTRA_WIDGET_VISIBILITY_SETTING; + +import android.accessibilityservice.AccessibilityServiceInfo; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.om.IOverlayManager; +import android.content.om.OverlayInfo; +import android.graphics.drawable.Drawable; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.provider.SearchIndexableResource; +import android.provider.Settings; +import android.text.TextUtils; +import android.view.accessibility.AccessibilityManager; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.PreferenceScreen; + +import com.android.settings.SettingsTutorialDialogWrapperActivity; +import com.android.settings.R; +import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider; +import com.android.settings.overlay.FeatureFactory; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; +import com.android.settings.widget.RadioButtonPickerFragment; +import com.android.settings.widget.RadioButtonPreference; +import com.android.settings.widget.RadioButtonPreferenceWithExtraWidget; +import com.android.settings.widget.VideoPreference; +import com.android.settingslib.search.SearchIndexable; +import com.android.settingslib.widget.CandidateInfo; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@SearchIndexable +public class SystemNavigationGestureSettings extends RadioButtonPickerFragment { + + private static final String TAG = "SystemNavigationGesture"; + + @VisibleForTesting + static final String SHARED_PREFERENCES_NAME = "system_navigation_settings_preferences"; + @VisibleForTesting + static final String PREFS_BACK_SENSITIVITY_KEY = "system_navigation_back_sensitivity"; + + @VisibleForTesting + static final String KEY_SYSTEM_NAV_3BUTTONS = "system_nav_3buttons"; + @VisibleForTesting + static final String KEY_SYSTEM_NAV_2BUTTONS = "system_nav_2buttons"; + @VisibleForTesting + static final String KEY_SYSTEM_NAV_GESTURAL = "system_nav_gestural"; + + public static final String PREF_KEY_SUGGESTION_COMPLETE = + "pref_system_navigation_suggestion_complete"; + + @VisibleForTesting + static final String NAV_BAR_MODE_GESTURAL_OVERLAY_NARROW_BACK + = "com.android.internal.systemui.navbar.gestural_narrow_back"; + @VisibleForTesting + static final String NAV_BAR_MODE_GESTURAL_OVERLAY_WIDE_BACK + = "com.android.internal.systemui.navbar.gestural_wide_back"; + @VisibleForTesting + static final String NAV_BAR_MODE_GESTURAL_OVERLAY_EXTRA_WIDE_BACK + = "com.android.internal.systemui.navbar.gestural_extra_wide_back"; + @VisibleForTesting + static final String[] BACK_GESTURE_INSET_OVERLAYS = { + NAV_BAR_MODE_GESTURAL_OVERLAY_NARROW_BACK, + NAV_BAR_MODE_GESTURAL_OVERLAY, + NAV_BAR_MODE_GESTURAL_OVERLAY_WIDE_BACK, + NAV_BAR_MODE_GESTURAL_OVERLAY_EXTRA_WIDE_BACK + }; + @VisibleForTesting + static int BACK_GESTURE_INSET_DEFAULT_OVERLAY = 1; + + private IOverlayManager mOverlayManager; + + private VideoPreference mVideoPreference; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + SuggestionFeatureProvider suggestionFeatureProvider = FeatureFactory.getFactory(context) + .getSuggestionFeatureProvider(context); + SharedPreferences prefs = suggestionFeatureProvider.getSharedPrefs(context); + prefs.edit().putBoolean(PREF_KEY_SUGGESTION_COMPLETE, true).apply(); + + mOverlayManager = IOverlayManager.Stub.asInterface( + ServiceManager.getService(Context.OVERLAY_SERVICE)); + + mVideoPreference = new VideoPreference(context); + setIllustrationVideo(mVideoPreference, getDefaultKey()); + mVideoPreference.setHeight( /* Illustration height in dp */ + getResources().getDimension(R.dimen.system_navigation_illustration_height) + / getResources().getDisplayMetrics().density); + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.SETTINGS_GESTURE_SWIPE_UP; + } + + @Override + public void updateCandidates() { + final String defaultKey = getDefaultKey(); + final String systemDefaultKey = getSystemDefaultKey(); + final PreferenceScreen screen = getPreferenceScreen(); + screen.removeAll(); + screen.addPreference(mVideoPreference); + + final List<? extends CandidateInfo> candidateList = getCandidates(); + if (candidateList == null) { + return; + } + for (CandidateInfo info : candidateList) { + RadioButtonPreferenceWithExtraWidget pref = + new RadioButtonPreferenceWithExtraWidget(getPrefContext()); + bindPreference(pref, info.getKey(), info, defaultKey); + bindPreferenceExtra(pref, info.getKey(), info, defaultKey, systemDefaultKey); + screen.addPreference(pref); + } + mayCheckOnlyRadioButton(); + } + + @Override + public void bindPreferenceExtra(RadioButtonPreference pref, + String key, CandidateInfo info, String defaultKey, String systemDefaultKey) { + if (!(info instanceof NavModeCandidateInfo) + || !(pref instanceof RadioButtonPreferenceWithExtraWidget)) { + return; + } + + pref.setSummary(((NavModeCandidateInfo) info).loadSummary()); + + RadioButtonPreferenceWithExtraWidget p = (RadioButtonPreferenceWithExtraWidget) pref; + if (info.getKey() == KEY_SYSTEM_NAV_GESTURAL) { + if (SystemNavigationPreferenceController.isGestureNavSupportedByDefaultLauncher( + getContext())) { + p.setExtraWidgetVisibility(EXTRA_WIDGET_VISIBILITY_SETTING); + p.setExtraWidgetOnClickListener((v) -> GestureNavigationBackSensitivityDialog + .show(this, getBackSensitivity(getContext(), mOverlayManager))); + } else { + p.setEnabled(false); + p.setExtraWidgetVisibility(EXTRA_WIDGET_VISIBILITY_INFO); + p.setExtraWidgetOnClickListener((v) -> + GestureNavigationNotAvailableDialog.show(this)); + } + } else { + p.setExtraWidgetVisibility(EXTRA_WIDGET_VISIBILITY_GONE); + } + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.system_navigation_gesture_settings; + } + + @Override + protected List<? extends CandidateInfo> getCandidates() { + final Context c = getContext(); + List<NavModeCandidateInfo> candidates = new ArrayList<>(); + + if (SystemNavigationPreferenceController.isOverlayPackageAvailable(c, + NAV_BAR_MODE_GESTURAL_OVERLAY)) { + candidates.add(new NavModeCandidateInfo( + c.getText(R.string.edge_to_edge_navigation_title), + c.getText(R.string.edge_to_edge_navigation_summary), + KEY_SYSTEM_NAV_GESTURAL, true /* enabled */)); + } + if (SystemNavigationPreferenceController.isOverlayPackageAvailable(c, + NAV_BAR_MODE_2BUTTON_OVERLAY)) { + candidates.add(new NavModeCandidateInfo( + c.getText(R.string.swipe_up_to_switch_apps_title), + c.getText(R.string.swipe_up_to_switch_apps_summary), + KEY_SYSTEM_NAV_2BUTTONS, true /* enabled */)); + } + if (SystemNavigationPreferenceController.isOverlayPackageAvailable(c, + NAV_BAR_MODE_3BUTTON_OVERLAY)) { + candidates.add(new NavModeCandidateInfo( + c.getText(R.string.legacy_navigation_title), + c.getText(R.string.legacy_navigation_summary), + KEY_SYSTEM_NAV_3BUTTONS, true /* enabled */)); + } + + return candidates; + } + + @Override + protected String getDefaultKey() { + return getCurrentSystemNavigationMode(getContext()); + } + + @Override + protected boolean setDefaultKey(String key) { + final Context c = getContext(); + if (key == KEY_SYSTEM_NAV_GESTURAL && + !SystemNavigationPreferenceController.isGestureNavSupportedByDefaultLauncher(c)) { + // This should not happen since the preference is disabled. Return to be safe. + return false; + } + + setCurrentSystemNavigationMode(c, mOverlayManager, key); + setIllustrationVideo(mVideoPreference, key); + if (TextUtils.equals(KEY_SYSTEM_NAV_GESTURAL, key) && ( + isAnyServiceSupportAccessibilityButton() || isNavBarMagnificationEnabled())) { + Intent intent = new Intent(getActivity(), SettingsTutorialDialogWrapperActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + } + return true; + } + + @VisibleForTesting + static void setBackSensitivity(Context context, IOverlayManager overlayManager, + int sensitivity) { + if (sensitivity < 0 || sensitivity >= BACK_GESTURE_INSET_OVERLAYS.length) { + throw new IllegalArgumentException("Sensitivity out of range."); + } + + // Store the sensitivity level, to be able to restore when user returns to Gesture Nav mode + context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE).edit() + .putInt(PREFS_BACK_SENSITIVITY_KEY, sensitivity).apply(); + if (getCurrentSystemNavigationMode(context) == KEY_SYSTEM_NAV_GESTURAL) { + setNavBarInteractionMode(overlayManager, BACK_GESTURE_INSET_OVERLAYS[sensitivity]); + } + } + + @VisibleForTesting + static int getBackSensitivity(Context context, IOverlayManager overlayManager) { + for (int i = 0; i < BACK_GESTURE_INSET_OVERLAYS.length; i++) { + OverlayInfo info = null; + try { + info = overlayManager.getOverlayInfo(BACK_GESTURE_INSET_OVERLAYS[i], USER_CURRENT); + } catch (RemoteException e) { /* Do nothing */ } + if (info != null && info.isEnabled()) { + return i; + } + } + // If Gesture nav is not selected, read the value from shared preferences. + return context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE) + .getInt(PREFS_BACK_SENSITIVITY_KEY, BACK_GESTURE_INSET_DEFAULT_OVERLAY); + } + + @VisibleForTesting + static String getCurrentSystemNavigationMode(Context context) { + if (SystemNavigationPreferenceController.isEdgeToEdgeEnabled(context)) { + return KEY_SYSTEM_NAV_GESTURAL; + } else if (SystemNavigationPreferenceController.isSwipeUpEnabled(context)) { + return KEY_SYSTEM_NAV_2BUTTONS; + } else { + return KEY_SYSTEM_NAV_3BUTTONS; + } + } + + @VisibleForTesting + static void setCurrentSystemNavigationMode(Context context, IOverlayManager overlayManager, + String key) { + switch (key) { + case KEY_SYSTEM_NAV_GESTURAL: + int sensitivity = getBackSensitivity(context, overlayManager); + setNavBarInteractionMode(overlayManager, BACK_GESTURE_INSET_OVERLAYS[sensitivity]); + break; + case KEY_SYSTEM_NAV_2BUTTONS: + setNavBarInteractionMode(overlayManager, NAV_BAR_MODE_2BUTTON_OVERLAY); + break; + case KEY_SYSTEM_NAV_3BUTTONS: + setNavBarInteractionMode(overlayManager, NAV_BAR_MODE_3BUTTON_OVERLAY); + break; + } + } + + private static void setNavBarInteractionMode(IOverlayManager overlayManager, + String overlayPackage) { + try { + overlayManager.setEnabledExclusiveInCategory(overlayPackage, USER_CURRENT); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + private static void setIllustrationVideo(VideoPreference videoPref, String systemNavKey) { + videoPref.setVideo(0, 0); + switch (systemNavKey) { + case KEY_SYSTEM_NAV_GESTURAL: + videoPref.setVideo(R.raw.system_nav_fully_gestural, + R.drawable.system_nav_fully_gestural); + break; + case KEY_SYSTEM_NAV_2BUTTONS: + videoPref.setVideo(R.raw.system_nav_2_button, R.drawable.system_nav_2_button); + break; + case KEY_SYSTEM_NAV_3BUTTONS: + videoPref.setVideo(R.raw.system_nav_3_button, R.drawable.system_nav_3_button); + break; + } + } + + private boolean isAnyServiceSupportAccessibilityButton() { + final AccessibilityManager ams = (AccessibilityManager) getContext().getSystemService( + Context.ACCESSIBILITY_SERVICE); + final List<AccessibilityServiceInfo> services = ams.getEnabledAccessibilityServiceList( + AccessibilityServiceInfo.FEEDBACK_ALL_MASK); + + for (AccessibilityServiceInfo info : services) { + if ((info.flags & AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0) { + return true; + } + } + + return false; + } + + private boolean isNavBarMagnificationEnabled() { + return Settings.Secure.getInt(getContext().getContentResolver(), + Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED, 0) == 1; + } + + static class NavModeCandidateInfo extends CandidateInfo { + private final CharSequence mLabel; + private final CharSequence mSummary; + private final String mKey; + + NavModeCandidateInfo(CharSequence label, CharSequence summary, String key, + boolean enabled) { + super(enabled); + mLabel = label; + mSummary = summary; + mKey = key; + } + + @Override + public CharSequence loadLabel() { + return mLabel; + } + + public CharSequence loadSummary() { + return mSummary; + } + + @Override + public Drawable loadIcon() { + return null; + } + + @Override + public String getKey() { + return mKey; + } + } + + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex( + Context context, boolean enabled) { + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.system_navigation_gesture_settings; + return Arrays.asList(sir); + } + + @Override + protected boolean isPageSearchEnabled(Context context) { + return SystemNavigationPreferenceController.isGestureAvailable(context); + } + }; +} diff --git a/src/com/android/settings/gestures/SystemNavigationPreferenceController.java b/src/com/android/settings/gestures/SystemNavigationPreferenceController.java new file mode 100644 index 0000000000..a151dc1746 --- /dev/null +++ b/src/com/android/settings/gestures/SystemNavigationPreferenceController.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2019 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.gestures; + +import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_2BUTTON; +import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; + +import java.util.ArrayList; + +public class SystemNavigationPreferenceController extends BasePreferenceController { + + static final String PREF_KEY_SYSTEM_NAVIGATION = "gesture_system_navigation"; + private static final String ACTION_QUICKSTEP = "android.intent.action.QUICKSTEP_SERVICE"; + + public SystemNavigationPreferenceController(Context context, String key) { + super(context, key); + } + + @Override + public int getAvailabilityStatus() { + return isGestureAvailable(mContext) ? AVAILABLE : UNSUPPORTED_ON_DEVICE; + } + + @Override + public CharSequence getSummary() { + if (isEdgeToEdgeEnabled(mContext)) { + return mContext.getText(R.string.edge_to_edge_navigation_title); + } else if (isSwipeUpEnabled(mContext)) { + return mContext.getText(R.string.swipe_up_to_switch_apps_title); + } else { + return mContext.getText(R.string.legacy_navigation_title); + } + } + + static boolean isGestureAvailable(Context context) { + // Skip if the swipe up settings are not available + if (!context.getResources().getBoolean( + com.android.internal.R.bool.config_swipe_up_gesture_setting_available)) { + return false; + } + + // Skip if the recents component is not defined + final ComponentName recentsComponentName = ComponentName.unflattenFromString( + context.getString(com.android.internal.R.string.config_recentsComponentName)); + if (recentsComponentName == null) { + return false; + } + + // Skip if the overview proxy service exists + final Intent quickStepIntent = new Intent(ACTION_QUICKSTEP) + .setPackage(recentsComponentName.getPackageName()); + if (context.getPackageManager().resolveService(quickStepIntent, + PackageManager.MATCH_SYSTEM_ONLY) == null) { + return false; + } + + return true; + } + + static boolean isOverlayPackageAvailable(Context context, String overlayPackage) { + try { + return context.getPackageManager().getPackageInfo(overlayPackage, 0) != null; + } catch (PackageManager.NameNotFoundException e) { + // Not found, just return unavailable + return false; + } + } + + static boolean isSwipeUpEnabled(Context context) { + if (isEdgeToEdgeEnabled(context)) { + return false; + } + return NAV_BAR_MODE_2BUTTON == context.getResources().getInteger( + com.android.internal.R.integer.config_navBarInteractionMode); + } + + static boolean isEdgeToEdgeEnabled(Context context) { + return NAV_BAR_MODE_GESTURAL == context.getResources().getInteger( + com.android.internal.R.integer.config_navBarInteractionMode); + } + + static boolean isGestureNavSupportedByDefaultLauncher(Context context) { + final ComponentName cn = context.getPackageManager().getHomeActivities(new ArrayList<>()); + if (cn == null) { + // There is no default home app set for the current user, don't make any changes yet. + return true; + } + ComponentName recentsComponentName = ComponentName.unflattenFromString(context.getString( + com.android.internal.R.string.config_recentsComponentName)); + return recentsComponentName.getPackageName().equals(cn.getPackageName()); + } + + static String getDefaultHomeAppName(Context context) { + final PackageManager pm = context.getPackageManager(); + final ComponentName cn = pm.getHomeActivities(new ArrayList<>()); + if (cn != null) { + try { + ApplicationInfo ai = pm.getApplicationInfo(cn.getPackageName(), 0); + if (ai != null) { + return pm.getApplicationLabel(ai).toString(); + } + } catch (final PackageManager.NameNotFoundException e) { + // Do nothing + } + } + return ""; + } +} diff --git a/src/com/android/settings/gestures/TapScreenGesturePreferenceController.java b/src/com/android/settings/gestures/TapScreenGesturePreferenceController.java new file mode 100644 index 0000000000..ba2b86973f --- /dev/null +++ b/src/com/android/settings/gestures/TapScreenGesturePreferenceController.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2019 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.gestures; + +import static android.provider.Settings.Secure.DOZE_TAP_SCREEN_GESTURE; + +import android.annotation.UserIdInt; +import android.content.Context; +import android.hardware.display.AmbientDisplayConfiguration; +import android.os.UserHandle; +import android.provider.Settings; +import android.text.TextUtils; + +public class TapScreenGesturePreferenceController extends GesturePreferenceController { + + private static final String PREF_KEY_VIDEO = "gesture_tap_screen_video"; + + private AmbientDisplayConfiguration mAmbientConfig; + @UserIdInt + private final int mUserId; + + public TapScreenGesturePreferenceController(Context context, String key) { + super(context, key); + mUserId = UserHandle.myUserId(); + } + + public TapScreenGesturePreferenceController setConfig(AmbientDisplayConfiguration config) { + mAmbientConfig = config; + return this; + } + + @Override + public int getAvailabilityStatus() { + // No hardware support for this Gesture + if (!getAmbientConfig().tapSensorAvailable()) { + return UNSUPPORTED_ON_DEVICE; + } + + return AVAILABLE; + } + + @Override + public boolean isSliceable() { + return true; + } + + @Override + protected String getVideoPrefKey() { + return PREF_KEY_VIDEO; + } + + @Override + public CharSequence getSummary() { + return super.getSummary(); + } + + @Override + public boolean isChecked() { + return getAmbientConfig().tapGestureEnabled(mUserId); + } + + @Override + public boolean setChecked(boolean isChecked) { + return Settings.Secure.putInt(mContext.getContentResolver(), DOZE_TAP_SCREEN_GESTURE, + isChecked ? 1 : 0); + } + + private AmbientDisplayConfiguration getAmbientConfig() { + if (mAmbientConfig == null) { + mAmbientConfig = new AmbientDisplayConfiguration(mContext); + } + return mAmbientConfig; + } +}
\ No newline at end of file diff --git a/src/com/android/settings/gestures/SwipeUpGestureSettings.java b/src/com/android/settings/gestures/TapScreenGestureSettings.java index 1fe74c6c72..a86e6820af 100644 --- a/src/com/android/settings/gestures/SwipeUpGestureSettings.java +++ b/src/com/android/settings/gestures/TapScreenGestureSettings.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018 The Android Open Source Project + * Copyright (C) 2019 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. @@ -11,13 +11,14 @@ * 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. + * limitations under the License */ package com.android.settings.gestures; import android.content.Context; import android.content.SharedPreferences; +import android.hardware.display.AmbientDisplayConfiguration; import android.provider.SearchIndexableResource; import com.android.internal.logging.nano.MetricsProto; @@ -26,16 +27,18 @@ import com.android.settings.dashboard.DashboardFragment; import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider; import com.android.settings.overlay.FeatureFactory; import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; +import com.android.settingslib.search.SearchIndexable; import java.util.Arrays; import java.util.List; -public class SwipeUpGestureSettings extends DashboardFragment { - - private static final String TAG = "SwipeUpGesture"; +@SearchIndexable +public class TapScreenGestureSettings extends DashboardFragment { + private static final String TAG = "TapScreenGestureSettings"; public static final String PREF_KEY_SUGGESTION_COMPLETE = - "pref_swipe_up_suggestion_complete"; + "pref_tap_gesture_suggestion_complete"; @Override public void onAttach(Context context) { @@ -44,11 +47,14 @@ public class SwipeUpGestureSettings extends DashboardFragment { .getSuggestionFeatureProvider(context); SharedPreferences prefs = suggestionFeatureProvider.getSharedPrefs(context); prefs.edit().putBoolean(PREF_KEY_SUGGESTION_COMPLETE, true).apply(); + + use(TapScreenGesturePreferenceController.class) + .setConfig(new AmbientDisplayConfiguration(context)); } @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.SETTINGS_GESTURE_SWIPE_UP; + return MetricsProto.MetricsEvent.SETTINGS_GESTURE_TAP_SCREEN; } @Override @@ -58,22 +64,17 @@ public class SwipeUpGestureSettings extends DashboardFragment { @Override protected int getPreferenceScreenResId() { - return R.xml.swipe_up_gesture_settings; + return R.xml.tap_screen_gesture_settings; } - public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = new BaseSearchIndexProvider() { @Override public List<SearchIndexableResource> getXmlResourcesToIndex( Context context, boolean enabled) { final SearchIndexableResource sir = new SearchIndexableResource(context); - sir.xmlResId = R.xml.swipe_up_gesture_settings; + sir.xmlResId = R.xml.tap_screen_gesture_settings; return Arrays.asList(sir); } - - @Override - protected boolean isPageSearchEnabled(Context context) { - return SwipeUpPreferenceController.isGestureAvailable(context); - } }; } diff --git a/src/com/android/settings/homepage/SettingsHomepageActivity.java b/src/com/android/settings/homepage/SettingsHomepageActivity.java new file mode 100644 index 0000000000..fa231012f9 --- /dev/null +++ b/src/com/android/settings/homepage/SettingsHomepageActivity.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2018 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.homepage; + +import android.animation.LayoutTransition; +import android.app.ActivityManager; +import android.app.settings.SettingsEnums; +import android.os.Bundle; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.Toolbar; + +import androidx.annotation.VisibleForTesting; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; + +import com.android.settings.R; +import com.android.settings.accounts.AvatarViewMixin; +import com.android.settings.homepage.contextualcards.ContextualCardsFragment; +import com.android.settings.overlay.FeatureFactory; + +public class SettingsHomepageActivity extends FragmentActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.settings_homepage_container); + final View root = findViewById(R.id.settings_homepage_container); + root.setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); + + setHomepageContainerPaddingTop(); + + final Toolbar toolbar = findViewById(R.id.search_action_bar); + FeatureFactory.getFactory(this).getSearchFeatureProvider() + .initSearchToolbar(this /* activity */, toolbar, SettingsEnums.SETTINGS_HOMEPAGE); + + final ImageView avatarView = findViewById(R.id.account_avatar); + final AvatarViewMixin avatarViewMixin = new AvatarViewMixin(this, avatarView); + getLifecycle().addObserver(avatarViewMixin); + + if (!getSystemService(ActivityManager.class).isLowRamDevice()) { + // Only allow contextual feature on high ram devices. + showFragment(new ContextualCardsFragment(), R.id.contextual_cards_content); + } + showFragment(new TopLevelSettings(), R.id.main_content); + ((FrameLayout) findViewById(R.id.main_content)) + .getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING); + } + + private void showFragment(Fragment fragment, int id) { + final FragmentManager fragmentManager = getSupportFragmentManager(); + final FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); + final Fragment showFragment = fragmentManager.findFragmentById(id); + + if (showFragment == null) { + fragmentTransaction.add(id, fragment); + } else { + fragmentTransaction.show(showFragment); + } + fragmentTransaction.commit(); + } + + @VisibleForTesting + void setHomepageContainerPaddingTop() { + final View view = this.findViewById(R.id.homepage_container); + + final int searchBarHeight = getResources().getDimensionPixelSize(R.dimen.search_bar_height); + final int searchBarMargin = getResources().getDimensionPixelSize(R.dimen.search_bar_margin); + + // The top padding is the height of action bar(48dp) + top/bottom margins(16dp) + final int paddingTop = searchBarHeight + searchBarMargin * 2; + view.setPadding(0 /* left */, paddingTop, 0 /* right */, 0 /* bottom */); + } +}
\ No newline at end of file diff --git a/src/com/android/settings/homepage/TopLevelSettings.java b/src/com/android/settings/homepage/TopLevelSettings.java new file mode 100644 index 0000000000..f5dee0c256 --- /dev/null +++ b/src/com/android/settings/homepage/TopLevelSettings.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2018 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.homepage; + +import static com.android.settings.search.actionbar.SearchMenuController.NEED_SEARCH_ICON_IN_ACTION_BAR; +import static com.android.settingslib.search.SearchIndexable.MOBILE; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.os.Bundle; +import android.provider.SearchIndexableResource; + +import androidx.fragment.app.Fragment; +import androidx.preference.Preference; +import androidx.preference.PreferenceFragmentCompat; + +import com.android.settings.R; +import com.android.settings.core.SubSettingLauncher; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.support.SupportPreferenceController; +import com.android.settingslib.core.instrumentation.Instrumentable; +import com.android.settingslib.search.SearchIndexable; + +import java.util.Arrays; +import java.util.List; + +@SearchIndexable(forTarget = MOBILE) +public class TopLevelSettings extends DashboardFragment implements + PreferenceFragmentCompat.OnPreferenceStartFragmentCallback { + + private static final String TAG = "TopLevelSettings"; + + public TopLevelSettings() { + final Bundle args = new Bundle(); + // Disable the search icon because this page uses a full search view in actionbar. + args.putBoolean(NEED_SEARCH_ICON_IN_ACTION_BAR, false); + setArguments(args); + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.top_level_settings; + } + + @Override + protected String getLogTag() { + return TAG; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.DASHBOARD_SUMMARY; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + use(SupportPreferenceController.class).setActivity(getActivity()); + } + + @Override + public int getHelpResource() { + // Disable the help icon because this page uses a full search view in actionbar. + return 0; + } + + @Override + public Fragment getCallbackFragment() { + return this; + } + + @Override + public boolean onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref) { + new SubSettingLauncher(getActivity()) + .setDestination(pref.getFragment()) + .setArguments(pref.getExtras()) + .setSourceMetricsCategory(caller instanceof Instrumentable + ? ((Instrumentable) caller).getMetricsCategory() + : Instrumentable.METRICS_CATEGORY_UNKNOWN) + .setTitleRes(-1) + .launch(); + return true; + } + + @Override + protected boolean shouldForceRoundedIcon() { + return getContext().getResources() + .getBoolean(R.bool.config_force_rounded_icon_TopLevelSettings); + } + + public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex( + Context context, boolean enabled) { + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.top_level_settings; + return Arrays.asList(sir); + } + + @Override + protected boolean isPageSearchEnabled(Context context) { + // Never searchable, all entries in this page are already indexed elsewhere. + return false; + } + }; +} diff --git a/src/com/android/settings/homepage/contextualcards/CardContentProvider.java b/src/com/android/settings/homepage/contextualcards/CardContentProvider.java new file mode 100644 index 0000000000..a9a832d664 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/CardContentProvider.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards; + +import android.content.ContentProvider; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.UriMatcher; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteQueryBuilder; +import android.net.Uri; +import android.os.Build; +import android.os.StrictMode; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; + +import com.android.settingslib.utils.ThreadUtils; + +/** + * Provider stores and manages user interaction feedback for homepage contextual cards. + */ +public class CardContentProvider extends ContentProvider { + + public static final String CARD_AUTHORITY = "com.android.settings.homepage.CardContentProvider"; + + public static final Uri REFRESH_CARD_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(CardContentProvider.CARD_AUTHORITY) + .appendPath(CardDatabaseHelper.CARD_TABLE) + .build(); + + public static final Uri DELETE_CARD_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(CardContentProvider.CARD_AUTHORITY) + .appendPath(CardDatabaseHelper.CardColumns.CARD_DISMISSED) + .build(); + + private static final String TAG = "CardContentProvider"; + /** URI matcher for ContentProvider queries. */ + private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH); + /** URI matcher type for cards table */ + private static final int MATCH_CARDS = 100; + + static { + URI_MATCHER.addURI(CARD_AUTHORITY, CardDatabaseHelper.CARD_TABLE, MATCH_CARDS); + } + + private CardDatabaseHelper mDBHelper; + + @Override + public boolean onCreate() { + mDBHelper = CardDatabaseHelper.getInstance(getContext()); + return true; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + final ContentValues[] cvs = {values}; + bulkInsert(uri, cvs); + return uri; + } + + @Override + public int bulkInsert(Uri uri, ContentValues[] values) { + final StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy(); + int numInserted = 0; + final SQLiteDatabase database = mDBHelper.getWritableDatabase(); + + try { + maybeEnableStrictMode(); + + final String table = getTableFromMatch(uri); + database.beginTransaction(); + + // Here deletion first is avoiding redundant insertion. According to cl/215350754 + database.delete(table, null /* whereClause */, null /* whereArgs */); + for (ContentValues value : values) { + long ret = database.insert(table, null /* nullColumnHack */, value); + if (ret != -1L) { + numInserted++; + } else { + Log.e(TAG, "The row " + value.getAsString(CardDatabaseHelper.CardColumns.NAME) + + " insertion failed! Please check your data."); + } + } + database.setTransactionSuccessful(); + getContext().getContentResolver().notifyChange(uri, null /* observer */); + } finally { + database.endTransaction(); + StrictMode.setThreadPolicy(oldPolicy); + } + return numInserted; + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException("delete operation not supported currently."); + } + + @Override + public String getType(Uri uri) { + throw new UnsupportedOperationException("getType operation not supported currently."); + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { + final StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy(); + try { + maybeEnableStrictMode(); + + final SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); + final String table = getTableFromMatch(uri); + queryBuilder.setTables(table); + final SQLiteDatabase database = mDBHelper.getReadableDatabase(); + final Cursor cursor = queryBuilder.query(database, + projection, selection, selectionArgs, null /* groupBy */, null /* having */, + sortOrder); + + cursor.setNotificationUri(getContext().getContentResolver(), uri); + return cursor; + } finally { + StrictMode.setThreadPolicy(oldPolicy); + } + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException("update operation not supported currently."); + } + + @VisibleForTesting + void maybeEnableStrictMode() { + if (Build.IS_DEBUGGABLE && ThreadUtils.isMainThread()) { + enableStrictMode(); + } + } + + @VisibleForTesting + void enableStrictMode() { + StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectAll().build()); + } + + @VisibleForTesting + String getTableFromMatch(Uri uri) { + final int match = URI_MATCHER.match(uri); + String table; + switch (match) { + case MATCH_CARDS: + table = CardDatabaseHelper.CARD_TABLE; + break; + default: + throw new IllegalArgumentException("Unknown Uri format: " + uri); + } + return table; + } +} diff --git a/src/com/android/settings/homepage/contextualcards/CardDatabaseHelper.java b/src/com/android/settings/homepage/contextualcards/CardDatabaseHelper.java new file mode 100644 index 0000000000..39c48c1e84 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/CardDatabaseHelper.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; + +/** + * Defines the schema for the Homepage Cards database. + */ +public class CardDatabaseHelper extends SQLiteOpenHelper { + private static final String TAG = "CardDatabaseHelper"; + private static final String DATABASE_NAME = "homepage_cards.db"; + private static final int DATABASE_VERSION = 5; + + public static final String CARD_TABLE = "cards"; + + public interface CardColumns { + /** + * Primary key. Name of the card. + */ + String NAME = "name"; + + /** + * Type of the card. + */ + String TYPE = "type"; + + /** + * Score of the card. Higher numbers have higher priorities. + */ + String SCORE = "score"; + + /** + * URI of the slice card. + */ + String SLICE_URI = "slice_uri"; + + /** + * Category of the card. + */ + String CATEGORY = "category"; + + /** + * Keep the card last display's locale. + */ + String LOCALIZED_TO_LOCALE = "localized_to_locale"; + + /** + * Package name for all card candidates. + */ + String PACKAGE_NAME = "package_name"; + + /** + * Application version of the package. + */ + String APP_VERSION = "app_version"; + + /** + * Title resource name of the package. + */ + String TITLE_RES_NAME = "title_res_name"; + + /** + * Title of the package to be shown. + */ + String TITLE_TEXT = "title_text"; + + /** + * Summary resource name of the package. + */ + String SUMMARY_RES_NAME = "summary_res_name"; + + /** + * Summary of the package to be shown. + */ + String SUMMARY_TEXT = "summary_text"; + + /** + * Icon resource name of the package. + */ + String ICON_RES_NAME = "icon_res_name"; + + /** + * Icon resource id of the package. + */ + String ICON_RES_ID = "icon_res_id"; + + /** + * Key value mapping to Intent in Settings. Do action when user presses card. + */ + String CARD_ACTION = "card_action"; + + /** + * Expire time of the card. The unit of the value is mini-second. + */ + String EXPIRE_TIME_MS = "expire_time_ms"; + + /** + * Decide the card display full-length width or half-width in screen. + */ + String SUPPORT_HALF_WIDTH = "support_half_width"; + + /** + * Decide the card is dismissed or not. + */ + String CARD_DISMISSED = "card_dismissed"; + } + + private static final String CREATE_CARD_TABLE = + "CREATE TABLE " + CARD_TABLE + + "(" + + CardColumns.NAME + + " TEXT NOT NULL PRIMARY KEY, " + + CardColumns.TYPE + + " INTEGER NOT NULL, " + + CardColumns.SCORE + + " DOUBLE NOT NULL, " + + CardColumns.SLICE_URI + + " TEXT, " + + CardColumns.CATEGORY + + " INTEGER DEFAULT 0, " + + CardColumns.LOCALIZED_TO_LOCALE + + " TEXT, " + + CardColumns.PACKAGE_NAME + + " TEXT NOT NULL, " + + CardColumns.APP_VERSION + + " INTEGER NOT NULL, " + + CardColumns.TITLE_RES_NAME + + " TEXT, " + + CardColumns.TITLE_TEXT + + " TEXT, " + + CardColumns.SUMMARY_RES_NAME + + " TEXT, " + + CardColumns.SUMMARY_TEXT + + " TEXT, " + + CardColumns.ICON_RES_NAME + + " TEXT, " + + CardColumns.ICON_RES_ID + + " INTEGER DEFAULT 0, " + + CardColumns.CARD_ACTION + + " INTEGER, " + + CardColumns.EXPIRE_TIME_MS + + " INTEGER, " + + CardColumns.SUPPORT_HALF_WIDTH + + " INTEGER DEFAULT 0, " + + CardColumns.CARD_DISMISSED + + " INTEGER DEFAULT 0 " + + ");"; + + public CardDatabaseHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL(CREATE_CARD_TABLE); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion < newVersion) { + Log.d(TAG, "Reconstructing DB from " + oldVersion + " to " + newVersion); + db.execSQL("DROP TABLE IF EXISTS " + CARD_TABLE); + onCreate(db); + } + } + + @VisibleForTesting + static CardDatabaseHelper sCardDatabaseHelper; + + public static synchronized CardDatabaseHelper getInstance(Context context) { + if (sCardDatabaseHelper == null) { + sCardDatabaseHelper = new CardDatabaseHelper(context.getApplicationContext()); + } + return sCardDatabaseHelper; + } + + Cursor getContextualCards() { + final SQLiteDatabase db = getReadableDatabase(); + final String selection = CardColumns.CARD_DISMISSED + "=0"; + return db.query(CARD_TABLE, null /* columns */, selection, + null /* selectionArgs */, null /* groupBy */, null /* having */, + CardColumns.SCORE + " DESC" /* orderBy */); + } + + /** + * Mark a specific ContextualCard with dismissal flag in the database to indicate that the + * card has been dismissed. + * + * @param context Context + * @param cardName The card name of the ContextualCard which is dismissed by user. + * @return The number of rows updated + */ + public int markContextualCardAsDismissed(Context context, String cardName) { + final SQLiteDatabase database = getWritableDatabase(); + final ContentValues values = new ContentValues(); + values.put(CardColumns.CARD_DISMISSED, 1); + final String selection = CardColumns.NAME + "=?"; + final String[] selectionArgs = {cardName}; + final int rowsUpdated = database.update(CARD_TABLE, values, selection, selectionArgs); + database.close(); + context.getContentResolver().notifyChange(CardContentProvider.DELETE_CARD_URI, null); + return rowsUpdated; + } +} diff --git a/src/com/android/settings/homepage/contextualcards/ContextualCard.java b/src/com/android/settings/homepage/contextualcards/ContextualCard.java new file mode 100644 index 0000000000..ccfb22d3e1 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/ContextualCard.java @@ -0,0 +1,408 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards; + +import android.annotation.IntDef; +import android.database.Cursor; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.text.TextUtils; + +import androidx.annotation.LayoutRes; + +import com.android.settings.homepage.contextualcards.slices.SliceContextualCardRenderer; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Data class representing a {@link ContextualCard}. + */ +public class ContextualCard { + + /** + * Flags indicating the type of the ContextualCard. + */ + @IntDef({CardType.DEFAULT, CardType.SLICE, CardType.LEGACY_SUGGESTION, CardType.CONDITIONAL, + CardType.CONDITIONAL_HEADER, CardType.CONDITIONAL_FOOTER}) + @Retention(RetentionPolicy.SOURCE) + public @interface CardType { + int DEFAULT = 0; + int SLICE = 1; + int LEGACY_SUGGESTION = 2; + int CONDITIONAL = 3; + int CONDITIONAL_HEADER = 4; + int CONDITIONAL_FOOTER = 5; + } + + private final Builder mBuilder; + private final String mName; + @CardType + private final int mCardType; + private final double mRankingScore; + private final String mSliceUri; + private final int mCategory; + private final String mLocalizedToLocale; + private final String mPackageName; + private final long mAppVersion; + private final String mTitleResName; + private final String mTitleText; + private final String mSummaryResName; + private final String mSummaryText; + private final String mIconResName; + private final int mIconResId; + private final int mCardAction; + private final long mExpireTimeMS; + private final boolean mIsLargeCard; + private final Drawable mIconDrawable; + @LayoutRes + private final int mViewType; + private final boolean mIsPendingDismiss; + private final boolean mHasInlineAction; + + public String getName() { + return mName; + } + + public int getCardType() { + return mCardType; + } + + public double getRankingScore() { + return mRankingScore; + } + + public String getTextSliceUri() { + return mSliceUri; + } + + public Uri getSliceUri() { + return Uri.parse(mSliceUri); + } + + public int getCategory() { + return mCategory; + } + + public String getLocalizedToLocale() { + return mLocalizedToLocale; + } + + public String getPackageName() { + return mPackageName; + } + + public long getAppVersion() { + return mAppVersion; + } + + public String getTitleResName() { + return mTitleResName; + } + + public String getTitleText() { + return mTitleText; + } + + public String getSummaryResName() { + return mSummaryResName; + } + + public String getSummaryText() { + return mSummaryText; + } + + public String getIconResName() { + return mIconResName; + } + + public int getIconResId() { + return mIconResId; + } + + public int getCardAction() { + return mCardAction; + } + + public long getExpireTimeMS() { + return mExpireTimeMS; + } + + public Drawable getIconDrawable() { + return mIconDrawable; + } + + public boolean isLargeCard() { + return mIsLargeCard; + } + + boolean isCustomCard() { + return TextUtils.isEmpty(mSliceUri); + } + + public int getViewType() { + return mViewType; + } + + public boolean isPendingDismiss() { + return mIsPendingDismiss; + } + + public boolean hasInlineAction() { + return mHasInlineAction; + } + + public Builder mutate() { + return mBuilder; + } + + public ContextualCard(Builder builder) { + mBuilder = builder; + mName = builder.mName; + mCardType = builder.mCardType; + mRankingScore = builder.mRankingScore; + mSliceUri = builder.mSliceUri; + mCategory = builder.mCategory; + mLocalizedToLocale = builder.mLocalizedToLocale; + mPackageName = builder.mPackageName; + mAppVersion = builder.mAppVersion; + mTitleResName = builder.mTitleResName; + mTitleText = builder.mTitleText; + mSummaryResName = builder.mSummaryResName; + mSummaryText = builder.mSummaryText; + mIconResName = builder.mIconResName; + mIconResId = builder.mIconResId; + mCardAction = builder.mCardAction; + mExpireTimeMS = builder.mExpireTimeMS; + mIconDrawable = builder.mIconDrawable; + mIsLargeCard = builder.mIsLargeCard; + mViewType = builder.mViewType; + mIsPendingDismiss = builder.mIsPendingDismiss; + mHasInlineAction = builder.mHasInlineAction; + } + + ContextualCard(Cursor c) { + mBuilder = new Builder(); + mName = c.getString(c.getColumnIndex(CardDatabaseHelper.CardColumns.NAME)); + mBuilder.setName(mName); + mCardType = c.getInt(c.getColumnIndex(CardDatabaseHelper.CardColumns.TYPE)); + mBuilder.setCardType(mCardType); + mRankingScore = c.getDouble(c.getColumnIndex(CardDatabaseHelper.CardColumns.SCORE)); + mBuilder.setRankingScore(mRankingScore); + mSliceUri = c.getString(c.getColumnIndex(CardDatabaseHelper.CardColumns.SLICE_URI)); + mBuilder.setSliceUri(Uri.parse(mSliceUri)); + mCategory = c.getInt(c.getColumnIndex(CardDatabaseHelper.CardColumns.CATEGORY)); + mBuilder.setCategory(mCategory); + mLocalizedToLocale = c.getString( + c.getColumnIndex(CardDatabaseHelper.CardColumns.LOCALIZED_TO_LOCALE)); + mBuilder.setLocalizedToLocale(mLocalizedToLocale); + mPackageName = c.getString(c.getColumnIndex(CardDatabaseHelper.CardColumns.PACKAGE_NAME)); + mBuilder.setPackageName(mPackageName); + mAppVersion = c.getLong(c.getColumnIndex(CardDatabaseHelper.CardColumns.APP_VERSION)); + mBuilder.setAppVersion(mAppVersion); + mTitleResName = c.getString( + c.getColumnIndex(CardDatabaseHelper.CardColumns.TITLE_RES_NAME)); + mBuilder.setTitleResName(mTitleResName); + mTitleText = c.getString(c.getColumnIndex(CardDatabaseHelper.CardColumns.TITLE_TEXT)); + mBuilder.setTitleText(mTitleText); + mSummaryResName = c.getString( + c.getColumnIndex(CardDatabaseHelper.CardColumns.SUMMARY_RES_NAME)); + mBuilder.setSummaryResName(mSummaryResName); + mSummaryText = c.getString(c.getColumnIndex(CardDatabaseHelper.CardColumns.SUMMARY_TEXT)); + mBuilder.setSummaryText(mSummaryText); + mIconResName = c.getString(c.getColumnIndex(CardDatabaseHelper.CardColumns.ICON_RES_NAME)); + mBuilder.setIconResName(mIconResName); + mIconResId = c.getInt(c.getColumnIndex(CardDatabaseHelper.CardColumns.ICON_RES_ID)); + mBuilder.setIconResId(mIconResId); + mCardAction = c.getInt(c.getColumnIndex(CardDatabaseHelper.CardColumns.CARD_ACTION)); + mBuilder.setCardAction(mCardAction); + mExpireTimeMS = c.getLong(c.getColumnIndex(CardDatabaseHelper.CardColumns.EXPIRE_TIME_MS)); + mBuilder.setExpireTimeMS(mExpireTimeMS); + mIsLargeCard = false; + mBuilder.setIsLargeCard(mIsLargeCard); + mIconDrawable = null; + mBuilder.setIconDrawable(mIconDrawable); + mViewType = getViewTypeByCardType(mCardType); + mBuilder.setViewType(mViewType); + mIsPendingDismiss = false; + mBuilder.setIsPendingDismiss(mIsPendingDismiss); + mHasInlineAction = false; + mBuilder.setHasInlineAction(mHasInlineAction); + } + + @Override + public int hashCode() { + return mName.hashCode(); + } + + /** + * Note that {@link #mName} is treated as a primary key for this class and determines equality. + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof ContextualCard)) { + return false; + } + final ContextualCard that = (ContextualCard) obj; + + return TextUtils.equals(mName, that.mName); + } + + private int getViewTypeByCardType(int cardType) { + if (cardType == CardType.SLICE) { + return SliceContextualCardRenderer.VIEW_TYPE_FULL_WIDTH; + } + return 0; + } + + public static class Builder { + private String mName; + private int mCardType; + private double mRankingScore; + private String mSliceUri; + private int mCategory; + private String mLocalizedToLocale; + private String mPackageName; + private long mAppVersion; + private String mTitleResName; + private String mTitleText; + private String mSummaryResName; + private String mSummaryText; + private String mIconResName; + private int mIconResId; + private int mCardAction; + private long mExpireTimeMS; + private Drawable mIconDrawable; + private boolean mIsLargeCard; + @LayoutRes + private int mViewType; + private boolean mIsPendingDismiss; + private boolean mHasInlineAction; + + public Builder setName(String name) { + mName = name; + return this; + } + + public Builder setCardType(int cardType) { + mCardType = cardType; + return this; + } + + public Builder setRankingScore(double rankingScore) { + mRankingScore = rankingScore; + return this; + } + + public Builder setSliceUri(Uri sliceUri) { + mSliceUri = sliceUri.toString(); + return this; + } + + public Builder setCategory(int category) { + mCategory = category; + return this; + } + + public Builder setLocalizedToLocale(String localizedToLocale) { + mLocalizedToLocale = localizedToLocale; + return this; + } + + public Builder setPackageName(String packageName) { + mPackageName = packageName; + return this; + } + + public Builder setAppVersion(long appVersion) { + mAppVersion = appVersion; + return this; + } + + public Builder setTitleResName(String titleResName) { + mTitleResName = titleResName; + return this; + } + + public Builder setTitleText(String titleText) { + mTitleText = titleText; + return this; + } + + public Builder setSummaryResName(String summaryResName) { + mSummaryResName = summaryResName; + return this; + } + + public Builder setSummaryText(String summaryText) { + mSummaryText = summaryText; + return this; + } + + public Builder setIconResName(String iconResName) { + mIconResName = iconResName; + return this; + } + + public Builder setIconResId(int iconResId) { + mIconResId = iconResId; + return this; + } + + public Builder setCardAction(int cardAction) { + mCardAction = cardAction; + return this; + } + + public Builder setExpireTimeMS(long expireTimeMS) { + mExpireTimeMS = expireTimeMS; + return this; + } + + public Builder setIconDrawable(Drawable iconDrawable) { + mIconDrawable = iconDrawable; + return this; + } + + public Builder setIsLargeCard(boolean isLargeCard) { + mIsLargeCard = isLargeCard; + return this; + } + + public Builder setViewType(@LayoutRes int viewType) { + mViewType = viewType; + return this; + } + + public Builder setIsPendingDismiss(boolean isPendingDismiss) { + mIsPendingDismiss = isPendingDismiss; + return this; + } + + public Builder setHasInlineAction(boolean hasInlineAction) { + mHasInlineAction = hasInlineAction; + return this; + } + + public ContextualCard build() { + return new ContextualCard(this); + } + } +} diff --git a/src/com/android/settings/homepage/contextualcards/ContextualCardController.java b/src/com/android/settings/homepage/contextualcards/ContextualCardController.java new file mode 100644 index 0000000000..4d31e79605 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/ContextualCardController.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards; + +/** + * Data controller for {@link ContextualCard}. + */ +public interface ContextualCardController { + + @ContextualCard.CardType + int getCardType(); + + void onPrimaryClick(ContextualCard card); + + void onActionClick(ContextualCard card); + + void onDismissed(ContextualCard card); + + void setCardUpdateListener(ContextualCardUpdateListener listener); +} diff --git a/src/com/android/settings/homepage/contextualcards/ContextualCardFeatureProvider.java b/src/com/android/settings/homepage/contextualcards/ContextualCardFeatureProvider.java new file mode 100644 index 0000000000..bdf863e338 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/ContextualCardFeatureProvider.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards; + +import androidx.slice.Slice; + +/** Feature provider for the contextual card feature. */ +public interface ContextualCardFeatureProvider { + + /** Log package when user clicks contextual notification channel card. */ + void logNotificationPackage(Slice slice); +} diff --git a/src/com/android/settings/homepage/contextualcards/ContextualCardFeatureProviderImpl.java b/src/com/android/settings/homepage/contextualcards/ContextualCardFeatureProviderImpl.java new file mode 100644 index 0000000000..4af2838baa --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/ContextualCardFeatureProviderImpl.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards; + +import static android.content.Context.MODE_PRIVATE; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.ArraySet; + +import androidx.slice.Slice; +import androidx.slice.SliceMetadata; +import androidx.slice.core.SliceAction; + +import com.android.settings.SettingsActivity; +import com.android.settings.applications.AppInfoBase; +import com.android.settings.homepage.contextualcards.slices.ContextualNotificationChannelSlice; +import com.android.settings.slices.CustomSliceRegistry; + +import java.util.Set; + +public class ContextualCardFeatureProviderImpl implements ContextualCardFeatureProvider { + private final Context mContext; + + public ContextualCardFeatureProviderImpl(Context context) { + mContext = context; + } + + @Override + public void logNotificationPackage(Slice slice) { + if (slice == null || !slice.getUri().equals( + CustomSliceRegistry.CONTEXTUAL_NOTIFICATION_CHANNEL_SLICE_URI)) { + return; + } + + final SliceAction primaryAction = SliceMetadata.from(mContext, slice).getPrimaryAction(); + final String currentPackage = primaryAction.getAction().getIntent() + .getBundleExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS) + .getString(AppInfoBase.ARG_PACKAGE_NAME); + + final SharedPreferences prefs = mContext.getSharedPreferences( + ContextualNotificationChannelSlice.PREFS, MODE_PRIVATE); + final Set<String> interactedPackages = prefs.getStringSet( + ContextualNotificationChannelSlice.PREF_KEY_INTERACTED_PACKAGES, new ArraySet<>()); + + final Set<String> newInteractedPackages = new ArraySet<>(interactedPackages); + newInteractedPackages.add(currentPackage); + prefs.edit().putStringSet(ContextualNotificationChannelSlice.PREF_KEY_INTERACTED_PACKAGES, + newInteractedPackages).apply(); + } +} diff --git a/src/com/android/settings/homepage/contextualcards/ContextualCardFeedbackDialog.java b/src/com/android/settings/homepage/contextualcards/ContextualCardFeedbackDialog.java new file mode 100644 index 0000000000..0d5b275861 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/ContextualCardFeedbackDialog.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards; + +import android.content.DialogInterface; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; + +import com.android.internal.app.AlertActivity; +import com.android.internal.app.AlertController; +import com.android.settings.R; + +public class ContextualCardFeedbackDialog extends AlertActivity implements + DialogInterface.OnClickListener { + + public static final String EXTRA_CARD_NAME = "card_name"; + public static final String EXTRA_FEEDBACK_EMAIL = "feedback_email"; + + private static final String TAG = "CardFeedbackDialog"; + private static final String SUBJECT = "Settings Contextual Card Feedback - "; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + final AlertController.AlertParams alertParams = mAlertParams; + alertParams.mMessage = getText(R.string.contextual_card_feedback_confirm_message); + alertParams.mPositiveButtonText = getText(R.string.contextual_card_feedback_send); + alertParams.mPositiveButtonListener = this; + alertParams.mNegativeButtonText = getText(R.string.skip_label); + + setupAlert(); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + final String cardName = getIntent().getStringExtra(EXTRA_CARD_NAME); + final String email = getIntent().getStringExtra(EXTRA_FEEDBACK_EMAIL); + final Intent intent = new Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:" + email)); + intent.putExtra(Intent.EXTRA_SUBJECT, SUBJECT + cardName); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + try { + startActivity(intent); + } catch (Exception e) { + Log.e(TAG, "Send feedback failed.", e); + } + finish(); + } +} diff --git a/src/com/android/settings/homepage/contextualcards/ContextualCardLoader.java b/src/com/android/settings/homepage/contextualcards/ContextualCardLoader.java new file mode 100644 index 0000000000..6d3649d405 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/ContextualCardLoader.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards; + +import static com.android.settings.slices.CustomSliceRegistry.BLUETOOTH_DEVICES_SLICE_URI; +import static com.android.settings.slices.CustomSliceRegistry.CONTEXTUAL_NOTIFICATION_CHANNEL_SLICE_URI; +import static com.android.settings.slices.CustomSliceRegistry.CONTEXTUAL_WIFI_SLICE_URI; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.database.ContentObserver; +import android.database.Cursor; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + +import com.android.settings.R; +import com.android.settings.homepage.contextualcards.logging.ContextualCardLogUtils; +import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; +import com.android.settingslib.utils.AsyncLoaderCompat; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public class ContextualCardLoader extends AsyncLoaderCompat<List<ContextualCard>> { + + @VisibleForTesting + static final int DEFAULT_CARD_COUNT = 2; + static final int CARD_CONTENT_LOADER_ID = 1; + + private static final String TAG = "ContextualCardLoader"; + private static final long ELIGIBILITY_CHECKER_TIMEOUT_MS = 250; + + private final ExecutorService mExecutorService; + private final ContentObserver mObserver = new ContentObserver( + new Handler(Looper.getMainLooper())) { + @Override + public void onChange(boolean selfChange, Uri uri) { + if (isStarted()) { + mNotifyUri = uri; + forceLoad(); + } + } + }; + + @VisibleForTesting + Uri mNotifyUri; + + private final Context mContext; + + ContextualCardLoader(Context context) { + super(context); + mContext = context.getApplicationContext(); + mExecutorService = Executors.newCachedThreadPool(); + } + + @Override + protected void onStartLoading() { + super.onStartLoading(); + mNotifyUri = null; + mContext.getContentResolver().registerContentObserver(CardContentProvider.REFRESH_CARD_URI, + false /*notifyForDescendants*/, mObserver); + mContext.getContentResolver().registerContentObserver(CardContentProvider.DELETE_CARD_URI, + false /*notifyForDescendants*/, mObserver); + } + + @Override + protected void onStopLoading() { + super.onStopLoading(); + mContext.getContentResolver().unregisterContentObserver(mObserver); + } + + @Override + protected void onDiscardResult(List<ContextualCard> result) { + + } + + @NonNull + @Override + public List<ContextualCard> loadInBackground() { + final List<ContextualCard> result = new ArrayList<>(); + if (mContext.getResources().getBoolean(R.bool.config_use_legacy_suggestion)) { + Log.d(TAG, "Skipping - in legacy suggestion mode"); + return result; + } + try (Cursor cursor = getContextualCardsFromProvider()) { + if (cursor.getCount() > 0) { + for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { + final ContextualCard card = new ContextualCard(cursor); + if (card.isCustomCard()) { + //TODO(b/114688391): Load and generate custom card,then add into list + } else if (isLargeCard(card)) { + result.add(card.mutate().setIsLargeCard(true).build()); + } else { + result.add(card); + } + } + } + } + return getDisplayableCards(result); + } + + // Get final displayed cards and log what cards will be displayed/hidden + @VisibleForTesting + List<ContextualCard> getDisplayableCards(List<ContextualCard> candidates) { + final List<ContextualCard> eligibleCards = filterEligibleCards(candidates); + final List<ContextualCard> visibleCards = new ArrayList<>(); + final List<ContextualCard> hiddenCards = new ArrayList<>(); + + final int size = eligibleCards.size(); + for (int i = 0; i < size; i++) { + if (i < DEFAULT_CARD_COUNT) { + visibleCards.add(eligibleCards.get(i)); + } else { + hiddenCards.add(eligibleCards.get(i)); + } + } + + if (!CardContentProvider.DELETE_CARD_URI.equals(mNotifyUri)) { + final MetricsFeatureProvider metricsFeatureProvider = + FeatureFactory.getFactory(mContext).getMetricsFeatureProvider(); + + metricsFeatureProvider.action(mContext, + SettingsEnums.ACTION_CONTEXTUAL_CARD_NOT_SHOW, + ContextualCardLogUtils.buildCardListLog(hiddenCards)); + } + return visibleCards; + } + + @VisibleForTesting + Cursor getContextualCardsFromProvider() { + return CardDatabaseHelper.getInstance(mContext).getContextualCards(); + } + + @VisibleForTesting + List<ContextualCard> filterEligibleCards(List<ContextualCard> candidates) { + final List<ContextualCard> cards = new ArrayList<>(); + final List<Future<ContextualCard>> eligibleCards = new ArrayList<>(); + + for (ContextualCard card : candidates) { + final EligibleCardChecker future = new EligibleCardChecker(mContext, card); + eligibleCards.add(mExecutorService.submit(future)); + } + // Collect future and eligible cards + for (Future<ContextualCard> cardFuture : eligibleCards) { + try { + final ContextualCard card = cardFuture.get(ELIGIBILITY_CHECKER_TIMEOUT_MS, + TimeUnit.MILLISECONDS); + if (card != null) { + cards.add(card); + } + } catch (ExecutionException | InterruptedException | TimeoutException e) { + Log.w(TAG, "Failed to get eligible state for card, likely timeout. Skipping", e); + } + } + return cards; + } + + private boolean isLargeCard(ContextualCard card) { + return card.getSliceUri().equals(CONTEXTUAL_WIFI_SLICE_URI) + || card.getSliceUri().equals(BLUETOOTH_DEVICES_SLICE_URI) + || card.getSliceUri().equals(CONTEXTUAL_NOTIFICATION_CHANNEL_SLICE_URI); + } + + public interface CardContentLoaderListener { + void onFinishCardLoading(List<ContextualCard> contextualCards); + } +} diff --git a/src/com/android/settings/homepage/contextualcards/ContextualCardLookupTable.java b/src/com/android/settings/homepage/contextualcards/ContextualCardLookupTable.java new file mode 100644 index 0000000000..e11ce30522 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/ContextualCardLookupTable.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards; + +import android.util.Log; + +import androidx.annotation.LayoutRes; +import androidx.annotation.VisibleForTesting; + +import com.android.settings.homepage.contextualcards.ContextualCard.CardType; +import com.android.settings.homepage.contextualcards.conditional.ConditionContextualCardController; +import com.android.settings.homepage.contextualcards.conditional.ConditionContextualCardRenderer; +import com.android.settings.homepage.contextualcards.conditional.ConditionFooterContextualCardRenderer; +import com.android.settings.homepage.contextualcards.conditional.ConditionHeaderContextualCardRenderer; +import com.android.settings.homepage.contextualcards.legacysuggestion.LegacySuggestionContextualCardController; +import com.android.settings.homepage.contextualcards.legacysuggestion.LegacySuggestionContextualCardRenderer; +import com.android.settings.homepage.contextualcards.slices.SliceContextualCardController; +import com.android.settings.homepage.contextualcards.slices.SliceContextualCardRenderer; + +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; + +public class ContextualCardLookupTable { + private static final String TAG = "ContextualCardLookup"; + + static class ControllerRendererMapping implements Comparable<ControllerRendererMapping> { + @CardType + final int mCardType; + final int mViewType; + final Class<? extends ContextualCardController> mControllerClass; + final Class<? extends ContextualCardRenderer> mRendererClass; + + ControllerRendererMapping(@CardType int cardType, @LayoutRes int viewType, + Class<? extends ContextualCardController> controllerClass, + Class<? extends ContextualCardRenderer> rendererClass) { + mCardType = cardType; + mViewType = viewType; + mControllerClass = controllerClass; + mRendererClass = rendererClass; + } + + @Override + public int compareTo(ControllerRendererMapping other) { + return Comparator.comparingInt((ControllerRendererMapping mapping) -> mapping.mCardType) + .thenComparingInt(mapping -> mapping.mViewType) + .compare(this, other); + } + } + + @VisibleForTesting + static final Set<ControllerRendererMapping> LOOKUP_TABLE = + new TreeSet<ControllerRendererMapping>() {{ + add(new ControllerRendererMapping(CardType.CONDITIONAL, + ConditionContextualCardRenderer.VIEW_TYPE_HALF_WIDTH, + ConditionContextualCardController.class, + ConditionContextualCardRenderer.class)); + add(new ControllerRendererMapping(CardType.CONDITIONAL, + ConditionContextualCardRenderer.VIEW_TYPE_FULL_WIDTH, + ConditionContextualCardController.class, + ConditionContextualCardRenderer.class)); + add(new ControllerRendererMapping(CardType.LEGACY_SUGGESTION, + LegacySuggestionContextualCardRenderer.VIEW_TYPE, + LegacySuggestionContextualCardController.class, + LegacySuggestionContextualCardRenderer.class)); + add(new ControllerRendererMapping(CardType.SLICE, + SliceContextualCardRenderer.VIEW_TYPE_DEFERRED_SETUP, + SliceContextualCardController.class, + SliceContextualCardRenderer.class)); + add(new ControllerRendererMapping(CardType.SLICE, + SliceContextualCardRenderer.VIEW_TYPE_FULL_WIDTH, + SliceContextualCardController.class, + SliceContextualCardRenderer.class)); + add(new ControllerRendererMapping(CardType.SLICE, + SliceContextualCardRenderer.VIEW_TYPE_HALF_WIDTH, + SliceContextualCardController.class, + SliceContextualCardRenderer.class)); + add(new ControllerRendererMapping(CardType.CONDITIONAL_FOOTER, + ConditionFooterContextualCardRenderer.VIEW_TYPE, + ConditionContextualCardController.class, + ConditionFooterContextualCardRenderer.class)); + add(new ControllerRendererMapping(CardType.CONDITIONAL_HEADER, + ConditionHeaderContextualCardRenderer.VIEW_TYPE, + ConditionContextualCardController.class, + ConditionHeaderContextualCardRenderer.class)); + }}; + + public static Class<? extends ContextualCardController> getCardControllerClass( + @CardType int cardType) { + for (ControllerRendererMapping mapping : LOOKUP_TABLE) { + if (mapping.mCardType == cardType) { + return mapping.mControllerClass; + } + } + return null; + } + + public static Class<? extends ContextualCardRenderer> getCardRendererClassByViewType( + int viewType) throws IllegalStateException { + List<ControllerRendererMapping> validMappings = LOOKUP_TABLE.stream() + .filter(m -> m.mViewType == viewType).collect(Collectors.toList()); + if (validMappings == null || validMappings.isEmpty()) { + Log.w(TAG, "No matching mapping"); + return null; + } + if (validMappings.size() != 1) { + throw new IllegalStateException("Have duplicate VIEW_TYPE in lookup table."); + } + + return validMappings.get(0).mRendererClass; + } +} diff --git a/src/com/android/settings/homepage/contextualcards/ContextualCardManager.java b/src/com/android/settings/homepage/contextualcards/ContextualCardManager.java new file mode 100644 index 0000000000..a941fdbfdf --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/ContextualCardManager.java @@ -0,0 +1,402 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards; + +import static com.android.settings.homepage.contextualcards.ContextualCardLoader.CARD_CONTENT_LOADER_ID; +import static com.android.settings.intelligence.ContextualCardProto.ContextualCard.Category.DEFERRED_SETUP_VALUE; +import static com.android.settings.intelligence.ContextualCardProto.ContextualCard.Category.SUGGESTION_VALUE; + +import static java.util.stream.Collectors.groupingBy; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.os.Bundle; +import android.provider.Settings; +import android.text.format.DateUtils; +import android.util.ArrayMap; +import android.util.Log; +import android.widget.BaseAdapter; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.Loader; + +import com.android.settings.homepage.contextualcards.conditional.ConditionalCardController; +import com.android.settings.homepage.contextualcards.logging.ContextualCardLogUtils; +import com.android.settings.homepage.contextualcards.slices.SliceContextualCardRenderer; +import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; +import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnSaveInstanceState; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.core.lifecycle.events.OnStop; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; + +/** + * This is a centralized manager of multiple {@link ContextualCardController}. + * + * {@link ContextualCardManager} first loads data from {@link ContextualCardLoader} and gets back a + * list of {@link ContextualCard}. All subclasses of {@link ContextualCardController} are loaded + * here, which will then trigger the {@link ContextualCardController} to load its data and listen to + * corresponding changes. When every single {@link ContextualCardController} updates its data, the + * data will be passed here, then going through some sorting mechanisms. The + * {@link ContextualCardController} will end up building a list of {@link ContextualCard} for + * {@link ContextualCardsAdapter} and {@link BaseAdapter#notifyDataSetChanged()} will be called to + * get the page refreshed. + */ +public class ContextualCardManager implements ContextualCardLoader.CardContentLoaderListener, + ContextualCardUpdateListener, LifecycleObserver, OnSaveInstanceState { + + @VisibleForTesting + static final long CARD_CONTENT_LOADER_TIMEOUT_MS = DateUtils.SECOND_IN_MILLIS; + @VisibleForTesting + static final String KEY_GLOBAL_CARD_LOADER_TIMEOUT = "global_card_loader_timeout_key"; + @VisibleForTesting + static final String KEY_CONTEXTUAL_CARDS = "key_contextual_cards"; + + private static final String TAG = "ContextualCardManager"; + + //The list for Settings Custom Card + private static final int[] SETTINGS_CARDS = + {ContextualCard.CardType.CONDITIONAL, ContextualCard.CardType.LEGACY_SUGGESTION}; + + private final Context mContext; + private final Lifecycle mLifecycle; + private final List<LifecycleObserver> mLifecycleObservers; + private ContextualCardUpdateListener mListener; + + @VisibleForTesting + final ControllerRendererPool mControllerRendererPool; + @VisibleForTesting + final List<ContextualCard> mContextualCards; + @VisibleForTesting + long mStartTime; + @VisibleForTesting + boolean mIsFirstLaunch; + @VisibleForTesting + List<String> mSavedCards; + + public ContextualCardManager(Context context, Lifecycle lifecycle, Bundle savedInstanceState) { + mContext = context; + mLifecycle = lifecycle; + mContextualCards = new ArrayList<>(); + mLifecycleObservers = new ArrayList<>(); + mControllerRendererPool = new ControllerRendererPool(); + mLifecycle.addObserver(this); + if (savedInstanceState == null) { + mIsFirstLaunch = true; + mSavedCards = null; + } else { + mSavedCards = savedInstanceState.getStringArrayList(KEY_CONTEXTUAL_CARDS); + } + //for data provided by Settings + for (@ContextualCard.CardType int cardType : SETTINGS_CARDS) { + setupController(cardType); + } + } + + void loadContextualCards(LoaderManager loaderManager) { + mStartTime = System.currentTimeMillis(); + final CardContentLoaderCallbacks cardContentLoaderCallbacks = + new CardContentLoaderCallbacks(mContext); + cardContentLoaderCallbacks.setListener(this); + // Use the cached data when navigating back to the first page and upon screen rotation. + loaderManager.initLoader(CARD_CONTENT_LOADER_ID, null /* bundle */, + cardContentLoaderCallbacks); + } + + private void loadCardControllers() { + for (ContextualCard card : mContextualCards) { + setupController(card.getCardType()); + } + } + + @VisibleForTesting + void setupController(@ContextualCard.CardType int cardType) { + final ContextualCardController controller = mControllerRendererPool.getController(mContext, + cardType); + if (controller == null) { + Log.w(TAG, "Cannot find ContextualCardController for type " + cardType); + return; + } + controller.setCardUpdateListener(this); + if (controller instanceof LifecycleObserver && !mLifecycleObservers.contains(controller)) { + mLifecycleObservers.add((LifecycleObserver) controller); + mLifecycle.addObserver((LifecycleObserver) controller); + } + } + + @VisibleForTesting + List<ContextualCard> sortCards(List<ContextualCard> cards) { + //take mContextualCards as the source and do the ranking based on the rule. + return cards.stream() + .sorted((c1, c2) -> Double.compare(c2.getRankingScore(), c1.getRankingScore())) + .collect(Collectors.toList()); + } + + @Override + public void onContextualCardUpdated(Map<Integer, List<ContextualCard>> updateList) { + final Set<Integer> cardTypes = updateList.keySet(); + //Remove the existing data that matches the certain cardType before inserting new data. + List<ContextualCard> cardsToKeep; + + // We are not sure how many card types will be in the database, so when the list coming + // from the database is empty (e.g. no eligible cards/cards are dismissed), we cannot + // assign a specific card type for its map which is sending here. Thus, we assume that + // except Conditional cards, all other cards are from the database. So when the map sent + // here is empty, we only keep Conditional cards. + if (cardTypes.isEmpty()) { + final Set<Integer> conditionalCardTypes = new TreeSet() {{ + add(ContextualCard.CardType.CONDITIONAL); + add(ContextualCard.CardType.CONDITIONAL_HEADER); + add(ContextualCard.CardType.CONDITIONAL_FOOTER); + }}; + cardsToKeep = mContextualCards.stream() + .filter(card -> conditionalCardTypes.contains(card.getCardType())) + .collect(Collectors.toList()); + } else { + cardsToKeep = mContextualCards.stream() + .filter(card -> !cardTypes.contains(card.getCardType())) + .collect(Collectors.toList()); + } + + final List<ContextualCard> allCards = new ArrayList<>(); + allCards.addAll(cardsToKeep); + allCards.addAll( + updateList.values().stream().flatMap(List::stream).collect(Collectors.toList())); + + //replace with the new data + mContextualCards.clear(); + final List<ContextualCard> sortedCards = sortCards(allCards); + mContextualCards.addAll(getCardsWithViewType(sortedCards)); + + loadCardControllers(); + + if (mListener != null) { + final Map<Integer, List<ContextualCard>> cardsToUpdate = new ArrayMap<>(); + cardsToUpdate.put(ContextualCard.CardType.DEFAULT, mContextualCards); + mListener.onContextualCardUpdated(cardsToUpdate); + } + } + + @Override + public void onFinishCardLoading(List<ContextualCard> cards) { + final long loadTime = System.currentTimeMillis() - mStartTime; + Log.d(TAG, "Total loading time = " + loadTime); + + final List<ContextualCard> cardsToKeep = getCardsToKeep(cards); + + final MetricsFeatureProvider metricsFeatureProvider = + FeatureFactory.getFactory(mContext).getMetricsFeatureProvider(); + + //navigate back to the homepage, screen rotate or after card dismissal + if (!mIsFirstLaunch) { + onContextualCardUpdated(cardsToKeep.stream() + .collect(groupingBy(ContextualCard::getCardType))); + metricsFeatureProvider.action(mContext, + SettingsEnums.ACTION_CONTEXTUAL_CARD_SHOW, + ContextualCardLogUtils.buildCardListLog(cardsToKeep)); + return; + } + + final long timeoutLimit = getCardLoaderTimeout(); + if (loadTime <= timeoutLimit) { + onContextualCardUpdated(cards.stream() + .collect(groupingBy(ContextualCard::getCardType))); + metricsFeatureProvider.action(mContext, + SettingsEnums.ACTION_CONTEXTUAL_CARD_SHOW, + ContextualCardLogUtils.buildCardListLog(cards)); + } else { + // log timeout occurrence + metricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, + SettingsEnums.ACTION_CONTEXTUAL_CARD_LOAD_TIMEOUT, + SettingsEnums.SETTINGS_HOMEPAGE, + null /* key */, (int) loadTime /* value */); + } + //only log homepage display upon a fresh launch + final long totalTime = System.currentTimeMillis() - mStartTime; + metricsFeatureProvider.action(mContext, + SettingsEnums.ACTION_CONTEXTUAL_HOME_SHOW, (int) totalTime); + + mIsFirstLaunch = false; + } + + @Override + public void onSaveInstanceState(Bundle outState) { + final ArrayList<String> cards = mContextualCards.stream() + .map(ContextualCard::getName) + .collect(Collectors.toCollection(ArrayList::new)); + + outState.putStringArrayList(KEY_CONTEXTUAL_CARDS, cards); + } + + public void onWindowFocusChanged(boolean hasWindowFocus) { + // Duplicate a list to avoid java.util.ConcurrentModificationException. + final List<ContextualCard> cards = new ArrayList<>(mContextualCards); + boolean hasConditionController = false; + for (ContextualCard card : cards) { + final ContextualCardController controller = getControllerRendererPool() + .getController(mContext, card.getCardType()); + if (controller instanceof ConditionalCardController) { + hasConditionController = true; + } + if (hasWindowFocus && controller instanceof OnStart) { + ((OnStart) controller).onStart(); + } + if (!hasWindowFocus && controller instanceof OnStop) { + ((OnStop) controller).onStop(); + } + } + // Conditional cards will always be refreshed whether or not there are conditional cards + // in the homepage. + if (!hasConditionController) { + final ContextualCardController controller = getControllerRendererPool() + .getController(mContext, ContextualCard.CardType.CONDITIONAL); + if (hasWindowFocus && controller instanceof OnStart) { + ((OnStart) controller).onStart(); + } + if (!hasWindowFocus && controller instanceof OnStop) { + ((OnStop) controller).onStop(); + } + } + } + + public ControllerRendererPool getControllerRendererPool() { + return mControllerRendererPool; + } + + void setListener(ContextualCardUpdateListener listener) { + mListener = listener; + } + + @VisibleForTesting + List<ContextualCard> getCardsWithViewType(List<ContextualCard> cards) { + if (cards.isEmpty()) { + return cards; + } + + final List<ContextualCard> result = getCardsWithDeferredSetupViewType(cards); + return getCardsWithSuggestionViewType(result); + } + + @VisibleForTesting + long getCardLoaderTimeout() { + // Return the timeout limit if Settings.Global has the KEY_GLOBAL_CARD_LOADER_TIMEOUT key, + // else return default timeout. + return Settings.Global.getLong(mContext.getContentResolver(), + KEY_GLOBAL_CARD_LOADER_TIMEOUT, CARD_CONTENT_LOADER_TIMEOUT_MS); + } + + private List<ContextualCard> getCardsWithSuggestionViewType(List<ContextualCard> cards) { + // Shows as half cards if 2 suggestion type of cards are next to each other. + // Shows as full card if 1 suggestion type of card lives alone. + final List<ContextualCard> result = new ArrayList<>(cards); + for (int index = 1; index < result.size(); index++) { + final ContextualCard previous = result.get(index - 1); + final ContextualCard current = result.get(index); + if (current.getCategory() == SUGGESTION_VALUE + && previous.getCategory() == SUGGESTION_VALUE) { + result.set(index - 1, previous.mutate().setViewType( + SliceContextualCardRenderer.VIEW_TYPE_HALF_WIDTH).build()); + result.set(index, current.mutate().setViewType( + SliceContextualCardRenderer.VIEW_TYPE_HALF_WIDTH).build()); + index++; + } + } + return result; + } + + private List<ContextualCard> getCardsWithDeferredSetupViewType(List<ContextualCard> cards) { + // Find the deferred setup card and assign it with proper view type. + // Reason: The returned card list will mix deferred setup card and other suggestion cards + // after device running 1 days. + final List<ContextualCard> result = new ArrayList<>(cards); + for (int index = 0; index < result.size(); index++) { + final ContextualCard card = cards.get(index); + if (card.getCategory() == DEFERRED_SETUP_VALUE) { + result.set(index, card.mutate().setViewType( + SliceContextualCardRenderer.VIEW_TYPE_DEFERRED_SETUP).build()); + return result; + } + } + return result; + } + + @VisibleForTesting + List<ContextualCard> getCardsToKeep(List<ContextualCard> cards) { + if (mSavedCards != null) { + //screen rotate + final List<ContextualCard> cardsToKeep = cards.stream() + .filter(card -> mSavedCards.contains(card.getName())) + .collect(Collectors.toList()); + mSavedCards = null; + return cardsToKeep; + } else { + //navigate back to the homepage or after dismissing a card + return cards.stream() + .filter(card -> mContextualCards.contains(card)) + .collect(Collectors.toList()); + } + } + + static class CardContentLoaderCallbacks implements + LoaderManager.LoaderCallbacks<List<ContextualCard>> { + + private Context mContext; + private ContextualCardLoader.CardContentLoaderListener mListener; + + CardContentLoaderCallbacks(Context context) { + mContext = context.getApplicationContext(); + } + + protected void setListener(ContextualCardLoader.CardContentLoaderListener listener) { + mListener = listener; + } + + @NonNull + @Override + public Loader<List<ContextualCard>> onCreateLoader(int id, @Nullable Bundle bundle) { + if (id == CARD_CONTENT_LOADER_ID) { + return new ContextualCardLoader(mContext); + } else { + throw new IllegalArgumentException("Unknown loader id: " + id); + } + } + + @Override + public void onLoadFinished(@NonNull Loader<List<ContextualCard>> loader, + List<ContextualCard> contextualCards) { + if (mListener != null) { + mListener.onFinishCardLoading(contextualCards); + } + } + + @Override + public void onLoaderReset(@NonNull Loader<List<ContextualCard>> loader) { + + } + } +} diff --git a/src/com/android/settings/homepage/contextualcards/ContextualCardRenderer.java b/src/com/android/settings/homepage/contextualcards/ContextualCardRenderer.java new file mode 100644 index 0000000000..fc1bda2e9b --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/ContextualCardRenderer.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards; + +import android.view.View; + +import androidx.annotation.LayoutRes; +import androidx.recyclerview.widget.RecyclerView; + +/** + * UI renderer for {@link ContextualCard}. + */ +public interface ContextualCardRenderer { + + /** + * When {@link ContextualCardsAdapter} calls {@link ContextualCardsAdapter#onCreateViewHolder}, + * this method will be called to retrieve the corresponding + * {@link androidx.recyclerview.widget.RecyclerView.ViewHolder}. + */ + RecyclerView.ViewHolder createViewHolder(View view, @LayoutRes int viewType); + + /** + * When {@link ContextualCardsAdapter} calls {@link ContextualCardsAdapter#onBindViewHolder}, + * this method will be called to bind data to the + * {@link androidx.recyclerview.widget.RecyclerView.ViewHolder}. + */ + void bindView(RecyclerView.ViewHolder holder, ContextualCard card); +}
\ No newline at end of file diff --git a/src/com/android/settings/homepage/contextualcards/ContextualCardUpdateListener.java b/src/com/android/settings/homepage/contextualcards/ContextualCardUpdateListener.java new file mode 100644 index 0000000000..9b90d41d8d --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/ContextualCardUpdateListener.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards; + +import androidx.annotation.MainThread; + +import java.util.List; +import java.util.Map; + +/** + * When {@link ContextualCardController} detects changes, it will notify the listeners registered. + */ +public interface ContextualCardUpdateListener { + + /** + * Called when a set of cards are updated. + * + * @param cards A map of updates grouped by {@link ContextualCard.CardType}. Values can be + * null, which means all cards from corresponding {@link + * ContextualCard.CardType} are removed. + */ + @MainThread + void onContextualCardUpdated(Map<Integer, List<ContextualCard>> cards); +}
\ No newline at end of file diff --git a/src/com/android/settings/homepage/contextualcards/ContextualCardsAdapter.java b/src/com/android/settings/homepage/contextualcards/ContextualCardsAdapter.java new file mode 100644 index 0000000000..4e010fd206 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/ContextualCardsAdapter.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.LayoutRes; +import androidx.annotation.VisibleForTesting; +import androidx.lifecycle.LifecycleOwner; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.android.settings.homepage.contextualcards.conditional.ConditionContextualCardRenderer; +import com.android.settings.homepage.contextualcards.slices.SliceContextualCardRenderer; +import com.android.settings.homepage.contextualcards.slices.SwipeDismissalDelegate; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class ContextualCardsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> + implements ContextualCardUpdateListener, SwipeDismissalDelegate.Listener { + static final int SPAN_COUNT = 2; + + private static final String TAG = "ContextualCardsAdapter"; + private static final int HALF_WIDTH = 1; + private static final int FULL_WIDTH = 2; + + @VisibleForTesting + final List<ContextualCard> mContextualCards; + + private final Context mContext; + private final ControllerRendererPool mControllerRendererPool; + private final LifecycleOwner mLifecycleOwner; + + private RecyclerView mRecyclerView; + + public ContextualCardsAdapter(Context context, LifecycleOwner lifecycleOwner, + ContextualCardManager manager) { + mContext = context; + mContextualCards = new ArrayList<>(); + mControllerRendererPool = manager.getControllerRendererPool(); + mLifecycleOwner = lifecycleOwner; + setHasStableIds(true); + } + + @Override + public long getItemId(int position) { + return mContextualCards.get(position).hashCode(); + } + + @Override + public int getItemViewType(int position) { + final ContextualCard card = mContextualCards.get(position); + return card.getViewType(); + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, @LayoutRes int viewType) { + final ContextualCardRenderer renderer = mControllerRendererPool.getRendererByViewType( + mContext, mLifecycleOwner, viewType); + final View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false); + return renderer.createViewHolder(view, viewType); + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { + final ContextualCard card = mContextualCards.get(position); + final ContextualCardRenderer renderer = mControllerRendererPool.getRendererByViewType( + mContext, mLifecycleOwner, card.getViewType()); + renderer.bindView(holder, card); + } + + @Override + public int getItemCount() { + return mContextualCards.size(); + } + + @Override + public void onAttachedToRecyclerView(RecyclerView recyclerView) { + super.onAttachedToRecyclerView(recyclerView); + mRecyclerView = recyclerView; + final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); + if (layoutManager instanceof GridLayoutManager) { + final GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager; + gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { + @Override + public int getSpanSize(int position) { + final int viewType = mContextualCards.get(position).getViewType(); + if (viewType == ConditionContextualCardRenderer.VIEW_TYPE_HALF_WIDTH + || viewType == SliceContextualCardRenderer.VIEW_TYPE_HALF_WIDTH) { + return HALF_WIDTH; + } + return FULL_WIDTH; + } + }); + } + } + + @Override + public void onContextualCardUpdated(Map<Integer, List<ContextualCard>> cards) { + final List<ContextualCard> contextualCards = cards.get(ContextualCard.CardType.DEFAULT); + final boolean previouslyEmpty = mContextualCards.isEmpty(); + final boolean nowEmpty = contextualCards == null || contextualCards.isEmpty(); + if (contextualCards == null) { + mContextualCards.clear(); + notifyDataSetChanged(); + } else { + final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff( + new ContextualCardsDiffCallback(mContextualCards, contextualCards)); + mContextualCards.clear(); + mContextualCards.addAll(contextualCards); + diffResult.dispatchUpdatesTo(this); + } + + if (mRecyclerView != null && previouslyEmpty && !nowEmpty) { + // Adding items to empty list, should animate. + mRecyclerView.scheduleLayoutAnimation(); + } + + //TODO(b/119465242): flickering conditional cards after collapsing/expanding + } + + @Override + public void onSwiped(int position) { + final ContextualCard card = mContextualCards.get(position).mutate() + .setIsPendingDismiss(true).build(); + mContextualCards.set(position, card); + notifyItemChanged(position); + } +} diff --git a/src/com/android/settings/homepage/contextualcards/ContextualCardsDiffCallback.java b/src/com/android/settings/homepage/contextualcards/ContextualCardsDiffCallback.java new file mode 100644 index 0000000000..58d6a4197c --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/ContextualCardsDiffCallback.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards; + +import androidx.recyclerview.widget.DiffUtil; + +import java.util.List; + +/** + * A DiffCallback to calculate the difference between old and new {@link ContextualCard} List. + */ +public class ContextualCardsDiffCallback extends DiffUtil.Callback { + + private final List<ContextualCard> mOldCards; + private final List<ContextualCard> mNewCards; + + public ContextualCardsDiffCallback(List<ContextualCard> oldCards, + List<ContextualCard> newCards) { + mOldCards = oldCards; + mNewCards = newCards; + } + + @Override + public int getOldListSize() { + return mOldCards.size(); + } + + @Override + public int getNewListSize() { + return mNewCards.size(); + } + + @Override + public boolean areItemsTheSame(int oldCardPosition, int newCardPosition) { + return mOldCards.get(oldCardPosition).getName().equals( + mNewCards.get(newCardPosition).getName()); + } + + @Override + public boolean areContentsTheSame(int oldCardPosition, int newCardPosition) { + // Slices with toggles needs to be updated continuously, which means their contents may + // change. So here we assume the content will always be different to force view rebinding. + if (mNewCards.get(newCardPosition).hasInlineAction()) { + return false; + } + return mOldCards.get(oldCardPosition).equals(mNewCards.get(newCardPosition)); + } +}
\ No newline at end of file diff --git a/src/com/android/settings/homepage/contextualcards/ContextualCardsFragment.java b/src/com/android/settings/homepage/contextualcards/ContextualCardsFragment.java new file mode 100644 index 0000000000..92892b3383 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/ContextualCardsFragment.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards; + +import static com.android.settings.homepage.contextualcards.ContextualCardsAdapter.SPAN_COUNT; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.loader.app.LoaderManager; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.ItemTouchHelper; + +import com.android.settings.R; +import com.android.settings.core.InstrumentedFragment; +import com.android.settings.homepage.contextualcards.slices.SwipeDismissalDelegate; +import com.android.settings.overlay.FeatureFactory; +import com.android.settings.wifi.slice.ContextualWifiScanWorker; + +public class ContextualCardsFragment extends InstrumentedFragment implements + FocusRecyclerView.FocusListener { + + private FocusRecyclerView mCardsContainer; + private GridLayoutManager mLayoutManager; + private ContextualCardsAdapter mContextualCardsAdapter; + private ContextualCardManager mContextualCardManager; + private ItemTouchHelper mItemTouchHelper; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + final Context context = getContext(); + if (savedInstanceState == null) { + FeatureFactory.getFactory(context).getSlicesFeatureProvider().newUiSession(); + } + mContextualCardManager = new ContextualCardManager(context, getSettingsLifecycle(), + savedInstanceState); + + } + + @Override + public void onStart() { + super.onStart(); + ContextualWifiScanWorker.newVisibleUiSession(); + mContextualCardManager.loadContextualCards(LoaderManager.getInstance(this)); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + final Context context = getContext(); + final View rootView = inflater.inflate(R.layout.settings_homepage, container, false); + mCardsContainer = rootView.findViewById(R.id.card_container); + mLayoutManager = new GridLayoutManager(getActivity(), SPAN_COUNT, + GridLayoutManager.VERTICAL, false /* reverseLayout */); + mCardsContainer.setLayoutManager(mLayoutManager); + mContextualCardsAdapter = new ContextualCardsAdapter(context, this /* lifecycleOwner */, + mContextualCardManager); + mCardsContainer.setAdapter(mContextualCardsAdapter); + mContextualCardManager.setListener(mContextualCardsAdapter); + mCardsContainer.setListener(this); + mItemTouchHelper = new ItemTouchHelper(new SwipeDismissalDelegate(mContextualCardsAdapter)); + mItemTouchHelper.attachToRecyclerView(mCardsContainer); + + return rootView; + } + + @Override + public void onWindowFocusChanged(boolean hasWindowFocus) { + mContextualCardManager.onWindowFocusChanged(hasWindowFocus); + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.SETTINGS_HOMEPAGE; + } +} diff --git a/src/com/android/settings/homepage/contextualcards/ControllerRendererPool.java b/src/com/android/settings/homepage/contextualcards/ControllerRendererPool.java new file mode 100644 index 0000000000..755a1057c1 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/ControllerRendererPool.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards; + +import android.annotation.NonNull; +import android.content.Context; +import android.util.Log; + +import androidx.collection.ArraySet; +import androidx.lifecycle.LifecycleOwner; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.settings.homepage.contextualcards.conditional.ConditionContextualCardController; +import com.android.settings.homepage.contextualcards.conditional.ConditionContextualCardRenderer; +import com.android.settings.homepage.contextualcards.conditional.ConditionFooterContextualCardRenderer; +import com.android.settings.homepage.contextualcards.conditional.ConditionHeaderContextualCardRenderer; +import com.android.settings.homepage.contextualcards.legacysuggestion.LegacySuggestionContextualCardController; +import com.android.settings.homepage.contextualcards.legacysuggestion.LegacySuggestionContextualCardRenderer; +import com.android.settings.homepage.contextualcards.slices.SliceContextualCardController; +import com.android.settings.homepage.contextualcards.slices.SliceContextualCardRenderer; + +import java.util.Set; + +/** + * This is a fragment scoped singleton holding a set of {@link ContextualCardController} and + * {@link ContextualCardRenderer}. + */ +public class ControllerRendererPool { + + private static final String TAG = "ControllerRendererPool"; + + private final Set<ContextualCardController> mControllers; + private final Set<ContextualCardRenderer> mRenderers; + + public ControllerRendererPool() { + mControllers = new ArraySet<>(); + mRenderers = new ArraySet<>(); + } + + public <T extends ContextualCardController> T getController(Context context, + @ContextualCard.CardType int cardType) { + final Class<? extends ContextualCardController> clz = + ContextualCardLookupTable.getCardControllerClass(cardType); + for (ContextualCardController controller : mControllers) { + if (controller.getClass() == clz) { + Log.d(TAG, "Controller is already there."); + return (T) controller; + } + } + + final ContextualCardController controller = createCardController(context, clz); + if (controller != null) { + mControllers.add(controller); + } + return (T) controller; + } + + @VisibleForTesting + Set<ContextualCardController> getControllers() { + return mControllers; + } + + @VisibleForTesting + Set<ContextualCardRenderer> getRenderers() { + return mRenderers; + } + + public ContextualCardRenderer getRendererByViewType(Context context, + LifecycleOwner lifecycleOwner, int viewType) { + final Class<? extends ContextualCardRenderer> clz = + ContextualCardLookupTable.getCardRendererClassByViewType(viewType); + return getRenderer(context, lifecycleOwner, clz); + } + + private ContextualCardRenderer getRenderer(Context context, LifecycleOwner lifecycleOwner, + @NonNull Class<? extends ContextualCardRenderer> clz) { + for (ContextualCardRenderer renderer : mRenderers) { + if (renderer.getClass() == clz) { + Log.d(TAG, "Renderer is already there."); + return renderer; + } + } + + final ContextualCardRenderer renderer = createCardRenderer(context, lifecycleOwner, clz); + if (renderer != null) { + mRenderers.add(renderer); + } + return renderer; + } + + private ContextualCardController createCardController(Context context, + Class<? extends ContextualCardController> clz) { + if (ConditionContextualCardController.class == clz) { + return new ConditionContextualCardController(context); + } else if (SliceContextualCardController.class == clz) { + return new SliceContextualCardController(context); + } else if (LegacySuggestionContextualCardController.class == clz) { + return new LegacySuggestionContextualCardController(context); + } + return null; + } + + private ContextualCardRenderer createCardRenderer(Context context, + LifecycleOwner lifecycleOwner, Class<?> clz) { + if (ConditionContextualCardRenderer.class == clz) { + return new ConditionContextualCardRenderer(context, this /* controllerRendererPool */); + } else if (SliceContextualCardRenderer.class == clz) { + return new SliceContextualCardRenderer(context, lifecycleOwner, + this /* controllerRendererPool */); + } else if (LegacySuggestionContextualCardRenderer.class == clz) { + return new LegacySuggestionContextualCardRenderer(context, + this /* controllerRendererPool */); + } else if (ConditionFooterContextualCardRenderer.class == clz) { + return new ConditionFooterContextualCardRenderer(context, + this /*controllerRendererPool*/); + } else if (ConditionHeaderContextualCardRenderer.class == clz) { + return new ConditionHeaderContextualCardRenderer(context, + this /*controllerRendererPool*/); + } + return null; + } +} diff --git a/src/com/android/settings/homepage/contextualcards/EligibleCardChecker.java b/src/com/android/settings/homepage/contextualcards/EligibleCardChecker.java new file mode 100644 index 0000000000..8558ee7956 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/EligibleCardChecker.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2019 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.homepage.contextualcards; + +import static android.app.slice.Slice.HINT_ERROR; + +import android.app.settings.SettingsEnums; +import android.content.ContentResolver; +import android.content.Context; +import android.net.Uri; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; +import androidx.slice.Slice; +import androidx.slice.SliceMetadata; +import androidx.slice.SliceViewManager; +import androidx.slice.core.SliceAction; + +import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; + +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class EligibleCardChecker implements Callable<ContextualCard> { + + private static final String TAG = "EligibleCardChecker"; + private static final long LATCH_TIMEOUT_MS = 200; + + private final Context mContext; + + @VisibleForTesting + ContextualCard mCard; + + EligibleCardChecker(Context context, ContextualCard card) { + mContext = context; + mCard = card; + } + + @Override + public ContextualCard call() throws Exception { + final long startTime = System.currentTimeMillis(); + final MetricsFeatureProvider metricsFeatureProvider = + FeatureFactory.getFactory(mContext).getMetricsFeatureProvider(); + ContextualCard result; + + if (isCardEligibleToDisplay(mCard)) { + metricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, + SettingsEnums.ACTION_CONTEXTUAL_CARD_ELIGIBILITY, + SettingsEnums.SETTINGS_HOMEPAGE, + mCard.getTextSliceUri() /* key */, 1 /* true */); + result = mCard; + } else { + metricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, + SettingsEnums.ACTION_CONTEXTUAL_CARD_ELIGIBILITY, + SettingsEnums.SETTINGS_HOMEPAGE, + mCard.getTextSliceUri() /* key */, 0 /* false */); + result = null; + } + // Log individual card loading time + metricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, + SettingsEnums.ACTION_CONTEXTUAL_CARD_LOAD, + SettingsEnums.SETTINGS_HOMEPAGE, + mCard.getTextSliceUri() /* key */, + (int) (System.currentTimeMillis() - startTime) /* value */); + + return result; + } + + @VisibleForTesting + boolean isCardEligibleToDisplay(ContextualCard card) { + if (card.getRankingScore() < 0) { + return false; + } + if (card.isCustomCard()) { + return true; + } + + final Uri uri = card.getSliceUri(); + if (!ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) { + return false; + } + + final Slice slice = bindSlice(uri); + + if (isSliceToggleable(slice)) { + mCard = card.mutate().setHasInlineAction(true).build(); + } + + if (slice == null || slice.hasHint(HINT_ERROR)) { + Log.w(TAG, "Failed to bind slice, not eligible for display " + uri); + return false; + } + return true; + } + + @VisibleForTesting + Slice bindSlice(Uri uri) { + final SliceViewManager manager = SliceViewManager.getInstance(mContext); + final Slice[] returnSlice = new Slice[1]; + final CountDownLatch latch = new CountDownLatch(1); + final SliceViewManager.SliceCallback callback = + new SliceViewManager.SliceCallback() { + @Override + public void onSliceUpdated(Slice slice) { + try { + // We are just making sure the existence of the slice, so ignore + // slice loading state here. + returnSlice[0] = slice; + latch.countDown(); + } catch (Exception e) { + Log.w(TAG, uri + " cannot be indexed", e); + } finally { + manager.unregisterSliceCallback(uri, this); + } + } + }; + // Register a callback until we get a loaded slice. + manager.registerSliceCallback(uri, callback); + // Trigger the binding. + callback.onSliceUpdated(manager.bindSlice(uri)); + try { + latch.await(LATCH_TIMEOUT_MS, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + Log.w(TAG, "Error waiting for slice binding for uri" + uri, e); + manager.unregisterSliceCallback(uri, callback); + } + return returnSlice[0]; + } + + @VisibleForTesting + boolean isSliceToggleable(Slice slice) { + final SliceMetadata metadata = SliceMetadata.from(mContext, slice); + final List<SliceAction> toggles = metadata.getToggles(); + + return !toggles.isEmpty(); + } +} diff --git a/src/com/android/settings/dashboard/conditional/FocusRecyclerView.java b/src/com/android/settings/homepage/contextualcards/FocusRecyclerView.java index fc44d0de3f..a2ec9afdf4 100644 --- a/src/com/android/settings/dashboard/conditional/FocusRecyclerView.java +++ b/src/com/android/settings/homepage/contextualcards/FocusRecyclerView.java @@ -1,32 +1,19 @@ -/* - * Copyright 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.dashboard.conditional; +package com.android.settings.homepage.contextualcards; import android.content.Context; +import android.util.AttributeSet; + import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; -import android.util.AttributeSet; -/** - * Version of RecyclerView that can have listeners for onWindowFocusChanged. - */ public class FocusRecyclerView extends RecyclerView { private FocusListener mListener; + public FocusRecyclerView(Context context) { + super(context); + } + public FocusRecyclerView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } diff --git a/src/com/android/settings/homepage/contextualcards/SettingsContextualCardProvider.java b/src/com/android/settings/homepage/contextualcards/SettingsContextualCardProvider.java new file mode 100644 index 0000000000..86fee03e55 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/SettingsContextualCardProvider.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards; + +import android.annotation.Nullable; + +import com.android.settings.intelligence.ContextualCardProto.ContextualCard; +import com.android.settings.intelligence.ContextualCardProto.ContextualCardList; +import com.android.settings.slices.CustomSliceRegistry; + +import com.google.android.settings.intelligence.libs.contextualcards.ContextualCardProvider; + +/** Provides dynamic card for SettingsIntelligence. */ +public class SettingsContextualCardProvider extends ContextualCardProvider { + + public static final String CARD_AUTHORITY = "com.android.settings.homepage.contextualcards"; + + @Override + @Nullable + public ContextualCardList getContextualCards() { + final ContextualCard wifiCard = + ContextualCard.newBuilder() + .setSliceUri(CustomSliceRegistry.CONTEXTUAL_WIFI_SLICE_URI.toString()) + .setCardName(CustomSliceRegistry.CONTEXTUAL_WIFI_SLICE_URI.toString()) + .setCardCategory(ContextualCard.Category.IMPORTANT) + .build(); + final ContextualCard connectedDeviceCard = + ContextualCard.newBuilder() + .setSliceUri(CustomSliceRegistry.BLUETOOTH_DEVICES_SLICE_URI.toString()) + .setCardName(CustomSliceRegistry.BLUETOOTH_DEVICES_SLICE_URI.toString()) + .setCardCategory(ContextualCard.Category.IMPORTANT) + .build(); + final ContextualCard lowStorageCard = + ContextualCard.newBuilder() + .setSliceUri(CustomSliceRegistry.LOW_STORAGE_SLICE_URI.toString()) + .setCardName(CustomSliceRegistry.LOW_STORAGE_SLICE_URI.toString()) + .setCardCategory(ContextualCard.Category.IMPORTANT) + .build(); + final ContextualCard batteryFixCard = + ContextualCard.newBuilder() + .setSliceUri(CustomSliceRegistry.BATTERY_FIX_SLICE_URI.toString()) + .setCardName(CustomSliceRegistry.BATTERY_FIX_SLICE_URI.toString()) + .setCardCategory(ContextualCard.Category.IMPORTANT) + .build(); + final String contextualNotificationChannelSliceUri = + CustomSliceRegistry.CONTEXTUAL_NOTIFICATION_CHANNEL_SLICE_URI.toString(); + final ContextualCard notificationChannelCard = + ContextualCard.newBuilder() + .setSliceUri(contextualNotificationChannelSliceUri) + .setCardName(contextualNotificationChannelSliceUri) + .setCardCategory(ContextualCard.Category.POSSIBLE) + .build(); + final ContextualCardList cards = ContextualCardList.newBuilder() + .addCard(wifiCard) + .addCard(connectedDeviceCard) + .addCard(lowStorageCard) + .addCard(batteryFixCard) + .addCard(notificationChannelCard) + .build(); + + return cards; + } +} diff --git a/src/com/android/settings/dashboard/conditional/AbnormalRingerConditionBase.java b/src/com/android/settings/homepage/contextualcards/conditional/AbnormalRingerConditionController.java index eaec6d08cc..6a66d2f955 100644 --- a/src/com/android/settings/dashboard/conditional/AbnormalRingerConditionBase.java +++ b/src/com/android/settings/homepage/contextualcards/conditional/AbnormalRingerConditionController.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.settings.dashboard.conditional; +package com.android.settings.homepage.contextualcards.conditional; import android.content.BroadcastReceiver; import android.content.Context; @@ -23,60 +23,51 @@ import android.content.IntentFilter; import android.media.AudioManager; import android.provider.Settings; -import com.android.settings.R; +public abstract class AbnormalRingerConditionController implements ConditionalCardController { -public abstract class AbnormalRingerConditionBase extends Condition { - - private final IntentFilter mFilter; + private static final IntentFilter FILTER = + new IntentFilter(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION); protected final AudioManager mAudioManager; - + private final Context mAppContext; + private final ConditionManager mConditionManager; private final RingerModeChangeReceiver mReceiver; - AbnormalRingerConditionBase(ConditionManager manager) { - super(manager); - mAudioManager = - (AudioManager) mManager.getContext().getSystemService(Context.AUDIO_SERVICE); - mReceiver = new RingerModeChangeReceiver(this); - - mFilter = new IntentFilter(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION); - manager.getContext().registerReceiver(mReceiver, mFilter); - } - - @Override - public CharSequence[] getActions() { - return new CharSequence[] { - mManager.getContext().getText(R.string.condition_device_muted_action_turn_on_sound) - }; + public AbnormalRingerConditionController(Context appContext, ConditionManager manager) { + mAppContext = appContext; + mConditionManager = manager; + mAudioManager = (AudioManager) appContext.getSystemService(Context.AUDIO_SERVICE); + mReceiver = new RingerModeChangeReceiver(); } @Override - public void onPrimaryClick() { - mManager.getContext().startActivity( - new Intent(Settings.ACTION_SOUND_SETTINGS) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + public void onPrimaryClick(Context context) { + context.startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS)); } @Override - public void onActionClick(int index) { + public void onActionClick() { mAudioManager.setRingerModeInternal(AudioManager.RINGER_MODE_NORMAL); mAudioManager.setStreamVolume(AudioManager.STREAM_RING, 1, 0 /* flags */); - refreshState(); } - static class RingerModeChangeReceiver extends BroadcastReceiver { + @Override + public void startMonitoringStateChange() { + mAppContext.registerReceiver(mReceiver, FILTER); + } - private final AbnormalRingerConditionBase mCondition; + @Override + public void stopMonitoringStateChange() { + mAppContext.unregisterReceiver(mReceiver); + } - public RingerModeChangeReceiver(AbnormalRingerConditionBase condition) { - mCondition = condition; - } + class RingerModeChangeReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); if (AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION.equals(action)) { - mCondition.refreshState(); + mConditionManager.onConditionChanged(); } } } diff --git a/src/com/android/settings/homepage/contextualcards/conditional/AirplaneModeConditionController.java b/src/com/android/settings/homepage/contextualcards/conditional/AirplaneModeConditionController.java new file mode 100644 index 0000000000..2f55b3f5f5 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/conditional/AirplaneModeConditionController.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards.conditional; + +import android.app.settings.SettingsEnums; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.provider.Settings; + +import com.android.settings.R; +import com.android.settings.homepage.contextualcards.ContextualCard; +import com.android.settingslib.WirelessUtils; + +import java.util.Objects; + +public class AirplaneModeConditionController implements ConditionalCardController { + + static final int ID = Objects.hash("AirplaneModeConditionController"); + + private static final IntentFilter AIRPLANE_MODE_FILTER = + new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED); + + private final ConditionManager mConditionManager; + private final Context mAppContext; + private final Receiver mReceiver; + + public AirplaneModeConditionController(Context appContext, ConditionManager conditionManager) { + mAppContext = appContext; + mConditionManager = conditionManager; + mReceiver = new Receiver(); + } + + @Override + public long getId() { + return ID; + } + + @Override + public boolean isDisplayable() { + return WirelessUtils.isAirplaneModeOn(mAppContext); + } + + @Override + public void onPrimaryClick(Context context) { + context.startActivity( + new Intent(Settings.ACTION_WIRELESS_SETTINGS)); + } + + @Override + public void onActionClick() { + ConnectivityManager.from(mAppContext).setAirplaneMode(false); + } + + @Override + public ContextualCard buildContextualCard() { + return new ConditionalContextualCard.Builder() + .setConditionId(ID) + .setMetricsConstant(SettingsEnums.SETTINGS_CONDITION_AIRPLANE_MODE) + .setActionText(mAppContext.getText(R.string.condition_turn_off)) + .setName(mAppContext.getPackageName() + "/" + + mAppContext.getText(R.string.condition_airplane_title)) + .setTitleText(mAppContext.getText(R.string.condition_airplane_title).toString()) + .setSummaryText(mAppContext.getText(R.string.condition_airplane_summary).toString()) + .setIconDrawable(mAppContext.getDrawable(R.drawable.ic_airplanemode_active)) + .setViewType(ConditionContextualCardRenderer.VIEW_TYPE_HALF_WIDTH) + .build(); + } + + @Override + public void startMonitoringStateChange() { + mAppContext.registerReceiver(mReceiver, AIRPLANE_MODE_FILTER); + } + + @Override + public void stopMonitoringStateChange() { + mAppContext.unregisterReceiver(mReceiver); + } + + public class Receiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (Intent.ACTION_AIRPLANE_MODE_CHANGED.equals(intent.getAction())) { + mConditionManager.onConditionChanged(); + } + } + } +} diff --git a/src/com/android/settings/homepage/contextualcards/conditional/BackgroundDataConditionController.java b/src/com/android/settings/homepage/contextualcards/conditional/BackgroundDataConditionController.java new file mode 100644 index 0000000000..419e5c6f7a --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/conditional/BackgroundDataConditionController.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards.conditional; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.Intent; +import android.net.NetworkPolicyManager; + +import com.android.settings.R; +import com.android.settings.Settings; +import com.android.settings.homepage.contextualcards.ContextualCard; + +import java.util.Objects; + +public class BackgroundDataConditionController implements ConditionalCardController { + static final int ID = Objects.hash("BackgroundDataConditionController"); + + private final Context mAppContext; + private final ConditionManager mConditionManager; + private final NetworkPolicyManager mNetworkPolicyManager; + + public BackgroundDataConditionController(Context appContext, ConditionManager manager) { + mAppContext = appContext; + mConditionManager = manager; + mNetworkPolicyManager = + (NetworkPolicyManager) appContext.getSystemService(Context.NETWORK_POLICY_SERVICE); + } + + @Override + public long getId() { + return ID; + } + + @Override + public boolean isDisplayable() { + return mNetworkPolicyManager.getRestrictBackground(); + } + + @Override + public void onPrimaryClick(Context context) { + context.startActivity(new Intent(context, Settings.DataUsageSummaryActivity.class)); + } + + @Override + public void onActionClick() { + mNetworkPolicyManager.setRestrictBackground(false); + mConditionManager.onConditionChanged(); + } + + @Override + public ContextualCard buildContextualCard() { + return new ConditionalContextualCard.Builder() + .setConditionId(ID) + .setMetricsConstant(SettingsEnums.SETTINGS_CONDITION_BACKGROUND_DATA) + .setActionText(mAppContext.getText(R.string.condition_turn_off)) + .setName(mAppContext.getPackageName() + "/" + + mAppContext.getText(R.string.condition_bg_data_title)) + .setTitleText(mAppContext.getText(R.string.condition_bg_data_title).toString()) + .setSummaryText(mAppContext.getText(R.string.condition_bg_data_summary).toString()) + .setIconDrawable(mAppContext.getDrawable(R.drawable.ic_data_saver)) + .setViewType(ConditionContextualCardRenderer.VIEW_TYPE_HALF_WIDTH) + .build(); + } + + @Override + public void startMonitoringStateChange() { + + } + + @Override + public void stopMonitoringStateChange() { + + } +} diff --git a/src/com/android/settings/homepage/contextualcards/conditional/BatterySaverConditionController.java b/src/com/android/settings/homepage/contextualcards/conditional/BatterySaverConditionController.java new file mode 100644 index 0000000000..b90c1b5c41 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/conditional/BatterySaverConditionController.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards.conditional; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.os.PowerManager; + +import com.android.settings.R; +import com.android.settings.core.SubSettingLauncher; +import com.android.settings.fuelgauge.BatterySaverReceiver; +import com.android.settings.fuelgauge.batterysaver.BatterySaverSettings; +import com.android.settings.homepage.contextualcards.ContextualCard; +import com.android.settingslib.fuelgauge.BatterySaverUtils; + +import java.util.Objects; + +public class BatterySaverConditionController implements ConditionalCardController, + BatterySaverReceiver.BatterySaverListener { + static final int ID = Objects.hash("BatterySaverConditionController"); + + private final Context mAppContext; + private final ConditionManager mConditionManager; + private final BatterySaverReceiver mReceiver; + private final PowerManager mPowerManager; + + public BatterySaverConditionController(Context appContext, ConditionManager conditionManager) { + mAppContext = appContext; + mConditionManager = conditionManager; + mPowerManager = appContext.getSystemService(PowerManager.class); + mReceiver = new BatterySaverReceiver(appContext); + mReceiver.setBatterySaverListener(this); + } + + @Override + public long getId() { + return ID; + } + + @Override + public boolean isDisplayable() { + return mPowerManager.isPowerSaveMode(); + } + + @Override + public void onPrimaryClick(Context context) { + new SubSettingLauncher(context) + .setDestination(BatterySaverSettings.class.getName()) + .setSourceMetricsCategory(SettingsEnums.DASHBOARD_SUMMARY) + .setTitleRes(R.string.battery_saver) + .launch(); + } + + @Override + public void onActionClick() { + BatterySaverUtils.setPowerSaveMode(mAppContext, false, + /*needFirstTimeWarning*/ false); + } + + @Override + public ContextualCard buildContextualCard() { + return new ConditionalContextualCard.Builder() + .setConditionId(ID) + .setMetricsConstant(SettingsEnums.SETTINGS_CONDITION_BATTERY_SAVER) + .setActionText(mAppContext.getText(R.string.condition_turn_off)) + .setName(mAppContext.getPackageName() + "/" + + mAppContext.getText(R.string.condition_battery_title)) + .setTitleText(mAppContext.getText(R.string.condition_battery_title).toString()) + .setSummaryText(mAppContext.getText(R.string.condition_battery_summary).toString()) + .setIconDrawable(mAppContext.getDrawable(R.drawable.ic_battery_saver_accent_24dp)) + .setViewType(ConditionContextualCardRenderer.VIEW_TYPE_HALF_WIDTH) + .build(); + } + + @Override + public void startMonitoringStateChange() { + mReceiver.setListening(true); + } + + @Override + public void stopMonitoringStateChange() { + mReceiver.setListening(false); + } + + @Override + public void onPowerSaveModeChanged() { + mConditionManager.onConditionChanged(); + } + + @Override + public void onBatteryChanged(boolean pluggedIn) { + + } +} diff --git a/src/com/android/settings/homepage/contextualcards/conditional/CellularDataConditionController.java b/src/com/android/settings/homepage/contextualcards/conditional/CellularDataConditionController.java new file mode 100644 index 0000000000..93b937b59a --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/conditional/CellularDataConditionController.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards.conditional; + +import android.app.settings.SettingsEnums; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.telephony.TelephonyManager; + +import com.android.internal.telephony.TelephonyIntents; +import com.android.settings.R; +import com.android.settings.Settings; +import com.android.settings.homepage.contextualcards.ContextualCard; + +import java.util.Objects; + +public class CellularDataConditionController implements ConditionalCardController { + + static final int ID = Objects.hash("CellularDataConditionController"); + + private static final IntentFilter DATA_CONNECTION_FILTER = + new IntentFilter(TelephonyIntents.ACTION_ANY_DATA_CONNECTION_STATE_CHANGED); + + private final Context mAppContext; + private final ConditionManager mConditionManager; + private final Receiver mReceiver; + private final TelephonyManager mTelephonyManager; + private final ConnectivityManager mConnectivityManager; + + public CellularDataConditionController(Context appContext, ConditionManager conditionManager) { + mAppContext = appContext; + mConditionManager = conditionManager; + mReceiver = new Receiver(); + mConnectivityManager = appContext.getSystemService( + ConnectivityManager.class); + mTelephonyManager = appContext.getSystemService(TelephonyManager.class); + } + + @Override + public long getId() { + return ID; + } + + @Override + public boolean isDisplayable() { + if (!mConnectivityManager.isNetworkSupported(ConnectivityManager.TYPE_MOBILE) + || mTelephonyManager.getSimState() != TelephonyManager.SIM_STATE_READY) { + return false; + } + return !mTelephonyManager.isDataEnabled(); + } + + @Override + public void onPrimaryClick(Context context) { + context.startActivity(new Intent(context, + Settings.DataUsageSummaryActivity.class)); + } + + @Override + public void onActionClick() { + mTelephonyManager.setDataEnabled(true); + } + + @Override + public ContextualCard buildContextualCard() { + return new ConditionalContextualCard.Builder() + .setConditionId(ID) + .setMetricsConstant(SettingsEnums.SETTINGS_CONDITION_CELLULAR_DATA) + .setActionText(mAppContext.getText(R.string.condition_turn_on)) + .setName(mAppContext.getPackageName() + "/" + + mAppContext.getText(R.string.condition_cellular_title)) + .setTitleText(mAppContext.getText(R.string.condition_cellular_title).toString()) + .setSummaryText(mAppContext.getText(R.string.condition_cellular_summary).toString()) + .setIconDrawable(mAppContext.getDrawable(R.drawable.ic_cellular_off)) + .setViewType(ConditionContextualCardRenderer.VIEW_TYPE_HALF_WIDTH) + .build(); + } + + @Override + public void startMonitoringStateChange() { + mAppContext.registerReceiver(mReceiver, DATA_CONNECTION_FILTER); + } + + @Override + public void stopMonitoringStateChange() { + mAppContext.unregisterReceiver(mReceiver); + } + + public class Receiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (TelephonyIntents.ACTION_ANY_DATA_CONNECTION_STATE_CHANGED.equals( + intent.getAction())) { + mConditionManager.onConditionChanged(); + } + } + } +} diff --git a/src/com/android/settings/homepage/contextualcards/conditional/ConditionContextualCardController.java b/src/com/android/settings/homepage/contextualcards/conditional/ConditionContextualCardController.java new file mode 100644 index 0000000000..25d1cdad17 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/conditional/ConditionContextualCardController.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards.conditional; + +import android.content.Context; +import android.util.ArrayMap; + +import androidx.annotation.VisibleForTesting; + +import com.android.settings.homepage.contextualcards.ContextualCard; +import com.android.settings.homepage.contextualcards.ContextualCardController; +import com.android.settings.homepage.contextualcards.ContextualCardUpdateListener; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.core.lifecycle.events.OnStop; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * This controller triggers the loading of conditional cards and monitors state changes to + * update the homepage. + */ +public class ConditionContextualCardController implements ContextualCardController, + ConditionListener, LifecycleObserver, OnStart, OnStop { + public static final int EXPANDING_THRESHOLD = 0; + + private static final double UNSUPPORTED_RANKING = -99999.0; + private static final String TAG = "ConditionCtxCardCtrl"; + private static final String CONDITION_FOOTER = "condition_footer"; + private static final String CONDITION_HEADER = "condition_header"; + + private final Context mContext; + private final ConditionManager mConditionManager; + + private ContextualCardUpdateListener mListener; + private boolean mIsExpanded; + + public ConditionContextualCardController(Context context) { + mContext = context; + mConditionManager = new ConditionManager(context.getApplicationContext(), this); + mConditionManager.startMonitoringStateChange(); + } + + public void setIsExpanded(boolean isExpanded) { + mIsExpanded = isExpanded; + } + + @Override + public void setCardUpdateListener(ContextualCardUpdateListener listener) { + mListener = listener; + } + + @Override + public int getCardType() { + return ContextualCard.CardType.CONDITIONAL; + } + + @Override + public void onStart() { + mConditionManager.startMonitoringStateChange(); + } + + @Override + public void onStop() { + mConditionManager.stopMonitoringStateChange(); + } + + @Override + public void onPrimaryClick(ContextualCard contextualCard) { + final ConditionalContextualCard card = (ConditionalContextualCard) contextualCard; + mConditionManager.onPrimaryClick(mContext, card.getConditionId()); + } + + @Override + public void onActionClick(ContextualCard contextualCard) { + final ConditionalContextualCard card = (ConditionalContextualCard) contextualCard; + mConditionManager.onActionClick(card.getConditionId()); + } + + @Override + public void onDismissed(ContextualCard contextualCard) { + + } + + @Override + public void onConditionsChanged() { + if (mListener == null) { + return; + } + final List<ContextualCard> conditionCards = mConditionManager.getDisplayableCards(); + final Map<Integer, List<ContextualCard>> conditionalCards = + buildConditionalCardsWithFooterOrHeader(conditionCards); + mListener.onContextualCardUpdated(conditionalCards); + } + + /** + * According to conditional cards, build a map that includes conditional cards, header card and + * footer card. + * + * Rules: + * - The last one of conditional cards will be displayed as a full-width card if the size of + * conditional cards is odd number. The rest will be displayed as a half-width card. + * - By default conditional cards will be collapsed if there are more than TWO cards. + * + * For examples: + * - Only one conditional card: Returns a map that contains a full-width conditional card, + * no header card and no footer card. + * <p>Map{(CONDITIONAL, conditionCards), (CONDITIONAL_FOOTER, EMPTY_LIST), (CONDITIONAL_HEADER, + * EMPTY_LIST)}</p> + * - Two conditional cards: Returns a map that contains two half-width conditional cards, + * no header card and no footer card. + * <p>Map{(CONDITIONAL, conditionCards), (CONDITIONAL_FOOTER, EMPTY_LIST), (CONDITIONAL_HEADER, + * EMPTY_LIST)}</p> + * - Three conditional cards or above: By default, returns a map that contains no conditional + * card, one header card and no footer card. If conditional cards are expanded, will returns a + * map that contains three conditional cards, no header card and one footer card. + * If expanding conditional cards: + * <p>Map{(CONDITIONAL, conditionCards), (CONDITIONAL_FOOTER, footerCards), (CONDITIONAL_HEADER, + * EMPTY_LIST)}</p> + * If collapsing conditional cards: + * <p>Map{(CONDITIONAL, EMPTY_LIST), (CONDITIONAL_FOOTER, EMPTY_LIST), (CONDITIONAL_HEADER, + * headerCards)}</p> + * + * @param conditionCards A list of conditional cards that are from {@link + * ConditionManager#getDisplayableCards} + * @return A map contained three types of lists + */ + @VisibleForTesting + Map<Integer, List<ContextualCard>> buildConditionalCardsWithFooterOrHeader( + List<ContextualCard> conditionCards) { + final Map<Integer, List<ContextualCard>> conditionalCards = new ArrayMap<>(); + conditionalCards.put(ContextualCard.CardType.CONDITIONAL, + getExpandedConditionalCards(conditionCards)); + conditionalCards.put(ContextualCard.CardType.CONDITIONAL_FOOTER, + getConditionalFooterCard(conditionCards)); + conditionalCards.put(ContextualCard.CardType.CONDITIONAL_HEADER, + getConditionalHeaderCard(conditionCards)); + return conditionalCards; + } + + private List<ContextualCard> getExpandedConditionalCards(List<ContextualCard> conditionCards) { + if (conditionCards.isEmpty() || (conditionCards.size() > EXPANDING_THRESHOLD + && !mIsExpanded)) { + return Collections.EMPTY_LIST; + } + final List<ContextualCard> expandedCards = conditionCards.stream().collect( + Collectors.toList()); + final boolean isOddNumber = expandedCards.size() % 2 == 1; + if (isOddNumber) { + final int lastIndex = expandedCards.size() - 1; + final ConditionalContextualCard card = + (ConditionalContextualCard) expandedCards.get(lastIndex); + expandedCards.set(lastIndex, card.mutate().setViewType( + ConditionContextualCardRenderer.VIEW_TYPE_FULL_WIDTH).build()); + } + return expandedCards; + } + + private List<ContextualCard> getConditionalFooterCard(List<ContextualCard> conditionCards) { + if (!conditionCards.isEmpty() && mIsExpanded + && conditionCards.size() > EXPANDING_THRESHOLD) { + final List<ContextualCard> footerCards = new ArrayList<>(); + footerCards.add(new ConditionFooterContextualCard.Builder() + .setName(CONDITION_FOOTER) + .setRankingScore(UNSUPPORTED_RANKING) + .setViewType(ConditionFooterContextualCardRenderer.VIEW_TYPE) + .build()); + return footerCards; + } + return Collections.EMPTY_LIST; + } + + private List<ContextualCard> getConditionalHeaderCard(List<ContextualCard> conditionCards) { + if (!conditionCards.isEmpty() && !mIsExpanded + && conditionCards.size() > EXPANDING_THRESHOLD) { + final List<ContextualCard> headerCards = new ArrayList<>(); + headerCards.add(new ConditionHeaderContextualCard.Builder() + .setConditionalCards(conditionCards) + .setName(CONDITION_HEADER) + .setRankingScore(UNSUPPORTED_RANKING) + .setViewType(ConditionHeaderContextualCardRenderer.VIEW_TYPE) + .build()); + return headerCards; + } + return Collections.EMPTY_LIST; + } +} diff --git a/src/com/android/settings/homepage/contextualcards/conditional/ConditionContextualCardRenderer.java b/src/com/android/settings/homepage/contextualcards/conditional/ConditionContextualCardRenderer.java new file mode 100644 index 0000000000..91b2e979f8 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/conditional/ConditionContextualCardRenderer.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards.conditional; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.text.TextUtils; +import android.view.View; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.LayoutRes; +import androidx.recyclerview.widget.RecyclerView; + +import com.android.settings.R; +import com.android.settings.homepage.contextualcards.ContextualCard; +import com.android.settings.homepage.contextualcards.ContextualCardRenderer; +import com.android.settings.homepage.contextualcards.ControllerRendererPool; +import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; + +/** + * Card renderer for {@link ConditionalContextualCard}. + */ +public class ConditionContextualCardRenderer implements ContextualCardRenderer { + @LayoutRes + public static final int VIEW_TYPE_HALF_WIDTH = R.layout.conditional_card_half_tile; + @LayoutRes + public static final int VIEW_TYPE_FULL_WIDTH = R.layout.conditional_card_full_tile; + + private final Context mContext; + private final ControllerRendererPool mControllerRendererPool; + + public ConditionContextualCardRenderer(Context context, + ControllerRendererPool controllerRendererPool) { + mContext = context; + mControllerRendererPool = controllerRendererPool; + } + + @Override + public RecyclerView.ViewHolder createViewHolder(View view, @LayoutRes int viewType) { + return new ConditionalCardHolder(view); + } + + @Override + public void bindView(RecyclerView.ViewHolder holder, ContextualCard contextualCard) { + final ConditionalCardHolder view = (ConditionalCardHolder) holder; + final ConditionalContextualCard card = (ConditionalContextualCard) contextualCard; + final MetricsFeatureProvider metricsFeatureProvider = FeatureFactory.getFactory( + mContext).getMetricsFeatureProvider(); + + metricsFeatureProvider.visible(mContext, SettingsEnums.SETTINGS_HOMEPAGE, + card.getMetricsConstant()); + initializePrimaryClick(view, card, metricsFeatureProvider); + initializeView(view, card); + initializeActionButton(view, card, metricsFeatureProvider); + } + + private void initializePrimaryClick(ConditionalCardHolder view, ConditionalContextualCard card, + MetricsFeatureProvider metricsFeatureProvider) { + view.itemView.findViewById(R.id.content).setOnClickListener( + v -> { + metricsFeatureProvider.action(mContext, + SettingsEnums.ACTION_SETTINGS_CONDITION_CLICK, + card.getMetricsConstant()); + mControllerRendererPool.getController(mContext, + card.getCardType()).onPrimaryClick(card); + }); + } + + private void initializeView(ConditionalCardHolder view, ConditionalContextualCard card) { + view.icon.setImageDrawable(card.getIconDrawable()); + view.title.setText(card.getTitleText()); + view.summary.setText(card.getSummaryText()); + } + + private void initializeActionButton(ConditionalCardHolder view, ConditionalContextualCard card, + MetricsFeatureProvider metricsFeatureProvider) { + final CharSequence action = card.getActionText(); + final boolean hasButtons = !TextUtils.isEmpty(action); + + final Button button = view.itemView.findViewById(R.id.first_action); + if (hasButtons) { + button.setVisibility(View.VISIBLE); + button.setText(action); + button.setOnClickListener(v -> { + final Context viewContext = v.getContext(); + metricsFeatureProvider.action( + viewContext, SettingsEnums.ACTION_SETTINGS_CONDITION_BUTTON, + card.getMetricsConstant()); + mControllerRendererPool.getController(mContext, card.getCardType()) + .onActionClick(card); + }); + } else { + button.setVisibility(View.GONE); + } + } + + public static class ConditionalCardHolder extends RecyclerView.ViewHolder { + + public final ImageView icon; + public final TextView title; + public final TextView summary; + + public ConditionalCardHolder(View itemView) { + super(itemView); + icon = itemView.findViewById(android.R.id.icon); + title = itemView.findViewById(android.R.id.title); + summary = itemView.findViewById(android.R.id.summary); + } + } +} diff --git a/src/com/android/settings/homepage/contextualcards/conditional/ConditionFooterContextualCard.java b/src/com/android/settings/homepage/contextualcards/conditional/ConditionFooterContextualCard.java new file mode 100644 index 0000000000..17a5bfa45d --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/conditional/ConditionFooterContextualCard.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards.conditional; + +import com.android.settings.homepage.contextualcards.ContextualCard; + +/** + * Data class representing a condition footer {@link ContextualCard}. + * + * Use this class for {@link ConditionFooterContextualCardRenderer} and + * {@link ConditionContextualCardController}. + */ +public class ConditionFooterContextualCard extends ContextualCard { + + private ConditionFooterContextualCard(Builder builder) { + super(builder); + } + + @Override + public int getCardType() { + return CardType.CONDITIONAL_FOOTER; + } + + public static class Builder extends ContextualCard.Builder { + + @Override + public Builder setCardType(int cardType) { + throw new IllegalArgumentException( + "Cannot change card type for " + getClass().getName()); + } + + public ConditionFooterContextualCard build() { + return new ConditionFooterContextualCard(this); + } + } +}
\ No newline at end of file diff --git a/src/com/android/settings/homepage/contextualcards/conditional/ConditionFooterContextualCardRenderer.java b/src/com/android/settings/homepage/contextualcards/conditional/ConditionFooterContextualCardRenderer.java new file mode 100644 index 0000000000..2944cc8868 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/conditional/ConditionFooterContextualCardRenderer.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards.conditional; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.view.View; + +import androidx.annotation.LayoutRes; +import androidx.recyclerview.widget.RecyclerView; + +import com.android.settings.R; +import com.android.settings.homepage.contextualcards.ContextualCard; +import com.android.settings.homepage.contextualcards.ContextualCardRenderer; +import com.android.settings.homepage.contextualcards.ControllerRendererPool; +import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; + +public class ConditionFooterContextualCardRenderer implements ContextualCardRenderer { + public static final int VIEW_TYPE = R.layout.conditional_card_footer; + private static final String TAG = "ConditionFooterRenderer"; + + private final Context mContext; + private final ControllerRendererPool mControllerRendererPool; + + public ConditionFooterContextualCardRenderer(Context context, + ControllerRendererPool controllerRendererPool) { + mContext = context; + mControllerRendererPool = controllerRendererPool; + } + + @Override + public RecyclerView.ViewHolder createViewHolder(View view, @LayoutRes int viewType) { + return new ConditionFooterCardHolder(view); + } + + @Override + public void bindView(RecyclerView.ViewHolder holder, ContextualCard card) { + final MetricsFeatureProvider metricsFeatureProvider = FeatureFactory.getFactory( + mContext).getMetricsFeatureProvider(); + holder.itemView.setOnClickListener(v -> { + metricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, + SettingsEnums.ACTION_SETTINGS_CONDITION_EXPAND, + SettingsEnums.SETTINGS_HOMEPAGE, + null /* key */, + 0 /* false */); + final ConditionContextualCardController controller = + mControllerRendererPool.getController(mContext, + ContextualCard.CardType.CONDITIONAL_FOOTER); + controller.setIsExpanded(false); + controller.onConditionsChanged(); + }); + } + + public static class ConditionFooterCardHolder extends RecyclerView.ViewHolder { + public ConditionFooterCardHolder(View itemView) { + super(itemView); + } + } +} diff --git a/src/com/android/settings/homepage/contextualcards/conditional/ConditionHeaderContextualCard.java b/src/com/android/settings/homepage/contextualcards/conditional/ConditionHeaderContextualCard.java new file mode 100644 index 0000000000..b72f9f7e6a --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/conditional/ConditionHeaderContextualCard.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards.conditional; + +import android.text.TextUtils; + +import com.android.settings.homepage.contextualcards.ContextualCard; + +import java.util.List; +import java.util.Objects; + +/** + * Data class representing a condition header {@link ContextualCard}. + * + * Use this class to store additional attributes on top of {@link ContextualCard} for + * {@link ConditionHeaderContextualCardRenderer} and {@link ConditionContextualCardController}. + */ +public class ConditionHeaderContextualCard extends ContextualCard { + + private final List<ContextualCard> mConditionalCards; + + private ConditionHeaderContextualCard(Builder builder) { + super(builder); + mConditionalCards = builder.mConditionalCards; + } + + @Override + public int getCardType() { + return CardType.CONDITIONAL_HEADER; + } + + public List<ContextualCard> getConditionalCards() { + return mConditionalCards; + } + + @Override + public int hashCode() { + return Objects.hash(getName(), mConditionalCards); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof ConditionHeaderContextualCard)) { + return false; + } + final ConditionHeaderContextualCard that = (ConditionHeaderContextualCard) obj; + + return TextUtils.equals(getName(), that.getName()) && mConditionalCards.equals( + that.mConditionalCards); + } + + public static class Builder extends ContextualCard.Builder { + + private List<ContextualCard> mConditionalCards; + + public Builder setConditionalCards(List<ContextualCard> conditionalCards) { + mConditionalCards = conditionalCards; + return this; + } + + @Override + public Builder setCardType(int cardType) { + throw new IllegalArgumentException( + "Cannot change card type for " + getClass().getName()); + } + + public ConditionHeaderContextualCard build() { + return new ConditionHeaderContextualCard(this); + } + } +}
\ No newline at end of file diff --git a/src/com/android/settings/homepage/contextualcards/conditional/ConditionHeaderContextualCardRenderer.java b/src/com/android/settings/homepage/contextualcards/conditional/ConditionHeaderContextualCardRenderer.java new file mode 100644 index 0000000000..c5e987a498 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/conditional/ConditionHeaderContextualCardRenderer.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards.conditional; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; + +import androidx.annotation.LayoutRes; +import androidx.recyclerview.widget.RecyclerView; + +import com.android.settings.R; +import com.android.settings.homepage.contextualcards.ContextualCard; +import com.android.settings.homepage.contextualcards.ContextualCardRenderer; +import com.android.settings.homepage.contextualcards.ControllerRendererPool; +import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; + +public class ConditionHeaderContextualCardRenderer implements ContextualCardRenderer { + public static final int VIEW_TYPE = R.layout.conditional_card_header; + private static final String TAG = "ConditionHeaderRenderer"; + + private final Context mContext; + private final ControllerRendererPool mControllerRendererPool; + + public ConditionHeaderContextualCardRenderer(Context context, + ControllerRendererPool controllerRendererPool) { + mContext = context; + mControllerRendererPool = controllerRendererPool; + } + + @Override + public RecyclerView.ViewHolder createViewHolder(View view, @LayoutRes int viewType) { + return new ConditionHeaderCardHolder(view); + } + + @Override + public void bindView(RecyclerView.ViewHolder holder, ContextualCard contextualCard) { + final ConditionHeaderContextualCard headerCard = + (ConditionHeaderContextualCard) contextualCard; + final ConditionHeaderCardHolder view = (ConditionHeaderCardHolder) holder; + final MetricsFeatureProvider metricsFeatureProvider = FeatureFactory.getFactory( + mContext).getMetricsFeatureProvider(); + view.icons.removeAllViews(); + headerCard.getConditionalCards().stream().forEach(card -> { + final ImageView icon = (ImageView) LayoutInflater.from(mContext).inflate( + R.layout.conditional_card_header_icon, view.icons, false); + icon.setImageDrawable(card.getIconDrawable()); + view.icons.addView(icon); + }); + view.itemView.setOnClickListener(v -> { + metricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, + SettingsEnums.ACTION_SETTINGS_CONDITION_EXPAND, + SettingsEnums.SETTINGS_HOMEPAGE, + null /* key */, + 1 /* true */); + final ConditionContextualCardController controller = + mControllerRendererPool.getController(mContext, + ContextualCard.CardType.CONDITIONAL_HEADER); + controller.setIsExpanded(true); + controller.onConditionsChanged(); + }); + } + + public static class ConditionHeaderCardHolder extends RecyclerView.ViewHolder { + public final LinearLayout icons; + public final ImageView expandIndicator; + + public ConditionHeaderCardHolder(View itemView) { + super(itemView); + icons = itemView.findViewById(R.id.header_icons_container); + expandIndicator = itemView.findViewById(R.id.expand_indicator); + } + } +} diff --git a/src/com/android/settings/password/StorageManagerWrapper.java b/src/com/android/settings/homepage/contextualcards/conditional/ConditionListener.java index 5adfaf2e51..5f743b3895 100644 --- a/src/com/android/settings/password/StorageManagerWrapper.java +++ b/src/com/android/settings/homepage/contextualcards/conditional/ConditionListener.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 The Android Open Source Project + * Copyright (C) 2018 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. @@ -14,16 +14,8 @@ * limitations under the License. */ -package com.android.settings.password; +package com.android.settings.homepage.contextualcards.conditional; -import android.os.storage.StorageManager; - -/** - * Wrapper class to allow Robolectric to shadow methods introduced in newer API - */ -public class StorageManagerWrapper { - - public static boolean isFileEncryptedNativeOrEmulated() { - return StorageManager.isFileEncryptedNativeOrEmulated(); - } +public interface ConditionListener { + void onConditionsChanged(); } diff --git a/src/com/android/settings/homepage/contextualcards/conditional/ConditionManager.java b/src/com/android/settings/homepage/contextualcards/conditional/ConditionManager.java new file mode 100644 index 0000000000..66f6c81e98 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/conditional/ConditionManager.java @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards.conditional; + +import android.content.Context; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + +import com.android.settings.homepage.contextualcards.ContextualCard; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public class ConditionManager { + private static final String TAG = "ConditionManager"; + + @VisibleForTesting + final List<ConditionalCardController> mCardControllers; + + private static final long DISPLAYABLE_CHECKER_TIMEOUT_MS = 20; + + private final ExecutorService mExecutorService; + private final Context mAppContext; + private final ConditionListener mListener; + + private boolean mIsListeningToStateChange; + + public ConditionManager(Context context, ConditionListener listener) { + mAppContext = context.getApplicationContext(); + mExecutorService = Executors.newCachedThreadPool(); + mCardControllers = new ArrayList<>(); + mListener = listener; + initCandidates(); + } + + /** + * Returns a list of {@link ContextualCard}s eligible for display. + */ + public List<ContextualCard> getDisplayableCards() { + final List<ContextualCard> cards = new ArrayList<>(); + final List<Future<ContextualCard>> displayableCards = new ArrayList<>(); + // Check displayable future + for (ConditionalCardController card : mCardControllers) { + final DisplayableChecker future = new DisplayableChecker(getController(card.getId())); + displayableCards.add(mExecutorService.submit(future)); + } + // Collect future and add displayable cards + for (Future<ContextualCard> cardFuture : displayableCards) { + try { + final ContextualCard card = cardFuture.get( + DISPLAYABLE_CHECKER_TIMEOUT_MS, TimeUnit.MILLISECONDS); + if (card != null) { + cards.add(card); + } + } catch (InterruptedException | ExecutionException | TimeoutException e) { + Log.w(TAG, "Failed to get displayable state for card, likely timeout. Skipping", e); + } + } + return cards; + } + + /** + * Handler when the card is clicked. + * + * @see {@link ConditionalCardController#onPrimaryClick(Context)} + */ + public void onPrimaryClick(Context context, long id) { + getController(id).onPrimaryClick(context); + } + + /** + * Handler when the card action is clicked. + * + * @see {@link ConditionalCardController#onActionClick()} + */ + public void onActionClick(long id) { + getController(id).onActionClick(); + } + + /** + * Start monitoring state change for all conditions + */ + public void startMonitoringStateChange() { + if (mIsListeningToStateChange) { + Log.d(TAG, "Already listening to condition state changes, skipping monitor setup"); + } else { + mIsListeningToStateChange = true; + for (ConditionalCardController controller : mCardControllers) { + controller.startMonitoringStateChange(); + } + } + // Force a refresh on listener + onConditionChanged(); + } + + /** + * Stop monitoring state change for all conditions + */ + public void stopMonitoringStateChange() { + if (!mIsListeningToStateChange) { + Log.d(TAG, "Not listening to condition state changes, skipping"); + return; + } + for (ConditionalCardController controller : mCardControllers) { + controller.stopMonitoringStateChange(); + } + mIsListeningToStateChange = false; + } + + /** + * Called when some conditional card's state has changed + */ + void onConditionChanged() { + if (mListener != null) { + mListener.onConditionsChanged(); + } + } + + @NonNull + private <T extends ConditionalCardController> T getController(long id) { + for (ConditionalCardController controller : mCardControllers) { + if (controller.getId() == id) { + return (T) controller; + } + } + throw new IllegalStateException("Cannot find controller for " + id); + } + + private void initCandidates() { + // Initialize controllers first. + mCardControllers.add(new AirplaneModeConditionController(mAppContext, this /* manager */)); + mCardControllers.add( + new BackgroundDataConditionController(mAppContext, this /* manager */)); + mCardControllers.add(new BatterySaverConditionController(mAppContext, this /* manager */)); + mCardControllers.add(new CellularDataConditionController(mAppContext, this /* manager */)); + mCardControllers.add(new DndConditionCardController(mAppContext, this /* manager */)); + mCardControllers.add(new HotspotConditionController(mAppContext, this /* manager */)); + mCardControllers.add(new NightDisplayConditionController(mAppContext, this /* manager */)); + mCardControllers.add(new RingerVibrateConditionController(mAppContext, this /* manager */)); + mCardControllers.add(new RingerMutedConditionController(mAppContext, this /* manager */)); + mCardControllers.add(new WorkModeConditionController(mAppContext, this /* manager */)); + mCardControllers.add(new GrayscaleConditionController(mAppContext, this /* manager */)); + } + + /** + * Returns card if controller says it's displayable. Otherwise returns null. + */ + public static class DisplayableChecker implements Callable<ContextualCard> { + + private final ConditionalCardController mController; + + private DisplayableChecker(ConditionalCardController controller) { + mController = controller; + } + + @Override + public ContextualCard call() throws Exception { + return mController.isDisplayable() ? mController.buildContextualCard() : null; + } + } +} diff --git a/src/com/android/settings/homepage/contextualcards/conditional/ConditionalCardController.java b/src/com/android/settings/homepage/contextualcards/conditional/ConditionalCardController.java new file mode 100644 index 0000000000..bc68cce787 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/conditional/ConditionalCardController.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards.conditional; + +import android.content.Context; + +import com.android.settings.homepage.contextualcards.ContextualCard; + +/** + * Data controller for a {@link ConditionalContextualCard}. + */ +public interface ConditionalCardController { + + /** + * A stable ID for this card. + */ + long getId(); + + /** + * Whether or not the card is displayable on the ui. + */ + boolean isDisplayable(); + + /** + * Handler when the card is clicked. + */ + void onPrimaryClick(Context context); + + /** + * Handler when the card action is clicked. + */ + void onActionClick(); + + /** + * Creates a UI model suitable for display, controlled by this controller. + */ + ContextualCard buildContextualCard(); + + void startMonitoringStateChange(); + + void stopMonitoringStateChange(); +} diff --git a/src/com/android/settings/homepage/contextualcards/conditional/ConditionalContextualCard.java b/src/com/android/settings/homepage/contextualcards/conditional/ConditionalContextualCard.java new file mode 100644 index 0000000000..4f92534cc1 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/conditional/ConditionalContextualCard.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards.conditional; + +import androidx.annotation.VisibleForTesting; + +import com.android.settings.homepage.contextualcards.ContextualCard; + +/** + * Data class representing a conditional {@link ContextualCard}. + * + * Use this class to store additional attributes on top of {@link ContextualCard} for + * {@link ConditionalCardController}. + */ +public class ConditionalContextualCard extends ContextualCard { + + @VisibleForTesting + static final double UNSUPPORTED_RANKING_SCORE = -100.0; + + private final long mConditionId; + private final int mMetricsConstant; + private final CharSequence mActionText; + + private ConditionalContextualCard(Builder builder) { + super(builder); + + mConditionId = builder.mConditionId; + mMetricsConstant = builder.mMetricsConstant; + mActionText = builder.mActionText; + } + + @Override + public int getCardType() { + return CardType.CONDITIONAL; + } + + public long getConditionId() { + return mConditionId; + } + + public int getMetricsConstant() { + return mMetricsConstant; + } + + public CharSequence getActionText() { + return mActionText; + } + + public static class Builder extends ContextualCard.Builder { + + private long mConditionId; + private int mMetricsConstant; + private CharSequence mActionText; + + public Builder setConditionId(long id) { + mConditionId = id; + return this; + } + + public Builder setMetricsConstant(int metricsConstant) { + mMetricsConstant = metricsConstant; + return this; + } + + public Builder setActionText(CharSequence actionText) { + mActionText = actionText; + return this; + } + + @Override + public Builder setCardType(int cardType) { + throw new IllegalArgumentException( + "Cannot change card type for " + getClass().getName()); + } + + public ConditionalContextualCard build() { + setRankingScore(UNSUPPORTED_RANKING_SCORE); + return new ConditionalContextualCard(this); + } + } +}
\ No newline at end of file diff --git a/src/com/android/settings/homepage/contextualcards/conditional/DndConditionCardController.java b/src/com/android/settings/homepage/contextualcards/conditional/DndConditionCardController.java new file mode 100644 index 0000000000..5f95de4bfc --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/conditional/DndConditionCardController.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards.conditional; + +import android.app.NotificationManager; +import android.app.settings.SettingsEnums; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.provider.Settings; +import android.service.notification.ZenModeConfig; + +import androidx.annotation.VisibleForTesting; + +import com.android.settings.R; +import com.android.settings.core.SubSettingLauncher; +import com.android.settings.homepage.contextualcards.ContextualCard; +import com.android.settings.notification.ZenModeSettings; + +import java.util.Objects; + + +public class DndConditionCardController implements ConditionalCardController { + static final int ID = Objects.hash("DndConditionCardController"); + + @VisibleForTesting + static final IntentFilter DND_FILTER = + new IntentFilter(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED_INTERNAL); + + private static final String TAG = "DndCondition"; + private final Context mAppContext; + private final ConditionManager mConditionManager; + private final NotificationManager mNotificationManager; + private final Receiver mReceiver; + + public DndConditionCardController(Context appContext, ConditionManager conditionManager) { + mAppContext = appContext; + mConditionManager = conditionManager; + mNotificationManager = mAppContext.getSystemService(NotificationManager.class); + mReceiver = new Receiver(); + } + + @Override + public long getId() { + return ID; + } + + @Override + public boolean isDisplayable() { + return mNotificationManager.getZenMode() != Settings.Global.ZEN_MODE_OFF; + } + + @Override + public void startMonitoringStateChange() { + mAppContext.registerReceiver(mReceiver, DND_FILTER); + } + + @Override + public void stopMonitoringStateChange() { + mAppContext.unregisterReceiver(mReceiver); + } + + @Override + public void onPrimaryClick(Context context) { + new SubSettingLauncher(context) + .setDestination(ZenModeSettings.class.getName()) + .setSourceMetricsCategory(SettingsEnums.SETTINGS_HOMEPAGE) + .setTitleRes(R.string.zen_mode_settings_title) + .launch(); + } + + @Override + public void onActionClick() { + mNotificationManager.setZenMode(Settings.Global.ZEN_MODE_OFF, null, TAG); + } + + @Override + public ContextualCard buildContextualCard() { + return new ConditionalContextualCard.Builder() + .setConditionId(ID) + .setMetricsConstant(SettingsEnums.SETTINGS_CONDITION_DND) + .setActionText(mAppContext.getText(R.string.condition_turn_off)) + .setName(mAppContext.getPackageName() + "/" + + mAppContext.getText(R.string.condition_zen_title)) + .setTitleText(mAppContext.getText(R.string.condition_zen_title).toString()) + .setSummaryText(getSummary()) + .setIconDrawable(mAppContext.getDrawable(R.drawable.ic_do_not_disturb_on_24dp)) + .setViewType(ConditionContextualCardRenderer.VIEW_TYPE_HALF_WIDTH) + .build(); + } + + public class Receiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED_INTERNAL + .equals(intent.getAction())) { + mConditionManager.onConditionChanged(); + } + } + } + + private String getSummary() { + if (ZenModeConfig.areAllZenBehaviorSoundsMuted(mNotificationManager.getZenModeConfig())) { + return mAppContext.getText(R.string.condition_zen_summary_phone_muted).toString(); + } + return mAppContext.getText(R.string.condition_zen_summary_with_exceptions).toString(); + } +} diff --git a/src/com/android/settings/homepage/contextualcards/conditional/GrayscaleConditionController.java b/src/com/android/settings/homepage/contextualcards/conditional/GrayscaleConditionController.java new file mode 100644 index 0000000000..f847612395 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/conditional/GrayscaleConditionController.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2019 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.homepage.contextualcards.conditional; + +import android.Manifest; +import android.app.settings.SettingsEnums; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.hardware.display.ColorDisplayManager; +import android.os.UserHandle; +import android.util.Log; + +import com.android.settings.R; +import com.android.settings.homepage.contextualcards.ContextualCard; + +import java.net.URISyntaxException; +import java.util.Objects; + +public class GrayscaleConditionController implements ConditionalCardController { + static final int ID = Objects.hash("GrayscaleConditionController"); + + private static final String TAG = "GrayscaleCondition"; + private static final String ACTION_GRAYSCALE_CHANGED = + "android.settings.action.GRAYSCALE_CHANGED"; + private static final IntentFilter GRAYSCALE_CHANGED_FILTER = new IntentFilter( + ACTION_GRAYSCALE_CHANGED); + + private final Context mAppContext; + private final ConditionManager mConditionManager; + private final ColorDisplayManager mColorDisplayManager; + private final Receiver mReceiver; + + private Intent mIntent; + + public GrayscaleConditionController(Context appContext, ConditionManager conditionManager) { + mAppContext = appContext; + mConditionManager = conditionManager; + mColorDisplayManager = mAppContext.getSystemService(ColorDisplayManager.class); + mReceiver = new Receiver(); + } + + @Override + public long getId() { + return ID; + } + + @Override + public boolean isDisplayable() { + try { + mIntent = Intent.parseUri( + mAppContext.getString(R.string.config_grayscale_settings_intent), + Intent.URI_INTENT_SCHEME); + } catch (URISyntaxException e) { + Log.w(TAG, "Failure parsing grayscale settings intent, skipping", e); + return false; + } + return mColorDisplayManager.isSaturationActivated(); + } + + @Override + public void onPrimaryClick(Context context) { + mAppContext.startActivity(mIntent); + } + + @Override + public void onActionClick() { + // Turn off grayscale + mColorDisplayManager.setSaturationLevel(100 /* staturationLevel */); + sendBroadcast(); + mConditionManager.onConditionChanged(); + } + + @Override + public ContextualCard buildContextualCard() { + return new ConditionalContextualCard.Builder() + .setConditionId(ID) + .setMetricsConstant(SettingsEnums.SETTINGS_CONDITION_GRAYSCALE_MODE) + .setActionText(mAppContext.getText(R.string.condition_turn_off)) + .setName(mAppContext.getPackageName() + "/" + mAppContext.getText( + R.string.condition_grayscale_title)) + .setTitleText(mAppContext.getText(R.string.condition_grayscale_title).toString()) + .setSummaryText( + mAppContext.getText(R.string.condition_grayscale_summary).toString()) + .setIconDrawable(mAppContext.getDrawable(R.drawable.ic_gray_scale_24dp)) + .setViewType(ConditionContextualCardRenderer.VIEW_TYPE_HALF_WIDTH) + .build(); + } + + @Override + public void startMonitoringStateChange() { + mAppContext.registerReceiver(mReceiver, GRAYSCALE_CHANGED_FILTER, + Manifest.permission.CONTROL_DISPLAY_COLOR_TRANSFORMS, null /* scheduler */); + } + + @Override + public void stopMonitoringStateChange() { + mAppContext.unregisterReceiver(mReceiver); + } + + private void sendBroadcast() { + final Intent intent = new Intent(ACTION_GRAYSCALE_CHANGED); + intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND); + mAppContext.sendBroadcastAsUser(intent, UserHandle.CURRENT, + Manifest.permission.CONTROL_DISPLAY_COLOR_TRANSFORMS); + } + + public class Receiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (ACTION_GRAYSCALE_CHANGED.equals(intent.getAction())) { + mConditionManager.onConditionChanged(); + } + } + } +} diff --git a/src/com/android/settings/homepage/contextualcards/conditional/HotspotConditionController.java b/src/com/android/settings/homepage/contextualcards/conditional/HotspotConditionController.java new file mode 100644 index 0000000000..3557e49d2f --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/conditional/HotspotConditionController.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards.conditional; + +import android.app.settings.SettingsEnums; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiManager; +import android.os.UserHandle; +import android.os.UserManager; + +import com.android.settings.R; +import com.android.settings.TetherSettings; +import com.android.settings.core.SubSettingLauncher; +import com.android.settings.homepage.contextualcards.ContextualCard; +import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; + +import java.util.Objects; + +public class HotspotConditionController implements ConditionalCardController { + static final int ID = Objects.hash("HotspotConditionController"); + + private static final IntentFilter WIFI_AP_STATE_FILTER = + new IntentFilter(WifiManager.WIFI_AP_STATE_CHANGED_ACTION); + + private final Context mAppContext; + private final ConditionManager mConditionManager; + private final WifiManager mWifiManager; + private final Receiver mReceiver; + + + public HotspotConditionController(Context appContext, ConditionManager conditionManager) { + mAppContext = appContext; + mConditionManager = conditionManager; + mWifiManager = appContext.getSystemService(WifiManager.class); + mReceiver = new Receiver(); + } + + @Override + public long getId() { + return ID; + } + + @Override + public boolean isDisplayable() { + return mWifiManager.isWifiApEnabled(); + } + + @Override + public void onPrimaryClick(Context context) { + new SubSettingLauncher(context) + .setDestination(TetherSettings.class.getName()) + .setSourceMetricsCategory(SettingsEnums.DASHBOARD_SUMMARY) + .setTitleRes(R.string.tether_settings_title_all) + .launch(); + } + + @Override + public void onActionClick() { + final RestrictedLockUtils.EnforcedAdmin admin = + RestrictedLockUtilsInternal.checkIfRestrictionEnforced( + mAppContext, UserManager.DISALLOW_CONFIG_TETHERING, UserHandle.myUserId()); + if (admin != null) { + RestrictedLockUtils.sendShowAdminSupportDetailsIntent(mAppContext, admin); + } else { + ConnectivityManager cm = (ConnectivityManager) mAppContext.getSystemService( + Context.CONNECTIVITY_SERVICE); + cm.stopTethering(ConnectivityManager.TETHERING_WIFI); + } + } + + @Override + public ContextualCard buildContextualCard() { + return new ConditionalContextualCard.Builder() + .setConditionId(ID) + .setMetricsConstant(SettingsEnums.SETTINGS_CONDITION_HOTSPOT) + .setActionText(mAppContext.getText(R.string.condition_turn_off)) + .setName(mAppContext.getPackageName() + "/" + + mAppContext.getText(R.string.condition_hotspot_title)) + .setTitleText(mAppContext.getText(R.string.condition_hotspot_title).toString()) + .setSummaryText(getSsid().toString()) + .setIconDrawable(mAppContext.getDrawable(R.drawable.ic_hotspot)) + .setViewType(ConditionContextualCardRenderer.VIEW_TYPE_HALF_WIDTH) + .build(); + } + + @Override + public void startMonitoringStateChange() { + mAppContext.registerReceiver(mReceiver, WIFI_AP_STATE_FILTER); + } + + @Override + public void stopMonitoringStateChange() { + mAppContext.unregisterReceiver(mReceiver); + } + + private CharSequence getSsid() { + WifiConfiguration wifiConfig = mWifiManager.getWifiApConfiguration(); + if (wifiConfig == null) { + return mAppContext.getText( + com.android.internal.R.string.wifi_tether_configure_ssid_default); + } else { + return wifiConfig.SSID; + } + } + + public class Receiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (WifiManager.WIFI_AP_STATE_CHANGED_ACTION.equals(intent.getAction())) { + mConditionManager.onConditionChanged(); + } + } + } +} diff --git a/src/com/android/settings/homepage/contextualcards/conditional/NightDisplayConditionController.java b/src/com/android/settings/homepage/contextualcards/conditional/NightDisplayConditionController.java new file mode 100644 index 0000000000..82bec0b4e4 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/conditional/NightDisplayConditionController.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards.conditional; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.hardware.display.ColorDisplayManager; +import android.hardware.display.NightDisplayListener; + +import com.android.settings.R; +import com.android.settings.core.SubSettingLauncher; +import com.android.settings.display.NightDisplaySettings; +import com.android.settings.homepage.contextualcards.ContextualCard; + +import java.util.Objects; + +public class NightDisplayConditionController implements ConditionalCardController, + NightDisplayListener.Callback { + + static final int ID = Objects.hash("NightDisplayConditionController"); + + private final Context mAppContext; + private final ConditionManager mConditionManager; + private final ColorDisplayManager mColorDisplayManager; + private final NightDisplayListener mNightDisplayListener; + + public NightDisplayConditionController(Context appContext, ConditionManager manager) { + mAppContext = appContext; + mConditionManager = manager; + mColorDisplayManager = appContext.getSystemService(ColorDisplayManager.class); + mNightDisplayListener = new NightDisplayListener(appContext); + } + + @Override + public long getId() { + return ID; + } + + @Override + public boolean isDisplayable() { + return mColorDisplayManager.isNightDisplayActivated(); + } + + @Override + public void onPrimaryClick(Context context) { + new SubSettingLauncher(context) + .setDestination(NightDisplaySettings.class.getName()) + .setSourceMetricsCategory(SettingsEnums.SETTINGS_HOMEPAGE) + .setTitleRes(R.string.night_display_title) + .launch(); + } + + @Override + public void onActionClick() { + mColorDisplayManager.setNightDisplayActivated(false); + } + + @Override + public ContextualCard buildContextualCard() { + return new ConditionalContextualCard.Builder() + .setConditionId(ID) + .setMetricsConstant(SettingsEnums.SETTINGS_CONDITION_NIGHT_DISPLAY) + .setActionText(mAppContext.getText(R.string.condition_turn_off)) + .setName(mAppContext.getPackageName() + "/" + + mAppContext.getText(R.string.condition_night_display_title)) + .setTitleText(mAppContext.getText( + R.string.condition_night_display_title).toString()) + .setSummaryText( + mAppContext.getText(R.string.condition_night_display_summary).toString()) + .setIconDrawable(mAppContext.getDrawable(R.drawable.ic_settings_night_display)) + .setViewType(ConditionContextualCardRenderer.VIEW_TYPE_HALF_WIDTH) + .build(); + } + + @Override + public void startMonitoringStateChange() { + mNightDisplayListener.setCallback(this); + } + + @Override + public void stopMonitoringStateChange() { + mNightDisplayListener.setCallback(null); + } + + @Override + public void onActivated(boolean activated) { + mConditionManager.onConditionChanged(); + } +} diff --git a/src/com/android/settings/homepage/contextualcards/conditional/RingerMutedConditionController.java b/src/com/android/settings/homepage/contextualcards/conditional/RingerMutedConditionController.java new file mode 100644 index 0000000000..f800f9200a --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/conditional/RingerMutedConditionController.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards.conditional; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.media.AudioManager; + +import com.android.settings.R; +import com.android.settings.homepage.contextualcards.ContextualCard; + +import java.util.Objects; + +public class RingerMutedConditionController extends AbnormalRingerConditionController { + static final int ID = Objects.hash("RingerMutedConditionController"); + + private final Context mAppContext; + + public RingerMutedConditionController(Context appContext, ConditionManager conditionManager) { + super(appContext, conditionManager); + mAppContext = appContext; + } + + @Override + public long getId() { + return ID; + } + + @Override + public boolean isDisplayable() { + return mAudioManager.getRingerModeInternal() == AudioManager.RINGER_MODE_SILENT; + } + + @Override + public ContextualCard buildContextualCard() { + return new ConditionalContextualCard.Builder() + .setConditionId(ID) + .setMetricsConstant(SettingsEnums.SETTINGS_CONDITION_DEVICE_MUTED) + .setActionText( + mAppContext.getText(R.string.condition_device_muted_action_turn_on_sound)) + .setName(mAppContext.getPackageName() + "/" + + mAppContext.getText(R.string.condition_device_muted_title)) + .setTitleText(mAppContext.getText(R.string.condition_device_muted_title).toString()) + .setSummaryText( + mAppContext.getText(R.string.condition_device_muted_summary).toString()) + .setIconDrawable(mAppContext.getDrawable(R.drawable.ic_notifications_off_24dp)) + .setViewType(ConditionContextualCardRenderer.VIEW_TYPE_HALF_WIDTH) + .build(); + } +} diff --git a/src/com/android/settings/homepage/contextualcards/conditional/RingerVibrateConditionController.java b/src/com/android/settings/homepage/contextualcards/conditional/RingerVibrateConditionController.java new file mode 100644 index 0000000000..c19311669d --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/conditional/RingerVibrateConditionController.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards.conditional; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.media.AudioManager; + +import com.android.settings.R; +import com.android.settings.homepage.contextualcards.ContextualCard; + +import java.util.Objects; + +public class RingerVibrateConditionController extends AbnormalRingerConditionController { + static final int ID = Objects.hash("RingerVibrateConditionController"); + + private final Context mAppContext; + + public RingerVibrateConditionController(Context appContext, ConditionManager conditionManager) { + super(appContext, conditionManager); + mAppContext = appContext; + + } + + @Override + public long getId() { + return ID; + } + + @Override + public boolean isDisplayable() { + return mAudioManager.getRingerModeInternal() == AudioManager.RINGER_MODE_VIBRATE; + } + + @Override + public ContextualCard buildContextualCard() { + return new ConditionalContextualCard.Builder() + .setConditionId(ID) + .setMetricsConstant(SettingsEnums.SETTINGS_CONDITION_DEVICE_VIBRATE) + .setActionText( + mAppContext.getText(R.string.condition_device_muted_action_turn_on_sound)) + .setName(mAppContext.getPackageName() + "/" + + mAppContext.getText(R.string.condition_device_vibrate_title)) + .setTitleText( + mAppContext.getText(R.string.condition_device_vibrate_title).toString()) + .setSummaryText( + mAppContext.getText(R.string.condition_device_vibrate_summary).toString()) + .setIconDrawable(mAppContext.getDrawable(R.drawable.ic_volume_ringer_vibrate)) + .setViewType(ConditionContextualCardRenderer.VIEW_TYPE_HALF_WIDTH) + .build(); + } +} diff --git a/src/com/android/settings/homepage/contextualcards/conditional/WorkModeConditionController.java b/src/com/android/settings/homepage/contextualcards/conditional/WorkModeConditionController.java new file mode 100644 index 0000000000..4e901fd191 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/conditional/WorkModeConditionController.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards.conditional; + +import android.app.settings.SettingsEnums; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.UserInfo; +import android.os.UserHandle; +import android.os.UserManager; +import android.text.TextUtils; + +import com.android.settings.R; +import com.android.settings.Settings; +import com.android.settings.homepage.contextualcards.ContextualCard; + +import java.util.List; +import java.util.Objects; + +public class WorkModeConditionController implements ConditionalCardController { + + static final int ID = Objects.hash("WorkModeConditionController"); + + private static final IntentFilter FILTER = new IntentFilter(); + + static { + FILTER.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE); + FILTER.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE); + } + + private final Context mAppContext; + private final UserManager mUm; + private final ConditionManager mConditionManager; + private final Receiver mReceiver; + + private UserHandle mUserHandle; + + public WorkModeConditionController(Context appContext, ConditionManager manager) { + mAppContext = appContext; + mUm = mAppContext.getSystemService(UserManager.class); + mConditionManager = manager; + mReceiver = new Receiver(); + } + + @Override + public long getId() { + return ID; + } + + @Override + public boolean isDisplayable() { + updateUserHandle(); + return mUserHandle != null && mUm.isQuietModeEnabled(mUserHandle); + } + + @Override + public void onPrimaryClick(Context context) { + context.startActivity(new Intent(context, + Settings.AccountDashboardActivity.class)); + } + + @Override + public void onActionClick() { + if (mUserHandle != null) { + mUm.requestQuietModeEnabled(false, mUserHandle); + } + } + + @Override + public ContextualCard buildContextualCard() { + return new ConditionalContextualCard.Builder() + .setConditionId(ID) + .setMetricsConstant(SettingsEnums.SETTINGS_CONDITION_WORK_MODE) + .setActionText(mAppContext.getText(R.string.condition_turn_on)) + .setName(mAppContext.getPackageName() + "/" + + mAppContext.getText(R.string.condition_work_title)) + .setTitleText(mAppContext.getText(R.string.condition_work_title).toString()) + .setSummaryText(mAppContext.getText(R.string.condition_work_summary).toString()) + .setIconDrawable(mAppContext.getDrawable(R.drawable.ic_signal_workmode_enable)) + .setViewType(ConditionContextualCardRenderer.VIEW_TYPE_HALF_WIDTH) + .build(); + } + + @Override + public void startMonitoringStateChange() { + mAppContext.registerReceiver(mReceiver, FILTER); + } + + @Override + public void stopMonitoringStateChange() { + mAppContext.unregisterReceiver(mReceiver); + } + + private void updateUserHandle() { + List<UserInfo> profiles = mUm.getProfiles(UserHandle.myUserId()); + final int profilesCount = profiles.size(); + mUserHandle = null; + for (int i = 0; i < profilesCount; i++) { + UserInfo userInfo = profiles.get(i); + if (userInfo.isManagedProfile()) { + // We assume there's only one managed profile, otherwise UI needs to change. + mUserHandle = userInfo.getUserHandle(); + break; + } + } + } + + public class Receiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (TextUtils.equals(action, Intent.ACTION_MANAGED_PROFILE_AVAILABLE) + || TextUtils.equals(action, Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)) { + mConditionManager.onConditionChanged(); + } + } + } +} diff --git a/src/com/android/settings/homepage/contextualcards/deviceinfo/DataUsageSlice.java b/src/com/android/settings/homepage/contextualcards/deviceinfo/DataUsageSlice.java new file mode 100644 index 0000000000..419b770867 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/deviceinfo/DataUsageSlice.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards.deviceinfo; + +import android.app.PendingIntent; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.format.Formatter; +import android.text.style.TextAppearanceSpan; + +import androidx.core.graphics.drawable.IconCompat; +import androidx.slice.Slice; +import androidx.slice.builders.ListBuilder; +import androidx.slice.builders.SliceAction; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.settings.R; +import com.android.settings.SubSettings; +import com.android.settings.Utils; +import com.android.settings.datausage.DataUsageSummary; +import com.android.settings.datausage.DataUsageUtils; +import com.android.settings.slices.CustomSliceRegistry; +import com.android.settings.slices.CustomSliceable; +import com.android.settings.slices.SliceBuilderUtils; +import com.android.settingslib.net.DataUsageController; + +import java.util.concurrent.TimeUnit; + +public class DataUsageSlice implements CustomSliceable { + private static final String TAG = "DataUsageSlice"; + private static final long MILLIS_IN_A_DAY = TimeUnit.DAYS.toMillis(1); + + + private final Context mContext; + + public DataUsageSlice(Context context) { + mContext = context; + } + + @Override + public Uri getUri() { + return CustomSliceRegistry.DATA_USAGE_SLICE_URI; + } + + @Override + public Slice getSlice() { + final IconCompat icon = IconCompat.createWithResource(mContext, + R.drawable.ic_settings_data_usage); + final String title = mContext.getString(R.string.data_usage_summary_title); + final SliceAction primaryAction = SliceAction.createDeeplink(getPrimaryAction(), icon, + ListBuilder.ICON_IMAGE, title); + final DataUsageController dataUsageController = new DataUsageController(mContext); + final DataUsageController.DataUsageInfo info = dataUsageController.getDataUsageInfo(); + final ListBuilder listBuilder = + new ListBuilder(mContext, CustomSliceRegistry.DATA_USAGE_SLICE_URI, + ListBuilder.INFINITY) + .setAccentColor(Utils.getColorAccentDefaultColor(mContext)) + .setHeader(new ListBuilder.HeaderBuilder().setTitle(title)); + if (DataUsageUtils.hasSim(mContext)) { + listBuilder.addRow(new ListBuilder.RowBuilder() + .setTitle(getDataUsageText(info)) + .setSubtitle(getCycleTime(info)) + .setPrimaryAction(primaryAction)); + } else { + listBuilder.addRow(new ListBuilder.RowBuilder() + .setTitle(mContext.getText(R.string.no_sim_card)) + .setPrimaryAction(primaryAction)); + } + return listBuilder.build(); + } + + @Override + public Intent getIntent() { + final String screenTitle = mContext.getText(R.string.data_usage_wifi_title).toString(); + return SliceBuilderUtils.buildSearchResultPageIntent(mContext, + DataUsageSummary.class.getName(), "" /* key */, screenTitle, + SettingsEnums.SLICE) + .setClassName(mContext.getPackageName(), SubSettings.class.getName()) + .setData(CustomSliceRegistry.DATA_USAGE_SLICE_URI); + } + + private PendingIntent getPrimaryAction() { + final Intent intent = getIntent(); + return PendingIntent.getActivity(mContext, 0 /* requestCode */, intent, 0 /* flags */); + } + + @VisibleForTesting + CharSequence getDataUsageText(DataUsageController.DataUsageInfo info) { + final Formatter.BytesResult usedResult = Formatter.formatBytes(mContext.getResources(), + info.usageLevel, Formatter.FLAG_CALCULATE_ROUNDED | Formatter.FLAG_IEC_UNITS); + final SpannableString usageNumberText = new SpannableString(usedResult.value); + usageNumberText.setSpan( + new TextAppearanceSpan(mContext, android.R.style.TextAppearance_Large), 0, + usageNumberText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + return TextUtils.expandTemplate(mContext.getText(R.string.data_used_formatted), + usageNumberText, usedResult.units); + } + + @VisibleForTesting + CharSequence getCycleTime(DataUsageController.DataUsageInfo info) { + final long millisLeft = info.cycleEnd - System.currentTimeMillis(); + if (millisLeft <= 0) { + return mContext.getString(R.string.billing_cycle_none_left); + } else { + final int daysLeft = (int) (millisLeft / MILLIS_IN_A_DAY); + return daysLeft < 1 ? mContext.getString(R.string.billing_cycle_less_than_one_day_left) + : mContext.getResources().getQuantityString(R.plurals.billing_cycle_days_left, + daysLeft, daysLeft); + } + } + + @Override + public void onNotifyChange(Intent intent) { + + } +} diff --git a/src/com/android/settings/homepage/contextualcards/deviceinfo/DeviceInfoSlice.java b/src/com/android/settings/homepage/contextualcards/deviceinfo/DeviceInfoSlice.java new file mode 100644 index 0000000000..48a9aa5461 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/deviceinfo/DeviceInfoSlice.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards.deviceinfo; + +import android.app.PendingIntent; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.text.BidiFormatter; +import android.text.TextDirectionHeuristics; +import android.text.TextUtils; + +import androidx.annotation.VisibleForTesting; +import androidx.core.graphics.drawable.IconCompat; +import androidx.slice.Slice; +import androidx.slice.builders.ListBuilder; +import androidx.slice.builders.SliceAction; + +import com.android.settings.R; +import com.android.settings.SubSettings; +import com.android.settings.Utils; +import com.android.settings.deviceinfo.HardwareInfoPreferenceController; +import com.android.settings.deviceinfo.aboutphone.MyDeviceInfoFragment; +import com.android.settings.slices.CustomSliceRegistry; +import com.android.settings.slices.CustomSliceable; +import com.android.settings.slices.SliceBuilderUtils; +import com.android.settingslib.DeviceInfoUtils; + +import java.util.List; + +public class DeviceInfoSlice implements CustomSliceable { + private static final String TAG = "DeviceInfoSlice"; + + private final Context mContext; + private final SubscriptionManager mSubscriptionManager; + + public DeviceInfoSlice(Context context) { + mContext = context; + mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class); + } + + @Override + public Slice getSlice() { + final IconCompat icon = IconCompat.createWithResource(mContext, + R.drawable.ic_info_outline_24dp); + final String title = mContext.getString(R.string.device_info_label); + final SliceAction primaryAction = SliceAction.createDeeplink(getPrimaryAction(), icon, + ListBuilder.ICON_IMAGE, title); + return new ListBuilder(mContext, CustomSliceRegistry.DEVICE_INFO_SLICE_URI, + ListBuilder.INFINITY) + .setAccentColor((Utils.getColorAccentDefaultColor(mContext))) + .setHeader(new ListBuilder.HeaderBuilder().setTitle(title)) + .addRow(new ListBuilder.RowBuilder() + .setTitle(getPhoneNumber()) + .setSubtitle(getDeviceModel()) + .setPrimaryAction(primaryAction)) + .build(); + } + + @Override + public Uri getUri() { + return CustomSliceRegistry.DEVICE_INFO_SLICE_URI; + } + + @Override + public Intent getIntent() { + final String screenTitle = mContext.getText(R.string.device_info_label).toString(); + return SliceBuilderUtils.buildSearchResultPageIntent(mContext, + MyDeviceInfoFragment.class.getName(), "" /* key */, screenTitle, + SettingsEnums.SLICE) + .setClassName(mContext.getPackageName(), SubSettings.class.getName()) + .setData(CustomSliceRegistry.DEVICE_INFO_SLICE_URI); + } + + private PendingIntent getPrimaryAction() { + final Intent intent = getIntent(); + return PendingIntent.getActivity(mContext, 0 /* requestCode */, intent, 0 /* flags */); + } + + @VisibleForTesting + CharSequence getPhoneNumber() { + final SubscriptionInfo subscriptionInfo = getFirstSubscriptionInfo(); + if (subscriptionInfo == null) { + return mContext.getString(R.string.device_info_default); + } + final String phoneNumber = DeviceInfoUtils.getFormattedPhoneNumber(mContext, + subscriptionInfo); + return TextUtils.isEmpty(phoneNumber) ? mContext.getString(R.string.device_info_default) + : BidiFormatter.getInstance().unicodeWrap(phoneNumber, TextDirectionHeuristics.LTR); + } + + private CharSequence getDeviceModel() { + return HardwareInfoPreferenceController.getDeviceModel(); + } + + @VisibleForTesting + SubscriptionInfo getFirstSubscriptionInfo() { + final List<SubscriptionInfo> subscriptionInfoList = + mSubscriptionManager.getActiveSubscriptionInfoList(true); + if (subscriptionInfoList == null || subscriptionInfoList.isEmpty()) { + return null; + } + return subscriptionInfoList.get(0); + } + + @Override + public void onNotifyChange(Intent intent) { + + } +} diff --git a/src/com/android/settings/homepage/contextualcards/deviceinfo/EmergencyInfoSlice.java b/src/com/android/settings/homepage/contextualcards/deviceinfo/EmergencyInfoSlice.java new file mode 100644 index 0000000000..10e87ff836 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/deviceinfo/EmergencyInfoSlice.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards.deviceinfo; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; + +import androidx.core.graphics.drawable.IconCompat; +import androidx.slice.Slice; +import androidx.slice.builders.ListBuilder; +import androidx.slice.builders.SliceAction; + +import com.android.settings.R; +import com.android.settings.accounts.EmergencyInfoPreferenceController; +import com.android.settings.slices.CustomSliceRegistry; +import com.android.settings.slices.CustomSliceable; + +// This is a slice helper class for EmergencyInfo +public class EmergencyInfoSlice implements CustomSliceable { + + private final Context mContext; + + public EmergencyInfoSlice(Context context) { + mContext = context; + } + + @Override + public Slice getSlice() { + final ListBuilder listBuilder = new ListBuilder(mContext, + CustomSliceRegistry.EMERGENCY_INFO_SLICE_URI, + ListBuilder.INFINITY); + listBuilder.addRow( + new ListBuilder.RowBuilder() + .setTitle(mContext.getText(R.string.emergency_info_title)) + .setSubtitle( + mContext.getText(R.string.emergency_info_contextual_card_summary)) + .setPrimaryAction(createPrimaryAction())); + return listBuilder.build(); + } + + @Override + public Uri getUri() { + return CustomSliceRegistry.EMERGENCY_INFO_SLICE_URI; + } + + @Override + public Intent getIntent() { + return new Intent(EmergencyInfoPreferenceController.getIntentAction(mContext)); + } + + @Override + public void onNotifyChange(Intent intent) { + } + + private SliceAction createPrimaryAction() { + final PendingIntent pendingIntent = + PendingIntent.getActivity( + mContext, + 0 /* requestCode */, + getIntent(), + PendingIntent.FLAG_UPDATE_CURRENT); + + return SliceAction.createDeeplink( + pendingIntent, + IconCompat.createWithResource(mContext, R.drawable.empty_icon), + ListBuilder.ICON_IMAGE, + mContext.getText(R.string.emergency_info_title)); + } +} diff --git a/src/com/android/settings/homepage/contextualcards/deviceinfo/StorageSlice.java b/src/com/android/settings/homepage/contextualcards/deviceinfo/StorageSlice.java new file mode 100644 index 0000000000..d03b3593bd --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/deviceinfo/StorageSlice.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards.deviceinfo; + +import android.app.PendingIntent; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.storage.StorageManager; +import android.text.format.Formatter; + +import androidx.core.graphics.drawable.IconCompat; +import androidx.slice.Slice; +import androidx.slice.builders.ListBuilder; +import androidx.slice.builders.SliceAction; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.settings.R; +import com.android.settings.SubSettings; +import com.android.settings.Utils; +import com.android.settings.deviceinfo.StorageDashboardFragment; +import com.android.settings.deviceinfo.storage.StorageSummaryDonutPreferenceController; +import com.android.settings.slices.CustomSliceRegistry; +import com.android.settings.slices.CustomSliceable; +import com.android.settings.slices.SliceBuilderUtils; +import com.android.settingslib.deviceinfo.PrivateStorageInfo; +import com.android.settingslib.deviceinfo.StorageManagerVolumeProvider; + +public class StorageSlice implements CustomSliceable { + private static final String TAG = "StorageSlice"; + + private final Context mContext; + + public StorageSlice(Context context) { + mContext = context; + } + + @Override + public Uri getUri() { + return CustomSliceRegistry.STORAGE_SLICE_URI; + } + + @Override + public Slice getSlice() { + final IconCompat icon = IconCompat.createWithResource(mContext, + R.drawable.ic_homepage_storage); + final String title = mContext.getString(R.string.storage_label); + final SliceAction primaryAction = SliceAction.createDeeplink(getPrimaryAction(), icon, + ListBuilder.ICON_IMAGE, title); + final PrivateStorageInfo info = getPrivateStorageInfo(); + return new ListBuilder(mContext, CustomSliceRegistry.STORAGE_SLICE_URI, + ListBuilder.INFINITY) + .setAccentColor(Utils.getColorAccentDefaultColor(mContext)) + .setHeader(new ListBuilder.HeaderBuilder().setTitle(title)) + .addRow(new ListBuilder.RowBuilder() + .setTitle(getStorageUsedText(info)) + .setSubtitle(getStorageSummaryText(info)) + .setPrimaryAction(primaryAction)) + .build(); + } + + @Override + public Intent getIntent() { + final String screenTitle = mContext.getText(R.string.storage_label).toString(); + return SliceBuilderUtils.buildSearchResultPageIntent(mContext, + StorageDashboardFragment.class.getName(), "" /* key */, screenTitle, + SettingsEnums.SLICE) + .setClassName(mContext.getPackageName(), SubSettings.class.getName()) + .setData(CustomSliceRegistry.STORAGE_SLICE_URI); + } + + private PendingIntent getPrimaryAction() { + final Intent intent = getIntent(); + return PendingIntent.getActivity(mContext, 0 /* requestCode */, intent, 0 /* flags */); + } + + @VisibleForTesting + PrivateStorageInfo getPrivateStorageInfo() { + final StorageManager storageManager = mContext.getSystemService(StorageManager.class); + final StorageManagerVolumeProvider smvp = new StorageManagerVolumeProvider(storageManager); + return PrivateStorageInfo.getPrivateStorageInfo(smvp); + } + + @VisibleForTesting + CharSequence getStorageUsedText(PrivateStorageInfo info) { + final long usedBytes = info.totalBytes - info.freeBytes; + return StorageSummaryDonutPreferenceController.convertUsedBytesToFormattedText(mContext, + usedBytes); + } + + @VisibleForTesting + CharSequence getStorageSummaryText(PrivateStorageInfo info) { + return mContext.getString(R.string.storage_volume_total, + Formatter.formatShortFileSize(mContext, info.totalBytes)); + } + + @Override + public void onNotifyChange(Intent intent) { + + } +} diff --git a/src/com/android/settings/homepage/contextualcards/legacysuggestion/LegacySuggestionContextualCard.java b/src/com/android/settings/homepage/contextualcards/legacysuggestion/LegacySuggestionContextualCard.java new file mode 100644 index 0000000000..d11f77120e --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/legacysuggestion/LegacySuggestionContextualCard.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards.legacysuggestion; + +import android.app.PendingIntent; + +import com.android.settings.homepage.contextualcards.ContextualCard; + +public class LegacySuggestionContextualCard extends ContextualCard { + + private final PendingIntent mPendingIntent; + + public LegacySuggestionContextualCard(Builder builder) { + super(builder); + mPendingIntent = builder.mPendingIntent; + } + + @Override + public int getCardType() { + return CardType.LEGACY_SUGGESTION; + } + + public PendingIntent getPendingIntent() { + return mPendingIntent; + } + + public static class Builder extends ContextualCard.Builder { + + private PendingIntent mPendingIntent; + + public Builder setPendingIntent(PendingIntent pendingIntent) { + mPendingIntent = pendingIntent; + return this; + } + + @Override + public Builder setCardType(int cardType) { + throw new IllegalArgumentException( + "Cannot change card type for " + getClass().getName()); + } + + public LegacySuggestionContextualCard build() { + return new LegacySuggestionContextualCard(this); + } + } + +} diff --git a/src/com/android/settings/homepage/contextualcards/legacysuggestion/LegacySuggestionContextualCardController.java b/src/com/android/settings/homepage/contextualcards/legacysuggestion/LegacySuggestionContextualCardController.java new file mode 100644 index 0000000000..3b0b46d2f2 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/legacysuggestion/LegacySuggestionContextualCardController.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards.legacysuggestion; + +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.service.settings.suggestions.Suggestion; +import android.util.ArrayMap; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; + +import com.android.settings.R; +import com.android.settings.homepage.contextualcards.ContextualCard; +import com.android.settings.homepage.contextualcards.ContextualCardController; +import com.android.settings.homepage.contextualcards.ContextualCardUpdateListener; +import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.core.lifecycle.events.OnStop; +import com.android.settingslib.suggestions.SuggestionController; +import com.android.settingslib.suggestions.SuggestionController.ServiceConnectionListener; +import com.android.settingslib.utils.ThreadUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class LegacySuggestionContextualCardController implements ContextualCardController, + LifecycleObserver, OnStart, OnStop, ServiceConnectionListener { + + private static final String TAG = "LegacySuggestCardCtrl"; + + @VisibleForTesting + SuggestionController mSuggestionController; + + private ContextualCardUpdateListener mCardUpdateListener; + private final Context mContext; + + + public LegacySuggestionContextualCardController(Context context) { + mContext = context; + if (!mContext.getResources().getBoolean(R.bool.config_use_legacy_suggestion)) { + Log.w(TAG, "Legacy suggestion contextual card disabled, skipping."); + return; + } + final ComponentName suggestionServiceComponent = + FeatureFactory.getFactory(mContext).getSuggestionFeatureProvider(mContext) + .getSuggestionServiceComponent(); + mSuggestionController = new SuggestionController( + mContext, suggestionServiceComponent, this /* listener */); + + } + + @Override + public int getCardType() { + return ContextualCard.CardType.LEGACY_SUGGESTION; + } + + @Override + public void onPrimaryClick(ContextualCard card) { + try { + ((LegacySuggestionContextualCard) card).getPendingIntent().send(); + } catch (PendingIntent.CanceledException e) { + Log.w(TAG, "Failed to start suggestion " + card.getTitleText()); + } + } + + @Override + public void onActionClick(ContextualCard card) { + + } + + @Override + public void onDismissed(ContextualCard card) { + + } + + @Override + public void setCardUpdateListener(ContextualCardUpdateListener listener) { + mCardUpdateListener = listener; + } + + @Override + public void onStart() { + if (mSuggestionController == null) { + return; + } + mSuggestionController.start(); + } + + @Override + public void onStop() { + if (mSuggestionController == null) { + return; + } + mSuggestionController.stop(); + } + + @Override + public void onServiceConnected() { + loadSuggestions(); + } + + @Override + public void onServiceDisconnected() { + + } + + private void loadSuggestions() { + ThreadUtils.postOnBackgroundThread(() -> { + if (mSuggestionController == null || mCardUpdateListener == null) { + return; + } + final List<Suggestion> suggestions = mSuggestionController.getSuggestions(); + final String suggestionCount = suggestions == null + ? "null" + : String.valueOf(suggestions.size()); + Log.d(TAG, "Loaded suggests: " + suggestionCount); + + final List<ContextualCard> cards = new ArrayList<>(); + if (suggestions != null) { + // Convert suggestion to ContextualCard + for (Suggestion suggestion : suggestions) { + final LegacySuggestionContextualCard.Builder cardBuilder = + new LegacySuggestionContextualCard.Builder(); + if (suggestion.getIcon() != null) { + cardBuilder.setIconDrawable(suggestion.getIcon().loadDrawable(mContext)); + } + cardBuilder + .setPendingIntent(suggestion.getPendingIntent()) + .setName(suggestion.getId()) + .setTitleText(suggestion.getTitle().toString()) + .setSummaryText(suggestion.getSummary().toString()) + .setViewType(LegacySuggestionContextualCardRenderer.VIEW_TYPE); + + cards.add(cardBuilder.build()); + } + } + + // Update adapter + final Map<Integer, List<ContextualCard>> suggestionCards = new ArrayMap<>(); + suggestionCards.put(ContextualCard.CardType.LEGACY_SUGGESTION, cards); + ThreadUtils.postOnMainThread( + () -> mCardUpdateListener.onContextualCardUpdated(suggestionCards)); + + }); + } +} diff --git a/src/com/android/settings/homepage/contextualcards/legacysuggestion/LegacySuggestionContextualCardRenderer.java b/src/com/android/settings/homepage/contextualcards/legacysuggestion/LegacySuggestionContextualCardRenderer.java new file mode 100644 index 0000000000..3bccabca4d --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/legacysuggestion/LegacySuggestionContextualCardRenderer.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards.legacysuggestion; + +import android.content.Context; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.LayoutRes; +import androidx.recyclerview.widget.RecyclerView; + +import com.android.settings.R; +import com.android.settings.homepage.contextualcards.ContextualCard; +import com.android.settings.homepage.contextualcards.ContextualCardRenderer; +import com.android.settings.homepage.contextualcards.ControllerRendererPool; + +public class LegacySuggestionContextualCardRenderer implements ContextualCardRenderer { + + @LayoutRes + public static final int VIEW_TYPE = R.layout.legacy_suggestion_tile; + + private final Context mContext; + private final ControllerRendererPool mControllerRendererPool; + + public LegacySuggestionContextualCardRenderer(Context context, + ControllerRendererPool controllerRendererPool) { + mContext = context; + mControllerRendererPool = controllerRendererPool; + } + + @Override + public RecyclerView.ViewHolder createViewHolder(View view, @LayoutRes int viewType) { + return new LegacySuggestionViewHolder(view); + } + + @Override + public void bindView(RecyclerView.ViewHolder holder, ContextualCard card) { + final LegacySuggestionViewHolder vh = (LegacySuggestionViewHolder) holder; + vh.icon.setImageDrawable(card.getIconDrawable()); + vh.title.setText(card.getTitleText()); + vh.summary.setText(card.getSummaryText()); + vh.itemView.setOnClickListener(v -> + mControllerRendererPool.getController(mContext, + card.getCardType()).onPrimaryClick(card)); + } + + private static class LegacySuggestionViewHolder extends RecyclerView.ViewHolder { + + public final ImageView icon; + public final TextView title; + public final TextView summary; + + public LegacySuggestionViewHolder(View itemView) { + super(itemView); + icon = itemView.findViewById(android.R.id.icon); + title = itemView.findViewById(android.R.id.title); + summary = itemView.findViewById(android.R.id.summary); + } + } +} + diff --git a/src/com/android/settings/homepage/contextualcards/logging/ContextualCardLogUtils.java b/src/com/android/settings/homepage/contextualcards/logging/ContextualCardLogUtils.java new file mode 100644 index 0000000000..585eca3db6 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/logging/ContextualCardLogUtils.java @@ -0,0 +1,263 @@ +/* + * Copyright (C) 2019 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.homepage.contextualcards.logging; + +import android.util.Log; + +import androidx.slice.widget.EventInfo; + +import com.android.settings.homepage.contextualcards.ContextualCard; + +import java.util.ArrayList; +import java.util.List; + +/** + * Utils of building contextual card to string, and parse string back to {@link CardLog} + */ +public class ContextualCardLogUtils { + + private static final String TAG = "ContextualCardLogUtils"; + + private static final class TapTarget { + static int TARGET_DEFAULT = 0; + static int TARGET_TITLE = 1; + static int TARGET_TOGGLE = 2; + static int TARGET_SLIDER = 3; + } + + /** + * Log data for a general contextual card event + */ + public static class CardLog { + private final String mSliceUri; + private final double mRankingScore; + + public CardLog(Builder builder) { + mSliceUri = builder.mSliceUri; + mRankingScore = builder.mRankingScore; + } + + public String getSliceUri() { + return mSliceUri; + } + + public double getRankingScore() { + return mRankingScore; + } + + public static class Builder { + private String mSliceUri; + private double mRankingScore; + + public Builder setSliceUri(String sliceUri) { + mSliceUri = sliceUri; + return this; + } + + public Builder setRankingScore(double rankingScore) { + mRankingScore = rankingScore; + return this; + } + public CardLog build() { + return new CardLog(this); + } + } + } + + /** + * Log data for a contextual card click event + */ + public static class CardClickLog extends CardLog { + private final int mSliceRow; + private final int mSliceTapTarget; + private final int mUiPosition; + + public CardClickLog(Builder builder) { + super(builder); + mSliceRow = builder.mSliceRow; + mSliceTapTarget = builder.mSliceTapTarget; + mUiPosition = builder.mUiPosition; + } + + public int getSliceRow() { + return mSliceRow; + } + + public int getSliceTapTarget() { + return mSliceTapTarget; + } + + public int getUiPosition() { + return mUiPosition; + } + + public static class Builder extends CardLog.Builder { + private int mSliceRow; + private int mSliceTapTarget; + private int mUiPosition; + + public Builder setSliceRow(int sliceRow) { + mSliceRow = sliceRow; + return this; + } + + public Builder setSliceTapTarget(int sliceTapTarget) { + mSliceTapTarget = sliceTapTarget; + return this; + } + + public Builder setUiPosition(int uiPosition) { + mUiPosition = uiPosition; + return this; + } + @Override + public CardClickLog build() { + return new CardClickLog(this); + } + } + } + + /** + * Serialize {@link ContextualCard} click event to string + * + * @param card Clicked Contextual card. + * @param sliceRow A Slice can contains multiple row, which row are we clicked + * @param tapTarget Integer value of {@link TapTarget} + * @param uiPosition Contextual card position in Listview + */ + public static String buildCardClickLog(ContextualCard card, int sliceRow, int tapTarget, + int uiPosition) { + final StringBuilder log = new StringBuilder(); + log.append(card.getTextSliceUri()).append("|") + .append(card.getRankingScore()).append("|") + .append(sliceRow).append("|") + .append(actionTypeToTapTarget(tapTarget)).append("|") + .append(uiPosition); + return log.toString(); + } + + /** + * Parse string to a {@link CardClickLog} + */ + public static CardClickLog parseCardClickLog(String clickLog) { + if (clickLog != null) { + final String[] parts = clickLog.split("\\|"); + if (parts.length < 5) { + return null; + } + try { + final CardClickLog.Builder builder = new CardClickLog.Builder(); + builder.setSliceRow(Integer.parseInt(parts[2])) + .setSliceTapTarget(Integer.parseInt(parts[3])) + .setUiPosition(Integer.parseInt(parts[4])) + .setSliceUri(parts[0]) + .setRankingScore(Double.parseDouble(parts[1])); + return builder.build(); + } catch (Exception e) { + Log.e(TAG, "error parsing log", e); + return null; + } + } + return null; + } + + /** + * Serialize {@link ContextualCard} to string + * + * @param card Contextual card. + */ + public static String buildCardDismissLog(ContextualCard card) { + final StringBuilder log = new StringBuilder(); + log.append(card.getTextSliceUri()) + .append("|") + .append(card.getRankingScore()); + return log.toString(); + } + + /** + * Parse string to a {@link CardLog} + */ + public static CardLog parseCardDismissLog(String dismissLog) { + if (dismissLog != null) { + final String[] parts = dismissLog.split("\\|"); + if (parts.length < 2) { + return null; + } + try { + final CardLog.Builder builder = new CardLog.Builder(); + builder.setSliceUri(parts[0]) + .setRankingScore(Double.parseDouble(parts[1])); + return builder.build(); + } catch (Exception e) { + Log.e(TAG, "error parsing log", e); + return null; + } + } + return null; + } + + /** + * Serialize List of {@link ContextualCard} to string + */ + public static String buildCardListLog(List<ContextualCard> cards) { + final StringBuilder log = new StringBuilder(); + log.append(cards.size()); + for (ContextualCard card : cards) { + log.append("|").append(card.getTextSliceUri()) + .append("|").append(card.getRankingScore()); + } + return log.toString(); + } + + /** + * Parse string to a List of {@link CardLog} + */ + public static List<CardLog> parseCardListLog(String listLog) { + final List<CardLog> logList = new ArrayList<>(); + try { + final String[] parts = listLog.split("\\|"); + if (Integer.parseInt(parts[0]) < 0) { + return logList; + } + final int size = parts.length; + for (int i = 1; i < size; ) { + final CardLog.Builder builder = new CardLog.Builder(); + builder.setSliceUri(parts[i++]) + .setRankingScore(Double.parseDouble(parts[i++])); + logList.add(builder.build()); + } + } catch (Exception e) { + Log.e(TAG, "error parsing log", e); + return logList; + } + return logList; + } + + public static int actionTypeToTapTarget(int actionType) { + switch (actionType) { + case EventInfo.ACTION_TYPE_CONTENT: + return TapTarget.TARGET_TITLE; + case EventInfo.ACTION_TYPE_TOGGLE: + return TapTarget.TARGET_TOGGLE; + case EventInfo.ACTION_TYPE_SLIDER: + return TapTarget.TARGET_SLIDER; + default: + Log.w(TAG, "unknown type " + actionType); + return TapTarget.TARGET_DEFAULT; + } + } +} diff --git a/src/com/android/settings/homepage/contextualcards/slices/BatteryFixSlice.java b/src/com/android/settings/homepage/contextualcards/slices/BatteryFixSlice.java new file mode 100644 index 0000000000..d1051fe867 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/slices/BatteryFixSlice.java @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards.slices; + +import static android.content.Context.MODE_PRIVATE; + +import static com.android.settings.slices.CustomSliceRegistry.BATTERY_FIX_SLICE_URI; + +import android.app.PendingIntent; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.util.ArrayMap; +import android.view.View; + +import androidx.annotation.VisibleForTesting; +import androidx.annotation.WorkerThread; +import androidx.core.graphics.drawable.IconCompat; +import androidx.slice.Slice; +import androidx.slice.builders.ListBuilder; +import androidx.slice.builders.ListBuilder.RowBuilder; +import androidx.slice.builders.SliceAction; + +import com.android.internal.os.BatteryStatsHelper; +import com.android.settings.R; +import com.android.settings.SubSettings; +import com.android.settings.Utils; +import com.android.settings.fuelgauge.BatteryStatsHelperLoader; +import com.android.settings.fuelgauge.PowerUsageSummary; +import com.android.settings.fuelgauge.batterytip.BatteryTipLoader; +import com.android.settings.fuelgauge.batterytip.BatteryTipPreferenceController; +import com.android.settings.fuelgauge.batterytip.tips.BatteryTip; +import com.android.settings.slices.CustomSliceable; +import com.android.settings.slices.SliceBackgroundWorker; +import com.android.settings.slices.SliceBuilderUtils; +import com.android.settingslib.utils.ThreadUtils; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +public class BatteryFixSlice implements CustomSliceable { + + @VisibleForTesting + static final String PREFS = "battery_fix_prefs"; + @VisibleForTesting + static final String KEY_CURRENT_TIPS_TYPE = "current_tip_type"; + static final String KEY_CURRENT_TIPS_STATE = "current_tip_state"; + + // A map tracking which BatteryTip and which state of that tip is not important. + private static final Map<Integer, List<Integer>> UNIMPORTANT_BATTERY_TIPS; + + static { + UNIMPORTANT_BATTERY_TIPS = new ArrayMap<>(); + UNIMPORTANT_BATTERY_TIPS.put(BatteryTip.TipType.SUMMARY, + Arrays.asList(BatteryTip.StateType.NEW, BatteryTip.StateType.HANDLED)); + UNIMPORTANT_BATTERY_TIPS.put(BatteryTip.TipType.BATTERY_SAVER, + Arrays.asList(BatteryTip.StateType.HANDLED)); + } + + private static final String TAG = "BatteryFixSlice"; + + private final Context mContext; + + public BatteryFixSlice(Context context) { + mContext = context; + } + + @Override + public Uri getUri() { + return BATTERY_FIX_SLICE_URI; + } + + @Override + public Slice getSlice() { + final ListBuilder sliceBuilder = + new ListBuilder(mContext, BATTERY_FIX_SLICE_URI, ListBuilder.INFINITY) + .setAccentColor(COLOR_NOT_TINTED); + + if (!isBatteryTipAvailableFromCache(mContext)) { + return buildBatteryGoodSlice(sliceBuilder, true /* isError */); + } + + final SliceBackgroundWorker worker = SliceBackgroundWorker.getInstance(getUri()); + final List<BatteryTip> batteryTips = worker != null ? worker.getResults() : null; + + if (batteryTips == null) { + // Because we need wait slice background worker return data + return buildBatteryGoodSlice(sliceBuilder, false /* isError */); + } + + for (BatteryTip batteryTip : batteryTips) { + if (batteryTip.getState() == BatteryTip.StateType.INVISIBLE) { + continue; + } + final Drawable drawable = mContext.getDrawable(batteryTip.getIconId()); + final int iconTintColorId = batteryTip.getIconTintColorId(); + if (iconTintColorId != View.NO_ID) { + drawable.setColorFilter(new PorterDuffColorFilter( + mContext.getResources().getColor(iconTintColorId), + PorterDuff.Mode.SRC_IN)); + } + + final IconCompat icon = Utils.createIconWithDrawable(drawable); + final SliceAction primaryAction = SliceAction.createDeeplink(getPrimaryAction(), + icon, + ListBuilder.ICON_IMAGE, + batteryTip.getTitle(mContext)); + sliceBuilder.addRow(new RowBuilder() + .setTitleItem(icon, ListBuilder.ICON_IMAGE) + .setTitle(batteryTip.getTitle(mContext)) + .setSubtitle(batteryTip.getSummary(mContext)) + .setPrimaryAction(primaryAction)); + break; + } + return sliceBuilder.build(); + } + + @Override + public Intent getIntent() { + final String screenTitle = mContext.getText(R.string.power_usage_summary_title) + .toString(); + final Uri contentUri = new Uri.Builder() + .appendPath(BatteryTipPreferenceController.PREF_NAME).build(); + + return SliceBuilderUtils.buildSearchResultPageIntent(mContext, + PowerUsageSummary.class.getName(), BatteryTipPreferenceController.PREF_NAME, + screenTitle, + SettingsEnums.SLICE) + .setClassName(mContext.getPackageName(), SubSettings.class.getName()) + .setData(contentUri); + } + + @Override + public void onNotifyChange(Intent intent) { + } + + @Override + public Class getBackgroundWorkerClass() { + return BatteryTipWorker.class; + } + + private PendingIntent getPrimaryAction() { + final Intent intent = getIntent(); + return PendingIntent.getActivity(mContext, 0 /* requestCode */, intent, 0 /* flags */); + } + + private Slice buildBatteryGoodSlice(ListBuilder sliceBuilder, boolean isError) { + final IconCompat icon = IconCompat.createWithResource(mContext, + R.drawable.ic_battery_status_good_24dp); + final String title = mContext.getString(R.string.power_usage_summary_title); + final SliceAction primaryAction = SliceAction.createDeeplink(getPrimaryAction(), icon, + ListBuilder.ICON_IMAGE, title); + sliceBuilder.addRow(new RowBuilder() + .setTitleItem(icon, ListBuilder.ICON_IMAGE) + .setTitle(title) + .setPrimaryAction(primaryAction)) + .setIsError(isError); + return sliceBuilder.build(); + } + + // TODO(b/114807643): we should find a better way to get current battery tip type quickly + // Now we save battery tip type to shared preference when battery level changes + public static void updateBatteryTipAvailabilityCache(Context context) { + ThreadUtils.postOnBackgroundThread(() -> refreshBatteryTips(context)); + } + + + @VisibleForTesting + static boolean isBatteryTipAvailableFromCache(Context context) { + final SharedPreferences prefs = context.getSharedPreferences(PREFS, MODE_PRIVATE); + + final int type = prefs.getInt(KEY_CURRENT_TIPS_TYPE, BatteryTip.TipType.SUMMARY); + final int state = prefs.getInt(KEY_CURRENT_TIPS_STATE, BatteryTip.StateType.INVISIBLE); + if (state == BatteryTip.StateType.INVISIBLE) { + // State is INVISIBLE, We should not show anything. + return false; + } + final boolean unimportant = UNIMPORTANT_BATTERY_TIPS.containsKey(type) + && UNIMPORTANT_BATTERY_TIPS.get(type).contains(state); + return !unimportant; + } + + @WorkerThread + private static List<BatteryTip> refreshBatteryTips(Context context) { + final BatteryStatsHelperLoader statsLoader = new BatteryStatsHelperLoader(context); + final BatteryStatsHelper statsHelper = statsLoader.loadInBackground(); + final BatteryTipLoader loader = new BatteryTipLoader(context, statsHelper); + final List<BatteryTip> batteryTips = loader.loadInBackground(); + for (BatteryTip batteryTip : batteryTips) { + if (batteryTip.getState() != BatteryTip.StateType.INVISIBLE) { + context.getSharedPreferences(PREFS, MODE_PRIVATE) + .edit() + .putInt(KEY_CURRENT_TIPS_TYPE, batteryTip.getType()) + .putInt(KEY_CURRENT_TIPS_STATE, batteryTip.getState()) + .apply(); + break; + } + } + return batteryTips; + } + + public static class BatteryTipWorker extends SliceBackgroundWorker<BatteryTip> { + + private final Context mContext; + + public BatteryTipWorker(Context context, Uri uri) { + super(context, uri); + mContext = context; + } + + @Override + protected void onSlicePinned() { + ThreadUtils.postOnBackgroundThread(() -> { + final List<BatteryTip> batteryTips = refreshBatteryTips(mContext); + updateResults(batteryTips); + }); + } + + @Override + protected void onSliceUnpinned() { + } + + @Override + public void close() { + } + } +} diff --git a/src/com/android/settings/homepage/contextualcards/slices/BluetoothDevicesSlice.java b/src/com/android/settings/homepage/contextualcards/slices/BluetoothDevicesSlice.java new file mode 100644 index 0000000000..aa4d53b7f5 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/slices/BluetoothDevicesSlice.java @@ -0,0 +1,277 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards.slices; + +import android.app.PendingIntent; +import android.app.settings.SettingsEnums; +import android.bluetooth.BluetoothAdapter; +import android.content.Context; +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; +import android.util.Pair; + +import androidx.core.graphics.drawable.IconCompat; +import androidx.slice.Slice; +import androidx.slice.builders.ListBuilder; +import androidx.slice.builders.SliceAction; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.settings.R; +import com.android.settings.SubSettings; +import com.android.settings.Utils; +import com.android.settings.bluetooth.BluetoothDeviceDetailsFragment; +import com.android.settings.connecteddevice.ConnectedDeviceDashboardFragment; +import com.android.settings.core.SubSettingLauncher; +import com.android.settings.slices.CustomSliceRegistry; +import com.android.settings.slices.CustomSliceable; +import com.android.settings.slices.SliceBroadcastReceiver; +import com.android.settings.slices.SliceBuilderUtils; +import com.android.settingslib.bluetooth.BluetoothUtils; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.LocalBluetoothManager; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +public class BluetoothDevicesSlice implements CustomSliceable { + + @VisibleForTesting + static final String BLUETOOTH_DEVICE_HASH_CODE = "bluetooth_device_hash_code"; + + /** + * Add the "Pair new device" in the end of slice, when the number of Bluetooth devices is less + * than {@link #DEFAULT_EXPANDED_ROW_COUNT}. + */ + @VisibleForTesting + static final int DEFAULT_EXPANDED_ROW_COUNT = 3; + + /** + * Refer {@link com.android.settings.bluetooth.BluetoothDevicePreference#compareTo} to sort the + * Bluetooth devices by {@link CachedBluetoothDevice}. + */ + private static final Comparator<CachedBluetoothDevice> COMPARATOR + = Comparator.naturalOrder(); + + private static final String TAG = "BluetoothDevicesSlice"; + + private final Context mContext; + + public BluetoothDevicesSlice(Context context) { + mContext = context; + } + + @Override + public Uri getUri() { + return CustomSliceRegistry.BLUETOOTH_DEVICES_SLICE_URI; + } + + @Override + public Slice getSlice() { + // Reload theme for switching dark mode on/off + mContext.getTheme().applyStyle(R.style.Theme_Settings_Home, true /* force */); + + final IconCompat icon = IconCompat.createWithResource(mContext, + com.android.internal.R.drawable.ic_settings_bluetooth); + final CharSequence title = mContext.getText(R.string.bluetooth_devices); + final CharSequence titleNoBluetoothDevices = mContext.getText( + R.string.no_bluetooth_devices); + final PendingIntent primaryActionIntent = PendingIntent.getActivity(mContext, 0, + getIntent(), 0); + final SliceAction primarySliceAction = SliceAction.createDeeplink(primaryActionIntent, icon, + ListBuilder.ICON_IMAGE, title); + final ListBuilder listBuilder = + new ListBuilder(mContext, getUri(), ListBuilder.INFINITY) + .setAccentColor(COLOR_NOT_TINTED); + + // Get row builders by Bluetooth devices. + final List<ListBuilder.RowBuilder> rows = getBluetoothRowBuilder(); + + // Return a header with IsError flag, if no Bluetooth devices. + if (rows.isEmpty()) { + return listBuilder.setHeader(new ListBuilder.HeaderBuilder() + .setTitle(titleNoBluetoothDevices) + .setPrimaryAction(primarySliceAction)) + .setIsError(true) + .build(); + } + + // Get displayable device count. + final int deviceCount = Math.min(rows.size(), DEFAULT_EXPANDED_ROW_COUNT); + + // According to the displayable device count to set sub title of header. + listBuilder.setHeader(new ListBuilder.HeaderBuilder() + .setTitle(title) + .setSubtitle(getSubTitle(deviceCount)) + .setPrimaryAction(primarySliceAction)); + + // According to the displayable device count to add bluetooth device rows. + for (int i = 0; i < deviceCount; i++) { + listBuilder.addRow(rows.get(i)); + } + + return listBuilder.build(); + } + + @Override + public Intent getIntent() { + final String screenTitle = mContext.getText(R.string.connected_devices_dashboard_title) + .toString(); + + return SliceBuilderUtils.buildSearchResultPageIntent(mContext, + ConnectedDeviceDashboardFragment.class.getName(), "" /* key */, + screenTitle, + SettingsEnums.SLICE) + .setClassName(mContext.getPackageName(), SubSettings.class.getName()) + .setData(getUri()); + } + + @Override + public void onNotifyChange(Intent intent) { + // Activate available media device. + final int bluetoothDeviceHashCode = intent.getIntExtra(BLUETOOTH_DEVICE_HASH_CODE, -1); + for (CachedBluetoothDevice cachedBluetoothDevice : getConnectedBluetoothDevices()) { + if (cachedBluetoothDevice.hashCode() == bluetoothDeviceHashCode) { + cachedBluetoothDevice.setActive(); + return; + } + } + } + + @Override + public Class getBackgroundWorkerClass() { + return BluetoothUpdateWorker.class; + } + + @VisibleForTesting + List<CachedBluetoothDevice> getConnectedBluetoothDevices() { + final List<CachedBluetoothDevice> bluetoothDeviceList = new ArrayList<>(); + + // If Bluetooth is disable, skip to get the Bluetooth devices. + if (!BluetoothAdapter.getDefaultAdapter().isEnabled()) { + Log.i(TAG, "Cannot get Bluetooth devices, Bluetooth is disabled."); + return bluetoothDeviceList; + } + + // Get the Bluetooth devices from LocalBluetoothManager. + final LocalBluetoothManager bluetoothManager = + com.android.settings.bluetooth.Utils.getLocalBtManager(mContext); + if (bluetoothManager == null) { + Log.i(TAG, "Cannot get Bluetooth devices, Bluetooth is unsupported."); + return bluetoothDeviceList; + } + final Collection<CachedBluetoothDevice> cachedDevices = + bluetoothManager.getCachedDeviceManager().getCachedDevicesCopy(); + + // Get all connected devices and sort them. + return cachedDevices.stream() + .filter(device -> device.getDevice().isConnected()) + .sorted(COMPARATOR).collect(Collectors.toList()); + } + + @VisibleForTesting + PendingIntent getBluetoothDetailIntent(CachedBluetoothDevice device) { + final Bundle args = new Bundle(); + args.putString(BluetoothDeviceDetailsFragment.KEY_DEVICE_ADDRESS, + device.getDevice().getAddress()); + final SubSettingLauncher subSettingLauncher = new SubSettingLauncher(mContext); + subSettingLauncher.setDestination(BluetoothDeviceDetailsFragment.class.getName()) + .setArguments(args) + .setTitleRes(R.string.device_details_title) + .setSourceMetricsCategory(SettingsEnums.BLUETOOTH_DEVICE_DETAILS); + + // The requestCode should be unique, use the hashcode of device as request code. + return PendingIntent + .getActivity(mContext, device.hashCode() /* requestCode */, + subSettingLauncher.toIntent(), + 0 /* flags */); + } + + @VisibleForTesting + IconCompat getBluetoothDeviceIcon(CachedBluetoothDevice device) { + final Pair<Drawable, String> pair = + BluetoothUtils.getBtRainbowDrawableWithDescription(mContext, device); + final Drawable drawable = pair.first; + + // Use default bluetooth icon if can't get icon. + if (drawable == null) { + return IconCompat.createWithResource(mContext, + com.android.internal.R.drawable.ic_settings_bluetooth); + } + + return Utils.createIconWithDrawable(drawable); + } + + private List<ListBuilder.RowBuilder> getBluetoothRowBuilder() { + // According to Bluetooth devices to create row builders. + final List<ListBuilder.RowBuilder> bluetoothRows = new ArrayList<>(); + final List<CachedBluetoothDevice> bluetoothDevices = getConnectedBluetoothDevices(); + for (CachedBluetoothDevice bluetoothDevice : bluetoothDevices) { + final ListBuilder.RowBuilder rowBuilder = new ListBuilder.RowBuilder() + .setTitleItem(getBluetoothDeviceIcon(bluetoothDevice), ListBuilder.ICON_IMAGE) + .setTitle(bluetoothDevice.getName()) + .setSubtitle(bluetoothDevice.getConnectionSummary()); + + if (bluetoothDevice.isConnectedA2dpDevice()) { + // For available media devices, the primary action is to activate audio stream and + // add setting icon to the end to link detail page. + rowBuilder.setPrimaryAction(buildMediaBluetoothAction(bluetoothDevice)); + rowBuilder.addEndItem(buildBluetoothDetailDeepLinkAction(bluetoothDevice)); + } else { + // For other devices, the primary action is to link detail page. + rowBuilder.setPrimaryAction(buildBluetoothDetailDeepLinkAction(bluetoothDevice)); + } + + bluetoothRows.add(rowBuilder); + } + + return bluetoothRows; + } + + @VisibleForTesting + SliceAction buildMediaBluetoothAction(CachedBluetoothDevice bluetoothDevice) { + // Send broadcast to activate available media device. + final Intent intent = new Intent(getUri().toString()) + .setClass(mContext, SliceBroadcastReceiver.class) + .putExtra(BLUETOOTH_DEVICE_HASH_CODE, bluetoothDevice.hashCode()); + + return SliceAction.create( + PendingIntent.getBroadcast(mContext, bluetoothDevice.hashCode(), intent, 0), + getBluetoothDeviceIcon(bluetoothDevice), + ListBuilder.ICON_IMAGE, + bluetoothDevice.getName()); + } + + @VisibleForTesting + SliceAction buildBluetoothDetailDeepLinkAction(CachedBluetoothDevice bluetoothDevice) { + return SliceAction.createDeeplink( + getBluetoothDetailIntent(bluetoothDevice), + IconCompat.createWithResource(mContext, R.drawable.ic_settings_accent), + ListBuilder.ICON_IMAGE, + bluetoothDevice.getName()); + } + + private CharSequence getSubTitle(int deviceCount) { + return mContext.getResources().getQuantityString(R.plurals.show_bluetooth_devices, + deviceCount, deviceCount); + } +} diff --git a/src/com/android/settings/homepage/contextualcards/slices/BluetoothUpdateWorker.java b/src/com/android/settings/homepage/contextualcards/slices/BluetoothUpdateWorker.java new file mode 100644 index 0000000000..1602f56499 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/slices/BluetoothUpdateWorker.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards.slices; + +import android.content.Context; +import android.net.Uri; +import android.util.Log; + +import com.android.settings.bluetooth.Utils; +import com.android.settings.slices.SliceBackgroundWorker; +import com.android.settingslib.bluetooth.BluetoothCallback; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.LocalBluetoothManager; + +public class BluetoothUpdateWorker extends SliceBackgroundWorker implements BluetoothCallback { + + private static final String TAG = "BluetoothUpdateWorker"; + + private final LocalBluetoothManager mLocalBluetoothManager; + + public BluetoothUpdateWorker(Context context, Uri uri) { + super(context, uri); + mLocalBluetoothManager = Utils.getLocalBtManager(context); + } + + @Override + protected void onSlicePinned() { + if (mLocalBluetoothManager == null) { + Log.i(TAG, "onSlicePinned() Bluetooth is unsupported."); + return; + } + mLocalBluetoothManager.getEventManager().registerCallback(this); + } + + @Override + protected void onSliceUnpinned() { + if (mLocalBluetoothManager == null) { + Log.i(TAG, "onSliceUnpinned() Bluetooth is unsupported."); + return; + } + mLocalBluetoothManager.getEventManager().unregisterCallback(this); + } + + @Override + public void close() { + } + + @Override + public void onAclConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) { + notifySliceChange(); + } + + @Override + public void onActiveDeviceChanged(CachedBluetoothDevice activeDevice, int bluetoothProfile) { + notifySliceChange(); + } + + @Override + public void onBluetoothStateChanged(int bluetoothState) { + notifySliceChange(); + } + + @Override + public void onConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) { + notifySliceChange(); + } + + @Override + public void onProfileConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state, + int bluetoothProfile) { + notifySliceChange(); + } +}
\ No newline at end of file diff --git a/src/com/android/settings/homepage/contextualcards/slices/ContextualNotificationChannelSlice.java b/src/com/android/settings/homepage/contextualcards/slices/ContextualNotificationChannelSlice.java new file mode 100644 index 0000000000..17cae777b7 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/slices/ContextualNotificationChannelSlice.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2019 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.homepage.contextualcards.slices; + +import static android.content.Context.MODE_PRIVATE; + +import android.content.Context; +import android.net.Uri; +import android.util.ArraySet; + +import com.android.settings.R; +import com.android.settings.slices.CustomSliceRegistry; +import com.android.settings.slices.SliceBackgroundWorker; + +import java.util.Set; + +public class ContextualNotificationChannelSlice extends NotificationChannelSlice { + + public static final String PREFS = "notification_channel_slice_prefs"; + public static final String PREF_KEY_INTERACTED_PACKAGES = "interacted_packages"; + + public ContextualNotificationChannelSlice(Context context) { + super(context); + } + + @Override + public Uri getUri() { + return CustomSliceRegistry.CONTEXTUAL_NOTIFICATION_CHANNEL_SLICE_URI; + } + + @Override + protected CharSequence getSubTitle(String packageName, int uid) { + return mContext.getText(R.string.recently_installed_app); + } + + @Override + protected boolean isUserInteracted(String packageName) { + // Check the package has been interacted on current slice or not. + final Set<String> interactedPackages = + mContext.getSharedPreferences(PREFS, MODE_PRIVATE) + .getStringSet(PREF_KEY_INTERACTED_PACKAGES, new ArraySet<>()); + return interactedPackages.contains(packageName); + } + + @Override + public Class<? extends SliceBackgroundWorker> getBackgroundWorkerClass() { + return NotificationChannelWorker.class; + } +} diff --git a/src/com/android/settings/homepage/contextualcards/slices/LowStorageSlice.java b/src/com/android/settings/homepage/contextualcards/slices/LowStorageSlice.java new file mode 100644 index 0000000000..2bc09e9f72 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/slices/LowStorageSlice.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards.slices; + +import android.app.PendingIntent; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.storage.StorageManager; +import android.text.format.Formatter; + +import androidx.core.graphics.drawable.IconCompat; +import androidx.slice.Slice; +import androidx.slice.builders.ListBuilder; +import androidx.slice.builders.ListBuilder.RowBuilder; +import androidx.slice.builders.SliceAction; + +import com.android.settings.R; +import com.android.settings.SubSettings; +import com.android.settings.Utils; +import com.android.settings.deviceinfo.StorageSettings; +import com.android.settings.slices.CustomSliceRegistry; +import com.android.settings.slices.CustomSliceable; +import com.android.settings.slices.SliceBuilderUtils; +import com.android.settingslib.deviceinfo.PrivateStorageInfo; +import com.android.settingslib.deviceinfo.StorageManagerVolumeProvider; + +import java.text.NumberFormat; + +public class LowStorageSlice implements CustomSliceable { + + /** + * If used storage >= 85%, it would be low storage. + */ + private static final double LOW_STORAGE_THRESHOLD = 0.85; + + private final Context mContext; + + public LowStorageSlice(Context context) { + mContext = context; + } + + @Override + public Slice getSlice() { + // Get used storage percentage from StorageManager. + final PrivateStorageInfo info = PrivateStorageInfo.getPrivateStorageInfo( + new StorageManagerVolumeProvider(mContext.getSystemService(StorageManager.class))); + final double usedPercentage = (double) (info.totalBytes - info.freeBytes) / info.totalBytes; + + // Generate Low storage Slice. + final String percentageString = NumberFormat.getPercentInstance().format(usedPercentage); + final String freeSizeString = Formatter.formatFileSize(mContext, info.freeBytes); + final ListBuilder listBuilder = new ListBuilder(mContext, + CustomSliceRegistry.LOW_STORAGE_SLICE_URI, ListBuilder.INFINITY).setAccentColor( + Utils.getColorAccentDefaultColor(mContext)); + final IconCompat icon = IconCompat.createWithResource(mContext, R.drawable.ic_storage); + + if (usedPercentage < LOW_STORAGE_THRESHOLD) { + // For clients that ignore error checking, a generic storage slice will be given. + final CharSequence titleStorage = mContext.getText(R.string.storage_settings); + final String summaryStorage = mContext.getString(R.string.storage_summary, + percentageString, freeSizeString); + + return listBuilder + .addRow(buildRowBuilder(titleStorage, summaryStorage, icon)) + .setIsError(true) + .build(); + } + + final CharSequence titleLowStorage = mContext.getText(R.string.storage_menu_free); + final String summaryLowStorage = mContext.getString(R.string.low_storage_summary, + percentageString, freeSizeString); + + return listBuilder + .addRow(buildRowBuilder(titleLowStorage, summaryLowStorage, icon)) + .build(); + } + + @Override + public Uri getUri() { + return CustomSliceRegistry.LOW_STORAGE_SLICE_URI; + } + + @Override + public void onNotifyChange(Intent intent) { + + } + + @Override + public Intent getIntent() { + final String screenTitle = mContext.getText(R.string.storage_label) + .toString(); + + return SliceBuilderUtils.buildSearchResultPageIntent(mContext, + StorageSettings.class.getName(), "" /* key */, + screenTitle, + SettingsEnums.SLICE) + .setClassName(mContext.getPackageName(), SubSettings.class.getName()) + .setData(CustomSliceRegistry.LOW_STORAGE_SLICE_URI); + } + + private RowBuilder buildRowBuilder(CharSequence title, String summary, IconCompat icon) { + final SliceAction primarySliceAction = SliceAction.createDeeplink( + PendingIntent.getActivity(mContext, 0, getIntent(), 0), icon, + ListBuilder.ICON_IMAGE, title); + + return new RowBuilder() + .setTitleItem(icon, ListBuilder.ICON_IMAGE) + .setTitle(title) + .setSubtitle(summary) + .setPrimaryAction(primarySliceAction); + } +}
\ No newline at end of file diff --git a/src/com/android/settings/homepage/contextualcards/slices/NotificationChannelSlice.java b/src/com/android/settings/homepage/contextualcards/slices/NotificationChannelSlice.java new file mode 100644 index 0000000000..ccccd39c68 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/slices/NotificationChannelSlice.java @@ -0,0 +1,508 @@ +/* + * Copyright (C) 2019 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.homepage.contextualcards.slices; + +import static android.app.NotificationManager.IMPORTANCE_LOW; +import static android.app.NotificationManager.IMPORTANCE_NONE; +import static android.app.slice.Slice.EXTRA_TOGGLE_STATE; + +import static com.android.settings.notification.NotificationSettingsBase.ARG_FROM_SETTINGS; + +import android.app.Application; +import android.app.NotificationChannel; +import android.app.NotificationChannelGroup; +import android.app.PendingIntent; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Bundle; +import android.os.UserHandle; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Log; + +import androidx.core.graphics.drawable.IconCompat; +import androidx.slice.Slice; +import androidx.slice.builders.ListBuilder; +import androidx.slice.builders.SliceAction; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.settings.R; +import com.android.settings.SubSettings; +import com.android.settings.Utils; +import com.android.settings.applications.AppAndNotificationDashboardFragment; +import com.android.settings.applications.AppInfoBase; +import com.android.settings.core.SubSettingLauncher; +import com.android.settings.notification.AppNotificationSettings; +import com.android.settings.notification.ChannelNotificationSettings; +import com.android.settings.notification.NotificationBackend; +import com.android.settings.notification.NotificationBackend.NotificationsSentState; +import com.android.settings.slices.CustomSliceRegistry; +import com.android.settings.slices.CustomSliceable; +import com.android.settings.slices.SliceBroadcastReceiver; +import com.android.settings.slices.SliceBuilderUtils; +import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; +import com.android.settingslib.applications.ApplicationsState; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; + +public class NotificationChannelSlice implements CustomSliceable { + + /** + * Recently app condition: + * App was installed between 3 and 7 days ago. + */ + @VisibleForTesting + static final long DURATION_START_DAYS = TimeUnit.DAYS.toMillis(7); + @VisibleForTesting + static final long DURATION_END_DAYS = TimeUnit.DAYS.toMillis(3); + + /** + * Notification count condition: + * App has sent at least ~10 notifications. + */ + @VisibleForTesting + static final int MIN_NOTIFICATION_SENT_COUNT = 10; + + /** + * Limit rows when the number of notification channel is more than {@link + * #DEFAULT_EXPANDED_ROW_COUNT}. + */ + @VisibleForTesting + static final int DEFAULT_EXPANDED_ROW_COUNT = 3; + + private static final String TAG = "NotifChannelSlice"; + private static final String PACKAGE_NAME = "package_name"; + private static final String PACKAGE_UID = "package_uid"; + private static final String CHANNEL_ID = "channel_id"; + private static final long TASK_TIMEOUT_MS = 100; + + /** + * Sort notification channel with weekly average sent count by descending. + * + * Note: + * When the sent count of notification channels is the same, follow the sorting mechanism from + * {@link com.android.settings.notification.NotificationSettingsBase#mChannelComparator}. + * Since slice view only shows displayable notification channels, so those deleted ones are + * excluded from the comparison here. + */ + private static final Comparator<NotificationChannelState> CHANNEL_STATE_COMPARATOR = + (left, right) -> { + final NotificationsSentState leftState = left.getNotificationsSentState(); + final NotificationsSentState rightState = right.getNotificationsSentState(); + if (rightState.avgSentWeekly != leftState.avgSentWeekly) { + return rightState.avgSentWeekly - leftState.avgSentWeekly; + } + + final NotificationChannel leftChannel = left.getNotificationChannel(); + final NotificationChannel rightChannel = right.getNotificationChannel(); + if (TextUtils.equals(leftChannel.getId(), NotificationChannel.DEFAULT_CHANNEL_ID)) { + return 1; + } else if (TextUtils.equals(rightChannel.getId(), + NotificationChannel.DEFAULT_CHANNEL_ID)) { + return -1; + } + + return leftChannel.getId().compareTo(rightChannel.getId()); + }; + + protected final Context mContext; + private final ExecutorService mExecutorService; + @VisibleForTesting + NotificationBackend mNotificationBackend; + private NotificationBackend.AppRow mAppRow; + private String mPackageName; + private int mUid; + + public NotificationChannelSlice(Context context) { + mContext = context; + mNotificationBackend = new NotificationBackend(); + mExecutorService = Executors.newCachedThreadPool(); + } + + @Override + public Slice getSlice() { + final ListBuilder listBuilder = + new ListBuilder(mContext, getUri(), ListBuilder.INFINITY) + .setAccentColor(COLOR_NOT_TINTED); + /** + * Get package which is satisfied with: + * 1. Recently installed. + * 2. Multiple channels. + * 3. Sent at least ~10 notifications. + */ + mPackageName = getEligibleNotificationsPackage(getRecentlyInstalledPackages()); + if (mPackageName == null) { + // Return a header with IsError flag, if package is not found. + return listBuilder.setHeader(getNoSuggestedAppHeader()) + .setIsError(true).build(); + } + mUid = getApplicationUid(mPackageName); + + // Add notification channel header. + final IconCompat icon = getApplicationIcon(mPackageName); + final CharSequence title = mContext.getString(R.string.manage_app_notification, + Utils.getApplicationLabel(mContext, mPackageName)); + listBuilder.addRow(new ListBuilder.RowBuilder() + .setTitleItem(icon, ListBuilder.ICON_IMAGE) + .setTitle(title) + .setSubtitle(getSubTitle(mPackageName, mUid)) + .setPrimaryAction(getPrimarySliceAction(icon, title, getIntent()))); + + // Add notification channel rows. + final List<ListBuilder.RowBuilder> rows = getNotificationChannelRows(icon); + for (ListBuilder.RowBuilder rowBuilder : rows) { + listBuilder.addRow(rowBuilder); + } + + return listBuilder.build(); + } + + @Override + public Uri getUri() { + return CustomSliceRegistry.NOTIFICATION_CHANNEL_SLICE_URI; + } + + @Override + public void onNotifyChange(Intent intent) { + final boolean newState = intent.getBooleanExtra(EXTRA_TOGGLE_STATE, false); + final String packageName = intent.getStringExtra(PACKAGE_NAME); + final int uid = intent.getIntExtra(PACKAGE_UID, -1); + final String channelId = intent.getStringExtra(CHANNEL_ID); + final NotificationChannel channel = mNotificationBackend.getChannel(packageName, uid, + channelId); + final int importance = newState ? IMPORTANCE_LOW : IMPORTANCE_NONE; + channel.setImportance(importance); + channel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE); + mNotificationBackend.updateChannel(packageName, uid, channel); + } + + @Override + public Intent getIntent() { + final Bundle args = new Bundle(); + args.putString(AppInfoBase.ARG_PACKAGE_NAME, mPackageName); + args.putInt(AppInfoBase.ARG_PACKAGE_UID, mUid); + + return new SubSettingLauncher(mContext) + .setDestination(AppNotificationSettings.class.getName()) + .setTitleRes(R.string.notifications_title) + .setArguments(args) + .setSourceMetricsCategory(SettingsEnums.SLICE) + .toIntent(); + } + + /** + * Check the package has been interacted by user or not. + * Will use to filter package in {@link #getRecentlyInstalledPackages()}. + * + * @param packageName The app package name. + * @return true if the package was interacted, false otherwise. + */ + protected boolean isUserInteracted(String packageName) { + return false; + } + + @VisibleForTesting + IconCompat getApplicationIcon(String packageName) { + final Drawable drawable; + try { + drawable = mContext.getPackageManager().getApplicationIcon(packageName); + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "No such package to get application icon."); + return null; + } + + return Utils.createIconWithDrawable(drawable); + } + + @VisibleForTesting + int getApplicationUid(String packageName) { + final ApplicationsState.AppEntry appEntry = + ApplicationsState.getInstance((Application) mContext.getApplicationContext()) + .getEntry(packageName, UserHandle.myUserId()); + + return appEntry.info.uid; + } + + private SliceAction buildRowSliceAction(NotificationChannel channel, IconCompat icon) { + final Bundle channelArgs = new Bundle(); + channelArgs.putInt(AppInfoBase.ARG_PACKAGE_UID, mUid); + channelArgs.putString(AppInfoBase.ARG_PACKAGE_NAME, mPackageName); + channelArgs.putString(Settings.EXTRA_CHANNEL_ID, channel.getId()); + channelArgs.putBoolean(ARG_FROM_SETTINGS, true); + + final Intent channelIntent = new SubSettingLauncher(mContext) + .setDestination(ChannelNotificationSettings.class.getName()) + .setArguments(channelArgs) + .setTitleRes(R.string.notification_channel_title) + .setSourceMetricsCategory(SettingsEnums.SLICE) + .toIntent(); + + return SliceAction.createDeeplink( + PendingIntent.getActivity(mContext, channel.hashCode(), channelIntent, 0), icon, + ListBuilder.ICON_IMAGE, channel.getName()); + } + + private ListBuilder.HeaderBuilder getNoSuggestedAppHeader() { + final IconCompat icon = IconCompat.createWithResource(mContext, + R.drawable.ic_homepage_apps); + final CharSequence titleNoSuggestedApp = mContext.getString(R.string.no_suggested_app); + final SliceAction primarySliceActionForNoSuggestedApp = getPrimarySliceAction(icon, + titleNoSuggestedApp, getAppAndNotificationPageIntent()); + + return new ListBuilder.HeaderBuilder() + .setTitle(titleNoSuggestedApp) + .setPrimaryAction(primarySliceActionForNoSuggestedApp); + } + + private List<ListBuilder.RowBuilder> getNotificationChannelRows(IconCompat icon) { + final List<ListBuilder.RowBuilder> notificationChannelRows = new ArrayList<>(); + final List<NotificationChannel> displayableChannels = getDisplayableChannels(mAppRow); + + for (NotificationChannel channel : displayableChannels) { + notificationChannelRows.add(new ListBuilder.RowBuilder() + .setTitle(channel.getName()) + .setSubtitle(NotificationBackend.getSentSummary( + mContext, mAppRow.sentByChannel.get(channel.getId()), false)) + .setPrimaryAction(buildRowSliceAction(channel, icon)) + .addEndItem(SliceAction.createToggle(getToggleIntent(channel.getId()), + null /* actionTitle */, channel.getImportance() != IMPORTANCE_NONE))); + } + + return notificationChannelRows; + } + + private PendingIntent getToggleIntent(String channelId) { + // Send broadcast to enable/disable channel. + final Intent intent = new Intent(getUri().toString()) + .setClass(mContext, SliceBroadcastReceiver.class) + .putExtra(PACKAGE_NAME, mPackageName) + .putExtra(PACKAGE_UID, mUid) + .putExtra(CHANNEL_ID, channelId); + + return PendingIntent.getBroadcast(mContext, intent.hashCode(), intent, 0); + } + + private List<PackageInfo> getRecentlyInstalledPackages() { + final long startTime = System.currentTimeMillis() - DURATION_START_DAYS; + final long endTime = System.currentTimeMillis() - DURATION_END_DAYS; + + // Get recently installed packages between 3 and 7 days ago. + final List<PackageInfo> recentlyInstalledPackages = new ArrayList<>(); + final List<PackageInfo> installedPackages = + mContext.getPackageManager().getInstalledPackages(0); + for (PackageInfo packageInfo : installedPackages) { + // Not include system app and interacted app. + if (packageInfo.applicationInfo.isSystemApp() + || isUserInteracted(packageInfo.packageName)) { + continue; + } + + if (packageInfo.firstInstallTime >= startTime + && packageInfo.firstInstallTime <= endTime) { + recentlyInstalledPackages.add(packageInfo); + } + } + + return recentlyInstalledPackages; + } + + private SliceAction getPrimarySliceAction(IconCompat icon, CharSequence title, Intent intent) { + return SliceAction.createDeeplink( + PendingIntent.getActivity(mContext, intent.hashCode(), intent, 0), + icon, + ListBuilder.ICON_IMAGE, + title); + } + + private List<NotificationChannel> getDisplayableChannels(NotificationBackend.AppRow appRow) { + final List<NotificationChannelGroup> channelGroupList = + mNotificationBackend.getGroups(appRow.pkg, appRow.uid).getList(); + final List<NotificationChannel> channels = channelGroupList.stream() + .flatMap(group -> group.getChannels().stream().filter( + channel -> isChannelEnabled(group, channel, appRow))) + .collect(Collectors.toList()); + + // Pack the notification channel with notification sent state for sorting. + final List<NotificationChannelState> channelStates = new ArrayList<>(); + for (NotificationChannel channel : channels) { + NotificationsSentState sentState = appRow.sentByChannel.get(channel.getId()); + if (sentState == null) { + sentState = new NotificationsSentState(); + } + channelStates.add(new NotificationChannelState(sentState, channel)); + } + + // Sort the notification channels with notification sent count by descending. + return channelStates.stream() + .sorted(CHANNEL_STATE_COMPARATOR) + .map(state -> state.getNotificationChannel()) + .limit(DEFAULT_EXPANDED_ROW_COUNT) + .collect(Collectors.toList()); + } + + private String getEligibleNotificationsPackage(List<PackageInfo> packageInfoList) { + if (packageInfoList.isEmpty()) { + return null; + } + + // Create tasks to get notification data for multi-channel packages. + final List<Future<NotificationBackend.AppRow>> appRowTasks = new ArrayList<>(); + for (PackageInfo packageInfo : packageInfoList) { + final NotificationMultiChannelAppRow future = new NotificationMultiChannelAppRow( + mContext, mNotificationBackend, packageInfo); + appRowTasks.add(mExecutorService.submit(future)); + } + + // Get the package which has sent at least ~10 notifications and not turn off channels. + int maxSentCount = 0; + String maxSentCountPackage = null; + for (Future<NotificationBackend.AppRow> appRowTask : appRowTasks) { + NotificationBackend.AppRow appRow = null; + try { + appRow = appRowTask.get(TASK_TIMEOUT_MS, TimeUnit.MILLISECONDS); + } catch (ExecutionException | InterruptedException | TimeoutException e) { + Log.w(TAG, "Failed to get notification data.", e); + } + + // Ignore packages which are banned notifications or block all displayable channels. + if (appRow == null || appRow.banned || isAllChannelsBlocked( + getDisplayableChannels(appRow))) { + continue; + } + + // Get sent notification count from app. + final int sentCount = appRow.sentByApp.sentCount; + if (sentCount >= MIN_NOTIFICATION_SENT_COUNT && sentCount > maxSentCount) { + maxSentCount = sentCount; + maxSentCountPackage = appRow.pkg; + mAppRow = appRow; + } + } + + return maxSentCountPackage; + } + + private boolean isAllChannelsBlocked(List<NotificationChannel> channels) { + for (NotificationChannel channel : channels) { + if (channel.getImportance() != IMPORTANCE_NONE) { + return false; + } + } + return true; + } + + protected CharSequence getSubTitle(String packageName, int uid) { + final int channelCount = mNotificationBackend.getChannelCount(packageName, uid); + + if (channelCount > DEFAULT_EXPANDED_ROW_COUNT) { + return mContext.getString( + R.string.notification_many_channel_count_summary, channelCount); + } + + return mContext.getResources().getQuantityString( + R.plurals.notification_few_channel_count_summary, channelCount, channelCount); + } + + private Intent getAppAndNotificationPageIntent() { + final String screenTitle = mContext.getText(R.string.app_and_notification_dashboard_title) + .toString(); + + return SliceBuilderUtils.buildSearchResultPageIntent(mContext, + AppAndNotificationDashboardFragment.class.getName(), "" /* key */, + screenTitle, + SettingsEnums.SLICE) + .setClassName(mContext.getPackageName(), SubSettings.class.getName()) + .setData(getUri()); + } + + private boolean isChannelEnabled(NotificationChannelGroup group, NotificationChannel channel, + NotificationBackend.AppRow appRow) { + final RestrictedLockUtils.EnforcedAdmin suspendedAppsAdmin = + RestrictedLockUtilsInternal.checkIfApplicationIsSuspended(mContext, mPackageName, + mUid); + + return suspendedAppsAdmin == null + && isChannelBlockable(channel, appRow) + && isChannelConfigurable(channel, appRow) + && !group.isBlocked(); + } + + private boolean isChannelConfigurable(NotificationChannel channel, + NotificationBackend.AppRow appRow) { + if (channel != null && appRow != null) { + return !channel.isImportanceLockedByOEM(); + } + + return false; + } + + private boolean isChannelBlockable(NotificationChannel channel, + NotificationBackend.AppRow appRow) { + if (channel != null && appRow != null) { + if (!appRow.systemApp) { + return true; + } + + return channel.isBlockableSystem() + || channel.getImportance() == IMPORTANCE_NONE; + } + + return false; + } + + /** + * This class is used to sort notification channels according to notification sent count and + * notification id in {@link NotificationChannelSlice#CHANNEL_STATE_COMPARATOR}. + * + * Include {@link NotificationsSentState#avgSentWeekly} and {@link NotificationChannel#getId()} + * to get the number of notifications being sent and notification id. + */ + private static class NotificationChannelState { + + final private NotificationsSentState mNotificationsSentState; + final private NotificationChannel mNotificationChannel; + + public NotificationChannelState(NotificationsSentState notificationsSentState, + NotificationChannel notificationChannel) { + mNotificationsSentState = notificationsSentState; + mNotificationChannel = notificationChannel; + } + + public NotificationChannel getNotificationChannel() { + return mNotificationChannel; + } + + public NotificationsSentState getNotificationsSentState() { + return mNotificationsSentState; + } + } +} diff --git a/src/com/android/settings/homepage/contextualcards/slices/NotificationChannelWorker.java b/src/com/android/settings/homepage/contextualcards/slices/NotificationChannelWorker.java new file mode 100644 index 0000000000..f1d0d593cf --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/slices/NotificationChannelWorker.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2019 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.homepage.contextualcards.slices; + +import static android.content.Context.MODE_PRIVATE; + +import static com.android.settings.homepage.contextualcards.slices.ContextualNotificationChannelSlice.PREFS; +import static com.android.settings.homepage.contextualcards.slices.ContextualNotificationChannelSlice.PREF_KEY_INTERACTED_PACKAGES; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.net.Uri; +import android.util.ArraySet; + +import com.android.settings.slices.SliceBackgroundWorker; + +import java.io.IOException; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class NotificationChannelWorker extends SliceBackgroundWorker<Void> { + + public NotificationChannelWorker(Context context, Uri uri) { + super(context, uri); + } + + @Override + protected void onSlicePinned() { + } + + @Override + protected void onSliceUnpinned() { + removeUninstalledPackages(); + } + + @Override + public void close() throws IOException { + } + + private void removeUninstalledPackages() { + final SharedPreferences prefs = getContext().getSharedPreferences(PREFS, MODE_PRIVATE); + final Set<String> interactedPackages = + prefs.getStringSet(PREF_KEY_INTERACTED_PACKAGES, new ArraySet()); + if (interactedPackages.isEmpty()) { + return; + } + + final List<PackageInfo> installedPackageInfos = + getContext().getPackageManager().getInstalledPackages(0); + final List<String> installedPackages = installedPackageInfos.stream() + .map(packageInfo -> packageInfo.packageName) + .collect(Collectors.toList()); + final Set<String> newInteractedPackages = new ArraySet<>(); + for (String packageName : interactedPackages) { + if (installedPackages.contains(packageName)) { + newInteractedPackages.add(packageName); + } + } + prefs.edit().putStringSet(PREF_KEY_INTERACTED_PACKAGES, newInteractedPackages).apply(); + } +} diff --git a/src/com/android/settings/homepage/contextualcards/slices/NotificationMultiChannelAppRow.java b/src/com/android/settings/homepage/contextualcards/slices/NotificationMultiChannelAppRow.java new file mode 100644 index 0000000000..bf91f53f3a --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/slices/NotificationMultiChannelAppRow.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2019 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.homepage.contextualcards.slices; + +import android.app.role.RoleManager; +import android.content.Context; +import android.content.pm.PackageInfo; + +import com.android.settings.notification.NotificationBackend; + +import java.util.concurrent.Callable; + +/** + * This class is responsible for getting notification app row from package which has multiple + * notification channels.{@link NotificationChannelSlice} uses it to improve latency. + */ +class NotificationMultiChannelAppRow implements Callable<NotificationBackend.AppRow> { + + private final Context mContext; + private final NotificationBackend mNotificationBackend; + private final PackageInfo mPackageInfo; + + public NotificationMultiChannelAppRow(Context context, NotificationBackend notificationBackend, + PackageInfo packageInfo) { + mContext = context; + mNotificationBackend = notificationBackend; + mPackageInfo = packageInfo; + } + + @Override + public NotificationBackend.AppRow call() throws Exception { + final int channelCount = mNotificationBackend.getChannelCount( + mPackageInfo.applicationInfo.packageName, mPackageInfo.applicationInfo.uid); + if (channelCount > 1) { + return mNotificationBackend.loadAppRow(mContext, mContext.getPackageManager(), + mContext.getSystemService(RoleManager.class), mPackageInfo); + } + return null; + } +} diff --git a/src/com/android/settings/homepage/contextualcards/slices/SliceContextualCardController.java b/src/com/android/settings/homepage/contextualcards/slices/SliceContextualCardController.java new file mode 100644 index 0000000000..9eb7faef90 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/slices/SliceContextualCardController.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards.slices; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.text.TextUtils; + +import androidx.annotation.VisibleForTesting; + +import com.android.settings.R; +import com.android.settings.homepage.contextualcards.CardDatabaseHelper; +import com.android.settings.homepage.contextualcards.ContextualCard; +import com.android.settings.homepage.contextualcards.ContextualCardController; +import com.android.settings.homepage.contextualcards.ContextualCardFeedbackDialog; +import com.android.settings.homepage.contextualcards.ContextualCardUpdateListener; +import com.android.settings.homepage.contextualcards.logging.ContextualCardLogUtils; +import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; +import com.android.settingslib.utils.ThreadUtils; + +/** + * Card controller for {@link ContextualCard} built as slices. + */ +public class SliceContextualCardController implements ContextualCardController { + + private static final String TAG = "SliceCardController"; + + private final Context mContext; + + private ContextualCardUpdateListener mCardUpdateListener; + + public SliceContextualCardController(Context context) { + mContext = context; + } + + @Override + public int getCardType() { + return ContextualCard.CardType.SLICE; + } + + @Override + public void onPrimaryClick(ContextualCard card) { + + } + + @Override + public void onActionClick(ContextualCard card) { + + } + + @Override + public void onDismissed(ContextualCard card) { + ThreadUtils.postOnBackgroundThread(() -> { + final CardDatabaseHelper dbHelper = CardDatabaseHelper.getInstance(mContext); + dbHelper.markContextualCardAsDismissed(mContext, card.getName()); + }); + showFeedbackDialog(card); + + final MetricsFeatureProvider metricsFeatureProvider = + FeatureFactory.getFactory(mContext).getMetricsFeatureProvider(); + + metricsFeatureProvider.action(mContext, + SettingsEnums.ACTION_CONTEXTUAL_CARD_DISMISS, + ContextualCardLogUtils.buildCardDismissLog(card)); + } + + @Override + public void setCardUpdateListener(ContextualCardUpdateListener listener) { + mCardUpdateListener = listener; + } + + @VisibleForTesting + void showFeedbackDialog(ContextualCard card) { + final String email = mContext.getString(R.string.config_contextual_card_feedback_email); + if (!isFeedbackEnabled(email)) { + return; + } + final Intent feedbackIntent = new Intent(mContext, ContextualCardFeedbackDialog.class); + feedbackIntent.putExtra(ContextualCardFeedbackDialog.EXTRA_CARD_NAME, + getSimpleCardName(card)); + feedbackIntent.putExtra(ContextualCardFeedbackDialog.EXTRA_FEEDBACK_EMAIL, email); + feedbackIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mContext.startActivity(feedbackIntent); + } + + @VisibleForTesting + boolean isFeedbackEnabled(String email) { + return !TextUtils.isEmpty(email) && Build.IS_DEBUGGABLE; + } + + private String getSimpleCardName(ContextualCard card) { + final String[] split = card.getName().split("/"); + return split[split.length - 1]; + } +} diff --git a/src/com/android/settings/homepage/contextualcards/slices/SliceContextualCardRenderer.java b/src/com/android/settings/homepage/contextualcards/slices/SliceContextualCardRenderer.java new file mode 100644 index 0000000000..9d1b883177 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/slices/SliceContextualCardRenderer.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards.slices; + +import static android.app.slice.Slice.HINT_ERROR; + +import android.content.ContentResolver; +import android.content.Context; +import android.net.Uri; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.Log; +import android.view.View; +import android.widget.Button; + +import androidx.annotation.LayoutRes; +import androidx.annotation.VisibleForTesting; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.OnLifecycleEvent; +import androidx.recyclerview.widget.RecyclerView; +import androidx.slice.Slice; +import androidx.slice.widget.SliceLiveData; + +import com.android.settings.R; +import com.android.settings.homepage.contextualcards.CardContentProvider; +import com.android.settings.homepage.contextualcards.ContextualCard; +import com.android.settings.homepage.contextualcards.ContextualCardRenderer; +import com.android.settings.homepage.contextualcards.ControllerRendererPool; + +import java.util.Map; +import java.util.Set; + +/** + * Card renderer for {@link ContextualCard} built as slice full card or slice half card. + */ +public class SliceContextualCardRenderer implements ContextualCardRenderer, LifecycleObserver { + public static final int VIEW_TYPE_FULL_WIDTH = R.layout.contextual_slice_full_tile; + public static final int VIEW_TYPE_HALF_WIDTH = R.layout.contextual_slice_half_tile; + public static final int VIEW_TYPE_DEFERRED_SETUP = R.layout.contextual_slice_deferred_setup; + + private static final String TAG = "SliceCardRenderer"; + + @VisibleForTesting + final Map<Uri, LiveData<Slice>> mSliceLiveDataMap; + @VisibleForTesting + final Set<RecyclerView.ViewHolder> mFlippedCardSet; + + private final Context mContext; + private final LifecycleOwner mLifecycleOwner; + private final ControllerRendererPool mControllerRendererPool; + private final SliceDeferredSetupCardRendererHelper mDeferredSetupCardHelper; + private final SliceFullCardRendererHelper mFullCardHelper; + private final SliceHalfCardRendererHelper mHalfCardHelper; + + public SliceContextualCardRenderer(Context context, LifecycleOwner lifecycleOwner, + ControllerRendererPool controllerRendererPool) { + mContext = context; + mLifecycleOwner = lifecycleOwner; + mSliceLiveDataMap = new ArrayMap<>(); + mControllerRendererPool = controllerRendererPool; + mFlippedCardSet = new ArraySet<>(); + mLifecycleOwner.getLifecycle().addObserver(this); + mFullCardHelper = new SliceFullCardRendererHelper(context); + mHalfCardHelper = new SliceHalfCardRendererHelper(context); + mDeferredSetupCardHelper = new SliceDeferredSetupCardRendererHelper(context); + } + + @Override + public RecyclerView.ViewHolder createViewHolder(View view, @LayoutRes int viewType) { + if (viewType == VIEW_TYPE_DEFERRED_SETUP) { + return mDeferredSetupCardHelper.createViewHolder(view); + } else if (viewType == VIEW_TYPE_HALF_WIDTH) { + return mHalfCardHelper.createViewHolder(view); + } + return mFullCardHelper.createViewHolder(view); + } + + @Override + public void bindView(RecyclerView.ViewHolder holder, ContextualCard card) { + final Uri uri = card.getSliceUri(); + + if (!ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) { + Log.w(TAG, "Invalid uri, skipping slice: " + uri); + return; + } + + LiveData<Slice> sliceLiveData = mSliceLiveDataMap.get(uri); + + if (sliceLiveData == null) { + sliceLiveData = SliceLiveData.fromUri(mContext, uri); + mSliceLiveDataMap.put(uri, sliceLiveData); + } + + final View swipeBackground = holder.itemView.findViewById(R.id.dismissal_swipe_background); + sliceLiveData.removeObservers(mLifecycleOwner); + // set the background to GONE in case the holder is reused. + if (swipeBackground != null) { + swipeBackground.setVisibility(View.GONE); + } + sliceLiveData.observe(mLifecycleOwner, slice -> { + if (slice == null) { + Log.w(TAG, "Slice is null"); + mContext.getContentResolver().notifyChange(CardContentProvider.REFRESH_CARD_URI, + null); + return; + } + + if (slice.hasHint(HINT_ERROR)) { + Log.w(TAG, "Slice has HINT_ERROR, skipping rendering. uri=" + slice.getUri()); + mSliceLiveDataMap.get(slice.getUri()).removeObservers(mLifecycleOwner); + mContext.getContentResolver().notifyChange(CardContentProvider.REFRESH_CARD_URI, + null); + return; + } + + if (holder.getItemViewType() == VIEW_TYPE_DEFERRED_SETUP) { + mDeferredSetupCardHelper.bindView(holder, card, slice); + } else if (holder.getItemViewType() == VIEW_TYPE_HALF_WIDTH) { + mHalfCardHelper.bindView(holder, card, slice); + } else { + mFullCardHelper.bindView(holder, card, slice); + } + if (swipeBackground != null) { + swipeBackground.setVisibility(View.VISIBLE); + } + }); + + if (holder.getItemViewType() + == VIEW_TYPE_DEFERRED_SETUP) {// Deferred setup is never dismissible. + } else if (holder.getItemViewType() == VIEW_TYPE_HALF_WIDTH) { + initDismissalActions(holder, card); + } else { + initDismissalActions(holder, card); + } + + if (card.isPendingDismiss()) { + showDismissalView(holder); + mFlippedCardSet.add(holder); + } + } + + private void initDismissalActions(RecyclerView.ViewHolder holder, ContextualCard card) { + final Button btnKeep = holder.itemView.findViewById(R.id.keep); + btnKeep.setOnClickListener(v -> { + mFlippedCardSet.remove(holder); + resetCardView(holder); + }); + + final Button btnRemove = holder.itemView.findViewById(R.id.remove); + btnRemove.setOnClickListener(v -> { + mControllerRendererPool.getController(mContext, card.getCardType()).onDismissed(card); + mFlippedCardSet.remove(holder); + resetCardView(holder); + mSliceLiveDataMap.get(card.getSliceUri()).removeObservers(mLifecycleOwner); + }); + } + + @OnLifecycleEvent(Lifecycle.Event.ON_STOP) + public void onStop() { + mFlippedCardSet.stream().forEach(holder -> resetCardView(holder)); + mFlippedCardSet.clear(); + } + + private void resetCardView(RecyclerView.ViewHolder holder) { + holder.itemView.findViewById(R.id.dismissal_view).setVisibility(View.GONE); + getInitialView(holder).setVisibility(View.VISIBLE); + } + + private void showDismissalView(RecyclerView.ViewHolder holder) { + holder.itemView.findViewById(R.id.dismissal_view).setVisibility(View.VISIBLE); + getInitialView(holder).setVisibility(View.INVISIBLE); + } + + private View getInitialView(RecyclerView.ViewHolder viewHolder) { + if (viewHolder.getItemViewType() == VIEW_TYPE_HALF_WIDTH) { + return ((SliceHalfCardRendererHelper.HalfCardViewHolder) viewHolder).content; + } + return ((SliceFullCardRendererHelper.SliceViewHolder) viewHolder).sliceView; + } +} diff --git a/src/com/android/settings/homepage/contextualcards/slices/SliceDeferredSetupCardRendererHelper.java b/src/com/android/settings/homepage/contextualcards/slices/SliceDeferredSetupCardRendererHelper.java new file mode 100644 index 0000000000..ea9e424cb3 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/slices/SliceDeferredSetupCardRendererHelper.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2019 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.homepage.contextualcards.slices; + +import android.app.PendingIntent; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.recyclerview.widget.RecyclerView; +import androidx.slice.Slice; +import androidx.slice.SliceMetadata; +import androidx.slice.core.SliceAction; +import androidx.slice.widget.EventInfo; + +import com.android.settings.R; +import com.android.settings.homepage.contextualcards.ContextualCard; +import com.android.settings.homepage.contextualcards.logging.ContextualCardLogUtils; +import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; + +/** + * Card renderer helper for {@link ContextualCard} built as slice deferred setup card. + */ +class SliceDeferredSetupCardRendererHelper { + private static final String TAG = "SliceDSCRendererHelper"; + + private final Context mContext; + + SliceDeferredSetupCardRendererHelper(Context context) { + mContext = context; + } + + RecyclerView.ViewHolder createViewHolder(View view) { + return new DeferredSetupCardViewHolder(view); + } + + void bindView(RecyclerView.ViewHolder holder, ContextualCard card, Slice slice) { + final DeferredSetupCardViewHolder view = (DeferredSetupCardViewHolder) holder; + final SliceMetadata sliceMetadata = SliceMetadata.from(mContext, slice); + final SliceAction primaryAction = sliceMetadata.getPrimaryAction(); + view.icon.setImageDrawable(primaryAction.getIcon().loadDrawable(mContext)); + view.title.setText(primaryAction.getTitle()); + view.summary.setText(sliceMetadata.getSubtitle()); + view.button.setOnClickListener(v -> { + try { + primaryAction.getAction().send(); + } catch (PendingIntent.CanceledException e) { + Log.w(TAG, "Failed to start intent " + primaryAction.getTitle()); + } + final String log = ContextualCardLogUtils.buildCardClickLog(card, 0 /* row */, + EventInfo.ACTION_TYPE_CONTENT, view.getAdapterPosition()); + + final MetricsFeatureProvider metricsFeatureProvider = + FeatureFactory.getFactory(mContext).getMetricsFeatureProvider(); + + metricsFeatureProvider.action(mContext, + SettingsEnums.ACTION_CONTEXTUAL_CARD_CLICK, log); + }); + } + + static class DeferredSetupCardViewHolder extends RecyclerView.ViewHolder { + public final LinearLayout content; + public final ImageView icon; + public final TextView title; + public final TextView summary; + public final Button button; + + public DeferredSetupCardViewHolder(View itemView) { + super(itemView); + content = itemView.findViewById(R.id.content); + icon = itemView.findViewById(android.R.id.icon); + title = itemView.findViewById(android.R.id.title); + summary = itemView.findViewById(android.R.id.summary); + button = itemView.findViewById(R.id.finish_setup); + } + } +}
\ No newline at end of file diff --git a/src/com/android/settings/homepage/contextualcards/slices/SliceFullCardRendererHelper.java b/src/com/android/settings/homepage/contextualcards/slices/SliceFullCardRendererHelper.java new file mode 100644 index 0000000000..3f35fb5ba1 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/slices/SliceFullCardRendererHelper.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2019 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.homepage.contextualcards.slices; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.view.View; + +import androidx.recyclerview.widget.RecyclerView; +import androidx.slice.Slice; +import androidx.slice.widget.SliceView; + +import com.android.settings.R; +import com.android.settings.homepage.contextualcards.ContextualCard; +import com.android.settings.homepage.contextualcards.ContextualCardFeatureProvider; +import com.android.settings.homepage.contextualcards.logging.ContextualCardLogUtils; +import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; + +/** + * Card renderer helper for {@link ContextualCard} built as slice full card. + */ +class SliceFullCardRendererHelper { + + private final Context mContext; + + SliceFullCardRendererHelper(Context context) { + mContext = context; + } + + RecyclerView.ViewHolder createViewHolder(View view) { + return new SliceViewHolder(view); + } + + void bindView(RecyclerView.ViewHolder holder, ContextualCard card, Slice slice) { + final SliceViewHolder cardHolder = (SliceViewHolder) holder; + cardHolder.sliceView.setScrollable(false); + cardHolder.sliceView.setTag(card.getSliceUri()); + //TODO(b/114009676): We will soon have a field to decide what slice mode we should set. + cardHolder.sliceView.setMode(SliceView.MODE_LARGE); + cardHolder.sliceView.setSlice(slice); + // Set this listener so we can log the interaction users make on the slice + cardHolder.sliceView.setOnSliceActionListener( + (eventInfo, sliceItem) -> { + final String log = ContextualCardLogUtils.buildCardClickLog(card, eventInfo.rowIndex, + eventInfo.actionType, cardHolder.getAdapterPosition()); + + final MetricsFeatureProvider metricsFeatureProvider = + FeatureFactory.getFactory(mContext).getMetricsFeatureProvider(); + + metricsFeatureProvider.action(mContext, + SettingsEnums.ACTION_CONTEXTUAL_CARD_CLICK, log); + + final ContextualCardFeatureProvider contextualCardFeatureProvider = + FeatureFactory.getFactory(mContext).getContextualCardFeatureProvider( + mContext); + + contextualCardFeatureProvider.logNotificationPackage(slice); + }); + + // Customize slice view for Settings + cardHolder.sliceView.setShowTitleItems(true); + if (card.isLargeCard()) { + cardHolder.sliceView.setShowHeaderDivider(true); + cardHolder.sliceView.setShowActionDividers(true); + } + } + + static class SliceViewHolder extends RecyclerView.ViewHolder { + public final SliceView sliceView; + + public SliceViewHolder(View view) { + super(view); + sliceView = view.findViewById(R.id.slice_view); + } + } +} diff --git a/src/com/android/settings/homepage/contextualcards/slices/SliceHalfCardRendererHelper.java b/src/com/android/settings/homepage/contextualcards/slices/SliceHalfCardRendererHelper.java new file mode 100644 index 0000000000..f7745423fb --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/slices/SliceHalfCardRendererHelper.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2019 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.homepage.contextualcards.slices; + +import android.app.PendingIntent; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.util.Log; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.recyclerview.widget.RecyclerView; +import androidx.slice.Slice; +import androidx.slice.SliceMetadata; +import androidx.slice.core.SliceAction; +import androidx.slice.widget.EventInfo; + +import com.android.settings.R; +import com.android.settings.homepage.contextualcards.ContextualCard; +import com.android.settings.homepage.contextualcards.logging.ContextualCardLogUtils; +import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; + +/** + * Card renderer helper for {@link ContextualCard} built as slice half card. + */ +class SliceHalfCardRendererHelper { + private static final String TAG = "SliceHCRendererHelper"; + + private final Context mContext; + + SliceHalfCardRendererHelper(Context context) { + mContext = context; + } + + RecyclerView.ViewHolder createViewHolder(View view) { + return new HalfCardViewHolder(view); + } + + void bindView(RecyclerView.ViewHolder holder, ContextualCard card, Slice slice) { + final HalfCardViewHolder view = (HalfCardViewHolder) holder; + final SliceMetadata sliceMetadata = SliceMetadata.from(mContext, slice); + final SliceAction primaryAction = sliceMetadata.getPrimaryAction(); + view.icon.setImageDrawable(primaryAction.getIcon().loadDrawable(mContext)); + view.title.setText(primaryAction.getTitle()); + view.content.setOnClickListener(v -> { + try { + primaryAction.getAction().send(); + } catch (PendingIntent.CanceledException e) { + Log.w(TAG, "Failed to start intent " + primaryAction.getTitle()); + } + final String log = ContextualCardLogUtils.buildCardClickLog(card, 0 /* row */, + EventInfo.ACTION_TYPE_CONTENT, view.getAdapterPosition()); + + final MetricsFeatureProvider metricsFeatureProvider = + FeatureFactory.getFactory(mContext).getMetricsFeatureProvider(); + + metricsFeatureProvider.action(mContext, + SettingsEnums.ACTION_CONTEXTUAL_CARD_CLICK, log); + }); + } + + static class HalfCardViewHolder extends RecyclerView.ViewHolder { + public final LinearLayout content; + public final ImageView icon; + public final TextView title; + + public HalfCardViewHolder(View itemView) { + super(itemView); + content = itemView.findViewById(R.id.content); + icon = itemView.findViewById(android.R.id.icon); + title = itemView.findViewById(android.R.id.title); + } + } +} diff --git a/src/com/android/settings/homepage/contextualcards/slices/SwipeDismissalDelegate.java b/src/com/android/settings/homepage/contextualcards/slices/SwipeDismissalDelegate.java new file mode 100644 index 0000000000..181359047f --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/slices/SwipeDismissalDelegate.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2019 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.homepage.contextualcards.slices; + +import android.graphics.Canvas; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.RecyclerView; + +import com.android.settings.R; +import com.android.settings.homepage.contextualcards.ContextualCard; + +public class SwipeDismissalDelegate extends ItemTouchHelper.Callback { + + private static final String TAG = "SwipeDismissalDelegate"; + + public interface Listener { + void onSwiped(int position); + } + + private final SwipeDismissalDelegate.Listener mListener; + + public SwipeDismissalDelegate(SwipeDismissalDelegate.Listener listener) { + mListener = listener; + } + + /** + * Determine whether the ability to drag or swipe should be enabled or not. + * + * Only allow swipe on {@link ContextualCard} built with view type + * {@link SliceContextualCardRenderer#VIEW_TYPE_FULL_WIDTH} or + * {@link SliceContextualCardRenderer#VIEW_TYPE_HALF_WIDTH}. + * + * When the dismissal view is displayed, the swipe will also be disabled. + */ + @Override + public int getMovementFlags(@NonNull RecyclerView recyclerView, + @NonNull RecyclerView.ViewHolder viewHolder) { + if (viewHolder.getItemViewType() == SliceContextualCardRenderer.VIEW_TYPE_FULL_WIDTH + || viewHolder.getItemViewType() + == SliceContextualCardRenderer.VIEW_TYPE_HALF_WIDTH) {// Here we are making sure + // the current displayed view is the initial view of + // either slice full card or half card, and only allow swipe on these two types. + if (viewHolder.itemView.findViewById(R.id.dismissal_view).getVisibility() + == View.VISIBLE) { + // Disable swiping when we are in the dismissal view + return 0; + } + return makeMovementFlags(0 /*dragFlags*/, + ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT /*swipeFlags*/); + } + return 0; + } + + @Override + public boolean onMove(@NonNull RecyclerView recyclerView, + @NonNull RecyclerView.ViewHolder viewHolder, + @NonNull RecyclerView.ViewHolder target) { + return false; + } + + @Override + public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { + mListener.onSwiped(viewHolder.getAdapterPosition()); + } + + @Override + public void clearView(@NonNull RecyclerView recyclerView, + @NonNull RecyclerView.ViewHolder viewHolder) { + final View view = getSwipeableView(viewHolder); + getDefaultUIUtil().clearView(view); + } + + @Override + public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, + @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, + boolean isCurrentlyActive) { + final View view = getSwipeableView(viewHolder); + final View iconStart = viewHolder.itemView.findViewById(R.id.dismissal_icon_start); + final View iconEnd = viewHolder.itemView.findViewById(R.id.dismissal_icon_end); + + if (dX > 0) { + iconStart.setVisibility(View.VISIBLE); + iconEnd.setVisibility(View.GONE); + } else if (dX < 0) { + iconStart.setVisibility(View.GONE); + iconEnd.setVisibility(View.VISIBLE); + } + getDefaultUIUtil().onDraw(c, recyclerView, view, dX, dY, actionState, isCurrentlyActive); + } + + /** + * Get the foreground view from the {@link android.widget.FrameLayout} as we only swipe + * the foreground out in {@link SwipeDismissalDelegate#onChildDraw} and gets the view + * beneath revealed. + * + * @return The foreground view. + */ + private View getSwipeableView(RecyclerView.ViewHolder viewHolder) { + if (viewHolder.getItemViewType() == SliceContextualCardRenderer.VIEW_TYPE_HALF_WIDTH) { + return ((SliceHalfCardRendererHelper.HalfCardViewHolder) viewHolder).content; + } + return ((SliceFullCardRendererHelper.SliceViewHolder) viewHolder).sliceView; + } +}
\ No newline at end of file diff --git a/src/com/android/settings/inputmethod/AvailableVirtualKeyboardFragment.java b/src/com/android/settings/inputmethod/AvailableVirtualKeyboardFragment.java index 48b0b4ada4..c37d2b5ae5 100644 --- a/src/com/android/settings/inputmethod/AvailableVirtualKeyboardFragment.java +++ b/src/com/android/settings/inputmethod/AvailableVirtualKeyboardFragment.java @@ -16,39 +16,31 @@ package com.android.settings.inputmethod; -import android.annotation.DrawableRes; -import android.annotation.NonNull; -import android.annotation.Nullable; import android.app.Activity; import android.app.admin.DevicePolicyManager; +import android.app.settings.SettingsEnums; import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.pm.ServiceInfo; import android.content.res.Configuration; -import android.graphics.Color; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; import android.os.Bundle; import android.provider.SearchIndexableResource; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodManager; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; import com.android.settings.search.BaseSearchIndexProvider; -import com.android.settings.search.Indexable; -import com.android.settingslib.inputmethod.InputMethodAndSubtypeUtil; +import com.android.settingslib.inputmethod.InputMethodAndSubtypeUtilCompat; import com.android.settingslib.inputmethod.InputMethodPreference; import com.android.settingslib.inputmethod.InputMethodSettingValuesWrapper; +import com.android.settingslib.search.SearchIndexable; import java.text.Collator; import java.util.ArrayList; import java.util.List; +@SearchIndexable public final class AvailableVirtualKeyboardFragment extends SettingsPreferenceFragment - implements InputMethodPreference.OnSavePreferenceListener, Indexable { + implements InputMethodPreference.OnSavePreferenceListener { private final ArrayList<InputMethodPreference> mInputMethodPreferenceList = new ArrayList<>(); private InputMethodSettingValuesWrapper mInputMethodSettingValues; @@ -78,7 +70,7 @@ public final class AvailableVirtualKeyboardFragment extends SettingsPreferenceFr public void onSaveInputMethodPreference(final InputMethodPreference pref) { final boolean hasHardwareKeyboard = getResources().getConfiguration().keyboard == Configuration.KEYBOARD_QWERTY; - InputMethodAndSubtypeUtil.saveInputMethodSubtypeList(this, getContentResolver(), + InputMethodAndSubtypeUtilCompat.saveInputMethodSubtypeList(this, getContentResolver(), mImm.getInputMethodList(), hasHardwareKeyboard); // Update input method settings and preference list. mInputMethodSettingValues.refreshAllInputMethodAndSubtypes(); @@ -89,53 +81,7 @@ public final class AvailableVirtualKeyboardFragment extends SettingsPreferenceFr @Override public int getMetricsCategory() { - return MetricsEvent.ENABLE_VIRTUAL_KEYBOARDS; - } - - @Nullable - private static Drawable loadDrawable(@NonNull final PackageManager packageManager, - @NonNull final String packageName, @DrawableRes final int resId, - @NonNull final ApplicationInfo applicationInfo) { - if (resId == 0) { - return null; - } - try { - return packageManager.getDrawable(packageName, resId, applicationInfo); - } catch (Exception e) { - return null; - } - } - - @NonNull - private static Drawable getInputMethodIcon(@NonNull final PackageManager packageManager, - @NonNull final InputMethodInfo imi) { - final ServiceInfo si = imi.getServiceInfo(); - final ApplicationInfo ai = si != null ? si.applicationInfo : null; - final String packageName = imi.getPackageName(); - if (si == null || ai == null || packageName == null) { - return new ColorDrawable(Color.TRANSPARENT); - } - // We do not use ServiceInfo#loadLogo() and ServiceInfo#loadIcon here since those methods - // internally have some fallback rules, which we want to do manually. - Drawable drawable = loadDrawable(packageManager, packageName, si.logo, ai); - if (drawable != null) { - return drawable; - } - drawable = loadDrawable(packageManager, packageName, si.icon, ai); - if (drawable != null) { - return drawable; - } - // We do not use ApplicationInfo#loadLogo() and ApplicationInfo#loadIcon here since those - // methods internally have some fallback rules, which we want to do manually. - drawable = loadDrawable(packageManager, packageName, ai.logo, ai); - if (drawable != null) { - return drawable; - } - drawable = loadDrawable(packageManager, packageName, ai.icon, ai); - if (drawable != null) { - return drawable; - } - return new ColorDrawable(Color.TRANSPARENT); + return SettingsEnums.ENABLE_VIRTUAL_KEYBOARDS; } private void updateInputMethodPreferenceViews() { @@ -144,7 +90,6 @@ public final class AvailableVirtualKeyboardFragment extends SettingsPreferenceFr mInputMethodPreferenceList.clear(); List<String> permittedList = mDpm.getPermittedInputMethodsForCurrentUser(); final Context context = getPrefContext(); - final PackageManager packageManager = getActivity().getPackageManager(); final List<InputMethodInfo> imis = mInputMethodSettingValues.getInputMethodList(); final int numImis = (imis == null ? 0 : imis.size()); for (int i = 0; i < numImis; ++i) { @@ -153,7 +98,7 @@ public final class AvailableVirtualKeyboardFragment extends SettingsPreferenceFr || permittedList.contains(imi.getPackageName()); final InputMethodPreference pref = new InputMethodPreference( context, imi, true, isAllowedByOrganization, this); - pref.setIcon(getInputMethodIcon(packageManager, imi)); + pref.setIcon(imi.loadIcon(context.getPackageManager())); mInputMethodPreferenceList.add(pref); } final Collator collator = Collator.getInstance(); @@ -163,7 +108,7 @@ public final class AvailableVirtualKeyboardFragment extends SettingsPreferenceFr final InputMethodPreference pref = mInputMethodPreferenceList.get(i); pref.setOrder(i); getPreferenceScreen().addPreference(pref); - InputMethodAndSubtypeUtil.removeUnnecessaryNonPersistentPreference(pref); + InputMethodAndSubtypeUtilCompat.removeUnnecessaryNonPersistentPreference(pref); pref.updatePreferenceViews(); } } diff --git a/src/com/android/settings/inputmethod/GameControllerPreferenceController.java b/src/com/android/settings/inputmethod/GameControllerPreferenceController.java index a635842aa4..80f668069f 100644 --- a/src/com/android/settings/inputmethod/GameControllerPreferenceController.java +++ b/src/com/android/settings/inputmethod/GameControllerPreferenceController.java @@ -19,9 +19,10 @@ package com.android.settings.inputmethod; import android.content.Context; import android.hardware.input.InputManager; import android.provider.Settings; +import android.view.InputDevice; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.view.InputDevice; import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; diff --git a/src/com/android/settings/inputmethod/InputMethodAndSubtypeEnabler.java b/src/com/android/settings/inputmethod/InputMethodAndSubtypeEnabler.java index 37f6413391..38df8d767d 100644 --- a/src/com/android/settings/inputmethod/InputMethodAndSubtypeEnabler.java +++ b/src/com/android/settings/inputmethod/InputMethodAndSubtypeEnabler.java @@ -16,26 +16,37 @@ package com.android.settings.inputmethod; +import android.app.settings.SettingsEnums; +import android.content.Context; import android.content.Intent; import android.os.Bundle; -import androidx.preference.PreferenceScreen; import android.text.TextUtils; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.settings.SettingsPreferenceFragment; -import com.android.settingslib.inputmethod.InputMethodAndSubtypeEnablerManager; +import com.android.settings.R; +import com.android.settings.dashboard.DashboardFragment; -public class InputMethodAndSubtypeEnabler extends SettingsPreferenceFragment { - private InputMethodAndSubtypeEnablerManager mManager; +public class InputMethodAndSubtypeEnabler extends DashboardFragment { + + private static final String TAG = "InputMethodAndSubtypeEnabler"; @Override public int getMetricsCategory() { - return MetricsEvent.INPUTMETHOD_SUBTYPE_ENABLER; + return SettingsEnums.INPUTMETHOD_SUBTYPE_ENABLER; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.input_methods_subtype; + } + + @Override + protected String getLogTag() { + return TAG; } @Override - public void onCreate(final Bundle icicle) { - super.onCreate(icicle); + public void onAttach(Context context) { + super.onAttach(context); // Input method id should be available from an Intent when this preference is launched as a // single Activity (see InputMethodAndSubtypeEnablerActivity). It should be available @@ -44,11 +55,8 @@ public class InputMethodAndSubtypeEnabler extends SettingsPreferenceFragment { final String targetImi = getStringExtraFromIntentOrArguments( android.provider.Settings.EXTRA_INPUT_METHOD_ID); - final PreferenceScreen root = - getPreferenceManager().createPreferenceScreen(getPrefContext()); - mManager = new InputMethodAndSubtypeEnablerManager(this); - mManager.init(this, targetImi, root); - setPreferenceScreen(root); + use(InputMethodAndSubtypePreferenceController.class).initialize(this /* fragment */, + targetImi); } private String getStringExtraFromIntentOrArguments(final String name) { @@ -69,16 +77,4 @@ public class InputMethodAndSubtypeEnabler extends SettingsPreferenceFragment { getActivity().setTitle(title); } } - - @Override - public void onResume() { - super.onResume(); - mManager.refresh(getContext(), this); - } - - @Override - public void onPause() { - super.onPause(); - mManager.save(getContext(), this); - } } diff --git a/src/com/android/settings/inputmethod/InputMethodAndSubtypePreferenceController.java b/src/com/android/settings/inputmethod/InputMethodAndSubtypePreferenceController.java new file mode 100644 index 0000000000..49e07b0be2 --- /dev/null +++ b/src/com/android/settings/inputmethod/InputMethodAndSubtypePreferenceController.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2018 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.inputmethod; + +import android.content.Context; + +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceScreen; + +import com.android.settings.core.BasePreferenceController; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.core.lifecycle.events.OnStop; +import com.android.settingslib.inputmethod.InputMethodAndSubtypeEnablerManagerCompat; + +public class InputMethodAndSubtypePreferenceController extends BasePreferenceController implements + LifecycleObserver, OnStart, OnStop { + + private PreferenceFragmentCompat mFragment; + private InputMethodAndSubtypeEnablerManagerCompat mManager; + private String mTargetImi; + + public InputMethodAndSubtypePreferenceController(Context context, String key) { + super(context, key); + } + + public void initialize(PreferenceFragmentCompat fragment, String imi) { + mFragment = fragment; + mTargetImi = imi; + mManager = new InputMethodAndSubtypeEnablerManagerCompat(mFragment); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mManager.init(mFragment, mTargetImi, screen); + } + + @Override + public void onStart() { + mManager.refresh(mContext, mFragment); + } + + @Override + public void onStop() { + mManager.save(mContext, mFragment); + } +} diff --git a/src/com/android/settings/inputmethod/KeyboardLayoutDialogFragment.java b/src/com/android/settings/inputmethod/KeyboardLayoutDialogFragment.java index e100dd2dc9..f91c9d81ca 100644 --- a/src/com/android/settings/inputmethod/KeyboardLayoutDialogFragment.java +++ b/src/com/android/settings/inputmethod/KeyboardLayoutDialogFragment.java @@ -17,15 +17,11 @@ package com.android.settings.inputmethod; import android.app.Activity; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.DialogFragment; -import android.app.LoaderManager.LoaderCallbacks; -import android.content.AsyncTaskLoader; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; -import android.content.Loader; import android.hardware.input.InputDeviceIdentifier; import android.hardware.input.InputManager; import android.hardware.input.InputManager.InputDeviceListener; @@ -40,7 +36,11 @@ import android.widget.CheckedTextView; import android.widget.RadioButton; import android.widget.TextView; -import com.android.internal.logging.nano.MetricsProto; +import androidx.appcompat.app.AlertDialog; +import androidx.loader.app.LoaderManager.LoaderCallbacks; +import androidx.loader.content.AsyncTaskLoader; +import androidx.loader.content.Loader; + import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; @@ -66,7 +66,7 @@ public class KeyboardLayoutDialogFragment extends InstrumentedDialogFragment @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DIALOG_KEYBOARD_LAYOUT; + return SettingsEnums.DIALOG_KEYBOARD_LAYOUT; } @Override @@ -157,7 +157,7 @@ public class KeyboardLayoutDialogFragment extends InstrumentedDialogFragment @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); - show(getActivity().getFragmentManager(), "layout"); + show(getActivity().getSupportFragmentManager(), "layout"); } private void onKeyboardLayoutClicked(int which) { @@ -215,7 +215,7 @@ public class KeyboardLayoutDialogFragment extends InstrumentedDialogFragment private void updateSwitchHintVisibility() { AlertDialog dialog = (AlertDialog)getDialog(); if (dialog != null) { - View customPanel = dialog.findViewById(com.android.internal.R.id.customPanel); + View customPanel = dialog.findViewById(R.id.customPanel); customPanel.setVisibility(mAdapter.getCount() > 1 ? View.VISIBLE : View.GONE); } } diff --git a/src/com/android/settings/inputmethod/KeyboardLayoutPickerController.java b/src/com/android/settings/inputmethod/KeyboardLayoutPickerController.java new file mode 100644 index 0000000000..4ac52bc1af --- /dev/null +++ b/src/com/android/settings/inputmethod/KeyboardLayoutPickerController.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2018 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.inputmethod; + + +import android.content.Context; +import android.hardware.input.InputDeviceIdentifier; +import android.hardware.input.InputManager; +import android.hardware.input.KeyboardLayout; +import android.view.InputDevice; + +import androidx.fragment.app.Fragment; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreference; + +import com.android.settings.core.BasePreferenceController; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.core.lifecycle.events.OnStop; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + + +public class KeyboardLayoutPickerController extends BasePreferenceController implements + InputManager.InputDeviceListener, LifecycleObserver, OnStart, OnStop { + + private final InputManager mIm; + private final Map<SwitchPreference, KeyboardLayout> mPreferenceMap; + + private Fragment mParent; + private int mInputDeviceId; + private InputDeviceIdentifier mInputDeviceIdentifier; + private KeyboardLayout[] mKeyboardLayouts; + private PreferenceScreen mScreen; + + + public KeyboardLayoutPickerController(Context context, String key) { + super(context, key); + mIm = (InputManager) context.getSystemService(Context.INPUT_SERVICE); + mInputDeviceId = -1; + mPreferenceMap = new HashMap<>(); + } + + public void initialize(Fragment parent, InputDeviceIdentifier inputDeviceIdentifier) { + mParent = parent; + mInputDeviceIdentifier = inputDeviceIdentifier; + mKeyboardLayouts = mIm.getKeyboardLayoutsForInputDevice(mInputDeviceIdentifier); + Arrays.sort(mKeyboardLayouts); + } + + @Override + public void onStart() { + mIm.registerInputDeviceListener(this, null); + + final InputDevice inputDevice = + mIm.getInputDeviceByDescriptor(mInputDeviceIdentifier.getDescriptor()); + if (inputDevice == null) { + mParent.getActivity().finish(); + return; + } + mInputDeviceId = inputDevice.getId(); + + updateCheckedState(); + } + + @Override + public void onStop() { + mIm.unregisterInputDeviceListener(this); + mInputDeviceId = -1; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mScreen = screen; + createPreferenceHierarchy(); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } + + @Override + public boolean handlePreferenceTreeClick(Preference preference) { + if (!(preference instanceof SwitchPreference)) { + return false; + } + + final SwitchPreference switchPref = (SwitchPreference) preference; + final KeyboardLayout layout = mPreferenceMap.get(switchPref); + if (layout != null) { + final boolean checked = switchPref.isChecked(); + if (checked) { + mIm.addKeyboardLayoutForInputDevice(mInputDeviceIdentifier, + layout.getDescriptor()); + } else { + mIm.removeKeyboardLayoutForInputDevice(mInputDeviceIdentifier, + layout.getDescriptor()); + } + } + return true; + } + + @Override + public void onInputDeviceAdded(int deviceId) { + + } + + @Override + public void onInputDeviceRemoved(int deviceId) { + if (mInputDeviceId >= 0 && deviceId == mInputDeviceId) { + mParent.getActivity().finish(); + } + } + + @Override + public void onInputDeviceChanged(int deviceId) { + if (mInputDeviceId >= 0 && deviceId == mInputDeviceId) { + updateCheckedState(); + } + } + + private void updateCheckedState() { + final String[] enabledKeyboardLayouts = mIm.getEnabledKeyboardLayoutsForInputDevice( + mInputDeviceIdentifier); + Arrays.sort(enabledKeyboardLayouts); + + for (Map.Entry<SwitchPreference, KeyboardLayout> entry : mPreferenceMap.entrySet()) { + entry.getKey().setChecked(Arrays.binarySearch(enabledKeyboardLayouts, + entry.getValue().getDescriptor()) >= 0); + } + } + + private void createPreferenceHierarchy() { + for (KeyboardLayout layout : mKeyboardLayouts) { + final SwitchPreference pref = new SwitchPreference(mScreen.getContext()); + pref.setTitle(layout.getLabel()); + pref.setSummary(layout.getCollection()); + pref.setKey(layout.getDescriptor()); + mScreen.addPreference(pref); + mPreferenceMap.put(pref, layout); + } + } +} + diff --git a/src/com/android/settings/inputmethod/KeyboardLayoutPickerFragment.java b/src/com/android/settings/inputmethod/KeyboardLayoutPickerFragment.java index 47625e768b..a13ebc00ff 100644 --- a/src/com/android/settings/inputmethod/KeyboardLayoutPickerFragment.java +++ b/src/com/android/settings/inputmethod/KeyboardLayoutPickerFragment.java @@ -16,31 +16,17 @@ package com.android.settings.inputmethod; +import android.app.settings.SettingsEnums; import android.content.Context; import android.hardware.input.InputDeviceIdentifier; -import android.hardware.input.InputManager; -import android.hardware.input.InputManager.InputDeviceListener; -import android.hardware.input.KeyboardLayout; -import android.os.Bundle; -import androidx.preference.CheckBoxPreference; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; -import android.view.InputDevice; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.settings.SettingsPreferenceFragment; +import com.android.settings.R; +import com.android.settings.dashboard.DashboardFragment; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -public class KeyboardLayoutPickerFragment extends SettingsPreferenceFragment - implements InputDeviceListener { - private InputDeviceIdentifier mInputDeviceIdentifier; - private int mInputDeviceId = -1; - private InputManager mIm; - private KeyboardLayout[] mKeyboardLayouts; - private HashMap<CheckBoxPreference, KeyboardLayout> mPreferenceMap = new HashMap<>(); +public class KeyboardLayoutPickerFragment extends DashboardFragment { + + private static final String TAG = "KeyboardLayoutPicker"; /** * Intent extra: The input device descriptor of the keyboard whose keyboard @@ -50,109 +36,29 @@ public class KeyboardLayoutPickerFragment extends SettingsPreferenceFragment @Override public int getMetricsCategory() { - return MetricsEvent.INPUTMETHOD_KEYBOARD; - } - - @Override - public void onCreate(Bundle icicle) { - super.onCreate(icicle); - - mInputDeviceIdentifier = getActivity().getIntent().getParcelableExtra( - EXTRA_INPUT_DEVICE_IDENTIFIER); - if (mInputDeviceIdentifier == null) { - getActivity().finish(); - } - - mIm = (InputManager) getSystemService(Context.INPUT_SERVICE); - mKeyboardLayouts = mIm.getKeyboardLayoutsForInputDevice(mInputDeviceIdentifier); - Arrays.sort(mKeyboardLayouts); - setPreferenceScreen(createPreferenceHierarchy()); + return SettingsEnums.INPUTMETHOD_KEYBOARD; } @Override - public void onResume() { - super.onResume(); + public void onAttach(Context context) { + super.onAttach(context); - mIm.registerInputDeviceListener(this, null); - - InputDevice inputDevice = - mIm.getInputDeviceByDescriptor(mInputDeviceIdentifier.getDescriptor()); - if (inputDevice == null) { + final InputDeviceIdentifier inputDeviceIdentifier = getActivity().getIntent(). + getParcelableExtra(EXTRA_INPUT_DEVICE_IDENTIFIER); + if (inputDeviceIdentifier == null) { getActivity().finish(); - return; - } - mInputDeviceId = inputDevice.getId(); - - updateCheckedState(); - } - - @Override - public void onPause() { - mIm.unregisterInputDeviceListener(this); - mInputDeviceId = -1; - - super.onPause(); - } - - @Override - public boolean onPreferenceTreeClick(Preference preference) { - if (preference instanceof CheckBoxPreference) { - CheckBoxPreference checkboxPref = (CheckBoxPreference)preference; - KeyboardLayout layout = mPreferenceMap.get(checkboxPref); - if (layout != null) { - boolean checked = checkboxPref.isChecked(); - if (checked) { - mIm.addKeyboardLayoutForInputDevice(mInputDeviceIdentifier, - layout.getDescriptor()); - } else { - mIm.removeKeyboardLayoutForInputDevice(mInputDeviceIdentifier, - layout.getDescriptor()); - } - return true; - } } - return super.onPreferenceTreeClick(preference); - } - - @Override - public void onInputDeviceAdded(int deviceId) { - } - @Override - public void onInputDeviceChanged(int deviceId) { - if (mInputDeviceId >= 0 && deviceId == mInputDeviceId) { - updateCheckedState(); - } + use(KeyboardLayoutPickerController.class).initialize(this /*parent*/, + inputDeviceIdentifier); } @Override - public void onInputDeviceRemoved(int deviceId) { - if (mInputDeviceId >= 0 && deviceId == mInputDeviceId) { - getActivity().finish(); - } - } - - private PreferenceScreen createPreferenceHierarchy() { - PreferenceScreen root = getPreferenceManager().createPreferenceScreen(getActivity()); - - for (KeyboardLayout layout : mKeyboardLayouts) { - CheckBoxPreference pref = new CheckBoxPreference(getPrefContext()); - pref.setTitle(layout.getLabel()); - pref.setSummary(layout.getCollection()); - root.addPreference(pref); - mPreferenceMap.put(pref, layout); - } - return root; + protected String getLogTag() { + return TAG; } - private void updateCheckedState() { - String[] enabledKeyboardLayouts = mIm.getEnabledKeyboardLayoutsForInputDevice( - mInputDeviceIdentifier); - Arrays.sort(enabledKeyboardLayouts); - - for (Map.Entry<CheckBoxPreference, KeyboardLayout> entry : mPreferenceMap.entrySet()) { - entry.getKey().setChecked(Arrays.binarySearch(enabledKeyboardLayouts, - entry.getValue().getDescriptor()) >= 0); - } + protected int getPreferenceScreenResId() { + return R.xml.keyboard_layout_picker_fragment; } } diff --git a/src/com/android/settings/inputmethod/PhysicalKeyboardFragment.java b/src/com/android/settings/inputmethod/PhysicalKeyboardFragment.java index cc2e8aa82f..119571dc1c 100644 --- a/src/com/android/settings/inputmethod/PhysicalKeyboardFragment.java +++ b/src/com/android/settings/inputmethod/PhysicalKeyboardFragment.java @@ -19,6 +19,7 @@ package com.android.settings.inputmethod; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.Activity; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; import android.database.ContentObserver; @@ -30,34 +31,34 @@ import android.os.Handler; import android.os.UserHandle; import android.provider.SearchIndexableResource; import android.provider.Settings.Secure; -import androidx.preference.SwitchPreference; +import android.text.TextUtils; +import android.view.InputDevice; + import androidx.preference.Preference; import androidx.preference.Preference.OnPreferenceChangeListener; import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceScreen; -import android.text.TextUtils; -import android.view.InputDevice; +import androidx.preference.SwitchPreference; -import com.android.internal.inputmethod.InputMethodUtils; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.util.Preconditions; import com.android.settings.R; import com.android.settings.Settings; import com.android.settings.SettingsPreferenceFragment; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; +import com.android.settingslib.search.SearchIndexable; import com.android.settingslib.utils.ThreadUtils; import java.text.Collator; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; import java.util.List; import java.util.Objects; +@SearchIndexable public final class PhysicalKeyboardFragment extends SettingsPreferenceFragment implements InputManager.InputDeviceListener, - KeyboardLayoutDialogFragment.OnSetupKeyboardLayoutsListener, Indexable { + KeyboardLayoutDialogFragment.OnSetupKeyboardLayoutsListener { private static final String KEYBOARD_ASSISTANCE_CATEGORY = "keyboard_assistance_category"; private static final String SHOW_VIRTUAL_KEYBOARD_SWITCH = "show_virtual_keyboard_switch"; @@ -71,8 +72,6 @@ public final class PhysicalKeyboardFragment extends SettingsPreferenceFragment private PreferenceCategory mKeyboardAssistanceCategory; @NonNull private SwitchPreference mShowVirtualKeyboardSwitch; - @NonNull - private InputMethodUtils.InputMethodSettings mSettings; private Intent mIntentWaitingForResult; @@ -81,13 +80,6 @@ public final class PhysicalKeyboardFragment extends SettingsPreferenceFragment Activity activity = Preconditions.checkNotNull(getActivity()); addPreferencesFromResource(R.xml.physical_keyboard_settings); mIm = Preconditions.checkNotNull(activity.getSystemService(InputManager.class)); - mSettings = new InputMethodUtils.InputMethodSettings( - activity.getResources(), - getContentResolver(), - new HashMap<>(), - new ArrayList<>(), - UserHandle.myUserId(), - false /* copyOnWrite */); mKeyboardAssistanceCategory = Preconditions.checkNotNull( (PreferenceCategory) findPreference(KEYBOARD_ASSISTANCE_CATEGORY)); mShowVirtualKeyboardSwitch = Preconditions.checkNotNull( @@ -140,7 +132,7 @@ public final class PhysicalKeyboardFragment extends SettingsPreferenceFragment @Override public int getMetricsCategory() { - return MetricsEvent.PHYSICAL_KEYBOARDS; + return SettingsEnums.PHYSICAL_KEYBOARDS; } private void scheduleUpdateHardKeyboards() { @@ -190,7 +182,7 @@ public final class PhysicalKeyboardFragment extends SettingsPreferenceFragment KeyboardLayoutDialogFragment fragment = new KeyboardLayoutDialogFragment( inputDeviceIdentifier); fragment.setTargetFragment(this, 0); - fragment.show(getActivity().getFragmentManager(), "keyboardLayout"); + fragment.show(getActivity().getSupportFragmentManager(), "keyboardLayout"); } private void registerShowVirtualKeyboardSettingsObserver() { @@ -208,7 +200,8 @@ public final class PhysicalKeyboardFragment extends SettingsPreferenceFragment } private void updateShowVirtualKeyboardSwitch() { - mShowVirtualKeyboardSwitch.setChecked(mSettings.isShowImeWithHardKeyboardEnabled()); + mShowVirtualKeyboardSwitch.setChecked( + Secure.getInt(getContentResolver(), Secure.SHOW_IME_WITH_HARD_KEYBOARD, 0) != 0); } private void toggleKeyboardShortcutsMenu() { @@ -216,12 +209,10 @@ public final class PhysicalKeyboardFragment extends SettingsPreferenceFragment } private final OnPreferenceChangeListener mShowVirtualKeyboardSwitchPreferenceChangeListener = - new OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - mSettings.setShowImeWithHardKeyboard((Boolean) newValue); - return true; - } + (preference, newValue) -> { + Secure.putInt(getContentResolver(), Secure.SHOW_IME_WITH_HARD_KEYBOARD, + ((Boolean) newValue) ? 1 : 0); + return true; }; private final ContentObserver mContentObserver = new ContentObserver(new Handler(true)) { diff --git a/src/com/android/settings/inputmethod/PhysicalKeyboardPreferenceController.java b/src/com/android/settings/inputmethod/PhysicalKeyboardPreferenceController.java index 38937de3b8..367ea80e07 100644 --- a/src/com/android/settings/inputmethod/PhysicalKeyboardPreferenceController.java +++ b/src/com/android/settings/inputmethod/PhysicalKeyboardPreferenceController.java @@ -18,6 +18,8 @@ package com.android.settings.inputmethod; import android.content.Context; import android.hardware.input.InputManager; +import android.icu.text.ListFormatter; + import androidx.preference.Preference; import com.android.settings.R; @@ -29,8 +31,10 @@ import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnPause; import com.android.settingslib.core.lifecycle.events.OnResume; +import java.util.ArrayList; import java.util.List; + public class PhysicalKeyboardPreferenceController extends AbstractPreferenceController implements PreferenceControllerMixin, LifecycleObserver, OnResume, OnPause, InputManager.InputDeviceListener { @@ -96,18 +100,13 @@ public class PhysicalKeyboardPreferenceController extends AbstractPreferenceCont final List<HardKeyboardDeviceInfo> keyboards = PhysicalKeyboardFragment.getHardKeyboards(mContext); if (keyboards.isEmpty()) { - mPreference.setSummary(R.string.disconnected); + mPreference.setSummary(R.string.keyboard_disconnected); return; } - String summary = null; + final List<String> summaries = new ArrayList<>(); for (HardKeyboardDeviceInfo info : keyboards) { - if (summary == null) { - summary = info.mDeviceName; - } else { - summary = mContext.getString(R.string.join_many_items_middle, summary, - info.mDeviceName); - } + summaries.add(info.mDeviceName); } - mPreference.setSummary(summary); + mPreference.setSummary(ListFormatter.getInstance().format(summaries)); } } diff --git a/src/com/android/settings/inputmethod/SpellCheckerForWorkPreferenceController.java b/src/com/android/settings/inputmethod/SpellCheckerForWorkPreferenceController.java new file mode 100644 index 0000000000..327af5b2bb --- /dev/null +++ b/src/com/android/settings/inputmethod/SpellCheckerForWorkPreferenceController.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2019 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.inputmethod; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.view.inputmethod.InputMethodSystemProperty; + +import com.android.settings.R; +import com.android.settings.core.WorkProfilePreferenceController; + +/** + * Preference controller for "Spell checker for work". + * + * @see SpellCheckerPreferenceController + */ +public final class SpellCheckerForWorkPreferenceController extends WorkProfilePreferenceController { + + public SpellCheckerForWorkPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + protected int getSourceMetricsCategory() { + return SettingsEnums.SETTINGS_LANGUAGE_CATEGORY; + } + + @AvailabilityStatus + @Override + public int getAvailabilityStatus() { + if (!mContext.getResources().getBoolean(R.bool.config_show_spellcheckers_settings) + || !InputMethodSystemProperty.PER_PROFILE_IME_ENABLED) { + return UNSUPPORTED_ON_DEVICE; + } + return super.getAvailabilityStatus(); + } +} diff --git a/src/com/android/settings/inputmethod/SpellCheckerPreference.java b/src/com/android/settings/inputmethod/SpellCheckerPreference.java index 62874266bf..116f1c7b18 100644 --- a/src/com/android/settings/inputmethod/SpellCheckerPreference.java +++ b/src/com/android/settings/inputmethod/SpellCheckerPreference.java @@ -16,17 +16,18 @@ package com.android.settings.inputmethod; -import android.app.AlertDialog.Builder; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; -import androidx.preference.PreferenceViewHolder; import android.text.TextUtils; import android.view.View; import android.view.View.OnClickListener; import android.view.textservice.SpellCheckerInfo; +import androidx.appcompat.app.AlertDialog.Builder; +import androidx.preference.PreferenceViewHolder; + import com.android.settings.CustomListPreference; import com.android.settings.R; diff --git a/src/com/android/settings/inputmethod/SpellCheckerPreferenceController.java b/src/com/android/settings/inputmethod/SpellCheckerPreferenceController.java index 8adf238977..f0bb8d9828 100644 --- a/src/com/android/settings/inputmethod/SpellCheckerPreferenceController.java +++ b/src/com/android/settings/inputmethod/SpellCheckerPreferenceController.java @@ -17,15 +17,16 @@ package com.android.settings.inputmethod; import android.content.Context; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; import android.view.textservice.SpellCheckerInfo; import android.view.textservice.TextServicesManager; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.core.AbstractPreferenceController; -import com.android.settingslib.inputmethod.InputMethodAndSubtypeUtil; +import com.android.settingslib.inputmethod.InputMethodAndSubtypeUtilCompat; public class SpellCheckerPreferenceController extends AbstractPreferenceController implements PreferenceControllerMixin { @@ -45,7 +46,7 @@ public class SpellCheckerPreferenceController extends AbstractPreferenceControll super.displayPreference(screen); final Preference preference = screen.findPreference(KEY_SPELL_CHECKERS); if (preference != null) { - InputMethodAndSubtypeUtil.removeUnnecessaryNonPersistentPreference(preference); + InputMethodAndSubtypeUtilCompat.removeUnnecessaryNonPersistentPreference(preference); } } diff --git a/src/com/android/settings/inputmethod/SpellCheckersSettings.java b/src/com/android/settings/inputmethod/SpellCheckersSettings.java index 3f38704de6..974f2c41ea 100644 --- a/src/com/android/settings/inputmethod/SpellCheckersSettings.java +++ b/src/com/android/settings/inputmethod/SpellCheckersSettings.java @@ -16,23 +16,24 @@ package com.android.settings.inputmethod; -import android.app.AlertDialog; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.DialogInterface; import android.content.pm.ApplicationInfo; import android.os.Bundle; import android.provider.Settings; -import androidx.preference.Preference; -import androidx.preference.Preference.OnPreferenceChangeListener; -import androidx.preference.Preference.OnPreferenceClickListener; -import androidx.preference.PreferenceScreen; import android.util.Log; import android.view.textservice.SpellCheckerInfo; import android.view.textservice.SpellCheckerSubtype; import android.view.textservice.TextServicesManager; import android.widget.Switch; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.appcompat.app.AlertDialog; +import androidx.preference.Preference; +import androidx.preference.Preference.OnPreferenceChangeListener; +import androidx.preference.Preference.OnPreferenceClickListener; +import androidx.preference.PreferenceScreen; + import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.SettingsPreferenceFragment; @@ -57,7 +58,7 @@ public class SpellCheckersSettings extends SettingsPreferenceFragment @Override public int getMetricsCategory() { - return MetricsEvent.INPUTMETHOD_SPELL_CHECKERS; + return SettingsEnums.INPUTMETHOD_SPELL_CHECKERS; } @Override diff --git a/src/com/android/settings/inputmethod/UserDictionaryAddWordContents.java b/src/com/android/settings/inputmethod/UserDictionaryAddWordContents.java index 620bc65281..716226db4c 100644 --- a/src/com/android/settings/inputmethod/UserDictionaryAddWordContents.java +++ b/src/com/android/settings/inputmethod/UserDictionaryAddWordContents.java @@ -60,8 +60,8 @@ public class UserDictionaryAddWordContents { private String mSavedShortcut; /* package */ UserDictionaryAddWordContents(final View view, final Bundle args) { - mWordEditText = (EditText)view.findViewById(R.id.user_dictionary_add_word_text); - mShortcutEditText = (EditText)view.findViewById(R.id.user_dictionary_add_shortcut); + mWordEditText = (EditText) view.findViewById(R.id.user_dictionary_add_word_text); + mShortcutEditText = (EditText) view.findViewById(R.id.user_dictionary_add_shortcut); final String word = args.getString(EXTRA_WORD); if (null != word) { mWordEditText.setText(word); @@ -81,8 +81,8 @@ public class UserDictionaryAddWordContents { /* package */ UserDictionaryAddWordContents(final View view, final UserDictionaryAddWordContents oldInstanceToBeEdited) { - mWordEditText = (EditText)view.findViewById(R.id.user_dictionary_add_word_text); - mShortcutEditText = (EditText)view.findViewById(R.id.user_dictionary_add_shortcut); + mWordEditText = (EditText) view.findViewById(R.id.user_dictionary_add_word_text); + mShortcutEditText = (EditText) view.findViewById(R.id.user_dictionary_add_shortcut); mMode = MODE_EDIT; mOldWord = oldInstanceToBeEdited.mSavedWord; mOldShortcut = oldInstanceToBeEdited.mSavedShortcut; @@ -167,23 +167,24 @@ public class UserDictionaryAddWordContents { return UserDictionaryAddWordActivity.CODE_WORD_ADDED; } - private static final String[] HAS_WORD_PROJECTION = { UserDictionary.Words.WORD }; + private static final String[] HAS_WORD_PROJECTION = {UserDictionary.Words.WORD}; private static final String HAS_WORD_SELECTION_ONE_LOCALE = UserDictionary.Words.WORD + "=? AND " + UserDictionary.Words.LOCALE + "=?"; private static final String HAS_WORD_SELECTION_ALL_LOCALES = UserDictionary.Words.WORD + "=? AND " + UserDictionary.Words.LOCALE + " is null"; + private boolean hasWord(final String word, final Context context) { final Cursor cursor; // mLocale == "" indicates this is an entry for all languages. Here, mLocale can't // be null at all (it's ensured by the updateLocale method). if ("".equals(mLocale)) { cursor = context.getContentResolver().query(UserDictionary.Words.CONTENT_URI, - HAS_WORD_PROJECTION, HAS_WORD_SELECTION_ALL_LOCALES, - new String[] { word }, null /* sort order */); + HAS_WORD_PROJECTION, HAS_WORD_SELECTION_ALL_LOCALES, + new String[] {word}, null /* sort order */); } else { cursor = context.getContentResolver().query(UserDictionary.Words.CONTENT_URI, - HAS_WORD_PROJECTION, HAS_WORD_SELECTION_ONE_LOCALE, - new String[] { word, mLocale }, null /* sort order */); + HAS_WORD_PROJECTION, HAS_WORD_SELECTION_ONE_LOCALE, + new String[] {word, mLocale}, null /* sort order */); } try { if (null == cursor) return false; @@ -196,6 +197,7 @@ public class UserDictionaryAddWordContents { public static class LocaleRenderer { private final String mLocaleString; private final String mDescription; + // LocaleString may NOT be null. public LocaleRenderer(final Context context, final String localeString) { mLocaleString = localeString; @@ -207,13 +209,16 @@ public class UserDictionaryAddWordContents { mDescription = Utils.createLocaleFromString(localeString).getDisplayName(); } } + @Override public String toString() { return mDescription; } + public String getLocaleString() { return mLocaleString; } + // "More languages..." is null ; "All languages" is the empty string. public boolean isMoreLanguages() { return null == mLocaleString; @@ -229,7 +234,8 @@ public class UserDictionaryAddWordContents { // Helper method to get the list of locales to display for this word public ArrayList<LocaleRenderer> getLocalesList(final Activity activity) { - final TreeSet<String> locales = UserDictionaryList.getUserDictionaryLocalesSet(activity); + final TreeSet<String> locales = + UserDictionaryListPreferenceController.getUserDictionaryLocalesSet(activity); // Remove our locale if it's in, because we're always gonna put it at the top locales.remove(mLocale); // mLocale may not be null final String systemLocale = Locale.getDefault().toString(); diff --git a/src/com/android/settings/inputmethod/UserDictionaryAddWordFragment.java b/src/com/android/settings/inputmethod/UserDictionaryAddWordFragment.java index fd33051a08..dc2aa49f62 100644 --- a/src/com/android/settings/inputmethod/UserDictionaryAddWordFragment.java +++ b/src/com/android/settings/inputmethod/UserDictionaryAddWordFragment.java @@ -16,6 +16,7 @@ package com.android.settings.inputmethod; +import android.app.settings.SettingsEnums; import android.os.Bundle; import android.view.LayoutInflater; import android.view.Menu; @@ -25,7 +26,6 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.core.InstrumentedFragment; import com.android.settings.inputmethod.UserDictionaryAddWordContents.LocaleRenderer; @@ -105,7 +105,7 @@ public class UserDictionaryAddWordFragment extends InstrumentedFragment { @Override public int getMetricsCategory() { - return MetricsEvent.INPUTMETHOD_USER_DICTIONARY_ADD_WORD; + return SettingsEnums.INPUTMETHOD_USER_DICTIONARY_ADD_WORD; } @Override diff --git a/src/com/android/settings/inputmethod/UserDictionaryCursorLoader.java b/src/com/android/settings/inputmethod/UserDictionaryCursorLoader.java index 3f1122a58c..e0c016f121 100644 --- a/src/com/android/settings/inputmethod/UserDictionaryCursorLoader.java +++ b/src/com/android/settings/inputmethod/UserDictionaryCursorLoader.java @@ -17,13 +17,14 @@ package com.android.settings.inputmethod; import android.content.Context; -import android.content.CursorLoader; import android.database.Cursor; import android.database.MatrixCursor; import android.provider.UserDictionary; -import androidx.annotation.VisibleForTesting; import android.util.ArraySet; +import androidx.annotation.VisibleForTesting; +import androidx.loader.content.CursorLoader; + import java.util.Locale; import java.util.Objects; import java.util.Set; diff --git a/src/com/android/settings/inputmethod/UserDictionaryList.java b/src/com/android/settings/inputmethod/UserDictionaryList.java index 990c12bcdb..497b380ca6 100644 --- a/src/com/android/settings/inputmethod/UserDictionaryList.java +++ b/src/com/android/settings/inputmethod/UserDictionaryList.java @@ -16,49 +16,34 @@ package com.android.settings.inputmethod; -import android.app.Activity; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; -import android.database.Cursor; import android.os.Bundle; -import android.provider.UserDictionary; -import androidx.annotation.NonNull; -import androidx.preference.Preference; -import androidx.preference.PreferenceGroup; -import android.text.TextUtils; -import android.view.inputmethod.InputMethodInfo; -import android.view.inputmethod.InputMethodManager; -import android.view.inputmethod.InputMethodSubtype; - -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import android.provider.SearchIndexableResource; + import com.android.settings.R; -import com.android.settings.SettingsPreferenceFragment; -import com.android.settings.Utils; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; +import com.android.settingslib.search.SearchIndexable; +import java.util.ArrayList; import java.util.List; -import java.util.Locale; -import java.util.TreeSet; -public class UserDictionaryList extends SettingsPreferenceFragment { - public static final String USER_DICTIONARY_SETTINGS_INTENT_ACTION = - "android.settings.USER_DICTIONARY_SETTINGS"; - private String mLocale; +@SearchIndexable +public class UserDictionaryList extends DashboardFragment { - @Override - public int getMetricsCategory() { - return MetricsEvent.INPUTMETHOD_USER_DICTIONARY; - } + private static final String TAG = "UserDictionaryList"; @Override - public void onCreate(Bundle icicle) { - super.onCreate(icicle); - setPreferenceScreen(getPreferenceManager().createPreferenceScreen(getActivity())); + public int getMetricsCategory() { + return SettingsEnums.INPUTMETHOD_USER_DICTIONARY; } @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - getActivity().getActionBar().setTitle(R.string.user_dict_settings_title); + public void onAttach(Context context) { + super.onAttach(context); final Intent intent = getActivity().getIntent(); final String localeFromIntent = @@ -76,122 +61,31 @@ public class UserDictionaryList extends SettingsPreferenceFragment { } else { locale = null; } - mLocale = locale; - } - - @NonNull - public static TreeSet<String> getUserDictionaryLocalesSet(Context context) { - final Cursor cursor = context.getContentResolver().query( - UserDictionary.Words.CONTENT_URI, new String[]{UserDictionary.Words.LOCALE}, - null, null, null); - final TreeSet<String> localeSet = new TreeSet<>(); - if (cursor == null) { - // The user dictionary service is not present or disabled. Return empty set. - return localeSet; - } - try { - if (cursor.moveToFirst()) { - final int columnIndex = cursor.getColumnIndex(UserDictionary.Words.LOCALE); - do { - final String locale = cursor.getString(columnIndex); - localeSet.add(null != locale ? locale : ""); - } while (cursor.moveToNext()); - } - } finally { - cursor.close(); - } - - // CAVEAT: Keep this for consistency of the implementation between Keyboard and Settings - // if (!UserDictionarySettings.IS_SHORTCUT_API_SUPPORTED) { - // // For ICS, we need to show "For all languages" in case that the keyboard locale - // // is different from the system locale - // localeSet.add(""); - // } - - final InputMethodManager imm = - (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); - final List<InputMethodInfo> imis = imm.getEnabledInputMethodList(); - for (final InputMethodInfo imi : imis) { - final List<InputMethodSubtype> subtypes = - imm.getEnabledInputMethodSubtypeList( - imi, true /* allowsImplicitlySelectedSubtypes */); - for (InputMethodSubtype subtype : subtypes) { - final String locale = subtype.getLocale(); - if (!TextUtils.isEmpty(locale)) { - localeSet.add(locale); - } - } - } - - // We come here after we have collected locales from existing user dictionary entries and - // enabled subtypes. If we already have the locale-without-country version of the system - // locale, we don't add the system locale to avoid confusion even though it's technically - // correct to add it. - if (!localeSet.contains(Locale.getDefault().getLanguage().toString())) { - localeSet.add(Locale.getDefault().toString()); - } - return localeSet; + use(UserDictionaryListPreferenceController.class).setLocale(locale); } - /** - * Creates the entries that allow the user to go into the user dictionary for each locale. - * - * @param userDictGroup The group to put the settings in. - */ - protected void createUserDictSettings(PreferenceGroup userDictGroup) { - final Activity activity = getActivity(); - userDictGroup.removeAll(); - final TreeSet<String> localeSet = - UserDictionaryList.getUserDictionaryLocalesSet(activity); - if (mLocale != null) { - // If the caller explicitly specify empty string as a locale, we'll show "all languages" - // in the list. - localeSet.add(mLocale); - } - if (localeSet.size() > 1) { - // Have an "All languages" entry in the languages list if there are two or more active - // languages - localeSet.add(""); - } - - if (localeSet.isEmpty()) { - userDictGroup.addPreference(createUserDictionaryPreference(null, activity)); - } else { - for (String locale : localeSet) { - userDictGroup.addPreference(createUserDictionaryPreference(locale, activity)); - } - } - } - - /** - * Create a single User Dictionary Preference object, with its parameters set. - * - * @param locale The locale for which this user dictionary is for. - * @return The corresponding preference. - */ - protected Preference createUserDictionaryPreference(String locale, Activity activity) { - final Preference newPref = new Preference(getPrefContext()); - final Intent intent = new Intent(USER_DICTIONARY_SETTINGS_INTENT_ACTION); - if (null == locale) { - newPref.setTitle(Locale.getDefault().getDisplayName()); - } else { - if ("".equals(locale)) { - newPref.setTitle(getString(R.string.user_dict_settings_all_languages)); - } else { - newPref.setTitle(Utils.createLocaleFromString(locale).getDisplayName()); - } - intent.putExtra("locale", locale); - newPref.getExtras().putString("locale", locale); - } - newPref.setIntent(intent); - newPref.setFragment(UserDictionarySettings.class.getName()); - return newPref; + @Override + protected int getPreferenceScreenResId() { + return R.xml.user_dictionary_list_fragment; } @Override - public void onResume() { - super.onResume(); - createUserDictSettings(getPreferenceScreen()); + protected String getLogTag() { + return TAG; } + + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, + boolean enabled) { + final ArrayList<SearchIndexableResource> result = new ArrayList<>(); + + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.user_dictionary_list_fragment; + result.add(sir); + return result; + } + }; } diff --git a/src/com/android/settings/inputmethod/UserDictionaryListPreferenceController.java b/src/com/android/settings/inputmethod/UserDictionaryListPreferenceController.java new file mode 100644 index 0000000000..9343493de2 --- /dev/null +++ b/src/com/android/settings/inputmethod/UserDictionaryListPreferenceController.java @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2018 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.inputmethod; + +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.provider.UserDictionary; +import android.text.TextUtils; +import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.InputMethodSubtype; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.Utils; +import com.android.settings.core.BasePreferenceController; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnStart; + +import java.util.List; +import java.util.Locale; +import java.util.TreeSet; + +public class UserDictionaryListPreferenceController extends BasePreferenceController implements + LifecycleObserver, OnStart { + + public static final String USER_DICTIONARY_SETTINGS_INTENT_ACTION = + "android.settings.USER_DICTIONARY_SETTINGS"; + private final String KEY_ALL_LANGUAGE = "all_languages"; + private String mLocale; + private PreferenceScreen mScreen; + + public UserDictionaryListPreferenceController(Context context, String key) { + super(context, key); + } + + public void setLocale(String locale) { + mLocale = locale; + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + // This is to make newly inserted languages being sorted alphabetically when updating + // the existing preferenceScreen, and for "For all languages" to be always on the top. + screen.setOrderingAsAdded(false); + mScreen = screen; + } + + @Override + public void onStart() { + createUserDictSettings(); + } + + @NonNull + public static TreeSet<String> getUserDictionaryLocalesSet(Context context) { + final Cursor cursor = context.getContentResolver().query( + UserDictionary.Words.CONTENT_URI, new String[]{UserDictionary.Words.LOCALE}, + null, null, null); + final TreeSet<String> localeSet = new TreeSet<>(); + if (cursor == null) { + // The user dictionary service is not present or disabled. Return empty set. + return localeSet; + } + try { + if (cursor.moveToFirst()) { + final int columnIndex = cursor.getColumnIndex(UserDictionary.Words.LOCALE); + do { + final String locale = cursor.getString(columnIndex); + localeSet.add(null != locale ? locale : ""); + } while (cursor.moveToNext()); + } + } finally { + cursor.close(); + } + + // CAVEAT: Keep this for consistency of the implementation between Keyboard and Settings + // if (!UserDictionarySettings.IS_SHORTCUT_API_SUPPORTED) { + // // For ICS, we need to show "For all languages" in case that the keyboard locale + // // is different from the system locale + // localeSet.add(""); + // } + + final InputMethodManager imm = + (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + final List<InputMethodInfo> imis = imm.getEnabledInputMethodList(); + for (final InputMethodInfo imi : imis) { + final List<InputMethodSubtype> subtypes = + imm.getEnabledInputMethodSubtypeList( + imi, true /* allowsImplicitlySelectedSubtypes */); + for (InputMethodSubtype subtype : subtypes) { + final String locale = subtype.getLocale(); + if (!TextUtils.isEmpty(locale)) { + localeSet.add(locale); + } + } + } + + // We come here after we have collected locales from existing user dictionary entries and + // enabled subtypes. If we already have the locale-without-country version of the system + // locale, we don't add the system locale to avoid confusion even though it's technically + // correct to add it. + if (!localeSet.contains(Locale.getDefault().getLanguage())) { + localeSet.add(Locale.getDefault().toString()); + } + + return localeSet; + } + + @VisibleForTesting + TreeSet<String> getUserDictLocalesSet(Context context) { + return getUserDictionaryLocalesSet(context); + } + + /** + * Creates the entries that allow the user to go into the user dictionary for each locale. + */ + private void createUserDictSettings() { + final TreeSet<String> localeSet = getUserDictLocalesSet(mContext); + final int prefCount = mScreen.getPreferenceCount(); + String prefKey; + + if (mLocale != null) { + // If the caller explicitly specify empty string as a locale, we'll show "all languages" + // in the list. + localeSet.add(mLocale); + } + if (localeSet.size() > 1) { + // Have an "All languages" entry in the languages list if there are two or more active + // languages + localeSet.add(""); + } + + // Update the existing preferenceScreen according to the corresponding data set. + if (prefCount > 0) { + for (int i = prefCount - 1; i >= 0; i--) { + prefKey = mScreen.getPreference(i).getKey(); + if (TextUtils.isEmpty(prefKey) || TextUtils.equals(KEY_ALL_LANGUAGE, prefKey)) { + continue; + } + if (!localeSet.isEmpty() && localeSet.contains(prefKey)) { + localeSet.remove(prefKey); + continue; + } + mScreen.removePreference(mScreen.findPreference(prefKey)); + } + } + + if (localeSet.isEmpty() && prefCount == 0) { + mScreen.addPreference(createUserDictionaryPreference(null)); + } else { + for (String locale : localeSet) { + final Preference pref = createUserDictionaryPreference(locale); + if (mScreen.findPreference(pref.getKey()) == null) { + mScreen.addPreference(pref); + } + } + } + } + + /** + * Create a single User Dictionary Preference object, with its parameters set. + * + * @param locale The locale for which this user dictionary is for. + * @return The corresponding preference. + */ + private Preference createUserDictionaryPreference(String locale) { + final String KEY_LOCALE = "locale"; + final Preference newPref = new Preference(mScreen.getContext()); + final Intent intent = new Intent(USER_DICTIONARY_SETTINGS_INTENT_ACTION); + if (locale == null) { + newPref.setTitle(Locale.getDefault().getDisplayName()); + newPref.setKey(Locale.getDefault().toString()); + } else { + if (TextUtils.isEmpty(locale)) { + newPref.setTitle(mContext.getString(R.string.user_dict_settings_all_languages)); + newPref.setKey(KEY_ALL_LANGUAGE); + newPref.setOrder(0); + } else { + newPref.setTitle(Utils.createLocaleFromString(locale).getDisplayName()); + newPref.setKey(locale); + } + intent.putExtra(KEY_LOCALE, locale); + newPref.getExtras().putString(KEY_LOCALE, locale); + } + newPref.setIntent(intent); + newPref.setFragment(UserDictionarySettings.class.getName()); + return newPref; + } +} diff --git a/src/com/android/settings/inputmethod/UserDictionarySettings.java b/src/com/android/settings/inputmethod/UserDictionarySettings.java index e80fdf3765..67420a2c5d 100644 --- a/src/com/android/settings/inputmethod/UserDictionarySettings.java +++ b/src/com/android/settings/inputmethod/UserDictionarySettings.java @@ -18,12 +18,10 @@ package com.android.settings.inputmethod; import android.annotation.Nullable; import android.app.ActionBar; -import android.app.ListFragment; -import android.app.LoaderManager; +import android.app.settings.SettingsEnums; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; -import android.content.Loader; import android.database.Cursor; import android.os.Bundle; import android.provider.UserDictionary; @@ -41,7 +39,10 @@ import android.widget.SectionIndexer; import android.widget.SimpleCursorAdapter; import android.widget.TextView; -import com.android.internal.logging.nano.MetricsProto; +import androidx.fragment.app.ListFragment; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.Loader; + import com.android.settings.R; import com.android.settings.core.SubSettingLauncher; import com.android.settings.overlay.FeatureFactory; @@ -67,7 +68,7 @@ public class UserDictionarySettings extends ListFragment implements Instrumentab @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.USER_DICTIONARY_SETTINGS; + return SettingsEnums.USER_DICTIONARY_SETTINGS; } @Override @@ -153,7 +154,7 @@ public class UserDictionarySettings extends ListFragment implements Instrumentab public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { MenuItem actionItem = menu.add(0, OPTIONS_MENU_ADD, 0, R.string.user_dict_settings_add_menu_title) - .setIcon(R.drawable.ic_menu_add_white); + .setIcon(R.drawable.ic_add_24dp); actionItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT); } @@ -191,7 +192,7 @@ public class UserDictionarySettings extends ListFragment implements Instrumentab new SubSettingLauncher(getContext()) .setDestination(UserDictionaryAddWordFragment.class.getName()) .setArguments(args) - .setTitle(R.string.user_dict_settings_add_dialog_title) + .setTitleRes(R.string.user_dict_settings_add_dialog_title) .setSourceMetricsCategory(getMetricsCategory()) .launch(); diff --git a/src/com/android/settings/inputmethod/VirtualKeyboardForWorkPreferenceController.java b/src/com/android/settings/inputmethod/VirtualKeyboardForWorkPreferenceController.java new file mode 100644 index 0000000000..6cdd386bb4 --- /dev/null +++ b/src/com/android/settings/inputmethod/VirtualKeyboardForWorkPreferenceController.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2019 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.inputmethod; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.view.inputmethod.InputMethodSystemProperty; + +import com.android.settings.R; +import com.android.settings.core.WorkProfilePreferenceController; + +public final class VirtualKeyboardForWorkPreferenceController + extends WorkProfilePreferenceController { + + public VirtualKeyboardForWorkPreferenceController(Context context, + String preferenceKey) { + super(context, preferenceKey); + } + + @Override + protected int getSourceMetricsCategory() { + return SettingsEnums.SETTINGS_LANGUAGE_CATEGORY; + } + + @AvailabilityStatus + @Override + public int getAvailabilityStatus() { + if (!mContext.getResources().getBoolean(R.bool.config_show_virtual_keyboard_pref) + || !InputMethodSystemProperty.PER_PROFILE_IME_ENABLED) { + return UNSUPPORTED_ON_DEVICE; + } + return super.getAvailabilityStatus(); + } +} diff --git a/src/com/android/settings/inputmethod/VirtualKeyboardFragment.java b/src/com/android/settings/inputmethod/VirtualKeyboardFragment.java index bd33b3ff4c..ef07d11d24 100644 --- a/src/com/android/settings/inputmethod/VirtualKeyboardFragment.java +++ b/src/com/android/settings/inputmethod/VirtualKeyboardFragment.java @@ -18,34 +18,34 @@ package com.android.settings.inputmethod; import android.app.Activity; import android.app.admin.DevicePolicyManager; +import android.app.settings.SettingsEnums; import android.content.Context; -import android.graphics.Color; -import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.provider.SearchIndexableResource; -import androidx.preference.Preference; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodManager; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.preference.Preference; + import com.android.internal.util.Preconditions; import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; -import com.android.settingslib.inputmethod.InputMethodAndSubtypeUtil; +import com.android.settingslib.inputmethod.InputMethodAndSubtypeUtilCompat; import com.android.settingslib.inputmethod.InputMethodPreference; +import com.android.settingslib.search.SearchIndexable; import java.text.Collator; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +@SearchIndexable public final class VirtualKeyboardFragment extends SettingsPreferenceFragment implements Indexable { private static final String ADD_VIRTUAL_KEYBOARD_SCREEN = "add_virtual_keyboard_screen"; - private static final Drawable NO_ICON = new ColorDrawable(Color.TRANSPARENT); private final ArrayList<InputMethodPreference> mInputMethodPreferenceList = new ArrayList<>(); private InputMethodManager mImm; @@ -72,7 +72,7 @@ public final class VirtualKeyboardFragment extends SettingsPreferenceFragment im @Override public int getMetricsCategory() { - return MetricsEvent.VIRTUAL_KEYBOARDS; + return SettingsEnums.VIRTUAL_KEYBOARDS; } private void updateInputMethodPreferenceViews() { @@ -86,14 +86,7 @@ public final class VirtualKeyboardFragment extends SettingsPreferenceFragment im final InputMethodInfo imi = imis.get(i); final boolean isAllowedByOrganization = permittedList == null || permittedList.contains(imi.getPackageName()); - Drawable icon; - try { - // TODO: Consider other ways to retrieve an icon to show here. - icon = getActivity().getPackageManager().getApplicationIcon(imi.getPackageName()); - } catch (Exception e) { - // TODO: Consider handling the error differently perhaps by showing default icons. - icon = NO_ICON; - } + final Drawable icon = imi.loadIcon(context.getPackageManager()); final InputMethodPreference pref = new InputMethodPreference( context, imi, @@ -110,7 +103,7 @@ public final class VirtualKeyboardFragment extends SettingsPreferenceFragment im final InputMethodPreference pref = mInputMethodPreferenceList.get(i); pref.setOrder(i); getPreferenceScreen().addPreference(pref); - InputMethodAndSubtypeUtil.removeUnnecessaryNonPersistentPreference(pref); + InputMethodAndSubtypeUtilCompat.removeUnnecessaryNonPersistentPreference(pref); pref.updatePreferenceViews(); } mAddVirtualKeyboardScreen.setIcon(R.drawable.ic_add_24dp); @@ -127,12 +120,5 @@ public final class VirtualKeyboardFragment extends SettingsPreferenceFragment im sir.xmlResId = R.xml.virtual_keyboard_settings; return Arrays.asList(sir); } - - @Override - public List<String> getNonIndexableKeys(Context context) { - final List<String> keys = super.getNonIndexableKeys(context); - keys.add("add_virtual_keyboard_screen"); - return keys; - } }; } diff --git a/src/com/android/settings/inputmethod/VirtualKeyboardPreferenceController.java b/src/com/android/settings/inputmethod/VirtualKeyboardPreferenceController.java index 4a046469d9..61e6a42464 100644 --- a/src/com/android/settings/inputmethod/VirtualKeyboardPreferenceController.java +++ b/src/com/android/settings/inputmethod/VirtualKeyboardPreferenceController.java @@ -19,11 +19,13 @@ package com.android.settings.inputmethod; import android.app.admin.DevicePolicyManager; import android.content.Context; import android.content.pm.PackageManager; -import androidx.preference.Preference; +import android.icu.text.ListFormatter; import android.text.BidiFormatter; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodManager; +import androidx.preference.Preference; + import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.core.AbstractPreferenceController; @@ -81,15 +83,10 @@ public class VirtualKeyboardPreferenceController extends AbstractPreferenceContr final BidiFormatter bidiFormatter = BidiFormatter.getInstance(); - String summary = null; + final List<String> summaries = new ArrayList<>(); for (String label : labels) { - if (summary == null) { - summary = bidiFormatter.unicodeWrap(label); - } else { - summary = mContext.getString(R.string.join_many_items_middle, summary, - bidiFormatter.unicodeWrap(label)); - } + summaries.add(bidiFormatter.unicodeWrap(label)); } - preference.setSummary(summary); + preference.setSummary(ListFormatter.getInstance().format(summaries)); } } diff --git a/src/com/android/settings/language/LanguageAndInputSettings.java b/src/com/android/settings/language/LanguageAndInputSettings.java index 7e7bdcaecd..3a8aeac4b9 100644 --- a/src/com/android/settings/language/LanguageAndInputSettings.java +++ b/src/com/android/settings/language/LanguageAndInputSettings.java @@ -17,22 +17,21 @@ package com.android.settings.language; import android.app.Activity; +import android.app.settings.SettingsEnums; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.pm.PackageManager; import android.provider.SearchIndexableResource; import android.provider.Settings; -import android.speech.tts.TtsEngines; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import android.text.TextUtils; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodManager; -import com.android.internal.logging.nano.MetricsProto; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import com.android.settings.R; -import com.android.settings.applications.defaultapps.DefaultAutofillPreferenceController; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.dashboard.SummaryLoader; import com.android.settings.inputmethod.PhysicalKeyboardPreferenceController; @@ -42,11 +41,13 @@ import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.widget.PreferenceCategoryController; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +@SearchIndexable public class LanguageAndInputSettings extends DashboardFragment { private static final String TAG = "LangAndInputSettings"; @@ -54,12 +55,10 @@ public class LanguageAndInputSettings extends DashboardFragment { private static final String KEY_KEYBOARDS_CATEGORY = "keyboards_category"; private static final String KEY_TEXT_TO_SPEECH = "tts_settings_summary"; private static final String KEY_POINTER_AND_TTS_CATEGORY = "pointer_and_tts_category"; - private static final String KEY_GAME_CONTROLLER_CATEGORY = "game_controller_settings_category"; - private static final String KEY_PHYSICAL_KEYBOARD = "physical_keyboard_pref"; @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.SETTINGS_LANGUAGE_CATEGORY; + return SettingsEnums.SETTINGS_LANGUAGE_CATEGORY; } @Override @@ -87,7 +86,7 @@ public class LanguageAndInputSettings extends DashboardFragment { @Override protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { - return buildPreferenceControllers(context, getLifecycle()); + return buildPreferenceControllers(context, getSettingsLifecycle()); } private static List<AbstractPreferenceController> buildPreferenceControllers( @@ -110,7 +109,7 @@ public class LanguageAndInputSettings extends DashboardFragment { // Pointer and Tts final TtsPreferenceController ttsPreferenceController = - new TtsPreferenceController(context, new TtsEngines(context)); + new TtsPreferenceController(context, KEY_TEXT_TO_SPEECH); controllers.add(ttsPreferenceController); final PointerSpeedController pointerController = new PointerSpeedController(context); controllers.add(pointerController); @@ -120,8 +119,6 @@ public class LanguageAndInputSettings extends DashboardFragment { // Input Assistance controllers.add(new SpellCheckerPreferenceController(context)); - controllers.add(new DefaultAutofillPreferenceController(context)); - controllers.add(new UserDictionaryPreferenceController(context)); return controllers; } @@ -179,14 +176,5 @@ public class LanguageAndInputSettings extends DashboardFragment { Context context) { return buildPreferenceControllers(context, null); } - - @Override - public List<String> getNonIndexableKeys(Context context) { - List<String> keys = super.getNonIndexableKeys(context); - // Duplicates in summary and details pages. - keys.add(KEY_TEXT_TO_SPEECH); - keys.add(KEY_PHYSICAL_KEYBOARD); - return keys; - } }; } diff --git a/src/com/android/settings/language/PhoneLanguagePreferenceController.java b/src/com/android/settings/language/PhoneLanguagePreferenceController.java index af60ba0fa4..4f67a8ff6e 100644 --- a/src/com/android/settings/language/PhoneLanguagePreferenceController.java +++ b/src/com/android/settings/language/PhoneLanguagePreferenceController.java @@ -16,10 +16,11 @@ package com.android.settings.language; +import android.app.settings.SettingsEnums; import android.content.Context; + import androidx.preference.Preference; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.core.SubSettingLauncher; @@ -73,8 +74,8 @@ public class PhoneLanguagePreferenceController extends AbstractPreferenceControl } new SubSettingLauncher(mContext) .setDestination(LocaleListEditor.class.getName()) - .setSourceMetricsCategory(MetricsProto.MetricsEvent.SETTINGS_LANGUAGE_CATEGORY) - .setTitle(R.string.pref_title_lang_selection) + .setSourceMetricsCategory(SettingsEnums.SETTINGS_LANGUAGE_CATEGORY) + .setTitleRes(R.string.language_picker_title) .launch(); return true; } diff --git a/src/com/android/settings/language/PointerSpeedController.java b/src/com/android/settings/language/PointerSpeedController.java index 8a0226a20d..67326ddab0 100644 --- a/src/com/android/settings/language/PointerSpeedController.java +++ b/src/com/android/settings/language/PointerSpeedController.java @@ -18,11 +18,11 @@ package com.android.settings.language; import android.content.Context; -import com.android.settings.core.BasePreferenceController; -import com.android.settings.R; - import androidx.annotation.VisibleForTesting; +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; + public class PointerSpeedController extends BasePreferenceController { diff --git a/src/com/android/settings/language/TtsPreferenceController.java b/src/com/android/settings/language/TtsPreferenceController.java index f19047b5b8..7e34175ce9 100644 --- a/src/com/android/settings/language/TtsPreferenceController.java +++ b/src/com/android/settings/language/TtsPreferenceController.java @@ -19,31 +19,26 @@ package com.android.settings.language; import android.content.Context; import android.speech.tts.TtsEngines; -import com.android.settings.core.PreferenceControllerMixin; -import com.android.settings.R; -import com.android.settingslib.core.AbstractPreferenceController; +import androidx.annotation.VisibleForTesting; -public class TtsPreferenceController extends AbstractPreferenceController - implements PreferenceControllerMixin { +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; - private static final String KEY_VOICE_CATEGORY = "voice_category"; - private static final String KEY_TTS_SETTINGS = "tts_settings_summary"; +public class TtsPreferenceController extends BasePreferenceController { - private final TtsEngines mTtsEngines; + @VisibleForTesting + TtsEngines mTtsEngines; - public TtsPreferenceController(Context context, TtsEngines ttsEngines) { - super(context); - mTtsEngines = ttsEngines; + public TtsPreferenceController(Context context, String key) { + super(context, key); + mTtsEngines = new TtsEngines(context); } @Override - public boolean isAvailable() { + public int getAvailabilityStatus() { return !mTtsEngines.getEngines().isEmpty() && - mContext.getResources().getBoolean(R.bool.config_show_tts_settings_summary); - } - - @Override - public String getPreferenceKey() { - return KEY_TTS_SETTINGS; + mContext.getResources().getBoolean(R.bool.config_show_tts_settings_summary) + ? AVAILABLE_UNSEARCHABLE + : CONDITIONALLY_UNAVAILABLE; } } diff --git a/src/com/android/settings/language/UserDictionaryForWorkPreferenceController.java b/src/com/android/settings/language/UserDictionaryForWorkPreferenceController.java new file mode 100644 index 0000000000..7ff8aec36e --- /dev/null +++ b/src/com/android/settings/language/UserDictionaryForWorkPreferenceController.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2019 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.language; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.view.inputmethod.InputMethodSystemProperty; + +import com.android.settings.R; +import com.android.settings.core.WorkProfilePreferenceController; + +/** + * Preference controller for "UserDictionary for work". + * + * @see UserDictionaryPreferenceController + */ +public final class UserDictionaryForWorkPreferenceController + extends WorkProfilePreferenceController { + + public UserDictionaryForWorkPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + protected int getSourceMetricsCategory() { + return SettingsEnums.SETTINGS_LANGUAGE_CATEGORY; + } + + @AvailabilityStatus + @Override + public int getAvailabilityStatus() { + if (!InputMethodSystemProperty.PER_PROFILE_IME_ENABLED) { + return UNSUPPORTED_ON_DEVICE; + } + return super.getAvailabilityStatus(); + } +} diff --git a/src/com/android/settings/language/UserDictionaryPreferenceController.java b/src/com/android/settings/language/UserDictionaryPreferenceController.java index 9338a296da..d7829c612b 100644 --- a/src/com/android/settings/language/UserDictionaryPreferenceController.java +++ b/src/com/android/settings/language/UserDictionaryPreferenceController.java @@ -16,35 +16,28 @@ package com.android.settings.language; -import android.app.Fragment; import android.content.Context; import android.os.Bundle; + +import androidx.fragment.app.Fragment; import androidx.preference.Preference; -import com.android.settings.core.PreferenceControllerMixin; +import com.android.settings.core.BasePreferenceController; import com.android.settings.inputmethod.UserDictionaryList; +import com.android.settings.inputmethod.UserDictionaryListPreferenceController; import com.android.settings.inputmethod.UserDictionarySettings; -import com.android.settingslib.core.AbstractPreferenceController; import java.util.TreeSet; -public class UserDictionaryPreferenceController extends AbstractPreferenceController - implements PreferenceControllerMixin { - - private static final String KEY_USER_DICTIONARY_SETTINGS = "key_user_dictionary_settings"; +public class UserDictionaryPreferenceController extends BasePreferenceController { - public UserDictionaryPreferenceController(Context context) { - super(context); - } - - @Override - public boolean isAvailable() { - return true; + public UserDictionaryPreferenceController(Context context, String key) { + super(context, key); } @Override - public String getPreferenceKey() { - return KEY_USER_DICTIONARY_SETTINGS; + public int getAvailabilityStatus() { + return AVAILABLE_UNSEARCHABLE; } @Override @@ -61,10 +54,10 @@ public class UserDictionaryPreferenceController extends AbstractPreferenceContro // parameter in the extras. This will be interpreted by the // UserDictionarySettings class as meaning // "the current locale". Note that with the current code for - // UserDictionaryList#getUserDictionaryLocalesSet() + // UserDictionaryListPreferenceController#getUserDictionaryLocalesSet() // the locale list always has at least one element, since it // always includes the current locale explicitly. - // @see UserDictionaryList.getUserDictionaryLocalesSet(). + // @see UserDictionaryListPreferenceController.getUserDictionaryLocalesSet(). extras.putString("locale", localeSet.first()); } targetFragment = UserDictionarySettings.class; @@ -75,6 +68,6 @@ public class UserDictionaryPreferenceController extends AbstractPreferenceContro } protected TreeSet<String> getDictionaryLocales() { - return UserDictionaryList.getUserDictionaryLocalesSet(mContext); + return UserDictionaryListPreferenceController.getUserDictionaryLocalesSet(mContext); } } diff --git a/src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java b/src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java index be6447ef19..a06c77eac1 100644 --- a/src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java +++ b/src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java @@ -20,22 +20,23 @@ import android.content.Context; import android.graphics.Canvas; import android.os.Bundle; import android.os.LocaleList; -import androidx.core.view.MotionEventCompat; -import androidx.recyclerview.widget.RecyclerView; -import androidx.recyclerview.widget.ItemTouchHelper; import android.util.Log; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.widget.CheckBox; import android.widget.CompoundButton; +import androidx.core.view.MotionEventCompat; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.RecyclerView; + import com.android.internal.app.LocalePicker; import com.android.internal.app.LocaleStore; - -import com.android.settings.shortcut.CreateShortcut; import com.android.settings.R; +import com.android.settings.shortcut.ShortcutsUpdateTask; import java.text.NumberFormat; import java.util.ArrayList; @@ -159,10 +160,13 @@ class LocaleDragAndDropAdapter dragCell.setShowCheckbox(mRemoveMode); dragCell.setShowMiniLabel(!mRemoveMode); dragCell.setShowHandle(!mRemoveMode && mDragEnabled); - dragCell.setChecked(mRemoveMode ? feedItem.getChecked() : false); dragCell.setTag(feedItem); - dragCell.getCheckbox() - .setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + CheckBox checkbox = dragCell.getCheckbox(); + // clear listener before setChecked() in case another item already bind to + // current ViewHolder and checked event is triggered on stale listener mistakenly. + checkbox.setOnCheckedChangeListener(null); + checkbox.setChecked(mRemoveMode ? feedItem.getChecked() : false); + checkbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { LocaleStore.LocaleInfo feedItem = @@ -290,7 +294,7 @@ class LocaleDragAndDropAdapter LocalePicker.updateLocales(mLocalesToSetNext); mLocalesSetLast = mLocalesToSetNext; - new CreateShortcut.ShortcutsUpdateTask(mContext).execute(); + new ShortcutsUpdateTask(mContext).execute(); mLocalesToSetNext = null; diff --git a/src/com/android/settings/localepicker/LocaleLinearLayoutManager.java b/src/com/android/settings/localepicker/LocaleLinearLayoutManager.java index d3d58ff73e..5fb9440e01 100644 --- a/src/com/android/settings/localepicker/LocaleLinearLayoutManager.java +++ b/src/com/android/settings/localepicker/LocaleLinearLayoutManager.java @@ -18,10 +18,11 @@ package com.android.settings.localepicker; import android.content.Context; import android.os.Bundle; +import android.view.View; + import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import android.view.View; import com.android.settings.R; @@ -116,39 +117,33 @@ public class LocaleLinearLayoutManager extends LinearLayoutManager { final int position = this.getPosition(host); boolean result = false; - switch (action) { - case R.id.action_drag_move_up: - if (position > 0) { - mAdapter.onItemMove(position, position - 1); - result = true; - } - break; - case R.id.action_drag_move_down: - if (position + 1 < itemCount) { - mAdapter.onItemMove(position, position + 1); - result = true; - } - break; - case R.id.action_drag_move_top: - if (position != 0) { - mAdapter.onItemMove(position, 0); - result = true; - } - break; - case R.id.action_drag_move_bottom: - if (position != itemCount - 1) { - mAdapter.onItemMove(position, itemCount - 1); - result = true; - } - break; - case R.id.action_drag_remove: - if (itemCount > 1) { - mAdapter.removeItem(position); - result = true; - } - break; - default: - return super.performAccessibilityActionForItem(recycler, state, host, action, args); + if (action == R.id.action_drag_move_up) { + if (position > 0) { + mAdapter.onItemMove(position, position - 1); + result = true; + } + } else if (action == R.id.action_drag_move_down) { + if (position + 1 < itemCount) { + mAdapter.onItemMove(position, position + 1); + result = true; + } + } else if (action == R.id.action_drag_move_top) { + if (position != 0) { + mAdapter.onItemMove(position, 0); + result = true; + } + } else if (action == R.id.action_drag_move_bottom) { + if (position != itemCount - 1) { + mAdapter.onItemMove(position, itemCount - 1); + result = true; + } + } else if (action == R.id.action_drag_remove) { + if (itemCount > 1) { + mAdapter.removeItem(position); + result = true; + } + } else { + return super.performAccessibilityActionForItem(recycler, state, host, action, args); } if (result) { diff --git a/src/com/android/settings/localepicker/LocaleListEditor.java b/src/com/android/settings/localepicker/LocaleListEditor.java index de68f00e31..bee5152956 100644 --- a/src/com/android/settings/localepicker/LocaleListEditor.java +++ b/src/com/android/settings/localepicker/LocaleListEditor.java @@ -16,24 +16,27 @@ package com.android.settings.localepicker; -import android.app.AlertDialog; -import android.app.FragmentTransaction; +import static android.os.UserManager.DISALLOW_CONFIG_LOCALE; + +import android.app.Activity; +import android.app.settings.SettingsEnums; import android.content.DialogInterface; +import android.content.Intent; import android.os.Bundle; import android.os.LocaleList; -import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; - import android.widget.TextView; + +import androidx.appcompat.app.AlertDialog; +import androidx.recyclerview.widget.RecyclerView; + import com.android.internal.app.LocalePicker; -import com.android.internal.app.LocalePickerWithRegion; import com.android.internal.app.LocaleStore; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.RestrictedSettingsFragment; @@ -41,17 +44,16 @@ import java.util.ArrayList; import java.util.List; import java.util.Locale; -import static android.os.UserManager.DISALLOW_CONFIG_LOCALE; - /** * Drag-and-drop editor for the user-ordered locale lists. */ -public class LocaleListEditor extends RestrictedSettingsFragment - implements LocalePickerWithRegion.LocaleSelectedListener { +public class LocaleListEditor extends RestrictedSettingsFragment { + protected static final String INTENT_LOCALE_KEY = "localeInfo"; private static final String CFGKEY_REMOVE_MODE = "localeRemoveMode"; private static final String CFGKEY_REMOVE_DIALOG = "showingLocaleRemoveDialog"; private static final int MENU_ID_REMOVE = Menu.FIRST + 1; + private static final int REQUEST_LOCALE_PICKER = 0; private LocaleDragAndDropAdapter mAdapter; private Menu mMenu; @@ -66,7 +68,7 @@ public class LocaleListEditor extends RestrictedSettingsFragment @Override public int getMetricsCategory() { - return MetricsEvent.USER_LOCALE_LIST; + return SettingsEnums.USER_LOCALE_LIST; } @Override @@ -150,6 +152,19 @@ public class LocaleListEditor extends RestrictedSettingsFragment return super.onOptionsItemSelected(menuItem); } + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == REQUEST_LOCALE_PICKER && resultCode == Activity.RESULT_OK + && data != null) { + final LocaleStore.LocaleInfo locale = + (LocaleStore.LocaleInfo) data.getSerializableExtra( + INTENT_LOCALE_KEY); + mAdapter.addLocale(locale); + updateVisibilityOfRemoveMenu(); + } + super.onActivityResult(requestCode, resultCode, data); + } + private void setRemoveMode(boolean mRemoveMode) { this.mRemoveMode = mRemoveMode; mAdapter.setRemoveMode(mRemoveMode); @@ -267,24 +282,13 @@ public class LocaleListEditor extends RestrictedSettingsFragment mAddLanguage.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - final LocalePickerWithRegion selector = LocalePickerWithRegion.createLanguagePicker( - getContext(), LocaleListEditor.this, false /* translate only */); - getFragmentManager() - .beginTransaction() - .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN) - .replace(getId(), selector) - .addToBackStack("localeListEditor") - .commit(); + final Intent intent = new Intent(getActivity(), + LocalePickerWithRegionActivity.class); + startActivityForResult(intent, REQUEST_LOCALE_PICKER); } }); } - @Override - public void onLocaleSelected(LocaleStore.LocaleInfo locale) { - mAdapter.addLocale(locale); - updateVisibilityOfRemoveMenu(); - } - // Hide the "Remove" menu if there is only one locale in the list, show it otherwise // This is called when the menu is first created, and then one add / remove locale private void updateVisibilityOfRemoveMenu() { diff --git a/src/com/android/settings/localepicker/LocalePickerWithRegionActivity.java b/src/com/android/settings/localepicker/LocalePickerWithRegionActivity.java new file mode 100644 index 0000000000..6ddcf2396c --- /dev/null +++ b/src/com/android/settings/localepicker/LocalePickerWithRegionActivity.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2018 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.localepicker; + +import android.app.Activity; +import android.app.FragmentTransaction; +import android.content.Intent; +import android.os.Bundle; +import android.view.MenuItem; + +import com.android.internal.app.LocalePickerWithRegion; +import com.android.internal.app.LocaleStore; + +public class LocalePickerWithRegionActivity extends Activity + implements LocalePickerWithRegion.LocaleSelectedListener { + + private static final String PARENT_FRAGMENT_NAME = "localeListEditor"; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + getActionBar().setDisplayHomeAsUpEnabled(true); + + final LocalePickerWithRegion selector = LocalePickerWithRegion.createLanguagePicker( + this, LocalePickerWithRegionActivity.this, false /* translate only */); + getFragmentManager() + .beginTransaction() + .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN) + .replace(android.R.id.content, selector) + .addToBackStack(PARENT_FRAGMENT_NAME) + .commit(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + handleBackPressed(); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public void onLocaleSelected(LocaleStore.LocaleInfo locale) { + final Intent intent = new Intent(); + intent.putExtra(LocaleListEditor.INTENT_LOCALE_KEY, locale); + setResult(RESULT_OK, intent); + finish(); + } + + @Override + public void onBackPressed() { + handleBackPressed(); + } + + private void handleBackPressed() { + if (getFragmentManager().getBackStackEntryCount() > 1) { + super.onBackPressed(); + } else { + setResult(RESULT_CANCELED); + finish(); + } + } +} + diff --git a/src/com/android/settings/localepicker/LocaleRecyclerView.java b/src/com/android/settings/localepicker/LocaleRecyclerView.java index 93d54a49a8..e82874dab7 100644 --- a/src/com/android/settings/localepicker/LocaleRecyclerView.java +++ b/src/com/android/settings/localepicker/LocaleRecyclerView.java @@ -17,10 +17,11 @@ package com.android.settings.localepicker; import android.content.Context; -import androidx.recyclerview.widget.RecyclerView; import android.util.AttributeSet; import android.view.MotionEvent; +import androidx.recyclerview.widget.RecyclerView; + class LocaleRecyclerView extends RecyclerView { public LocaleRecyclerView(Context context) { super(context); diff --git a/src/com/android/settings/location/AppLocationPermissionPreferenceController.java b/src/com/android/settings/location/AppLocationPermissionPreferenceController.java index fabe295fef..65abe997a9 100644 --- a/src/com/android/settings/location/AppLocationPermissionPreferenceController.java +++ b/src/com/android/settings/location/AppLocationPermissionPreferenceController.java @@ -1,18 +1,48 @@ package com.android.settings.location; +import static android.Manifest.permission.ACCESS_COARSE_LOCATION; +import static android.Manifest.permission.ACCESS_FINE_LOCATION; + import android.content.Context; +import android.location.LocationManager; +import android.os.UserHandle; +import android.os.UserManager; +import android.permission.PermissionControllerManager; import android.provider.Settings; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; + +import com.android.settings.R; +import com.android.settings.Utils; import com.android.settings.core.PreferenceControllerMixin; -import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.core.lifecycle.Lifecycle; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; public class AppLocationPermissionPreferenceController extends - AbstractPreferenceController implements PreferenceControllerMixin { + LocationBasePreferenceController implements PreferenceControllerMixin { private static final String KEY_APP_LEVEL_PERMISSIONS = "app_level_permissions"; + /** Total number of apps that has location permission. */ + @VisibleForTesting + int mNumTotal = -1; + /** Total number of apps that has background location permission. */ + @VisibleForTesting + int mNumHasLocation = -1; + + final AtomicInteger loadingInProgress = new AtomicInteger(0); + private int mNumTotalLoading = 0; + private int mNumHasLocationLoading = 0; - public AppLocationPermissionPreferenceController(Context context) { - super(context); + private final LocationManager mLocationManager; + private Preference mPreference; + + public AppLocationPermissionPreferenceController(Context context, Lifecycle lifecycle) { + super(context, lifecycle); + mLocationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); } @Override @@ -23,7 +53,82 @@ public class AppLocationPermissionPreferenceController extends @Override public boolean isAvailable() { return Settings.Global.getInt(mContext.getContentResolver(), - android.provider.Settings.Global.LOCATION_SETTINGS_LINK_TO_PERMISSIONS_ENABLED, 1) - == 1; + Settings.Global.LOCATION_SETTINGS_LINK_TO_PERMISSIONS_ENABLED, 1) == 1; + } + + @Override + public CharSequence getSummary() { + if (mLocationManager.isLocationEnabled()) { + if (mNumTotal == -1 || mNumHasLocation == -1) { + return mContext.getString(R.string.location_settings_loading_app_permission_stats); + } + return mContext.getResources().getQuantityString( + R.plurals.location_app_permission_summary_location_on, mNumHasLocation, + mNumHasLocation, mNumTotal); + } else { + return mContext.getString(R.string.location_app_permission_summary_location_off); + } + } + + private void setAppCounts(int numTotal, int numHasLocation) { + mNumTotal = numTotal; + mNumHasLocation = numHasLocation; + refreshSummary(mPreference); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + mPreference = preference; + refreshSummary(preference); + // Bail out if location has been disabled, or there's another loading request in progress. + if (!mLocationManager.isLocationEnabled() || + loadingInProgress.get() != 0) { + return; + } + mNumTotalLoading = 0; + mNumHasLocationLoading = 0; + // Retrieve a list of users inside the current user profile group. + final List<UserHandle> users = mContext.getSystemService( + UserManager.class).getUserProfiles(); + loadingInProgress.set(2 * users.size()); + for (UserHandle user : users) { + final Context userContext = Utils.createPackageContextAsUser(mContext, + user.getIdentifier()); + if (userContext == null) { + for (int i = 0; i < 2; ++i) { + if (loadingInProgress.decrementAndGet() == 0) { + setAppCounts(mNumTotalLoading, mNumHasLocationLoading); + } + } + continue; + } + final PermissionControllerManager permController = + userContext.getSystemService(PermissionControllerManager.class); + permController.countPermissionApps( + Arrays.asList(ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION), 0, + (numApps) -> { + mNumTotalLoading += numApps; + if (loadingInProgress.decrementAndGet() == 0) { + setAppCounts(mNumTotalLoading, mNumHasLocationLoading); + } + }, null); + permController.countPermissionApps( + Arrays.asList(ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION), + PermissionControllerManager.COUNT_ONLY_WHEN_GRANTED, + (numApps) -> { + mNumHasLocationLoading += numApps; + if (loadingInProgress.decrementAndGet() == 0) { + setAppCounts(mNumTotalLoading, mNumHasLocationLoading); + } + }, null); + } + } + + @Override + public void onLocationModeChanged(int mode, boolean restricted) { + if (mPreference != null) { + updateState(mPreference); + } } } diff --git a/src/com/android/settings/location/AppSettingsInjector.java b/src/com/android/settings/location/AppSettingsInjector.java new file mode 100644 index 0000000000..812082122a --- /dev/null +++ b/src/com/android/settings/location/AppSettingsInjector.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2018 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.location; + +import android.content.Context; +import android.text.TextUtils; + +import androidx.preference.Preference; + +import com.android.settings.widget.RestrictedAppPreference; +import com.android.settingslib.location.InjectedSetting; +import com.android.settingslib.location.SettingsInjector; +import com.android.settingslib.widget.apppreference.AppPreference; + +/** + * Adds the preferences specified by the {@link InjectedSetting} objects to a preference group. + */ +public class AppSettingsInjector extends SettingsInjector { + + public AppSettingsInjector(Context context) { + super(context); + } + + @Override + protected Preference createPreference(Context prefContext, InjectedSetting setting) { + return TextUtils.isEmpty(setting.userRestriction) + ? new AppPreference(prefContext) + : new RestrictedAppPreference(prefContext, setting.userRestriction); + } +} diff --git a/src/com/android/settings/location/BluetoothScanningPreferenceController.java b/src/com/android/settings/location/BluetoothScanningPreferenceController.java index 6350ffa49f..d16e1e58e2 100644 --- a/src/com/android/settings/location/BluetoothScanningPreferenceController.java +++ b/src/com/android/settings/location/BluetoothScanningPreferenceController.java @@ -15,8 +15,9 @@ package com.android.settings.location; import android.content.Context; import android.provider.Settings; -import androidx.preference.SwitchPreference; + import androidx.preference.Preference; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.core.AbstractPreferenceController; diff --git a/src/com/android/settings/location/InjectedSetting.java b/src/com/android/settings/location/InjectedSetting.java deleted file mode 100644 index 4877cb4d58..0000000000 --- a/src/com/android/settings/location/InjectedSetting.java +++ /dev/null @@ -1,191 +0,0 @@ -/* - * 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.location; - -import android.content.Intent; -import android.os.UserHandle; -import android.text.TextUtils; -import android.util.Log; - -import com.android.internal.annotations.Immutable; - -import java.util.Objects; - -/** - * Specifies a setting that is being injected into Settings > Location > Location services. - * - * @see android.location.SettingInjectorService - */ -@Immutable -class InjectedSetting { - - /** - * Package for the subclass of {@link android.location.SettingInjectorService} and for the - * settings activity. - */ - public final String packageName; - - /** - * Class name for the subclass of {@link android.location.SettingInjectorService} that - * specifies dynamic values for the location setting. - */ - public final String className; - - /** - * The {@link androidx.preference.Preference#getTitle()} value. - */ - public final String title; - - /** - * The {@link androidx.preference.Preference#getIcon()} value. - */ - public final int iconId; - - /** - * The user/profile associated with this setting (e.g. managed profile) - */ - public final UserHandle mUserHandle; - - /** - * The activity to launch to allow the user to modify the settings value. Assumed to be in the - * {@link #packageName} package. - */ - public final String settingsActivity; - - /** - * The user restriction associated with this setting. - */ - public final String userRestriction; - - private InjectedSetting(Builder builder) { - this.packageName = builder.mPackageName; - this.className = builder.mClassName; - this.title = builder.mTitle; - this.iconId = builder.mIconId; - this.mUserHandle = builder.mUserHandle; - this.settingsActivity = builder.mSettingsActivity; - this.userRestriction = builder.mUserRestriction; - } - - @Override - public String toString() { - return "InjectedSetting{" + - "mPackageName='" + packageName + '\'' + - ", mClassName='" + className + '\'' + - ", label=" + title + - ", iconId=" + iconId + - ", userId=" + mUserHandle.getIdentifier() + - ", settingsActivity='" + settingsActivity + '\'' + - ", userRestriction='" + userRestriction + - '}'; - } - - /** - * Returns the intent to start the {@link #className} service. - */ - public Intent getServiceIntent() { - Intent intent = new Intent(); - intent.setClassName(packageName, className); - return intent; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof InjectedSetting)) return false; - - InjectedSetting that = (InjectedSetting) o; - - return Objects.equals(packageName, that.packageName) - && Objects.equals(className, that.className) - && Objects.equals(title, that.title) - && Objects.equals(iconId, that.iconId) - && Objects.equals(mUserHandle, that.mUserHandle) - && Objects.equals(settingsActivity, that.settingsActivity) - && Objects.equals(userRestriction, that.userRestriction); - } - - @Override - public int hashCode() { - int result = packageName.hashCode(); - result = 31 * result + className.hashCode(); - result = 31 * result + title.hashCode(); - result = 31 * result + iconId; - result = 31 * result + (mUserHandle == null ? 0 : mUserHandle.hashCode()); - result = 31 * result + settingsActivity.hashCode(); - result = 31 * result + (userRestriction == null ? 0 : userRestriction.hashCode()); - return result; - } - - public static class Builder { - private String mPackageName; - private String mClassName; - private String mTitle; - private int mIconId; - private UserHandle mUserHandle; - private String mSettingsActivity; - private String mUserRestriction; - - public Builder setPackageName(String packageName) { - mPackageName = packageName; - return this; - } - - public Builder setClassName(String className) { - mClassName = className; - return this; - } - - public Builder setTitle(String title) { - mTitle = title; - return this; - } - - public Builder setIconId(int iconId) { - mIconId = iconId; - return this; - } - - public Builder setUserHandle(UserHandle userHandle) { - mUserHandle = userHandle; - return this; - } - - public Builder setSettingsActivity(String settingsActivity) { - mSettingsActivity = settingsActivity; - return this; - } - - public Builder setUserRestriction(String userRestriction) { - mUserRestriction = userRestriction; - return this; - } - - public InjectedSetting build() { - if (mPackageName == null || mClassName == null || TextUtils.isEmpty(mTitle) - || TextUtils.isEmpty(mSettingsActivity)) { - if (Log.isLoggable(SettingsInjector.TAG, Log.WARN)) { - Log.w(SettingsInjector.TAG, "Illegal setting specification: package=" - + mPackageName + ", class=" + mClassName - + ", title=" + mTitle + ", settingsActivity=" + mSettingsActivity); - } - return null; - } - return new InjectedSetting(this); - } - } -} diff --git a/src/com/android/settings/location/LocationEnabler.java b/src/com/android/settings/location/LocationEnabler.java index d45128cea3..db5397330e 100644 --- a/src/com/android/settings/location/LocationEnabler.java +++ b/src/com/android/settings/location/LocationEnabler.java @@ -13,8 +13,9 @@ */ package com.android.settings.location; -import android.app.ActivityManager; -import android.Manifest.permission; +import static com.android.settingslib.RestrictedLockUtilsInternal.checkIfRestrictionEnforced; +import static com.android.settingslib.Utils.updateLocationEnabled; + import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -23,26 +24,24 @@ import android.location.LocationManager; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; -import androidx.annotation.VisibleForTesting; import android.util.Log; +import androidx.annotation.VisibleForTesting; + import com.android.settings.Utils; import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.core.lifecycle.LifecycleObserver; -import com.android.settingslib.core.lifecycle.events.OnPause; -import com.android.settingslib.core.lifecycle.events.OnResume; - -import static com.android.settingslib.Utils.updateLocationMode; -import static com.android.settingslib.Utils.updateLocationEnabled; -import static com.android.settingslib.RestrictedLockUtils.checkIfRestrictionEnforced; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.core.lifecycle.events.OnStop; /** * A class that listens to location settings change and modifies location settings * settings. */ -public class LocationEnabler implements LifecycleObserver, OnResume, OnPause { +public class LocationEnabler implements LifecycleObserver, OnStart, OnStop { private static final String TAG = "LocationEnabler"; @VisibleForTesting @@ -72,7 +71,7 @@ public class LocationEnabler implements LifecycleObserver, OnResume, OnPause { } @Override - public void onResume() { + public void onStart() { if (mReceiver == null) { mReceiver = new BroadcastReceiver() { @Override @@ -89,12 +88,8 @@ public class LocationEnabler implements LifecycleObserver, OnResume, OnPause { } @Override - public void onPause() { - try { - mContext.unregisterReceiver(mReceiver); - } catch (RuntimeException e) { - // Ignore exceptions caused by race condition - } + public void onStop() { + mContext.unregisterReceiver(mReceiver); } void refreshLocationMode() { @@ -128,26 +123,6 @@ public class LocationEnabler implements LifecycleObserver, OnResume, OnPause { refreshLocationMode(); } - void setLocationMode(int mode) { - final int currentMode = Settings.Secure.getInt(mContext.getContentResolver(), - Settings.Secure.LOCATION_MODE, Settings.Secure.LOCATION_MODE_OFF); - if (isRestricted()) { - // Location toggling disabled by user restriction. Read the current location mode to - // update the location master switch. - if (Log.isLoggable(TAG, Log.INFO)) { - Log.i(TAG, "Restricted user, not setting location mode"); - } - if (mListener != null) { - mListener.onLocationModeChanged(currentMode, true); - } - return; - } - - updateLocationMode(mContext, currentMode, mode, ActivityManager.getCurrentUser(), - Settings.Secure.LOCATION_CHANGER_SYSTEM_SETTINGS); - refreshLocationMode(); - } - boolean isEnabled(int mode) { return mode != Settings.Secure.LOCATION_MODE_OFF && !isRestricted(); } @@ -168,14 +143,14 @@ public class LocationEnabler implements LifecycleObserver, OnResume, OnPause { mContext, UserManager.DISALLOW_SHARE_LOCATION, userId); if (admin == null) { - admin = RestrictedLockUtils.checkIfRestrictionEnforced( + admin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced( mContext, UserManager.DISALLOW_CONFIG_LOCATION, userId); } return admin; } boolean hasShareLocationRestriction(int userId) { - return RestrictedLockUtils.hasBaseUserRestriction( + return RestrictedLockUtilsInternal.hasBaseUserRestriction( mContext, UserManager.DISALLOW_SHARE_LOCATION, userId); } diff --git a/src/com/android/settings/location/LocationFooterPreferenceController.java b/src/com/android/settings/location/LocationFooterPreferenceController.java index 658c3cd452..55fea9f93c 100644 --- a/src/com/android/settings/location/LocationFooterPreferenceController.java +++ b/src/com/android/settings/location/LocationFooterPreferenceController.java @@ -22,14 +22,17 @@ import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; import android.location.LocationManager; +import android.util.Log; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceCategory; -import android.util.Log; + import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnPause; import com.android.settingslib.widget.FooterPreference; + import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -83,12 +86,10 @@ public class LocationFooterPreferenceController extends LocationBasePreferenceCo .getResourcesForApplication(data.applicationInfo) .getString(data.footerStringRes); } catch (NameNotFoundException exception) { - if (Log.isLoggable(TAG, Log.WARN)) { - Log.w( - TAG, - "Resources not found for application " - + data.applicationInfo.packageName); - } + Log.w( + TAG, + "Resources not found for application " + + data.applicationInfo.packageName); continue; } footerPreference.setTitle(footerString); @@ -149,11 +150,11 @@ public class LocationFooterPreferenceController extends LocationBasePreferenceCo mPackageManager.queryBroadcastReceivers( INJECT_INTENT, PackageManager.GET_META_DATA); if (resolveInfos == null) { - if (Log.isLoggable(TAG, Log.ERROR)) { - Log.e(TAG, "Unable to resolve intent " + INJECT_INTENT); - return Collections.emptyList(); - } - } else if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.e(TAG, "Unable to resolve intent " + INJECT_INTENT); + return Collections.emptyList(); + } + + if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Found broadcast receivers: " + resolveInfos); } @@ -164,30 +165,26 @@ public class LocationFooterPreferenceController extends LocationBasePreferenceCo // If a non-system app tries to inject footer, ignore it if ((appInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) { - if (Log.isLoggable(TAG, Log.WARN)) { - Log.w(TAG, "Ignoring attempt to inject footer from app not in system image: " - + resolveInfo); - continue; - } + Log.w(TAG, "Ignoring attempt to inject footer from app not in system image: " + + resolveInfo); + continue; } // Get the footer text resource id from broadcast receiver's metadata if (activityInfo.metaData == null) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "No METADATA in broadcast receiver " + activityInfo.name); - continue; } + continue; } final int footerTextRes = activityInfo.metaData.getInt(LocationManager.METADATA_SETTINGS_FOOTER_STRING); if (footerTextRes == 0) { - if (Log.isLoggable(TAG, Log.WARN)) { - Log.w( - TAG, - "No mapping of integer exists for " - + LocationManager.METADATA_SETTINGS_FOOTER_STRING); - } + Log.w( + TAG, + "No mapping of integer exists for " + + LocationManager.METADATA_SETTINGS_FOOTER_STRING); continue; } footerDataList.add( diff --git a/src/com/android/settings/location/LocationForWorkPreferenceController.java b/src/com/android/settings/location/LocationForWorkPreferenceController.java index 3f51d6f4db..1208ea2ee0 100644 --- a/src/com/android/settings/location/LocationForWorkPreferenceController.java +++ b/src/com/android/settings/location/LocationForWorkPreferenceController.java @@ -17,6 +17,7 @@ package com.android.settings.location; import android.content.Context; import android.os.UserManager; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; @@ -56,8 +57,7 @@ public class LocationForWorkPreferenceController extends LocationBasePreferenceC @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - mPreference = - (RestrictedSwitchPreference) screen.findPreference(KEY_MANAGED_PROFILE_SWITCH); + mPreference = screen.findPreference(KEY_MANAGED_PROFILE_SWITCH); } @Override diff --git a/src/com/android/settings/location/LocationPreferenceController.java b/src/com/android/settings/location/LocationPreferenceController.java deleted file mode 100644 index a091ca82b0..0000000000 --- a/src/com/android/settings/location/LocationPreferenceController.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright (C) 2017 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.location; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.location.LocationManager; -import android.provider.Settings.Secure; -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; -import com.android.settings.R; -import com.android.settings.core.PreferenceControllerMixin; -import com.android.settingslib.core.AbstractPreferenceController; -import com.android.settings.search.DatabaseIndexingUtils; -import com.android.settings.search.InlineListPayload; -import com.android.settings.search.ResultPayload; -import com.android.settingslib.core.lifecycle.Lifecycle; -import com.android.settingslib.core.lifecycle.LifecycleObserver; -import com.android.settingslib.core.lifecycle.events.OnPause; -import com.android.settingslib.core.lifecycle.events.OnResume; - -public class LocationPreferenceController extends AbstractPreferenceController - implements PreferenceControllerMixin, LifecycleObserver, OnResume, OnPause { - - private static final String KEY_LOCATION = "location"; - private Context mContext; - private Preference mPreference; - - @VisibleForTesting - BroadcastReceiver mLocationProvidersChangedReceiver; - - public LocationPreferenceController(Context context, Lifecycle lifecycle) { - super(context); - mContext = context; - mLocationProvidersChangedReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (intent.getAction().equals(LocationManager.PROVIDERS_CHANGED_ACTION)) { - updateSummary(); - } - } - }; - if (lifecycle != null) { - lifecycle.addObserver(this); - } - } - - @Override - public void displayPreference(PreferenceScreen screen) { - super.displayPreference(screen); - mPreference = screen.findPreference(KEY_LOCATION); - } - - @Override - public void onResume() { - if (mLocationProvidersChangedReceiver != null) { - mContext.registerReceiver(mLocationProvidersChangedReceiver, new IntentFilter( - LocationManager.PROVIDERS_CHANGED_ACTION)); - } - } - - @Override - public void onPause() { - if (mLocationProvidersChangedReceiver != null) { - mContext.unregisterReceiver(mLocationProvidersChangedReceiver); - } - } - - @Override - public void updateState(Preference preference) { - preference.setSummary(getLocationSummary(mContext)); - } - - @Override - public String getPreferenceKey() { - return KEY_LOCATION; - } - - @Override - public boolean isAvailable() { - return true; - } - - public void updateSummary() { - updateState(mPreference); - } - - public static String getLocationSummary(Context context) { - int mode = Secure.getInt(context.getContentResolver(), - Secure.LOCATION_MODE, Secure.LOCATION_MODE_OFF); - if (mode != Secure.LOCATION_MODE_OFF) { - return context.getString(R.string.location_on_summary); - } - return context.getString(R.string.location_off_summary); - } - - @Override - public ResultPayload getResultPayload() { - final Intent intent = DatabaseIndexingUtils.buildSearchResultPageIntent(mContext, - LocationSettings.class.getName(), KEY_LOCATION, - mContext.getString(R.string.location_settings_title)); - - return new InlineListPayload(Secure.LOCATION_MODE, - ResultPayload.SettingsSource.SECURE, intent, isAvailable(), - Secure.LOCATION_MODE_HIGH_ACCURACY + 1, Secure.LOCATION_MODE_OFF); - } -} diff --git a/src/com/android/settings/location/LocationScanningPreferenceController.java b/src/com/android/settings/location/LocationScanningPreferenceController.java index 33fe612913..2c05a39e54 100644 --- a/src/com/android/settings/location/LocationScanningPreferenceController.java +++ b/src/com/android/settings/location/LocationScanningPreferenceController.java @@ -17,19 +17,40 @@ package com.android.settings.location; import android.content.Context; - -import com.android.settings.core.BasePreferenceController; -import com.android.settings.R; +import android.provider.Settings; import androidx.annotation.VisibleForTesting; +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; -public class LocationScanningPreferenceController extends BasePreferenceController { +public class LocationScanningPreferenceController extends BasePreferenceController { @VisibleForTesting static final String KEY_LOCATION_SCANNING = "location_scanning"; + private final Context mContext; public LocationScanningPreferenceController(Context context) { super(context, KEY_LOCATION_SCANNING); + mContext = context; + } + + @Override + public CharSequence getSummary() { + final boolean wifiScanOn = Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.WIFI_SCAN_ALWAYS_AVAILABLE, 0) == 1; + final boolean bleScanOn = Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.BLE_SCAN_ALWAYS_AVAILABLE, 0) == 1; + int resId; + if (wifiScanOn && bleScanOn) { + resId = R.string.scanning_status_text_wifi_on_ble_on; + } else if (wifiScanOn && !bleScanOn) { + resId = R.string.scanning_status_text_wifi_on_ble_off; + } else if (!wifiScanOn && bleScanOn) { + resId = R.string.scanning_status_text_wifi_off_ble_on; + } else { + resId = R.string.scanning_status_text_wifi_off_ble_off; + } + return mContext.getString(resId); } @AvailabilityStatus diff --git a/src/com/android/settings/location/LocationServicePreferenceController.java b/src/com/android/settings/location/LocationServicePreferenceController.java index f865b44904..70246cbe3a 100644 --- a/src/com/android/settings/location/LocationServicePreferenceController.java +++ b/src/com/android/settings/location/LocationServicePreferenceController.java @@ -19,12 +19,14 @@ import android.content.Intent; import android.content.IntentFilter; import android.location.SettingInjectorService; import android.os.UserHandle; +import android.util.Log; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceScreen; -import android.util.Log; +import com.android.settings.Utils; import com.android.settings.widget.RestrictedAppPreference; import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.core.lifecycle.LifecycleObserver; @@ -32,6 +34,7 @@ import com.android.settingslib.core.lifecycle.events.OnPause; import com.android.settingslib.core.lifecycle.events.OnResume; import java.util.List; +import java.util.Map; public class LocationServicePreferenceController extends LocationBasePreferenceController implements LifecycleObserver, OnResume, OnPause { @@ -39,25 +42,28 @@ public class LocationServicePreferenceController extends LocationBasePreferenceC private static final String TAG = "LocationServicePrefCtrl"; /** Key for preference category "Location services" */ private static final String KEY_LOCATION_SERVICES = "location_services"; + /** Key for preference category "Location services for work" */ + private static final String KEY_LOCATION_SERVICES_MANAGED = "location_services_managed_profile"; @VisibleForTesting static final IntentFilter INTENT_FILTER_INJECTED_SETTING_CHANGED = new IntentFilter(SettingInjectorService.ACTION_INJECTED_SETTING_CHANGED); private PreferenceCategory mCategoryLocationServices; + private PreferenceCategory mCategoryLocationServicesManaged; private final LocationSettings mFragment; - private final SettingsInjector mInjector; - /** Receives UPDATE_INTENT */ + private final AppSettingsInjector mInjector; + /** Receives UPDATE_INTENT */ @VisibleForTesting BroadcastReceiver mInjectedSettingsReceiver; public LocationServicePreferenceController(Context context, LocationSettings fragment, Lifecycle lifecycle) { - this(context, fragment, lifecycle, new SettingsInjector(context)); + this(context, fragment, lifecycle, new AppSettingsInjector(context)); } @VisibleForTesting LocationServicePreferenceController(Context context, LocationSettings fragment, - Lifecycle lifecycle, SettingsInjector injector) { + Lifecycle lifecycle, AppSettingsInjector injector) { super(context, lifecycle); mFragment = fragment; mInjector = injector; @@ -72,30 +78,36 @@ public class LocationServicePreferenceController extends LocationBasePreferenceC } @Override - public boolean isAvailable() { - // If managed profile has lock-down on location access then its injected location services - // must not be shown. - return mInjector.hasInjectedSettings(mLocationEnabler.isManagedProfileRestrictedByBase() - ? UserHandle.myUserId() : UserHandle.USER_CURRENT); - } - - @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - mCategoryLocationServices = - (PreferenceCategory) screen.findPreference(KEY_LOCATION_SERVICES); + mCategoryLocationServices = screen.findPreference(KEY_LOCATION_SERVICES); + mCategoryLocationServicesManaged = screen.findPreference(KEY_LOCATION_SERVICES_MANAGED); } @Override public void updateState(Preference preference) { mCategoryLocationServices.removeAll(); - final List<Preference> prefs = getLocationServices(); - for (Preference pref : prefs) { - if (pref instanceof RestrictedAppPreference) { - ((RestrictedAppPreference) pref).checkRestrictionAndSetDisabled(); + mCategoryLocationServicesManaged.removeAll(); + final Map<Integer, List<Preference>> prefs = getLocationServices(); + boolean showPrimary = false; + boolean showManaged = false; + for (Map.Entry<Integer, List<Preference>> entry : prefs.entrySet()) { + for (Preference pref : entry.getValue()) { + if (pref instanceof RestrictedAppPreference) { + ((RestrictedAppPreference) pref).checkRestrictionAndSetDisabled(); + } + } + if (entry.getKey() == UserHandle.myUserId()) { + LocationSettings.addPreferencesSorted(entry.getValue(), mCategoryLocationServices); + showPrimary = true; + } else { + LocationSettings.addPreferencesSorted(entry.getValue(), + mCategoryLocationServicesManaged); + showManaged = true; } } - LocationSettings.addPreferencesSorted(prefs, mCategoryLocationServices); + mCategoryLocationServices.setVisible(showPrimary); + mCategoryLocationServicesManaged.setVisible(showManaged); } @Override @@ -127,11 +139,14 @@ public class LocationServicePreferenceController extends LocationBasePreferenceC mContext.unregisterReceiver(mInjectedSettingsReceiver); } - private List<Preference> getLocationServices() { + private Map<Integer, List<Preference>> getLocationServices() { // If location access is locked down by device policy then we only show injected settings // for the primary profile. + final int profileUserId = Utils.getManagedProfileId(mUserManager, UserHandle.myUserId()); + return mInjector.getInjectedSettings(mFragment.getPreferenceManager().getContext(), - mLocationEnabler.isManagedProfileRestrictedByBase() + (profileUserId != UserHandle.USER_NULL + && mLocationEnabler.getShareLocationEnforcedAdmin(profileUserId) != null) ? UserHandle.myUserId() : UserHandle.USER_CURRENT); } } diff --git a/src/com/android/settings/location/LocationSettings.java b/src/com/android/settings/location/LocationSettings.java index 8b0ee8a496..71424867d6 100644 --- a/src/com/android/settings/location/LocationSettings.java +++ b/src/com/android/settings/location/LocationSettings.java @@ -16,24 +16,26 @@ package com.android.settings.location; -import android.app.Activity; +import android.app.settings.SettingsEnums; import android.content.Context; import android.location.SettingInjectorService; import android.os.Bundle; import android.provider.SearchIndexableResource; + import androidx.preference.Preference; import androidx.preference.PreferenceGroup; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; + import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.dashboard.DashboardFragment; -import com.android.settings.dashboard.SummaryLoader; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; import com.android.settings.widget.SwitchBar; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.location.RecentLocationApps; +import com.android.settingslib.search.SearchIndexable; + import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -58,6 +60,7 @@ import java.util.List; * other things, this simplifies integration with future changes to the default (AOSP) * implementation. */ +@SearchIndexable public class LocationSettings extends DashboardFragment { private static final String TAG = "LocationSettings"; @@ -66,7 +69,7 @@ public class LocationSettings extends DashboardFragment { @Override public int getMetricsCategory() { - return MetricsEvent.LOCATION; + return SettingsEnums.LOCATION; } @Override @@ -76,7 +79,8 @@ public class LocationSettings extends DashboardFragment { final SwitchBar switchBar = activity.getSwitchBar(); switchBar.setSwitchBarText(R.string.location_settings_master_switch_title, R.string.location_settings_master_switch_title); - mSwitchBarController = new LocationSwitchBarController(activity, switchBar, getLifecycle()); + mSwitchBarController = new LocationSwitchBarController(activity, switchBar, + getSettingsLifecycle()); switchBar.show(); } @@ -92,17 +96,13 @@ public class LocationSettings extends DashboardFragment { @Override protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { - return buildPreferenceControllers(context, this, getLifecycle()); + return buildPreferenceControllers(context, this, getSettingsLifecycle()); } static void addPreferencesSorted(List<Preference> prefs, PreferenceGroup container) { // If there's some items to display, sort the items and add them to the container. - Collections.sort(prefs, new Comparator<Preference>() { - @Override - public int compare(Preference lhs, Preference rhs) { - return lhs.getTitle().toString().compareTo(rhs.getTitle().toString()); - } - }); + Collections.sort(prefs, + Comparator.comparing(lhs -> lhs.getTitle().toString())); for (Preference entry : prefs) { container.addPreference(entry); } @@ -116,45 +116,15 @@ public class LocationSettings extends DashboardFragment { private static List<AbstractPreferenceController> buildPreferenceControllers( Context context, LocationSettings fragment, Lifecycle lifecycle) { final List<AbstractPreferenceController> controllers = new ArrayList<>(); - controllers.add(new AppLocationPermissionPreferenceController(context)); + controllers.add(new AppLocationPermissionPreferenceController(context, lifecycle)); controllers.add(new LocationForWorkPreferenceController(context, lifecycle)); - controllers.add( - new RecentLocationRequestPreferenceController(context, fragment, lifecycle)); + controllers.add(new RecentLocationRequestPreferenceController(context, fragment, lifecycle)); controllers.add(new LocationScanningPreferenceController(context)); - controllers.add( - new LocationServicePreferenceController(context, fragment, lifecycle)); + controllers.add(new LocationServicePreferenceController(context, fragment, lifecycle)); controllers.add(new LocationFooterPreferenceController(context, lifecycle)); return controllers; } - private static class SummaryProvider implements SummaryLoader.SummaryProvider { - - private final Context mContext; - private final SummaryLoader mSummaryLoader; - - public SummaryProvider(Context context, SummaryLoader summaryLoader) { - mContext = context; - mSummaryLoader = summaryLoader; - } - - @Override - public void setListening(boolean listening) { - if (listening) { - mSummaryLoader.setSummary( - this, LocationPreferenceController.getLocationSummary(mContext)); - } - } - } - - public static final SummaryLoader.SummaryProviderFactory SUMMARY_PROVIDER_FACTORY - = new SummaryLoader.SummaryProviderFactory() { - @Override - public SummaryLoader.SummaryProvider createSummaryProvider(Activity activity, - SummaryLoader summaryLoader) { - return new SummaryProvider(activity, summaryLoader); - } - }; - /** * For Search. */ diff --git a/src/com/android/settings/location/LocationSlice.java b/src/com/android/settings/location/LocationSlice.java new file mode 100644 index 0000000000..275969046a --- /dev/null +++ b/src/com/android/settings/location/LocationSlice.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2018 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.location; + +import static android.provider.SettingsSlicesContract.KEY_LOCATION; + +import static androidx.slice.builders.ListBuilder.ICON_IMAGE; + +import android.annotation.ColorInt; +import android.app.PendingIntent; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; + +import androidx.core.graphics.drawable.IconCompat; +import androidx.slice.Slice; +import androidx.slice.builders.ListBuilder; +import androidx.slice.builders.ListBuilder.RowBuilder; +import androidx.slice.builders.SliceAction; + +import com.android.settings.R; +import com.android.settings.SubSettings; +import com.android.settings.Utils; +import com.android.settings.slices.CustomSliceRegistry; +import com.android.settings.slices.CustomSliceable; +import com.android.settings.slices.SliceBuilderUtils; + +/** + * Utility class to build an intent-based Location Slice. + */ +public class LocationSlice implements CustomSliceable { + + private final Context mContext; + + public LocationSlice(Context context) { + mContext = context; + } + + @Override + public Slice getSlice() { + final IconCompat icon = IconCompat.createWithResource(mContext, + com.android.internal.R.drawable.ic_signal_location); + final CharSequence title = mContext.getText(R.string.location_settings_title); + @ColorInt final int color = Utils.getColorAccentDefaultColor(mContext); + final PendingIntent primaryAction = getPrimaryAction(); + final SliceAction primarySliceAction = SliceAction.createDeeplink(primaryAction, icon, + ListBuilder.ICON_IMAGE, title); + + return new ListBuilder(mContext, CustomSliceRegistry.LOCATION_SLICE_URI, + ListBuilder.INFINITY) + .setAccentColor(color) + .addRow(new RowBuilder() + .setTitle(title) + .setTitleItem(icon, ICON_IMAGE) + .setPrimaryAction(primarySliceAction)) + .build(); + } + + @Override + public Uri getUri() { + return CustomSliceRegistry.LOCATION_SLICE_URI; + } + + @Override + public void onNotifyChange(Intent intent) { + + } + + @Override + public Intent getIntent() { + final String screenTitle = mContext.getText(R.string.location_settings_title).toString(); + final Uri contentUri = new Uri.Builder().appendPath(KEY_LOCATION).build(); + return SliceBuilderUtils.buildSearchResultPageIntent(mContext, + LocationSettings.class.getName(), KEY_LOCATION, screenTitle, + SettingsEnums.LOCATION) + .setClassName(mContext.getPackageName(), SubSettings.class.getName()) + .setData(contentUri); + } + + private PendingIntent getPrimaryAction() { + final Intent intent = getIntent(); + return PendingIntent.getActivity(mContext, 0 /* requestCode */, + intent, 0 /* flags */); + } +} diff --git a/src/com/android/settings/location/LocationSliceBuilder.java b/src/com/android/settings/location/LocationSliceBuilder.java deleted file mode 100644 index 160d90a639..0000000000 --- a/src/com/android/settings/location/LocationSliceBuilder.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright (C) 2018 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.location; - -import static android.provider.SettingsSlicesContract.KEY_LOCATION; - -import static androidx.slice.builders.ListBuilder.ICON_IMAGE; - -import android.annotation.ColorInt; -import android.app.PendingIntent; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.provider.SettingsSlicesContract; - -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.settings.R; -import com.android.settings.SubSettings; -import com.android.settings.Utils; -import com.android.settings.search.DatabaseIndexingUtils; - -import androidx.slice.Slice; -import androidx.slice.builders.ListBuilder; -import androidx.slice.builders.SliceAction; - -import androidx.core.graphics.drawable.IconCompat; - -/** - * Utility class to build an intent-based Location Slice. - */ -public class LocationSliceBuilder { - - /** - * Backing Uri for the Location Slice. - */ - public static final Uri LOCATION_URI = new Uri.Builder() - .scheme(ContentResolver.SCHEME_CONTENT) - .authority(SettingsSlicesContract.AUTHORITY) - .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) - .appendPath(KEY_LOCATION) - .build(); - - private LocationSliceBuilder() { - } - - /** - * Return a Location Slice bound to {@link #LOCATION_URI}. - */ - public static Slice getSlice(Context context) { - final IconCompat icon = IconCompat.createWithResource(context, - R.drawable.ic_signal_location); - final String title = context.getString(R.string.location_settings_title); - @ColorInt final int color = Utils.getColorAccent(context); - final PendingIntent primaryAction = getPrimaryAction(context); - final SliceAction primarySliceAction = new SliceAction(primaryAction, icon, title); - - return new ListBuilder(context, LOCATION_URI, ListBuilder.INFINITY) - .setAccentColor(color) - .addRow(b -> b - .setTitle(title) - .setTitleItem(icon, ICON_IMAGE) - .setPrimaryAction(primarySliceAction)) - .build(); - } - - public static Intent getIntent(Context context) { - final String screenTitle = context.getText(R.string.location_settings_title).toString(); - final Uri contentUri = new Uri.Builder().appendPath(KEY_LOCATION).build(); - return DatabaseIndexingUtils.buildSearchResultPageIntent(context, - LocationSettings.class.getName(), KEY_LOCATION, screenTitle, - MetricsEvent.LOCATION) - .setClassName(context.getPackageName(), SubSettings.class.getName()) - .setData(contentUri); - } - - private static PendingIntent getPrimaryAction(Context context) { - final Intent intent = getIntent(context); - return PendingIntent.getActivity(context, 0 /* requestCode */, - intent, 0 /* flags */); - } -} diff --git a/src/com/android/settings/location/LocationSwitchBarController.java b/src/com/android/settings/location/LocationSwitchBarController.java index 70ecc1eb85..7aa66ce204 100644 --- a/src/com/android/settings/location/LocationSwitchBarController.java +++ b/src/com/android/settings/location/LocationSwitchBarController.java @@ -16,6 +16,7 @@ package com.android.settings.location; import android.content.Context; import android.os.UserHandle; import android.widget.Switch; + import com.android.settings.widget.SwitchBar; import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.core.lifecycle.Lifecycle; diff --git a/src/com/android/settings/location/RecentLocationAccessPreferenceController.java b/src/com/android/settings/location/RecentLocationAccessPreferenceController.java new file mode 100644 index 0000000000..2f0dafdf6c --- /dev/null +++ b/src/com/android/settings/location/RecentLocationAccessPreferenceController.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2017 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.location; + +import static java.util.concurrent.TimeUnit.DAYS; + +import android.Manifest; +import android.content.Context; +import android.content.Intent; +import android.icu.text.RelativeDateTimeFormatter; +import android.provider.DeviceConfig; +import android.view.View; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.Utils; +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.location.RecentLocationAccesses; +import com.android.settingslib.utils.StringUtil; +import com.android.settingslib.widget.AppEntitiesHeaderController; +import com.android.settingslib.widget.AppEntityInfo; +import com.android.settingslib.widget.LayoutPreference; + +import java.util.List; + +public class RecentLocationAccessPreferenceController extends AbstractPreferenceController + implements PreferenceControllerMixin { + /** Key for the recent location apps dashboard */ + private static final String KEY_APPS_DASHBOARD = "apps_dashboard"; + private final RecentLocationAccesses mRecentLocationAccesses; + private AppEntitiesHeaderController mController; + private static final int MAXIMUM_APP_COUNT = 3; + + public RecentLocationAccessPreferenceController(Context context) { + this(context, new RecentLocationAccesses(context)); + } + + @VisibleForTesting + RecentLocationAccessPreferenceController(Context context, + RecentLocationAccesses recentAccesses) { + super(context); + mRecentLocationAccesses = recentAccesses; + } + + @Override + public String getPreferenceKey() { + return KEY_APPS_DASHBOARD; + } + + @Override + public boolean isAvailable() { + return false; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + final LayoutPreference preference = screen.findPreference(KEY_APPS_DASHBOARD); + final View view = preference.findViewById(R.id.app_entities_header); + mController = AppEntitiesHeaderController.newInstance(mContext, view) + .setHeaderTitleRes(R.string.location_category_recent_location_access) + .setHeaderDetailsRes(R.string.location_recent_location_access_view_details) + .setHeaderEmptyRes(R.string.location_no_recent_accesses) + .setHeaderDetailsClickListener((View v) -> { + final Intent intent = new Intent(Intent.ACTION_REVIEW_PERMISSION_USAGE); + intent.putExtra(Intent.EXTRA_PERMISSION_NAME, + Manifest.permission.ACCESS_FINE_LOCATION); + intent.putExtra(Intent.EXTRA_DURATION_MILLIS, DAYS.toMillis(1)); + mContext.startActivity(intent); + }); + } + + @Override + public void updateState(Preference preference) { + updateRecentApps(); + } + + private void updateRecentApps() { + final List<RecentLocationAccesses.Access> recentLocationAccesses = + mRecentLocationAccesses.getAppListSorted(); + if (recentLocationAccesses.size() > 0) { + // Display the top 3 preferences to container in original order. + int i = 0; + for (; i < Math.min(recentLocationAccesses.size(), MAXIMUM_APP_COUNT); i++) { + final RecentLocationAccesses.Access access = recentLocationAccesses.get(i); + final AppEntityInfo appEntityInfo = new AppEntityInfo.Builder() + .setIcon(access.icon) + .setTitle(access.label) + .setSummary(StringUtil.formatRelativeTime(mContext, + System.currentTimeMillis() - access.accessFinishTime, false, + RelativeDateTimeFormatter.Style.SHORT)) + .setOnClickListener((v) -> { + final Intent intent = new Intent(Intent.ACTION_MANAGE_APP_PERMISSION); + intent.putExtra(Intent.EXTRA_PERMISSION_NAME, + Manifest.permission.ACCESS_FINE_LOCATION); + intent.putExtra(Intent.EXTRA_PACKAGE_NAME, access.packageName); + intent.putExtra(Intent.EXTRA_USER, access.userHandle); + mContext.startActivity(intent); + }) + .build(); + mController.setAppEntity(i, appEntityInfo); + } + for (; i < MAXIMUM_APP_COUNT; i++) { + mController.removeAppEntity(i); + } + } + mController.apply(); + } +} diff --git a/src/com/android/settings/location/RecentLocationRequestPreferenceController.java b/src/com/android/settings/location/RecentLocationRequestPreferenceController.java index f4ae15bdc5..fb8c62c657 100644 --- a/src/com/android/settings/location/RecentLocationRequestPreferenceController.java +++ b/src/com/android/settings/location/RecentLocationRequestPreferenceController.java @@ -16,21 +16,23 @@ package com.android.settings.location; import android.content.Context; import android.os.Bundle; import android.os.UserHandle; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceScreen; + import com.android.settings.R; import com.android.settings.applications.appinfo.AppInfoDashboardFragment; import com.android.settings.core.SubSettingLauncher; import com.android.settings.dashboard.DashboardFragment; -import com.android.settings.widget.AppPreference; import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.location.RecentLocationApps; +import com.android.settingslib.widget.apppreference.AppPreference; + import java.util.List; public class RecentLocationRequestPreferenceController extends LocationBasePreferenceController { - /** Key for preference category "Recent location requests" */ private static final String KEY_RECENT_LOCATION_REQUESTS = "recent_location_requests"; @VisibleForTesting @@ -38,9 +40,8 @@ public class RecentLocationRequestPreferenceController extends LocationBasePrefe private final LocationSettings mFragment; private final RecentLocationApps mRecentLocationApps; private PreferenceCategory mCategoryRecentLocationRequests; - private Preference mSeeAllButton; - /** Used in this class and {@link RecentLocationRequestSeeAllPreferenceController}*/ + /** Used in this class and {@link RecentLocationRequestSeeAllPreferenceController} */ static class PackageEntryClickedListener implements Preference.OnPreferenceClickListener { private final DashboardFragment mFragment; private final String mPackage; @@ -58,11 +59,10 @@ public class RecentLocationRequestPreferenceController extends LocationBasePrefe // start new fragment to display extended information final Bundle args = new Bundle(); args.putString(AppInfoDashboardFragment.ARG_PACKAGE_NAME, mPackage); - new SubSettingLauncher(mFragment.getContext()) .setDestination(AppInfoDashboardFragment.class.getName()) .setArguments(args) - .setTitle(R.string.application_info_label) + .setTitleRes(R.string.application_info_label) .setUserHandle(mUserHandle) .setSourceMetricsCategory(mFragment.getMetricsCategory()) .launch(); @@ -93,27 +93,20 @@ public class RecentLocationRequestPreferenceController extends LocationBasePrefe super.displayPreference(screen); mCategoryRecentLocationRequests = (PreferenceCategory) screen.findPreference(KEY_RECENT_LOCATION_REQUESTS); - mSeeAllButton = screen.findPreference(KEY_SEE_ALL_BUTTON); - } @Override public void updateState(Preference preference) { mCategoryRecentLocationRequests.removeAll(); - mSeeAllButton.setVisible(false); - final Context prefContext = preference.getContext(); final List<RecentLocationApps.Request> recentLocationRequests = - mRecentLocationApps.getAppListSorted(); - + mRecentLocationApps.getAppListSorted(false); if (recentLocationRequests.size() > 3) { // Display the top 3 preferences to container in original order. - for (int i = 0; i < 3; i ++) { + for (int i = 0; i < 3; i++) { mCategoryRecentLocationRequests.addPreference( createAppPreference(prefContext, recentLocationRequests.get(i))); } - // Display a button to list all requests - mSeeAllButton.setVisible(true); } else if (recentLocationRequests.size() > 0) { // Add preferences to container in original order (already sorted by recency). for (RecentLocationApps.Request request : recentLocationRequests) { diff --git a/src/com/android/settings/location/RecentLocationRequestSeeAllFragment.java b/src/com/android/settings/location/RecentLocationRequestSeeAllFragment.java index 0b7614c22b..9e4a77fdb2 100644 --- a/src/com/android/settings/location/RecentLocationRequestSeeAllFragment.java +++ b/src/com/android/settings/location/RecentLocationRequestSeeAllFragment.java @@ -13,12 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.android.settings.location; - import android.content.Context; import android.provider.SearchIndexableResource; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; + import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; @@ -26,21 +28,30 @@ import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.search.SearchIndexable; + import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** Dashboard Fragment to display all recent location requests, sorted by recency. */ +@SearchIndexable public class RecentLocationRequestSeeAllFragment extends DashboardFragment { - private static final String TAG = "RecentLocationReqAll"; - public static final String PATH = "com.android.settings.location.RecentLocationRequestSeeAllFragment"; + private static final int MENU_SHOW_SYSTEM = Menu.FIRST + 1; + private static final int MENU_HIDE_SYSTEM = Menu.FIRST + 2; + + private boolean mShowSystem = false; + private MenuItem mShowSystemMenu; + private MenuItem mHideSystemMenu; + private RecentLocationRequestSeeAllPreferenceController mController; + @Override public int getMetricsCategory() { - return MetricsEvent.RECENT_LOCATION_REQUESTS_ALL; + return MetricsEvent.RECENT_LOCATION_REQUESTS_ALL; } @Override @@ -50,19 +61,44 @@ public class RecentLocationRequestSeeAllFragment extends DashboardFragment { @Override protected String getLogTag() { - return TAG; + return TAG; } @Override protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { - return buildPreferenceControllers(context, getLifecycle(), this); + return buildPreferenceControllers(context, getSettingsLifecycle(), this); + } + + @Override + public boolean onOptionsItemSelected(MenuItem menuItem) { + switch (menuItem.getItemId()) { + case MENU_SHOW_SYSTEM: + case MENU_HIDE_SYSTEM: + mShowSystem = menuItem.getItemId() == MENU_SHOW_SYSTEM; + updateMenu(); + if (mController != null) { + mController.setShowSystem(mShowSystem); + } + return true; + default: + return super.onOptionsItemSelected(menuItem); + } + } + + private void updateMenu() { + mShowSystemMenu.setVisible(!mShowSystem); + mHideSystemMenu.setVisible(mShowSystem); } private static List<AbstractPreferenceController> buildPreferenceControllers( Context context, Lifecycle lifecycle, RecentLocationRequestSeeAllFragment fragment) { final List<AbstractPreferenceController> controllers = new ArrayList<>(); - controllers.add( - new RecentLocationRequestSeeAllPreferenceController(context, lifecycle, fragment)); + final RecentLocationRequestSeeAllPreferenceController controller = + new RecentLocationRequestSeeAllPreferenceController(context, lifecycle, fragment); + controllers.add(controller); + if (fragment != null) { + fragment.mController = controller; + } return controllers; } @@ -86,4 +122,14 @@ public class RecentLocationRequestSeeAllFragment extends DashboardFragment { context, /* lifecycle = */ null, /* fragment = */ null); } }; + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + mShowSystemMenu = menu.add(Menu.NONE, MENU_SHOW_SYSTEM, Menu.NONE, + R.string.menu_show_system); + mHideSystemMenu = menu.add(Menu.NONE, MENU_HIDE_SYSTEM, Menu.NONE, + R.string.menu_hide_system); + updateMenu(); + } } diff --git a/src/com/android/settings/location/RecentLocationRequestSeeAllPreferenceController.java b/src/com/android/settings/location/RecentLocationRequestSeeAllPreferenceController.java index 99dc9fc348..3abccf7d23 100644 --- a/src/com/android/settings/location/RecentLocationRequestSeeAllPreferenceController.java +++ b/src/com/android/settings/location/RecentLocationRequestSeeAllPreferenceController.java @@ -13,28 +13,33 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.android.settings.location; import android.content.Context; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceScreen; -import com.android.settings.widget.AppPreference; + import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.location.RecentLocationApps; +import com.android.settingslib.widget.apppreference.AppPreference; + import java.util.List; +import com.android.settings.R; + /** Preference controller for preference category displaying all recent location requests. */ public class RecentLocationRequestSeeAllPreferenceController extends LocationBasePreferenceController { - /** Key for preference category "All recent location requests" */ private static final String KEY_ALL_RECENT_LOCATION_REQUESTS = "all_recent_location_requests"; private final RecentLocationRequestSeeAllFragment mFragment; private PreferenceCategory mCategoryAllRecentLocationRequests; private RecentLocationApps mRecentLocationApps; + private boolean mShowSystem = false; + private Preference mPreference; public RecentLocationRequestSeeAllPreferenceController( Context context, Lifecycle lifecycle, RecentLocationRequestSeeAllFragment fragment) { @@ -67,16 +72,25 @@ public class RecentLocationRequestSeeAllPreferenceController super.displayPreference(screen); mCategoryAllRecentLocationRequests = (PreferenceCategory) screen.findPreference(KEY_ALL_RECENT_LOCATION_REQUESTS); - } @Override public void updateState(Preference preference) { mCategoryAllRecentLocationRequests.removeAll(); - List<RecentLocationApps.Request> requests = mRecentLocationApps.getAppListSorted(); - for (RecentLocationApps.Request request : requests) { - Preference appPreference = createAppPreference(preference.getContext(), request); - mCategoryAllRecentLocationRequests.addPreference(appPreference); + mPreference = preference; + List<RecentLocationApps.Request> requests = mRecentLocationApps.getAppListSorted( + mShowSystem); + if (requests.isEmpty()) { + // If there's no item to display, add a "No recent apps" item. + final Preference banner = new AppPreference(mContext); + banner.setTitle(R.string.location_no_recent_apps); + banner.setSelectable(false); + mCategoryAllRecentLocationRequests.addPreference(banner); + } else { + for (RecentLocationApps.Request request : requests) { + Preference appPreference = createAppPreference(preference.getContext(), request); + mCategoryAllRecentLocationRequests.addPreference(appPreference); + } } } @@ -92,4 +106,11 @@ public class RecentLocationRequestSeeAllPreferenceController mFragment, request.packageName, request.userHandle)); return pref; } + + public void setShowSystem(boolean showSystem) { + mShowSystem = showSystem; + if (mPreference != null) { + updateState(mPreference); + } + } } diff --git a/src/com/android/settings/location/ScanningSettings.java b/src/com/android/settings/location/ScanningSettings.java index 91359520ea..31ec9553d3 100644 --- a/src/com/android/settings/location/ScanningSettings.java +++ b/src/com/android/settings/location/ScanningSettings.java @@ -16,15 +16,16 @@ package com.android.settings.location; +import android.app.settings.SettingsEnums; import android.content.Context; import android.provider.SearchIndexableResource; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.Arrays; @@ -33,12 +34,13 @@ import java.util.List; /** * A page that configures the background scanning settings for Wi-Fi and Bluetooth. */ +@SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC) public class ScanningSettings extends DashboardFragment { private static final String TAG = "ScanningSettings"; @Override public int getMetricsCategory() { - return MetricsEvent.LOCATION_SCANNING; + return SettingsEnums.LOCATION_SCANNING; } @Override diff --git a/src/com/android/settings/location/SettingsInjector.java b/src/com/android/settings/location/SettingsInjector.java deleted file mode 100644 index 52b06564f7..0000000000 --- a/src/com/android/settings/location/SettingsInjector.java +++ /dev/null @@ -1,576 +0,0 @@ -/* - * 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.location; - -import android.app.ActivityManager; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageItemInfo; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.content.pm.ServiceInfo; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.content.res.XmlResourceParser; -import android.graphics.drawable.Drawable; -import android.location.SettingInjectorService; -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -import android.os.Messenger; -import android.os.SystemClock; -import android.os.UserHandle; -import android.os.UserManager; -import androidx.preference.Preference; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.util.IconDrawableFactory; -import android.util.Log; -import android.util.Xml; - -import com.android.settings.widget.AppPreference; -import com.android.settings.widget.RestrictedAppPreference; - -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Set; - -/** - * Adds the preferences specified by the {@link InjectedSetting} objects to a preference group. - * - * Duplicates some code from {@link android.content.pm.RegisteredServicesCache}. We do not use that - * class directly because it is not a good match for our use case: we do not need the caching, and - * so do not want the additional resource hit at app install/upgrade time; and we would have to - * suppress the tie-breaking between multiple services reporting settings with the same name. - * Code-sharing would require extracting {@link - * android.content.pm.RegisteredServicesCache#parseServiceAttributes(android.content.res.Resources, - * String, android.util.AttributeSet)} into an interface, which didn't seem worth it. - */ -class SettingsInjector { - static final String TAG = "SettingsInjector"; - - /** - * If reading the status of a setting takes longer than this, we go ahead and start reading - * the next setting. - */ - private static final long INJECTED_STATUS_UPDATE_TIMEOUT_MILLIS = 1000; - - /** - * {@link Message#what} value for starting to load status values - * in case we aren't already in the process of loading them. - */ - private static final int WHAT_RELOAD = 1; - - /** - * {@link Message#what} value sent after receiving a status message. - */ - private static final int WHAT_RECEIVED_STATUS = 2; - - /** - * {@link Message#what} value sent after the timeout waiting for a status message. - */ - private static final int WHAT_TIMEOUT = 3; - - private final Context mContext; - - /** - * The settings that were injected - */ - private final Set<Setting> mSettings; - - private final Handler mHandler; - - public SettingsInjector(Context context) { - mContext = context; - mSettings = new HashSet<Setting>(); - mHandler = new StatusLoadingHandler(); - } - - /** - * Returns a list for a profile with one {@link InjectedSetting} object for each - * {@link android.app.Service} that responds to - * {@link SettingInjectorService#ACTION_SERVICE_INTENT} and provides the expected setting - * metadata. - * - * Duplicates some code from {@link android.content.pm.RegisteredServicesCache}. - * - * TODO: unit test - */ - private List<InjectedSetting> getSettings(final UserHandle userHandle) { - PackageManager pm = mContext.getPackageManager(); - Intent intent = new Intent(SettingInjectorService.ACTION_SERVICE_INTENT); - - final int profileId = userHandle.getIdentifier(); - List<ResolveInfo> resolveInfos = - pm.queryIntentServicesAsUser(intent, PackageManager.GET_META_DATA, profileId); - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Found services for profile id " + profileId + ": " + resolveInfos); - } - List<InjectedSetting> settings = new ArrayList<InjectedSetting>(resolveInfos.size()); - for (ResolveInfo resolveInfo : resolveInfos) { - try { - InjectedSetting setting = parseServiceInfo(resolveInfo, userHandle, pm); - if (setting == null) { - Log.w(TAG, "Unable to load service info " + resolveInfo); - } else { - settings.add(setting); - } - } catch (XmlPullParserException e) { - Log.w(TAG, "Unable to load service info " + resolveInfo, e); - } catch (IOException e) { - Log.w(TAG, "Unable to load service info " + resolveInfo, e); - } - } - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Loaded settings for profile id " + profileId + ": " + settings); - } - - return settings; - } - - /** - * Returns the settings parsed from the attributes of the - * {@link SettingInjectorService#META_DATA_NAME} tag, or null. - * - * Duplicates some code from {@link android.content.pm.RegisteredServicesCache}. - */ - private static InjectedSetting parseServiceInfo(ResolveInfo service, UserHandle userHandle, - PackageManager pm) throws XmlPullParserException, IOException { - - ServiceInfo si = service.serviceInfo; - ApplicationInfo ai = si.applicationInfo; - - if ((ai.flags & ApplicationInfo.FLAG_SYSTEM) == 0) { - if (Log.isLoggable(TAG, Log.WARN)) { - Log.w(TAG, "Ignoring attempt to inject setting from app not in system image: " - + service); - return null; - } - } - - XmlResourceParser parser = null; - try { - parser = si.loadXmlMetaData(pm, SettingInjectorService.META_DATA_NAME); - if (parser == null) { - throw new XmlPullParserException("No " + SettingInjectorService.META_DATA_NAME - + " meta-data for " + service + ": " + si); - } - - AttributeSet attrs = Xml.asAttributeSet(parser); - - int type; - while ((type = parser.next()) != XmlPullParser.END_DOCUMENT - && type != XmlPullParser.START_TAG) { - } - - String nodeName = parser.getName(); - if (!SettingInjectorService.ATTRIBUTES_NAME.equals(nodeName)) { - throw new XmlPullParserException("Meta-data does not start with " - + SettingInjectorService.ATTRIBUTES_NAME + " tag"); - } - - Resources res = pm.getResourcesForApplicationAsUser(si.packageName, - userHandle.getIdentifier()); - return parseAttributes(si.packageName, si.name, userHandle, res, attrs); - } catch (PackageManager.NameNotFoundException e) { - throw new XmlPullParserException( - "Unable to load resources for package " + si.packageName); - } finally { - if (parser != null) { - parser.close(); - } - } - } - - /** - * Returns an immutable representation of the static attributes for the setting, or null. - */ - private static InjectedSetting parseAttributes(String packageName, String className, - UserHandle userHandle, Resources res, AttributeSet attrs) { - - TypedArray sa = res.obtainAttributes(attrs, android.R.styleable.SettingInjectorService); - try { - // Note that to help guard against malicious string injection, we do not allow dynamic - // specification of the label (setting title) - final String title = sa.getString(android.R.styleable.SettingInjectorService_title); - final int iconId = - sa.getResourceId(android.R.styleable.SettingInjectorService_icon, 0); - final String settingsActivity = - sa.getString(android.R.styleable.SettingInjectorService_settingsActivity); - final String userRestriction = sa.getString( - android.R.styleable.SettingInjectorService_userRestriction); - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "parsed title: " + title + ", iconId: " + iconId - + ", settingsActivity: " + settingsActivity); - } - return new InjectedSetting.Builder() - .setPackageName(packageName) - .setClassName(className) - .setTitle(title) - .setIconId(iconId) - .setUserHandle(userHandle) - .setSettingsActivity(settingsActivity) - .setUserRestriction(userRestriction) - .build(); - } finally { - sa.recycle(); - } - } - - /** - * Gets a list of preferences that other apps have injected. - * - * @param profileId Identifier of the user/profile to obtain the injected settings for or - * UserHandle.USER_CURRENT for all profiles associated with current user. - */ - public List<Preference> getInjectedSettings(Context prefContext, final int profileId) { - final UserManager um = (UserManager) mContext.getSystemService(Context.USER_SERVICE); - final List<UserHandle> profiles = um.getUserProfiles(); - ArrayList<Preference> prefs = new ArrayList<>(); - final int profileCount = profiles.size(); - for (int i = 0; i < profileCount; ++i) { - final UserHandle userHandle = profiles.get(i); - if (profileId == UserHandle.USER_CURRENT || profileId == userHandle.getIdentifier()) { - Iterable<InjectedSetting> settings = getSettings(userHandle); - for (InjectedSetting setting : settings) { - Preference pref = addServiceSetting(prefContext, prefs, setting); - mSettings.add(new Setting(setting, pref)); - } - } - } - - reloadStatusMessages(); - - return prefs; - } - - /** - * Checks wheteher there is any preference that other apps have injected. - * - * @param profileId Identifier of the user/profile to obtain the injected settings for or - * UserHandle.USER_CURRENT for all profiles associated with current user. - */ - public boolean hasInjectedSettings(final int profileId) { - final UserManager um = (UserManager) mContext.getSystemService(Context.USER_SERVICE); - final List<UserHandle> profiles = um.getUserProfiles(); - final int profileCount = profiles.size(); - for (int i = 0; i < profileCount; ++i) { - final UserHandle userHandle = profiles.get(i); - if (profileId == UserHandle.USER_CURRENT || profileId == userHandle.getIdentifier()) { - Iterable<InjectedSetting> settings = getSettings(userHandle); - for (InjectedSetting setting : settings) { - return true; - } - } - } - return false; - } - - /** - * Reloads the status messages for all the preference items. - */ - public void reloadStatusMessages() { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "reloadingStatusMessages: " + mSettings); - } - mHandler.sendMessage(mHandler.obtainMessage(WHAT_RELOAD)); - } - - /** - * Adds an injected setting to the root. - */ - private Preference addServiceSetting(Context prefContext, List<Preference> prefs, - InjectedSetting info) { - final PackageManager pm = mContext.getPackageManager(); - Drawable appIcon = null; - try { - final PackageItemInfo itemInfo = new PackageItemInfo(); - itemInfo.icon = info.iconId; - itemInfo.packageName = info.packageName; - final ApplicationInfo appInfo = pm.getApplicationInfo(info.packageName, - PackageManager.GET_META_DATA); - appIcon = IconDrawableFactory.newInstance(mContext) - .getBadgedIcon(itemInfo, appInfo, info.mUserHandle.getIdentifier()); - } catch (PackageManager.NameNotFoundException e) { - Log.e(TAG, "Can't get ApplicationInfo for " + info.packageName, e); - } - Preference pref = TextUtils.isEmpty(info.userRestriction) - ? new AppPreference(prefContext) - : new RestrictedAppPreference(prefContext, info.userRestriction); - pref.setTitle(info.title); - pref.setSummary(null); - pref.setIcon(appIcon); - pref.setOnPreferenceClickListener(new ServiceSettingClickedListener(info)); - prefs.add(pref); - return pref; - } - - private class ServiceSettingClickedListener - implements Preference.OnPreferenceClickListener { - private InjectedSetting mInfo; - - public ServiceSettingClickedListener(InjectedSetting info) { - mInfo = info; - } - - @Override - public boolean onPreferenceClick(Preference preference) { - // Activity to start if they click on the preference. Must start in new task to ensure - // that "android.settings.LOCATION_SOURCE_SETTINGS" brings user back to - // Settings > Location. - Intent settingIntent = new Intent(); - settingIntent.setClassName(mInfo.packageName, mInfo.settingsActivity); - // Sometimes the user may navigate back to "Settings" and launch another different - // injected setting after one injected setting has been launched. - // - // FLAG_ACTIVITY_CLEAR_TOP allows multiple Activities to stack on each other. When - // "back" button is clicked, the user will navigate through all the injected settings - // launched before. Such behavior could be quite confusing sometimes. - // - // In order to avoid such confusion, we use FLAG_ACTIVITY_CLEAR_TASK, which always clear - // up all existing injected settings and make sure that "back" button always brings the - // user back to "Settings" directly. - settingIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - mContext.startActivityAsUser(settingIntent, mInfo.mUserHandle); - return true; - } - } - - /** - * Loads the setting status values one at a time. Each load starts a subclass of {@link - * SettingInjectorService}, so to reduce memory pressure we don't want to load too many at - * once. - */ - private final class StatusLoadingHandler extends Handler { - - /** - * Settings whose status values need to be loaded. A set is used to prevent redundant loads. - */ - private Set<Setting> mSettingsToLoad = new HashSet<Setting>(); - - /** - * Settings that are being loaded now and haven't timed out. In practice this should have - * zero or one elements. - */ - private Set<Setting> mSettingsBeingLoaded = new HashSet<Setting>(); - - /** - * Settings that are being loaded but have timed out. If only one setting has timed out, we - * will go ahead and start loading the next setting so that one slow load won't delay the - * load of the other settings. - */ - private Set<Setting> mTimedOutSettings = new HashSet<Setting>(); - - private boolean mReloadRequested; - - private StatusLoadingHandler() { - super(Looper.getMainLooper()); - } - @Override - public void handleMessage(Message msg) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "handleMessage start: " + msg + ", " + this); - } - - // Update state in response to message - switch (msg.what) { - case WHAT_RELOAD: - mReloadRequested = true; - break; - case WHAT_RECEIVED_STATUS: - final Setting receivedSetting = (Setting) msg.obj; - receivedSetting.maybeLogElapsedTime(); - mSettingsBeingLoaded.remove(receivedSetting); - mTimedOutSettings.remove(receivedSetting); - removeMessages(WHAT_TIMEOUT, receivedSetting); - break; - case WHAT_TIMEOUT: - final Setting timedOutSetting = (Setting) msg.obj; - mSettingsBeingLoaded.remove(timedOutSetting); - mTimedOutSettings.add(timedOutSetting); - if (Log.isLoggable(TAG, Log.WARN)) { - Log.w(TAG, "Timed out after " + timedOutSetting.getElapsedTime() - + " millis trying to get status for: " + timedOutSetting); - } - break; - default: - Log.wtf(TAG, "Unexpected what: " + msg); - } - - // Decide whether to load additional settings based on the new state. Start by seeing - // if we have headroom to load another setting. - if (mSettingsBeingLoaded.size() > 0 || mTimedOutSettings.size() > 1) { - // Don't load any more settings until one of the pending settings has completed. - // To reduce memory pressure, we want to be loading at most one setting (plus at - // most one timed-out setting) at a time. This means we'll be responsible for - // bringing in at most two services. - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "too many services already live for " + msg + ", " + this); - } - return; - } - - if (mReloadRequested && mSettingsToLoad.isEmpty() && mSettingsBeingLoaded.isEmpty() - && mTimedOutSettings.isEmpty()) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "reloading because idle and reload requesteed " + msg + ", " + this); - } - // Reload requested, so must reload all settings - mSettingsToLoad.addAll(mSettings); - mReloadRequested = false; - } - - // Remove the next setting to load from the queue, if any - Iterator<Setting> iter = mSettingsToLoad.iterator(); - if (!iter.hasNext()) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "nothing left to do for " + msg + ", " + this); - } - return; - } - Setting setting = iter.next(); - iter.remove(); - - // Request the status value - setting.startService(); - mSettingsBeingLoaded.add(setting); - - // Ensure that if receiving the status value takes too long, we start loading the - // next value anyway - Message timeoutMsg = obtainMessage(WHAT_TIMEOUT, setting); - sendMessageDelayed(timeoutMsg, INJECTED_STATUS_UPDATE_TIMEOUT_MILLIS); - - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "handleMessage end " + msg + ", " + this - + ", started loading " + setting); - } - } - - @Override - public String toString() { - return "StatusLoadingHandler{" + - "mSettingsToLoad=" + mSettingsToLoad + - ", mSettingsBeingLoaded=" + mSettingsBeingLoaded + - ", mTimedOutSettings=" + mTimedOutSettings + - ", mReloadRequested=" + mReloadRequested + - '}'; - } - } - - /** - * Represents an injected setting and the corresponding preference. - */ - private final class Setting { - - public final InjectedSetting setting; - public final Preference preference; - public long startMillis; - - private Setting(InjectedSetting setting, Preference preference) { - this.setting = setting; - this.preference = preference; - } - - @Override - public String toString() { - return "Setting{" + - "setting=" + setting + - ", preference=" + preference + - '}'; - } - - /** - * Returns true if they both have the same {@link #setting} value. Ignores mutable - * {@link #preference} and {@link #startMillis} so that it's safe to use in sets. - */ - @Override - public boolean equals(Object o) { - return this == o || o instanceof Setting && setting.equals(((Setting) o).setting); - } - - @Override - public int hashCode() { - return setting.hashCode(); - } - - /** - * Starts the service to fetch for the current status for the setting, and updates the - * preference when the service replies. - */ - public void startService() { - final ActivityManager am = (ActivityManager) - mContext.getSystemService(Context.ACTIVITY_SERVICE); - if (!am.isUserRunning(setting.mUserHandle.getIdentifier())) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "Cannot start service as user " - + setting.mUserHandle.getIdentifier() + " is not running"); - } - return; - } - Handler handler = new Handler() { - @Override - public void handleMessage(Message msg) { - Bundle bundle = msg.getData(); - boolean enabled = bundle.getBoolean(SettingInjectorService.ENABLED_KEY, true); - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, setting + ": received " + msg + ", bundle: " + bundle); - } - preference.setSummary(null); - preference.setEnabled(enabled); - mHandler.sendMessage( - mHandler.obtainMessage(WHAT_RECEIVED_STATUS, Setting.this)); - } - }; - Messenger messenger = new Messenger(handler); - - Intent intent = setting.getServiceIntent(); - intent.putExtra(SettingInjectorService.MESSENGER_KEY, messenger); - - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, setting + ": sending update intent: " + intent - + ", handler: " + handler); - startMillis = SystemClock.elapsedRealtime(); - } else { - startMillis = 0; - } - - // Start the service, making sure that this is attributed to the user associated with - // the setting rather than the system user. - mContext.startServiceAsUser(intent, setting.mUserHandle); - } - - public long getElapsedTime() { - long end = SystemClock.elapsedRealtime(); - return end - startMillis; - } - - public void maybeLogElapsedTime() { - if (Log.isLoggable(TAG, Log.DEBUG) && startMillis != 0) { - long elapsed = getElapsedTime(); - Log.d(TAG, this + " update took " + elapsed + " millis"); - } - } - } -} diff --git a/src/com/android/settings/location/TopLevelLocationPreferenceController.java b/src/com/android/settings/location/TopLevelLocationPreferenceController.java new file mode 100644 index 0000000000..ec58a3422d --- /dev/null +++ b/src/com/android/settings/location/TopLevelLocationPreferenceController.java @@ -0,0 +1,133 @@ +package com.android.settings.location; + +import static android.Manifest.permission.ACCESS_COARSE_LOCATION; +import static android.Manifest.permission.ACCESS_FINE_LOCATION; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.location.LocationManager; +import android.os.UserHandle; +import android.os.UserManager; +import android.permission.PermissionControllerManager; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; + +import com.android.settings.R; +import com.android.settings.Utils; +import com.android.settings.core.BasePreferenceController; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.core.lifecycle.events.OnStop; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +public class TopLevelLocationPreferenceController extends BasePreferenceController implements + LifecycleObserver, OnStart, OnStop { + private static final IntentFilter INTENT_FILTER_LOCATION_MODE_CHANGED = + new IntentFilter(LocationManager.MODE_CHANGED_ACTION); + private final LocationManager mLocationManager; + /** Total number of apps that has location permission. */ + private int mNumTotal = -1; + private int mNumTotalLoading = 0; + private BroadcastReceiver mReceiver; + private Preference mPreference; + private AtomicInteger loadingInProgress = new AtomicInteger(0); + + public TopLevelLocationPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + mLocationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } + + @Override + public CharSequence getSummary() { + if (mLocationManager.isLocationEnabled()) { + if (mNumTotal == -1) { + return mContext.getString(R.string.location_settings_loading_app_permission_stats); + } + return mContext.getResources().getQuantityString( + R.plurals.location_settings_summary_location_on, + mNumTotal, mNumTotal); + } else { + return mContext.getString(R.string.location_settings_summary_location_off); + } + } + + @VisibleForTesting + void setLocationAppCount(int numApps) { + mNumTotal = numApps; + refreshSummary(mPreference); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + mPreference = preference; + refreshSummary(preference); + // Bail out if location has been disabled, or there's another loading request in progress. + if (!mLocationManager.isLocationEnabled() || + loadingInProgress.get() != 0) { + return; + } + mNumTotalLoading = 0; + // Retrieve a list of users inside the current user profile group. + final List<UserHandle> users = mContext.getSystemService( + UserManager.class).getUserProfiles(); + loadingInProgress.set(users.size()); + for (UserHandle user : users) { + final Context userContext = Utils.createPackageContextAsUser(mContext, + user.getIdentifier()); + if (userContext == null) { + if (loadingInProgress.decrementAndGet() == 0) { + setLocationAppCount(mNumTotalLoading); + } + continue; + } + final PermissionControllerManager permController = + userContext.getSystemService(PermissionControllerManager.class); + permController.countPermissionApps( + Arrays.asList(ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION), + PermissionControllerManager.COUNT_ONLY_WHEN_GRANTED, + (numApps) -> { + mNumTotalLoading += numApps; + if (loadingInProgress.decrementAndGet() == 0) { + setLocationAppCount(mNumTotalLoading); + } + }, null); + } + } + + @Override + public void onStart() { + if (mReceiver == null) { + mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + refreshLocationMode(); + } + }; + } + mContext.registerReceiver(mReceiver, INTENT_FILTER_LOCATION_MODE_CHANGED); + refreshLocationMode(); + } + + @Override + public void onStop() { + mContext.unregisterReceiver(mReceiver); + } + + private void refreshLocationMode() { + if (mPreference != null) { + updateState(mPreference); + } + } +} diff --git a/src/com/android/settings/location/WifiScanningPreferenceController.java b/src/com/android/settings/location/WifiScanningPreferenceController.java index c5389ca496..234ec5e53e 100644 --- a/src/com/android/settings/location/WifiScanningPreferenceController.java +++ b/src/com/android/settings/location/WifiScanningPreferenceController.java @@ -15,8 +15,9 @@ package com.android.settings.location; import android.content.Context; import android.provider.Settings; -import androidx.preference.SwitchPreference; + import androidx.preference.Preference; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.core.AbstractPreferenceController; diff --git a/src/com/android/settings/media/MediaDeviceUpdateWorker.java b/src/com/android/settings/media/MediaDeviceUpdateWorker.java new file mode 100644 index 0000000000..f67afdc394 --- /dev/null +++ b/src/com/android/settings/media/MediaDeviceUpdateWorker.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2019 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.media; + +import android.content.Context; +import android.net.Uri; + +import androidx.annotation.VisibleForTesting; + +import com.android.settings.slices.SliceBackgroundWorker; +import com.android.settingslib.media.LocalMediaManager; +import com.android.settingslib.media.MediaDevice; +import com.android.settingslib.utils.ThreadUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * SliceBackgroundWorker for get MediaDevice list and handle MediaDevice state change event. + */ +public class MediaDeviceUpdateWorker extends SliceBackgroundWorker + implements LocalMediaManager.DeviceCallback { + + private final Context mContext; + private final List<MediaDevice> mMediaDevices = new ArrayList<>(); + + private String mPackageName; + + @VisibleForTesting + LocalMediaManager mLocalMediaManager; + + public MediaDeviceUpdateWorker(Context context, Uri uri) { + super(context, uri); + mContext = context; + } + + public void setPackageName(String packageName) { + mPackageName = packageName; + } + + @Override + protected void onSlicePinned() { + mMediaDevices.clear(); + if (mLocalMediaManager == null) { + mLocalMediaManager = new LocalMediaManager(mContext, mPackageName, null); + } + + mLocalMediaManager.registerCallback(this); + mLocalMediaManager.startScan(); + } + + @Override + protected void onSliceUnpinned() { + mLocalMediaManager.unregisterCallback(this); + mLocalMediaManager.stopScan(); + } + + @Override + public void close() { + mLocalMediaManager = null; + } + + @Override + public void onDeviceListUpdate(List<MediaDevice> devices) { + buildMediaDevices(devices); + notifySliceChange(); + } + + private void buildMediaDevices(List<MediaDevice> devices) { + mMediaDevices.clear(); + mMediaDevices.addAll(devices); + } + + @Override + public void onSelectedDeviceStateChanged(MediaDevice device, int state) { + notifySliceChange(); + } + + public List<MediaDevice> getMediaDevices() { + return new ArrayList<>(mMediaDevices); + } + + public void connectDevice(MediaDevice device) { + ThreadUtils.postOnBackgroundThread(() -> { + mLocalMediaManager.connectDevice(device); + }); + } + + public MediaDevice getMediaDeviceById(String id) { + return mLocalMediaManager.getMediaDeviceById(mMediaDevices, id); + } + + public MediaDevice getCurrentConnectedMediaDevice() { + return mLocalMediaManager.getCurrentConnectedDevice(); + } +} diff --git a/src/com/android/settings/media/MediaOutputIndicatorSlice.java b/src/com/android/settings/media/MediaOutputIndicatorSlice.java new file mode 100644 index 0000000000..0ffa9ff200 --- /dev/null +++ b/src/com/android/settings/media/MediaOutputIndicatorSlice.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2019 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.media; + +import static com.android.settings.slices.CustomSliceRegistry.MEDIA_OUTPUT_INDICATOR_SLICE_URI; + +import android.annotation.ColorInt; +import android.app.PendingIntent; +import android.bluetooth.BluetoothDevice; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.telephony.TelephonyManager; +import android.util.Log; + +import androidx.core.graphics.drawable.IconCompat; +import androidx.slice.Slice; +import androidx.slice.builders.ListBuilder; +import androidx.slice.builders.SliceAction; + +import com.android.internal.util.CollectionUtils; +import com.android.settings.R; +import com.android.settings.Utils; +import com.android.settings.slices.CustomSliceable; +import com.android.settingslib.bluetooth.A2dpProfile; +import com.android.settingslib.bluetooth.HearingAidProfile; +import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; +import com.android.settingslib.media.MediaOutputSliceConstants; + +import java.util.ArrayList; +import java.util.List; + +public class MediaOutputIndicatorSlice implements CustomSliceable { + + private static final String TAG = "MediaOutputIndicatorSlice"; + + private Context mContext; + private LocalBluetoothManager mLocalBluetoothManager; + private LocalBluetoothProfileManager mProfileManager; + + public MediaOutputIndicatorSlice(Context context) { + mContext = context; + mLocalBluetoothManager = com.android.settings.bluetooth.Utils.getLocalBtManager(context); + if (mLocalBluetoothManager == null) { + Log.e(TAG, "Bluetooth is not supported on this device"); + return; + } + mProfileManager = mLocalBluetoothManager.getProfileManager(); + } + + @Override + public Slice getSlice() { + if (!isVisible()) { + return null; + } + final IconCompat icon = IconCompat.createWithResource(mContext, + com.android.internal.R.drawable.ic_settings_bluetooth); + final CharSequence title = mContext.getText(R.string.media_output_title); + final PendingIntent primaryActionIntent = PendingIntent.getActivity(mContext, + 0 /* requestCode */, getMediaOutputSliceIntent(), 0 /* flags */); + final SliceAction primarySliceAction = SliceAction.createDeeplink( + primaryActionIntent, icon, ListBuilder.ICON_IMAGE, title); + @ColorInt final int color = Utils.getColorAccentDefaultColor(mContext); + + final ListBuilder listBuilder = new ListBuilder(mContext, + MEDIA_OUTPUT_INDICATOR_SLICE_URI, + ListBuilder.INFINITY) + .setAccentColor(color) + .addRow(new ListBuilder.RowBuilder() + .setTitle(title) + .setSubtitle(findActiveDeviceName()) + .setPrimaryAction(primarySliceAction)); + return listBuilder.build(); + } + + private Intent getMediaOutputSliceIntent() { + final Intent intent = new Intent() + .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + return intent; + } + + @Override + public Uri getUri() { + return MEDIA_OUTPUT_INDICATOR_SLICE_URI; + } + + @Override + public Intent getIntent() { + // This Slice reflects active media device information and launch MediaOutputSlice. It does + // not contain its owned Slice data + return null; + } + + @Override + public Class getBackgroundWorkerClass() { + return MediaOutputIndicatorWorker.class; + } + + private boolean isVisible() { + // To decide Slice's visibility. + // Return true if + // 1. phone is not in ongoing call mode + // 2. Bluetooth device is connected + final TelephonyManager telephonyManager = + (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); + return telephonyManager.getCallState() == TelephonyManager.CALL_STATE_IDLE + && (!CollectionUtils.isEmpty(getConnectedA2dpDevices()) + || !CollectionUtils.isEmpty(getConnectedHearingAidDevices())); + } + + private List<BluetoothDevice> getConnectedA2dpDevices() { + // Get A2dp devices on states + // (STATE_CONNECTING, STATE_CONNECTED, STATE_DISCONNECTING) + final A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile(); + if (a2dpProfile == null) { + return new ArrayList<>(); + } + return a2dpProfile.getConnectedDevices(); + } + + private List<BluetoothDevice> getConnectedHearingAidDevices() { + // Get hearing aid profile devices on states + // (STATE_CONNECTING, STATE_CONNECTED, STATE_DISCONNECTING) + final HearingAidProfile hapProfile = mProfileManager.getHearingAidProfile(); + if (hapProfile == null) { + return new ArrayList<>(); + } + + return hapProfile.getConnectedDevices(); + } + + private CharSequence findActiveDeviceName() { + // Return Hearing Aid device name if it is active + BluetoothDevice activeDevice = findActiveHearingAidDevice(); + if (activeDevice != null) { + return activeDevice.getAliasName(); + } + // Return A2DP device name if it is active + final A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile(); + if (a2dpProfile != null) { + activeDevice = a2dpProfile.getActiveDevice(); + if (activeDevice != null) { + return activeDevice.getAliasName(); + } + } + // No active device, return default summary + return mContext.getText(R.string.media_output_default_summary); + } + + private BluetoothDevice findActiveHearingAidDevice() { + final HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile(); + if (hearingAidProfile == null) { + return null; + } + + final List<BluetoothDevice> activeDevices = hearingAidProfile.getActiveDevices(); + for (BluetoothDevice btDevice : activeDevices) { + if (btDevice != null) { + return btDevice; + } + } + return null; + } +} diff --git a/src/com/android/settings/media/MediaOutputIndicatorWorker.java b/src/com/android/settings/media/MediaOutputIndicatorWorker.java new file mode 100644 index 0000000000..0937d4b3b4 --- /dev/null +++ b/src/com/android/settings/media/MediaOutputIndicatorWorker.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2019 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.media; + +import android.bluetooth.BluetoothProfile; +import android.content.Context; +import android.net.Uri; +import android.util.Log; + +import com.android.settings.bluetooth.Utils; +import com.android.settings.slices.SliceBackgroundWorker; +import com.android.settingslib.bluetooth.BluetoothCallback; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.LocalBluetoothManager; + +import java.io.IOException; + +/** + * Listener for background change from {@code BluetoothCallback} to update media output indicator. + */ +public class MediaOutputIndicatorWorker extends SliceBackgroundWorker implements BluetoothCallback { + + private static final String TAG = "MediaOutputIndicatorWorker"; + + private LocalBluetoothManager mLocalBluetoothManager; + + public MediaOutputIndicatorWorker(Context context, Uri uri) { + super(context, uri); + } + + @Override + protected void onSlicePinned() { + mLocalBluetoothManager = Utils.getLocalBtManager(getContext()); + if (mLocalBluetoothManager == null) { + Log.e(TAG, "Bluetooth is not supported on this device"); + return; + } + mLocalBluetoothManager.getEventManager().registerCallback(this); + } + + @Override + protected void onSliceUnpinned() { + if (mLocalBluetoothManager == null) { + Log.e(TAG, "Bluetooth is not supported on this device"); + return; + } + mLocalBluetoothManager.getEventManager().unregisterCallback(this); + } + + @Override + public void close() throws IOException { + mLocalBluetoothManager = null; + } + + @Override + public void onBluetoothStateChanged(int bluetoothState) { + // To handle the case that Bluetooth on and no connected devices + notifySliceChange(); + } + + @Override + public void onActiveDeviceChanged(CachedBluetoothDevice activeDevice, int bluetoothProfile) { + if (bluetoothProfile == BluetoothProfile.A2DP) { + notifySliceChange(); + } + } + + @Override + public void onAudioModeChanged() { + notifySliceChange(); + } +} diff --git a/src/com/android/settings/media/MediaOutputSlice.java b/src/com/android/settings/media/MediaOutputSlice.java new file mode 100644 index 0000000000..734c31e5b4 --- /dev/null +++ b/src/com/android/settings/media/MediaOutputSlice.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2019 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.media; + +import static com.android.settings.slices.CustomSliceRegistry.MEDIA_OUTPUT_SLICE_URI; + +import android.app.PendingIntent; +import android.bluetooth.BluetoothAdapter; +import android.content.Context; +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; +import androidx.core.graphics.drawable.IconCompat; +import androidx.slice.Slice; +import androidx.slice.builders.ListBuilder; +import androidx.slice.builders.SliceAction; + +import com.android.settings.R; +import com.android.settings.Utils; +import com.android.settings.slices.CustomSliceable; +import com.android.settings.slices.SliceBackgroundWorker; +import com.android.settings.slices.SliceBroadcastReceiver; +import com.android.settingslib.media.MediaDevice; + +import java.util.List; + +/** + * Show the Media device that can be transfer the media. + */ +public class MediaOutputSlice implements CustomSliceable { + + private static final String TAG = "MediaOutputSlice"; + private static final String MEDIA_DEVICE_ID = "media_device_id"; + + public static final String MEDIA_PACKAGE_NAME = "media_package_name"; + + private final Context mContext; + + private MediaDeviceUpdateWorker mWorker; + private String mPackageName; + + public MediaOutputSlice(Context context) { + mContext = context; + mPackageName = getUri().getQueryParameter(MEDIA_PACKAGE_NAME); + } + + @VisibleForTesting + void init(String packageName, MediaDeviceUpdateWorker worker) { + mPackageName = packageName; + mWorker = worker; + } + + @Override + public Slice getSlice() { + // Reload theme for switching dark mode on/off + mContext.getTheme().applyStyle(R.style.Theme_Settings_Home, true /* force */); + + final ListBuilder listBuilder = new ListBuilder(mContext, getUri(), ListBuilder.INFINITY) + .setAccentColor(COLOR_NOT_TINTED); + + if (!isVisible()) { + Log.d(TAG, "getSlice() is not visible"); + return listBuilder.build(); + } + + final List<MediaDevice> devices = getMediaDevices(); + + final MediaDevice connectedDevice = getWorker().getCurrentConnectedMediaDevice(); + if (connectedDevice != null) { + listBuilder.addRow(getActiveDeviceHeaderRow(connectedDevice)); + } + + for (MediaDevice device : devices) { + if (connectedDevice == null + || !TextUtils.equals(connectedDevice.getId(), device.getId())) { + listBuilder.addRow(getMediaDeviceRow(device)); + } + } + + return listBuilder.build(); + } + + private ListBuilder.RowBuilder getActiveDeviceHeaderRow(MediaDevice device) { + final String title = device.getName(); + final IconCompat icon = getDeviceIconCompat(device); + + final PendingIntent broadcastAction = + getBroadcastIntent(mContext, device.getId(), device.hashCode()); + final SliceAction primarySliceAction = SliceAction.createDeeplink(broadcastAction, icon, + ListBuilder.ICON_IMAGE, title); + + final ListBuilder.RowBuilder rowBuilder = new ListBuilder.RowBuilder() + .setTitleItem(icon, ListBuilder.ICON_IMAGE) + .setTitle(title) + .setSubtitle(device.getSummary()) + .setPrimaryAction(primarySliceAction); + + return rowBuilder; + } + + private IconCompat getDeviceIconCompat(MediaDevice device) { + Drawable drawable = device.getIcon(); + if (drawable == null) { + Log.d(TAG, "getDeviceIconCompat() device : " + device.getName() + ", drawable is null"); + // Use default Bluetooth device icon to handle getIcon() is null case. + drawable = mContext.getDrawable(com.android.internal.R.drawable.ic_bt_headphones_a2dp); + } + + return Utils.createIconWithDrawable(drawable); + } + + private MediaDeviceUpdateWorker getWorker() { + if (mWorker == null) { + mWorker = (MediaDeviceUpdateWorker) SliceBackgroundWorker.getInstance(getUri()); + if (mWorker != null) { + mWorker.setPackageName(mPackageName); + } + } + return mWorker; + } + + private List<MediaDevice> getMediaDevices() { + final List<MediaDevice> devices = getWorker().getMediaDevices(); + return devices; + } + + private ListBuilder.RowBuilder getMediaDeviceRow(MediaDevice device) { + final String title = device.getName(); + final PendingIntent broadcastAction = + getBroadcastIntent(mContext, device.getId(), device.hashCode()); + final IconCompat deviceIcon = getDeviceIconCompat(device); + + final ListBuilder.RowBuilder rowBuilder = new ListBuilder.RowBuilder() + .setTitleItem(deviceIcon, ListBuilder.ICON_IMAGE) + .setPrimaryAction(SliceAction.create(broadcastAction, deviceIcon, + ListBuilder.ICON_IMAGE, title)) + .setTitle(title) + .setSubtitle(device.getSummary()); + + return rowBuilder; + } + + private PendingIntent getBroadcastIntent(Context context, String id, int requestCode) { + final Intent intent = new Intent(getUri().toString()); + intent.setClass(context, SliceBroadcastReceiver.class); + intent.putExtra(MEDIA_DEVICE_ID, id); + intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); + return PendingIntent.getBroadcast(context, requestCode /* requestCode */, intent, + PendingIntent.FLAG_CANCEL_CURRENT); + } + + @Override + public Uri getUri() { + return MEDIA_OUTPUT_SLICE_URI; + } + + @Override + public void onNotifyChange(Intent intent) { + final MediaDeviceUpdateWorker worker = getWorker(); + final String id = intent != null ? intent.getStringExtra(MEDIA_DEVICE_ID) : ""; + final MediaDevice device = worker.getMediaDeviceById(id); + if (device != null) { + Log.d(TAG, "onNotifyChange() device name : " + device.getName()); + worker.connectDevice(device); + } + } + + @Override + public Intent getIntent() { + return null; + } + + @Override + public Class getBackgroundWorkerClass() { + return MediaDeviceUpdateWorker.class; + } + + private boolean isVisible() { + // To decide Slice's visibility. + // Return true if + // 1. phone is not in ongoing call mode + // 2. worker is not null + // 3. Bluetooth is enabled + final TelephonyManager telephonyManager = + (TelephonyManager)mContext.getSystemService(Context.TELEPHONY_SERVICE); + final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); + + return telephonyManager.getCallState() == TelephonyManager.CALL_STATE_IDLE + && adapter.isEnabled() + && getWorker() != null; + } +} diff --git a/src/com/android/settings/network/AirplaneModePreferenceController.java b/src/com/android/settings/network/AirplaneModePreferenceController.java index 469449ff10..5c1913b575 100644 --- a/src/com/android/settings/network/AirplaneModePreferenceController.java +++ b/src/com/android/settings/network/AirplaneModePreferenceController.java @@ -17,16 +17,15 @@ package com.android.settings.network; import static android.provider.SettingsSlicesContract.KEY_AIRPLANE_MODE; -import android.app.Fragment; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.os.SystemProperties; -import android.provider.SettingsSlicesContract; -import androidx.preference.SwitchPreference; + +import androidx.fragment.app.Fragment; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.text.TextUtils; +import androidx.preference.SwitchPreference; import com.android.internal.telephony.TelephonyIntents; import com.android.internal.telephony.TelephonyProperties; @@ -82,7 +81,7 @@ public class AirplaneModePreferenceController extends TogglePreferenceController public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); if (isAvailable()) { - mAirplaneModePreference = (SwitchPreference) screen.findPreference(getPreferenceKey()); + mAirplaneModePreference = screen.findPreference(getPreferenceKey()); } } @@ -93,7 +92,7 @@ public class AirplaneModePreferenceController extends TogglePreferenceController @Override public boolean isSliceable() { - return TextUtils.equals(getPreferenceKey(), "toggle_airplane"); + return true; } @Override diff --git a/src/com/android/settings/network/ApnEditor.java b/src/com/android/settings/network/ApnEditor.java index 7413626dae..542a86952d 100644 --- a/src/com/android/settings/network/ApnEditor.java +++ b/src/com/android/settings/network/ApnEditor.java @@ -18,8 +18,8 @@ package com.android.settings.network; import static android.content.Context.TELEPHONY_SERVICE; -import android.app.AlertDialog; import android.app.Dialog; +import android.app.settings.SettingsEnums; import android.content.ContentValues; import android.content.Context; import android.content.Intent; @@ -28,13 +28,6 @@ import android.net.Uri; import android.os.Bundle; import android.os.PersistableBundle; import android.provider.Telephony; -import androidx.annotation.VisibleForTesting; -import androidx.preference.MultiSelectListPreference; -import androidx.preference.SwitchPreference; -import androidx.preference.EditTextPreference; -import androidx.preference.ListPreference; -import androidx.preference.Preference; -import androidx.preference.Preference.OnPreferenceChangeListener; import android.telephony.CarrierConfigManager; import android.telephony.ServiceState; import android.telephony.SubscriptionManager; @@ -48,7 +41,15 @@ import android.view.MenuItem; import android.view.View; import android.view.View.OnKeyListener; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; +import androidx.preference.EditTextPreference; +import androidx.preference.ListPreference; +import androidx.preference.MultiSelectListPreference; +import androidx.preference.Preference; +import androidx.preference.Preference.OnPreferenceChangeListener; +import androidx.preference.SwitchPreference; + import com.android.internal.telephony.PhoneConstants; import com.android.internal.util.ArrayUtils; import com.android.settings.R; @@ -230,9 +231,13 @@ public class ApnEditor extends SettingsPreferenceFragment final Intent intent = getIntent(); final String action = intent.getAction(); + if (TextUtils.isEmpty(action)) { + finish(); + return; + } + mSubId = intent.getIntExtra(ApnSettings.SUB_ID, SubscriptionManager.INVALID_SUBSCRIPTION_ID); - mReadOnlyApn = false; mReadOnlyApnTypes = null; mReadOnlyApnFields = null; @@ -478,7 +483,7 @@ public class ApnEditor extends SettingsPreferenceFragment @Override public int getMetricsCategory() { - return MetricsEvent.APN_EDITOR; + return SettingsEnums.APN_EDITOR; } @VisibleForTesting @@ -1198,7 +1203,7 @@ public class ApnEditor extends SettingsPreferenceFragment @Override public int getMetricsCategory() { - return MetricsEvent.DIALOG_APN_EDITOR_ERROR; + return SettingsEnums.DIALOG_APN_EDITOR_ERROR; } } diff --git a/src/com/android/settings/network/ApnPreference.java b/src/com/android/settings/network/ApnPreference.java index 73837af6b5..f039539889 100755 --- a/src/com/android/settings/network/ApnPreference.java +++ b/src/com/android/settings/network/ApnPreference.java @@ -21,8 +21,6 @@ import android.content.Context; import android.content.Intent; import android.net.Uri; import android.provider.Telephony; -import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; import android.telephony.SubscriptionManager; import android.util.AttributeSet; import android.util.Log; @@ -31,6 +29,9 @@ import android.widget.CompoundButton; import android.widget.RadioButton; import android.widget.Toast; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + import com.android.settings.R; public class ApnPreference extends Preference implements CompoundButton.OnCheckedChangeListener { diff --git a/src/com/android/settings/network/ApnSettings.java b/src/com/android/settings/network/ApnSettings.java index 0054e9bf3c..f0603bb388 100755 --- a/src/com/android/settings/network/ApnSettings.java +++ b/src/com/android/settings/network/ApnSettings.java @@ -19,6 +19,7 @@ package com.android.settings.network; import android.app.Activity; import android.app.Dialog; import android.app.ProgressDialog; +import android.app.settings.SettingsEnums; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.ContentValues; @@ -36,8 +37,6 @@ import android.os.PersistableBundle; import android.os.UserHandle; import android.os.UserManager; import android.provider.Telephony; -import androidx.preference.Preference; -import androidx.preference.PreferenceGroup; import android.telephony.CarrierConfigManager; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; @@ -50,7 +49,9 @@ import android.view.MenuItem; import android.view.MotionEvent; import android.widget.Toast; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.preference.Preference; +import androidx.preference.PreferenceGroup; + import com.android.internal.telephony.PhoneConstants; import com.android.internal.telephony.TelephonyIntents; import com.android.internal.telephony.uicc.UiccController; @@ -172,7 +173,7 @@ public class ApnSettings extends RestrictedSettingsFragment @Override public int getMetricsCategory() { - return MetricsEvent.APN; + return SettingsEnums.APN; } @Override @@ -350,7 +351,7 @@ public class ApnSettings extends RestrictedSettingsFragment if (mAllowAddingApns) { menu.add(0, MENU_NEW, 0, getResources().getString(R.string.menu_new)) - .setIcon(R.drawable.ic_menu_add_white) + .setIcon(R.drawable.ic_add_24dp) .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); } menu.add(0, MENU_RESTORE, 0, @@ -517,7 +518,7 @@ public class ApnSettings extends RestrictedSettingsFragment @Override public int getDialogMetricsCategory(int dialogId) { if (dialogId == DIALOG_RESTORE_DEFAULTAPN) { - return MetricsEvent.DIALOG_APN_RESTORE_DEFAULT; + return SettingsEnums.DIALOG_APN_RESTORE_DEFAULT; } return 0; } diff --git a/src/com/android/settings/network/MobileDataContentObserver.java b/src/com/android/settings/network/MobileDataContentObserver.java new file mode 100644 index 0000000000..b8a1c8c1a5 --- /dev/null +++ b/src/com/android/settings/network/MobileDataContentObserver.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2019 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.network; + +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.provider.Settings; +import android.telephony.TelephonyManager; + +/** + * {@link ContentObserver} to listen to update of mobile data change + */ +public class MobileDataContentObserver extends ContentObserver { + private OnMobileDataChangedListener mListener; + + public MobileDataContentObserver(Handler handler) { + super(handler); + } + + public static Uri getObservableUri(int subId) { + Uri uri = Settings.Global.getUriFor(Settings.Global.MOBILE_DATA); + if (TelephonyManager.getDefault().getSimCount() != 1) { + uri = Settings.Global.getUriFor(Settings.Global.MOBILE_DATA + subId); + } + return uri; + } + + public void setOnMobileDataChangedListener(OnMobileDataChangedListener lsn) { + mListener = lsn; + } + + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + if (mListener != null) { + mListener.onMobileDataChanged(); + } + } + + public void register(Context context, int subId) { + final Uri uri = getObservableUri(subId); + context.getContentResolver().registerContentObserver(uri, false, this); + + } + + public void unRegister(Context context) { + context.getContentResolver().unregisterContentObserver(this); + } + + /** + * Listener for update of mobile data(ON vs OFF) + */ + public interface OnMobileDataChangedListener { + void onMobileDataChanged(); + } +} diff --git a/src/com/android/settings/network/MobileDataEnabledListener.java b/src/com/android/settings/network/MobileDataEnabledListener.java new file mode 100644 index 0000000000..8344f886f7 --- /dev/null +++ b/src/com/android/settings/network/MobileDataEnabledListener.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2019 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.network; + +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.provider.Settings; +import android.telephony.SubscriptionManager; + +/** Helper class to listen for changes in the enabled state of mobile data. */ +public class MobileDataEnabledListener extends ContentObserver { + private Context mContext; + private Client mClient; + private int mSubId; + + public interface Client { + void onMobileDataEnabledChange(); + } + + public MobileDataEnabledListener(Context context, Client client) { + super(new Handler()); + mContext = context; + mClient = client; + mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; + } + + /** Starts listening to changes in the enabled state for data on the given subscription id. */ + public void start(int subId) { + mSubId = subId; + Uri uri; + if (mSubId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + uri = Settings.Global.getUriFor(Settings.Global.MOBILE_DATA); + } else { + uri = Settings.Global.getUriFor(Settings.Global.MOBILE_DATA + mSubId); + } + mContext.getContentResolver().registerContentObserver(uri, true /*notifyForDescendants*/, + this); + } + + public int getSubId() { + return mSubId; + } + + public MobileDataEnabledListener stop() { + mContext.getContentResolver().unregisterContentObserver(this); + return this; + } + + @Override + public void onChange(boolean selfChange) { + mClient.onMobileDataEnabledChange(); + } +} diff --git a/src/com/android/settings/network/MobileNetworkListController.java b/src/com/android/settings/network/MobileNetworkListController.java new file mode 100644 index 0000000000..ab41fad623 --- /dev/null +++ b/src/com/android/settings/network/MobileNetworkListController.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2019 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.network; + +import static androidx.lifecycle.Lifecycle.Event.ON_PAUSE; +import static androidx.lifecycle.Lifecycle.Event.ON_RESUME; + +import android.content.Context; +import android.content.Intent; +import android.provider.Settings; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.util.ArrayMap; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.settings.R; +import com.android.settings.network.telephony.MobileNetworkActivity; +import com.android.settings.network.telephony.MobileNetworkUtils; +import com.android.settingslib.core.AbstractPreferenceController; + +import java.util.List; +import java.util.Map; + +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.OnLifecycleEvent; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +/** + * This populates the entries on a page which lists all available mobile subscriptions. Each entry + * has the name of the subscription with some subtext giving additional detail, and clicking on the + * entry brings you to a details page for that network. + */ +public class MobileNetworkListController extends AbstractPreferenceController implements + LifecycleObserver, SubscriptionsChangeListener.SubscriptionsChangeListenerClient { + private static final String TAG = "MobileNetworkListCtlr"; + + @VisibleForTesting + static final String KEY_ADD_MORE = "add_more"; + + private SubscriptionManager mSubscriptionManager; + private SubscriptionsChangeListener mChangeListener; + private PreferenceScreen mPreferenceScreen; + private Map<Integer, Preference> mPreferences; + + public MobileNetworkListController(Context context, Lifecycle lifecycle) { + super(context); + mSubscriptionManager = context.getSystemService(SubscriptionManager.class); + mChangeListener = new SubscriptionsChangeListener(context, this); + mPreferences = new ArrayMap<>(); + lifecycle.addObserver(this); + } + + @OnLifecycleEvent(ON_RESUME) + public void onResume() { + mChangeListener.start(); + update(); + } + + @OnLifecycleEvent(ON_PAUSE) + public void onPause() { + mChangeListener.stop(); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreferenceScreen = screen; + mPreferenceScreen.findPreference(KEY_ADD_MORE).setVisible( + MobileNetworkUtils.showEuiccSettings(mContext)); + update(); + } + + private void update() { + if (mPreferenceScreen == null) { + return; + } + + // Since we may already have created some preferences previously, we first grab the list of + // those, then go through the current available subscriptions making sure they are all + // present in the screen, and finally remove any now-outdated ones. + final Map<Integer, Preference> existingPreferences = mPreferences; + mPreferences = new ArrayMap<>(); + + final List<SubscriptionInfo> subscriptions = SubscriptionUtil.getAvailableSubscriptions( + mContext); + for (SubscriptionInfo info : subscriptions) { + final int subId = info.getSubscriptionId(); + Preference pref = existingPreferences.remove(subId); + if (pref == null) { + pref = new Preference(mPreferenceScreen.getContext()); + mPreferenceScreen.addPreference(pref); + } + pref.setTitle(info.getDisplayName()); + + if (info.isEmbedded()) { + if (mSubscriptionManager.isActiveSubscriptionId(subId)) { + pref.setSummary(R.string.mobile_network_active_esim); + } else { + pref.setSummary(R.string.mobile_network_inactive_esim); + } + } else { + if (mSubscriptionManager.isActiveSubscriptionId(subId)) { + pref.setSummary(R.string.mobile_network_active_sim); + } else { + pref.setSummary(mContext.getString(R.string.mobile_network_tap_to_activate, + SubscriptionUtil.getDisplayName(info))); + } + } + + pref.setOnPreferenceClickListener(clickedPref -> { + if (!info.isEmbedded() && !mSubscriptionManager.isActiveSubscriptionId(subId)) { + mSubscriptionManager.setSubscriptionEnabled(subId, true); + } else { + final Intent intent = new Intent(mContext, MobileNetworkActivity.class); + intent.putExtra(Settings.EXTRA_SUB_ID, info.getSubscriptionId()); + mContext.startActivity(intent); + } + return true; + }); + mPreferences.put(subId, pref); + } + for (Preference pref : existingPreferences.values()) { + mPreferenceScreen.removePreference(pref); + } + } + + @Override + public boolean isAvailable() { + return true; + } + + @Override + public String getPreferenceKey() { + return null; + } + + @Override + public void onAirplaneModeChanged(boolean airplaneModeEnabled) { + } + + @Override + public void onSubscriptionsChanged() { + update(); + } +} diff --git a/src/com/android/settings/network/MobileNetworkListFragment.java b/src/com/android/settings/network/MobileNetworkListFragment.java new file mode 100644 index 0000000000..4690a28815 --- /dev/null +++ b/src/com/android/settings/network/MobileNetworkListFragment.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2019 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.network; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.os.UserManager; +import android.provider.SearchIndexableResource; + +import com.android.settings.R; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.search.SearchIndexable; + +import java.util.ArrayList; +import java.util.List; + +@SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC) +public class MobileNetworkListFragment extends DashboardFragment { + private static final String LOG_TAG = "NetworkListFragment"; + + @Override + protected int getPreferenceScreenResId() { + return R.xml.mobile_network_list; + } + + @Override + protected String getLogTag() { + return LOG_TAG; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.MOBILE_NETWORK_LIST; + } + + @Override + protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { + final List<AbstractPreferenceController> controllers = new ArrayList<>(); + controllers.add(new MobileNetworkListController(getContext(), getLifecycle())); + return controllers; + } + + public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, + boolean enabled) { + final ArrayList<SearchIndexableResource> result = new ArrayList<>(); + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.mobile_network_list; + result.add(sir); + return result; + } + + @Override + protected boolean isPageSearchEnabled(Context context) { + return context.getSystemService(UserManager.class).isAdminUser(); + } + }; +} diff --git a/src/com/android/settings/network/MobileNetworkPreferenceController.java b/src/com/android/settings/network/MobileNetworkPreferenceController.java index e4d52bc8d0..381343505e 100644 --- a/src/com/android/settings/network/MobileNetworkPreferenceController.java +++ b/src/com/android/settings/network/MobileNetworkPreferenceController.java @@ -19,20 +19,25 @@ import static android.os.UserHandle.myUserId; import static android.os.UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS; import android.content.BroadcastReceiver; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.UserManager; import android.provider.Settings; -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; import android.telephony.PhoneStateListener; import android.telephony.ServiceState; import android.telephony.TelephonyManager; +import android.util.FeatureFlagUtils; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; +import com.android.settings.core.FeatureFlags; import com.android.settings.core.PreferenceControllerMixin; -import com.android.settingslib.RestrictedLockUtils; +import com.android.settings.network.telephony.MobileNetworkActivity; +import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.RestrictedPreference; import com.android.settingslib.Utils; import com.android.settingslib.core.AbstractPreferenceController; @@ -43,7 +48,12 @@ import com.android.settingslib.core.lifecycle.events.OnStop; public class MobileNetworkPreferenceController extends AbstractPreferenceController implements PreferenceControllerMixin, LifecycleObserver, OnStart, OnStop { - private static final String KEY_MOBILE_NETWORK_SETTINGS = "mobile_network_settings"; + @VisibleForTesting + static final String KEY_MOBILE_NETWORK_SETTINGS = "mobile_network_settings"; + @VisibleForTesting + static final String MOBILE_NETWORK_PACKAGE = "com.android.phone"; + @VisibleForTesting + static final String MOBILE_NETWORK_CLASS = "com.android.phone.MobileNetworkSettings"; private final boolean mIsSecondaryUser; private final TelephonyManager mTelephonyManager; @@ -75,7 +85,7 @@ public class MobileNetworkPreferenceController extends AbstractPreferenceControl public boolean isUserRestricted() { return mIsSecondaryUser || - RestrictedLockUtils.hasBaseUserRestriction( + RestrictedLockUtilsInternal.hasBaseUserRestriction( mContext, DISALLOW_CONFIG_MOBILE_NETWORKS, myUserId()); @@ -134,6 +144,23 @@ public class MobileNetworkPreferenceController extends AbstractPreferenceControl } @Override + public boolean handlePreferenceTreeClick(Preference preference) { + if (KEY_MOBILE_NETWORK_SETTINGS.equals(preference.getKey())) { + if (FeatureFlagUtils.isEnabled(mContext, FeatureFlags.MOBILE_NETWORK_V2)) { + final Intent intent = new Intent(mContext, MobileNetworkActivity.class); + mContext.startActivity(intent); + } else { + final Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setComponent( + new ComponentName(MOBILE_NETWORK_PACKAGE, MOBILE_NETWORK_CLASS)); + mContext.startActivity(intent); + } + return true; + } + return false; + } + + @Override public CharSequence getSummary() { return mTelephonyManager.getNetworkOperatorName(); } diff --git a/src/com/android/settings/network/MobileNetworkSummaryController.java b/src/com/android/settings/network/MobileNetworkSummaryController.java new file mode 100644 index 0000000000..0f76f24de5 --- /dev/null +++ b/src/com/android/settings/network/MobileNetworkSummaryController.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2019 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.network; + +import static androidx.lifecycle.Lifecycle.Event.ON_PAUSE; +import static androidx.lifecycle.Lifecycle.Event.ON_RESUME; + +import android.content.Context; +import android.content.Intent; +import android.os.UserManager; +import android.provider.Settings; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.telephony.euicc.EuiccManager; + +import com.android.settings.R; +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settings.network.telephony.MobileNetworkActivity; +import com.android.settings.network.telephony.MobileNetworkUtils; +import com.android.settings.widget.AddPreference; +import com.android.settingslib.Utils; +import com.android.settingslib.core.AbstractPreferenceController; + +import java.util.List; + +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.OnLifecycleEvent; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +public class MobileNetworkSummaryController extends AbstractPreferenceController implements + SubscriptionsChangeListener.SubscriptionsChangeListenerClient, LifecycleObserver, + PreferenceControllerMixin { + private static final String TAG = "MobileNetSummaryCtlr"; + + private static final String KEY = "mobile_network_list"; + + private SubscriptionManager mSubscriptionManager; + private UserManager mUserManager; + private SubscriptionsChangeListener mChangeListener; + private AddPreference mPreference; + + /** + * This controls the summary text and click behavior of the "Mobile network" item on the + * Network & internet page. There are 3 separate cases depending on the number of mobile network + * subscriptions: + * <ul> + * <li>No subscription: click action begins a UI flow to add a network subscription, and + * the summary text indicates this</li> + * + * <li>One subscription: click action takes you to details for that one network, and + * the summary text is the network name</li> + * + * <li>More than one subscription: click action takes you to a page listing the subscriptions, + * and the summary text gives the count of SIMs</li> + * </ul> + */ + public MobileNetworkSummaryController(Context context, Lifecycle lifecycle) { + super(context); + mSubscriptionManager = context.getSystemService(SubscriptionManager.class); + mUserManager = context.getSystemService(UserManager.class); + if (lifecycle != null) { + mChangeListener = new SubscriptionsChangeListener(context, this); + lifecycle.addObserver(this); + } + } + + @OnLifecycleEvent(ON_RESUME) + public void onResume() { + mChangeListener.start(); + update(); + } + + @OnLifecycleEvent(ON_PAUSE) + public void onPause() { + mChangeListener.stop(); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + } + + @Override + public CharSequence getSummary() { + final List<SubscriptionInfo> subs = SubscriptionUtil.getAvailableSubscriptions( + mContext); + if (subs.isEmpty()) { + if (MobileNetworkUtils.showEuiccSettings(mContext)) { + return mContext.getResources().getString( + R.string.mobile_network_summary_add_a_network); + } + return null; + } else if (subs.size() == 1) { + final SubscriptionInfo info = subs.get(0); + final int subId = info.getSubscriptionId(); + if (!info.isEmbedded() && !mSubscriptionManager.isActiveSubscriptionId(subId)) { + return mContext.getString(R.string.mobile_network_tap_to_activate, + SubscriptionUtil.getDisplayName(info)); + } else { + return subs.get(0).getDisplayName(); + } + } else { + final int count = subs.size(); + return mContext.getResources().getQuantityString(R.plurals.mobile_network_summary_count, + count, count); + } + } + + private void startAddSimFlow() { + final Intent intent = new Intent(EuiccManager.ACTION_PROVISION_EMBEDDED_SUBSCRIPTION); + intent.putExtra(EuiccManager.EXTRA_FORCE_PROVISION, true); + mContext.startActivity(intent); + } + + private void update() { + if (mPreference == null) { + return; + } + refreshSummary(mPreference); + mPreference.setOnPreferenceClickListener(null); + mPreference.setOnAddClickListener(null); + mPreference.setFragment(null); + mPreference.setEnabled(!mChangeListener.isAirplaneModeOn()); + + final List<SubscriptionInfo> subs = SubscriptionUtil.getAvailableSubscriptions( + mContext); + + if (subs.isEmpty()) { + if (MobileNetworkUtils.showEuiccSettings(mContext)) { + mPreference.setOnPreferenceClickListener((Preference pref) -> { + startAddSimFlow(); + return true; + }); + } else { + mPreference.setEnabled(false); + } + } else { + // We have one or more existing subscriptions, so we want the plus button if eSIM is + // supported. + if (MobileNetworkUtils.showEuiccSettings(mContext)) { + mPreference.setAddWidgetEnabled(!mChangeListener.isAirplaneModeOn()); + mPreference.setOnAddClickListener(p -> startAddSimFlow()); + } + + if (subs.size() == 1) { + mPreference.setOnPreferenceClickListener((Preference pref) -> { + final SubscriptionInfo info = subs.get(0); + final int subId = info.getSubscriptionId(); + if (!info.isEmbedded() && !mSubscriptionManager.isActiveSubscriptionId(subId)) { + mSubscriptionManager.setSubscriptionEnabled(subId, true); + } else { + final Intent intent = new Intent(mContext, MobileNetworkActivity.class); + intent.putExtra(Settings.EXTRA_SUB_ID, subs.get(0).getSubscriptionId()); + mContext.startActivity(intent); + } + return true; + }); + } else { + mPreference.setFragment(MobileNetworkListFragment.class.getCanonicalName()); + } + } + } + + @Override + public boolean isAvailable() { + return !Utils.isWifiOnly(mContext) && mUserManager.isAdminUser(); + } + + @Override + public String getPreferenceKey() { + return KEY; + } + + @Override + public void onAirplaneModeChanged(boolean airplaneModeEnabled) { + update(); + } + + @Override + public void onSubscriptionsChanged() { + refreshSummary(mPreference); + update(); + } +} diff --git a/src/com/android/settings/network/MobilePlanPreferenceController.java b/src/com/android/settings/network/MobilePlanPreferenceController.java index f29f11c332..dc41708099 100644 --- a/src/com/android/settings/network/MobilePlanPreferenceController.java +++ b/src/com/android/settings/network/MobilePlanPreferenceController.java @@ -20,7 +20,7 @@ import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.os.UserHandle.myUserId; import static android.os.UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS; -import static com.android.settingslib.RestrictedLockUtils.hasBaseUserRestriction; +import static com.android.settingslib.RestrictedLockUtilsInternal.hasBaseUserRestriction; import android.content.ActivityNotFoundException; import android.content.Context; @@ -31,11 +31,12 @@ import android.net.NetworkInfo; import android.net.Uri; import android.os.Bundle; import android.os.UserManager; -import androidx.preference.Preference; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.Log; +import androidx.preference.Preference; + import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.Utils; diff --git a/src/com/android/settings/network/MultiNetworkHeaderController.java b/src/com/android/settings/network/MultiNetworkHeaderController.java new file mode 100644 index 0000000000..e99cbb6e06 --- /dev/null +++ b/src/com/android/settings/network/MultiNetworkHeaderController.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2018 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.network; + +import android.app.settings.SettingsEnums; +import android.content.Context; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceScreen; + +import com.android.settings.core.BasePreferenceController; +import com.android.settings.wifi.WifiConnectionPreferenceController; +import com.android.settingslib.core.lifecycle.Lifecycle; + +// This controls a header at the top of the Network & internet page that only appears when there +// are two or more active mobile subscriptions. It shows an overview of available network +// connections with an entry for wifi (if connected) and an entry for each subscription. +public class MultiNetworkHeaderController extends BasePreferenceController implements + WifiConnectionPreferenceController.UpdateListener, + SubscriptionsPreferenceController.UpdateListener { + public static final String TAG = "MultiNetworkHdrCtrl"; + + private WifiConnectionPreferenceController mWifiController; + private SubscriptionsPreferenceController mSubscriptionsController; + private PreferenceCategory mPreferenceCategory; + private PreferenceScreen mPreferenceScreen; + private int mOriginalExpandedChildrenCount; + + public MultiNetworkHeaderController(Context context, String key) { + super(context, key); + } + + public void init(Lifecycle lifecycle) { + mWifiController = createWifiController(lifecycle); + mSubscriptionsController = createSubscriptionsController(lifecycle); + } + + @VisibleForTesting + WifiConnectionPreferenceController createWifiController(Lifecycle lifecycle) { + final int prefOrder = 0; + return new WifiConnectionPreferenceController(mContext, lifecycle, this, mPreferenceKey, + prefOrder, SettingsEnums.SETTINGS_NETWORK_CATEGORY); + } + + @VisibleForTesting + SubscriptionsPreferenceController createSubscriptionsController(Lifecycle lifecycle) { + final int prefStartOrder = 10; + return new SubscriptionsPreferenceController(mContext, lifecycle, this, mPreferenceKey, + prefStartOrder); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreferenceScreen = screen; + mOriginalExpandedChildrenCount = mPreferenceScreen.getInitialExpandedChildrenCount(); + mPreferenceCategory = screen.findPreference(mPreferenceKey); + mPreferenceCategory.setVisible(isAvailable()); + mWifiController.displayPreference(screen); + mSubscriptionsController.displayPreference(screen); + } + + @Override + public int getAvailabilityStatus() { + if (mSubscriptionsController == null || !mSubscriptionsController.isAvailable()) { + return CONDITIONALLY_UNAVAILABLE; + } else { + return AVAILABLE; + } + } + + @Override + public void onChildrenUpdated() { + final boolean available = isAvailable(); + // TODO(b/129893781) we need a better way to express where the advanced collapsing starts + // for preference groups that have items dynamically added/removed in the top expanded + // section. + if (mOriginalExpandedChildrenCount != Integer.MAX_VALUE) { + if (available) { + mPreferenceScreen.setInitialExpandedChildrenCount( + mOriginalExpandedChildrenCount + mPreferenceCategory.getPreferenceCount()); + } else { + mPreferenceScreen.setInitialExpandedChildrenCount(mOriginalExpandedChildrenCount); + } + } + mPreferenceCategory.setVisible(available); + } +} diff --git a/src/com/android/settings/network/NetworkDashboardFragment.java b/src/com/android/settings/network/NetworkDashboardFragment.java index 5dafeec2e1..8c686a54aa 100644 --- a/src/com/android/settings/network/NetworkDashboardFragment.java +++ b/src/com/android/settings/network/NetworkDashboardFragment.java @@ -15,34 +15,34 @@ */ package com.android.settings.network; -import static com.android.settings.network.MobilePlanPreferenceController - .MANAGE_MOBILE_PLAN_DIALOG_ID; +import static com.android.settings.network.MobilePlanPreferenceController.MANAGE_MOBILE_PLAN_DIALOG_ID; -import android.app.Activity; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.Fragment; +import android.app.settings.SettingsEnums; import android.content.Context; import android.provider.SearchIndexableResource; -import androidx.annotation.VisibleForTesting; -import android.text.BidiFormatter; import android.util.Log; -import com.android.internal.logging.nano.MetricsProto; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; + import com.android.settings.R; +import com.android.settings.core.FeatureFlags; import com.android.settings.dashboard.DashboardFragment; -import com.android.settings.dashboard.SummaryLoader; +import com.android.settings.development.featureflags.FeatureFlagPersistent; import com.android.settings.network.MobilePlanPreferenceController.MobilePlanPreferenceHost; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.wifi.WifiMasterSwitchPreferenceController; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +@SearchIndexable public class NetworkDashboardFragment extends DashboardFragment implements MobilePlanPreferenceHost { @@ -50,7 +50,7 @@ public class NetworkDashboardFragment extends DashboardFragment implements @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.SETTINGS_NETWORK_CATEGORY; + return SettingsEnums.SETTINGS_NETWORK_CATEGORY; } @Override @@ -60,13 +60,20 @@ public class NetworkDashboardFragment extends DashboardFragment implements @Override protected int getPreferenceScreenResId() { - return R.xml.network_and_internet; + if (FeatureFlagPersistent.isEnabled(getContext(), FeatureFlags.NETWORK_INTERNET_V2)) { + return R.xml.network_and_internet_v2; + } else { + return R.xml.network_and_internet; + } } @Override public void onAttach(Context context) { super.onAttach(context); + if (FeatureFlagPersistent.isEnabled(context, FeatureFlags.NETWORK_INTERNET_V2)) { + use(MultiNetworkHeaderController.class).init(getSettingsLifecycle()); + } use(AirplaneModePreferenceController.class).setFragment(this); } @@ -77,9 +84,8 @@ public class NetworkDashboardFragment extends DashboardFragment implements @Override protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { - return buildPreferenceControllers(context, getLifecycle(), mMetricsFeatureProvider, this - /* fragment */, - this /* mobilePlanHost */); + return buildPreferenceControllers(context, getSettingsLifecycle(), mMetricsFeatureProvider, + this /* fragment */, this /* mobilePlanHost */); } private static List<AbstractPreferenceController> buildPreferenceControllers(Context context, @@ -89,8 +95,11 @@ public class NetworkDashboardFragment extends DashboardFragment implements new MobilePlanPreferenceController(context, mobilePlanHost); final WifiMasterSwitchPreferenceController wifiPreferenceController = new WifiMasterSwitchPreferenceController(context, metricsFeatureProvider); - final MobileNetworkPreferenceController mobileNetworkPreferenceController = - new MobileNetworkPreferenceController(context); + MobileNetworkPreferenceController mobileNetworkPreferenceController = null; + if (!FeatureFlagPersistent.isEnabled(context, FeatureFlags.NETWORK_INTERNET_V2)) { + mobileNetworkPreferenceController = new MobileNetworkPreferenceController(context); + } + final VpnPreferenceController vpnPreferenceController = new VpnPreferenceController(context); final PrivateDnsPreferenceController privateDnsPreferenceController = @@ -99,13 +108,21 @@ public class NetworkDashboardFragment extends DashboardFragment implements if (lifecycle != null) { lifecycle.addObserver(mobilePlanPreferenceController); lifecycle.addObserver(wifiPreferenceController); - lifecycle.addObserver(mobileNetworkPreferenceController); + if (mobileNetworkPreferenceController != null) { + lifecycle.addObserver(mobileNetworkPreferenceController); + } lifecycle.addObserver(vpnPreferenceController); lifecycle.addObserver(privateDnsPreferenceController); } final List<AbstractPreferenceController> controllers = new ArrayList<>(); - controllers.add(mobileNetworkPreferenceController); + + if (FeatureFlagPersistent.isEnabled(context, FeatureFlags.NETWORK_INTERNET_V2)) { + controllers.add(new MobileNetworkSummaryController(context, lifecycle)); + } + if (mobileNetworkPreferenceController != null) { + controllers.add(mobileNetworkPreferenceController); + } controllers.add(new TetherPreferenceController(context, lifecycle)); controllers.add(vpnPreferenceController); controllers.add(new ProxyPreferenceController(context)); @@ -140,79 +157,23 @@ public class NetworkDashboardFragment extends DashboardFragment implements @Override public int getDialogMetricsCategory(int dialogId) { if (MANAGE_MOBILE_PLAN_DIALOG_ID == dialogId) { - return MetricsProto.MetricsEvent.DIALOG_MANAGE_MOBILE_PLAN; + return SettingsEnums.DIALOG_MANAGE_MOBILE_PLAN; } return 0; } - @VisibleForTesting - static class SummaryProvider implements SummaryLoader.SummaryProvider { - - private final Context mContext; - private final SummaryLoader mSummaryLoader; - private final MobileNetworkPreferenceController mMobileNetworkPreferenceController; - private final TetherPreferenceController mTetherPreferenceController; - - public SummaryProvider(Context context, SummaryLoader summaryLoader) { - this(context, summaryLoader, - new MobileNetworkPreferenceController(context), - new TetherPreferenceController(context, null /* lifecycle */)); - } - - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - SummaryProvider(Context context, SummaryLoader summaryLoader, - MobileNetworkPreferenceController mobileNetworkPreferenceController, - TetherPreferenceController tetherPreferenceController) { - mContext = context; - mSummaryLoader = summaryLoader; - mMobileNetworkPreferenceController = mobileNetworkPreferenceController; - mTetherPreferenceController = tetherPreferenceController; - } - - - @Override - public void setListening(boolean listening) { - if (listening) { - String summary = BidiFormatter.getInstance() - .unicodeWrap(mContext.getString(R.string.wifi_settings_title)); - if (mMobileNetworkPreferenceController.isAvailable()) { - final String mobileSettingSummary = mContext.getString( - R.string.network_dashboard_summary_mobile); - summary = mContext.getString(R.string.join_many_items_middle, summary, - mobileSettingSummary); - } - final String dataUsageSettingSummary = mContext.getString( - R.string.network_dashboard_summary_data_usage); - summary = mContext.getString(R.string.join_many_items_middle, summary, - dataUsageSettingSummary); - if (mTetherPreferenceController.isAvailable()) { - final String hotspotSettingSummary = mContext.getString( - R.string.network_dashboard_summary_hotspot); - summary = mContext.getString(R.string.join_many_items_middle, summary, - hotspotSettingSummary); - } - mSummaryLoader.setSummary(this, summary); - } - } - } - - public static final SummaryLoader.SummaryProviderFactory SUMMARY_PROVIDER_FACTORY - = new SummaryLoader.SummaryProviderFactory() { - @Override - public SummaryLoader.SummaryProvider createSummaryProvider(Activity activity, - SummaryLoader summaryLoader) { - return new SummaryProvider(activity, summaryLoader); - } - }; - - public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = new BaseSearchIndexProvider() { @Override public List<SearchIndexableResource> getXmlResourcesToIndex( Context context, boolean enabled) { final SearchIndexableResource sir = new SearchIndexableResource(context); - sir.xmlResId = R.xml.network_and_internet; + if (FeatureFlagPersistent.isEnabled(context, + FeatureFlags.NETWORK_INTERNET_V2)) { + sir.xmlResId = R.xml.network_and_internet_v2; + } else { + sir.xmlResId = R.xml.network_and_internet; + } return Arrays.asList(sir); } diff --git a/src/com/android/settings/network/NetworkResetRestrictionChecker.java b/src/com/android/settings/network/NetworkResetRestrictionChecker.java index 5ae3a77423..46227f9f5a 100644 --- a/src/com/android/settings/network/NetworkResetRestrictionChecker.java +++ b/src/com/android/settings/network/NetworkResetRestrictionChecker.java @@ -19,9 +19,10 @@ package com.android.settings.network; import android.content.Context; import android.os.UserHandle; import android.os.UserManager; + import androidx.annotation.VisibleForTesting; -import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; public class NetworkResetRestrictionChecker { @@ -35,13 +36,13 @@ public class NetworkResetRestrictionChecker { @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) boolean hasUserBaseRestriction() { - return RestrictedLockUtils.hasBaseUserRestriction(mContext, + return RestrictedLockUtilsInternal.hasBaseUserRestriction(mContext, UserManager.DISALLOW_NETWORK_RESET, UserHandle.myUserId()); } @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) boolean isRestrictionEnforcedByAdmin() { - return RestrictedLockUtils.checkIfRestrictionEnforced( + return RestrictedLockUtilsInternal.checkIfRestrictionEnforced( mContext, UserManager.DISALLOW_NETWORK_RESET, UserHandle.myUserId()) != null; } diff --git a/src/com/android/settings/network/NetworkScorerPicker.java b/src/com/android/settings/network/NetworkScorerPicker.java index 0a63adca80..52e4ed9835 100644 --- a/src/com/android/settings/network/NetworkScorerPicker.java +++ b/src/com/android/settings/network/NetworkScorerPicker.java @@ -15,19 +15,20 @@ */ package com.android.settings.network; +import android.app.settings.SettingsEnums; import android.content.Context; import android.net.NetworkScoreManager; import android.net.NetworkScorerAppData; import android.os.Bundle; -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import com.android.internal.logging.nano.MetricsProto; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + import com.android.settings.R; import com.android.settings.core.InstrumentedPreferenceFragment; import com.android.settings.widget.RadioButtonPreference; @@ -44,7 +45,7 @@ public class NetworkScorerPicker extends InstrumentedPreferenceFragment implemen @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.SETTINGS_NETWORK_SCORER; + return SettingsEnums.SETTINGS_NETWORK_SCORER; } @Override diff --git a/src/com/android/settings/network/NetworkScorerPickerPreferenceController.java b/src/com/android/settings/network/NetworkScorerPickerPreferenceController.java index 98482016e4..7239b00d08 100644 --- a/src/com/android/settings/network/NetworkScorerPickerPreferenceController.java +++ b/src/com/android/settings/network/NetworkScorerPickerPreferenceController.java @@ -18,6 +18,7 @@ package com.android.settings.network; import android.content.Context; import android.net.NetworkScoreManager; import android.net.NetworkScorerAppData; + import androidx.preference.Preference; import com.android.settings.R; diff --git a/src/com/android/settings/network/PrivateDnsModeDialogPreference.java b/src/com/android/settings/network/PrivateDnsModeDialogPreference.java index 0627cb3054..0086fecb1d 100644 --- a/src/com/android/settings/network/PrivateDnsModeDialogPreference.java +++ b/src/com/android/settings/network/PrivateDnsModeDialogPreference.java @@ -19,18 +19,19 @@ import static android.net.ConnectivityManager.PRIVATE_DNS_DEFAULT_MODE_FALLBACK; import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_OFF; import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_OPPORTUNISTIC; import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME; -import static android.system.OsConstants.AF_INET; -import static android.system.OsConstants.AF_INET6; -import android.app.AlertDialog; +import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; + +import android.app.settings.SettingsEnums; import android.content.ActivityNotFoundException; import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +import android.net.NetworkUtils; +import android.os.UserHandle; +import android.os.UserManager; import android.provider.Settings; -import androidx.annotation.VisibleForTesting; -import android.system.Os; import android.text.Editable; import android.text.TextWatcher; import android.text.method.LinkMovementMethod; @@ -42,21 +43,25 @@ import android.widget.EditText; import android.widget.RadioGroup; import android.widget.TextView; -import com.android.internal.logging.nano.MetricsProto; +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; +import androidx.preference.PreferenceViewHolder; + import com.android.settings.R; import com.android.settings.overlay.FeatureFactory; import com.android.settings.utils.AnnotationSpan; -import com.android.settingslib.CustomDialogPreference; +import com.android.settingslib.CustomDialogPreferenceCompat; import com.android.settingslib.HelpUtils; +import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; -import java.net.InetAddress; import java.util.HashMap; import java.util.Map; /** * Dialog to set the Private DNS */ -public class PrivateDnsModeDialogPreference extends CustomDialogPreference implements +public class PrivateDnsModeDialogPreference extends CustomDialogPreferenceCompat implements DialogInterface.OnClickListener, RadioGroup.OnCheckedChangeListener, TextWatcher { public static final String ANNOTATION_URL = "url"; @@ -72,8 +77,6 @@ public class PrivateDnsModeDialogPreference extends CustomDialogPreference imple PRIVATE_DNS_MAP.put(PRIVATE_DNS_MODE_PROVIDER_HOSTNAME, R.id.private_dns_mode_provider); } - private static final int[] ADDRESS_FAMILIES = new int[]{AF_INET, AF_INET6}; - @VisibleForTesting static final String MODE_KEY = Settings.Global.PRIVATE_DNS_MODE; @VisibleForTesting @@ -100,19 +103,23 @@ public class PrivateDnsModeDialogPreference extends CustomDialogPreference imple public PrivateDnsModeDialogPreference(Context context) { super(context); + initialize(); } public PrivateDnsModeDialogPreference(Context context, AttributeSet attrs) { super(context, attrs); + initialize(); } public PrivateDnsModeDialogPreference(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); + initialize(); } public PrivateDnsModeDialogPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); + initialize(); } private final AnnotationSpan.LinkInfo mUrlLinkInfo = new AnnotationSpan.LinkInfo( @@ -130,6 +137,30 @@ public class PrivateDnsModeDialogPreference extends CustomDialogPreference imple } }); + private void initialize() { + // Add the "Restricted" icon resource so that if the preference is disabled by the + // admin, an information button will be shown. + setWidgetLayoutResource(R.layout.restricted_icon); + } + + @Override + public void onBindViewHolder(PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + if (isDisabledByAdmin()) { + // If the preference is disabled by the admin, set the inner item as enabled so + // it could act as a click target. The preference itself will have been disabled + // by the controller. + holder.itemView.setEnabled(true); + } + + final View restrictedIcon = holder.findViewById(R.id.restricted_icon); + if (restrictedIcon != null) { + // Show the "Restricted" icon if, and only if, the preference was disabled by + // the admin. + restrictedIcon.setVisibility(isDisabledByAdmin() ? View.VISIBLE : View.GONE); + } + } + @Override protected void onBindDialogView(View view) { final Context context = getContext(); @@ -169,23 +200,19 @@ public class PrivateDnsModeDialogPreference extends CustomDialogPreference imple } FeatureFactory.getFactory(context).getMetricsFeatureProvider().action(context, - MetricsProto.MetricsEvent.ACTION_PRIVATE_DNS_MODE, mMode); + SettingsEnums.ACTION_PRIVATE_DNS_MODE, mMode); Settings.Global.putString(context.getContentResolver(), MODE_KEY, mMode); } } @Override public void onCheckedChanged(RadioGroup group, int checkedId) { - switch (checkedId) { - case R.id.private_dns_mode_off: - mMode = PRIVATE_DNS_MODE_OFF; - break; - case R.id.private_dns_mode_opportunistic: - mMode = PRIVATE_DNS_MODE_OPPORTUNISTIC; - break; - case R.id.private_dns_mode_provider: - mMode = PRIVATE_DNS_MODE_PROVIDER_HOSTNAME; - break; + if (checkedId == R.id.private_dns_mode_off) { + mMode = PRIVATE_DNS_MODE_OFF; + } else if (checkedId == R.id.private_dns_mode_opportunistic) { + mMode = PRIVATE_DNS_MODE_OPPORTUNISTIC; + } else if (checkedId == R.id.private_dns_mode_provider) { + mMode = PRIVATE_DNS_MODE_PROVIDER_HOSTNAME; } updateDialogInfo(); } @@ -203,21 +230,26 @@ public class PrivateDnsModeDialogPreference extends CustomDialogPreference imple updateDialogInfo(); } - private boolean isWeaklyValidatedHostname(String hostname) { - // TODO(b/34953048): Use a validation method that permits more accurate, - // but still inexpensive, checking of likely valid DNS hostnames. - final String WEAK_HOSTNAME_REGEX = "^[a-zA-Z0-9_.-]+$"; - if (!hostname.matches(WEAK_HOSTNAME_REGEX)) { - return false; + @Override + public void performClick() { + EnforcedAdmin enforcedAdmin = getEnforcedAdmin(); + + if (enforcedAdmin == null) { + // If the restriction is not restricted by admin, continue as usual. + super.performClick(); + } else { + // Show a dialog explaining to the user why they cannot change the preference. + RestrictedLockUtils.sendShowAdminSupportDetailsIntent(getContext(), enforcedAdmin); } + } - for (int address_family : ADDRESS_FAMILIES) { - if (Os.inet_pton(address_family, hostname) != null) { - return false; - } - } + private EnforcedAdmin getEnforcedAdmin() { + return RestrictedLockUtilsInternal.checkIfRestrictionEnforced( + getContext(), UserManager.DISALLOW_CONFIG_PRIVATE_DNS, UserHandle.myUserId()); + } - return true; + private boolean isDisabledByAdmin() { + return getEnforcedAdmin() != null; } private Button getSaveButton() { @@ -236,7 +268,7 @@ public class PrivateDnsModeDialogPreference extends CustomDialogPreference imple final Button saveButton = getSaveButton(); if (saveButton != null) { saveButton.setEnabled(modeProvider - ? isWeaklyValidatedHostname(mEditText.getText().toString()) + ? NetworkUtils.isWeaklyValidatedHostname(mEditText.getText().toString()) : true); } } diff --git a/src/com/android/settings/network/PrivateDnsPreferenceController.java b/src/com/android/settings/network/PrivateDnsPreferenceController.java index 67f300c0ce..84cae88f85 100644 --- a/src/com/android/settings/network/PrivateDnsPreferenceController.java +++ b/src/com/android/settings/network/PrivateDnsPreferenceController.java @@ -23,8 +23,8 @@ import static android.provider.Settings.Global.PRIVATE_DNS_DEFAULT_MODE; import static android.provider.Settings.Global.PRIVATE_DNS_MODE; import static android.provider.Settings.Global.PRIVATE_DNS_SPECIFIER; -import android.content.Context; import android.content.ContentResolver; +import android.content.Context; import android.content.res.Resources; import android.database.ContentObserver; import android.net.ConnectivityManager; @@ -34,7 +34,10 @@ import android.net.Network; import android.net.Uri; import android.os.Handler; import android.os.Looper; +import android.os.UserHandle; +import android.os.UserManager; import android.provider.Settings; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; @@ -42,9 +45,11 @@ import com.android.internal.util.ArrayUtils; import com.android.settings.R; import com.android.settings.core.BasePreferenceController; import com.android.settings.core.PreferenceControllerMixin; +import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; +import com.android.settingslib.RestrictedLockUtilsInternal; +import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnStart; import com.android.settingslib.core.lifecycle.events.OnStop; -import com.android.settingslib.core.lifecycle.LifecycleObserver; import java.net.InetAddress; import java.util.List; @@ -79,7 +84,9 @@ public class PrivateDnsPreferenceController extends BasePreferenceController @Override public int getAvailabilityStatus() { - return AVAILABLE; + return mContext.getResources().getBoolean(R.bool.config_show_private_dns_settings) + ? AVAILABLE + : UNSUPPORTED_ON_DEVICE; } @Override @@ -129,6 +136,18 @@ public class PrivateDnsPreferenceController extends BasePreferenceController return ""; } + @Override + public void updateState(Preference preference) { + super.updateState(preference); + preference.setEnabled(!isManagedByAdmin()); + } + + private boolean isManagedByAdmin() { + EnforcedAdmin enforcedAdmin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced( + mContext, UserManager.DISALLOW_CONFIG_PRIVATE_DNS, UserHandle.myUserId()); + return enforcedAdmin != null; + } + private class PrivateDnsSettingsObserver extends ContentObserver { public PrivateDnsSettingsObserver(Handler h) { super(h); diff --git a/src/com/android/settings/network/ProxyPreferenceController.java b/src/com/android/settings/network/ProxyPreferenceController.java index dc2eb9f5bd..8f26bfb6b1 100644 --- a/src/com/android/settings/network/ProxyPreferenceController.java +++ b/src/com/android/settings/network/ProxyPreferenceController.java @@ -17,6 +17,7 @@ package com.android.settings.network; import android.app.admin.DevicePolicyManager; import android.content.Context; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; diff --git a/src/com/android/settings/network/SubscriptionUtil.java b/src/com/android/settings/network/SubscriptionUtil.java new file mode 100644 index 0000000000..f7235754f6 --- /dev/null +++ b/src/com/android/settings/network/SubscriptionUtil.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2018 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.network; + +import static android.telephony.UiccSlotInfo.CARD_STATE_INFO_PRESENT; + +import static com.android.internal.util.CollectionUtils.emptyIfNull; + +import android.content.Context; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.telephony.UiccSlotInfo; + +import androidx.annotation.VisibleForTesting; + +import java.util.ArrayList; +import java.util.List; + +public class SubscriptionUtil { + private static final String TAG = "SubscriptionUtil"; + private static List<SubscriptionInfo> sAvailableResultsForTesting; + private static List<SubscriptionInfo> sActiveResultsForTesting; + + @VisibleForTesting + public static void setAvailableSubscriptionsForTesting(List<SubscriptionInfo> results) { + sAvailableResultsForTesting = results; + } + + @VisibleForTesting + public static void setActiveSubscriptionsForTesting(List<SubscriptionInfo> results) { + sActiveResultsForTesting = results; + } + + public static List<SubscriptionInfo> getActiveSubscriptions(SubscriptionManager manager) { + if (sActiveResultsForTesting != null) { + return sActiveResultsForTesting; + } + final List<SubscriptionInfo> subscriptions = manager.getActiveSubscriptionInfoList(true); + if (subscriptions == null) { + return new ArrayList<>(); + } + return subscriptions; + } + + @VisibleForTesting + static boolean isInactiveInsertedPSim(UiccSlotInfo slotInfo) { + if (slotInfo == null) { + return false; + } + return !slotInfo.getIsEuicc() && !slotInfo.getIsActive() && + slotInfo.getCardStateInfo() == CARD_STATE_INFO_PRESENT; + } + + public static List<SubscriptionInfo> getAvailableSubscriptions(Context context) { + if (sAvailableResultsForTesting != null) { + return sAvailableResultsForTesting; + } + final SubscriptionManager subMgr = context.getSystemService(SubscriptionManager.class); + final TelephonyManager telMgr = context.getSystemService(TelephonyManager.class); + + List<SubscriptionInfo> subscriptions = + new ArrayList<>(emptyIfNull(subMgr.getSelectableSubscriptionInfoList())); + + // Look for inactive but present physical SIMs that are missing from the selectable list. + final List<UiccSlotInfo> missing = new ArrayList<>(); + UiccSlotInfo[] slotsInfo = telMgr.getUiccSlotsInfo(); + for (int i = 0; slotsInfo != null && i < slotsInfo.length; i++) { + final UiccSlotInfo slotInfo = slotsInfo[i]; + if (isInactiveInsertedPSim(slotInfo)) { + final int index = slotInfo.getLogicalSlotIdx(); + final String cardId = slotInfo.getCardId(); + + final boolean found = subscriptions.stream().anyMatch(info -> + index == info.getSimSlotIndex() && cardId.equals(info.getCardString())); + if (!found) { + missing.add(slotInfo); + } + } + } + if (!missing.isEmpty()) { + for (SubscriptionInfo info : subMgr.getAllSubscriptionInfoList()) { + for (UiccSlotInfo slotInfo : missing) { + if (info.getSimSlotIndex() == slotInfo.getLogicalSlotIdx() && + info.getCardString().equals(slotInfo.getCardId())) { + subscriptions.add(info); + break; + } + } + } + } + return subscriptions; + } + + public static String getDisplayName(SubscriptionInfo info) { + final CharSequence name = info.getDisplayName(); + if (name != null) { + return name.toString(); + } + return ""; + } +} diff --git a/src/com/android/settings/network/SubscriptionsChangeListener.java b/src/com/android/settings/network/SubscriptionsChangeListener.java new file mode 100644 index 0000000000..1ecd770540 --- /dev/null +++ b/src/com/android/settings/network/SubscriptionsChangeListener.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2018 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.network; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.provider.Settings; +import android.telephony.SubscriptionManager; +import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener; + +import com.android.internal.telephony.TelephonyIntents; + +/** Helper class for listening to changes in availability of telephony subscriptions */ +public class SubscriptionsChangeListener extends ContentObserver { + + public interface SubscriptionsChangeListenerClient { + void onAirplaneModeChanged(boolean airplaneModeEnabled); + void onSubscriptionsChanged(); + } + + private Context mContext; + private SubscriptionsChangeListenerClient mClient; + private SubscriptionManager mSubscriptionManager; + private OnSubscriptionsChangedListener mSubscriptionsChangedListener; + private Uri mAirplaneModeSettingUri; + private BroadcastReceiver mBroadcastReceiver; + + public SubscriptionsChangeListener(Context context, SubscriptionsChangeListenerClient client) { + super(new Handler()); + mContext = context; + mClient = client; + mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class); + mSubscriptionsChangedListener = new OnSubscriptionsChangedListener() { + @Override + public void onSubscriptionsChanged() { + subscriptionsChangedCallback(); + } + }; + mAirplaneModeSettingUri = Settings.Global.getUriFor(Settings.Global.AIRPLANE_MODE_ON); + mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (!isInitialStickyBroadcast()) { + subscriptionsChangedCallback(); + } + } + }; + } + + public void start() { + mSubscriptionManager.addOnSubscriptionsChangedListener(mSubscriptionsChangedListener); + mContext.getContentResolver() + .registerContentObserver(mAirplaneModeSettingUri, false, this); + final IntentFilter radioTechnologyChangedFilter = new IntentFilter( + TelephonyIntents.ACTION_RADIO_TECHNOLOGY_CHANGED); + mContext.registerReceiver(mBroadcastReceiver, radioTechnologyChangedFilter); + } + + public void stop() { + mSubscriptionManager.removeOnSubscriptionsChangedListener(mSubscriptionsChangedListener); + mContext.getContentResolver().unregisterContentObserver(this); + mContext.unregisterReceiver(mBroadcastReceiver); + } + + public boolean isAirplaneModeOn() { + return Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.AIRPLANE_MODE_ON, 0) != 0; + } + + private void subscriptionsChangedCallback() { + mClient.onSubscriptionsChanged(); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + if (uri.equals(mAirplaneModeSettingUri)) { + mClient.onAirplaneModeChanged(isAirplaneModeOn()); + } + } +} diff --git a/src/com/android/settings/network/SubscriptionsPreferenceController.java b/src/com/android/settings/network/SubscriptionsPreferenceController.java new file mode 100644 index 0000000000..e9cdb46e6f --- /dev/null +++ b/src/com/android/settings/network/SubscriptionsPreferenceController.java @@ -0,0 +1,333 @@ +/* + * Copyright (C) 2018 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.network; + +import static androidx.lifecycle.Lifecycle.Event.ON_PAUSE; +import static androidx.lifecycle.Lifecycle.Event.ON_RESUME; + +import static com.android.settings.network.telephony.MobileNetworkUtils.NO_CELL_DATA_TYPE_ICON; + +import android.content.Context; +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.provider.Settings; +import android.telephony.SignalStrength; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.util.ArraySet; + +import androidx.annotation.VisibleForTesting; +import androidx.collection.ArrayMap; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.OnLifecycleEvent; +import androidx.preference.Preference; +import androidx.preference.PreferenceGroup; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.network.telephony.DataConnectivityListener; +import com.android.settings.network.telephony.MobileNetworkActivity; +import com.android.settings.network.telephony.MobileNetworkUtils; +import com.android.settings.network.telephony.SignalStrengthListener; +import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.net.SignalStrengthUtil; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +/** + * This manages a set of Preferences it places into a PreferenceGroup owned by some parent + * controller class - one for each available subscription. This controller is only considered + * available if there are 2 or more subscriptions. + */ +public class SubscriptionsPreferenceController extends AbstractPreferenceController implements + LifecycleObserver, SubscriptionsChangeListener.SubscriptionsChangeListenerClient, + MobileDataEnabledListener.Client, DataConnectivityListener.Client, + SignalStrengthListener.Callback { + private static final String TAG = "SubscriptionsPrefCntrlr"; + + private UpdateListener mUpdateListener; + private String mPreferenceGroupKey; + private PreferenceGroup mPreferenceGroup; + private SubscriptionManager mManager; + private ConnectivityManager mConnectivityManager; + private SubscriptionsChangeListener mSubscriptionsListener; + private MobileDataEnabledListener mDataEnabledListener; + private DataConnectivityListener mConnectivityListener; + private SignalStrengthListener mSignalStrengthListener; + + + // Map of subscription id to Preference + private Map<Integer, Preference> mSubscriptionPreferences; + private int mStartOrder; + + /** + * This interface lets a parent of this class know that some change happened - this could + * either be because overall availability changed, or because we've added/removed/updated some + * preferences. + */ + public interface UpdateListener { + void onChildrenUpdated(); + } + + /** + * @param context the context for the UI where we're placing these preferences + * @param lifecycle for listening to lifecycle events for the UI + * @param updateListener called to let our parent controller know that our availability has + * changed, or that one or more of the preferences we've placed in the + * PreferenceGroup has changed + * @param preferenceGroupKey the key used to lookup the PreferenceGroup where Preferences will + * be placed + * @param startOrder the order that should be given to the first Preference placed into + * the PreferenceGroup; the second will use startOrder+1, third will + * use startOrder+2, etc. - this is useful for when the parent wants + * to have other preferences in the same PreferenceGroup and wants + * a specific ordering relative to this controller's prefs. + */ + public SubscriptionsPreferenceController(Context context, Lifecycle lifecycle, + UpdateListener updateListener, String preferenceGroupKey, int startOrder) { + super(context); + mUpdateListener = updateListener; + mPreferenceGroupKey = preferenceGroupKey; + mStartOrder = startOrder; + mManager = context.getSystemService(SubscriptionManager.class); + mConnectivityManager = mContext.getSystemService(ConnectivityManager.class); + mSubscriptionPreferences = new ArrayMap<>(); + mSubscriptionsListener = new SubscriptionsChangeListener(context, this); + mDataEnabledListener = new MobileDataEnabledListener(context, this); + mConnectivityListener = new DataConnectivityListener(context, this); + mSignalStrengthListener = new SignalStrengthListener(context, this); + lifecycle.addObserver(this); + } + + @OnLifecycleEvent(ON_RESUME) + public void onResume() { + mSubscriptionsListener.start(); + mDataEnabledListener.start(SubscriptionManager.getDefaultDataSubscriptionId()); + mConnectivityListener.start(); + mSignalStrengthListener.resume(); + update(); + } + + @OnLifecycleEvent(ON_PAUSE) + public void onPause() { + mSubscriptionsListener.stop(); + mDataEnabledListener.stop(); + mConnectivityListener.stop(); + mSignalStrengthListener.pause(); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + mPreferenceGroup = screen.findPreference(mPreferenceGroupKey); + update(); + } + + private void update() { + if (mPreferenceGroup == null) { + return; + } + + if (!isAvailable()) { + for (Preference pref : mSubscriptionPreferences.values()) { + mPreferenceGroup.removePreference(pref); + } + mSubscriptionPreferences.clear(); + mSignalStrengthListener.updateSubscriptionIds(Collections.emptySet()); + mUpdateListener.onChildrenUpdated(); + return; + } + + final Map<Integer, Preference> existingPrefs = mSubscriptionPreferences; + mSubscriptionPreferences = new ArrayMap<>(); + + int order = mStartOrder; + final Set<Integer> activeSubIds = new ArraySet<>(); + final int dataDefaultSubId = SubscriptionManager.getDefaultDataSubscriptionId(); + for (SubscriptionInfo info : SubscriptionUtil.getActiveSubscriptions(mManager)) { + final int subId = info.getSubscriptionId(); + activeSubIds.add(subId); + Preference pref = existingPrefs.remove(subId); + if (pref == null) { + pref = new Preference(mPreferenceGroup.getContext()); + mPreferenceGroup.addPreference(pref); + } + pref.setTitle(info.getDisplayName()); + final boolean isDefaultForData = (subId == dataDefaultSubId); + pref.setSummary(getSummary(subId, isDefaultForData)); + setIcon(pref, subId, isDefaultForData); + pref.setOrder(order++); + + pref.setOnPreferenceClickListener(clickedPref -> { + final Intent intent = new Intent(mContext, MobileNetworkActivity.class); + intent.putExtra(Settings.EXTRA_SUB_ID, subId); + mContext.startActivity(intent); + return true; + }); + + mSubscriptionPreferences.put(subId, pref); + } + mSignalStrengthListener.updateSubscriptionIds(activeSubIds); + + // Remove any old preferences that no longer map to a subscription. + for (Preference pref : existingPrefs.values()) { + mPreferenceGroup.removePreference(pref); + } + mUpdateListener.onChildrenUpdated(); + } + + @VisibleForTesting + boolean shouldInflateSignalStrength(int subId) { + return SignalStrengthUtil.shouldInflateSignalStrength(mContext, subId); + } + + @VisibleForTesting + void setIcon(Preference pref, int subId, boolean isDefaultForData) { + final TelephonyManager mgr = mContext.getSystemService( + TelephonyManager.class).createForSubscriptionId(subId); + final SignalStrength strength = mgr.getSignalStrength(); + int level = (strength == null) ? 0 : strength.getLevel(); + int numLevels = SignalStrength.NUM_SIGNAL_STRENGTH_BINS; + if (shouldInflateSignalStrength(subId)) { + level += 1; + numLevels += 1; + } + final boolean showCutOut = !isDefaultForData || !mgr.isDataEnabled(); + pref.setIcon(getIcon(level, numLevels, showCutOut)); + } + + @VisibleForTesting + Drawable getIcon(int level, int numLevels, boolean cutOut) { + return MobileNetworkUtils.getSignalStrengthIcon(mContext, level, numLevels, + NO_CELL_DATA_TYPE_ICON, cutOut); + } + + private boolean activeNetworkIsCellular() { + final Network activeNetwork = mConnectivityManager.getActiveNetwork(); + if (activeNetwork == null) { + return false; + } + final NetworkCapabilities networkCapabilities = mConnectivityManager.getNetworkCapabilities( + activeNetwork); + if (networkCapabilities == null) { + return false; + } + return networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR); + } + + /** + * The summary can have either 1 or 2 lines depending on which services (calls, SMS, data) this + * subscription is the default for. + * + * If this subscription is the default for calls and/or SMS, we add a line to show that. + * + * If this subscription is the default for data, we add a line with detail about + * whether the data connection is active. + * + * If a subscription isn't the default for anything, we just say it is available. + */ + protected String getSummary(int subId, boolean isDefaultForData) { + final int callsDefaultSubId = SubscriptionManager.getDefaultVoiceSubscriptionId(); + final int smsDefaultSubId = SubscriptionManager.getDefaultSmsSubscriptionId(); + + String line1 = null; + if (subId == callsDefaultSubId && subId == smsDefaultSubId) { + line1 = mContext.getString(R.string.default_for_calls_and_sms); + } else if (subId == callsDefaultSubId) { + line1 = mContext.getString(R.string.default_for_calls); + } else if (subId == smsDefaultSubId) { + line1 = mContext.getString(R.string.default_for_sms); + } + + String line2 = null; + if (isDefaultForData) { + final TelephonyManager telMgrForSub = mContext.getSystemService( + TelephonyManager.class).createForSubscriptionId(subId); + final boolean dataEnabled = telMgrForSub.isDataEnabled(); + if (dataEnabled && activeNetworkIsCellular()) { + line2 = mContext.getString(R.string.mobile_data_active); + } else if (!dataEnabled) { + line2 = mContext.getString(R.string.mobile_data_off); + } else { + line2 = mContext.getString(R.string.default_for_mobile_data); + } + } + + if (line1 != null && line2 != null) { + return String.join(System.lineSeparator(), line1, line2); + } else if (line1 != null) { + return line1; + } else if (line2 != null) { + return line2; + } else { + return mContext.getString(R.string.subscription_available); + } + } + + /** + * @return true if there are at least 2 available subscriptions. + */ + @Override + public boolean isAvailable() { + if (mSubscriptionsListener.isAirplaneModeOn()) { + return false; + } + return SubscriptionUtil.getActiveSubscriptions(mManager).size() >= 2; + } + + @Override + public String getPreferenceKey() { + return null; + } + + @Override + public void onAirplaneModeChanged(boolean airplaneModeEnabled) { + update(); + } + + @Override + public void onSubscriptionsChanged() { + // See if we need to change which sub id we're using to listen for enabled/disabled changes. + int defaultDataSubId = SubscriptionManager.getDefaultDataSubscriptionId(); + if (defaultDataSubId != mDataEnabledListener.getSubId()) { + mDataEnabledListener.stop(); + mDataEnabledListener.start(defaultDataSubId); + } + update(); + } + + @Override + public void onMobileDataEnabledChange() { + update(); + } + + @Override + public void onDataConnectivityChange() { + update(); + } + + @Override + public void onSignalStrengthChanged() { + update(); + } +} diff --git a/src/com/android/settings/network/TetherPreferenceController.java b/src/com/android/settings/network/TetherPreferenceController.java index 4bafa2511d..0d4a6a6189 100644 --- a/src/com/android/settings/network/TetherPreferenceController.java +++ b/src/com/android/settings/network/TetherPreferenceController.java @@ -16,8 +16,8 @@ package com.android.settings.network; import static android.os.UserManager.DISALLOW_CONFIG_TETHERING; -import static com.android.settingslib.RestrictedLockUtils.checkIfRestrictionEnforced; -import static com.android.settingslib.RestrictedLockUtils.hasBaseUserRestriction; + +import static com.android.settingslib.RestrictedLockUtilsInternal.checkIfRestrictionEnforced; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothPan; @@ -33,6 +33,7 @@ import android.os.Bundle; import android.os.Handler; import android.os.UserHandle; import android.provider.Settings; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; @@ -40,6 +41,7 @@ import androidx.preference.PreferenceScreen; import com.android.settings.R; import com.android.settings.TetherSettings; import com.android.settings.core.PreferenceControllerMixin; +import com.android.settingslib.TetherUtil; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.core.lifecycle.LifecycleObserver; @@ -112,11 +114,7 @@ public class TetherPreferenceController extends AbstractPreferenceController imp @Override public boolean isAvailable() { - final boolean isBlocked = - (!mConnectivityManager.isTetheringSupported() && !mAdminDisallowedTetherConfig) - || hasBaseUserRestriction(mContext, DISALLOW_CONFIG_TETHERING, - UserHandle.myUserId()); - return !isBlocked; + return TetherUtil.isTetherAvailable(mContext); } @Override diff --git a/src/com/android/settings/network/TopLevelNetworkEntryPreferenceController.java b/src/com/android/settings/network/TopLevelNetworkEntryPreferenceController.java new file mode 100644 index 0000000000..11367367f7 --- /dev/null +++ b/src/com/android/settings/network/TopLevelNetworkEntryPreferenceController.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2018 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.network; + +import android.content.Context; +import android.icu.text.ListFormatter; +import android.text.BidiFormatter; +import android.text.TextUtils; + +import com.android.settings.R; +import com.android.settings.Utils; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.wifi.WifiMasterSwitchPreferenceController; + +import java.util.ArrayList; +import java.util.List; + +public class TopLevelNetworkEntryPreferenceController extends BasePreferenceController { + + private final WifiMasterSwitchPreferenceController mWifiPreferenceController; + private final MobileNetworkPreferenceController mMobileNetworkPreferenceController; + private final TetherPreferenceController mTetherPreferenceController; + + public TopLevelNetworkEntryPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + mMobileNetworkPreferenceController = new MobileNetworkPreferenceController(mContext); + mTetherPreferenceController = new TetherPreferenceController( + mContext, null /* lifecycle */); + mWifiPreferenceController = new WifiMasterSwitchPreferenceController( + mContext, null /* metrics */); + } + + @Override + public int getAvailabilityStatus() { + return Utils.isDemoUser(mContext) ? UNSUPPORTED_ON_DEVICE : AVAILABLE_UNSEARCHABLE; + } + + @Override + public CharSequence getSummary() { + final String wifiSummary = BidiFormatter.getInstance() + .unicodeWrap(mContext.getString(R.string.wifi_settings_title)); + final String mobileSummary = mContext.getString( + R.string.network_dashboard_summary_mobile); + final String dataUsageSummary = mContext.getString( + R.string.network_dashboard_summary_data_usage); + final String hotspotSummary = mContext.getString( + R.string.network_dashboard_summary_hotspot); + + final List<String> summaries = new ArrayList<>(); + if (mWifiPreferenceController.isAvailable() + && !TextUtils.isEmpty(wifiSummary)) { + summaries.add(wifiSummary); + } + if (mMobileNetworkPreferenceController.isAvailable() && !TextUtils.isEmpty(mobileSummary)) { + summaries.add(mobileSummary); + } + if (!TextUtils.isEmpty(dataUsageSummary)) { + summaries.add(dataUsageSummary); + } + if (mTetherPreferenceController.isAvailable() + && !TextUtils.isEmpty(hotspotSummary)) { + summaries.add(hotspotSummary); + } + return ListFormatter.getInstance().format(summaries); + } +} diff --git a/src/com/android/settings/network/VpnPreferenceController.java b/src/com/android/settings/network/VpnPreferenceController.java index bb0bff638e..62589d5bc0 100644 --- a/src/com/android/settings/network/VpnPreferenceController.java +++ b/src/com/android/settings/network/VpnPreferenceController.java @@ -29,17 +29,18 @@ import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; import android.provider.SettingsSlicesContract; +import android.util.Log; +import android.util.SparseArray; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.util.Log; -import android.util.SparseArray; import com.android.internal.net.LegacyVpnInfo; import com.android.internal.net.VpnConfig; import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; -import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnPause; @@ -91,7 +92,7 @@ public class VpnPreferenceController extends AbstractPreferenceController @Override public boolean isAvailable() { - return !RestrictedLockUtils.hasBaseUserRestriction(mContext, + return !RestrictedLockUtilsInternal.hasBaseUserRestriction(mContext, UserManager.DISALLOW_CONFIG_VPN, UserHandle.myUserId()); } diff --git a/src/com/android/settings/network/telephony/ApnPreferenceController.java b/src/com/android/settings/network/telephony/ApnPreferenceController.java new file mode 100644 index 0000000000..98c1f5b6fd --- /dev/null +++ b/src/com/android/settings/network/telephony/ApnPreferenceController.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2018 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.network.telephony; + +import static android.provider.Telephony.Carriers.ENFORCE_MANAGED_URI; + +import android.content.Context; +import android.content.Intent; +import android.database.ContentObserver; +import android.os.Handler; +import android.os.Looper; +import android.os.PersistableBundle; +import android.provider.Settings; +import android.telephony.CarrierConfigManager; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.SettingsActivity; +import com.android.settings.network.ApnSettings; +import com.android.settingslib.RestrictedLockUtilsInternal; +import com.android.settingslib.RestrictedPreference; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.core.lifecycle.events.OnStop; + +/** + * Preference controller for "Apn settings" + */ +public class ApnPreferenceController extends TelephonyBasePreferenceController implements + LifecycleObserver, OnStart, OnStop { + + @VisibleForTesting + CarrierConfigManager mCarrierConfigManager; + private Preference mPreference; + private DpcApnEnforcedObserver mDpcApnEnforcedObserver; + + public ApnPreferenceController(Context context, String key) { + super(context, key); + mCarrierConfigManager = new CarrierConfigManager(context); + mDpcApnEnforcedObserver = new DpcApnEnforcedObserver(new Handler(Looper.getMainLooper())); + } + + @Override + public int getAvailabilityStatus(int subId) { + final PersistableBundle carrierConfig = mCarrierConfigManager.getConfigForSubId(subId); + final boolean isCdmaApn = MobileNetworkUtils.isCdmaOptions(mContext, subId) + && carrierConfig != null + && carrierConfig.getBoolean(CarrierConfigManager.KEY_SHOW_APN_SETTING_CDMA_BOOL); + final boolean isGsmApn = MobileNetworkUtils.isGsmOptions(mContext, subId) + && carrierConfig != null + && carrierConfig.getBoolean(CarrierConfigManager.KEY_APN_EXPAND_BOOL); + final boolean hideCarrierNetwork = carrierConfig == null + || carrierConfig.getBoolean( + CarrierConfigManager.KEY_HIDE_CARRIER_NETWORK_SETTINGS_BOOL); + + return !hideCarrierNetwork && (isCdmaApn || isGsmApn) + ? AVAILABLE + : CONDITIONALLY_UNAVAILABLE; + } + + @Override + public void onStart() { + mDpcApnEnforcedObserver.register(mContext); + } + + @Override + public void onStop() { + mDpcApnEnforcedObserver.unRegister(mContext); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + ((RestrictedPreference) mPreference).setDisabledByAdmin( + MobileNetworkUtils.isDpcApnEnforced(mContext) + ? RestrictedLockUtilsInternal.getDeviceOwner(mContext) + : null); + } + + @Override + public boolean handlePreferenceTreeClick(Preference preference) { + if (getPreferenceKey().equals(preference.getKey())) { + // This activity runs in phone process, we must use intent to start + final Intent intent = new Intent(Settings.ACTION_APN_SETTINGS); + // This will setup the Home and Search affordance + intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_AS_SUBSETTING, true); + intent.putExtra(ApnSettings.SUB_ID, mSubId); + mContext.startActivity(intent); + return true; + } + + return false; + } + + public void init(int subId) { + mSubId = subId; + } + + @VisibleForTesting + void setPreference(Preference preference) { + mPreference = preference; + } + + private class DpcApnEnforcedObserver extends ContentObserver { + DpcApnEnforcedObserver(Handler handler) { + super(handler); + } + + public void register(Context context) { + context.getContentResolver().registerContentObserver(ENFORCE_MANAGED_URI, false, this); + + } + + public void unRegister(Context context) { + context.getContentResolver().unregisterContentObserver(this); + } + + @Override + public void onChange(boolean selfChange) { + updateState(mPreference); + } + } +} diff --git a/src/com/android/settings/network/telephony/CallsDefaultSubscriptionController.java b/src/com/android/settings/network/telephony/CallsDefaultSubscriptionController.java new file mode 100644 index 0000000000..008a3e4929 --- /dev/null +++ b/src/com/android/settings/network/telephony/CallsDefaultSubscriptionController.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2019 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.network.telephony; + +import android.content.Context; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; + +public class CallsDefaultSubscriptionController extends DefaultSubscriptionController { + + public CallsDefaultSubscriptionController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + protected SubscriptionInfo getDefaultSubscriptionInfo() { + return mManager.getDefaultVoiceSubscriptionInfo(); + } + + @Override + protected int getDefaultSubscriptionId() { + return SubscriptionManager.getDefaultVoiceSubscriptionId(); + } + + @Override + protected void setDefaultSubscription(int subscriptionId) { + mManager.setDefaultVoiceSubId(subscriptionId); + } +} diff --git a/src/com/android/settings/network/telephony/CarrierPreferenceController.java b/src/com/android/settings/network/telephony/CarrierPreferenceController.java new file mode 100644 index 0000000000..c381dc422b --- /dev/null +++ b/src/com/android/settings/network/telephony/CarrierPreferenceController.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2018 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.network.telephony; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.PersistableBundle; +import android.telephony.CarrierConfigManager; +import android.telephony.SubscriptionManager; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; + +/** + * Preference controller for "Carrier Settings" + */ +public class CarrierPreferenceController extends TelephonyBasePreferenceController { + + @VisibleForTesting + CarrierConfigManager mCarrierConfigManager; + + public CarrierPreferenceController(Context context, String key) { + super(context, key); + mCarrierConfigManager = new CarrierConfigManager(context); + } + + public void init(int subId) { + mSubId = subId; + } + + @Override + public int getAvailabilityStatus(int subId) { + final PersistableBundle carrierConfig = mCarrierConfigManager.getConfigForSubId(subId); + + // Return available if it is in CDMA or GSM mode, and the flag is on + return carrierConfig != null + && carrierConfig.getBoolean(CarrierConfigManager.KEY_CARRIER_SETTINGS_ENABLE_BOOL) + && (MobileNetworkUtils.isCdmaOptions(mContext, subId) + || MobileNetworkUtils.isGsmOptions(mContext, subId)) + ? AVAILABLE + : CONDITIONALLY_UNAVAILABLE; + } + + @Override + public boolean handlePreferenceTreeClick(Preference preference) { + if (getPreferenceKey().equals(preference.getKey())) { + final Intent carrierSettingsIntent = getCarrierSettingsActivityIntent(mSubId); + if (carrierSettingsIntent != null) { + mContext.startActivity(carrierSettingsIntent); + } + return true; + } + + return false; + } + + private Intent getCarrierSettingsActivityIntent(int subId) { + final PersistableBundle config = mCarrierConfigManager.getConfigForSubId(subId); + final ComponentName cn = ComponentName.unflattenFromString( + config == null ? "" : config.getString( + CarrierConfigManager.KEY_CARRIER_SETTINGS_ACTIVITY_COMPONENT_NAME_STRING, + "" /* default value */)); + + if (cn == null) return null; + + final Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setComponent(cn); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX, subId); + + final PackageManager pm = mContext.getPackageManager(); + final ResolveInfo resolveInfo = pm.resolveActivity(intent, 0 /* flags */); + return resolveInfo != null ? intent : null; + } +} diff --git a/src/com/android/settings/network/telephony/CarrierSettingsVersionPreferenceController.java b/src/com/android/settings/network/telephony/CarrierSettingsVersionPreferenceController.java new file mode 100644 index 0000000000..a6db773d70 --- /dev/null +++ b/src/com/android/settings/network/telephony/CarrierSettingsVersionPreferenceController.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2019 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.network.telephony; + +import android.content.Context; +import android.os.PersistableBundle; +import android.telephony.CarrierConfigManager; +import android.telephony.SubscriptionManager; + +import com.android.settings.core.BasePreferenceController; + +public class CarrierSettingsVersionPreferenceController extends BasePreferenceController { + + private int mSubscriptionId; + private CarrierConfigManager mManager; + + public CarrierSettingsVersionPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + mManager = context.getSystemService(CarrierConfigManager.class); + mSubscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; + } + + public void init(int subscriptionId) { + mSubscriptionId = subscriptionId; + } + + @Override + public CharSequence getSummary() { + final PersistableBundle config = mManager.getConfigForSubId(mSubscriptionId); + if (config == null) { + return null; + } + return config.getString(CarrierConfigManager.KEY_CARRIER_CONFIG_VERSION_STRING); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } +} diff --git a/src/com/android/settings/network/telephony/CellInfoUtil.java b/src/com/android/settings/network/telephony/CellInfoUtil.java new file mode 100644 index 0000000000..def81a10c6 --- /dev/null +++ b/src/com/android/settings/network/telephony/CellInfoUtil.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2018 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.network.telephony; + +import android.telephony.CellIdentity; +import android.telephony.CellIdentityCdma; +import android.telephony.CellIdentityGsm; +import android.telephony.CellIdentityLte; +import android.telephony.CellIdentityWcdma; +import android.telephony.CellInfo; +import android.telephony.CellInfoCdma; +import android.telephony.CellInfoGsm; +import android.telephony.CellInfoLte; +import android.telephony.CellInfoWcdma; +import android.text.BidiFormatter; +import android.text.TextDirectionHeuristics; +import android.text.TextUtils; +import android.util.Log; + +import com.android.internal.telephony.OperatorInfo; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * Add static Utility functions to get information from the CellInfo object. + * TODO: Modify {@link CellInfo} for simplify those functions + */ +public final class CellInfoUtil { + private static final String TAG = "NetworkSelectSetting"; + + private CellInfoUtil() { + } + + /** + * Wrap a CellIdentity into a CellInfo. + */ + public static CellInfo wrapCellInfoWithCellIdentity(CellIdentity cellIdentity) { + if (cellIdentity instanceof CellIdentityLte) { + CellInfoLte cellInfo = new CellInfoLte(); + cellInfo.setCellIdentity((CellIdentityLte) cellIdentity); + return cellInfo; + } else if (cellIdentity instanceof CellIdentityCdma) { + CellInfoCdma cellInfo = new CellInfoCdma(); + cellInfo.setCellIdentity((CellIdentityCdma) cellIdentity); + return cellInfo; + } else if (cellIdentity instanceof CellIdentityWcdma) { + CellInfoWcdma cellInfo = new CellInfoWcdma(); + cellInfo.setCellIdentity((CellIdentityWcdma) cellIdentity); + return cellInfo; + } else if (cellIdentity instanceof CellIdentityGsm) { + CellInfoGsm cellInfo = new CellInfoGsm(); + cellInfo.setCellIdentity((CellIdentityGsm) cellIdentity); + return cellInfo; + } else { + Log.e(TAG, "Invalid CellInfo type"); + return null; + } + } + + /** + * Returns the title of the network obtained in the manual search. + * + * @param cellInfo contains the information of the network. + * @return Long Name if not null/empty, otherwise Short Name if not null/empty, + * else MCCMNC string. + */ + public static String getNetworkTitle(CellInfo cellInfo) { + OperatorInfo oi = getOperatorInfoFromCellInfo(cellInfo); + + if (!TextUtils.isEmpty(oi.getOperatorAlphaLong())) { + return oi.getOperatorAlphaLong(); + } else if (!TextUtils.isEmpty(oi.getOperatorAlphaShort())) { + return oi.getOperatorAlphaShort(); + } else { + BidiFormatter bidiFormatter = BidiFormatter.getInstance(); + return bidiFormatter.unicodeWrap(oi.getOperatorNumeric(), TextDirectionHeuristics.LTR); + } + } + + /** + * Wrap a cell info into an operator info. + */ + public static OperatorInfo getOperatorInfoFromCellInfo(CellInfo cellInfo) { + OperatorInfo oi; + if (cellInfo instanceof CellInfoLte) { + CellInfoLte lte = (CellInfoLte) cellInfo; + oi = new OperatorInfo( + (String) lte.getCellIdentity().getOperatorAlphaLong(), + (String) lte.getCellIdentity().getOperatorAlphaShort(), + lte.getCellIdentity().getMobileNetworkOperator()); + } else if (cellInfo instanceof CellInfoWcdma) { + CellInfoWcdma wcdma = (CellInfoWcdma) cellInfo; + oi = new OperatorInfo( + (String) wcdma.getCellIdentity().getOperatorAlphaLong(), + (String) wcdma.getCellIdentity().getOperatorAlphaShort(), + wcdma.getCellIdentity().getMobileNetworkOperator()); + } else if (cellInfo instanceof CellInfoGsm) { + CellInfoGsm gsm = (CellInfoGsm) cellInfo; + oi = new OperatorInfo( + (String) gsm.getCellIdentity().getOperatorAlphaLong(), + (String) gsm.getCellIdentity().getOperatorAlphaShort(), + gsm.getCellIdentity().getMobileNetworkOperator()); + } else if (cellInfo instanceof CellInfoCdma) { + CellInfoCdma cdma = (CellInfoCdma) cellInfo; + oi = new OperatorInfo( + (String) cdma.getCellIdentity().getOperatorAlphaLong(), + (String) cdma.getCellIdentity().getOperatorAlphaShort(), + "" /* operator numeric */); + } else { + Log.e(TAG, "Invalid CellInfo type"); + oi = new OperatorInfo("", "", ""); + } + return oi; + } + + /** + * Creates a CellInfo object from OperatorInfo. GsmCellInfo is used here only because + * operatorInfo does not contain technology type while CellInfo is an abstract object that + * requires to specify technology type. It doesn't matter which CellInfo type to use here, since + * we only want to wrap the operator info and PLMN to a CellInfo object. + */ + public static CellInfo convertOperatorInfoToCellInfo(OperatorInfo operatorInfo) { + String operatorNumeric = operatorInfo.getOperatorNumeric(); + String mcc = null; + String mnc = null; + if (operatorNumeric != null && operatorNumeric.matches("^[0-9]{5,6}$")) { + mcc = operatorNumeric.substring(0, 3); + mnc = operatorNumeric.substring(3); + } + CellIdentityGsm cig = new CellIdentityGsm( + Integer.MAX_VALUE /* lac */, + Integer.MAX_VALUE /* cid */, + Integer.MAX_VALUE /* arfcn */, + Integer.MAX_VALUE /* bsic */, + mcc, + mnc, + operatorInfo.getOperatorAlphaLong(), + operatorInfo.getOperatorAlphaShort()); + + CellInfoGsm ci = new CellInfoGsm(); + ci.setCellIdentity(cig); + return ci; + } + + /** Checks whether the network operator is forbidden. */ + public static boolean isForbidden(CellInfo cellInfo, List<String> forbiddenPlmns) { + String plmn = CellInfoUtil.getOperatorInfoFromCellInfo(cellInfo).getOperatorNumeric(); + return forbiddenPlmns != null && forbiddenPlmns.contains(plmn); + } + + /** Convert a list of cellInfos to readable string without sensitive info. */ + public static String cellInfoListToString(List<CellInfo> cellInfos) { + return cellInfos.stream() + .map(cellInfo -> cellInfoToString(cellInfo)) + .collect(Collectors.joining(", ")); + } + + /** Convert {@code cellInfo} to a readable string without sensitive info. */ + public static String cellInfoToString(CellInfo cellInfo) { + String cellType = cellInfo.getClass().getSimpleName(); + CellIdentity cid = cellInfo.getCellIdentity(); + return String.format( + "{CellType = %s, isRegistered = %b, mcc = %s, mnc = %s, alphaL = %s, alphaS = %s}", + cellType, cellInfo.isRegistered(), cid.getMccString(), cid.getMncString(), + cid.getOperatorAlphaLong(), cid.getOperatorAlphaShort()); + } +} diff --git a/src/com/android/settings/network/telephony/DataConnectivityListener.java b/src/com/android/settings/network/telephony/DataConnectivityListener.java new file mode 100644 index 0000000000..adb39c64be --- /dev/null +++ b/src/com/android/settings/network/telephony/DataConnectivityListener.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2019 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.network.telephony; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; + +/** A helper class to listen to a few different kinds of connectivity changes that could be relevant + * to changes in which network is active, and whether the active network has internet data + * connectivity. */ +public class DataConnectivityListener extends ConnectivityManager.NetworkCallback { + private Context mContext; + private ConnectivityManager mConnectivityManager; + private final NetworkRequest mNetworkRequest; + private Client mClient; + + public interface Client { + void onDataConnectivityChange(); + } + + public DataConnectivityListener(Context context, Client client) { + mContext = context; + mConnectivityManager = mContext.getSystemService(ConnectivityManager.class); + mClient = client; + mNetworkRequest = new NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .build(); + } + + public void start() { + mConnectivityManager.registerNetworkCallback(mNetworkRequest, this, + mContext.getMainThreadHandler()); + } + + public void stop() { + mConnectivityManager.unregisterNetworkCallback(this); + } + + @Override + public void onCapabilitiesChanged(Network network, NetworkCapabilities networkCapabilities) { + final Network activeNetwork = mConnectivityManager.getActiveNetwork(); + if (activeNetwork != null && activeNetwork.equals(network)) { + mClient.onDataConnectivityChange(); + } + } + + @Override + public void onLosing(Network network, int maxMsToLive) { + mClient.onDataConnectivityChange(); + } + + @Override + public void onLost(Network network) { + mClient.onDataConnectivityChange(); + } +} diff --git a/src/com/android/settings/network/telephony/DataDuringCallsPreferenceController.java b/src/com/android/settings/network/telephony/DataDuringCallsPreferenceController.java new file mode 100644 index 0000000000..6f61d915b9 --- /dev/null +++ b/src/com/android/settings/network/telephony/DataDuringCallsPreferenceController.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2019 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.network.telephony; + +import static androidx.lifecycle.Lifecycle.Event.ON_PAUSE; +import static androidx.lifecycle.Lifecycle.Event.ON_RESUME; + +import android.content.Context; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; + +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.OnLifecycleEvent; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreference; + +import com.android.settings.network.SubscriptionsChangeListener; + +public class DataDuringCallsPreferenceController extends TelephonyTogglePreferenceController + implements LifecycleObserver, + SubscriptionsChangeListener.SubscriptionsChangeListenerClient { + + private SwitchPreference mPreference; + private SubscriptionsChangeListener mChangeListener; + private TelephonyManager mManager; + + public DataDuringCallsPreferenceController(Context context, + String preferenceKey) { + super(context, preferenceKey); + mChangeListener = new SubscriptionsChangeListener(mContext, this); + } + + public void init(Lifecycle lifecycle, int subId) { + this.mSubId = subId; + mManager = mContext.getSystemService(TelephonyManager.class).createForSubscriptionId(subId); + lifecycle.addObserver(this); + } + + @OnLifecycleEvent(ON_RESUME) + public void onResume() { + mChangeListener.start(); + } + + @OnLifecycleEvent(ON_PAUSE) + public void onPause() { + mChangeListener.stop(); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + } + + @Override + public boolean isChecked() { + return mManager.isDataAllowedInVoiceCall(); + } + + @Override + public boolean setChecked(boolean isChecked) { + mManager.setDataAllowedDuringVoiceCall(isChecked); + return true; + } + + @Override + public int getAvailabilityStatus(int subId) { + if (mSubId == SubscriptionManager.INVALID_SUBSCRIPTION_ID || + SubscriptionManager.getDefaultDataSubscriptionId() == mSubId) { + return CONDITIONALLY_UNAVAILABLE; + } + return AVAILABLE; + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + preference.setVisible(isAvailable()); + } + + @Override + public void onAirplaneModeChanged(boolean airplaneModeEnabled) {} + + @Override + public void onSubscriptionsChanged() { + updateState(mPreference); + } +} diff --git a/src/com/android/settings/network/telephony/DataServiceSetupPreferenceController.java b/src/com/android/settings/network/telephony/DataServiceSetupPreferenceController.java new file mode 100644 index 0000000000..d1fbd73e6b --- /dev/null +++ b/src/com/android/settings/network/telephony/DataServiceSetupPreferenceController.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2018 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.network.telephony; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.PersistableBundle; +import android.provider.Settings; +import android.telephony.CarrierConfigManager; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.text.TextUtils; + +import androidx.preference.Preference; + +import com.android.internal.telephony.PhoneConstants; + +/** + * Preference controller for "Data service setup" + */ +public class DataServiceSetupPreferenceController extends TelephonyBasePreferenceController { + + private CarrierConfigManager mCarrierConfigManager; + private TelephonyManager mTelephonyManager; + private String mSetupUrl; + + public DataServiceSetupPreferenceController(Context context, String key) { + super(context, key); + mCarrierConfigManager = context.getSystemService(CarrierConfigManager.class); + mTelephonyManager = context.getSystemService(TelephonyManager.class); + mSetupUrl = Settings.Global.getString(mContext.getContentResolver(), + Settings.Global.SETUP_PREPAID_DATA_SERVICE_URL); + } + + @Override + public int getAvailabilityStatus(int subId) { + final boolean isLteOnCdma = mTelephonyManager.getLteOnCdmaMode() + == PhoneConstants.LTE_ON_CDMA_TRUE; + final PersistableBundle carrierConfig = mCarrierConfigManager.getConfigForSubId(subId); + return subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID + && carrierConfig != null + && !carrierConfig.getBoolean( + CarrierConfigManager.KEY_HIDE_CARRIER_NETWORK_SETTINGS_BOOL) + && isLteOnCdma && !TextUtils.isEmpty(mSetupUrl) + ? AVAILABLE + : CONDITIONALLY_UNAVAILABLE; + } + + public void init(int subId) { + mSubId = subId; + mTelephonyManager = TelephonyManager.from(mContext).createForSubscriptionId(mSubId); + } + + @Override + public boolean handlePreferenceTreeClick(Preference preference) { + if (getPreferenceKey().equals(preference.getKey())) { + if (!TextUtils.isEmpty(mSetupUrl)) { + String imsi = mTelephonyManager.getSubscriberId(); + if (imsi == null) { + imsi = ""; + } + final String url = TextUtils.expandTemplate(mSetupUrl, imsi).toString(); + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + mContext.startActivity(intent); + } + return true; + } + + return false; + } +} diff --git a/src/com/android/settings/network/telephony/DataUsagePreferenceController.java b/src/com/android/settings/network/telephony/DataUsagePreferenceController.java new file mode 100644 index 0000000000..b8a31fe185 --- /dev/null +++ b/src/com/android/settings/network/telephony/DataUsagePreferenceController.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2018 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.network.telephony; + +import android.content.Context; +import android.content.Intent; +import android.net.NetworkTemplate; +import android.provider.Settings; +import android.telephony.SubscriptionManager; +import android.text.TextUtils; + +import androidx.preference.Preference; + +import com.android.settings.R; +import com.android.settings.datausage.DataUsageUtils; +import com.android.settingslib.net.DataUsageController; + +/** + * Preference controller for "Data usage" + */ +public class DataUsagePreferenceController extends TelephonyBasePreferenceController { + + private NetworkTemplate mTemplate; + private DataUsageController.DataUsageInfo mDataUsageInfo; + private Intent mIntent; + + public DataUsagePreferenceController(Context context, String key) { + super(context, key); + } + + @Override + public int getAvailabilityStatus(int subId) { + return subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID + ? AVAILABLE + : AVAILABLE_UNSEARCHABLE; + } + + @Override + public boolean handlePreferenceTreeClick(Preference preference) { + if (TextUtils.equals(preference.getKey(), getPreferenceKey())) { + mContext.startActivity(mIntent); + return true; + } + + return false; + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + if (mSubId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + preference.setEnabled(false); + return; + } + long usageLevel = mDataUsageInfo.usageLevel; + if (usageLevel <= 0L) { + final DataUsageController controller = new DataUsageController(mContext); + usageLevel = controller.getHistoricalUsageLevel(mTemplate); + } + final boolean enabled = usageLevel > 0L; + preference.setEnabled(enabled); + + if (enabled) { + preference.setSummary(mContext.getString(R.string.data_usage_template, + DataUsageUtils.formatDataUsage(mContext, mDataUsageInfo.usageLevel), + mDataUsageInfo.period)); + } + } + + public void init(int subId) { + mSubId = subId; + + if (mSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + mTemplate = DataUsageUtils.getDefaultTemplate(mContext, mSubId); + + final DataUsageController controller = new DataUsageController(mContext); + controller.setSubscriptionId(mSubId); + mDataUsageInfo = controller.getDataUsageInfo(mTemplate); + + mIntent = new Intent(Settings.ACTION_MOBILE_DATA_USAGE); + mIntent.putExtra(Settings.EXTRA_NETWORK_TEMPLATE, mTemplate); + mIntent.putExtra(Settings.EXTRA_SUB_ID, mSubId); + } + } +} diff --git a/src/com/android/settings/network/telephony/DefaultSubscriptionController.java b/src/com/android/settings/network/telephony/DefaultSubscriptionController.java new file mode 100644 index 0000000000..9eb5f8c495 --- /dev/null +++ b/src/com/android/settings/network/telephony/DefaultSubscriptionController.java @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2019 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.network.telephony; + +import static androidx.lifecycle.Lifecycle.Event.ON_PAUSE; +import static androidx.lifecycle.Lifecycle.Event.ON_RESUME; + +import android.content.Context; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.network.SubscriptionUtil; +import com.android.settings.network.SubscriptionsChangeListener; + +import java.util.ArrayList; +import java.util.List; + +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.OnLifecycleEvent; +import androidx.preference.ListPreference; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +/** + * This implements common controller functionality for a Preference letting the user see/change + * what mobile network subscription is used by default for some service controlled by the + * SubscriptionManager. This can be used for services such as Calls or SMS. + */ +public abstract class DefaultSubscriptionController extends BasePreferenceController implements + LifecycleObserver, Preference.OnPreferenceChangeListener, + SubscriptionsChangeListener.SubscriptionsChangeListenerClient { + private static final String TAG = "DefaultSubController"; + + protected SubscriptionsChangeListener mChangeListener; + protected ListPreference mPreference; + protected SubscriptionManager mManager; + + public DefaultSubscriptionController(Context context, String preferenceKey) { + super(context, preferenceKey); + mManager = context.getSystemService(SubscriptionManager.class); + mChangeListener = new SubscriptionsChangeListener(context, this); + } + + public void init(Lifecycle lifecycle) { + lifecycle.addObserver(this); + } + + /** @return SubscriptionInfo for the default subscription for the service, or null if there + * isn't one. */ + protected abstract SubscriptionInfo getDefaultSubscriptionInfo(); + + /** @return the id of the default subscription for the service, or + * SubscriptionManager.INVALID_SUBSCRIPTION_ID if there isn't one. */ + protected abstract int getDefaultSubscriptionId(); + + /** Called to change the default subscription for the service. */ + protected abstract void setDefaultSubscription(int subscriptionId); + + @Override + public int getAvailabilityStatus() { + final List<SubscriptionInfo> subs = SubscriptionUtil.getActiveSubscriptions(mManager); + if (subs.size() > 1) { + return AVAILABLE; + } else { + return CONDITIONALLY_UNAVAILABLE; + } + } + + @OnLifecycleEvent(ON_RESUME) + public void onResume() { + mChangeListener.start(); + updateEntries(); + } + + @OnLifecycleEvent(ON_PAUSE) + public void onPause() { + mChangeListener.stop(); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + updateEntries(); + } + + @Override + public CharSequence getSummary() { + final SubscriptionInfo info = getDefaultSubscriptionInfo(); + if (info != null) { + return info.getDisplayName(); + } else { + return mContext.getString(R.string.calls_and_sms_ask_every_time); + } + } + + private void updateEntries() { + if (mPreference == null) { + return; + } + if (!isAvailable()) { + mPreference.setVisible(false); + return; + } + mPreference.setVisible(true); + + // TODO(b/135142209) - for now we need to manually ensure we're registered as a change + // listener, because this might not have happened during displayPreference if + // getAvailabilityStatus returned CONDITIONALLY_UNAVAILABLE at the time. + mPreference.setOnPreferenceChangeListener(this); + + final List<SubscriptionInfo> subs = SubscriptionUtil.getActiveSubscriptions(mManager); + + // We'll have one entry for each available subscription, plus one for a "ask me every + // time" entry at the end. + final ArrayList<CharSequence> displayNames = new ArrayList<>(); + final ArrayList<CharSequence> subscriptionIds = new ArrayList<>(); + + final int serviceDefaultSubId = getDefaultSubscriptionId(); + boolean subIsAvailable = false; + + for (SubscriptionInfo sub : subs) { + if (sub.isOpportunistic()) { + continue; + } + displayNames.add(sub.getDisplayName()); + final int subId = sub.getSubscriptionId(); + subscriptionIds.add(Integer.toString(subId)); + if (subId == serviceDefaultSubId) { + subIsAvailable = true; + } + } + // Add the extra "Ask every time" value at the end. + displayNames.add(mContext.getString(R.string.calls_and_sms_ask_every_time)); + subscriptionIds.add(Integer.toString(SubscriptionManager.INVALID_SUBSCRIPTION_ID)); + + mPreference.setEntries(displayNames.toArray(new CharSequence[0])); + mPreference.setEntryValues(subscriptionIds.toArray(new CharSequence[0])); + + if (subIsAvailable) { + mPreference.setValue(Integer.toString(serviceDefaultSubId)); + } else { + mPreference.setValue(Integer.toString(SubscriptionManager.INVALID_SUBSCRIPTION_ID)); + } + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + final int subscriptionId = Integer.parseInt((String) newValue); + setDefaultSubscription(subscriptionId); + refreshSummary(mPreference); + return true; + } + + @Override + public void onAirplaneModeChanged(boolean airplaneModeEnabled) { + } + + @Override + public void onSubscriptionsChanged() { + if (mPreference != null) { + updateEntries(); + refreshSummary(mPreference); + } + } +} diff --git a/src/com/android/settings/network/telephony/DeleteSimProfilePreferenceController.java b/src/com/android/settings/network/telephony/DeleteSimProfilePreferenceController.java new file mode 100644 index 0000000000..daabf8bc0d --- /dev/null +++ b/src/com/android/settings/network/telephony/DeleteSimProfilePreferenceController.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2019 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.network.telephony; + +import android.content.Context; +import android.content.Intent; +import android.telephony.SubscriptionInfo; +import android.telephony.euicc.EuiccManager; + +import androidx.fragment.app.Fragment; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.core.BasePreferenceController; +import com.android.settings.network.SubscriptionUtil; + +/** This controls a preference allowing the user to delete the profile for an eSIM. */ +public class DeleteSimProfilePreferenceController extends BasePreferenceController { + + private SubscriptionInfo mSubscriptionInfo; + private Fragment mParentFragment; + private int mRequestCode; + + public DeleteSimProfilePreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + public void init(int subscriptionId, Fragment parentFragment, int requestCode) { + mParentFragment = parentFragment; + + for (SubscriptionInfo info : SubscriptionUtil.getAvailableSubscriptions( + mContext)) { + if (info.getSubscriptionId() == subscriptionId && info.isEmbedded()) { + mSubscriptionInfo = info; + break; + } + } + mRequestCode = requestCode; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + final Preference pref = screen.findPreference(getPreferenceKey()); + pref.setOnPreferenceClickListener(p -> { + final Intent intent = new Intent(EuiccManager.ACTION_DELETE_SUBSCRIPTION_PRIVILEGED); + intent.putExtra(EuiccManager.EXTRA_SUBSCRIPTION_ID, + mSubscriptionInfo.getSubscriptionId()); + mParentFragment.startActivityForResult(intent, mRequestCode); + return true; + }); + } + + @Override + public int getAvailabilityStatus() { + if (mSubscriptionInfo != null) { + return AVAILABLE; + } else { + return CONDITIONALLY_UNAVAILABLE; + } + } + +} diff --git a/src/com/android/settings/network/telephony/DisableSimFooterPreferenceController.java b/src/com/android/settings/network/telephony/DisableSimFooterPreferenceController.java new file mode 100644 index 0000000000..ab01b9d5a8 --- /dev/null +++ b/src/com/android/settings/network/telephony/DisableSimFooterPreferenceController.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2019 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.network.telephony; + +import android.content.Context; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; + +import com.android.settings.core.BasePreferenceController; +import com.android.settings.network.SubscriptionUtil; + +public class DisableSimFooterPreferenceController extends BasePreferenceController { + private int mSubId; + + public DisableSimFooterPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; + } + + public void init(int subId) { + mSubId = subId; + } + + @Override + public int getAvailabilityStatus() { + if (mSubId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + return CONDITIONALLY_UNAVAILABLE; + } + for (SubscriptionInfo info : SubscriptionUtil.getAvailableSubscriptions(mContext)) { + if (info.getSubscriptionId() == mSubId) { + if (info.isEmbedded()) { + return CONDITIONALLY_UNAVAILABLE; + } + break; + } + } + return AVAILABLE; + } +} diff --git a/src/com/android/settings/network/telephony/DisabledSubscriptionController.java b/src/com/android/settings/network/telephony/DisabledSubscriptionController.java new file mode 100644 index 0000000000..cd51735922 --- /dev/null +++ b/src/com/android/settings/network/telephony/DisabledSubscriptionController.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2019 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.network.telephony; + +import static androidx.lifecycle.Lifecycle.Event.ON_PAUSE; +import static androidx.lifecycle.Lifecycle.Event.ON_RESUME; + +import android.content.Context; +import android.telephony.SubscriptionManager; + +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.OnLifecycleEvent; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceScreen; + +import com.android.settings.core.BasePreferenceController; +import com.android.settings.network.SubscriptionsChangeListener; + +public class DisabledSubscriptionController extends BasePreferenceController implements + SubscriptionsChangeListener.SubscriptionsChangeListenerClient, LifecycleObserver { + private PreferenceCategory mCategory; + private int mSubId; + private SubscriptionsChangeListener mChangeListener; + private SubscriptionManager mSubscriptionManager; + + public DisabledSubscriptionController(Context context, String preferenceKey) { + super(context, preferenceKey); + mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; + mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class); + mChangeListener = new SubscriptionsChangeListener(context, this); + } + + public void init(Lifecycle lifecycle, int subId) { + lifecycle.addObserver(this); + mSubId = subId; + } + + @OnLifecycleEvent(ON_RESUME) + public void onResume() { + mChangeListener.start(); + update(); + } + + @OnLifecycleEvent(ON_PAUSE) + public void onPause() { + mChangeListener.stop(); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mCategory = screen.findPreference(getPreferenceKey()); + update(); + } + + private void update() { + if (mCategory == null || mSubId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + return; + } + // TODO b/135222940: re-evaluate whether to use mSubscriptionManager#isSubscriptionEnabled + mCategory.setVisible(mSubscriptionManager.isActiveSubId(mSubId)); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE_UNSEARCHABLE; + } + + @Override + public void onAirplaneModeChanged(boolean airplaneModeEnabled) { + } + + @Override + public void onSubscriptionsChanged() { + update(); + } +} diff --git a/src/com/android/settings/network/telephony/EnabledNetworkModePreferenceController.java b/src/com/android/settings/network/telephony/EnabledNetworkModePreferenceController.java new file mode 100644 index 0000000000..94b176107c --- /dev/null +++ b/src/com/android/settings/network/telephony/EnabledNetworkModePreferenceController.java @@ -0,0 +1,335 @@ +/* + * Copyright (C) 2018 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.network.telephony; + +import android.content.Context; +import android.os.PersistableBundle; +import android.provider.Settings; +import android.telephony.CarrierConfigManager; +import android.telephony.ServiceState; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.ListPreference; +import androidx.preference.Preference; + +import com.android.internal.telephony.Phone; +import com.android.internal.telephony.PhoneConstants; +import com.android.settings.R; + +/** + * Preference controller for "Enabled network mode" + */ +public class EnabledNetworkModePreferenceController extends + TelephonyBasePreferenceController implements + ListPreference.OnPreferenceChangeListener { + + private CarrierConfigManager mCarrierConfigManager; + private TelephonyManager mTelephonyManager; + private boolean mIsGlobalCdma; + @VisibleForTesting + boolean mShow4GForLTE; + + public EnabledNetworkModePreferenceController(Context context, String key) { + super(context, key); + mCarrierConfigManager = context.getSystemService(CarrierConfigManager.class); + } + + @Override + public int getAvailabilityStatus(int subId) { + boolean visible; + final PersistableBundle carrierConfig = mCarrierConfigManager.getConfigForSubId(subId); + final TelephonyManager telephonyManager = TelephonyManager + .from(mContext).createForSubscriptionId(subId); + if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + visible = false; + } else if (carrierConfig == null) { + visible = false; + } else if (carrierConfig.getBoolean( + CarrierConfigManager.KEY_HIDE_CARRIER_NETWORK_SETTINGS_BOOL)) { + visible = false; + } else if (carrierConfig.getBoolean( + CarrierConfigManager.KEY_HIDE_PREFERRED_NETWORK_TYPE_BOOL) + && !telephonyManager.getServiceState().getRoaming() + && telephonyManager.getServiceState().getDataRegState() + == ServiceState.STATE_IN_SERVICE) { + visible = false; + } else if (carrierConfig.getBoolean(CarrierConfigManager.KEY_WORLD_PHONE_BOOL)) { + visible = false; + } else { + visible = true; + } + + return visible ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + final ListPreference listPreference = (ListPreference) preference; + final int networkMode = getPreferredNetworkMode(); + updatePreferenceEntries(listPreference); + updatePreferenceValueAndSummary(listPreference, networkMode); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object object) { + final int settingsMode = Integer.parseInt((String) object); + + if (mTelephonyManager.setPreferredNetworkType(mSubId, settingsMode)) { + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.PREFERRED_NETWORK_MODE + mSubId, + settingsMode); + updatePreferenceValueAndSummary((ListPreference) preference, settingsMode); + return true; + } + + return false; + } + + public void init(int subId) { + mSubId = subId; + final PersistableBundle carrierConfig = mCarrierConfigManager.getConfigForSubId(mSubId); + mTelephonyManager = TelephonyManager.from(mContext).createForSubscriptionId(mSubId); + + final boolean isLteOnCdma = + mTelephonyManager.getLteOnCdmaMode() == PhoneConstants.LTE_ON_CDMA_TRUE; + mIsGlobalCdma = isLteOnCdma + && carrierConfig.getBoolean(CarrierConfigManager.KEY_SHOW_CDMA_CHOICES_BOOL); + mShow4GForLTE = carrierConfig != null + ? carrierConfig.getBoolean( + CarrierConfigManager.KEY_SHOW_4G_FOR_LTE_DATA_ICON_BOOL) + : false; + } + + private int getPreferredNetworkMode() { + return Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.PREFERRED_NETWORK_MODE + mSubId, + Phone.PREFERRED_NT_MODE); + } + + private void updatePreferenceEntries(ListPreference preference) { + final int phoneType = mTelephonyManager.getPhoneType(); + final PersistableBundle carrierConfig = mCarrierConfigManager.getConfigForSubId(mSubId); + if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) { + final int lteForced = android.provider.Settings.Global.getInt( + mContext.getContentResolver(), + android.provider.Settings.Global.LTE_SERVICE_FORCED + mSubId, + 0); + final boolean isLteOnCdma = mTelephonyManager.getLteOnCdmaMode() + == PhoneConstants.LTE_ON_CDMA_TRUE; + final int settingsNetworkMode = android.provider.Settings.Global.getInt( + mContext.getContentResolver(), + android.provider.Settings.Global.PREFERRED_NETWORK_MODE + mSubId, + Phone.PREFERRED_NT_MODE); + if (isLteOnCdma) { + if (lteForced == 0) { + preference.setEntries( + R.array.enabled_networks_cdma_choices); + preference.setEntryValues( + R.array.enabled_networks_cdma_values); + } else { + switch (settingsNetworkMode) { + case TelephonyManager.NETWORK_MODE_CDMA_EVDO: + case TelephonyManager.NETWORK_MODE_CDMA_NO_EVDO: + case TelephonyManager.NETWORK_MODE_EVDO_NO_CDMA: + preference.setEntries( + R.array.enabled_networks_cdma_no_lte_choices); + preference.setEntryValues( + R.array.enabled_networks_cdma_no_lte_values); + break; + case TelephonyManager.NETWORK_MODE_GLOBAL: + case TelephonyManager.NETWORK_MODE_LTE_CDMA_EVDO: + case TelephonyManager.NETWORK_MODE_LTE_CDMA_EVDO_GSM_WCDMA: + case TelephonyManager.NETWORK_MODE_LTE_ONLY: + preference.setEntries( + R.array.enabled_networks_cdma_only_lte_choices); + preference.setEntryValues( + R.array.enabled_networks_cdma_only_lte_values); + break; + default: + preference.setEntries( + R.array.enabled_networks_cdma_choices); + preference.setEntryValues( + R.array.enabled_networks_cdma_values); + break; + } + } + } + } else if (phoneType == PhoneConstants.PHONE_TYPE_GSM) { + if (MobileNetworkUtils.isTdscdmaSupported(mContext, mSubId)) { + preference.setEntries( + R.array.enabled_networks_tdscdma_choices); + preference.setEntryValues( + R.array.enabled_networks_tdscdma_values); + } else if (carrierConfig != null + && !carrierConfig.getBoolean(CarrierConfigManager.KEY_PREFER_2G_BOOL) + && !carrierConfig.getBoolean(CarrierConfigManager.KEY_LTE_ENABLED_BOOL)) { + preference.setEntries(R.array.enabled_networks_except_gsm_lte_choices); + preference.setEntryValues(R.array.enabled_networks_except_gsm_lte_values); + } else if (carrierConfig != null + && !carrierConfig.getBoolean(CarrierConfigManager.KEY_PREFER_2G_BOOL)) { + int select = mShow4GForLTE + ? R.array.enabled_networks_except_gsm_4g_choices + : R.array.enabled_networks_except_gsm_choices; + preference.setEntries(select); + preference.setEntryValues( + R.array.enabled_networks_except_gsm_values); + } else if (carrierConfig != null + && !carrierConfig.getBoolean(CarrierConfigManager.KEY_LTE_ENABLED_BOOL)) { + preference.setEntries( + R.array.enabled_networks_except_lte_choices); + preference.setEntryValues( + R.array.enabled_networks_except_lte_values); + } else if (mIsGlobalCdma) { + preference.setEntries(R.array.enabled_networks_cdma_choices); + preference.setEntryValues(R.array.enabled_networks_cdma_values); + } else { + int select = mShow4GForLTE ? R.array.enabled_networks_4g_choices + : R.array.enabled_networks_choices; + preference.setEntries(select); + preference.setEntryValues(R.array.enabled_networks_values); + } + } + //TODO(b/117881708): figure out what world mode is, then we can optimize code. Otherwise + // I prefer to keep this old code + if (MobileNetworkUtils.isWorldMode(mContext, mSubId)) { + preference.setEntries( + R.array.preferred_network_mode_choices_world_mode); + preference.setEntryValues( + R.array.preferred_network_mode_values_world_mode); + } + } + + private void updatePreferenceValueAndSummary(ListPreference preference, int networkMode) { + preference.setValue(Integer.toString(networkMode)); + switch (networkMode) { + case TelephonyManager.NETWORK_MODE_TDSCDMA_WCDMA: + case TelephonyManager.NETWORK_MODE_TDSCDMA_GSM_WCDMA: + case TelephonyManager.NETWORK_MODE_TDSCDMA_GSM: + preference.setValue( + Integer.toString(TelephonyManager.NETWORK_MODE_TDSCDMA_GSM_WCDMA)); + preference.setSummary(R.string.network_3G); + break; + case TelephonyManager.NETWORK_MODE_WCDMA_ONLY: + case TelephonyManager.NETWORK_MODE_GSM_UMTS: + case TelephonyManager.NETWORK_MODE_WCDMA_PREF: + if (!mIsGlobalCdma) { + preference.setValue(Integer.toString(TelephonyManager.NETWORK_MODE_WCDMA_PREF)); + preference.setSummary(R.string.network_3G); + } else { + preference.setValue(Integer.toString(TelephonyManager + .NETWORK_MODE_LTE_CDMA_EVDO_GSM_WCDMA)); + preference.setSummary(R.string.network_global); + } + break; + case TelephonyManager.NETWORK_MODE_GSM_ONLY: + if (!mIsGlobalCdma) { + preference.setValue( + Integer.toString(TelephonyManager.NETWORK_MODE_GSM_ONLY)); + preference.setSummary(R.string.network_2G); + } else { + preference.setValue( + Integer.toString(TelephonyManager + .NETWORK_MODE_LTE_CDMA_EVDO_GSM_WCDMA)); + preference.setSummary(R.string.network_global); + } + break; + case TelephonyManager.NETWORK_MODE_LTE_GSM_WCDMA: + if (MobileNetworkUtils.isWorldMode(mContext, mSubId)) { + preference.setSummary( + R.string.preferred_network_mode_lte_gsm_umts_summary); + break; + } + case TelephonyManager.NETWORK_MODE_LTE_ONLY: + case TelephonyManager.NETWORK_MODE_LTE_WCDMA: + if (!mIsGlobalCdma) { + preference.setValue( + Integer.toString(TelephonyManager.NETWORK_MODE_LTE_GSM_WCDMA)); + preference.setSummary( + mShow4GForLTE ? R.string.network_4G : R.string.network_lte); + } else { + preference.setValue( + Integer.toString(TelephonyManager + .NETWORK_MODE_LTE_CDMA_EVDO_GSM_WCDMA)); + preference.setSummary(R.string.network_global); + } + break; + case TelephonyManager.NETWORK_MODE_LTE_CDMA_EVDO: + if (MobileNetworkUtils.isWorldMode(mContext, mSubId)) { + preference.setSummary( + R.string.preferred_network_mode_lte_cdma_summary); + } else { + preference.setValue( + Integer.toString(TelephonyManager.NETWORK_MODE_LTE_CDMA_EVDO)); + preference.setSummary(R.string.network_lte); + } + break; + case TelephonyManager.NETWORK_MODE_TDSCDMA_CDMA_EVDO_GSM_WCDMA: + preference.setValue(Integer.toString(TelephonyManager + .NETWORK_MODE_TDSCDMA_CDMA_EVDO_GSM_WCDMA)); + preference.setSummary(R.string.network_3G); + break; + case TelephonyManager.NETWORK_MODE_CDMA_EVDO: + case TelephonyManager.NETWORK_MODE_EVDO_NO_CDMA: + case TelephonyManager.NETWORK_MODE_GLOBAL: + preference.setValue( + Integer.toString(TelephonyManager.NETWORK_MODE_CDMA_EVDO)); + preference.setSummary(R.string.network_3G); + break; + case TelephonyManager.NETWORK_MODE_CDMA_NO_EVDO: + preference.setValue( + Integer.toString(TelephonyManager.NETWORK_MODE_CDMA_NO_EVDO)); + preference.setSummary(R.string.network_1x); + break; + case TelephonyManager.NETWORK_MODE_TDSCDMA_ONLY: + preference.setValue( + Integer.toString(TelephonyManager.NETWORK_MODE_TDSCDMA_ONLY)); + preference.setSummary(R.string.network_3G); + break; + case TelephonyManager.NETWORK_MODE_LTE_TDSCDMA_GSM: + case TelephonyManager.NETWORK_MODE_LTE_TDSCDMA_GSM_WCDMA: + case TelephonyManager.NETWORK_MODE_LTE_TDSCDMA: + case TelephonyManager.NETWORK_MODE_LTE_TDSCDMA_WCDMA: + case TelephonyManager.NETWORK_MODE_LTE_TDSCDMA_CDMA_EVDO_GSM_WCDMA: + case TelephonyManager.NETWORK_MODE_LTE_CDMA_EVDO_GSM_WCDMA: + if (MobileNetworkUtils.isTdscdmaSupported(mContext, mSubId)) { + preference.setValue( + Integer.toString(TelephonyManager + .NETWORK_MODE_LTE_TDSCDMA_CDMA_EVDO_GSM_WCDMA)); + preference.setSummary(R.string.network_lte); + } else { + preference.setValue( + Integer.toString(TelephonyManager + .NETWORK_MODE_LTE_CDMA_EVDO_GSM_WCDMA)); + if (mTelephonyManager.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA + || mIsGlobalCdma + || MobileNetworkUtils.isWorldMode(mContext, mSubId)) { + preference.setSummary(R.string.network_global); + } else { + preference.setSummary(mShow4GForLTE + ? R.string.network_4G : R.string.network_lte); + } + } + break; + default: + preference.setSummary( + mContext.getString(R.string.mobile_network_mode_error, networkMode)); + } + } +} diff --git a/src/com/android/settings/network/telephony/Enhanced4gLtePreferenceController.java b/src/com/android/settings/network/telephony/Enhanced4gLtePreferenceController.java new file mode 100644 index 0000000000..ca45a328ba --- /dev/null +++ b/src/com/android/settings/network/telephony/Enhanced4gLtePreferenceController.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2018 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.network.telephony; + +import android.content.Context; +import android.os.Looper; +import android.os.PersistableBundle; +import android.telephony.CarrierConfigManager; +import android.telephony.PhoneStateListener; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreference; + +import com.android.ims.ImsManager; +import com.android.settings.R; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.core.lifecycle.events.OnStop; + +import java.util.ArrayList; +import java.util.List; + +/** + * Preference controller for "Enhanced 4G LTE" + */ +public class Enhanced4gLtePreferenceController extends TelephonyTogglePreferenceController + implements LifecycleObserver, OnStart, OnStop { + + private Preference mPreference; + private TelephonyManager mTelephonyManager; + private CarrierConfigManager mCarrierConfigManager; + private PersistableBundle mCarrierConfig; + @VisibleForTesting + ImsManager mImsManager; + private PhoneCallStateListener mPhoneStateListener; + private final List<On4gLteUpdateListener> m4gLteListeners; + private final CharSequence[] mVariantTitles; + private final CharSequence[] mVariantSumaries; + + private final int VARIANT_TITLE_VOLTE = 0; + private final int VARIANT_TITLE_ADVANCED_CALL = 1; + private final int VARIANT_TITLE_4G_CALLING = 2; + + public Enhanced4gLtePreferenceController(Context context, String key) { + super(context, key); + mCarrierConfigManager = context.getSystemService(CarrierConfigManager.class); + m4gLteListeners = new ArrayList<>(); + mPhoneStateListener = new PhoneCallStateListener(Looper.getMainLooper()); + mVariantTitles = context.getResources() + .getTextArray(R.array.enhanced_4g_lte_mode_title_variant); + mVariantSumaries = context.getResources() + .getTextArray(R.array.enhanced_4g_lte_mode_sumary_variant); + } + + @Override + public int getAvailabilityStatus(int subId) { + final PersistableBundle carrierConfig = mCarrierConfigManager.getConfigForSubId(subId); + final boolean isVisible = subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID + && mImsManager != null && carrierConfig != null + && mImsManager.isVolteEnabledByPlatform() + && mImsManager.isVolteProvisionedOnDevice() + && MobileNetworkUtils.isImsServiceStateReady(mImsManager) + && !carrierConfig.getBoolean(CarrierConfigManager.KEY_HIDE_ENHANCED_4G_LTE_BOOL); + return isVisible + ? (is4gLtePrefEnabled() ? AVAILABLE : AVAILABLE_UNSEARCHABLE) + : CONDITIONALLY_UNAVAILABLE; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + } + + @Override + public void onStart() { + mPhoneStateListener.register(mSubId); + } + + @Override + public void onStop() { + mPhoneStateListener.unregister(); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + final SwitchPreference switchPreference = (SwitchPreference) preference; + final boolean show4GForLTE = mCarrierConfig.getBoolean( + CarrierConfigManager.KEY_SHOW_4G_FOR_LTE_DATA_ICON_BOOL); + int variant4glteTitleIndex = mCarrierConfig.getInt( + CarrierConfigManager.KEY_ENHANCED_4G_LTE_TITLE_VARIANT_INT); + + if (variant4glteTitleIndex != VARIANT_TITLE_ADVANCED_CALL) { + variant4glteTitleIndex = show4GForLTE ? VARIANT_TITLE_4G_CALLING : VARIANT_TITLE_VOLTE; + } + + switchPreference.setTitle(mVariantTitles[variant4glteTitleIndex]); + switchPreference.setSummary(mVariantSumaries[variant4glteTitleIndex]); + switchPreference.setEnabled(is4gLtePrefEnabled()); + switchPreference.setChecked(mImsManager.isEnhanced4gLteModeSettingEnabledByUser() + && mImsManager.isNonTtyOrTtyOnVolteEnabled()); + } + + @Override + public boolean setChecked(boolean isChecked) { + mImsManager.setEnhanced4gLteModeSetting(isChecked); + for (final On4gLteUpdateListener lsn : m4gLteListeners) { + lsn.on4gLteUpdated(); + } + return true; + } + + @Override + public boolean isChecked() { + return mImsManager.isEnhanced4gLteModeSettingEnabledByUser(); + } + + public Enhanced4gLtePreferenceController init(int subId) { + mSubId = subId; + mTelephonyManager = TelephonyManager.from(mContext).createForSubscriptionId(mSubId); + mCarrierConfig = mCarrierConfigManager.getConfigForSubId(mSubId); + if (mSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + mImsManager = ImsManager.getInstance(mContext, SubscriptionManager.getPhoneId(mSubId)); + } + + return this; + } + + public Enhanced4gLtePreferenceController addListener(On4gLteUpdateListener lsn) { + m4gLteListeners.add(lsn); + return this; + } + + private boolean is4gLtePrefEnabled() { + return mSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID + && mTelephonyManager.getCallState(mSubId) == TelephonyManager.CALL_STATE_IDLE + && mImsManager != null + && mImsManager.isNonTtyOrTtyOnVolteEnabled() + && mCarrierConfig.getBoolean( + CarrierConfigManager.KEY_EDITABLE_ENHANCED_4G_LTE_BOOL); + } + + private class PhoneCallStateListener extends PhoneStateListener { + + public PhoneCallStateListener(Looper looper) { + super(looper); + } + + @Override + public void onCallStateChanged(int state, String incomingNumber) { + updateState(mPreference); + } + + public void register(int subId) { + mSubId = subId; + mTelephonyManager.listen(this, PhoneStateListener.LISTEN_CALL_STATE); + } + + public void unregister() { + mTelephonyManager.listen(this, PhoneStateListener.LISTEN_NONE); + } + } + + /** + * Update other preferences when 4gLte state is changed + */ + public interface On4gLteUpdateListener { + void on4gLteUpdated(); + } +} diff --git a/src/com/android/settings/network/telephony/Enhanced4gLteSliceHelper.java b/src/com/android/settings/network/telephony/Enhanced4gLteSliceHelper.java new file mode 100644 index 0000000000..be5d94866e --- /dev/null +++ b/src/com/android/settings/network/telephony/Enhanced4gLteSliceHelper.java @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2018 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.network.telephony; + +import static android.app.slice.Slice.EXTRA_TOGGLE_STATE; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.net.Uri; +import android.os.PersistableBundle; +import android.telephony.CarrierConfigManager; +import android.telephony.SubscriptionManager; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; +import androidx.core.graphics.drawable.IconCompat; +import androidx.slice.Slice; +import androidx.slice.builders.ListBuilder; +import androidx.slice.builders.ListBuilder.RowBuilder; +import androidx.slice.builders.SliceAction; + +import com.android.ims.ImsManager; +import com.android.settings.R; +import com.android.settings.Utils; +import com.android.settings.slices.CustomSliceRegistry; +import com.android.settings.slices.SliceBroadcastReceiver; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.FutureTask; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * Helper class to control slices for enhanced 4g LTE settings. + */ +public class Enhanced4gLteSliceHelper { + + private static final String TAG = "Enhanced4gLteSlice"; + + /** + * Action passed for changes to enhanced 4g LTE slice (toggle). + */ + public static final String ACTION_ENHANCED_4G_LTE_CHANGED = + "com.android.settings.mobilenetwork.action.ENHANCED_4G_LTE_CHANGED"; + + /** + * Action for mobile network settings activity which + * allows setting configuration for Enhanced 4G LTE + * related settings + */ + public static final String ACTION_MOBILE_NETWORK_SETTINGS_ACTIVITY = + "android.settings.NETWORK_OPERATOR_SETTINGS"; + + /** + * Timeout for querying enhanced 4g lte setting from ims manager. + */ + private static final int TIMEOUT_MILLIS = 2000; + + private final Context mContext; + + /** + * Phone package name + */ + private static final String PACKAGE_PHONE = "com.android.phone"; + + /** + * String resource type + */ + private static final String RESOURCE_TYPE_STRING = "string"; + + /** + * Enhanced 4g lte mode title variant resource name + */ + private static final String RESOURCE_ENHANCED_4G_LTE_MODE_TITLE_VARIANT = + "enhanced_4g_lte_mode_title_variant"; + + @VisibleForTesting + public Enhanced4gLteSliceHelper(Context context) { + mContext = context; + } + + /** + * Returns Slice object for enhanced_4g_lte settings. + * + * If enhanced 4g LTE is not supported for the current carrier, this method will return slice + * with not supported message. + * + * If enhanced 4g LTE is not editable for the current carrier, this method will return slice + * with not editable message. + * + * If enhanced 4g LTE setting can be changed, this method will return the slice to toggle + * enhanced 4g LTE option with ACTION_ENHANCED_4G_LTE_CHANGED as endItem. + */ + public Slice createEnhanced4gLteSlice(Uri sliceUri) { + final int subId = getDefaultVoiceSubId(); + + if (subId <= SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + Log.d(TAG, "Invalid subscription Id"); + return null; + } + + final ImsManager imsManager = getImsManager(subId); + + if (!imsManager.isVolteEnabledByPlatform() + || !imsManager.isVolteProvisionedOnDevice()) { + Log.d(TAG, "Setting is either not provisioned or not enabled by Platform"); + return null; + } + + if (isCarrierConfigManagerKeyEnabled( + CarrierConfigManager.KEY_HIDE_ENHANCED_4G_LTE_BOOL, subId, false) + || !isCarrierConfigManagerKeyEnabled( + CarrierConfigManager.KEY_EDITABLE_ENHANCED_4G_LTE_BOOL, subId, + true)) { + Log.d(TAG, "Setting is either hidden or not editable"); + return null; + } + + try { + return getEnhanced4gLteSlice(sliceUri, + isEnhanced4gLteModeEnabled(imsManager), subId); + } catch (InterruptedException | TimeoutException | ExecutionException e) { + Log.e(TAG, "Unable to read the current Enhanced 4g LTE status", e); + return null; + } + } + + private boolean isEnhanced4gLteModeEnabled(ImsManager imsManager) + throws InterruptedException, ExecutionException, TimeoutException { + final FutureTask<Boolean> isEnhanced4gLteOnTask = new FutureTask<>(new Callable<Boolean>() { + @Override + public Boolean call() { + return imsManager.isEnhanced4gLteModeSettingEnabledByUser(); + } + }); + final ExecutorService executor = Executors.newSingleThreadExecutor(); + executor.execute(isEnhanced4gLteOnTask); + + return isEnhanced4gLteOnTask.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); + } + + /** + * Builds a toggle slice where the intent takes you to the Enhanced 4G LTE page and the toggle + * enables/disables Enhanced 4G LTE mode setting. + */ + private Slice getEnhanced4gLteSlice(Uri sliceUri, boolean isEnhanced4gLteEnabled, int subId) { + final IconCompat icon = IconCompat.createWithResource(mContext, + R.drawable.ic_launcher_settings); + + return new ListBuilder(mContext, sliceUri, ListBuilder.INFINITY) + .setAccentColor(Utils.getColorAccentDefaultColor(mContext)) + .addRow(new RowBuilder() + .setTitle(getEnhanced4glteModeTitle(subId)) + .addEndItem( + SliceAction.createToggle( + getBroadcastIntent(ACTION_ENHANCED_4G_LTE_CHANGED), + null /* actionTitle */, isEnhanced4gLteEnabled)) + .setPrimaryAction( + SliceAction.createDeeplink( + getActivityIntent(ACTION_MOBILE_NETWORK_SETTINGS_ACTIVITY), + icon, + ListBuilder.ICON_IMAGE, + getEnhanced4glteModeTitle(subId)))) + .build(); + } + + protected ImsManager getImsManager(int subId) { + return ImsManager.getInstance(mContext, SubscriptionManager.getPhoneId(subId)); + } + + /** + * Handles Enhanced 4G LTE mode setting change from Enhanced 4G LTE slice and posts + * notification. Should be called when intent action is ACTION_ENHANCED_4G_LTE_CHANGED + * + * @param intent action performed + */ + public void handleEnhanced4gLteChanged(Intent intent) { + final int subId = getDefaultVoiceSubId(); + + if (subId > SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + final ImsManager imsManager = getImsManager(subId); + if (imsManager.isVolteEnabledByPlatform() + && imsManager.isVolteProvisionedOnDevice()) { + final boolean currentValue = imsManager.isEnhanced4gLteModeSettingEnabledByUser() + && imsManager.isNonTtyOrTtyOnVolteEnabled(); + final boolean newValue = intent.getBooleanExtra(EXTRA_TOGGLE_STATE, + currentValue); + if (newValue != currentValue) { + imsManager.setEnhanced4gLteModeSetting(newValue); + } + } + } + // notify change in slice in any case to get re-queried. This would result in displaying + // appropriate message with the updated setting. + mContext.getContentResolver().notifyChange(CustomSliceRegistry.ENHANCED_4G_SLICE_URI, null); + } + + private CharSequence getEnhanced4glteModeTitle(int subId) { + CharSequence ret = mContext.getText(R.string.enhanced_4g_lte_mode_title); + try { + if (isCarrierConfigManagerKeyEnabled( + CarrierConfigManager.KEY_ENHANCED_4G_LTE_TITLE_VARIANT_BOOL, + subId, + false)) { + final PackageManager manager = mContext.getPackageManager(); + final Resources resources = manager.getResourcesForApplication( + PACKAGE_PHONE); + final int resId = resources.getIdentifier( + RESOURCE_ENHANCED_4G_LTE_MODE_TITLE_VARIANT, + RESOURCE_TYPE_STRING, PACKAGE_PHONE); + ret = resources.getText(resId); + } + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "package name not found"); + } + return ret; + } + + /** + * Returns {@code true} when the key is enabled for the carrier, and {@code false} otherwise. + */ + private boolean isCarrierConfigManagerKeyEnabled(String key, + int subId, boolean defaultValue) { + final CarrierConfigManager configManager = getCarrierConfigManager(); + boolean ret = defaultValue; + if (configManager != null) { + final PersistableBundle bundle = configManager.getConfigForSubId(subId); + if (bundle != null) { + ret = bundle.getBoolean(key, defaultValue); + } + } + return ret; + } + + protected CarrierConfigManager getCarrierConfigManager() { + return mContext.getSystemService(CarrierConfigManager.class); + } + + private PendingIntent getBroadcastIntent(String action) { + final Intent intent = new Intent(action); + intent.setClass(mContext, SliceBroadcastReceiver.class); + return PendingIntent.getBroadcast(mContext, 0 /* requestCode */, intent, + PendingIntent.FLAG_CANCEL_CURRENT); + } + + /** + * Returns the current default voice subId obtained from SubscriptionManager + */ + protected int getDefaultVoiceSubId() { + return SubscriptionManager.getDefaultVoiceSubscriptionId(); + } + + /** + * Returns PendingIntent to start activity specified by action + */ + private PendingIntent getActivityIntent(String action) { + final Intent intent = new Intent(action); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + return PendingIntent.getActivity(mContext, 0 /* requestCode */, intent, 0 /* flags */); + } +} + diff --git a/src/com/android/settings/network/telephony/EuiccPreferenceController.java b/src/com/android/settings/network/telephony/EuiccPreferenceController.java new file mode 100644 index 0000000000..ecd20edfc5 --- /dev/null +++ b/src/com/android/settings/network/telephony/EuiccPreferenceController.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2018 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.network.telephony; + +import android.content.Context; +import android.content.Intent; +import android.telephony.TelephonyManager; +import android.telephony.euicc.EuiccManager; +import android.text.TextUtils; + +import androidx.preference.Preference; + +/** + * Preference controller for "Euicc preference" + */ +public class EuiccPreferenceController extends TelephonyBasePreferenceController { + + private TelephonyManager mTelephonyManager; + + public EuiccPreferenceController(Context context, String key) { + super(context, key); + mTelephonyManager = context.getSystemService(TelephonyManager.class); + } + + @Override + public int getAvailabilityStatus(int subId) { + return MobileNetworkUtils.showEuiccSettings(mContext) + ? AVAILABLE + : CONDITIONALLY_UNAVAILABLE; + } + + @Override + public CharSequence getSummary() { + return mTelephonyManager.getSimOperatorName(); + } + + public void init(int subId) { + mSubId = subId; + mTelephonyManager = TelephonyManager.from(mContext).createForSubscriptionId(mSubId); + } + + @Override + public boolean handlePreferenceTreeClick(Preference preference) { + if (TextUtils.equals(preference.getKey(), getPreferenceKey())) { + Intent intent = new Intent(EuiccManager.ACTION_MANAGE_EMBEDDED_SUBSCRIPTIONS); + mContext.startActivity(intent); + return true; + } + + return false; + } +} diff --git a/src/com/android/settings/network/telephony/MmsMessagePreferenceController.java b/src/com/android/settings/network/telephony/MmsMessagePreferenceController.java new file mode 100644 index 0000000000..b8d2081f2a --- /dev/null +++ b/src/com/android/settings/network/telephony/MmsMessagePreferenceController.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2019 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.network.telephony; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.telephony.data.ApnSetting; +import android.util.Log; + +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreference; + +import com.android.settings.network.MobileDataContentObserver; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.core.lifecycle.events.OnStop; + +/** + * Preference controller for "Mobile data" + */ +public class MmsMessagePreferenceController extends TelephonyTogglePreferenceController implements + LifecycleObserver, OnStart, OnStop { + private TelephonyManager mTelephonyManager; + private SubscriptionManager mSubscriptionManager; + private MobileDataContentObserver mMobileDataContentObserver; + private SwitchPreference mPreference; + + public MmsMessagePreferenceController(Context context, String key) { + super(context, key); + mSubscriptionManager = context.getSystemService(SubscriptionManager.class); + mMobileDataContentObserver = new MobileDataContentObserver( + new Handler(Looper.getMainLooper())); + mMobileDataContentObserver.setOnMobileDataChangedListener(()->updateState(mPreference)); + } + + @Override + public int getAvailabilityStatus(int subId) { + final TelephonyManager telephonyManager = TelephonyManager + .from(mContext).createForSubscriptionId(subId); + return (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID + && !telephonyManager.isDataEnabled() + && telephonyManager.isApnMetered(ApnSetting.TYPE_MMS)) + ? AVAILABLE + : CONDITIONALLY_UNAVAILABLE; + } + + @Override + public void onStart() { + if (mSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + mMobileDataContentObserver.register(mContext, mSubId); + } + } + + @Override + public void onStop() { + if (mSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + mMobileDataContentObserver.unRegister(mContext); + } + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + preference.setVisible(isAvailable()); + ((SwitchPreference) preference).setChecked(isChecked()); + } + + public void init(int subId) { + mSubId = subId; + mTelephonyManager = TelephonyManager.from(mContext).createForSubscriptionId(mSubId); + } + + @Override + public boolean setChecked(boolean isChecked) { + return mSubscriptionManager.setAlwaysAllowMmsData(mSubId, isChecked); + } + + @Override + public boolean isChecked() { + return mTelephonyManager.isDataEnabledForApn(ApnSetting.TYPE_MMS); + } +} diff --git a/src/com/android/settings/network/telephony/MobileDataDialogFragment.java b/src/com/android/settings/network/telephony/MobileDataDialogFragment.java new file mode 100644 index 0000000000..a6183e96a5 --- /dev/null +++ b/src/com/android/settings/network/telephony/MobileDataDialogFragment.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2018 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.network.telephony; + +import android.app.Dialog; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; + +import androidx.appcompat.app.AlertDialog; + +import com.android.settings.R; +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; + + +/** + * Dialog Fragment to show dialog for "mobile data" + * + * 1. When user want to disable data in single sim case, show dialog to confirm + * 2. When user want to enable data in multiple sim case, show dialog to confirm to disable other + * sim + */ +public class MobileDataDialogFragment extends InstrumentedDialogFragment implements + DialogInterface.OnClickListener { + + public static final int TYPE_DISABLE_DIALOG = 0; + public static final int TYPE_MULTI_SIM_DIALOG = 1; + + private static final String ARG_DIALOG_TYPE = "dialog_type"; + private static final String ARG_SUB_ID = "subId"; + + private SubscriptionManager mSubscriptionManager; + private int mType; + private int mSubId; + + public static MobileDataDialogFragment newInstance(int type, int subId) { + final MobileDataDialogFragment dialogFragment = new MobileDataDialogFragment(); + + Bundle args = new Bundle(); + args.putInt(ARG_DIALOG_TYPE, type); + args.putInt(ARG_SUB_ID, subId); + dialogFragment.setArguments(args); + + return dialogFragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mSubscriptionManager = getContext().getSystemService(SubscriptionManager.class); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Bundle bundle = getArguments(); + final Context context = getContext(); + + mType = bundle.getInt(ARG_DIALOG_TYPE); + mSubId = bundle.getInt(ARG_SUB_ID); + + switch (mType) { + case TYPE_DISABLE_DIALOG: + return new AlertDialog.Builder(context) + .setMessage(R.string.data_usage_disable_mobile) + .setPositiveButton(android.R.string.ok, this) + .setNegativeButton(android.R.string.cancel, null) + .create(); + case TYPE_MULTI_SIM_DIALOG: + final SubscriptionInfo currentSubInfo = + mSubscriptionManager.getActiveSubscriptionInfo(mSubId); + final SubscriptionInfo nextSubInfo = + mSubscriptionManager.getDefaultDataSubscriptionInfo(); + + final String previousName = (nextSubInfo == null) + ? getContext().getResources().getString( + R.string.sim_selection_required_pref) + : nextSubInfo.getDisplayName().toString(); + + final String newName = (currentSubInfo == null) + ? getContext().getResources().getString( + R.string.sim_selection_required_pref) + : currentSubInfo.getDisplayName().toString(); + + return new AlertDialog.Builder(context) + .setTitle(context.getString(R.string.sim_change_data_title, newName)) + .setMessage(context.getString(R.string.sim_change_data_message, + newName, previousName)) + .setPositiveButton( + context.getString(R.string.sim_change_data_ok, newName), + this) + .setNegativeButton(R.string.cancel, null) + .create(); + default: + throw new IllegalArgumentException("unknown type " + mType); + } + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.MOBILE_DATA_DIALOG; + } + + @Override + public void onClick(DialogInterface dialog, int which) { + switch (mType) { + case TYPE_DISABLE_DIALOG: + MobileNetworkUtils.setMobileDataEnabled(getContext(), mSubId, false /* enabled */, + false /* disableOtherSubscriptions */); + break; + case TYPE_MULTI_SIM_DIALOG: + mSubscriptionManager.setDefaultDataSubId(mSubId); + MobileNetworkUtils.setMobileDataEnabled(getContext(), mSubId, true /* enabled */, + true /* disableOtherSubscriptions */); + break; + default: + throw new IllegalArgumentException("unknown type " + mType); + } + } + +} diff --git a/src/com/android/settings/network/telephony/MobileDataPreferenceController.java b/src/com/android/settings/network/telephony/MobileDataPreferenceController.java new file mode 100644 index 0000000000..c06b78b025 --- /dev/null +++ b/src/com/android/settings/network/telephony/MobileDataPreferenceController.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2018 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.network.telephony; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.text.TextUtils; + +import androidx.annotation.VisibleForTesting; +import androidx.fragment.app.FragmentManager; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreference; + +import com.android.settings.R; +import com.android.settings.network.MobileDataContentObserver; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.core.lifecycle.events.OnStop; + +/** + * Preference controller for "Mobile data" + */ +public class MobileDataPreferenceController extends TelephonyTogglePreferenceController + implements LifecycleObserver, OnStart, OnStop { + + private static final String DIALOG_TAG = "MobileDataDialog"; + + private SwitchPreference mPreference; + private TelephonyManager mTelephonyManager; + private SubscriptionManager mSubscriptionManager; + private MobileDataContentObserver mDataContentObserver; + private FragmentManager mFragmentManager; + @VisibleForTesting + int mDialogType; + @VisibleForTesting + boolean mNeedDialog; + + public MobileDataPreferenceController(Context context, String key) { + super(context, key); + mSubscriptionManager = context.getSystemService(SubscriptionManager.class); + mDataContentObserver = new MobileDataContentObserver(new Handler(Looper.getMainLooper())); + mDataContentObserver.setOnMobileDataChangedListener(() -> updateState(mPreference)); + } + + @Override + public int getAvailabilityStatus(int subId) { + return subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID + ? AVAILABLE + : DISABLED_DEPENDENT_SETTING; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + } + + @Override + public void onStart() { + if (mSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + mDataContentObserver.register(mContext, mSubId); + } + } + + @Override + public void onStop() { + if (mSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + mDataContentObserver.unRegister(mContext); + } + } + + @Override + public boolean handlePreferenceTreeClick(Preference preference) { + if (TextUtils.equals(preference.getKey(), getPreferenceKey())) { + if (mNeedDialog) { + showDialog(mDialogType); + } + return true; + } + + return false; + } + + @Override + public boolean setChecked(boolean isChecked) { + mNeedDialog = isDialogNeeded(); + + if (!mNeedDialog) { + // Update data directly if we don't need dialog + MobileNetworkUtils.setMobileDataEnabled(mContext, mSubId, isChecked, false); + return true; + } + + return false; + } + + @Override + public boolean isChecked() { + return mTelephonyManager.isDataEnabled(); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + if (isOpportunistic()) { + preference.setEnabled(false); + preference.setSummary(R.string.mobile_data_settings_summary_auto_switch); + } else { + preference.setEnabled(true); + preference.setSummary(R.string.mobile_data_settings_summary); + } + } + + private boolean isOpportunistic() { + SubscriptionInfo info = mSubscriptionManager.getActiveSubscriptionInfo(mSubId); + return info != null && info.isOpportunistic(); + } + + public void init(FragmentManager fragmentManager, int subId) { + mFragmentManager = fragmentManager; + mSubId = subId; + mTelephonyManager = TelephonyManager.from(mContext).createForSubscriptionId(mSubId); + } + + @VisibleForTesting + boolean isDialogNeeded() { + final boolean enableData = !isChecked(); + final boolean isMultiSim = (mTelephonyManager.getSimCount() > 1); + final int defaultSubId = mSubscriptionManager.getDefaultDataSubscriptionId(); + final boolean needToDisableOthers = mSubscriptionManager + .isActiveSubscriptionId(defaultSubId) && defaultSubId != mSubId; + if (enableData && isMultiSim && needToDisableOthers) { + mDialogType = MobileDataDialogFragment.TYPE_MULTI_SIM_DIALOG; + return true; + } + return false; + } + + private void showDialog(int type) { + final MobileDataDialogFragment dialogFragment = MobileDataDialogFragment.newInstance(type, + mSubId); + dialogFragment.show(mFragmentManager, DIALOG_TAG); + } +} diff --git a/src/com/android/settings/network/telephony/MobileDataSlice.java b/src/com/android/settings/network/telephony/MobileDataSlice.java new file mode 100644 index 0000000000..65eaf8708b --- /dev/null +++ b/src/com/android/settings/network/telephony/MobileDataSlice.java @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2019 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.network.telephony; + +import static android.app.slice.Slice.EXTRA_TOGGLE_STATE; + +import android.annotation.ColorInt; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; + +import androidx.core.graphics.drawable.IconCompat; +import androidx.slice.Slice; +import androidx.slice.builders.ListBuilder; +import androidx.slice.builders.SliceAction; + +import com.android.settings.R; +import com.android.settings.Utils; +import com.android.settings.network.AirplaneModePreferenceController; +import com.android.settings.network.MobileDataContentObserver; +import com.android.settings.slices.CustomSliceRegistry; +import com.android.settings.slices.CustomSliceable; +import com.android.settings.slices.SliceBackgroundWorker; + +import com.google.common.annotations.VisibleForTesting; + +import java.io.IOException; +import java.util.List; + +/** + * Custom {@link Slice} for Mobile Data. + * <p> + * We make a custom slice instead of using {@link MobileDataPreferenceController} because the + * pref controller is generalized across any carrier, and thus does not control a specific + * subscription. We attempt to reuse any telephony-specific code from the preference controller. + * + * </p> + * + */ +public class MobileDataSlice implements CustomSliceable { + + private final Context mContext; + private final SubscriptionManager mSubscriptionManager; + private final TelephonyManager mTelephonyManager; + + public MobileDataSlice(Context context) { + mContext = context; + mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class); + mTelephonyManager = mContext.getSystemService(TelephonyManager.class); + } + + @Override + public Slice getSlice() { + final IconCompat icon = IconCompat.createWithResource(mContext, + R.drawable.ic_network_cell); + final String title = mContext.getText(R.string.mobile_data_settings_title).toString(); + @ColorInt final int color = Utils.getColorAccentDefaultColor(mContext); + + // Return null until we can show a disabled-action Slice, blaming Airplane mode. + if (isAirplaneModeEnabled()) { + return null; + } + + // Return null until we can show a disabled-action Slice. + if (!isMobileDataAvailable()) { + return null; + } + + final CharSequence summary = getSummary(); + final PendingIntent toggleAction = getBroadcastIntent(mContext); + final PendingIntent primaryAction = getPrimaryAction(); + final SliceAction primarySliceAction = SliceAction.createDeeplink(primaryAction, icon, + ListBuilder.ICON_IMAGE, title); + final SliceAction toggleSliceAction = SliceAction.createToggle(toggleAction, + null /* actionTitle */, isMobileDataEnabled()); + + final ListBuilder listBuilder = new ListBuilder(mContext, getUri(), + ListBuilder.INFINITY) + .setAccentColor(color) + .addRow(new ListBuilder.RowBuilder() + .setTitle(title) + .setSubtitle(summary) + .addEndItem(toggleSliceAction) + .setPrimaryAction(primarySliceAction)); + return listBuilder.build(); + } + + @Override + public Uri getUri() { + return CustomSliceRegistry.MOBILE_DATA_SLICE_URI; + } + + @Override + public void onNotifyChange(Intent intent) { + final boolean newState = intent.getBooleanExtra(EXTRA_TOGGLE_STATE, + isMobileDataEnabled()); + + final int defaultSubId = getDefaultSubscriptionId(mSubscriptionManager); + if (defaultSubId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + return; // No subscription - do nothing. + } + + MobileNetworkUtils.setMobileDataEnabled(mContext, defaultSubId, newState, + false /* disableOtherSubscriptions */); + // Do not notifyChange on Uri. The service takes longer to update the current value than it + // does for the Slice to check the current value again. Let {@link WifiScanWorker} + // handle it. + } + + @Override + public IntentFilter getIntentFilter() { + final IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED); + return filter; + } + + @Override + public Intent getIntent() { + return new Intent(mContext, MobileNetworkActivity.class); + } + + @Override + public Class<? extends SliceBackgroundWorker> getBackgroundWorkerClass() { + return MobileDataWorker.class; + } + + protected static int getDefaultSubscriptionId(SubscriptionManager subscriptionManager) { + final SubscriptionInfo defaultSubscription = + subscriptionManager.getDefaultDataSubscriptionInfo(); + if (defaultSubscription == null) { + return SubscriptionManager.INVALID_SUBSCRIPTION_ID; // No default subscription + } + + return defaultSubscription.getSubscriptionId(); + } + + private CharSequence getSummary() { + final SubscriptionInfo defaultSubscription = + mSubscriptionManager.getDefaultDataSubscriptionInfo(); + if (defaultSubscription == null) { + return null; // no summary text + } + + return defaultSubscription.getDisplayName(); + } + + private PendingIntent getPrimaryAction() { + final Intent intent = getIntent(); + return PendingIntent.getActivity(mContext, 0 /* requestCode */, + intent, 0 /* flags */); + } + + /** + * @return {@code true} when mobile data is not supported by the current device. + */ + private boolean isMobileDataAvailable() { + final List<SubscriptionInfo> subInfoList = + mSubscriptionManager.getSelectableSubscriptionInfoList(); + + return !(subInfoList == null || subInfoList.isEmpty()); + } + + @VisibleForTesting + boolean isAirplaneModeEnabled() { + // Generic key since we only want the method check - no UI. + AirplaneModePreferenceController controller = new AirplaneModePreferenceController(mContext, + "key" /* Key */); + return controller.isChecked(); + } + + @VisibleForTesting + boolean isMobileDataEnabled() { + if (mTelephonyManager == null) { + return false; + } + + return mTelephonyManager.isDataEnabled(); + } + + /** + * Listener for mobile data state changes. + * + * <p> + * Listen to individual subscription changes since there is no framework broadcast. + * + * This worker registers a ContentObserver in the background and updates the MobileData + * Slice when the value changes. + */ + public static class MobileDataWorker extends SliceBackgroundWorker<Void> { + + DataContentObserver mMobileDataObserver; + + public MobileDataWorker(Context context, Uri uri) { + super(context, uri); + final Handler handler = new Handler(Looper.getMainLooper()); + mMobileDataObserver = new DataContentObserver(handler, this); + } + + @Override + protected void onSlicePinned() { + final SubscriptionManager subscriptionManager = + getContext().getSystemService(SubscriptionManager.class); + mMobileDataObserver.register(getContext(), + getDefaultSubscriptionId(subscriptionManager)); + } + + @Override + protected void onSliceUnpinned() { + mMobileDataObserver.unRegister(getContext()); + } + + @Override + public void close() throws IOException { + mMobileDataObserver = null; + } + + public void updateSlice() { + notifySliceChange(); + } + + public class DataContentObserver extends ContentObserver { + + private final MobileDataWorker mSliceBackgroundWorker; + + public DataContentObserver(Handler handler, MobileDataWorker backgroundWorker) { + super(handler); + mSliceBackgroundWorker = backgroundWorker; + } + + @Override + public void onChange(boolean selfChange) { + mSliceBackgroundWorker.updateSlice(); + } + + public void register(Context context, int subId) { + final Uri uri = MobileDataContentObserver.getObservableUri(subId); + context.getContentResolver().registerContentObserver(uri, false, this); + } + + public void unRegister(Context context) { + context.getContentResolver().unregisterContentObserver(this); + } + } + } +} diff --git a/src/com/android/settings/network/telephony/MobileNetworkActivity.java b/src/com/android/settings/network/telephony/MobileNetworkActivity.java new file mode 100644 index 0000000000..b8ed31f94b --- /dev/null +++ b/src/com/android/settings/network/telephony/MobileNetworkActivity.java @@ -0,0 +1,276 @@ +/* + * Copyright (C) 2018 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.network.telephony; + +import android.app.ActionBar; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.provider.Settings; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.view.Menu; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; + +import com.android.internal.telephony.TelephonyIntents; +import com.android.internal.util.CollectionUtils; +import com.android.settings.R; +import com.android.settings.core.FeatureFlags; +import com.android.settings.core.SettingsBaseActivity; +import com.android.settings.development.featureflags.FeatureFlagPersistent; +import com.android.settings.network.SubscriptionUtil; + +import com.google.android.material.bottomnavigation.BottomNavigationView; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class MobileNetworkActivity extends SettingsBaseActivity { + + private static final String TAG = "MobileNetworkActivity"; + @VisibleForTesting + static final String MOBILE_SETTINGS_TAG = "mobile_settings:"; + @VisibleForTesting + static final int SUB_ID_NULL = Integer.MIN_VALUE; + + @VisibleForTesting + SubscriptionManager mSubscriptionManager; + @VisibleForTesting + int mCurSubscriptionId; + @VisibleForTesting + List<SubscriptionInfo> mSubscriptionInfos = new ArrayList<>(); + private PhoneChangeReceiver mPhoneChangeReceiver; + + private final SubscriptionManager.OnSubscriptionsChangedListener + mOnSubscriptionsChangeListener + = new SubscriptionManager.OnSubscriptionsChangedListener() { + @Override + public void onSubscriptionsChanged() { + if (!Objects.equals(mSubscriptionInfos, + mSubscriptionManager.getActiveSubscriptionInfoList(true))) { + updateSubscriptions(null); + } + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (FeatureFlagPersistent.isEnabled(this, FeatureFlags.NETWORK_INTERNET_V2)) { + setContentView(R.layout.mobile_network_settings_container_v2); + } else { + setContentView(R.layout.mobile_network_settings_container); + } + setActionBar(findViewById(R.id.mobile_action_bar)); + mPhoneChangeReceiver = new PhoneChangeReceiver(this, () -> { + if (mCurSubscriptionId != SUB_ID_NULL) { + // When the radio changes (ex: CDMA->GSM), refresh the fragment. + // This is very rare. + switchFragment(new MobileNetworkSettings(), mCurSubscriptionId, + true /* forceUpdate */); + } + }); + mSubscriptionManager = getSystemService(SubscriptionManager.class); + mSubscriptionInfos = mSubscriptionManager.getActiveSubscriptionInfoList(true); + mCurSubscriptionId = savedInstanceState != null + ? savedInstanceState.getInt(Settings.EXTRA_SUB_ID, SUB_ID_NULL) + : SUB_ID_NULL; + + final ActionBar actionBar = getActionBar(); + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(true); + } + + updateSubscriptions(savedInstanceState); + } + + @Override + protected void onStart() { + super.onStart(); + mPhoneChangeReceiver.register(); + mSubscriptionManager.addOnSubscriptionsChangedListener(mOnSubscriptionsChangeListener); + } + + @Override + protected void onStop() { + super.onStop(); + mPhoneChangeReceiver.unregister(); + mSubscriptionManager.removeOnSubscriptionsChangedListener(mOnSubscriptionsChangeListener); + } + + @Override + protected void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + saveInstanceState(outState); + } + + @VisibleForTesting + void saveInstanceState(@NonNull Bundle outState) { + outState.putInt(Settings.EXTRA_SUB_ID, mCurSubscriptionId); + } + + @VisibleForTesting + void updateSubscriptions(Bundle savedInstanceState) { + // Set the title to the name of the subscription. If we don't have subscription info, the + // title will just default to the label for this activity that's already specified in + // AndroidManifest.xml. + final SubscriptionInfo subscription = getSubscription(); + if (subscription != null) { + setTitle(subscription.getDisplayName()); + } + + mSubscriptionInfos = mSubscriptionManager.getActiveSubscriptionInfoList(true); + + if (!FeatureFlagPersistent.isEnabled(this, FeatureFlags.NETWORK_INTERNET_V2)) { + updateBottomNavigationView(); + } + + if (savedInstanceState == null) { + switchFragment(new MobileNetworkSettings(), getSubscriptionId()); + } + } + + /** + * Get the current subscription to display. First check whether intent has {@link + * Settings#EXTRA_SUB_ID} and if so find the subscription with that id. If not, just return the + * first one in the mSubscriptionInfos list since it is already sorted by sim slot. + */ + @VisibleForTesting + SubscriptionInfo getSubscription() { + final Intent intent = getIntent(); + if (intent != null) { + final int subId = intent.getIntExtra(Settings.EXTRA_SUB_ID, SUB_ID_NULL); + if (subId != SUB_ID_NULL) { + for (SubscriptionInfo subscription : + SubscriptionUtil.getAvailableSubscriptions(this)) { + if (subscription.getSubscriptionId() == subId) { + return subscription; + } + } + } + } + + if (CollectionUtils.isEmpty(mSubscriptionInfos)) { + return null; + } + return mSubscriptionInfos.get(0); + } + + /** + * Get the current subId to display. + */ + @VisibleForTesting + int getSubscriptionId() { + final SubscriptionInfo subscription = getSubscription(); + if (subscription != null) { + return subscription.getSubscriptionId(); + } + return SubscriptionManager.INVALID_SUBSCRIPTION_ID; + } + + @VisibleForTesting + void updateBottomNavigationView() { + final BottomNavigationView navigation = findViewById(R.id.bottom_nav); + + if (CollectionUtils.size(mSubscriptionInfos) <= 1) { + navigation.setVisibility(View.GONE); + } else { + final Menu menu = navigation.getMenu(); + menu.clear(); + for (int i = 0, size = mSubscriptionInfos.size(); i < size; i++) { + final SubscriptionInfo subscriptionInfo = mSubscriptionInfos.get(i); + menu.add(0, subscriptionInfo.getSubscriptionId(), i, + subscriptionInfo.getDisplayName()) + .setIcon(R.drawable.ic_settings_sim); + } + navigation.setOnNavigationItemSelectedListener(item -> { + switchFragment(new MobileNetworkSettings(), item.getItemId()); + return true; + }); + } + } + + @VisibleForTesting + void switchFragment(Fragment fragment, int subscriptionId) { + switchFragment(fragment, subscriptionId, false /* forceUpdate */); + } + + @VisibleForTesting + void switchFragment(Fragment fragment, int subscriptionId, boolean forceUpdate) { + if (mCurSubscriptionId != SUB_ID_NULL && subscriptionId == mCurSubscriptionId + && !forceUpdate) { + return; + } + final FragmentManager fragmentManager = getSupportFragmentManager(); + final FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); + final Bundle bundle = new Bundle(); + bundle.putInt(Settings.EXTRA_SUB_ID, subscriptionId); + + fragment.setArguments(bundle); + fragmentTransaction.replace(R.id.main_content, fragment, + buildFragmentTag(subscriptionId)); + fragmentTransaction.commit(); + mCurSubscriptionId = subscriptionId; + } + + private String buildFragmentTag(int subscriptionId) { + return MOBILE_SETTINGS_TAG + subscriptionId; + } + + @VisibleForTesting + static class PhoneChangeReceiver extends BroadcastReceiver { + private static final IntentFilter RADIO_TECHNOLOGY_CHANGED_FILTER = new IntentFilter( + TelephonyIntents.ACTION_RADIO_TECHNOLOGY_CHANGED); + + private Context mContext; + private Client mClient; + + interface Client { + void onPhoneChange(); + } + + public PhoneChangeReceiver(Context context, Client client) { + mContext = context; + mClient = client; + } + + public void register() { + mContext.registerReceiver(this, RADIO_TECHNOLOGY_CHANGED_FILTER); + } + + public void unregister() { + mContext.unregisterReceiver(this); + } + + @Override + public void onReceive(Context context, Intent intent) { + if (!isInitialStickyBroadcast()) { + mClient.onPhoneChange(); + } + } + } +} diff --git a/src/com/android/settings/network/telephony/MobileNetworkSettings.java b/src/com/android/settings/network/telephony/MobileNetworkSettings.java new file mode 100644 index 0000000000..c8e2247db1 --- /dev/null +++ b/src/com/android/settings/network/telephony/MobileNetworkSettings.java @@ -0,0 +1,289 @@ +/* + * Copyright (C) 2018 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.network.telephony; + +import android.app.Activity; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.UserManager; +import android.provider.SearchIndexableResource; +import android.provider.Settings; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; + +import com.android.internal.telephony.TelephonyIntents; +import com.android.settings.R; +import com.android.settings.core.FeatureFlags; +import com.android.settings.dashboard.RestrictedDashboardFragment; +import com.android.settings.datausage.BillingCyclePreferenceController; +import com.android.settings.datausage.DataUsageSummaryPreferenceController; +import com.android.settings.development.featureflags.FeatureFlagPersistent; +import com.android.settings.network.telephony.cdma.CdmaSubscriptionPreferenceController; +import com.android.settings.network.telephony.cdma.CdmaSystemSelectPreferenceController; +import com.android.settings.network.telephony.gsm.AutoSelectPreferenceController; +import com.android.settings.network.telephony.gsm.OpenNetworkSelectPagePreferenceController; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; +import com.android.settings.widget.PreferenceCategoryController; +import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.search.SearchIndexable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; + +@SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC) +public class MobileNetworkSettings extends RestrictedDashboardFragment { + + private static final String LOG_TAG = "NetworkSettings"; + public static final int REQUEST_CODE_EXIT_ECM = 17; + public static final int REQUEST_CODE_DELETE_SUBSCRIPTION = 18; + @VisibleForTesting + static final String KEY_CLICKED_PREF = "key_clicked_pref"; + + //String keys for preference lookup + private static final String BUTTON_CDMA_SYSTEM_SELECT_KEY = "cdma_system_select_key"; + private static final String BUTTON_CDMA_SUBSCRIPTION_KEY = "cdma_subscription_key"; + + private TelephonyManager mTelephonyManager; + private int mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; + + private CdmaSystemSelectPreferenceController mCdmaSystemSelectPreferenceController; + private CdmaSubscriptionPreferenceController mCdmaSubscriptionPreferenceController; + + private UserManager mUserManager; + private String mClickedPrefKey; + + public MobileNetworkSettings() { + super(UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS); + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.MOBILE_NETWORK; + } + + /** + * Invoked on each preference click in this hierarchy, overrides + * PreferenceActivity's implementation. Used to make sure we track the + * preference click events. + */ + @Override + public boolean onPreferenceTreeClick(Preference preference) { + if (super.onPreferenceTreeClick(preference)) { + return true; + } + final String key = preference.getKey(); + + if (TextUtils.equals(key, BUTTON_CDMA_SYSTEM_SELECT_KEY) + || TextUtils.equals(key, BUTTON_CDMA_SUBSCRIPTION_KEY)) { + if (mTelephonyManager.getEmergencyCallbackMode()) { + startActivityForResult( + new Intent(TelephonyIntents.ACTION_SHOW_NOTICE_ECM_BLOCK_OTHERS, null), + REQUEST_CODE_EXIT_ECM); + mClickedPrefKey = key; + } + return true; + } + + return false; + } + + @Override + protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { + mSubId = getArguments().getInt(Settings.EXTRA_SUB_ID, + MobileNetworkUtils.getSearchableSubscriptionId(context)); + + if (FeatureFlagPersistent.isEnabled(getContext(), FeatureFlags.NETWORK_INTERNET_V2) && + mSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + return Arrays.asList( + new DataUsageSummaryPreferenceController(getActivity(), getSettingsLifecycle(), + this, mSubId)); + } + return Arrays.asList(); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + + if (FeatureFlagPersistent.isEnabled(getContext(), FeatureFlags.NETWORK_INTERNET_V2)) { + use(CallsDefaultSubscriptionController.class).init(getLifecycle()); + use(SmsDefaultSubscriptionController.class).init(getLifecycle()); + use(MobileNetworkSwitchController.class).init(getLifecycle(), mSubId); + use(CarrierSettingsVersionPreferenceController.class).init(mSubId); + use(BillingCyclePreferenceController.class).init(mSubId); + use(MmsMessagePreferenceController.class).init(mSubId); + use(DataDuringCallsPreferenceController.class).init(getLifecycle(), mSubId); + use(DisabledSubscriptionController.class).init(getLifecycle(), mSubId); + use(DeleteSimProfilePreferenceController.class).init(mSubId, this, + REQUEST_CODE_DELETE_SUBSCRIPTION); + use(DisableSimFooterPreferenceController.class).init(mSubId); + } + use(MobileDataPreferenceController.class).init(getFragmentManager(), mSubId); + use(RoamingPreferenceController.class).init(getFragmentManager(), mSubId); + use(ApnPreferenceController.class).init(mSubId); + use(CarrierPreferenceController.class).init(mSubId); + use(DataUsagePreferenceController.class).init(mSubId); + use(PreferredNetworkModePreferenceController.class).init(mSubId); + use(EnabledNetworkModePreferenceController.class).init(mSubId); + use(DataServiceSetupPreferenceController.class).init(mSubId); + if (!FeatureFlagPersistent.isEnabled(getContext(), FeatureFlags.NETWORK_INTERNET_V2)) { + use(EuiccPreferenceController.class).init(mSubId); + } + use(WifiCallingPreferenceController.class).init(mSubId); + + final OpenNetworkSelectPagePreferenceController openNetworkSelectPagePreferenceController = + use(OpenNetworkSelectPagePreferenceController.class).init(mSubId); + final AutoSelectPreferenceController autoSelectPreferenceController = + use(AutoSelectPreferenceController.class) + .init(mSubId) + .addListener(openNetworkSelectPagePreferenceController); + use(PreferenceCategoryController.class).setChildren( + Arrays.asList(autoSelectPreferenceController)); + + mCdmaSystemSelectPreferenceController = use(CdmaSystemSelectPreferenceController.class); + mCdmaSystemSelectPreferenceController.init(getPreferenceManager(), mSubId); + mCdmaSubscriptionPreferenceController = use(CdmaSubscriptionPreferenceController.class); + mCdmaSubscriptionPreferenceController.init(getPreferenceManager(), mSubId); + + final VideoCallingPreferenceController videoCallingPreferenceController = + use(VideoCallingPreferenceController.class).init(mSubId); + use(Enhanced4gLtePreferenceController.class).init(mSubId) + .addListener(videoCallingPreferenceController); + } + + @Override + public void onCreate(Bundle icicle) { + Log.i(LOG_TAG, "onCreate:+"); + super.onCreate(icicle); + final Context context = getContext(); + + mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE); + mTelephonyManager = TelephonyManager.from(context).createForSubscriptionId(mSubId); + + onRestoreInstance(icicle); + } + + @VisibleForTesting + void onRestoreInstance(Bundle icicle) { + if (icicle != null) { + mClickedPrefKey = icicle.getString(KEY_CLICKED_PREF); + } + } + + @Override + protected int getPreferenceScreenResId() { + if (FeatureFlagPersistent.isEnabled(getContext(), FeatureFlags.NETWORK_INTERNET_V2)) { + return R.xml.mobile_network_settings_v2; + } else { + return R.xml.mobile_network_settings; + } + } + + @Override + protected String getLogTag() { + return LOG_TAG; + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putString(KEY_CLICKED_PREF, mClickedPrefKey); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case REQUEST_CODE_EXIT_ECM: + if (resultCode != Activity.RESULT_CANCELED) { + // If the phone exits from ECM mode, show the CDMA + final Preference preference = getPreferenceScreen() + .findPreference(mClickedPrefKey); + if (preference != null) { + preference.performClick(); + } + } + break; + + case REQUEST_CODE_DELETE_SUBSCRIPTION: + final Activity activity = getActivity(); + if (activity != null && !activity.isFinishing()) { + activity.finish(); + } + break; + + default: + break; + } + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + if (FeatureFlagPersistent.isEnabled(getContext(), FeatureFlags.NETWORK_INTERNET_V2) && + mSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + final MenuItem item = menu.add(Menu.NONE, R.id.edit_sim_name, Menu.NONE, + R.string.mobile_network_sim_name); + item.setIcon(com.android.internal.R.drawable.ic_mode_edit); + item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); + } + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public boolean onOptionsItemSelected(MenuItem menuItem) { + if (FeatureFlagPersistent.isEnabled(getContext(), FeatureFlags.NETWORK_INTERNET_V2) && + mSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + if (menuItem.getItemId() == R.id.edit_sim_name) { + RenameMobileNetworkDialogFragment.newInstance(mSubId).show( + getFragmentManager(), RenameMobileNetworkDialogFragment.TAG); + return true; + } + } + return super.onOptionsItemSelected(menuItem); + } + + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, + boolean enabled) { + final ArrayList<SearchIndexableResource> result = new ArrayList<>(); + + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.mobile_network_settings; + result.add(sir); + return result; + } + + /** suppress full page if user is not admin */ + @Override + protected boolean isPageSearchEnabled(Context context) { + return context.getSystemService(UserManager.class).isAdminUser(); + } + }; +} diff --git a/src/com/android/settings/network/telephony/MobileNetworkSwitchController.java b/src/com/android/settings/network/telephony/MobileNetworkSwitchController.java new file mode 100644 index 0000000000..02396ddef7 --- /dev/null +++ b/src/com/android/settings/network/telephony/MobileNetworkSwitchController.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2019 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.network.telephony; + +import static androidx.lifecycle.Lifecycle.Event.ON_PAUSE; +import static androidx.lifecycle.Lifecycle.Event.ON_RESUME; + +import android.content.Context; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.util.Log; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.core.FeatureFlags; +import com.android.settings.development.featureflags.FeatureFlagPersistent; +import com.android.settings.network.SubscriptionUtil; +import com.android.settings.network.SubscriptionsChangeListener; +import com.android.settings.widget.SwitchBar; +import com.android.settingslib.widget.LayoutPreference; + +import java.util.List; + +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.OnLifecycleEvent; +import androidx.preference.PreferenceScreen; + +/** This controls a switch to allow enabling/disabling a mobile network */ +public class MobileNetworkSwitchController extends BasePreferenceController implements + SubscriptionsChangeListener.SubscriptionsChangeListenerClient, LifecycleObserver { + private static final String TAG = "MobileNetworkSwitchCtrl"; + private SwitchBar mSwitchBar; + private int mSubId; + private SubscriptionsChangeListener mChangeListener; + private SubscriptionManager mSubscriptionManager; + + public MobileNetworkSwitchController(Context context, String preferenceKey) { + super(context, preferenceKey); + mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; + mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class); + mChangeListener = new SubscriptionsChangeListener(context, this); + } + + public void init(Lifecycle lifecycle, int subId) { + lifecycle.addObserver(this); + mSubId = subId; + } + + @OnLifecycleEvent(ON_RESUME) + public void onResume() { + mChangeListener.start(); + update(); + } + + @OnLifecycleEvent(ON_PAUSE) + public void onPause() { + mChangeListener.stop(); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + final LayoutPreference pref = screen.findPreference(mPreferenceKey); + mSwitchBar = pref.findViewById(R.id.switch_bar); + mSwitchBar.setSwitchBarText(R.string.mobile_network_use_sim_on, + R.string.mobile_network_use_sim_off); + + mSwitchBar.addOnSwitchChangeListener((switchView, isChecked) -> { + // TODO b/135222940: re-evaluate whether to use + // mSubscriptionManager#isSubscriptionEnabled + if (mSubscriptionManager.isActiveSubId(mSubId) != isChecked + && (!mSubscriptionManager.setSubscriptionEnabled(mSubId, isChecked))) { + mSwitchBar.setChecked(!isChecked); + } + }); + update(); + } + + private void update() { + if (mSwitchBar == null) { + return; + } + + SubscriptionInfo subInfo = null; + for (SubscriptionInfo info : SubscriptionUtil.getAvailableSubscriptions(mContext)) { + if (info.getSubscriptionId() == mSubId) { + subInfo = info; + break; + } + } + + // For eSIM, we always want the toggle. The telephony stack doesn't currently support + // disabling a pSIM directly (b/133379187), so we for now we don't include this on pSIM. + if (subInfo == null || !subInfo.isEmbedded()) { + mSwitchBar.hide(); + } else { + mSwitchBar.show(); + // TODO b/135222940: re-evaluate whether to use + // mSubscriptionManager#isSubscriptionEnabled + mSwitchBar.setChecked(mSubscriptionManager.isActiveSubId(mSubId)); + } + } + + @Override + public int getAvailabilityStatus() { + if (FeatureFlagPersistent.isEnabled(mContext, FeatureFlags.NETWORK_INTERNET_V2)) { + return AVAILABLE_UNSEARCHABLE; + } else { + return CONDITIONALLY_UNAVAILABLE; + } + } + + @Override + public void onAirplaneModeChanged(boolean airplaneModeEnabled) {} + + @Override + public void onSubscriptionsChanged() { + update(); + } +} diff --git a/src/com/android/settings/network/telephony/MobileNetworkUtils.java b/src/com/android/settings/network/telephony/MobileNetworkUtils.java new file mode 100644 index 0000000000..0e5eaa89ed --- /dev/null +++ b/src/com/android/settings/network/telephony/MobileNetworkUtils.java @@ -0,0 +1,538 @@ +/* + * Copyright (C) 2018 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.network.telephony; + +import static android.provider.Telephony.Carriers.ENFORCE_MANAGED_URI; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.database.Cursor; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; +import android.os.PersistableBundle; +import android.os.SystemProperties; +import android.provider.Settings; +import android.telecom.PhoneAccountHandle; +import android.telecom.TelecomManager; +import android.telephony.CarrierConfigManager; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.telephony.euicc.EuiccManager; +import android.telephony.ims.feature.ImsFeature; +import android.text.TextUtils; +import android.util.Log; +import android.view.Gravity; + +import androidx.annotation.VisibleForTesting; + +import com.android.ims.ImsException; +import com.android.ims.ImsManager; +import com.android.internal.telephony.Phone; +import com.android.internal.telephony.PhoneConstants; +import com.android.internal.util.ArrayUtils; +import com.android.settings.R; +import com.android.settings.Utils; +import com.android.settings.core.BasePreferenceController; +import com.android.settingslib.graph.SignalDrawable; + +import java.util.Arrays; +import java.util.List; + +public class MobileNetworkUtils { + + private static final String TAG = "MobileNetworkUtils"; + + // CID of the device. + private static final String KEY_CID = "ro.boot.cid"; + // CIDs of devices which should not show anything related to eSIM. + private static final String KEY_ESIM_CID_IGNORE = "ro.setupwizard.esim_cid_ignore"; + // System Property which is used to decide whether the default eSIM UI will be shown, + // the default value is false. + private static final String KEY_ENABLE_ESIM_UI_BY_DEFAULT = + "esim.enable_esim_system_ui_by_default"; + private static final String LEGACY_ACTION_CONFIGURE_PHONE_ACCOUNT = + "android.telecom.action.CONNECTION_SERVICE_CONFIGURE"; + + // The following constants are used to draw signal icon. + public static final int NO_CELL_DATA_TYPE_ICON = 0; + public static final Drawable EMPTY_DRAWABLE = new ColorDrawable(Color.TRANSPARENT); + + /** + * Returns if DPC APNs are enforced. + */ + public static boolean isDpcApnEnforced(Context context) { + try (Cursor enforceCursor = context.getContentResolver().query(ENFORCE_MANAGED_URI, + null, null, null, null)) { + if (enforceCursor == null || enforceCursor.getCount() != 1) { + return false; + } + enforceCursor.moveToFirst(); + return enforceCursor.getInt(0) > 0; + } + } + + /** + * Returns true if Wifi calling is enabled for at least one subscription. + */ + public static boolean isWifiCallingEnabled(Context context) { + SubscriptionManager subManager = context.getSystemService(SubscriptionManager.class); + if (subManager == null) { + Log.e(TAG, "isWifiCallingEnabled: couldn't get system service."); + return false; + } + for (int subId : subManager.getActiveSubscriptionIdList()) { + if (isWifiCallingEnabled(context, subId)) { + return true; + } + } + return false; + } + + /** + * Returns true if Wifi calling is enabled for the specific subscription with id {@code subId}. + */ + public static boolean isWifiCallingEnabled(Context context, int subId) { + final PhoneAccountHandle simCallManager = + TelecomManager.from(context).getSimCallManagerForSubscription(subId); + final int phoneId = SubscriptionManager.getSlotIndex(subId); + + boolean isWifiCallingEnabled; + if (simCallManager != null) { + Intent intent = buildPhoneAccountConfigureIntent( + context, simCallManager); + + isWifiCallingEnabled = intent != null; + } else { + ImsManager imsMgr = ImsManager.getInstance(context, phoneId); + isWifiCallingEnabled = imsMgr != null + && imsMgr.isWfcEnabledByPlatform() + && imsMgr.isWfcProvisionedOnDevice() + && isImsServiceStateReady(imsMgr); + } + + return isWifiCallingEnabled; + } + + @VisibleForTesting + static Intent buildPhoneAccountConfigureIntent( + Context context, PhoneAccountHandle accountHandle) { + Intent intent = buildConfigureIntent( + context, accountHandle, TelecomManager.ACTION_CONFIGURE_PHONE_ACCOUNT); + + if (intent == null) { + // If the new configuration didn't work, try the old configuration intent. + intent = buildConfigureIntent(context, accountHandle, + LEGACY_ACTION_CONFIGURE_PHONE_ACCOUNT); + } + return intent; + } + + private static Intent buildConfigureIntent( + Context context, PhoneAccountHandle accountHandle, String actionStr) { + if (accountHandle == null || accountHandle.getComponentName() == null + || TextUtils.isEmpty(accountHandle.getComponentName().getPackageName())) { + return null; + } + + // Build the settings intent. + Intent intent = new Intent(actionStr); + intent.setPackage(accountHandle.getComponentName().getPackageName()); + intent.addCategory(Intent.CATEGORY_DEFAULT); + intent.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, accountHandle); + + // Check to see that the phone account package can handle the setting intent. + PackageManager pm = context.getPackageManager(); + List<ResolveInfo> resolutions = pm.queryIntentActivities(intent, 0); + if (resolutions.size() == 0) { + intent = null; // set no intent if the package cannot handle it. + } + + return intent; + } + + public static boolean isImsServiceStateReady(ImsManager imsMgr) { + boolean isImsServiceStateReady = false; + + try { + if (imsMgr != null && imsMgr.getImsServiceState() == ImsFeature.STATE_READY) { + isImsServiceStateReady = true; + } + } catch (ImsException ex) { + Log.e(TAG, "Exception when trying to get ImsServiceStatus: " + ex); + } + + Log.d(TAG, "isImsServiceStateReady=" + isImsServiceStateReady); + return isImsServiceStateReady; + } + + /** + * Whether to show the entry point to eUICC settings. + * + * <p>We show the entry point on any device which supports eUICC as long as either the eUICC + * was ever provisioned (that is, at least one profile was ever downloaded onto it), or if + * the user has enabled development mode. + */ + public static boolean showEuiccSettings(Context context) { + EuiccManager euiccManager = + (EuiccManager) context.getSystemService(EuiccManager.class); + if (!euiccManager.isEnabled()) { + return false; + } + + final ContentResolver cr = context.getContentResolver(); + + TelephonyManager tm = + (TelephonyManager) context.getSystemService(TelephonyManager.class); + String currentCountry = tm.getNetworkCountryIso().toLowerCase(); + String supportedCountries = + Settings.Global.getString(cr, Settings.Global.EUICC_SUPPORTED_COUNTRIES); + boolean inEsimSupportedCountries = false; + if (TextUtils.isEmpty(currentCountry)) { + inEsimSupportedCountries = true; + } else if (!TextUtils.isEmpty(supportedCountries)) { + List<String> supportedCountryList = + Arrays.asList(TextUtils.split(supportedCountries.toLowerCase(), ",")); + if (supportedCountryList.contains(currentCountry)) { + inEsimSupportedCountries = true; + } + } + final boolean esimIgnoredDevice = + Arrays.asList(TextUtils.split(SystemProperties.get(KEY_ESIM_CID_IGNORE, ""), ",")) + .contains(SystemProperties.get(KEY_CID, null)); + final boolean enabledEsimUiByDefault = + SystemProperties.getBoolean(KEY_ENABLE_ESIM_UI_BY_DEFAULT, true); + final boolean euiccProvisioned = + Settings.Global.getInt(cr, Settings.Global.EUICC_PROVISIONED, 0) != 0; + final boolean inDeveloperMode = + Settings.Global.getInt(cr, Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0; + + return (inDeveloperMode || euiccProvisioned + || (!esimIgnoredDevice && enabledEsimUiByDefault && inEsimSupportedCountries)); + } + + /** + * Set whether to enable data for {@code subId}, also whether to disable data for other + * subscription + */ + public static void setMobileDataEnabled(Context context, int subId, boolean enabled, + boolean disableOtherSubscriptions) { + final TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class) + .createForSubscriptionId(subId); + final SubscriptionManager subscriptionManager = context.getSystemService( + SubscriptionManager.class); + telephonyManager.setDataEnabled(enabled); + + if (disableOtherSubscriptions) { + List<SubscriptionInfo> subInfoList = + subscriptionManager.getActiveSubscriptionInfoList(true); + if (subInfoList != null) { + for (SubscriptionInfo subInfo : subInfoList) { + // We never disable mobile data for opportunistic subscriptions. + if (subInfo.getSubscriptionId() != subId && !subInfo.isOpportunistic()) { + context.getSystemService(TelephonyManager.class).createForSubscriptionId( + subInfo.getSubscriptionId()).setDataEnabled(false); + } + } + } + } + } + + /** + * Return {@code true} if show CDMA category + */ + public static boolean isCdmaOptions(Context context, int subId) { + if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + return false; + } + final TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class) + .createForSubscriptionId(subId); + final PersistableBundle carrierConfig = context.getSystemService( + CarrierConfigManager.class).getConfigForSubId(subId); + + + if (telephonyManager.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) { + return true; + } else if (carrierConfig != null + && !carrierConfig.getBoolean( + CarrierConfigManager.KEY_HIDE_CARRIER_NETWORK_SETTINGS_BOOL) + && carrierConfig.getBoolean(CarrierConfigManager.KEY_WORLD_PHONE_BOOL)) { + return true; + } + + if (isWorldMode(context, subId)) { + final int settingsNetworkMode = android.provider.Settings.Global.getInt( + context.getContentResolver(), + android.provider.Settings.Global.PREFERRED_NETWORK_MODE + subId, + Phone.PREFERRED_NT_MODE); + if (settingsNetworkMode == TelephonyManager.NETWORK_MODE_LTE_GSM_WCDMA + || settingsNetworkMode == TelephonyManager.NETWORK_MODE_LTE_CDMA_EVDO) { + return true; + } + + if (shouldSpeciallyUpdateGsmCdma(context, subId)) { + return true; + } + } + + return false; + } + + /** + * return {@code true} if we need show Gsm related settings + */ + public static boolean isGsmOptions(Context context, int subId) { + if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + return false; + } + if (isGsmBasicOptions(context, subId)) { + return true; + } + final int networkMode = android.provider.Settings.Global.getInt( + context.getContentResolver(), + android.provider.Settings.Global.PREFERRED_NETWORK_MODE + subId, + Phone.PREFERRED_NT_MODE); + if (isWorldMode(context, subId)) { + if (networkMode == TelephonyManager.NETWORK_MODE_LTE_CDMA_EVDO + || networkMode == TelephonyManager.NETWORK_MODE_LTE_GSM_WCDMA) { + return true; + } else if (shouldSpeciallyUpdateGsmCdma(context, subId)) { + return true; + } + } + + return false; + } + + private static boolean isGsmBasicOptions(Context context, int subId) { + final TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class) + .createForSubscriptionId(subId); + final PersistableBundle carrierConfig = context.getSystemService( + CarrierConfigManager.class).getConfigForSubId(subId); + + if (telephonyManager.getPhoneType() == PhoneConstants.PHONE_TYPE_GSM) { + return true; + } else if (carrierConfig != null + && !carrierConfig.getBoolean( + CarrierConfigManager.KEY_HIDE_CARRIER_NETWORK_SETTINGS_BOOL) + && carrierConfig.getBoolean(CarrierConfigManager.KEY_WORLD_PHONE_BOOL)) { + return true; + } + + return false; + } + + /** + * Return {@code true} if it is world mode, and we may show advanced options in telephony + * settings + */ + public static boolean isWorldMode(Context context, int subId) { + final PersistableBundle carrierConfig = context.getSystemService( + CarrierConfigManager.class).getConfigForSubId(subId); + return carrierConfig == null + ? false + : carrierConfig.getBoolean(CarrierConfigManager.KEY_WORLD_MODE_ENABLED_BOOL); + } + + /** + * Return {@code true} if we need show settings for network selection(i.e. Verizon) + */ + public static boolean shouldDisplayNetworkSelectOptions(Context context, int subId) { + final TelephonyManager telephonyManager = TelephonyManager.from(context) + .createForSubscriptionId(subId); + final PersistableBundle carrierConfig = context.getSystemService( + CarrierConfigManager.class).getConfigForSubId(subId); + if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID + || carrierConfig == null + || !carrierConfig.getBoolean( + CarrierConfigManager.KEY_OPERATOR_SELECTION_EXPAND_BOOL) + || carrierConfig.getBoolean( + CarrierConfigManager.KEY_HIDE_CARRIER_NETWORK_SETTINGS_BOOL) + || (carrierConfig.getBoolean(CarrierConfigManager.KEY_CSP_ENABLED_BOOL) + && !telephonyManager.isManualNetworkSelectionAllowed())) { + return false; + } + + final int networkMode = android.provider.Settings.Global.getInt( + context.getContentResolver(), + android.provider.Settings.Global.PREFERRED_NETWORK_MODE + subId, + Phone.PREFERRED_NT_MODE); + if (networkMode == TelephonyManager.NETWORK_MODE_LTE_CDMA_EVDO + && isWorldMode(context, subId)) { + return false; + } + if (shouldSpeciallyUpdateGsmCdma(context, subId)) { + return false; + } + + if (isGsmBasicOptions(context, subId)) { + return true; + } + + if (isWorldMode(context, subId)) { + if (networkMode == TelephonyManager.NETWORK_MODE_LTE_GSM_WCDMA) { + return true; + } + } + + return false; + } + + /** + * Return {@code true} if Tdscdma is supported in current subscription + */ + public static boolean isTdscdmaSupported(Context context, int subId) { + return isTdscdmaSupported(context, + context.getSystemService(TelephonyManager.class).createForSubscriptionId(subId)); + } + + //TODO(b/117651939): move it to telephony + private static boolean isTdscdmaSupported(Context context, TelephonyManager telephonyManager) { + final PersistableBundle carrierConfig = context.getSystemService( + CarrierConfigManager.class).getConfig(); + + if (carrierConfig == null) { + return false; + } + + if (carrierConfig.getBoolean(CarrierConfigManager.KEY_SUPPORT_TDSCDMA_BOOL)) { + return true; + } + + String operatorNumeric = telephonyManager.getServiceState().getOperatorNumeric(); + String[] numericArray = carrierConfig.getStringArray( + CarrierConfigManager.KEY_SUPPORT_TDSCDMA_ROAMING_NETWORKS_STRING_ARRAY); + if (numericArray == null || operatorNumeric == null) { + return false; + } + for (String numeric : numericArray) { + if (operatorNumeric.equals(numeric)) { + return true; + } + } + return false; + } + + /** + * Return subId that supported by search. If there are more than one, return first one, + * otherwise return {@link SubscriptionManager#INVALID_SUBSCRIPTION_ID} + */ + public static int getSearchableSubscriptionId(Context context) { + final SubscriptionManager subscriptionManager = context.getSystemService( + SubscriptionManager.class); + final int subIds[] = subscriptionManager.getActiveSubscriptionIdList(); + + return subIds.length >= 1 ? subIds[0] : SubscriptionManager.INVALID_SUBSCRIPTION_ID; + } + + /** + * Return availability for a default subscription id. If subId already been set, use it to + * check, otherwise traverse all active subIds on device to check. + * @param context context + * @param defSubId Default subId get from telephony preference controller + * @param callback Callback to check availability for a specific subId + * @return Availability + * + * @see BasePreferenceController#getAvailabilityStatus() + */ + public static int getAvailability(Context context, int defSubId, + TelephonyAvailabilityCallback callback) { + final SubscriptionManager subscriptionManager = context.getSystemService( + SubscriptionManager.class); + if (defSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + // If subId has been set, return the corresponding status + return callback.getAvailabilityStatus(defSubId); + } else { + // Otherwise, search whether there is one subId in device that support this preference + final int[] subIds = subscriptionManager.getActiveSubscriptionIdList(); + if (ArrayUtils.isEmpty(subIds)) { + return callback.getAvailabilityStatus( + SubscriptionManager.INVALID_SUBSCRIPTION_ID); + } else { + for (final int subId : subIds) { + final int status = callback.getAvailabilityStatus(subId); + if (status == BasePreferenceController.AVAILABLE) { + return status; + } + } + return callback.getAvailabilityStatus(subIds[0]); + } + } + } + + /** + * This method is migrated from {@link com.android.phone.MobileNetworkSettings} and we should + * use it carefully. This code snippet doesn't have very clear meaning however we should + * update GSM or CDMA differently based on what it returns. + * + * 1. For all CDMA settings, make them visible if it return {@code true} + * 2. For GSM settings, make them visible if it return {@code true} unless 3 + * 3. For network select settings, make it invisible if it return {@code true} + */ + @VisibleForTesting + static boolean shouldSpeciallyUpdateGsmCdma(Context context, int subId) { + final int networkMode = android.provider.Settings.Global.getInt( + context.getContentResolver(), + android.provider.Settings.Global.PREFERRED_NETWORK_MODE + subId, + Phone.PREFERRED_NT_MODE); + if (networkMode == TelephonyManager.NETWORK_MODE_LTE_TDSCDMA_GSM + || networkMode == TelephonyManager.NETWORK_MODE_LTE_TDSCDMA_GSM_WCDMA + || networkMode == TelephonyManager.NETWORK_MODE_LTE_TDSCDMA + || networkMode == TelephonyManager.NETWORK_MODE_LTE_TDSCDMA_WCDMA + || networkMode == TelephonyManager.NETWORK_MODE_LTE_TDSCDMA_CDMA_EVDO_GSM_WCDMA + || networkMode == TelephonyManager.NETWORK_MODE_LTE_CDMA_EVDO_GSM_WCDMA) { + if (!isTdscdmaSupported(context, subId) && isWorldMode(context, subId)) { + return true; + } + } + + return false; + } + + public static Drawable getSignalStrengthIcon(Context context, int level, int numLevels, + int iconType, boolean cutOut) { + SignalDrawable signalDrawable = new SignalDrawable(context); + signalDrawable.setLevel( + SignalDrawable.getState(level, numLevels, cutOut)); + + // Make the network type drawable + Drawable networkDrawable = + iconType == NO_CELL_DATA_TYPE_ICON + ? EMPTY_DRAWABLE + : context + .getResources().getDrawable(iconType, context.getTheme()); + + // Overlay the two drawables + final Drawable[] layers = {networkDrawable, signalDrawable}; + final int iconSize = + context.getResources().getDimensionPixelSize(R.dimen.signal_strength_icon_size); + + LayerDrawable icons = new LayerDrawable(layers); + // Set the network type icon at the top left + icons.setLayerGravity(0 /* index of networkDrawable */, Gravity.TOP | Gravity.LEFT); + // Set the signal strength icon at the bottom right + icons.setLayerGravity(1 /* index of SignalDrawable */, Gravity.BOTTOM | Gravity.RIGHT); + icons.setLayerSize(1 /* index of SignalDrawable */, iconSize, iconSize); + icons.setTintList(Utils.getColorAttr(context, android.R.attr.colorControlNormal)); + return icons; + } +} diff --git a/src/com/android/settings/network/telephony/NetworkOperatorPreference.java b/src/com/android/settings/network/telephony/NetworkOperatorPreference.java new file mode 100644 index 0000000000..77f40da7f1 --- /dev/null +++ b/src/com/android/settings/network/telephony/NetworkOperatorPreference.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2018 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.network.telephony; + +import static android.telephony.SignalStrength.NUM_SIGNAL_STRENGTH_BINS; + +import android.content.Context; +import android.telephony.CellInfo; +import android.telephony.CellSignalStrength; +import android.util.Log; + +import androidx.preference.Preference; + +import com.android.settings.R; +import com.android.settings.Utils; + +import java.util.List; + +/** + * A Preference represents a network operator in the NetworkSelectSetting fragment. + */ +public class NetworkOperatorPreference extends Preference { + + private static final String TAG = "NetworkOperatorPref"; + private static final boolean DBG = false; + + private static final int LEVEL_NONE = -1; + + private CellInfo mCellInfo; + private List<String> mForbiddenPlmns; + private int mLevel = LEVEL_NONE; + private boolean mShow4GForLTE; + private boolean mUseNewApi; + + public NetworkOperatorPreference( + CellInfo cellinfo, Context context, List<String> forbiddenPlmns, boolean show4GForLTE) { + super(context); + mCellInfo = cellinfo; + mForbiddenPlmns = forbiddenPlmns; + mShow4GForLTE = show4GForLTE; + mUseNewApi = context.getResources().getBoolean( + com.android.internal.R.bool.config_enableNewAutoSelectNetworkUI); + refresh(); + } + + public CellInfo getCellInfo() { + return mCellInfo; + } + + /** + * Refresh the NetworkOperatorPreference by updating the title and the icon. + */ + public void refresh() { + if (DBG) Log.d(TAG, "refresh the network: " + CellInfoUtil.getNetworkTitle(mCellInfo)); + String networkTitle = CellInfoUtil.getNetworkTitle(mCellInfo); + if (CellInfoUtil.isForbidden(mCellInfo, mForbiddenPlmns)) { + networkTitle += " " + getContext().getResources().getString(R.string.forbidden_network); + } + setTitle(networkTitle); + + final CellSignalStrength signalStrength = mCellInfo.getCellSignalStrength(); + final int level = signalStrength != null ? signalStrength.getLevel() : LEVEL_NONE; + if (DBG) Log.d(TAG, "refresh level: " + String.valueOf(level)); + if (mLevel != level) { + mLevel = level; + updateIcon(mLevel); + } + } + + /** + * Update the icon according to the input signal strength level. + */ + public void setIcon(int level) { + updateIcon(level); + } + + private int getIconIdForCell(CellInfo ci) { + final int type = ci.getCellIdentity().getType(); + switch (type) { + case CellInfo.TYPE_GSM: + return R.drawable.signal_strength_g; + case CellInfo.TYPE_WCDMA: // fall through + case CellInfo.TYPE_TDSCDMA: + return R.drawable.signal_strength_3g; + case CellInfo.TYPE_LTE: + return mShow4GForLTE + ? R.drawable.ic_signal_strength_4g : R.drawable.signal_strength_lte; + case CellInfo.TYPE_CDMA: + return R.drawable.signal_strength_1x; + default: + return MobileNetworkUtils.NO_CELL_DATA_TYPE_ICON; + } + } + + private void updateIcon(int level) { + if (!mUseNewApi || level < 0 || level >= NUM_SIGNAL_STRENGTH_BINS) { + return; + } + Context context = getContext(); + setIcon(MobileNetworkUtils.getSignalStrengthIcon(context, level, NUM_SIGNAL_STRENGTH_BINS, + getIconIdForCell(mCellInfo), false)); + } +} diff --git a/src/com/android/settings/network/telephony/NetworkScanHelper.java b/src/com/android/settings/network/telephony/NetworkScanHelper.java new file mode 100644 index 0000000000..c4839b6141 --- /dev/null +++ b/src/com/android/settings/network/telephony/NetworkScanHelper.java @@ -0,0 +1,288 @@ +/* + * Copyright (C) 2018 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.network.telephony; + +import android.annotation.IntDef; +import android.telephony.AccessNetworkConstants.AccessNetworkType; +import android.telephony.CellInfo; +import android.telephony.NetworkScan; +import android.telephony.NetworkScanRequest; +import android.telephony.RadioAccessSpecifier; +import android.telephony.TelephonyManager; +import android.telephony.TelephonyScanManager; +import android.util.Log; + +import com.android.internal.telephony.CellNetworkScanResult; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.stream.Collectors; + +/** + * A helper class that builds the common interface and performs the network scan for two different + * network scan APIs. + */ +public class NetworkScanHelper { + public static final String TAG = "NetworkScanHelper"; + + /** + * Callbacks interface to inform the network scan results. + */ + public interface NetworkScanCallback { + /** + * Called when the results is returned from {@link TelephonyManager}. This method will be + * called at least one time if there is no error occurred during the network scan. + * + * <p> This method can be called multiple times in one network scan, until + * {@link #onComplete()} or {@link #onError(int)} is called. + * + * @param results + */ + void onResults(List<CellInfo> results); + + /** + * Called when the current network scan process is finished. No more + * {@link #onResults(List)} will be called for the current network scan after this method is + * called. + */ + void onComplete(); + + /** + * Called when an error occurred during the network scan process. + * + * <p> There is no more result returned from {@link TelephonyManager} if an error occurred. + * + * <p> {@link #onComplete()} will not be called if an error occurred. + * + * @see {@link NetworkScan.ScanErrorCode} + */ + void onError(int errorCode); + } + + @Retention(RetentionPolicy.SOURCE) + @IntDef({NETWORK_SCAN_TYPE_WAIT_FOR_ALL_RESULTS, NETWORK_SCAN_TYPE_INCREMENTAL_RESULTS}) + public @interface NetworkQueryType {} + + /** + * Performs the network scan using {@link TelephonyManager#getAvailableNetworks()}. The network + * scan results won't be returned to the caller until the network scan is completed. + * + * <p> This is typically used when the modem doesn't support the new network scan api + * {@link TelephonyManager#requestNetworkScan( + * NetworkScanRequest, Executor, TelephonyScanManager.NetworkScanCallback)}. + */ + public static final int NETWORK_SCAN_TYPE_WAIT_FOR_ALL_RESULTS = 1; + + /** + * Performs the network scan using {@link TelephonyManager#requestNetworkScan( + * NetworkScanRequest, Executor, TelephonyScanManager.NetworkScanCallback)} The network scan + * results will be returned to the caller periodically in a small time window until the network + * scan is completed. The complete results should be returned in the last called of + * {@link NetworkScanCallback#onResults(List)}. + * + * <p> This is recommended to be used if modem supports the new network scan api + * {@link TelephonyManager#requestNetworkScan( + * NetworkScanRequest, Executor, TelephonyScanManager.NetworkScanCallback)} + */ + public static final int NETWORK_SCAN_TYPE_INCREMENTAL_RESULTS = 2; + + /** The constants below are used in the async network scan. */ + private static final boolean INCREMENTAL_RESULTS = true; + private static final int SEARCH_PERIODICITY_SEC = 5; + private static final int MAX_SEARCH_TIME_SEC = 300; + private static final int INCREMENTAL_RESULTS_PERIODICITY_SEC = 3; + + private static final NetworkScanRequest NETWORK_SCAN_REQUEST = + new NetworkScanRequest( + NetworkScanRequest.SCAN_TYPE_ONE_SHOT, + new RadioAccessSpecifier[]{ + // GSM + new RadioAccessSpecifier( + AccessNetworkType.GERAN, + null /* bands */, + null /* channels */), + // LTE + new RadioAccessSpecifier( + AccessNetworkType.EUTRAN, + null /* bands */, + null /* channels */), + // WCDMA + new RadioAccessSpecifier( + AccessNetworkType.UTRAN, + null /* bands */, + null /* channels */) + }, + SEARCH_PERIODICITY_SEC, + MAX_SEARCH_TIME_SEC, + INCREMENTAL_RESULTS, + INCREMENTAL_RESULTS_PERIODICITY_SEC, + null /* List of PLMN ids (MCC-MNC) */); + + private final NetworkScanCallback mNetworkScanCallback; + private final TelephonyManager mTelephonyManager; + private final TelephonyScanManager.NetworkScanCallback mInternalNetworkScanCallback; + private final Executor mExecutor; + + private NetworkScan mNetworkScanRequester; + + /** Callbacks for sync network scan */ + private ListenableFuture<List<CellInfo>> mNetworkScanFuture; + + public NetworkScanHelper(TelephonyManager tm, NetworkScanCallback callback, Executor executor) { + mTelephonyManager = tm; + mNetworkScanCallback = callback; + mInternalNetworkScanCallback = new NetworkScanCallbackImpl(); + mExecutor = executor; + } + + /** + * Performs a network scan for the given type {@code type}. + * {@link #NETWORK_SCAN_TYPE_INCREMENTAL_RESULTS} is recommended if modem supports + * {@link TelephonyManager#requestNetworkScan( + * NetworkScanRequest, Executor, TelephonyScanManager.NetworkScanCallback)}. + * + * @param type used to tell which network scan API should be used. + */ + public void startNetworkScan(@NetworkQueryType int type) { + if (type == NETWORK_SCAN_TYPE_WAIT_FOR_ALL_RESULTS) { + mNetworkScanFuture = SettableFuture.create(); + Futures.addCallback(mNetworkScanFuture, new FutureCallback<List<CellInfo>>() { + @Override + public void onSuccess(List<CellInfo> result) { + onResults(result); + onComplete(); + } + + @Override + public void onFailure(Throwable t) { + int errCode = Integer.parseInt(t.getMessage()); + onError(errCode); + } + }, MoreExecutors.directExecutor()); + mExecutor.execute(new NetworkScanSyncTask( + mTelephonyManager, (SettableFuture) mNetworkScanFuture)); + } else if (type == NETWORK_SCAN_TYPE_INCREMENTAL_RESULTS) { + mNetworkScanRequester = mTelephonyManager.requestNetworkScan( + NETWORK_SCAN_REQUEST, + mExecutor, + mInternalNetworkScanCallback); + } + } + + /** + * The network scan of type {@link #NETWORK_SCAN_TYPE_WAIT_FOR_ALL_RESULTS} can't be stopped, + * however, the result of the current network scan won't be returned to the callback after + * calling this method. + */ + public void stopNetworkQuery() { + if (mNetworkScanRequester != null) { + mNetworkScanRequester.stopScan(); + mNetworkScanFuture = null; + } + + if (mNetworkScanFuture != null) { + mNetworkScanFuture.cancel(true /* mayInterruptIfRunning */); + mNetworkScanFuture = null; + } + } + + private void onResults(List<CellInfo> cellInfos) { + mNetworkScanCallback.onResults(cellInfos); + } + + private void onComplete() { + mNetworkScanCallback.onComplete(); + } + + private void onError(int errCode) { + mNetworkScanCallback.onError(errCode); + } + + /** + * Converts the status code of {@link CellNetworkScanResult} to one of the + * {@link NetworkScan.ScanErrorCode}. + * @param errCode status code from {@link CellNetworkScanResult}. + * + * @return one of the scan error code from {@link NetworkScan.ScanErrorCode}. + */ + private static int convertToScanErrorCode(int errCode) { + switch (errCode) { + case CellNetworkScanResult.STATUS_RADIO_NOT_AVAILABLE: + return NetworkScan.ERROR_RADIO_INTERFACE_ERROR; + case CellNetworkScanResult.STATUS_RADIO_GENERIC_FAILURE: + default: + return NetworkScan.ERROR_MODEM_ERROR; + } + } + + private final class NetworkScanCallbackImpl extends TelephonyScanManager.NetworkScanCallback { + public void onResults(List<CellInfo> results) { + Log.d(TAG, "Async scan onResults() results = " + + CellInfoUtil.cellInfoListToString(results)); + NetworkScanHelper.this.onResults(results); + } + + public void onComplete() { + Log.d(TAG, "async scan onComplete()"); + NetworkScanHelper.this.onComplete(); + } + + public void onError(@NetworkScan.ScanErrorCode int errCode) { + Log.d(TAG, "async scan onError() errorCode = " + errCode); + NetworkScanHelper.this.onError(errCode); + } + } + + private static final class NetworkScanSyncTask implements Runnable { + private final SettableFuture<List<CellInfo>> mCallback; + private final TelephonyManager mTelephonyManager; + + NetworkScanSyncTask( + TelephonyManager telephonyManager, SettableFuture<List<CellInfo>> callback) { + mTelephonyManager = telephonyManager; + mCallback = callback; + } + + @Override + public void run() { + final CellNetworkScanResult result = mTelephonyManager.getAvailableNetworks(); + if (result.getStatus() == CellNetworkScanResult.STATUS_SUCCESS) { + final List<CellInfo> cellInfos = result.getOperators() + .stream() + .map(operatorInfo + -> CellInfoUtil.convertOperatorInfoToCellInfo(operatorInfo)) + .collect(Collectors.toList()); + Log.d(TAG, "Sync network scan completed, cellInfos = " + + CellInfoUtil.cellInfoListToString(cellInfos)); + mCallback.set(cellInfos); + } else { + final Throwable error = new Throwable( + Integer.toString(convertToScanErrorCode(result.getStatus()))); + mCallback.setException(error); + Log.d(TAG, "Sync network scan error, ex = " + error); + } + } + } +} diff --git a/src/com/android/settings/network/telephony/NetworkSelectSettings.java b/src/com/android/settings/network/telephony/NetworkSelectSettings.java new file mode 100644 index 0000000000..62947d125d --- /dev/null +++ b/src/com/android/settings/network/telephony/NetworkSelectSettings.java @@ -0,0 +1,431 @@ +/* + * Copyright (C) 2018 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.network.telephony; + +import android.app.Activity; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.PersistableBundle; +import android.provider.Settings; +import android.telephony.AccessNetworkConstants; +import android.telephony.CarrierConfigManager; +import android.telephony.CellIdentity; +import android.telephony.CellInfo; +import android.telephony.NetworkRegistrationInfo; +import android.telephony.ServiceState; +import android.telephony.SignalStrength; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.util.Log; +import android.view.View; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; + +import com.android.internal.telephony.OperatorInfo; +import com.android.settings.R; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; +import com.android.settingslib.utils.ThreadUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * "Choose network" settings UI for the Phone app. + */ +public class NetworkSelectSettings extends DashboardFragment { + + private static final String TAG = "NetworkSelectSettings"; + + private static final int EVENT_SET_NETWORK_SELECTION_MANUALLY_DONE = 1; + private static final int EVENT_NETWORK_SCAN_RESULTS = 2; + private static final int EVENT_NETWORK_SCAN_ERROR = 3; + private static final int EVENT_NETWORK_SCAN_COMPLETED = 4; + + private static final String PREF_KEY_CONNECTED_NETWORK_OPERATOR = + "connected_network_operator_preference"; + private static final String PREF_KEY_NETWORK_OPERATORS = "network_operators_preference"; + + @VisibleForTesting + PreferenceCategory mPreferenceCategory; + @VisibleForTesting + PreferenceCategory mConnectedPreferenceCategory; + @VisibleForTesting + NetworkOperatorPreference mSelectedPreference; + private View mProgressHeader; + private Preference mStatusMessagePreference; + @VisibleForTesting + List<CellInfo> mCellInfoList; + private int mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; + @VisibleForTesting + TelephonyManager mTelephonyManager; + private List<String> mForbiddenPlmns; + private boolean mShow4GForLTE = false; + private NetworkScanHelper mNetworkScanHelper; + private final ExecutorService mNetworkScanExecutor = Executors.newFixedThreadPool(1); + private MetricsFeatureProvider mMetricsFeatureProvider; + private boolean mUseNewApi; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + mUseNewApi = getContext().getResources().getBoolean( + com.android.internal.R.bool.config_enableNewAutoSelectNetworkUI); + mSubId = getArguments().getInt(Settings.EXTRA_SUB_ID); + + mConnectedPreferenceCategory = findPreference(PREF_KEY_CONNECTED_NETWORK_OPERATOR); + mPreferenceCategory = findPreference(PREF_KEY_NETWORK_OPERATORS); + mStatusMessagePreference = new Preference(getContext()); + mStatusMessagePreference.setSelectable(false); + mSelectedPreference = null; + mTelephonyManager = TelephonyManager.from(getContext()).createForSubscriptionId(mSubId); + mNetworkScanHelper = new NetworkScanHelper( + mTelephonyManager, mCallback, mNetworkScanExecutor); + PersistableBundle bundle = ((CarrierConfigManager) getContext().getSystemService( + Context.CARRIER_CONFIG_SERVICE)).getConfigForSubId(mSubId); + if (bundle != null) { + mShow4GForLTE = bundle.getBoolean( + CarrierConfigManager.KEY_SHOW_4G_FOR_LTE_DATA_ICON_BOOL); + } + + mMetricsFeatureProvider = FeatureFactory + .getFactory(getContext()).getMetricsFeatureProvider(); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + final Activity activity = getActivity(); + if (activity != null) { + mProgressHeader = setPinnedHeaderView(R.layout.progress_header) + .findViewById(R.id.progress_bar_animation); + setProgressBarVisible(false); + } + forceUpdateConnectedPreferenceCategory(); + } + + @Override + public void onStart() { + super.onStart(); + + updateForbiddenPlmns(); + setProgressBarVisible(true); + + mNetworkScanHelper.startNetworkScan( + mUseNewApi + ? NetworkScanHelper.NETWORK_SCAN_TYPE_INCREMENTAL_RESULTS + : NetworkScanHelper.NETWORK_SCAN_TYPE_WAIT_FOR_ALL_RESULTS); + } + + /** + * Update forbidden PLMNs from the USIM App + */ + @VisibleForTesting + void updateForbiddenPlmns() { + final String[] forbiddenPlmns = mTelephonyManager.getForbiddenPlmns(); + mForbiddenPlmns = forbiddenPlmns != null + ? Arrays.asList(forbiddenPlmns) + : new ArrayList<>(); + } + + @Override + public void onStop() { + super.onStop(); + stopNetworkQuery(); + } + + @Override + public boolean onPreferenceTreeClick(Preference preference) { + if (preference != mSelectedPreference) { + stopNetworkQuery(); + // Refresh the last selected item in case users reselect network. + if (mSelectedPreference != null) { + mSelectedPreference.setSummary(null); + } + + mSelectedPreference = (NetworkOperatorPreference) preference; + CellInfo cellInfo = mSelectedPreference.getCellInfo(); + mSelectedPreference.setSummary(R.string.network_connecting); + + mMetricsFeatureProvider.action(getContext(), + SettingsEnums.ACTION_MOBILE_NETWORK_MANUAL_SELECT_NETWORK); + + // Set summary as "Disconnected" to the previously connected network + if (mConnectedPreferenceCategory.getPreferenceCount() > 0) { + NetworkOperatorPreference connectedNetworkOperator = (NetworkOperatorPreference) + (mConnectedPreferenceCategory.getPreference(0)); + if (!CellInfoUtil.getNetworkTitle(cellInfo).equals( + CellInfoUtil.getNetworkTitle(connectedNetworkOperator.getCellInfo()))) { + connectedNetworkOperator.setSummary(R.string.network_disconnected); + } + } + + setProgressBarVisible(true); + // Disable the screen until network is manually set + getPreferenceScreen().setEnabled(false); + + final OperatorInfo operatorInfo = CellInfoUtil.getOperatorInfoFromCellInfo(cellInfo); + ThreadUtils.postOnBackgroundThread(() -> { + Message msg = mHandler.obtainMessage(EVENT_SET_NETWORK_SELECTION_MANUALLY_DONE); + msg.obj = mTelephonyManager.setNetworkSelectionModeManual( + operatorInfo, true /* persistSelection */); + msg.sendToTarget(); + }); + } + + return true; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.choose_network; + } + + @Override + protected String getLogTag() { + return TAG; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.MOBILE_NETWORK_SELECT; + } + + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case EVENT_SET_NETWORK_SELECTION_MANUALLY_DONE: + setProgressBarVisible(false); + getPreferenceScreen().setEnabled(true); + + boolean isSucceed = (boolean) msg.obj; + mSelectedPreference.setSummary(isSucceed + ? R.string.network_connected + : R.string.network_could_not_connect); + break; + case EVENT_NETWORK_SCAN_RESULTS: + List<CellInfo> results = aggregateCellInfoList((List<CellInfo>) msg.obj); + Log.d(TAG, "CellInfoList after aggregation: " + + CellInfoUtil.cellInfoListToString(results)); + mCellInfoList = new ArrayList<>(results); + if (mCellInfoList != null && mCellInfoList.size() != 0) { + updateAllPreferenceCategory(); + } else { + addMessagePreference(R.string.empty_networks_list); + } + + break; + + case EVENT_NETWORK_SCAN_ERROR: + stopNetworkQuery(); + addMessagePreference(R.string.network_query_error); + break; + + case EVENT_NETWORK_SCAN_COMPLETED: + stopNetworkQuery(); + if (mCellInfoList == null) { + // In case the scan timeout before getting any results + addMessagePreference(R.string.empty_networks_list); + } + break; + } + return; + } + }; + + private final NetworkScanHelper.NetworkScanCallback mCallback = + new NetworkScanHelper.NetworkScanCallback() { + public void onResults(List<CellInfo> results) { + Message msg = mHandler.obtainMessage(EVENT_NETWORK_SCAN_RESULTS, results); + msg.sendToTarget(); + } + + public void onComplete() { + Message msg = mHandler.obtainMessage(EVENT_NETWORK_SCAN_COMPLETED); + msg.sendToTarget(); + } + + public void onError(int error) { + Message msg = mHandler.obtainMessage(EVENT_NETWORK_SCAN_ERROR, error, + 0 /* arg2 */); + msg.sendToTarget(); + } + }; + + /** + * Update the currently available network operators list, which only contains the unregistered + * network operators. So if the device has no data and the network operator in the connected + * network operator category shows "Disconnected", it will also exist in the available network + * operator category for user to select. On the other hand, if the device has data and the + * network operator in the connected network operator category shows "Connected", it will not + * exist in the available network category. + */ + @VisibleForTesting + void updateAllPreferenceCategory() { + updateConnectedPreferenceCategory(); + + mPreferenceCategory.removeAll(); + for (int index = 0; index < mCellInfoList.size(); index++) { + if (!mCellInfoList.get(index).isRegistered()) { + NetworkOperatorPreference pref = new NetworkOperatorPreference( + mCellInfoList.get(index), getPrefContext(), mForbiddenPlmns, mShow4GForLTE); + pref.setKey(CellInfoUtil.getNetworkTitle(mCellInfoList.get(index))); + pref.setOrder(index); + mPreferenceCategory.addPreference(pref); + } + } + } + + /** + * Config the connected network operator preference when the page was created. When user get + * into this page, the device might or might not have data connection. + * - If the device has data: + * 1. use {@code ServiceState#getNetworkRegistrationInfoList()} to get the currently + * registered cellIdentity, wrap it into a CellInfo; + * 2. set the signal strength level as strong; + * 3. use {@link TelephonyManager#getNetworkOperatorName()} to get the title of the + * previously connected network operator, since the CellIdentity got from step 1 only has + * PLMN. + * - If the device has no data, we will remove the connected network operators list from the + * screen. + */ + private void forceUpdateConnectedPreferenceCategory() { + if (mTelephonyManager.getDataState() == mTelephonyManager.DATA_CONNECTED) { + // Try to get the network registration states + ServiceState ss = mTelephonyManager.getServiceState(); + List<NetworkRegistrationInfo> networkList = + ss.getNetworkRegistrationInfoListForTransportType( + AccessNetworkConstants.TRANSPORT_TYPE_WWAN); + if (networkList == null || networkList.size() == 0) { + // Remove the connected network operators category + mConnectedPreferenceCategory.setVisible(false); + return; + } + CellIdentity cellIdentity = networkList.get(0).getCellIdentity(); + CellInfo cellInfo = CellInfoUtil.wrapCellInfoWithCellIdentity(cellIdentity); + if (cellInfo != null) { + NetworkOperatorPreference pref = new NetworkOperatorPreference( + cellInfo, getPrefContext(), mForbiddenPlmns, mShow4GForLTE); + pref.setTitle(mTelephonyManager.getNetworkOperatorName()); + pref.setSummary(R.string.network_connected); + // Update the signal strength icon, since the default signalStrength value would be + // zero (it would be quite confusing why the connected network has no signal) + pref.setIcon(SignalStrength.NUM_SIGNAL_STRENGTH_BINS - 1); + mConnectedPreferenceCategory.addPreference(pref); + } else { + // Remove the connected network operators category + mConnectedPreferenceCategory.setVisible(false); + } + } else { + // Remove the connected network operators category + mConnectedPreferenceCategory.setVisible(false); + } + } + + /** + * Configure the ConnectedNetworkOperatorsPreferenceCategory. The category only need to be + * configured if the category is currently empty or the operator network title of the previous + * connected network is different from the new one. + */ + private void updateConnectedPreferenceCategory() { + CellInfo connectedNetworkOperator = null; + for (CellInfo cellInfo : mCellInfoList) { + if (cellInfo.isRegistered()) { + connectedNetworkOperator = cellInfo; + break; + } + } + + if (connectedNetworkOperator != null) { + addConnectedNetworkOperatorPreference(connectedNetworkOperator); + } + } + + private void addConnectedNetworkOperatorPreference(CellInfo cellInfo) { + mConnectedPreferenceCategory.removeAll(); + final NetworkOperatorPreference pref = new NetworkOperatorPreference( + cellInfo, getPrefContext(), mForbiddenPlmns, mShow4GForLTE); + pref.setSummary(R.string.network_connected); + mConnectedPreferenceCategory.addPreference(pref); + mConnectedPreferenceCategory.setVisible(true); + } + + protected void setProgressBarVisible(boolean visible) { + if (mProgressHeader != null) { + mProgressHeader.setVisibility(visible ? View.VISIBLE : View.GONE); + } + } + + private void addMessagePreference(int messageId) { + setProgressBarVisible(false); + mStatusMessagePreference.setTitle(messageId); + mConnectedPreferenceCategory.setVisible(false); + mPreferenceCategory.removeAll(); + mPreferenceCategory.addPreference(mStatusMessagePreference); + } + + /** + * The Scan results may contains several cell infos with different radio technologies and signal + * strength for one network operator. Aggregate the CellInfoList by retaining only the cell info + * with the strongest signal strength. + */ + private List<CellInfo> aggregateCellInfoList(List<CellInfo> cellInfoList) { + Map<String, CellInfo> map = new HashMap<>(); + for (CellInfo cellInfo : cellInfoList) { + String plmn = CellInfoUtil.getOperatorInfoFromCellInfo(cellInfo).getOperatorNumeric(); + if (cellInfo.isRegistered() || !map.containsKey(plmn)) { + map.put(plmn, cellInfo); + } else { + if (map.get(plmn).isRegistered() + || map.get(plmn).getCellSignalStrength().getLevel() + > cellInfo.getCellSignalStrength().getLevel()) { + // Skip if the stored cellInfo is registered or has higher signal strength level + continue; + } + // Otherwise replace it with the new CellInfo + map.put(plmn, cellInfo); + } + } + return new ArrayList<>(map.values()); + } + + private void stopNetworkQuery() { + setProgressBarVisible(false); + if (mNetworkScanHelper != null) { + mNetworkScanHelper.stopNetworkQuery(); + } + } + + @Override + public void onDestroy() { + mNetworkScanExecutor.shutdown(); + super.onDestroy(); + } +} diff --git a/src/com/android/settings/network/telephony/PreferredNetworkModePreferenceController.java b/src/com/android/settings/network/telephony/PreferredNetworkModePreferenceController.java new file mode 100644 index 0000000000..294f05f2ae --- /dev/null +++ b/src/com/android/settings/network/telephony/PreferredNetworkModePreferenceController.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2018 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.network.telephony; + +import android.content.Context; +import android.os.PersistableBundle; +import android.provider.Settings; +import android.telephony.CarrierConfigManager; +import android.telephony.ServiceState; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; + +import androidx.preference.ListPreference; +import androidx.preference.Preference; + +import com.android.internal.telephony.Phone; +import com.android.internal.telephony.PhoneConstants; +import com.android.settings.R; + +/** + * Preference controller for "Preferred network mode" + */ +public class PreferredNetworkModePreferenceController extends TelephonyBasePreferenceController + implements ListPreference.OnPreferenceChangeListener { + + private CarrierConfigManager mCarrierConfigManager; + private TelephonyManager mTelephonyManager; + private PersistableBundle mPersistableBundle; + private boolean mIsGlobalCdma; + + public PreferredNetworkModePreferenceController(Context context, String key) { + super(context, key); + mCarrierConfigManager = context.getSystemService(CarrierConfigManager.class); + } + + @Override + public int getAvailabilityStatus(int subId) { + final PersistableBundle carrierConfig = mCarrierConfigManager.getConfigForSubId(subId); + final TelephonyManager telephonyManager = TelephonyManager + .from(mContext).createForSubscriptionId(subId); + boolean visible; + if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + visible = false; + } else if (carrierConfig == null) { + visible = false; + } else if (carrierConfig.getBoolean( + CarrierConfigManager.KEY_HIDE_CARRIER_NETWORK_SETTINGS_BOOL)) { + visible = false; + } else if (carrierConfig.getBoolean( + CarrierConfigManager.KEY_HIDE_PREFERRED_NETWORK_TYPE_BOOL) + && !telephonyManager.getServiceState().getRoaming() + && telephonyManager.getServiceState().getDataRegState() + == ServiceState.STATE_IN_SERVICE) { + visible = false; + } else if (carrierConfig.getBoolean(CarrierConfigManager.KEY_WORLD_PHONE_BOOL)) { + visible = true; + } else { + visible = false; + } + + return visible ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + final ListPreference listPreference = (ListPreference) preference; + final int networkMode = getPreferredNetworkMode(); + listPreference.setValue(Integer.toString(networkMode)); + listPreference.setSummary(getPreferredNetworkModeSummaryResId(networkMode)); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object object) { + final int settingsMode = Integer.parseInt((String) object); + + if (mTelephonyManager.setPreferredNetworkType(mSubId, settingsMode)) { + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.PREFERRED_NETWORK_MODE + mSubId, + settingsMode); + return true; + } + + return false; + } + + public void init(int subId) { + mSubId = subId; + final PersistableBundle carrierConfig = mCarrierConfigManager.getConfigForSubId(mSubId); + mTelephonyManager = TelephonyManager.from(mContext).createForSubscriptionId(mSubId); + + final boolean isLteOnCdma = + mTelephonyManager.getLteOnCdmaMode() == PhoneConstants.LTE_ON_CDMA_TRUE; + mIsGlobalCdma = isLteOnCdma + && carrierConfig.getBoolean(CarrierConfigManager.KEY_SHOW_CDMA_CHOICES_BOOL); + } + + private int getPreferredNetworkMode() { + return Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.PREFERRED_NETWORK_MODE + mSubId, + Phone.PREFERRED_NT_MODE); + } + + private int getPreferredNetworkModeSummaryResId(int NetworkMode) { + switch (NetworkMode) { + case TelephonyManager.NETWORK_MODE_TDSCDMA_GSM_WCDMA: + return R.string.preferred_network_mode_tdscdma_gsm_wcdma_summary; + case TelephonyManager.NETWORK_MODE_TDSCDMA_GSM: + return R.string.preferred_network_mode_tdscdma_gsm_summary; + case TelephonyManager.NETWORK_MODE_WCDMA_PREF: + return R.string.preferred_network_mode_wcdma_perf_summary; + case TelephonyManager.NETWORK_MODE_GSM_ONLY: + return R.string.preferred_network_mode_gsm_only_summary; + case TelephonyManager.NETWORK_MODE_TDSCDMA_WCDMA: + return R.string.preferred_network_mode_tdscdma_wcdma_summary; + case TelephonyManager.NETWORK_MODE_WCDMA_ONLY: + return R.string.preferred_network_mode_wcdma_only_summary; + case TelephonyManager.NETWORK_MODE_GSM_UMTS: + return R.string.preferred_network_mode_gsm_wcdma_summary; + case TelephonyManager.NETWORK_MODE_CDMA_EVDO: + switch (mTelephonyManager.getLteOnCdmaMode()) { + case PhoneConstants.LTE_ON_CDMA_TRUE: + return R.string.preferred_network_mode_cdma_summary; + default: + return R.string.preferred_network_mode_cdma_evdo_summary; + } + case TelephonyManager.NETWORK_MODE_CDMA_NO_EVDO: + return R.string.preferred_network_mode_cdma_only_summary; + case TelephonyManager.NETWORK_MODE_EVDO_NO_CDMA: + return R.string.preferred_network_mode_evdo_only_summary; + case TelephonyManager.NETWORK_MODE_LTE_TDSCDMA: + return R.string.preferred_network_mode_lte_tdscdma_summary; + case TelephonyManager.NETWORK_MODE_LTE_ONLY: + return R.string.preferred_network_mode_lte_summary; + case TelephonyManager.NETWORK_MODE_LTE_TDSCDMA_GSM: + return R.string.preferred_network_mode_lte_tdscdma_gsm_summary; + case TelephonyManager.NETWORK_MODE_LTE_TDSCDMA_GSM_WCDMA: + return R.string.preferred_network_mode_lte_tdscdma_gsm_wcdma_summary; + case TelephonyManager.NETWORK_MODE_LTE_GSM_WCDMA: + return R.string.preferred_network_mode_lte_gsm_wcdma_summary; + case TelephonyManager.NETWORK_MODE_LTE_CDMA_EVDO: + return R.string.preferred_network_mode_lte_cdma_evdo_summary; + case TelephonyManager.NETWORK_MODE_TDSCDMA_ONLY: + return R.string.preferred_network_mode_tdscdma_summary; + case TelephonyManager.NETWORK_MODE_LTE_TDSCDMA_CDMA_EVDO_GSM_WCDMA: + return R.string.preferred_network_mode_lte_tdscdma_cdma_evdo_gsm_wcdma_summary; + case TelephonyManager.NETWORK_MODE_LTE_CDMA_EVDO_GSM_WCDMA: + if (mTelephonyManager.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA + || mIsGlobalCdma + || MobileNetworkUtils.isWorldMode(mContext, mSubId)) { + return R.string.preferred_network_mode_global_summary; + } else { + return R.string.preferred_network_mode_lte_summary; + } + case TelephonyManager.NETWORK_MODE_TDSCDMA_CDMA_EVDO_GSM_WCDMA: + return R.string.preferred_network_mode_tdscdma_cdma_evdo_gsm_wcdma_summary; + case TelephonyManager.NETWORK_MODE_GLOBAL: + return R.string.preferred_network_mode_cdma_evdo_gsm_wcdma_summary; + case TelephonyManager.NETWORK_MODE_LTE_TDSCDMA_WCDMA: + return R.string.preferred_network_mode_lte_tdscdma_wcdma_summary; + case TelephonyManager.NETWORK_MODE_LTE_WCDMA: + return R.string.preferred_network_mode_lte_wcdma_summary; + default: + return R.string.preferred_network_mode_global_summary; + } + } +} diff --git a/src/com/android/settings/network/telephony/RenameMobileNetworkDialogFragment.java b/src/com/android/settings/network/telephony/RenameMobileNetworkDialogFragment.java new file mode 100644 index 0000000000..a28fc91e43 --- /dev/null +++ b/src/com/android/settings/network/telephony/RenameMobileNetworkDialogFragment.java @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2019 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.network.telephony; + +import android.app.Dialog; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Paint; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.OvalShape; +import android.os.Bundle; +import android.telephony.ServiceState; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.text.BidiFormatter; +import android.text.TextDirectionHeuristics; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.Spinner; +import android.widget.TextView; + +import com.android.settings.R; +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; +import com.android.settingslib.DeviceInfoUtils; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; + +/** + * A dialog allowing the display name of a mobile network subscription to be changed + */ +public class RenameMobileNetworkDialogFragment extends InstrumentedDialogFragment { + + public static final String TAG = "RenameMobileNetwork"; + + private static final String KEY_SUBSCRIPTION_ID = "subscription_id"; + + private TelephonyManager mTelephonyManager; + private SubscriptionManager mSubscriptionManager; + private int mSubId; + private EditText mNameView; + private Spinner mColorSpinner; + private Color[] mColors; + + public static RenameMobileNetworkDialogFragment newInstance(int subscriptionId) { + final Bundle args = new Bundle(1); + args.putInt(KEY_SUBSCRIPTION_ID, subscriptionId); + final RenameMobileNetworkDialogFragment fragment = new RenameMobileNetworkDialogFragment(); + fragment.setArguments(args); + return fragment; + } + + @VisibleForTesting + protected TelephonyManager getTelephonyManager(Context context) { + return context.getSystemService(TelephonyManager.class); + } + + @VisibleForTesting + protected SubscriptionManager getSubscriptionManager(Context context) { + return context.getSystemService(SubscriptionManager.class); + } + + @VisibleForTesting + protected EditText getNameView() { + return mNameView; + } + + @VisibleForTesting + protected Spinner getColorSpinnerView() { + return mColorSpinner; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + mTelephonyManager = getTelephonyManager(context); + mSubscriptionManager = getSubscriptionManager(context); + mSubId = getArguments().getInt(KEY_SUBSCRIPTION_ID); + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + mColors = getColors(); + + final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + final LayoutInflater layoutInflater = builder.getContext().getSystemService( + LayoutInflater.class); + final View view = layoutInflater.inflate(R.layout.dialog_mobile_network_rename, null); + populateView(view); + builder.setTitle(R.string.mobile_network_sim_name) + .setView(view) + .setPositiveButton(R.string.mobile_network_sim_name_rename, (dialog, which) -> { + mSubscriptionManager.setDisplayName(mNameView.getText().toString(), mSubId, + SubscriptionManager.NAME_SOURCE_USER_INPUT); + mSubscriptionManager.setIconTint( + mColors[mColorSpinner.getSelectedItemPosition()].getColor(), + mSubId); + }) + .setNegativeButton(android.R.string.cancel, null); + return builder.create(); + } + + @VisibleForTesting + protected void populateView(View view) { + mNameView = view.findViewById(R.id.name_edittext); + final SubscriptionInfo info = mSubscriptionManager.getActiveSubscriptionInfo(mSubId); + if (info == null) { + Log.w(TAG, "got null SubscriptionInfo for mSubId:" + mSubId); + return; + } + final CharSequence displayName = info.getDisplayName(); + mNameView.setText(displayName); + if (!TextUtils.isEmpty(displayName)) { + mNameView.setSelection(displayName.length()); + } + + mColorSpinner = view.findViewById(R.id.color_spinner); + final ColorAdapter adapter = new ColorAdapter(getContext(), + R.layout.dialog_mobile_network_color_picker_item, mColors); + mColorSpinner.setAdapter(adapter); + for (int i = 0; i < mColors.length; i++) { + if (mColors[i].getColor() == info.getIconTint()) { + mColorSpinner.setSelection(i); + break; + } + } + + final TextView operatorName = view.findViewById(R.id.operator_name_value); + final ServiceState serviceState = mTelephonyManager.getServiceStateForSubscriber(mSubId); + operatorName.setText(serviceState.getOperatorAlphaLong()); + + final TextView phoneTitle = view.findViewById(R.id.number_label); + phoneTitle.setVisibility(info.isOpportunistic() ? View.GONE : View.VISIBLE); + + final TextView phoneNumber = view.findViewById(R.id.number_value); + final String formattedNumber = DeviceInfoUtils.getFormattedPhoneNumber(getContext(), info); + phoneNumber.setText(BidiFormatter.getInstance().unicodeWrap(formattedNumber, + TextDirectionHeuristics.LTR)); + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.MOBILE_NETWORK_RENAME_DIALOG; + } + + private class ColorAdapter extends ArrayAdapter<Color> { + + private Context mContext; + private int mItemResId; + + public ColorAdapter(Context context, int resource, Color[] colors) { + super(context, resource, colors); + mContext = context; + mItemResId = resource; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final LayoutInflater inflater = (LayoutInflater) + mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + if (convertView == null) { + convertView = inflater.inflate(mItemResId, null); + } + ((ImageView) convertView.findViewById(R.id.color_icon)) + .setImageDrawable(getItem(position).getDrawable()); + ((TextView) convertView.findViewById(R.id.color_label)) + .setText(getItem(position).getLabel()); + + return convertView; + } + + @Override + public View getDropDownView(int position, View convertView, ViewGroup parent) { + return getView(position, convertView, parent); + } + } + + private Color[] getColors() { + final Resources res = getContext().getResources(); + final int[] colorInts = res.getIntArray(com.android.internal.R.array.sim_colors); + final String[] colorStrings = res.getStringArray(R.array.color_picker); + final int iconSize = res.getDimensionPixelSize(R.dimen.color_swatch_size); + final int strokeWidth = res.getDimensionPixelSize(R.dimen.color_swatch_stroke_width); + final Color[] colors = new Color[colorInts.length]; + for (int i = 0; i < colors.length; i++) { + colors[i] = new Color(colorStrings[i], colorInts[i], iconSize, strokeWidth); + } + return colors; + } + + private static class Color { + + private String mLabel; + private int mColor; + private ShapeDrawable mDrawable; + + private Color(String label, int color, int iconSize, int strokeWidth) { + mLabel = label; + mColor = color; + mDrawable = new ShapeDrawable(new OvalShape()); + mDrawable.setIntrinsicHeight(iconSize); + mDrawable.setIntrinsicWidth(iconSize); + mDrawable.getPaint().setStrokeWidth(strokeWidth); + mDrawable.getPaint().setStyle(Paint.Style.FILL_AND_STROKE); + mDrawable.getPaint().setColor(color); + } + + private String getLabel() { + return mLabel; + } + + private int getColor() { + return mColor; + } + + private ShapeDrawable getDrawable() { + return mDrawable; + } + } +} diff --git a/src/com/android/settings/network/telephony/RoamingDialogFragment.java b/src/com/android/settings/network/telephony/RoamingDialogFragment.java new file mode 100644 index 0000000000..1298445c87 --- /dev/null +++ b/src/com/android/settings/network/telephony/RoamingDialogFragment.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2018 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.network.telephony; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.os.Bundle; +import android.os.PersistableBundle; +import android.telephony.CarrierConfigManager; +import android.telephony.TelephonyManager; + +import com.android.settings.R; +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; + +/** + * A dialog fragment that asks the user if they are sure they want to turn on data roaming + * to avoid accidental charges. + */ +public class RoamingDialogFragment extends InstrumentedDialogFragment implements OnClickListener { + + public static final String SUB_ID_KEY = "sub_id_key"; + + private CarrierConfigManager mCarrierConfigManager; + private int mSubId; + + public static RoamingDialogFragment newInstance(int subId) { + final RoamingDialogFragment dialogFragment = new RoamingDialogFragment(); + Bundle args = new Bundle(); + args.putInt(SUB_ID_KEY, subId); + dialogFragment.setArguments(args); + + return dialogFragment; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + Bundle args = getArguments(); + mSubId = args.getInt(SUB_ID_KEY); + mCarrierConfigManager = new CarrierConfigManager(context); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); + int title = R.string.roaming_alert_title; + int message = R.string.roaming_warning; + PersistableBundle carrierConfig = mCarrierConfigManager.getConfigForSubId(mSubId); + if (carrierConfig != null && carrierConfig.getBoolean( + CarrierConfigManager.KEY_CHECK_PRICING_WITH_CARRIER_FOR_DATA_ROAMING_BOOL)) { + message = R.string.roaming_check_price_warning; + } + builder.setMessage(getResources().getString(message)) + .setTitle(title) + .setIconAttribute(android.R.attr.alertDialogIcon) + .setPositiveButton(android.R.string.yes, this) + .setNegativeButton(android.R.string.no, this); + return builder.create(); + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.MOBILE_ROAMING_DIALOG; + } + + @Override + public void onClick(DialogInterface dialog, int which) { + // let the host know that the positive button has been clicked + if (which == dialog.BUTTON_POSITIVE) { + TelephonyManager.from(getContext()).createForSubscriptionId( + mSubId).setDataRoamingEnabled(true); + } + } +} diff --git a/src/com/android/settings/network/telephony/RoamingPreferenceController.java b/src/com/android/settings/network/telephony/RoamingPreferenceController.java new file mode 100644 index 0000000000..dd5fd0e8fe --- /dev/null +++ b/src/com/android/settings/network/telephony/RoamingPreferenceController.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2018 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.network.telephony; + +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.os.PersistableBundle; +import android.provider.Settings; +import android.telephony.CarrierConfigManager; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.text.TextUtils; + +import androidx.annotation.VisibleForTesting; +import androidx.fragment.app.FragmentManager; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settingslib.RestrictedSwitchPreference; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.core.lifecycle.events.OnStop; + +/** + * Preference controller for "Roaming" + */ +public class RoamingPreferenceController extends TelephonyTogglePreferenceController implements + LifecycleObserver, OnStart, OnStop { + + private static final String DIALOG_TAG = "MobileDataDialog"; + + private RestrictedSwitchPreference mSwitchPreference; + private TelephonyManager mTelephonyManager; + private CarrierConfigManager mCarrierConfigManager; + private DataContentObserver mDataContentObserver; + @VisibleForTesting + boolean mNeedDialog; + @VisibleForTesting + FragmentManager mFragmentManager; + + public RoamingPreferenceController(Context context, String key) { + super(context, key); + mCarrierConfigManager = context.getSystemService(CarrierConfigManager.class); + mDataContentObserver = new DataContentObserver(new Handler(Looper.getMainLooper())); + } + + @Override + public void onStart() { + mDataContentObserver.register(mContext, mSubId); + } + + @Override + public void onStop() { + mDataContentObserver.unRegister(mContext); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mSwitchPreference = screen.findPreference(getPreferenceKey()); + } + + @Override + public int getAvailabilityStatus(int subId) { + return subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID + ? AVAILABLE + : AVAILABLE_UNSEARCHABLE; + } + + @Override + public boolean handlePreferenceTreeClick(Preference preference) { + if (TextUtils.equals(preference.getKey(), getPreferenceKey())) { + if (mNeedDialog) { + showDialog(); + } + return true; + } + + return false; + } + + @Override + public boolean setChecked(boolean isChecked) { + mNeedDialog = isDialogNeeded(); + + if (!mNeedDialog) { + // Update data directly if we don't need dialog + mTelephonyManager.setDataRoamingEnabled(isChecked); + return true; + } + + return false; + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + final RestrictedSwitchPreference switchPreference = (RestrictedSwitchPreference) preference; + if (!switchPreference.isDisabledByAdmin()) { + switchPreference.setEnabled(mSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID); + switchPreference.setChecked(isChecked()); + } + } + + @VisibleForTesting + boolean isDialogNeeded() { + final boolean isRoamingEnabled = mTelephonyManager.isDataRoamingEnabled(); + final PersistableBundle carrierConfig = mCarrierConfigManager.getConfigForSubId( + mSubId); + + // Need dialog if we need to turn on roaming and the roaming charge indication is allowed + if (!isRoamingEnabled && (carrierConfig == null || !carrierConfig.getBoolean( + CarrierConfigManager.KEY_DISABLE_CHARGE_INDICATION_BOOL))) { + return true; + } + return false; + } + + @Override + public boolean isChecked() { + return mTelephonyManager.isDataRoamingEnabled(); + } + + public void init(FragmentManager fragmentManager, int subId) { + mFragmentManager = fragmentManager; + mSubId = subId; + mTelephonyManager = TelephonyManager.from(mContext).createForSubscriptionId(mSubId); + } + + private void showDialog() { + final RoamingDialogFragment dialogFragment = RoamingDialogFragment.newInstance(mSubId); + + dialogFragment.show(mFragmentManager, DIALOG_TAG); + } + + /** + * Listener that listens data roaming change + */ + public class DataContentObserver extends ContentObserver { + + public DataContentObserver(Handler handler) { + super(handler); + } + + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + updateState(mSwitchPreference); + } + + public void register(Context context, int subId) { + Uri uri = Settings.Global.getUriFor(Settings.Global.DATA_ROAMING); + if (TelephonyManager.getDefault().getSimCount() != 1) { + uri = Settings.Global.getUriFor(Settings.Global.DATA_ROAMING + subId); + } + context.getContentResolver().registerContentObserver(uri, false, this); + + } + + public void unRegister(Context context) { + context.getContentResolver().unregisterContentObserver(this); + } + } +} diff --git a/src/com/android/settings/network/telephony/SignalStrengthListener.java b/src/com/android/settings/network/telephony/SignalStrengthListener.java new file mode 100644 index 0000000000..0e29a45fb5 --- /dev/null +++ b/src/com/android/settings/network/telephony/SignalStrengthListener.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2019 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.network.telephony; + +import android.content.Context; +import android.telephony.PhoneStateListener; +import android.telephony.SignalStrength; +import android.telephony.TelephonyManager; +import android.util.ArraySet; + +import com.google.common.collect.Sets; + +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +/** Helper class to manage listening to signal strength changes on a set of mobile network + * subscriptions */ +public class SignalStrengthListener { + + private TelephonyManager mBaseTelephonyManager; + private Callback mCallback; + private Map<Integer, PhoneStateListener> mListeners; + + public interface Callback { + void onSignalStrengthChanged(); + } + + public SignalStrengthListener(Context context, Callback callback) { + mBaseTelephonyManager = context.getSystemService(TelephonyManager.class); + mCallback = callback; + mListeners = new TreeMap<>(); + } + + /** Resumes listening for signal strength changes for the set of ids from the last call to + * {@link #updateSubscriptionIds(Set)} */ + public void resume() { + for (int subId : mListeners.keySet()) { + startListening(subId); + } + } + + /** Pauses listening for signal strength changes */ + public void pause() { + for (int subId : mListeners.keySet()) { + stopListening(subId); + } + } + + /** Updates the set of ids we want to be listening for, beginning to listen for any new ids and + * stopping listening for any ids not contained in the new set */ + public void updateSubscriptionIds(Set<Integer> ids) { + Set<Integer> currentIds = new ArraySet<>(mListeners.keySet()); + for (int idToRemove : Sets.difference(currentIds, ids)) { + stopListening(idToRemove); + mListeners.remove(idToRemove); + } + for (int idToAdd : Sets.difference(ids, currentIds)) { + PhoneStateListener listener = new PhoneStateListener() { + @Override + public void onSignalStrengthsChanged(SignalStrength signalStrength) { + mCallback.onSignalStrengthChanged(); + } + }; + mListeners.put(idToAdd, listener); + startListening(idToAdd); + } + } + + private void startListening(int subId) { + TelephonyManager mgr = mBaseTelephonyManager.createForSubscriptionId(subId); + mgr.listen(mListeners.get(subId), PhoneStateListener.LISTEN_SIGNAL_STRENGTHS); + } + + private void stopListening(int subId) { + TelephonyManager mgr = mBaseTelephonyManager.createForSubscriptionId(subId); + mgr.listen(mListeners.get(subId), PhoneStateListener.LISTEN_NONE); + } +} diff --git a/src/com/android/settings/network/telephony/SmsDefaultSubscriptionController.java b/src/com/android/settings/network/telephony/SmsDefaultSubscriptionController.java new file mode 100644 index 0000000000..b9992199ff --- /dev/null +++ b/src/com/android/settings/network/telephony/SmsDefaultSubscriptionController.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2019 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.network.telephony; + +import android.content.Context; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; + +public class SmsDefaultSubscriptionController extends DefaultSubscriptionController { + + public SmsDefaultSubscriptionController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + protected SubscriptionInfo getDefaultSubscriptionInfo() { + return mManager.getDefaultSmsSubscriptionInfo(); + } + + @Override + protected int getDefaultSubscriptionId() { + return SubscriptionManager.getDefaultSmsSubscriptionId(); + } + + @Override + protected void setDefaultSubscription(int subscriptionId) { + mManager.setDefaultSmsSubId(subscriptionId); + } +} diff --git a/src/com/android/settings/network/telephony/TelephonyAvailabilityCallback.java b/src/com/android/settings/network/telephony/TelephonyAvailabilityCallback.java new file mode 100644 index 0000000000..d60bccdbc1 --- /dev/null +++ b/src/com/android/settings/network/telephony/TelephonyAvailabilityCallback.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2019 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.network.telephony; + +/** + * Callback to decide whether preference is available based on subscription id + */ +public interface TelephonyAvailabilityCallback { + /** + * Return availability status for a specific subId + * + * @see TelephonyBasePreferenceController + * @see TelephonyTogglePreferenceController + */ + int getAvailabilityStatus(int subId); +} diff --git a/src/com/android/settings/network/telephony/TelephonyBasePreferenceController.java b/src/com/android/settings/network/telephony/TelephonyBasePreferenceController.java new file mode 100644 index 0000000000..e4ff5c4c78 --- /dev/null +++ b/src/com/android/settings/network/telephony/TelephonyBasePreferenceController.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2019 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.network.telephony; + +import android.content.Context; +import android.telephony.SubscriptionManager; + +import com.android.settings.core.BasePreferenceController; + +/** + * {@link BasePreferenceController} that used by all preferences that requires subscription id. + */ +public abstract class TelephonyBasePreferenceController extends BasePreferenceController + implements TelephonyAvailabilityCallback { + protected int mSubId; + + public TelephonyBasePreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; + } + + @Override + public int getAvailabilityStatus() { + return MobileNetworkUtils.getAvailability(mContext, mSubId, this::getAvailabilityStatus); + } +} diff --git a/src/com/android/settings/network/telephony/TelephonyTogglePreferenceController.java b/src/com/android/settings/network/telephony/TelephonyTogglePreferenceController.java new file mode 100644 index 0000000000..71efc57fb5 --- /dev/null +++ b/src/com/android/settings/network/telephony/TelephonyTogglePreferenceController.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2019 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.network.telephony; + +import android.content.Context; +import android.telephony.SubscriptionManager; + +import com.android.settings.core.TogglePreferenceController; + +/** + * {@link TogglePreferenceController} that used by all preferences that requires subscription id. + */ +public abstract class TelephonyTogglePreferenceController extends TogglePreferenceController + implements TelephonyAvailabilityCallback { + protected int mSubId; + + public TelephonyTogglePreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; + } + + @Override + public int getAvailabilityStatus() { + return MobileNetworkUtils.getAvailability(mContext, mSubId, this::getAvailabilityStatus); + } +} diff --git a/src/com/android/settings/network/telephony/VideoCallingPreferenceController.java b/src/com/android/settings/network/telephony/VideoCallingPreferenceController.java new file mode 100644 index 0000000000..1c78863815 --- /dev/null +++ b/src/com/android/settings/network/telephony/VideoCallingPreferenceController.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2018 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.network.telephony; + +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.os.PersistableBundle; +import android.provider.Settings; +import android.telephony.CarrierConfigManager; +import android.telephony.PhoneStateListener; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreference; + +import com.android.ims.ImsManager; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.core.lifecycle.events.OnStop; + +/** + * Preference controller for "Video Calling" + */ +public class VideoCallingPreferenceController extends TelephonyTogglePreferenceController implements + LifecycleObserver, OnStart, OnStop, + Enhanced4gLtePreferenceController.On4gLteUpdateListener { + + private Preference mPreference; + private TelephonyManager mTelephonyManager; + private CarrierConfigManager mCarrierConfigManager; + @VisibleForTesting + ImsManager mImsManager; + private PhoneCallStateListener mPhoneStateListener; + private DataContentObserver mDataContentObserver; + + public VideoCallingPreferenceController(Context context, String key) { + super(context, key); + mCarrierConfigManager = context.getSystemService(CarrierConfigManager.class); + mDataContentObserver = new DataContentObserver(new Handler(Looper.getMainLooper())); + mPhoneStateListener = new PhoneCallStateListener(Looper.getMainLooper()); + } + + @Override + public int getAvailabilityStatus(int subId) { + return subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID + && MobileNetworkUtils.isWifiCallingEnabled(mContext, subId) + && isVideoCallEnabled(subId) + ? AVAILABLE + : CONDITIONALLY_UNAVAILABLE; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + } + + @Override + public void onStart() { + mPhoneStateListener.register(mSubId); + mDataContentObserver.register(mContext, mSubId); + } + + @Override + public void onStop() { + mPhoneStateListener.unregister(); + mDataContentObserver.unRegister(mContext); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + final SwitchPreference switchPreference = (SwitchPreference) preference; + final boolean videoCallEnabled = isVideoCallEnabled(mSubId, mImsManager); + switchPreference.setVisible(videoCallEnabled); + if (videoCallEnabled) { + final boolean is4gLteEnabled = mImsManager.isEnhanced4gLteModeSettingEnabledByUser() + && mImsManager.isNonTtyOrTtyOnVolteEnabled(); + preference.setEnabled(is4gLteEnabled && + mTelephonyManager.getCallState(mSubId) == TelephonyManager.CALL_STATE_IDLE); + switchPreference.setChecked(is4gLteEnabled && mImsManager.isVtEnabledByUser()); + } + } + + @Override + public boolean setChecked(boolean isChecked) { + mImsManager.setVtSetting(isChecked); + return true; + } + + @Override + public boolean isChecked() { + return mImsManager.isVtEnabledByUser(); + } + + public VideoCallingPreferenceController init(int subId) { + mSubId = subId; + mTelephonyManager = TelephonyManager.from(mContext).createForSubscriptionId(mSubId); + if (mSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + mImsManager = ImsManager.getInstance(mContext, SubscriptionManager.getPhoneId(mSubId)); + } + + return this; + } + + private boolean isVideoCallEnabled(int subId) { + final ImsManager imsManager = subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID + ? ImsManager.getInstance(mContext, SubscriptionManager.getPhoneId(subId)) + : null; + return isVideoCallEnabled(subId, imsManager); + } + + @VisibleForTesting + boolean isVideoCallEnabled(int subId, ImsManager imsManager) { + final PersistableBundle carrierConfig = mCarrierConfigManager.getConfigForSubId(subId); + final TelephonyManager telephonyManager = TelephonyManager + .from(mContext).createForSubscriptionId(subId); + return carrierConfig != null && imsManager != null + && imsManager.isVtEnabledByPlatform() + && imsManager.isVtProvisionedOnDevice() + && MobileNetworkUtils.isImsServiceStateReady(imsManager) + && (carrierConfig.getBoolean( + CarrierConfigManager.KEY_IGNORE_DATA_ENABLED_CHANGED_FOR_VIDEO_CALLS) + || telephonyManager.isDataEnabled()); + } + + @Override + public void on4gLteUpdated() { + updateState(mPreference); + } + + private class PhoneCallStateListener extends PhoneStateListener { + + public PhoneCallStateListener(Looper looper) { + super(looper); + } + + @Override + public void onCallStateChanged(int state, String incomingNumber) { + updateState(mPreference); + } + + public void register(int subId) { + mSubId = subId; + mTelephonyManager.listen(this, PhoneStateListener.LISTEN_CALL_STATE); + } + + public void unregister() { + mTelephonyManager.listen(this, PhoneStateListener.LISTEN_NONE); + } + } + + /** + * Listener that listens mobile data state change. + */ + public class DataContentObserver extends ContentObserver { + + public DataContentObserver(Handler handler) { + super(handler); + } + + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + updateState(mPreference); + } + + public void register(Context context, int subId) { + Uri uri = Settings.Global.getUriFor(Settings.Global.MOBILE_DATA); + if (TelephonyManager.getDefault().getSimCount() != 1) { + uri = Settings.Global.getUriFor(Settings.Global.MOBILE_DATA + subId); + } + context.getContentResolver().registerContentObserver(uri, + false /* notifyForDescendants */, this /* observer */); + } + + public void unRegister(Context context) { + context.getContentResolver().unregisterContentObserver(this); + } + } +} diff --git a/src/com/android/settings/network/telephony/WifiCallingPreferenceController.java b/src/com/android/settings/network/telephony/WifiCallingPreferenceController.java new file mode 100644 index 0000000000..4d4d3ef7f0 --- /dev/null +++ b/src/com/android/settings/network/telephony/WifiCallingPreferenceController.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2018 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.network.telephony; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.Looper; +import android.os.PersistableBundle; +import android.provider.Settings; +import android.telecom.PhoneAccountHandle; +import android.telecom.TelecomManager; +import android.telephony.CarrierConfigManager; +import android.telephony.PhoneStateListener; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.ims.ImsConfig; +import com.android.ims.ImsManager; +import com.android.settings.R; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.core.lifecycle.events.OnStop; + +import java.util.List; + +/** + * Preference controller for "Wifi Calling" + */ +public class WifiCallingPreferenceController extends TelephonyBasePreferenceController implements + LifecycleObserver, OnStart, OnStop { + + @VisibleForTesting + static final String KEY_PREFERENCE_CATEGORY = "calling_category"; + + private TelephonyManager mTelephonyManager; + @VisibleForTesting + CarrierConfigManager mCarrierConfigManager; + @VisibleForTesting + ImsManager mImsManager; + @VisibleForTesting + PhoneAccountHandle mSimCallManager; + private PhoneCallStateListener mPhoneStateListener; + private Preference mPreference; + private boolean mEditableWfcRoamingMode; + private boolean mUseWfcHomeModeForRoaming; + + public WifiCallingPreferenceController(Context context, String key) { + super(context, key); + mCarrierConfigManager = context.getSystemService(CarrierConfigManager.class); + mTelephonyManager = context.getSystemService(TelephonyManager.class); + mPhoneStateListener = new PhoneCallStateListener(Looper.getMainLooper()); + mEditableWfcRoamingMode = true; + mUseWfcHomeModeForRoaming = false; + } + + @Override + public int getAvailabilityStatus(int subId) { + return subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID + && MobileNetworkUtils.isWifiCallingEnabled(mContext, subId) + ? AVAILABLE + : UNSUPPORTED_ON_DEVICE; + } + + @Override + public void onStart() { + mPhoneStateListener.register(mSubId); + } + + @Override + public void onStop() { + mPhoneStateListener.unregister(); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + Intent intent = mPreference.getIntent(); + if (intent != null) { + intent.putExtra(Settings.EXTRA_SUB_ID, mSubId); + } + if (!isAvailable()) { + // Set category as invisible + final Preference preferenceCateogry = screen.findPreference(KEY_PREFERENCE_CATEGORY); + if (preferenceCateogry != null) { + preferenceCateogry.setVisible(false); + } + } + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + if (mSimCallManager != null) { + Intent intent = MobileNetworkUtils.buildPhoneAccountConfigureIntent(mContext, + mSimCallManager); + if (intent == null) { + // Do nothing in this case since preference is invisible + return; + } + final PackageManager pm = mContext.getPackageManager(); + List<ResolveInfo> resolutions = pm.queryIntentActivities(intent, 0); + preference.setTitle(resolutions.get(0).loadLabel(pm)); + preference.setSummary(null); + preference.setIntent(intent); + } else { + final String title = SubscriptionManager.getResourcesForSubId(mContext, mSubId) + .getString(R.string.wifi_calling_settings_title); + preference.setTitle(title); + int resId = com.android.internal.R.string.wifi_calling_off_summary; + if (mImsManager.isWfcEnabledByUser()) { + boolean wfcRoamingEnabled = mEditableWfcRoamingMode && !mUseWfcHomeModeForRoaming; + final boolean isRoaming = mTelephonyManager.isNetworkRoaming(); + int wfcMode = mImsManager.getWfcMode(isRoaming && wfcRoamingEnabled); + switch (wfcMode) { + case ImsConfig.WfcModeFeatureValueConstants.WIFI_ONLY: + resId = com.android.internal.R.string.wfc_mode_wifi_only_summary; + break; + case ImsConfig.WfcModeFeatureValueConstants.CELLULAR_PREFERRED: + resId = com.android.internal.R.string + .wfc_mode_cellular_preferred_summary; + break; + case ImsConfig.WfcModeFeatureValueConstants.WIFI_PREFERRED: + resId = com.android.internal.R.string.wfc_mode_wifi_preferred_summary; + break; + default: + break; + } + } + preference.setSummary(resId); + } + preference.setEnabled( + mTelephonyManager.getCallState(mSubId) == TelephonyManager.CALL_STATE_IDLE); + } + + public void init(int subId) { + mSubId = subId; + mTelephonyManager = TelephonyManager.from(mContext).createForSubscriptionId(mSubId); + mImsManager = ImsManager.getInstance(mContext, SubscriptionManager.getPhoneId(mSubId)); + mSimCallManager = mContext.getSystemService(TelecomManager.class) + .getSimCallManagerForSubscription(mSubId); + if (mCarrierConfigManager != null) { + final PersistableBundle carrierConfig = mCarrierConfigManager.getConfigForSubId(mSubId); + if (carrierConfig != null) { + mEditableWfcRoamingMode = carrierConfig.getBoolean( + CarrierConfigManager.KEY_EDITABLE_WFC_ROAMING_MODE_BOOL); + mUseWfcHomeModeForRoaming = carrierConfig.getBoolean( + CarrierConfigManager + .KEY_USE_WFC_HOME_NETWORK_MODE_IN_ROAMING_NETWORK_BOOL); + } + } + } + + private class PhoneCallStateListener extends PhoneStateListener { + + public PhoneCallStateListener(Looper looper) { + super(looper); + } + + @Override + public void onCallStateChanged(int state, String incomingNumber) { + updateState(mPreference); + } + + public void register(int subId) { + mSubId = subId; + mTelephonyManager.listen(this, PhoneStateListener.LISTEN_CALL_STATE); + } + + public void unregister() { + mTelephonyManager.listen(this, PhoneStateListener.LISTEN_NONE); + } + } +} diff --git a/src/com/android/settings/network/telephony/cdma/CdmaBasePreferenceController.java b/src/com/android/settings/network/telephony/cdma/CdmaBasePreferenceController.java new file mode 100644 index 0000000000..ea7d82fe14 --- /dev/null +++ b/src/com/android/settings/network/telephony/cdma/CdmaBasePreferenceController.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2018 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.network.telephony.cdma; + +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.provider.Settings; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; + +import androidx.preference.Preference; +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceScreen; + +import com.android.settings.network.telephony.MobileNetworkUtils; +import com.android.settings.network.telephony.TelephonyBasePreferenceController; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.core.lifecycle.events.OnStop; + +/** + * Preference controller related to CDMA category + */ +public abstract class CdmaBasePreferenceController extends TelephonyBasePreferenceController + implements LifecycleObserver, OnStart, OnStop { + + protected Preference mPreference; + protected TelephonyManager mTelephonyManager; + protected PreferenceManager mPreferenceManager; + protected int mSubId; + private DataContentObserver mDataContentObserver; + + public CdmaBasePreferenceController(Context context, String key) { + super(context, key); + mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; + mDataContentObserver = new DataContentObserver(new Handler(Looper.getMainLooper())); + } + + @Override + public void onStart() { + mDataContentObserver.register(mContext, mSubId); + } + + @Override + public void onStop() { + mDataContentObserver.unRegister(mContext); + } + + @Override + public int getAvailabilityStatus(int subId) { + return MobileNetworkUtils.isCdmaOptions(mContext, subId) + ? AVAILABLE + : CONDITIONALLY_UNAVAILABLE; + } + + public void init(PreferenceManager preferenceManager, int subId) { + mPreferenceManager = preferenceManager; + mSubId = subId; + mTelephonyManager = TelephonyManager.from(mContext).createForSubscriptionId(mSubId); + } + + public void init(int subId) { + init(null, subId); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + if (mPreference instanceof CdmaListPreference) { + ((CdmaListPreference) mPreference).setSubId(mSubId); + } + } + + /** + * Listener that listens to mobile data state change. + */ + public class DataContentObserver extends ContentObserver { + + public DataContentObserver(Handler handler) { + super(handler); + } + + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + updateState(mPreference); + } + + public void register(Context context, int subId) { + final Uri uri = Settings.Global.getUriFor( + Settings.Global.PREFERRED_NETWORK_MODE + subId); + context.getContentResolver().registerContentObserver(uri, false, this); + } + + public void unRegister(Context context) { + context.getContentResolver().unregisterContentObserver(this); + } + } +} diff --git a/src/com/android/settings/network/telephony/cdma/CdmaListPreference.java b/src/com/android/settings/network/telephony/cdma/CdmaListPreference.java new file mode 100644 index 0000000000..14c7169294 --- /dev/null +++ b/src/com/android/settings/network/telephony/cdma/CdmaListPreference.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2018 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.network.telephony.cdma; + +import android.content.Context; +import android.telephony.TelephonyManager; +import android.util.AttributeSet; + +import androidx.preference.ListPreference; + +/** + * {@link ListPreference} that will launch ECM dialog when in ECM mode + */ +public class CdmaListPreference extends ListPreference { + private TelephonyManager mTelephonyManager; + + public CdmaListPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onClick() { + // Only show dialog when it is not in ECM + if (mTelephonyManager == null || !mTelephonyManager.getEmergencyCallbackMode()) { + super.onClick(); + } + } + + public void setSubId(int subId) { + mTelephonyManager = TelephonyManager.from(getContext()).createForSubscriptionId(subId); + } +} diff --git a/src/com/android/settings/network/telephony/cdma/CdmaSubscriptionPreferenceController.java b/src/com/android/settings/network/telephony/cdma/CdmaSubscriptionPreferenceController.java new file mode 100644 index 0000000000..e6c1c22ed1 --- /dev/null +++ b/src/com/android/settings/network/telephony/cdma/CdmaSubscriptionPreferenceController.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2018 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.network.telephony.cdma; + +import android.content.Context; +import android.os.SystemProperties; +import android.provider.Settings; +import android.text.TextUtils; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.ListPreference; +import androidx.preference.Preference; + +import com.android.internal.telephony.Phone; +import com.android.settings.network.telephony.MobileNetworkUtils; + +/** + * Preference controller for "CDMA subscription" + */ +public class CdmaSubscriptionPreferenceController extends CdmaBasePreferenceController + implements ListPreference.OnPreferenceChangeListener { + private static final String TYPE_NV = "NV"; + private static final String TYPE_RUIM = "RUIM"; + + @VisibleForTesting + ListPreference mPreference; + + public CdmaSubscriptionPreferenceController(Context context, String key) { + super(context, key); + } + + @Override + public int getAvailabilityStatus(int subId) { + return MobileNetworkUtils.isCdmaOptions(mContext, subId) && deviceSupportsNvAndRuim() + ? AVAILABLE + : CONDITIONALLY_UNAVAILABLE; + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + final ListPreference listPreference = (ListPreference) preference; + listPreference.setVisible(getAvailabilityStatus() == AVAILABLE); + final int mode = Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.CDMA_SUBSCRIPTION_MODE, Phone.PREFERRED_CDMA_SUBSCRIPTION); + if (mode != Phone.CDMA_SUBSCRIPTION_UNKNOWN) { + listPreference.setValue(Integer.toString(mode)); + } + } + + @Override + public boolean onPreferenceChange(Preference preference, Object object) { + final int newMode = Integer.parseInt((String) object); + //TODO(b/117611981): only set it in one place + if (mTelephonyManager.setCdmaSubscriptionMode(newMode)) { + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.CDMA_SUBSCRIPTION_MODE, newMode); + return true; + } + + return false; + } + + @VisibleForTesting + boolean deviceSupportsNvAndRuim() { + // retrieve the list of subscription types supported by device. + final String subscriptionsSupported = SystemProperties.get("ril.subscription.types"); + boolean nvSupported = false; + boolean ruimSupported = false; + + if (!TextUtils.isEmpty(subscriptionsSupported)) { + // Searches through the comma-separated list for a match for "NV" + // and "RUIM" to update nvSupported and ruimSupported. + for (String subscriptionType : subscriptionsSupported.split(",")) { + subscriptionType = subscriptionType.trim(); + if (subscriptionType.equalsIgnoreCase(TYPE_NV)) { + nvSupported = true; + } else if (subscriptionType.equalsIgnoreCase(TYPE_RUIM)) { + ruimSupported = true; + } + } + } + + return (nvSupported && ruimSupported); + } +} diff --git a/src/com/android/settings/network/telephony/cdma/CdmaSystemSelectPreferenceController.java b/src/com/android/settings/network/telephony/cdma/CdmaSystemSelectPreferenceController.java new file mode 100644 index 0000000000..5f259f8598 --- /dev/null +++ b/src/com/android/settings/network/telephony/cdma/CdmaSystemSelectPreferenceController.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2018 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.network.telephony.cdma; + +import android.content.Context; +import android.provider.Settings; +import android.telephony.TelephonyManager; + +import androidx.preference.ListPreference; +import androidx.preference.Preference; + +import com.android.internal.telephony.Phone; + +/** + * Preference controller for "System Select" + */ +public class CdmaSystemSelectPreferenceController extends CdmaBasePreferenceController + implements ListPreference.OnPreferenceChangeListener { + + public CdmaSystemSelectPreferenceController(Context context, String key) { + super(context, key); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + final ListPreference listPreference = (ListPreference) preference; + listPreference.setVisible(getAvailabilityStatus() == AVAILABLE); + final int mode = mTelephonyManager.getCdmaRoamingMode(); + if (mode != TelephonyManager.CDMA_ROAMING_MODE_RADIO_DEFAULT) { + if (mode == TelephonyManager.CDMA_ROAMING_MODE_HOME + || mode == TelephonyManager.CDMA_ROAMING_MODE_ANY) { + listPreference.setValue(Integer.toString(mode)); + } else { + resetCdmaRoamingModeToDefault(); + } + } + final int settingsNetworkMode = Settings.Global.getInt( + mContext.getContentResolver(), + Settings.Global.PREFERRED_NETWORK_MODE + mSubId, + Phone.PREFERRED_NT_MODE); + listPreference.setEnabled( + settingsNetworkMode != TelephonyManager.NETWORK_MODE_LTE_GSM_WCDMA); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object object) { + int newMode = Integer.parseInt((String) object); + //TODO(b/117611981): only set it in one place + if (mTelephonyManager.setCdmaRoamingMode(newMode)) { + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.CDMA_ROAMING_MODE, newMode); + return true; + } + + return false; + } + + private void resetCdmaRoamingModeToDefault() { + final ListPreference listPreference = (ListPreference) mPreference; + //set the mButtonCdmaRoam + listPreference.setValue(Integer.toString(TelephonyManager.CDMA_ROAMING_MODE_ANY)); + //set the Settings.System + Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.CDMA_ROAMING_MODE, + TelephonyManager.CDMA_ROAMING_MODE_ANY); + //Set the Status + mTelephonyManager.setCdmaRoamingMode(TelephonyManager.CDMA_ROAMING_MODE_ANY); + } +} diff --git a/src/com/android/settings/network/telephony/gsm/AutoSelectPreferenceController.java b/src/com/android/settings/network/telephony/gsm/AutoSelectPreferenceController.java new file mode 100644 index 0000000000..56d0b2dcc6 --- /dev/null +++ b/src/com/android/settings/network/telephony/gsm/AutoSelectPreferenceController.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2018 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.network.telephony.gsm; + +import android.app.ProgressDialog; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.PersistableBundle; +import android.os.SystemClock; +import android.provider.Settings; +import android.telephony.CarrierConfigManager; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreference; + +import com.android.settings.R; +import com.android.settings.core.SubSettingLauncher; +import com.android.settings.network.telephony.MobileNetworkUtils; +import com.android.settings.network.telephony.NetworkSelectSettings; +import com.android.settings.network.telephony.TelephonyTogglePreferenceController; +import com.android.settingslib.utils.ThreadUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Preference controller for "Auto Select Network" + */ +public class AutoSelectPreferenceController extends TelephonyTogglePreferenceController { + private static final long MINIMUM_DIALOG_TIME_MILLIS = TimeUnit.SECONDS.toMillis(1); + + private final Handler mUiHandler; + private TelephonyManager mTelephonyManager; + private boolean mOnlyAutoSelectInHome; + private List<OnNetworkSelectModeListener> mListeners; + @VisibleForTesting + ProgressDialog mProgressDialog; + @VisibleForTesting + SwitchPreference mSwitchPreference; + + public AutoSelectPreferenceController(Context context, String key) { + super(context, key); + mTelephonyManager = context.getSystemService(TelephonyManager.class); + mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; + mListeners = new ArrayList<>(); + mUiHandler = new Handler(Looper.getMainLooper()); + } + + @Override + public int getAvailabilityStatus(int subId) { + return MobileNetworkUtils.shouldDisplayNetworkSelectOptions(mContext, subId) + ? AVAILABLE + : CONDITIONALLY_UNAVAILABLE; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mSwitchPreference = screen.findPreference(getPreferenceKey()); + } + + @Override + public boolean isChecked() { + return mTelephonyManager.getNetworkSelectionMode() + == TelephonyManager.NETWORK_SELECTION_MODE_AUTO; + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + + preference.setSummary(null); + if (mTelephonyManager.getServiceState().getRoaming()) { + preference.setEnabled(true); + } else { + preference.setEnabled(!mOnlyAutoSelectInHome); + if (mOnlyAutoSelectInHome) { + preference.setSummary(mContext.getString( + R.string.manual_mode_disallowed_summary, + mTelephonyManager.getSimOperatorName())); + } + } + } + + @Override + public boolean setChecked(boolean isChecked) { + if (isChecked) { + final long startMillis = SystemClock.elapsedRealtime(); + showAutoSelectProgressBar(); + mSwitchPreference.setEnabled(false); + ThreadUtils.postOnBackgroundThread(() -> { + // set network selection mode in background + mTelephonyManager.setNetworkSelectionModeAutomatic(); + final int mode = mTelephonyManager.getNetworkSelectionMode(); + + //Update UI in UI thread + final long durationMillis = SystemClock.elapsedRealtime() - startMillis; + mUiHandler.postDelayed(() -> { + mSwitchPreference.setEnabled(true); + mSwitchPreference.setChecked( + mode == TelephonyManager.NETWORK_SELECTION_MODE_AUTO); + for (OnNetworkSelectModeListener lsn : mListeners) { + lsn.onNetworkSelectModeChanged(); + } + dismissProgressBar(); + }, + Math.max(MINIMUM_DIALOG_TIME_MILLIS - durationMillis, 0)); + }); + return false; + } else { + final Bundle bundle = new Bundle(); + bundle.putInt(Settings.EXTRA_SUB_ID, mSubId); + new SubSettingLauncher(mContext) + .setDestination(NetworkSelectSettings.class.getName()) + .setSourceMetricsCategory(SettingsEnums.MOBILE_NETWORK_SELECT) + .setTitleRes(R.string.choose_network_title) + .setArguments(bundle) + .launch(); + return false; + } + } + + public AutoSelectPreferenceController init(int subId) { + mSubId = subId; + mTelephonyManager = TelephonyManager.from(mContext).createForSubscriptionId(mSubId); + final PersistableBundle carrierConfig = mContext.getSystemService( + CarrierConfigManager.class).getConfigForSubId(mSubId); + mOnlyAutoSelectInHome = carrierConfig != null + ? carrierConfig.getBoolean( + CarrierConfigManager.KEY_ONLY_AUTO_SELECT_IN_HOME_NETWORK_BOOL) + : false; + + return this; + } + + public AutoSelectPreferenceController addListener(OnNetworkSelectModeListener lsn) { + mListeners.add(lsn); + + return this; + } + + private void showAutoSelectProgressBar() { + if (mProgressDialog == null) { + mProgressDialog = new ProgressDialog(mContext); + mProgressDialog.setMessage( + mContext.getResources().getString(R.string.register_automatically)); + mProgressDialog.setCanceledOnTouchOutside(false); + mProgressDialog.setCancelable(false); + mProgressDialog.setIndeterminate(true); + } + mProgressDialog.show(); + } + + private void dismissProgressBar() { + if (mProgressDialog != null && mProgressDialog.isShowing()) { + mProgressDialog.dismiss(); + } + } + + /** + * Callback when network select mode is changed + * + * @see TelephonyManager#getNetworkSelectionMode() + */ + public interface OnNetworkSelectModeListener { + void onNetworkSelectModeChanged(); + } +}
\ No newline at end of file diff --git a/src/com/android/settings/network/telephony/gsm/OpenNetworkSelectPagePreferenceController.java b/src/com/android/settings/network/telephony/gsm/OpenNetworkSelectPagePreferenceController.java new file mode 100644 index 0000000000..46171bc2ab --- /dev/null +++ b/src/com/android/settings/network/telephony/gsm/OpenNetworkSelectPagePreferenceController.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2018 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.network.telephony.gsm; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.os.Bundle; +import android.provider.Settings; +import android.telephony.ServiceState; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.text.TextUtils; + +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.core.SubSettingLauncher; +import com.android.settings.network.telephony.MobileNetworkUtils; +import com.android.settings.network.telephony.NetworkSelectSettings; +import com.android.settings.network.telephony.TelephonyBasePreferenceController; + +/** + * Preference controller for "Open network select" + */ +public class OpenNetworkSelectPagePreferenceController extends + TelephonyBasePreferenceController implements + AutoSelectPreferenceController.OnNetworkSelectModeListener { + + private TelephonyManager mTelephonyManager; + private Preference mPreference; + + public OpenNetworkSelectPagePreferenceController(Context context, String key) { + super(context, key); + mTelephonyManager = context.getSystemService(TelephonyManager.class); + mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; + } + + @Override + public int getAvailabilityStatus(int subId) { + return MobileNetworkUtils.shouldDisplayNetworkSelectOptions(mContext, subId) + ? AVAILABLE + : CONDITIONALLY_UNAVAILABLE; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + preference.setEnabled(mTelephonyManager.getNetworkSelectionMode() + != TelephonyManager.NETWORK_SELECTION_MODE_AUTO); + } + + @Override + public CharSequence getSummary() { + final ServiceState ss = mTelephonyManager.getServiceState(); + if (ss != null && ss.getState() == ServiceState.STATE_IN_SERVICE) { + return mTelephonyManager.getNetworkOperatorName(); + } else { + return mContext.getString(R.string.network_disconnected); + } + } + + @Override + public boolean handlePreferenceTreeClick(Preference preference) { + if (TextUtils.equals(preference.getKey(), getPreferenceKey())) { + final Bundle bundle = new Bundle(); + bundle.putInt(Settings.EXTRA_SUB_ID, mSubId); + new SubSettingLauncher(mContext) + .setDestination(NetworkSelectSettings.class.getName()) + .setSourceMetricsCategory(SettingsEnums.MOBILE_NETWORK_SELECT) + .setTitleRes(R.string.choose_network_title) + .setArguments(bundle) + .launch(); + return true; + } + + return false; + } + + public OpenNetworkSelectPagePreferenceController init(int subId) { + mSubId = subId; + mTelephonyManager = TelephonyManager.from(mContext).createForSubscriptionId(mSubId); + return this; + } + + @Override + public void onNetworkSelectModeChanged() { + updateState(mPreference); + } +}
\ No newline at end of file diff --git a/src/com/android/settings/nfc/AndroidBeam.java b/src/com/android/settings/nfc/AndroidBeam.java index 4e90680aac..1f75a2571c 100644 --- a/src/com/android/settings/nfc/AndroidBeam.java +++ b/src/com/android/settings/nfc/AndroidBeam.java @@ -16,6 +16,11 @@ package com.android.settings.nfc; +import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.pm.PackageManager; import android.nfc.NfcAdapter; import android.os.Bundle; import android.os.UserHandle; @@ -25,18 +30,17 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.ImageView; import android.widget.Switch; +import android.widget.TextView; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.settings.enterprise.ActionDisabledByAdminDialogHelper; -import com.android.settingslib.HelpUtils; -import com.android.settings.core.InstrumentedFragment; import com.android.settings.R; import com.android.settings.SettingsActivity; +import com.android.settings.core.InstrumentedFragment; +import com.android.settings.enterprise.ActionDisabledByAdminDialogHelper; import com.android.settings.widget.SwitchBar; -import com.android.settingslib.RestrictedLockUtils; - -import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; +import com.android.settingslib.HelpUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; public class AndroidBeam extends InstrumentedFragment implements SwitchBar.OnSwitchChangeListener { @@ -50,7 +54,11 @@ public class AndroidBeam extends InstrumentedFragment @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - mNfcAdapter = NfcAdapter.getDefaultAdapter(getActivity()); + final Context context = getActivity(); + mNfcAdapter = NfcAdapter.getDefaultAdapter(context); + final PackageManager pm = context.getPackageManager(); + if (mNfcAdapter == null || !pm.hasSystemFeature(PackageManager.FEATURE_NFC_BEAM)) + getActivity().finish(); setHasOptionsMenu(true); } @@ -64,10 +72,10 @@ public class AndroidBeam extends InstrumentedFragment @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - final EnforcedAdmin admin = RestrictedLockUtils.checkIfRestrictionEnforced( + final EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced( getActivity(), UserManager.DISALLOW_OUTGOING_BEAM, UserHandle.myUserId()); final UserManager um = UserManager.get(getActivity()); - mBeamDisallowedByBase = RestrictedLockUtils.hasBaseUserRestriction(getActivity(), + mBeamDisallowedByBase = RestrictedLockUtilsInternal.hasBaseUserRestriction(getActivity(), UserManager.DISALLOW_OUTGOING_BEAM, UserHandle.myUserId()); if (!mBeamDisallowedByBase && admin != null) { new ActionDisabledByAdminDialogHelper(getActivity()) @@ -75,14 +83,19 @@ public class AndroidBeam extends InstrumentedFragment mBeamDisallowedByOnlyAdmin = true; return new View(getContext()); } - mView = inflater.inflate(R.layout.android_beam, container, false); + mView = inflater.inflate(R.layout.preference_footer, container, false); + + ImageView iconInfo = mView.findViewById(android.R.id.icon); + iconInfo.setImageResource(R.drawable.ic_info_outline_24dp); + TextView textInfo = mView.findViewById(android.R.id.title); + textInfo.setText(R.string.android_beam_explained); + return mView; } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - SettingsActivity activity = (SettingsActivity) getActivity(); mOldActivityTitle = activity.getActionBar().getTitle(); @@ -129,6 +142,6 @@ public class AndroidBeam extends InstrumentedFragment @Override public int getMetricsCategory() { - return MetricsEvent.NFC_BEAM; + return SettingsEnums.NFC_BEAM; } } diff --git a/src/com/android/settings/nfc/AndroidBeamEnabler.java b/src/com/android/settings/nfc/AndroidBeamEnabler.java index 1808775353..31ef7028ee 100644 --- a/src/com/android/settings/nfc/AndroidBeamEnabler.java +++ b/src/com/android/settings/nfc/AndroidBeamEnabler.java @@ -22,7 +22,7 @@ import android.os.UserHandle; import android.os.UserManager; import com.android.settings.R; -import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.RestrictedPreference; /** @@ -36,7 +36,7 @@ public class AndroidBeamEnabler extends BaseNfcEnabler { public AndroidBeamEnabler(Context context, RestrictedPreference preference) { super(context); mPreference = preference; - mBeamDisallowedBySystem = RestrictedLockUtils.hasBaseUserRestriction(context, + mBeamDisallowedBySystem = RestrictedLockUtilsInternal.hasBaseUserRestriction(context, UserManager.DISALLOW_OUTGOING_BEAM, UserHandle.myUserId()); if (!isNfcAvailable()) { // NFC is not supported @@ -53,7 +53,7 @@ public class AndroidBeamEnabler extends BaseNfcEnabler { switch (newState) { case NfcAdapter.STATE_OFF: mPreference.setEnabled(false); - mPreference.setSummary(R.string.android_beam_disabled_summary); + mPreference.setSummary(R.string.nfc_disabled_summary); break; case NfcAdapter.STATE_ON: if (mBeamDisallowedBySystem) { diff --git a/src/com/android/settings/nfc/AndroidBeamPreferenceController.java b/src/com/android/settings/nfc/AndroidBeamPreferenceController.java index 9fc0aec223..15c15aa7e3 100644 --- a/src/com/android/settings/nfc/AndroidBeamPreferenceController.java +++ b/src/com/android/settings/nfc/AndroidBeamPreferenceController.java @@ -18,7 +18,7 @@ package com.android.settings.nfc; import android.content.Context; import android.content.pm.PackageManager; import android.nfc.NfcAdapter; -import androidx.preference.Preference; + import androidx.preference.PreferenceScreen; import com.android.settings.core.BasePreferenceController; @@ -27,15 +27,12 @@ import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnPause; import com.android.settingslib.core.lifecycle.events.OnResume; -import java.util.List; - public class AndroidBeamPreferenceController extends BasePreferenceController implements LifecycleObserver, OnResume, OnPause { public static final String KEY_ANDROID_BEAM_SETTINGS = "android_beam_settings"; private final NfcAdapter mNfcAdapter; private AndroidBeamEnabler mAndroidBeamEnabler; - private NfcAirplaneModeObserver mAirplaneModeObserver; public AndroidBeamPreferenceController(Context context, String key) { super(context, key); @@ -50,15 +47,8 @@ public class AndroidBeamPreferenceController extends BasePreferenceController return; } - final RestrictedPreference restrictedPreference = - (RestrictedPreference) screen.findPreference(getPreferenceKey()); + final RestrictedPreference restrictedPreference = screen.findPreference(getPreferenceKey()); mAndroidBeamEnabler = new AndroidBeamEnabler(mContext, restrictedPreference); - - // Manually set dependencies for NFC when not toggleable. - if (!NfcPreferenceController.isToggleableInAirplaneMode(mContext)) { - mAirplaneModeObserver = new NfcAirplaneModeObserver(mContext, mNfcAdapter, - (Preference) restrictedPreference); - } } @Override @@ -66,7 +56,7 @@ public class AndroidBeamPreferenceController extends BasePreferenceController public int getAvailabilityStatus() { PackageManager pm = mContext.getPackageManager(); if (!pm.hasSystemFeature(PackageManager.FEATURE_NFC_BEAM)) { - return UNSUPPORTED_ON_DEVICE; + return UNSUPPORTED_ON_DEVICE; } return mNfcAdapter != null ? AVAILABLE @@ -75,9 +65,6 @@ public class AndroidBeamPreferenceController extends BasePreferenceController @Override public void onResume() { - if (mAirplaneModeObserver != null) { - mAirplaneModeObserver.register(); - } if (mAndroidBeamEnabler != null) { mAndroidBeamEnabler.resume(); } @@ -85,9 +72,6 @@ public class AndroidBeamPreferenceController extends BasePreferenceController @Override public void onPause() { - if (mAirplaneModeObserver != null) { - mAirplaneModeObserver.unregister(); - } if (mAndroidBeamEnabler != null) { mAndroidBeamEnabler.pause(); } diff --git a/src/com/android/settings/nfc/BaseNfcEnabler.java b/src/com/android/settings/nfc/BaseNfcEnabler.java index 88bafb98fb..0deaab6f35 100644 --- a/src/com/android/settings/nfc/BaseNfcEnabler.java +++ b/src/com/android/settings/nfc/BaseNfcEnabler.java @@ -21,14 +21,13 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.nfc.NfcAdapter; -import androidx.preference.Preference; /** * BaseNfcEnabler is a abstract helper to manage the Nfc state for Nfc and Android Beam * preference. It will receive intent and update state to ensure preference show correct state. */ public abstract class BaseNfcEnabler { - private final Context mContext; + protected final Context mContext; protected final NfcAdapter mNfcAdapter; private final IntentFilter mIntentFilter; diff --git a/src/com/android/settings/nfc/NfcAirplaneModeObserver.java b/src/com/android/settings/nfc/NfcAirplaneModeObserver.java deleted file mode 100644 index d0ce045296..0000000000 --- a/src/com/android/settings/nfc/NfcAirplaneModeObserver.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (C) 2018 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.nfc; - -import android.content.Context; -import android.database.ContentObserver; -import android.net.Uri; -import android.nfc.NfcAdapter; -import android.os.Handler; -import android.os.Looper; -import android.provider.Settings; -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; - -/** - * NfcAirplaneModeObserver is a helper to manage the Nfc on/off when airplane mode status - * is changed. - */ -public class NfcAirplaneModeObserver extends ContentObserver { - - private final Context mContext; - private final NfcAdapter mNfcAdapter; - private final Preference mPreference; - private int mAirplaneMode; - - @VisibleForTesting - final static Uri AIRPLANE_MODE_URI = - Settings.Global.getUriFor(Settings.Global.AIRPLANE_MODE_ON); - - public NfcAirplaneModeObserver(Context context, NfcAdapter nfcAdapter, Preference preference) { - super(new Handler(Looper.getMainLooper())); - mContext = context; - mNfcAdapter = nfcAdapter; - mPreference = preference; - updateNfcPreference(); - } - - public void register() { - mContext.getContentResolver().registerContentObserver(AIRPLANE_MODE_URI, false, this); - } - - public void unregister() { - mContext.getContentResolver().unregisterContentObserver(this); - } - - @Override - public void onChange(boolean selfChange, Uri uri) { - super.onChange(selfChange, uri); - updateNfcPreference(); - } - - private void updateNfcPreference() { - final int airplaneMode = Settings.Global.getInt( - mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, mAirplaneMode); - if (airplaneMode == mAirplaneMode) { - return; - } - - mAirplaneMode = airplaneMode; - boolean toggleable = mAirplaneMode != 1; - if (toggleable) { - mNfcAdapter.enable(); - } else { - mNfcAdapter.disable(); - } - mPreference.setEnabled(toggleable); - } -} diff --git a/src/com/android/settings/nfc/NfcEnabler.java b/src/com/android/settings/nfc/NfcEnabler.java index b3450cbb8c..777e7d114b 100644 --- a/src/com/android/settings/nfc/NfcEnabler.java +++ b/src/com/android/settings/nfc/NfcEnabler.java @@ -18,6 +18,9 @@ package com.android.settings.nfc; import android.content.Context; import android.nfc.NfcAdapter; +import android.provider.Settings; + +import androidx.annotation.VisibleForTesting; import androidx.preference.SwitchPreference; /** @@ -37,7 +40,7 @@ public class NfcEnabler extends BaseNfcEnabler { switch (newState) { case NfcAdapter.STATE_OFF: mPreference.setChecked(false); - mPreference.setEnabled(true); + mPreference.setEnabled(isToggleable()); break; case NfcAdapter.STATE_ON: mPreference.setChecked(true); @@ -53,4 +56,15 @@ public class NfcEnabler extends BaseNfcEnabler { break; } } + + @VisibleForTesting + boolean isToggleable() { + if (NfcPreferenceController.isToggleableInAirplaneMode(mContext) + || !NfcPreferenceController.shouldTurnOffNFCInAirplaneMode(mContext)) { + return true; + } + final int airplaneMode = Settings.Global.getInt( + mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0); + return airplaneMode != 1; + } } diff --git a/src/com/android/settings/nfc/NfcForegroundPreference.java b/src/com/android/settings/nfc/NfcForegroundPreference.java deleted file mode 100644 index 728f2e4b59..0000000000 --- a/src/com/android/settings/nfc/NfcForegroundPreference.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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.nfc; - -import android.content.Context; -import androidx.preference.DropDownPreference; -import androidx.preference.Preference; - -import com.android.settings.R; - -public class NfcForegroundPreference extends DropDownPreference implements - PaymentBackend.Callback, Preference.OnPreferenceChangeListener { - - private final PaymentBackend mPaymentBackend; - public NfcForegroundPreference(Context context, PaymentBackend backend) { - super(context); - mPaymentBackend = backend; - mPaymentBackend.registerCallback(this); - - setTitle(getContext().getString(R.string.nfc_payment_use_default)); - setEntries(new CharSequence[] { - getContext().getString(R.string.nfc_payment_favor_open), - getContext().getString(R.string.nfc_payment_favor_default) - }); - setEntryValues(new CharSequence[] { "1", "0" }); - refresh(); - setOnPreferenceChangeListener(this); - } - - @Override - public void onPaymentAppsChanged() { - refresh(); - } - - void refresh() { - boolean foregroundMode = mPaymentBackend.isForegroundMode(); - if (foregroundMode) { - setValue("1"); - } else { - setValue("0"); - } - setSummary(getEntry()); - } - - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - String newValueString = (String) newValue; - setSummary(getEntries()[findIndexOfValue(newValueString)]); - mPaymentBackend.setForegroundMode(Integer.parseInt(newValueString) != 0); - return true; - } -} diff --git a/src/com/android/settings/nfc/NfcForegroundPreferenceController.java b/src/com/android/settings/nfc/NfcForegroundPreferenceController.java new file mode 100644 index 0000000000..b02608ef87 --- /dev/null +++ b/src/com/android/settings/nfc/NfcForegroundPreferenceController.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2018 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.nfc; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.pm.PackageManager; +import android.text.TextUtils; + +import androidx.preference.DropDownPreference; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.core.lifecycle.events.OnStop; + +import java.util.List; + +public class NfcForegroundPreferenceController extends BasePreferenceController implements + PaymentBackend.Callback, Preference.OnPreferenceChangeListener, + LifecycleObserver, OnStart, OnStop { + + private DropDownPreference mPreference; + private PaymentBackend mPaymentBackend; + private MetricsFeatureProvider mMetricsFeatureProvider; + + public NfcForegroundPreferenceController(Context context, String key) { + super(context, key); + mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider(); + } + + public void setPaymentBackend(PaymentBackend backend) { + mPaymentBackend = backend; + } + + @Override + public void onStart() { + if (mPaymentBackend != null) { + mPaymentBackend.registerCallback(this); + } + } + + @Override + public void onStop() { + if (mPaymentBackend != null) { + mPaymentBackend.unregisterCallback(this); + } + } + + @Override + public int getAvailabilityStatus() { + final PackageManager pm = mContext.getPackageManager(); + if (!pm.hasSystemFeature(PackageManager.FEATURE_NFC)) { + return UNSUPPORTED_ON_DEVICE; + } + if (mPaymentBackend == null) { + return UNSUPPORTED_ON_DEVICE; + } + final List<PaymentBackend.PaymentAppInfo> appInfos = mPaymentBackend.getPaymentAppInfos(); + return (appInfos != null && !appInfos.isEmpty()) + ? AVAILABLE + : UNSUPPORTED_ON_DEVICE; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + if (mPreference == null) { + return; + } + + mPreference.setEntries(new CharSequence[]{ + mContext.getText(R.string.nfc_payment_favor_open), + mContext.getText(R.string.nfc_payment_favor_default) + }); + mPreference.setEntryValues(new CharSequence[]{"1", "0"}); + } + + @Override + public void onPaymentAppsChanged() { + updateState(mPreference); + } + + @Override + public void updateState(Preference preference) { + if (preference instanceof DropDownPreference) { + ((DropDownPreference) preference).setValue( + mPaymentBackend.isForegroundMode() ? "1" : "0"); + } + super.updateState(preference); + } + + @Override + public CharSequence getSummary() { + return mPreference.getEntry(); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + if (!(preference instanceof DropDownPreference)) { + return false; + } + final DropDownPreference pref = (DropDownPreference) preference; + final String newValueString = (String) newValue; + pref.setSummary(pref.getEntries()[pref.findIndexOfValue(newValueString)]); + final boolean foregroundMode = Integer.parseInt(newValueString) != 0; + mPaymentBackend.setForegroundMode(foregroundMode); + mMetricsFeatureProvider.action(mContext, + foregroundMode ? SettingsEnums.ACTION_NFC_PAYMENT_FOREGROUND_SETTING + : SettingsEnums.ACTION_NFC_PAYMENT_ALWAYS_SETTING); + return true; + } + + @Override + public void updateNonIndexableKeys(List<String> keys) { + final String key = getPreferenceKey(); + if (!TextUtils.isEmpty(key)) { + keys.add(key); + } + } +}
\ No newline at end of file diff --git a/src/com/android/settings/nfc/NfcPaymentPreference.java b/src/com/android/settings/nfc/NfcPaymentPreference.java index 0838a90e62..421148bcc3 100644 --- a/src/com/android/settings/nfc/NfcPaymentPreference.java +++ b/src/com/android/settings/nfc/NfcPaymentPreference.java @@ -15,204 +15,59 @@ */ package com.android.settings.nfc; -import android.app.AlertDialog; -import android.content.ActivityNotFoundException; import android.content.Context; -import android.app.Dialog; import android.content.DialogInterface; -import android.content.Intent; -import androidx.preference.PreferenceViewHolder; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; -import android.widget.CompoundButton; -import android.widget.ImageView; -import android.widget.RadioButton; - -import com.android.settings.R; -import com.android.settings.nfc.PaymentBackend.PaymentAppInfo; -import com.android.settingslib.CustomDialogPreference; - -import java.util.List; +import android.util.AttributeSet; -public class NfcPaymentPreference extends CustomDialogPreference implements - PaymentBackend.Callback, View.OnClickListener { +import androidx.appcompat.app.AlertDialog.Builder; +import androidx.preference.PreferenceViewHolder; - private static final String TAG = "NfcPaymentPreference"; +import com.android.settingslib.CustomDialogPreferenceCompat; - private final NfcPaymentAdapter mAdapter; - private final Context mContext; - private final LayoutInflater mLayoutInflater; - private final PaymentBackend mPaymentBackend; +public class NfcPaymentPreference extends CustomDialogPreferenceCompat { - // Fields below only modified on UI thread - private ImageView mSettingsButtonView; + private Listener mListener; - public NfcPaymentPreference(Context context, PaymentBackend backend) { - super(context, null); - mPaymentBackend = backend; - mContext = context; - backend.registerCallback(this); - mAdapter = new NfcPaymentAdapter(); - setDialogTitle(context.getString(R.string.nfc_payment_pay_with)); - mLayoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - setWidgetLayoutResource(R.layout.preference_widget_gear); + interface Listener { + void onBindViewHolder(PreferenceViewHolder view); - refresh(); + void onPrepareDialogBuilder(Builder builder, + DialogInterface.OnClickListener listener); } - @Override - public void onBindViewHolder(PreferenceViewHolder view) { - super.onBindViewHolder(view); - - mSettingsButtonView = (ImageView) view.findViewById(R.id.settings_button); - mSettingsButtonView.setOnClickListener(this); - - updateSettingsVisibility(); + public NfcPaymentPreference(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); } - /** - * MUST be called on UI thread. - */ - public void refresh() { - List<PaymentAppInfo> appInfos = mPaymentBackend.getPaymentAppInfos(); - PaymentAppInfo defaultApp = mPaymentBackend.getDefaultApp(); - if (appInfos != null) { - PaymentAppInfo[] apps = appInfos.toArray(new PaymentAppInfo[appInfos.size()]); - mAdapter.updateApps(apps, defaultApp); - } - setTitle(R.string.nfc_payment_default); - if (defaultApp != null) { - setSummary(defaultApp.label); - } else { - setSummary(mContext.getString(R.string.nfc_payment_default_not_set)); - } - updateSettingsVisibility(); + public NfcPaymentPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); } - @Override - protected void onPrepareDialogBuilder(AlertDialog.Builder builder, - DialogInterface.OnClickListener listener) { - super.onPrepareDialogBuilder(builder, listener); - - builder.setSingleChoiceItems(mAdapter, 0, listener); + public NfcPaymentPreference(Context context, AttributeSet attrs) { + super(context, attrs); } - @Override - public void onPaymentAppsChanged() { - refresh(); + void initialize(Listener listener) { + mListener = listener; } @Override - public void onClick(View view) { - PaymentAppInfo defaultAppInfo = mPaymentBackend.getDefaultApp(); - if (defaultAppInfo != null && defaultAppInfo.settingsComponent != null) { - Intent settingsIntent = new Intent(Intent.ACTION_MAIN); - settingsIntent.setComponent(defaultAppInfo.settingsComponent); - settingsIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - try { - mContext.startActivity(settingsIntent); - } catch (ActivityNotFoundException e) { - Log.e(TAG, "Settings activity not found."); - } - } - } - - void updateSettingsVisibility() { - if (mSettingsButtonView != null) { - PaymentAppInfo defaultApp = mPaymentBackend.getDefaultApp(); - if (defaultApp == null || defaultApp.settingsComponent == null) { - mSettingsButtonView.setVisibility(View.GONE); - } else { - mSettingsButtonView.setVisibility(View.VISIBLE); + public void onBindViewHolder(PreferenceViewHolder view) { + super.onBindViewHolder(view); - } + if (mListener != null) { + mListener.onBindViewHolder(view); } } - class NfcPaymentAdapter extends BaseAdapter implements CompoundButton.OnCheckedChangeListener, - View.OnClickListener { - // Only modified on UI thread - private PaymentAppInfo[] appInfos; - - public NfcPaymentAdapter() { - } - - public void updateApps(PaymentAppInfo[] appInfos, PaymentAppInfo currentDefault) { - // Clone app infos, only add those with a banner - this.appInfos = appInfos; - notifyDataSetChanged(); - } - - @Override - public int getCount() { - return appInfos.length; - } - - @Override - public PaymentAppInfo getItem(int i) { - return appInfos[i]; - } - - @Override - public long getItemId(int i) { - return appInfos[i].componentName.hashCode(); - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - ViewHolder holder; - PaymentAppInfo appInfo = appInfos[position]; - if (convertView == null) { - convertView = mLayoutInflater.inflate( - R.layout.nfc_payment_option, parent, false); - holder = new ViewHolder(); - holder.imageView = (ImageView) convertView.findViewById(R.id.banner); - holder.radioButton = (RadioButton) convertView.findViewById(R.id.button); - convertView.setTag(holder); - } else { - holder = (ViewHolder) convertView.getTag(); - } - holder.imageView.setImageDrawable(appInfo.banner); - holder.imageView.setTag(appInfo); - holder.imageView.setContentDescription(appInfo.label); - holder.imageView.setOnClickListener(this); - - // Prevent checked callback getting called on recycled views - holder.radioButton.setOnCheckedChangeListener(null); - holder.radioButton.setChecked(appInfo.isDefault); - holder.radioButton.setContentDescription(appInfo.label); - holder.radioButton.setOnCheckedChangeListener(this); - holder.radioButton.setTag(appInfo); - return convertView; - } - - public class ViewHolder { - public ImageView imageView; - public RadioButton radioButton; - } - - @Override - public void onCheckedChanged(CompoundButton compoundButton, boolean b) { - PaymentAppInfo appInfo = (PaymentAppInfo) compoundButton.getTag(); - makeDefault(appInfo); - } - - @Override - public void onClick(View view) { - PaymentAppInfo appInfo = (PaymentAppInfo) view.getTag(); - makeDefault(appInfo); - } + @Override + protected void onPrepareDialogBuilder(Builder builder, + DialogInterface.OnClickListener listener) { + super.onPrepareDialogBuilder(builder, listener); - void makeDefault(PaymentAppInfo appInfo) { - if (!appInfo.isDefault) { - mPaymentBackend.setDefaultPaymentApp(appInfo.componentName); - } - Dialog dialog = getDialog(); - if (dialog != null) - dialog.dismiss(); + if (mListener != null) { + mListener.onPrepareDialogBuilder(builder, listener); } } } diff --git a/src/com/android/settings/nfc/NfcPaymentPreferenceController.java b/src/com/android/settings/nfc/NfcPaymentPreferenceController.java new file mode 100644 index 0000000000..9d3673dff1 --- /dev/null +++ b/src/com/android/settings/nfc/NfcPaymentPreferenceController.java @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2018 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.nfc; + +import android.app.Dialog; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.nfc.NfcAdapter; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.CompoundButton; +import android.widget.ImageView; +import android.widget.RadioButton; + +import androidx.appcompat.app.AlertDialog.Builder; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; +import androidx.preference.PreferenceViewHolder; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.nfc.PaymentBackend.PaymentAppInfo; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.core.lifecycle.events.OnStop; + +import java.util.List; + +public class NfcPaymentPreferenceController extends BasePreferenceController implements + PaymentBackend.Callback, View.OnClickListener, NfcPaymentPreference.Listener, + LifecycleObserver, OnStart, OnStop { + + private static final String TAG = "NfcPaymentController"; + + private final NfcPaymentAdapter mAdapter; + private PaymentBackend mPaymentBackend; + private NfcPaymentPreference mPreference; + private ImageView mSettingsButtonView; + + public NfcPaymentPreferenceController(Context context, String key) { + super(context, key); + mAdapter = new NfcPaymentAdapter(context); + } + + public void setPaymentBackend(PaymentBackend backend) { + mPaymentBackend = backend; + } + + @Override + public void onStart() { + if (mPaymentBackend != null) { + mPaymentBackend.registerCallback(this); + } + } + + @Override + public void onStop() { + if (mPaymentBackend != null) { + mPaymentBackend.unregisterCallback(this); + } + } + + @Override + public int getAvailabilityStatus() { + final PackageManager pm = mContext.getPackageManager(); + if (!pm.hasSystemFeature(PackageManager.FEATURE_NFC)) { + return UNSUPPORTED_ON_DEVICE; + } + if (NfcAdapter.getDefaultAdapter(mContext) == null) { + return UNSUPPORTED_ON_DEVICE; + } + if (mPaymentBackend == null) { + mPaymentBackend = new PaymentBackend(mContext); + } + final List<PaymentAppInfo> appInfos = mPaymentBackend.getPaymentAppInfos(); + return (appInfos != null && !appInfos.isEmpty()) + ? AVAILABLE + : UNSUPPORTED_ON_DEVICE; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + if (mPreference != null) { + mPreference.initialize(this); + } + } + + @Override + public void onBindViewHolder(PreferenceViewHolder view) { + mSettingsButtonView = (ImageView) view.findViewById(R.id.settings_button); + mSettingsButtonView.setOnClickListener(this); + + updateSettingsVisibility(); + } + + @Override + public void updateState(Preference preference) { + final List<PaymentAppInfo> appInfos = mPaymentBackend.getPaymentAppInfos(); + if (appInfos != null) { + final PaymentAppInfo[] apps = appInfos.toArray(new PaymentAppInfo[appInfos.size()]); + mAdapter.updateApps(apps); + } + super.updateState(preference); + updateSettingsVisibility(); + } + + @Override + public CharSequence getSummary() { + final PaymentAppInfo defaultApp = mPaymentBackend.getDefaultApp(); + if (defaultApp != null) { + return defaultApp.label; + } else { + return mContext.getText(R.string.nfc_payment_default_not_set); + } + } + + @Override + public void onPrepareDialogBuilder(Builder builder, + DialogInterface.OnClickListener listener) { + builder.setSingleChoiceItems(mAdapter, 0, listener); + } + + @Override + public void onPaymentAppsChanged() { + updateState(mPreference); + } + + @Override + public void onClick(View view) { + final PaymentAppInfo defaultAppInfo = mPaymentBackend.getDefaultApp(); + if (defaultAppInfo != null && defaultAppInfo.settingsComponent != null) { + final Intent settingsIntent = new Intent(Intent.ACTION_MAIN); + settingsIntent.setComponent(defaultAppInfo.settingsComponent); + settingsIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + try { + mContext.startActivity(settingsIntent); + } catch (ActivityNotFoundException e) { + Log.e(TAG, "Settings activity not found."); + } + } + } + + private void updateSettingsVisibility() { + if (mSettingsButtonView != null) { + final PaymentAppInfo defaultApp = mPaymentBackend.getDefaultApp(); + if (defaultApp == null || defaultApp.settingsComponent == null) { + mSettingsButtonView.setVisibility(View.GONE); + } else { + mSettingsButtonView.setVisibility(View.VISIBLE); + } + } + } + + private class NfcPaymentAdapter extends BaseAdapter implements + CompoundButton.OnCheckedChangeListener, View.OnClickListener { + private final LayoutInflater mLayoutInflater; + + // Only modified on UI thread + private PaymentAppInfo[] appInfos; + + public NfcPaymentAdapter(Context context) { + mLayoutInflater = (LayoutInflater) context.getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + } + + public void updateApps(PaymentAppInfo[] appInfos) { + // Clone app infos, only add an application label + this.appInfos = appInfos; + notifyDataSetChanged(); + } + + @Override + public int getCount() { + return (appInfos != null) ? appInfos.length : 0; + } + + @Override + public PaymentAppInfo getItem(int i) { + return appInfos[i]; + } + + @Override + public long getItemId(int i) { + return appInfos[i].componentName.hashCode(); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + ViewHolder holder; + final PaymentAppInfo appInfo = appInfos[position]; + if (convertView == null) { + convertView = mLayoutInflater.inflate( + R.layout.nfc_payment_option, parent, false); + holder = new ViewHolder(); + holder.radioButton = convertView.findViewById(R.id.button); + convertView.setTag(holder); + } else { + holder = (ViewHolder) convertView.getTag(); + } + + // Prevent checked callback getting called on recycled views + holder.radioButton.setOnCheckedChangeListener(null); + holder.radioButton.setChecked(appInfo.isDefault); + holder.radioButton.setContentDescription(appInfo.label); + holder.radioButton.setOnCheckedChangeListener(this); + holder.radioButton.setTag(appInfo); + holder.radioButton.setText(appInfo.label); + return convertView; + } + + private class ViewHolder { + public RadioButton radioButton; + } + + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean b) { + PaymentAppInfo appInfo = (PaymentAppInfo) compoundButton.getTag(); + makeDefault(appInfo); + } + + @Override + public void onClick(View view) { + PaymentAppInfo appInfo = (PaymentAppInfo) view.getTag(); + makeDefault(appInfo); + } + + private void makeDefault(PaymentAppInfo appInfo) { + if (!appInfo.isDefault) { + mPaymentBackend.setDefaultPaymentApp(appInfo.componentName); + } + final Dialog dialog = mPreference.getDialog(); + if (dialog != null) { + dialog.dismiss(); + } + } + } +} diff --git a/src/com/android/settings/nfc/NfcPreferenceController.java b/src/com/android/settings/nfc/NfcPreferenceController.java index a74b987560..34e7e24efd 100644 --- a/src/com/android/settings/nfc/NfcPreferenceController.java +++ b/src/com/android/settings/nfc/NfcPreferenceController.java @@ -15,22 +15,25 @@ */ package com.android.settings.nfc; +import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; import android.content.IntentFilter; +import android.net.Uri; import android.nfc.NfcAdapter; import android.provider.Settings; -import android.text.TextUtils; - -import androidx.preference.Preference; +import android.util.Log; +import androidx.annotation.VisibleForTesting; import androidx.preference.PreferenceScreen; import androidx.preference.SwitchPreference; import com.android.settings.core.TogglePreferenceController; +import com.android.settings.slices.SliceBackgroundWorker; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnPause; import com.android.settingslib.core.lifecycle.events.OnResume; -import java.util.List; +import java.io.IOException; public class NfcPreferenceController extends TogglePreferenceController implements LifecycleObserver, OnResume, OnPause { @@ -38,7 +41,6 @@ public class NfcPreferenceController extends TogglePreferenceController public static final String KEY_TOGGLE_NFC = "toggle_nfc"; private final NfcAdapter mNfcAdapter; private NfcEnabler mNfcEnabler; - private NfcAirplaneModeObserver mAirplaneModeObserver; public NfcPreferenceController(Context context, String key) { super(context, key); @@ -53,16 +55,10 @@ public class NfcPreferenceController extends TogglePreferenceController return; } - final SwitchPreference switchPreference = - (SwitchPreference) screen.findPreference(getPreferenceKey()); + final SwitchPreference switchPreference = screen.findPreference(getPreferenceKey()); mNfcEnabler = new NfcEnabler(mContext, switchPreference); - // Manually set dependencies for NFC when not toggleable. - if (!isToggleableInAirplaneMode(mContext)) { - mAirplaneModeObserver = new NfcAirplaneModeObserver(mContext, - mNfcAdapter, (Preference) switchPreference); - } } @Override @@ -89,28 +85,22 @@ public class NfcPreferenceController extends TogglePreferenceController } @Override - public IntentFilter getIntentFilter() { - final IntentFilter filter = new IntentFilter(); - filter.addAction(NfcAdapter.ACTION_ADAPTER_STATE_CHANGED); - filter.addAction(NfcAdapter.EXTRA_ADAPTER_STATE); - return filter; + public boolean hasAsyncUpdate() { + return true; } @Override - public boolean hasAsyncUpdate() { + public boolean isSliceable() { return true; } @Override - public boolean isSliceable() { - return TextUtils.equals(getPreferenceKey(), KEY_TOGGLE_NFC); + public Class<? extends SliceBackgroundWorker> getBackgroundWorkerClass() { + return NfcSliceWorker.class; } @Override public void onResume() { - if (mAirplaneModeObserver != null) { - mAirplaneModeObserver.register(); - } if (mNfcEnabler != null) { mNfcEnabler.resume(); } @@ -118,17 +108,93 @@ public class NfcPreferenceController extends TogglePreferenceController @Override public void onPause() { - if (mAirplaneModeObserver != null) { - mAirplaneModeObserver.unregister(); - } if (mNfcEnabler != null) { mNfcEnabler.pause(); } } + public static boolean shouldTurnOffNFCInAirplaneMode(Context context) { + final String airplaneModeRadios = Settings.Global.getString(context.getContentResolver(), + Settings.Global.AIRPLANE_MODE_RADIOS); + return airplaneModeRadios != null && airplaneModeRadios.contains(Settings.Global.RADIO_NFC); + } + public static boolean isToggleableInAirplaneMode(Context context) { final String toggleable = Settings.Global.getString(context.getContentResolver(), Settings.Global.AIRPLANE_MODE_TOGGLEABLE_RADIOS); return toggleable != null && toggleable.contains(Settings.Global.RADIO_NFC); } + + /** + * Listener for background changes to NFC. + * + * <p> + * Listen to broadcasts from {@link NfcAdapter}. The worker will call notify changed on the + * NFC Slice only when the following extras are present in the broadcast: + * <ul> + * <li>{@link NfcAdapter#STATE_ON}</li> + * <li>{@link NfcAdapter#STATE_OFF}</li> + * </ul> + */ + public static class NfcSliceWorker extends SliceBackgroundWorker<Void> { + + private static final String TAG = "NfcSliceWorker"; + + private static final IntentFilter NFC_FILTER = + new IntentFilter(NfcAdapter.ACTION_ADAPTER_STATE_CHANGED); + + private NfcUpdateReceiver mUpdateObserver; + + public NfcSliceWorker(Context context, Uri uri) { + super(context, uri); + mUpdateObserver = new NfcUpdateReceiver(this); + } + + @Override + protected void onSlicePinned() { + getContext().registerReceiver(mUpdateObserver, NFC_FILTER); + } + + @Override + protected void onSliceUnpinned() { + getContext().unregisterReceiver(mUpdateObserver); + } + + @Override + public void close() throws IOException { + mUpdateObserver = null; + } + + public void updateSlice() { + notifySliceChange(); + } + + public class NfcUpdateReceiver extends BroadcastReceiver { + + private final int NO_EXTRA = -1; + + private final NfcSliceWorker mSliceBackgroundWorker; + + public NfcUpdateReceiver(NfcSliceWorker sliceWorker) { + mSliceBackgroundWorker = sliceWorker; + } + + @Override + public void onReceive(Context context, Intent intent) { + final int nfcStateExtra = intent.getIntExtra(NfcAdapter.EXTRA_ADAPTER_STATE, + NO_EXTRA); + + // Do nothing if state change is empty, or an intermediate step. + if ( (nfcStateExtra == NO_EXTRA) + || (nfcStateExtra == NfcAdapter.STATE_TURNING_ON) + || (nfcStateExtra == NfcAdapter.STATE_TURNING_OFF)) { + Log.d(TAG, "Transitional update, dropping broadcast"); + return; + } + + Log.d(TAG, "Nfc broadcast received, updating Slice."); + mSliceBackgroundWorker.updateSlice(); + } + } + } } diff --git a/src/com/android/settings/nfc/OWNERS b/src/com/android/settings/nfc/OWNERS index f11f74f19d..2017826aef 100644 --- a/src/com/android/settings/nfc/OWNERS +++ b/src/com/android/settings/nfc/OWNERS @@ -1,5 +1,5 @@ # Default reviewers for this and subdirectories. -eisenbach@google.com -kandoiruchi@google.com +rmojumder@google.com -# Emergency approvers in case the above are not available
\ No newline at end of file +# Emergency approvers in case the above are not available +zachoverflow@google.com diff --git a/src/com/android/settings/nfc/PaymentBackend.java b/src/com/android/settings/nfc/PaymentBackend.java index 91cd96cd0b..a87855ea92 100644 --- a/src/com/android/settings/nfc/PaymentBackend.java +++ b/src/com/android/settings/nfc/PaymentBackend.java @@ -25,7 +25,9 @@ import android.nfc.NfcAdapter; import android.nfc.cardemulation.ApduServiceInfo; import android.nfc.cardemulation.CardEmulation; import android.os.Handler; +import android.os.Looper; import android.os.Message; +import android.os.UserHandle; import android.provider.Settings; import android.provider.Settings.SettingNotFoundException; @@ -44,7 +46,6 @@ public class PaymentBackend { public static class PaymentAppInfo { public CharSequence label; CharSequence description; - Drawable banner; boolean isDefault; public ComponentName componentName; public ComponentName settingsComponent; @@ -57,7 +58,7 @@ public class PaymentBackend { // Fields below only modified on UI thread private ArrayList<PaymentAppInfo> mAppInfos; private PaymentAppInfo mDefaultAppInfo; - private ArrayList<Callback> mCallbacks = new ArrayList<Callback>(); + private ArrayList<Callback> mCallbacks = new ArrayList<>(); public PaymentBackend(Context context) { mContext = context; @@ -102,13 +103,13 @@ public class PaymentBackend { appInfo.componentName = service.getComponent(); String settingsActivity = service.getSettingsActivityName(); if (settingsActivity != null) { - appInfo.settingsComponent = new ComponentName(appInfo.componentName.getPackageName(), + appInfo.settingsComponent = new ComponentName( + appInfo.componentName.getPackageName(), settingsActivity); } else { appInfo.settingsComponent = null; } appInfo.description = service.getDescription(); - appInfo.banner = service.loadBanner(pm); appInfos.add(appInfo); } mAppInfos = appInfos; @@ -138,19 +139,6 @@ public class PaymentBackend { } } - Drawable loadDrawableForPackage(String pkgName, int drawableResId) { - PackageManager pm = mContext.getPackageManager(); - try { - Resources res = pm.getResourcesForApplication(pkgName); - Drawable banner = res.getDrawable(drawableResId); - return banner; - } catch (Resources.NotFoundException e) { - return null; - } catch (PackageManager.NameNotFoundException e) { - return null; - } - } - boolean isForegroundMode() { try { return Settings.Secure.getInt(mContext.getContentResolver(), @@ -162,7 +150,7 @@ public class PaymentBackend { void setForegroundMode(boolean foreground) { Settings.Secure.putInt(mContext.getContentResolver(), - Settings.Secure.NFC_PAYMENT_FOREGROUND, foreground ? 1 : 0) ; + Settings.Secure.NFC_PAYMENT_FOREGROUND, foreground ? 1 : 0); } ComponentName getDefaultPaymentApp() { @@ -182,14 +170,23 @@ public class PaymentBackend { refresh(); } - private final Handler mHandler = new Handler() { + private class SettingsPackageMonitor extends PackageMonitor { + private Handler mHandler; + @Override - public void dispatchMessage(Message msg) { - refresh(); + public void register(Context context, Looper thread, UserHandle user, + boolean externalStorage) { + if (mHandler == null) { + mHandler = new Handler(thread) { + @Override + public void dispatchMessage(Message msg) { + refresh(); + } + }; + } + super.register(context, thread, user, externalStorage); } - }; - private class SettingsPackageMonitor extends PackageMonitor { @Override public void onPackageAdded(String packageName, int uid) { mHandler.obtainMessage().sendToTarget(); diff --git a/src/com/android/settings/nfc/PaymentDefaultDialog.java b/src/com/android/settings/nfc/PaymentDefaultDialog.java index 949f87d8b7..73b92e7b6b 100644 --- a/src/com/android/settings/nfc/PaymentDefaultDialog.java +++ b/src/com/android/settings/nfc/PaymentDefaultDialog.java @@ -42,7 +42,11 @@ public final class PaymentDefaultDialog extends AlertActivity implements @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - mBackend = new PaymentBackend(this); + try { + mBackend = new PaymentBackend(this); + } catch (NullPointerException e) { + finish(); + } Intent intent = getIntent(); ComponentName component = intent.getParcelableExtra( CardEmulation.EXTRA_SERVICE_COMPONENT); diff --git a/src/com/android/settings/nfc/PaymentSettings.java b/src/com/android/settings/nfc/PaymentSettings.java index 27ae093484..5742d1a4fa 100644 --- a/src/com/android/settings/nfc/PaymentSettings.java +++ b/src/com/android/settings/nfc/PaymentSettings.java @@ -16,39 +16,44 @@ package com.android.settings.nfc; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; -import android.content.res.Resources; +import android.content.pm.UserInfo; import android.os.Bundle; -import androidx.preference.PreferenceManager; -import androidx.preference.PreferenceScreen; +import android.os.UserHandle; +import android.os.UserManager; +import android.provider.SearchIndexableResource; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; -import com.android.settings.SettingsPreferenceFragment; +import com.android.settings.dashboard.DashboardFragment; import com.android.settings.search.BaseSearchIndexProvider; -import com.android.settings.search.Indexable; -import com.android.settings.search.SearchIndexableRaw; +import com.android.settingslib.search.SearchIndexable; -import java.util.ArrayList; +import java.util.Arrays; import java.util.List; -public class PaymentSettings extends SettingsPreferenceFragment implements Indexable { - public static final String TAG = "PaymentSettings"; - static final String PAYMENT_KEY = "payment"; +@SearchIndexable +public class PaymentSettings extends DashboardFragment { + public static final String TAG = "PaymentSettings"; private PaymentBackend mPaymentBackend; @Override + protected String getLogTag() { + return TAG; + } + + @Override public int getMetricsCategory() { - return MetricsEvent.NFC_PAYMENT; + return SettingsEnums.NFC_PAYMENT; } @Override @@ -57,24 +62,13 @@ public class PaymentSettings extends SettingsPreferenceFragment implements Index } @Override - public void onCreate(Bundle icicle) { - super.onCreate(icicle); - + public void onAttach(Context context) { + super.onAttach(context); mPaymentBackend = new PaymentBackend(getActivity()); setHasOptionsMenu(true); - final PreferenceScreen screen = getPreferenceScreen(); - - List<PaymentBackend.PaymentAppInfo> appInfos = mPaymentBackend.getPaymentAppInfos(); - if (appInfos != null && appInfos.size() > 0) { - NfcPaymentPreference preference = - new NfcPaymentPreference(getPrefContext(), mPaymentBackend); - preference.setKey(PAYMENT_KEY); - screen.addPreference(preference); - NfcForegroundPreference foreground = new NfcForegroundPreference(getPrefContext(), - mPaymentBackend); - screen.addPreference(foreground); - } + use(NfcPaymentPreferenceController.class).setPaymentBackend(mPaymentBackend); + use(NfcForegroundPreferenceController.class).setPaymentBackend(mPaymentBackend); } @Override @@ -109,31 +103,24 @@ public class PaymentSettings extends SettingsPreferenceFragment implements Index } public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = - new BaseSearchIndexProvider() { - @Override - public List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled) { - final List<SearchIndexableRaw> result = new ArrayList<SearchIndexableRaw>(); - final Resources res = context.getResources(); - - // Add fragment title - SearchIndexableRaw data = new SearchIndexableRaw(context); - data.key = PAYMENT_KEY; - data.title = res.getString(R.string.nfc_payment_settings_title); - data.screenTitle = res.getString(R.string.nfc_payment_settings_title); - data.keywords = res.getString(R.string.keywords_payment_settings); - result.add(data); - return result; - } - - @Override - public List<String> getNonIndexableKeys(Context context) { - final List<String> nonVisibleKeys = super.getNonIndexableKeys(context); - final PackageManager pm = context.getPackageManager(); - if (pm.hasSystemFeature(PackageManager.FEATURE_NFC)) { - return nonVisibleKeys; + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, + boolean enabled) { + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.nfc_payment_settings; + return Arrays.asList(sir); + } + + @Override + protected boolean isPageSearchEnabled(Context context) { + final UserManager userManager = context.getSystemService(UserManager.class); + final UserInfo myUserInfo = userManager.getUserInfo(UserHandle.myUserId()); + if (myUserInfo.isGuest()) { + return false; + } + final PackageManager pm = context.getPackageManager(); + return pm.hasSystemFeature(PackageManager.FEATURE_NFC); } - nonVisibleKeys.add(PAYMENT_KEY); - return nonVisibleKeys; - } - }; -} + }; +}
\ No newline at end of file diff --git a/src/com/android/settings/nfc/SecureNfcEnabler.java b/src/com/android/settings/nfc/SecureNfcEnabler.java new file mode 100644 index 0000000000..9acaf6461f --- /dev/null +++ b/src/com/android/settings/nfc/SecureNfcEnabler.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2019 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.nfc; + +import android.content.Context; +import android.nfc.NfcAdapter; +import android.provider.Settings; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.SwitchPreference; + +import com.android.settings.R; + +/** + * SecureNfcEnabler is a helper to manage the Secure Nfc on/off checkbox preference + * It turns on/off Secure NFC and ensures the summary of the preference reflects + * the current state. + */ +public class SecureNfcEnabler extends BaseNfcEnabler { + private final SwitchPreference mPreference; + + public SecureNfcEnabler(Context context, SwitchPreference preference) { + super(context); + mPreference = preference; + } + + @Override + protected void handleNfcStateChanged(int newState) { + switch (newState) { + case NfcAdapter.STATE_OFF: + mPreference.setSummary(R.string.nfc_disabled_summary); + mPreference.setEnabled(false); + break; + case NfcAdapter.STATE_ON: + mPreference.setSummary(R.string.nfc_secure_toggle_summary); + mPreference.setChecked(mPreference.isChecked()); + mPreference.setEnabled(true); + break; + case NfcAdapter.STATE_TURNING_ON: + mPreference.setEnabled(false); + break; + case NfcAdapter.STATE_TURNING_OFF: + mPreference.setEnabled(false); + break; + } + } +} diff --git a/src/com/android/settings/nfc/SecureNfcPreferenceController.java b/src/com/android/settings/nfc/SecureNfcPreferenceController.java new file mode 100644 index 0000000000..12dbd5749c --- /dev/null +++ b/src/com/android/settings/nfc/SecureNfcPreferenceController.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2019 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.nfc; + +import android.content.Context; +import android.nfc.NfcAdapter; + +import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreference; + +import com.android.settings.core.TogglePreferenceController; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnPause; +import com.android.settingslib.core.lifecycle.events.OnResume; + +public class SecureNfcPreferenceController extends TogglePreferenceController + implements LifecycleObserver, OnResume, OnPause { + + private final NfcAdapter mNfcAdapter; + private SecureNfcEnabler mSecureNfcEnabler; + + public SecureNfcPreferenceController(Context context, String key) { + super(context, key); + mNfcAdapter = NfcAdapter.getDefaultAdapter(context); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + if (!isAvailable()) { + mSecureNfcEnabler = null; + return; + } + + final SwitchPreference switchPreference = screen.findPreference(getPreferenceKey()); + + mSecureNfcEnabler = new SecureNfcEnabler(mContext, switchPreference); + } + + @Override + public boolean isChecked() { + return mNfcAdapter.isSecureNfcEnabled(); + } + + @Override + public boolean setChecked(boolean isChecked) { + return mNfcAdapter.enableSecureNfc(isChecked); + } + + @Override + @AvailabilityStatus + public int getAvailabilityStatus() { + if (mNfcAdapter == null) { + return UNSUPPORTED_ON_DEVICE; + } + return mNfcAdapter.isSecureNfcSupported() + ? AVAILABLE + : UNSUPPORTED_ON_DEVICE; + } + + @Override + public boolean hasAsyncUpdate() { + return true; + } + + @Override + public boolean isSliceable() { + return true; + } + + @Override + public void onResume() { + if (mSecureNfcEnabler != null) { + mSecureNfcEnabler.resume(); + } + } + + @Override + public void onPause() { + if (mSecureNfcEnabler != null) { + mSecureNfcEnabler.pause(); + } + } +} diff --git a/src/com/android/settings/notification/ZenModeBehaviorSoundPreferenceController.java b/src/com/android/settings/notification/AbstractZenCustomRulePreferenceController.java index 3d250639e1..409eec62ec 100644 --- a/src/com/android/settings/notification/ZenModeBehaviorSoundPreferenceController.java +++ b/src/com/android/settings/notification/AbstractZenCustomRulePreferenceController.java @@ -16,37 +16,46 @@ package com.android.settings.notification; +import android.app.AutomaticZenRule; import android.content.Context; +import android.os.Bundle; + import androidx.preference.Preference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.core.lifecycle.Lifecycle; -public class ZenModeBehaviorSoundPreferenceController extends +abstract class AbstractZenCustomRulePreferenceController extends AbstractZenModePreferenceController implements PreferenceControllerMixin { - protected static final String KEY_BEHAVIOR_SETTINGS = "zen_sound_vibration_settings"; - private final ZenModeSettings.SummaryBuilder mSummaryBuilder; + String mId; + AutomaticZenRule mRule; - public ZenModeBehaviorSoundPreferenceController(Context context, Lifecycle lifecycle) { - super(context, KEY_BEHAVIOR_SETTINGS, lifecycle); - mSummaryBuilder = new ZenModeSettings.SummaryBuilder(context); + AbstractZenCustomRulePreferenceController(Context context, String key, + Lifecycle lifecycle) { + super(context, key, lifecycle); } @Override - public String getPreferenceKey() { - return KEY_BEHAVIOR_SETTINGS; + public void updateState(Preference preference) { + if (mId != null) { + mRule = mBackend.getAutomaticZenRule(mId); + } } @Override public boolean isAvailable() { - return true; + return mRule != null; } - @Override - public void updateState(Preference preference) { - super.updateState(preference); + public void onResume(AutomaticZenRule rule, String id) { + mId = id; + mRule = rule; + } - preference.setSummary(mSummaryBuilder.getSoundSettingSummary(getPolicy())); + Bundle createBundle() { + Bundle bundle = new Bundle(); + bundle.putString(ZenCustomRuleSettings.RULE_ID, mId); + return bundle; } } diff --git a/src/com/android/settings/notification/AbstractZenModeAutomaticRulePreferenceController.java b/src/com/android/settings/notification/AbstractZenModeAutomaticRulePreferenceController.java index 4284ecfc79..f7b32221ea 100644 --- a/src/com/android/settings/notification/AbstractZenModeAutomaticRulePreferenceController.java +++ b/src/com/android/settings/notification/AbstractZenModeAutomaticRulePreferenceController.java @@ -17,36 +17,33 @@ package com.android.settings.notification; import android.app.AutomaticZenRule; -import android.app.Fragment; import android.app.NotificationManager; +import android.app.settings.SettingsEnums; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ComponentInfo; import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; import android.provider.Settings; import android.service.notification.ConditionProviderService; -import android.service.notification.ZenModeConfig; + +import androidx.fragment.app.Fragment; import androidx.preference.Preference; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.core.lifecycle.Lifecycle; -import java.util.Arrays; -import java.util.Comparator; -import java.util.List; import java.util.Map; -import java.util.Set; abstract public class AbstractZenModeAutomaticRulePreferenceController extends AbstractZenModePreferenceController implements PreferenceControllerMixin { protected ZenModeBackend mBackend; protected Fragment mParent; - protected Set<Map.Entry<String, AutomaticZenRule>> mRules; + protected Map.Entry<String, AutomaticZenRule>[] mRules; protected PackageManager mPm; - private static List<String> mDefaultRuleIds; public AbstractZenModeAutomaticRulePreferenceController(Context context, String key, Fragment parent, Lifecycle lifecycle) { @@ -59,20 +56,14 @@ abstract public class AbstractZenModeAutomaticRulePreferenceController extends @Override public void updateState(Preference preference) { super.updateState(preference); - mRules = getZenModeRules(); + mRules = mBackend.getAutomaticZenRules(); } - private static List<String> getDefaultRuleIds() { - if (mDefaultRuleIds == null) { - mDefaultRuleIds = ZenModeConfig.DEFAULT_RULE_IDS; + protected Map.Entry<String, AutomaticZenRule>[] getRules() { + if (mRules == null) { + mRules = mBackend.getAutomaticZenRules(); } - return mDefaultRuleIds; - } - - private Set<Map.Entry<String, AutomaticZenRule>> getZenModeRules() { - Map<String, AutomaticZenRule> ruleMap = - NotificationManager.from(mContext).getAutomaticZenRules(); - return ruleMap.entrySet(); + return mRules; } protected void showNameRuleDialog(final ZenRuleInfo ri, Fragment parent) { @@ -80,16 +71,6 @@ abstract public class AbstractZenModeAutomaticRulePreferenceController extends RuleNameChangeListener(ri)); } - protected Map.Entry<String, AutomaticZenRule>[] sortedRules() { - if (mRules == null) { - mRules = getZenModeRules(); - } - final Map.Entry<String, AutomaticZenRule>[] rt = - mRules.toArray(new Map.Entry[mRules.size()]); - Arrays.sort(rt, RULE_COMPARATOR); - return rt; - } - protected static Intent getRuleIntent(String settingsAction, ComponentName configurationActivity, String ruleId) { final Intent intent = new Intent() @@ -103,65 +84,53 @@ abstract public class AbstractZenModeAutomaticRulePreferenceController extends return intent; } - private static final Comparator<Map.Entry<String, AutomaticZenRule>> RULE_COMPARATOR = - new Comparator<Map.Entry<String, AutomaticZenRule>>() { - @Override - public int compare(Map.Entry<String, AutomaticZenRule> lhs, - Map.Entry<String, AutomaticZenRule> rhs) { - // if it's a default rule, should be at the top of automatic rules - boolean lhsIsDefaultRule = getDefaultRuleIds().contains(lhs.getKey()); - boolean rhsIsDefaultRule = getDefaultRuleIds().contains(rhs.getKey()); - if (lhsIsDefaultRule != rhsIsDefaultRule) { - return lhsIsDefaultRule ? -1 : 1; - } - - int byDate = Long.compare(lhs.getValue().getCreationTime(), - rhs.getValue().getCreationTime()); - if (byDate != 0) { - return byDate; - } else { - return key(lhs.getValue()).compareTo(key(rhs.getValue())); - } - } - - private String key(AutomaticZenRule rule) { - final int type = ZenModeConfig.isValidScheduleConditionId(rule.getConditionId()) - ? 1 : ZenModeConfig.isValidEventConditionId(rule.getConditionId()) - ? 2 : 3; - return type + rule.getName().toString(); - } - }; - - public static ZenRuleInfo getRuleInfo(PackageManager pm, ServiceInfo si) { - if (si == null || si.metaData == null) { + public static ZenRuleInfo getRuleInfo(PackageManager pm, ComponentInfo ci) { + if (ci == null || ci.metaData == null) { return null; } - final String ruleType = si.metaData.getString(ConditionProviderService.META_DATA_RULE_TYPE); - final ComponentName configurationActivity = getSettingsActivity(si); + final String ruleType = (ci instanceof ServiceInfo) + ? ci.metaData.getString(ConditionProviderService.META_DATA_RULE_TYPE) + : ci.metaData.getString(NotificationManager.META_DATA_AUTOMATIC_RULE_TYPE); + + final ComponentName configurationActivity = getSettingsActivity(null, ci); if (ruleType != null && !ruleType.trim().isEmpty() && configurationActivity != null) { final ZenRuleInfo ri = new ZenRuleInfo(); - ri.serviceComponent = new ComponentName(si.packageName, si.name); + ri.serviceComponent = + (ci instanceof ServiceInfo) ? new ComponentName(ci.packageName, ci.name) : null; ri.settingsAction = Settings.ACTION_ZEN_MODE_EXTERNAL_RULE_SETTINGS; ri.title = ruleType; - ri.packageName = si.packageName; - ri.configurationActivity = getSettingsActivity(si); - ri.packageLabel = si.applicationInfo.loadLabel(pm); - ri.ruleInstanceLimit = - si.metaData.getInt(ConditionProviderService.META_DATA_RULE_INSTANCE_LIMIT, -1); + ri.packageName = ci.packageName; + ri.configurationActivity = configurationActivity; + ri.packageLabel = ci.applicationInfo.loadLabel(pm); + ri.ruleInstanceLimit = (ci instanceof ServiceInfo) + ? ci.metaData.getInt(ConditionProviderService.META_DATA_RULE_INSTANCE_LIMIT, -1) + : ci.metaData.getInt(NotificationManager.META_DATA_RULE_INSTANCE_LIMIT, -1); return ri; } return null; } - protected static ComponentName getSettingsActivity(ServiceInfo si) { - if (si == null || si.metaData == null) { + protected static ComponentName getSettingsActivity(AutomaticZenRule rule, ComponentInfo ci) { + // prefer config activity on the rule itself; fallback to manifest definition + if (rule != null && rule.getConfigurationActivity() != null) { + return rule.getConfigurationActivity(); + } + if (ci == null) { return null; } - final String configurationActivity = - si.metaData.getString(ConditionProviderService.META_DATA_CONFIGURATION_ACTIVITY); - if (configurationActivity != null) { - return ComponentName.unflattenFromString(configurationActivity); + // new activity backed rule + if (ci instanceof ActivityInfo) { + return new ComponentName(ci.packageName, ci.name); } + // old service backed rule + if (ci.metaData != null) { + final String configurationActivity = ci.metaData.getString( + ConditionProviderService.META_DATA_CONFIGURATION_ACTIVITY); + if (configurationActivity != null) { + return ComponentName.unflattenFromString(configurationActivity); + } + } + return null; } @@ -175,9 +144,9 @@ abstract public class AbstractZenModeAutomaticRulePreferenceController extends @Override public void onOk(String ruleName, Fragment parent) { mMetricsFeatureProvider.action(mContext, - MetricsProto.MetricsEvent.ACTION_ZEN_MODE_RULE_NAME_CHANGE_OK); + SettingsEnums.ACTION_ZEN_MODE_RULE_NAME_CHANGE_OK); AutomaticZenRule rule = new AutomaticZenRule(ruleName, mRuleInfo.serviceComponent, - mRuleInfo.defaultConditionId, + mRuleInfo.configurationActivity, mRuleInfo.defaultConditionId, null, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); String savedRuleId = mBackend.addZenRule(rule); if (savedRuleId != null) { diff --git a/src/com/android/settings/notification/AbstractZenModePreferenceController.java b/src/com/android/settings/notification/AbstractZenModePreferenceController.java index 175a607b52..0e45e58c62 100644 --- a/src/com/android/settings/notification/AbstractZenModePreferenceController.java +++ b/src/com/android/settings/notification/AbstractZenModePreferenceController.java @@ -29,10 +29,11 @@ import android.os.UserHandle; import android.provider.Settings; import android.service.notification.ScheduleCalendar; import android.service.notification.ZenModeConfig; + +import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import com.android.internal.annotations.VisibleForTesting; import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.overlay.FeatureFactory; import com.android.settingslib.core.AbstractPreferenceController; @@ -49,7 +50,7 @@ abstract public class AbstractZenModePreferenceController extends @VisibleForTesting protected SettingObserver mSettingObserver; - private final String KEY; + final String KEY; final private NotificationManager mNotificationManager; protected static ZenModeConfigWrapper mZenModeConfigWrapper; protected MetricsFeatureProvider mMetricsFeatureProvider; @@ -116,7 +117,7 @@ abstract public class AbstractZenModePreferenceController extends } protected int getZenDuration() { - return Settings.Global.getInt(mContext.getContentResolver(), Settings.Global.ZEN_DURATION, + return Settings.Secure.getInt(mContext.getContentResolver(), Settings.Secure.ZEN_DURATION, 0); } @@ -124,8 +125,8 @@ abstract public class AbstractZenModePreferenceController extends private final Uri ZEN_MODE_URI = Settings.Global.getUriFor(Settings.Global.ZEN_MODE); private final Uri ZEN_MODE_CONFIG_ETAG_URI = Settings.Global.getUriFor( Settings.Global.ZEN_MODE_CONFIG_ETAG); - private final Uri ZEN_MODE_DURATION_URI = Settings.Global.getUriFor( - Settings.Global.ZEN_DURATION); + private final Uri ZEN_MODE_DURATION_URI = Settings.Secure.getUriFor( + Settings.Secure.ZEN_DURATION); private final Preference mPreference; diff --git a/src/com/android/settings/notification/AdjustVolumeRestrictedPreferenceController.java b/src/com/android/settings/notification/AdjustVolumeRestrictedPreferenceController.java index 2dcc31df8a..bed25cde00 100644 --- a/src/com/android/settings/notification/AdjustVolumeRestrictedPreferenceController.java +++ b/src/com/android/settings/notification/AdjustVolumeRestrictedPreferenceController.java @@ -21,21 +21,20 @@ import android.content.IntentFilter; import android.media.AudioManager; import android.os.UserHandle; import android.os.UserManager; + +import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; -import com.android.internal.annotations.VisibleForTesting; import com.android.settings.accounts.AccountRestrictionHelper; -import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.core.SliderPreferenceController; import com.android.settingslib.RestrictedPreference; -import com.android.settingslib.core.AbstractPreferenceController; /** * Base class for preference controller that handles preference that enforce adjust volume * restriction */ public abstract class AdjustVolumeRestrictedPreferenceController extends - SliderPreferenceController implements PreferenceControllerMixin { + SliderPreferenceController { private AccountRestrictionHelper mHelper; diff --git a/src/com/android/settings/notification/AlarmVolumePreferenceController.java b/src/com/android/settings/notification/AlarmVolumePreferenceController.java index 92a1e90776..d6a4110dcd 100644 --- a/src/com/android/settings/notification/AlarmVolumePreferenceController.java +++ b/src/com/android/settings/notification/AlarmVolumePreferenceController.java @@ -22,8 +22,7 @@ import android.text.TextUtils; import com.android.settings.R; -public class AlarmVolumePreferenceController extends - VolumeSeekBarPreferenceController { +public class AlarmVolumePreferenceController extends VolumeSeekBarPreferenceController { private static final String KEY_ALARM_VOLUME = "alarm_volume"; @@ -43,6 +42,11 @@ public class AlarmVolumePreferenceController extends } @Override + public boolean useDynamicSliceSummary() { + return true; + } + + @Override public String getPreferenceKey() { return KEY_ALARM_VOLUME; } diff --git a/src/com/android/settings/notification/AllowSoundPreferenceController.java b/src/com/android/settings/notification/AllowSoundPreferenceController.java index a1fd3c0c8f..7862f4bb9c 100644 --- a/src/com/android/settings/notification/AllowSoundPreferenceController.java +++ b/src/com/android/settings/notification/AllowSoundPreferenceController.java @@ -22,9 +22,10 @@ import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED; import android.app.NotificationChannel; import android.content.Context; -import androidx.preference.Preference; import android.util.Log; +import androidx.preference.Preference; + import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.RestrictedSwitchPreference; @@ -61,7 +62,7 @@ public class AllowSoundPreferenceController extends NotificationPreferenceContro if (mChannel != null) { RestrictedSwitchPreference pref = (RestrictedSwitchPreference) preference; pref.setDisabledByAdmin(mAdmin); - pref.setEnabled(isChannelConfigurable() && !pref.isDisabledByAdmin()); + pref.setEnabled(!pref.isDisabledByAdmin()); pref.setChecked(mChannel.getImportance() >= IMPORTANCE_DEFAULT || mChannel.getImportance() == IMPORTANCE_UNSPECIFIED); } else { Log.i(TAG, "tried to updatestate on a null channel?!"); } diff --git a/src/com/android/settings/notification/AppBubbleNotificationSettings.java b/src/com/android/settings/notification/AppBubbleNotificationSettings.java new file mode 100644 index 0000000000..2517573f0a --- /dev/null +++ b/src/com/android/settings/notification/AppBubbleNotificationSettings.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2019 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.notification; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.text.TextUtils; +import android.util.Log; + +import com.android.settings.R; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; +import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.search.SearchIndexable; + +import java.util.ArrayList; +import java.util.List; + +@SearchIndexable +public class AppBubbleNotificationSettings extends NotificationSettingsBase implements + GlobalBubblePermissionObserverMixin.Listener { + private static final String TAG = "AppBubNotiSettings"; + private GlobalBubblePermissionObserverMixin mObserverMixin; + + @Override + public int getMetricsCategory() { + return SettingsEnums.APP_BUBBLE_SETTINGS; + } + + @Override + protected String getLogTag() { + return TAG; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.app_bubble_notification_settings; + } + + @Override + protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { + mControllers = getPreferenceControllers(context, this); + return new ArrayList<>(mControllers); + } + + protected static List<NotificationPreferenceController> getPreferenceControllers( + Context context, AppBubbleNotificationSettings fragment) { + List<NotificationPreferenceController> controllers = new ArrayList<>(); + controllers.add(new HeaderPreferenceController(context, fragment)); + controllers.add(new BubblePreferenceController(context, fragment != null + ? fragment.getChildFragmentManager() + : null, + new NotificationBackend(), true /* isAppPage */)); + return controllers; + } + + @Override + public void onGlobalBubblePermissionChanged() { + updatePreferenceStates(); + } + + @Override + public void onResume() { + super.onResume(); + + if (mUid < 0 || TextUtils.isEmpty(mPkg) || mPkgInfo == null) { + Log.w(TAG, "Missing package or uid or packageinfo"); + finish(); + return; + } + + for (NotificationPreferenceController controller : mControllers) { + controller.onResume(mAppRow, mChannel, mChannelGroup, mSuspendedAppsAdmin); + controller.displayPreference(getPreferenceScreen()); + } + updatePreferenceStates(); + + mObserverMixin = new GlobalBubblePermissionObserverMixin(getContext(), this); + mObserverMixin.onStart(); + } + + @Override + public void onPause() { + mObserverMixin.onStop(); + super.onPause(); + } + + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + + @Override + protected boolean isPageSearchEnabled(Context context) { + return false; + } + + @Override + public List<AbstractPreferenceController> createPreferenceControllers(Context + context) { + return new ArrayList<>(AppBubbleNotificationSettings.getPreferenceControllers( + context, null)); + } + }; +} diff --git a/src/com/android/settings/notification/AppLinkPreferenceController.java b/src/com/android/settings/notification/AppLinkPreferenceController.java index 6e09ccebfe..c696e8ef8a 100644 --- a/src/com/android/settings/notification/AppLinkPreferenceController.java +++ b/src/com/android/settings/notification/AppLinkPreferenceController.java @@ -17,6 +17,7 @@ package com.android.settings.notification; import android.content.Context; + import androidx.preference.Preference; import com.android.settings.core.PreferenceControllerMixin; diff --git a/src/com/android/settings/notification/AppNotificationSettings.java b/src/com/android/settings/notification/AppNotificationSettings.java index b5a07ce432..531d8fa02c 100644 --- a/src/com/android/settings/notification/AppNotificationSettings.java +++ b/src/com/android/settings/notification/AppNotificationSettings.java @@ -16,27 +16,24 @@ package com.android.settings.notification; -import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; - import android.app.NotificationChannel; import android.app.NotificationChannelGroup; +import android.app.settings.SettingsEnums; import android.content.Context; import android.os.AsyncTask; import android.os.Bundle; -import androidx.preference.SwitchPreference; +import android.text.TextUtils; +import android.util.Log; + import androidx.preference.Preference; import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceGroup; import androidx.preference.PreferenceScreen; -import android.text.TextUtils; -import android.util.Log; -import android.view.Window; -import android.view.WindowManager; +import androidx.preference.SwitchPreference; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.widget.LockPatternUtils; import com.android.settings.R; -import com.android.settings.widget.MasterCheckBoxPreference; +import com.android.settings.widget.MasterSwitchPreference; import com.android.settingslib.RestrictedSwitchPreference; import com.android.settingslib.core.AbstractPreferenceController; @@ -54,12 +51,14 @@ public class AppNotificationSettings extends NotificationSettingsBase { private static String KEY_ADVANCED_CATEGORY = "app_advanced"; private static String KEY_BADGE = "badge"; private static String KEY_APP_LINK = "app_link"; + private static String KEY_BUBBLE = "bubble_link_pref"; + private static String[] LEGACY_NON_ADVANCED_KEYS = {KEY_BADGE, KEY_APP_LINK, KEY_BUBBLE}; private List<NotificationChannelGroup> mChannelGroupList; @Override public int getMetricsCategory() { - return MetricsEvent.NOTIFICATION_APP_NOTIFICATION; + return SettingsEnums.NOTIFICATION_APP_NOTIFICATION; } @Override @@ -68,15 +67,16 @@ public class AppNotificationSettings extends NotificationSettingsBase { final PreferenceScreen screen = getPreferenceScreen(); if (mShowLegacyChannelConfig && screen != null) { // if showing legacy settings, pull advanced settings out of the advanced category - Preference badge = findPreference(KEY_BADGE); - Preference appLink = findPreference(KEY_APP_LINK); + PreferenceGroup advanced = (PreferenceGroup) findPreference(KEY_ADVANCED_CATEGORY); removePreference(KEY_ADVANCED_CATEGORY); - if (badge != null) { - screen.addPreference(badge); - - } - if (appLink != null) { - screen.addPreference(appLink); + if (advanced != null) { + for (String key : LEGACY_NON_ADVANCED_KEYS) { + Preference pref = advanced.findPreference(key); + advanced.removePreference(pref); + if (pref != null) { + screen.addPreference(pref); + } + } } } } @@ -85,9 +85,6 @@ public class AppNotificationSettings extends NotificationSettingsBase { public void onResume() { super.onResume(); - getActivity().getWindow().addPrivateFlags(PRIVATE_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); - android.util.EventLog.writeEvent(0x534e4554, "119115683", -1, ""); - if (mUid < 0 || TextUtils.isEmpty(mPkg) || mPkgInfo == null) { Log.w(TAG, "Missing package or uid or packageinfo"); finish(); @@ -122,15 +119,6 @@ public class AppNotificationSettings extends NotificationSettingsBase { } @Override - public void onPause() { - super.onPause(); - final Window window = getActivity().getWindow(); - final WindowManager.LayoutParams attrs = window.getAttributes(); - attrs.privateFlags &= ~PRIVATE_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; - window.setAttributes(attrs); - } - - @Override protected String getLogTag() { return TAG; } @@ -150,6 +138,10 @@ public class AppNotificationSettings extends NotificationSettingsBase { context, mImportanceListener, mBackend)); mControllers.add(new ImportancePreferenceController( context, mImportanceListener, mBackend)); + mControllers.add(new MinImportancePreferenceController( + context, mImportanceListener, mBackend)); + mControllers.add(new HighImportancePreferenceController( + context, mImportanceListener, mBackend)); mControllers.add(new SoundPreferenceController(context, this, mImportanceListener, mBackend)); mControllers.add(new LightsPreferenceController(context, mBackend)); @@ -161,6 +153,7 @@ public class AppNotificationSettings extends NotificationSettingsBase { mControllers.add(new DescriptionPreferenceController(context)); mControllers.add(new NotificationsOffPreferenceController(context)); mControllers.add(new DeletedChannelsPreferenceController(context, mBackend)); + mControllers.add(new BubbleSummaryPreferenceController(context, mBackend)); return new ArrayList<>(mControllers); } @@ -263,7 +256,7 @@ public class AppNotificationSettings extends NotificationSettingsBase { int childCount = groupGroup.getPreferenceCount(); for (int i = 0; i < childCount; i++) { Preference pref = groupGroup.getPreference(i); - if (pref instanceof MasterCheckBoxPreference) { + if (pref instanceof MasterSwitchPreference) { toRemove.add(pref); } } diff --git a/src/com/android/settings/notification/AssistantCapabilityPreferenceController.java b/src/com/android/settings/notification/AssistantCapabilityPreferenceController.java new file mode 100644 index 0000000000..812bc65e7a --- /dev/null +++ b/src/com/android/settings/notification/AssistantCapabilityPreferenceController.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2019 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.notification; + +import android.content.Context; +import android.service.notification.Adjustment; + +import com.android.settings.core.TogglePreferenceController; + +import com.google.common.annotations.VisibleForTesting; + +import java.util.List; + +public class AssistantCapabilityPreferenceController extends TogglePreferenceController { + + static final String PRIORITIZER_KEY = "asst_capability_prioritizer"; + static final String SMART_KEY = "asst_capabilities_actions_replies"; + private NotificationBackend mBackend; + + public AssistantCapabilityPreferenceController(Context context, String key) { + super(context, key); + mBackend = new NotificationBackend(); + } + + @VisibleForTesting + void setBackend(NotificationBackend backend) { + mBackend = backend; + } + + @Override + public boolean isChecked() { + List<String> capabilities = mBackend.getAssistantAdjustments(mContext.getPackageName()); + if (PRIORITIZER_KEY.equals(getPreferenceKey())) { + return capabilities.contains(Adjustment.KEY_IMPORTANCE); + } else if (SMART_KEY.equals(getPreferenceKey())) { + return capabilities.contains(Adjustment.KEY_CONTEXTUAL_ACTIONS) + && capabilities.contains(Adjustment.KEY_TEXT_REPLIES); + } + return false; + } + + @Override + public boolean setChecked(boolean isChecked) { + if (PRIORITIZER_KEY.equals(getPreferenceKey())) { + mBackend.allowAssistantAdjustment(Adjustment.KEY_IMPORTANCE, isChecked); + } else if (SMART_KEY.equals(getPreferenceKey())) { + mBackend.allowAssistantAdjustment(Adjustment.KEY_CONTEXTUAL_ACTIONS, isChecked); + mBackend.allowAssistantAdjustment(Adjustment.KEY_TEXT_REPLIES, isChecked); + } + return true; + } + + @Override + public int getAvailabilityStatus() { + return mBackend.getAllowedNotificationAssistant() != null + ? AVAILABLE : DISABLED_DEPENDENT_SETTING; + } +} + + diff --git a/src/com/android/settings/notification/AudioHelper.java b/src/com/android/settings/notification/AudioHelper.java index 5f745c8a69..01945fd5f6 100644 --- a/src/com/android/settings/notification/AudioHelper.java +++ b/src/com/android/settings/notification/AudioHelper.java @@ -22,6 +22,8 @@ import android.media.AudioManager; import android.media.AudioSystem; import android.os.UserHandle; import android.os.UserManager; +import android.util.Log; + import com.android.settings.Utils; /** @@ -29,6 +31,7 @@ import com.android.settings.Utils; */ public class AudioHelper { + private static final String TAG = "AudioHelper"; private Context mContext; private AudioManager mAudioManager; @@ -73,4 +76,17 @@ public class AudioHelper { public int getMaxVolume(int stream) { return mAudioManager.getStreamMaxVolume(stream); } + + public int getMinVolume(int stream) { + int minVolume; + try { + minVolume = mAudioManager.getStreamMinVolume(stream); + } catch (IllegalArgumentException e) { + Log.w(TAG, "Invalid stream type " + stream); + // Fallback to STREAM_VOICE_CALL because CallVolumePreferenceController.java default + // return STREAM_VOICE_CALL in getAudioStream + minVolume = mAudioManager.getStreamMinVolume(AudioManager.STREAM_VOICE_CALL); + } + return minVolume; + } } diff --git a/src/com/android/settings/notification/BadgePreferenceController.java b/src/com/android/settings/notification/BadgePreferenceController.java index e6cd4494d2..e40324e4f8 100644 --- a/src/com/android/settings/notification/BadgePreferenceController.java +++ b/src/com/android/settings/notification/BadgePreferenceController.java @@ -18,9 +18,9 @@ package com.android.settings.notification; import static android.provider.Settings.Secure.NOTIFICATION_BADGING; -import android.app.NotificationChannel; import android.content.Context; import android.provider.Settings; + import androidx.preference.Preference; import com.android.settings.core.PreferenceControllerMixin; @@ -60,7 +60,7 @@ public class BadgePreferenceController extends NotificationPreferenceController if (isDefaultChannel()) { return true; } else { - return mAppRow.showBadge; + return mAppRow == null ? false : mAppRow.showBadge; } } return true; @@ -72,7 +72,7 @@ public class BadgePreferenceController extends NotificationPreferenceController pref.setDisabledByAdmin(mAdmin); if (mChannel != null) { pref.setChecked(mChannel.canShowBadge()); - pref.setEnabled(isChannelConfigurable() && !pref.isDisabledByAdmin()); + pref.setEnabled(!pref.isDisabledByAdmin()); } else { pref.setChecked(mAppRow.showBadge); } diff --git a/src/com/android/settings/notification/BadgingNotificationPreferenceController.java b/src/com/android/settings/notification/BadgingNotificationPreferenceController.java index 94419c4928..613204b937 100644 --- a/src/com/android/settings/notification/BadgingNotificationPreferenceController.java +++ b/src/com/android/settings/notification/BadgingNotificationPreferenceController.java @@ -16,32 +16,26 @@ package com.android.settings.notification; +import static android.provider.Settings.Secure.NOTIFICATION_BADGING; + import android.content.ContentResolver; import android.content.Context; -import android.content.Intent; import android.database.ContentObserver; import android.net.Uri; import android.os.Handler; import android.provider.Settings; +import android.text.TextUtils; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import androidx.preference.TwoStatePreference; -import android.text.TextUtils; import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.core.TogglePreferenceController; -import com.android.settingslib.core.AbstractPreferenceController; -import com.android.settings.R; -import com.android.settings.search.DatabaseIndexingUtils; -import com.android.settings.search.InlineSwitchPayload; -import com.android.settings.search.ResultPayload; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnPause; import com.android.settingslib.core.lifecycle.events.OnResume; -import static android.provider.Settings.Secure.NOTIFICATION_BADGING; - public class BadgingNotificationPreferenceController extends TogglePreferenceController implements PreferenceControllerMixin, Preference.OnPreferenceChangeListener, LifecycleObserver, OnResume, OnPause { @@ -133,15 +127,4 @@ public class BadgingNotificationPreferenceController extends TogglePreferenceCon } } } - - @Override - public ResultPayload getResultPayload() { - final Intent intent = DatabaseIndexingUtils.buildSearchResultPageIntent(mContext, - ConfigureNotificationSettings.class.getName(), getPreferenceKey(), - mContext.getString(R.string.configure_notification_settings)); - - return new InlineSwitchPayload(Settings.Secure.NOTIFICATION_BADGING, - ResultPayload.SettingsSource.SECURE, ON /* onValue */, intent, isAvailable(), - ON /* defaultValue */); - } } diff --git a/src/com/android/settings/notification/BlockPreferenceController.java b/src/com/android/settings/notification/BlockPreferenceController.java index 65d3119398..37589d9ea8 100644 --- a/src/com/android/settings/notification/BlockPreferenceController.java +++ b/src/com/android/settings/notification/BlockPreferenceController.java @@ -16,20 +16,20 @@ package com.android.settings.notification; -import static android.app.NotificationChannel.DEFAULT_CHANNEL_ID; import static android.app.NotificationManager.IMPORTANCE_DEFAULT; import static android.app.NotificationManager.IMPORTANCE_NONE; import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED; import android.app.NotificationManager; import android.content.Context; -import androidx.preference.Preference; import android.widget.Switch; +import androidx.preference.Preference; + import com.android.settings.R; -import com.android.settings.applications.LayoutPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.widget.SwitchBar; +import com.android.settingslib.widget.LayoutPreference; public class BlockPreferenceController extends NotificationPreferenceController implements PreferenceControllerMixin, SwitchBar.OnSwitchChangeListener { @@ -54,17 +54,12 @@ public class BlockPreferenceController extends NotificationPreferenceController if (mAppRow == null) { return false; } - if (mChannel != null) { - return isChannelBlockable(); - } else if (mChannelGroup != null) { - return isChannelGroupBlockable(); - } else { - return !mAppRow.systemApp || (mAppRow.systemApp && mAppRow.banned); - } + return true; } public void updateState(Preference preference) { LayoutPreference pref = (LayoutPreference) preference; + pref.setSelectable(false); SwitchBar bar = pref.findViewById(R.id.switch_bar); if (bar != null) { bar.setSwitchBarText(R.string.notification_switch_label, @@ -77,6 +72,19 @@ public class BlockPreferenceController extends NotificationPreferenceController } bar.setDisabledByAdmin(mAdmin); + if (mChannel != null && !isChannelBlockable()) { + bar.setEnabled(false); + } + + if (mChannelGroup != null && !isChannelGroupBlockable()) { + bar.setEnabled(false); + } + + if (mChannel == null && mAppRow.systemApp + && (!mAppRow.banned || mAppRow.lockedImportance)) { + bar.setEnabled(false); + } + if (mChannel != null) { bar.setChecked(!mAppRow.banned && mChannel.getImportance() != NotificationManager.IMPORTANCE_NONE); diff --git a/src/com/android/settings/notification/BootSoundPreferenceController.java b/src/com/android/settings/notification/BootSoundPreferenceController.java index 97639b1cd5..2e7327df51 100644 --- a/src/com/android/settings/notification/BootSoundPreferenceController.java +++ b/src/com/android/settings/notification/BootSoundPreferenceController.java @@ -18,10 +18,12 @@ package com.android.settings.notification; import android.content.Context; import android.os.SystemProperties; + import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreference; + import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.core.AbstractPreferenceController; @@ -41,7 +43,7 @@ public class BootSoundPreferenceController extends AbstractPreferenceController public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); if (isAvailable()) { - SwitchPreference preference = (SwitchPreference) screen.findPreference(KEY_BOOT_SOUNDS); + SwitchPreference preference = screen.findPreference(KEY_BOOT_SOUNDS); preference.setChecked(SystemProperties.getBoolean(PROPERTY_BOOT_SOUNDS, true)); } } diff --git a/src/com/android/settings/notification/BubblePreferenceController.java b/src/com/android/settings/notification/BubblePreferenceController.java new file mode 100644 index 0000000000..b68f11db0c --- /dev/null +++ b/src/com/android/settings/notification/BubblePreferenceController.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2019 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.notification; + +import static android.provider.Settings.Secure.NOTIFICATION_BUBBLES; + +import android.annotation.Nullable; +import android.content.Context; +import android.provider.Settings; + +import com.android.settings.R; +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settingslib.RestrictedSwitchPreference; + +import androidx.annotation.VisibleForTesting; +import androidx.fragment.app.FragmentManager; +import androidx.preference.Preference; + +public class BubblePreferenceController extends NotificationPreferenceController + implements PreferenceControllerMixin, Preference.OnPreferenceChangeListener { + + private static final String TAG = "BubblePrefContr"; + private static final String KEY = "bubble_pref"; + @VisibleForTesting + static final int SYSTEM_WIDE_ON = 1; + @VisibleForTesting + static final int SYSTEM_WIDE_OFF = 0; + + private FragmentManager mFragmentManager; + private boolean mIsAppPage; + + public BubblePreferenceController(Context context, @Nullable FragmentManager fragmentManager, + NotificationBackend backend, boolean isAppPage) { + super(context, backend); + mFragmentManager = fragmentManager; + mIsAppPage = isAppPage; + } + + @Override + public String getPreferenceKey() { + return KEY; + } + + @Override + public boolean isAvailable() { + if (!super.isAvailable()) { + return false; + } + if (!mIsAppPage && !isGloballyEnabled()) { + return false; + } + if (mChannel != null) { + if (isDefaultChannel()) { + return true; + } else { + return mAppRow != null && mAppRow.allowBubbles; + } + } + return true; + } + + public void updateState(Preference preference) { + if (mAppRow != null) { + RestrictedSwitchPreference pref = (RestrictedSwitchPreference) preference; + pref.setDisabledByAdmin(mAdmin); + if (mChannel != null) { + pref.setChecked(mChannel.canBubble() && isGloballyEnabled()); + pref.setEnabled(!pref.isDisabledByAdmin()); + } else { + pref.setChecked(mAppRow.allowBubbles && isGloballyEnabled()); + pref.setSummary(mContext.getString( + R.string.bubbles_app_toggle_summary, mAppRow.label)); + } + } + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + final boolean value = (Boolean) newValue && isGloballyEnabled(); + if (mChannel != null) { + mChannel.setAllowBubbles(value); + saveChannel(); + return true; + } else if (mAppRow != null && mFragmentManager != null) { + RestrictedSwitchPreference pref = (RestrictedSwitchPreference) preference; + // if the global setting is off, toggling app level permission requires extra + // confirmation + if (!isGloballyEnabled() && !pref.isChecked()) { + new BubbleWarningDialogFragment() + .setPkgInfo(mAppRow.pkg, mAppRow.uid) + .show(mFragmentManager, "dialog"); + return false; + } else { + mAppRow.allowBubbles = value; + mBackend.setAllowBubbles(mAppRow.pkg, mAppRow.uid, value); + } + } + return true; + } + + private boolean isGloballyEnabled() { + return Settings.Secure.getInt(mContext.getContentResolver(), + NOTIFICATION_BUBBLES, SYSTEM_WIDE_OFF) == SYSTEM_WIDE_ON; + } + + // Used in app level prompt that confirms the user is ok with turning on bubbles + // globally. If they aren't, undo what + public static void revertBubblesApproval(Context mContext, String pkg, int uid) { + NotificationBackend backend = new NotificationBackend(); + backend.setAllowBubbles(pkg, uid, false); + // changing the global settings will cause the observer on the host page to reload + // correct preference state + Settings.Secure.putInt(mContext.getContentResolver(), + NOTIFICATION_BUBBLES, SYSTEM_WIDE_OFF); + } + + // Apply global bubbles approval + public static void applyBubblesApproval(Context mContext, String pkg, int uid) { + NotificationBackend backend = new NotificationBackend(); + backend.setAllowBubbles(pkg, uid, true); + // changing the global settings will cause the observer on the host page to reload + // correct preference state + Settings.Secure.putInt(mContext.getContentResolver(), + NOTIFICATION_BUBBLES, SYSTEM_WIDE_ON); + } +} diff --git a/src/com/android/settings/notification/BubbleSummaryPreferenceController.java b/src/com/android/settings/notification/BubbleSummaryPreferenceController.java new file mode 100644 index 0000000000..b1632c4d1e --- /dev/null +++ b/src/com/android/settings/notification/BubbleSummaryPreferenceController.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2019 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.notification; + +import static android.provider.Settings.Secure.NOTIFICATION_BUBBLES; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.os.Bundle; +import android.provider.Settings; + +import com.android.settings.R; +import com.android.settings.applications.AppInfoBase; +import com.android.settings.core.SubSettingLauncher; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; + +public class BubbleSummaryPreferenceController extends NotificationPreferenceController { + + private static final String KEY = "bubble_link_pref"; + @VisibleForTesting + static final int SYSTEM_WIDE_ON = 1; + @VisibleForTesting + static final int SYSTEM_WIDE_OFF = 0; + + public BubbleSummaryPreferenceController(Context context, NotificationBackend backend) { + super(context, backend); + } + + @Override + public String getPreferenceKey() { + return KEY; + } + + @Override + public boolean isAvailable() { + if (!super.isAvailable()) { + return false; + } + if (mAppRow == null && mChannel == null) { + return false; + } + if (mChannel != null) { + if (!isGloballyEnabled()) { + return false; + } + if (isDefaultChannel()) { + return true; + } else { + return mAppRow != null && mAppRow.allowBubbles; + } + } + return isGloballyEnabled(); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + + if (mAppRow != null) { + Bundle args = new Bundle(); + args.putString(AppInfoBase.ARG_PACKAGE_NAME, mAppRow.pkg); + args.putInt(AppInfoBase.ARG_PACKAGE_UID, mAppRow.uid); + + preference.setIntent(new SubSettingLauncher(mContext) + .setDestination(AppBubbleNotificationSettings.class.getName()) + .setArguments(args) + .setSourceMetricsCategory( + SettingsEnums.NOTIFICATION_APP_NOTIFICATION) + .toIntent()); + } + } + + @Override + public CharSequence getSummary() { + boolean canBubble = false; + if (mAppRow != null) { + if (mChannel != null) { + canBubble |= mChannel.canBubble() && isGloballyEnabled(); + } else { + canBubble |= mAppRow.allowBubbles && isGloballyEnabled(); + } + } + return mContext.getString(canBubble ? R.string.switch_on_text : R.string.switch_off_text); + } + + private boolean isGloballyEnabled() { + return Settings.Secure.getInt(mContext.getContentResolver(), + NOTIFICATION_BUBBLES, SYSTEM_WIDE_OFF) == SYSTEM_WIDE_ON; + } +} diff --git a/src/com/android/settings/notification/BubbleWarningDialogFragment.java b/src/com/android/settings/notification/BubbleWarningDialogFragment.java new file mode 100644 index 0000000000..5086fb044a --- /dev/null +++ b/src/com/android/settings/notification/BubbleWarningDialogFragment.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2019 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.notification; + +import android.app.Dialog; +import android.app.settings.SettingsEnums; +import android.os.Bundle; + +import androidx.appcompat.app.AlertDialog; + +import com.android.settings.R; +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; + +public class BubbleWarningDialogFragment extends InstrumentedDialogFragment { + static final String KEY_PKG = "p"; + static final String KEY_UID = "u"; + + + @Override + public int getMetricsCategory() { + return SettingsEnums.DIALOG_APP_BUBBLE_SETTINGS; + } + + public BubbleWarningDialogFragment setPkgInfo(String pkg, int uid) { + Bundle args = new Bundle(); + args.putString(KEY_PKG, pkg); + args.putInt(KEY_UID, uid); + setArguments(args); + return this; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + final Bundle args = getArguments(); + final String pkg = args.getString(KEY_PKG); + final int uid = args.getInt(KEY_UID); + + final String title = + getResources().getString(R.string.bubbles_feature_disabled_dialog_title); + final String summary = getResources() + .getString(R.string.bubbles_feature_disabled_dialog_text); + return new AlertDialog.Builder(getContext()) + .setMessage(summary) + .setTitle(title) + .setCancelable(true) + .setPositiveButton(R.string.bubbles_feature_disabled_button_approve, + (dialog, id) -> + BubblePreferenceController.applyBubblesApproval( + getContext(), pkg, uid)) + .setNegativeButton(R.string.bubbles_feature_disabled_button_cancel, + (dialog, id) -> + BubblePreferenceController.revertBubblesApproval( + getContext(), pkg, uid)) + .create(); + } +} diff --git a/src/com/android/settings/notification/CallVolumePreferenceController.java b/src/com/android/settings/notification/CallVolumePreferenceController.java index a3eb87f898..ded57b326b 100644 --- a/src/com/android/settings/notification/CallVolumePreferenceController.java +++ b/src/com/android/settings/notification/CallVolumePreferenceController.java @@ -44,6 +44,11 @@ public class CallVolumePreferenceController extends VolumeSeekBarPreferenceContr } @Override + public boolean useDynamicSliceSummary() { + return true; + } + + @Override public int getAudioStream() { if (mAudioManager.isBluetoothScoOn()) { return AudioManager.STREAM_BLUETOOTH_SCO; diff --git a/src/com/android/settings/notification/ChannelGroupNotificationSettings.java b/src/com/android/settings/notification/ChannelGroupNotificationSettings.java index c17b3b0700..1f8b1c3321 100644 --- a/src/com/android/settings/notification/ChannelGroupNotificationSettings.java +++ b/src/com/android/settings/notification/ChannelGroupNotificationSettings.java @@ -17,11 +17,12 @@ package com.android.settings.notification; import android.app.NotificationChannel; +import android.app.settings.SettingsEnums; import android.content.Context; -import androidx.preference.Preference; import android.util.Log; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.preference.Preference; + import com.android.settings.R; import com.android.settingslib.core.AbstractPreferenceController; @@ -34,7 +35,7 @@ public class ChannelGroupNotificationSettings extends NotificationSettingsBase { @Override public int getMetricsCategory() { - return MetricsEvent.NOTIFICATION_CHANNEL_GROUP; + return SettingsEnums.NOTIFICATION_CHANNEL_GROUP; } @Override diff --git a/src/com/android/settings/notification/ChannelNotificationSettings.java b/src/com/android/settings/notification/ChannelNotificationSettings.java index 0ea0eaccff..8399a49499 100644 --- a/src/com/android/settings/notification/ChannelNotificationSettings.java +++ b/src/com/android/settings/notification/ChannelNotificationSettings.java @@ -16,18 +16,18 @@ package com.android.settings.notification; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.preference.PreferenceManager; -import androidx.preference.PreferenceScreen; import android.text.TextUtils; import android.util.Log; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.preference.PreferenceScreen; + import com.android.internal.widget.LockPatternUtils; import com.android.settings.R; -import com.android.settings.applications.AppInfoBase; import com.android.settingslib.core.AbstractPreferenceController; import java.util.ArrayList; @@ -38,7 +38,7 @@ public class ChannelNotificationSettings extends NotificationSettingsBase { @Override public int getMetricsCategory() { - return MetricsEvent.NOTIFICATION_TOPIC_NOTIFICATION; + return SettingsEnums.NOTIFICATION_TOPIC_NOTIFICATION; } @Override @@ -97,6 +97,10 @@ public class ChannelNotificationSettings extends NotificationSettingsBase { mControllers.add(new BlockPreferenceController(context, mImportanceListener, mBackend)); mControllers.add(new ImportancePreferenceController( context, mImportanceListener, mBackend)); + mControllers.add(new MinImportancePreferenceController( + context, mImportanceListener, mBackend)); + mControllers.add(new HighImportancePreferenceController( + context, mImportanceListener, mBackend)); mControllers.add(new AllowSoundPreferenceController( context, mImportanceListener, mBackend)); mControllers.add(new SoundPreferenceController(context, this, @@ -110,6 +114,8 @@ public class ChannelNotificationSettings extends NotificationSettingsBase { mControllers.add(new BadgePreferenceController(context, mBackend)); mControllers.add(new DndPreferenceController(context, mBackend)); mControllers.add(new NotificationsOffPreferenceController(context)); + mControllers.add(new BubblePreferenceController(context, getChildFragmentManager(), + mBackend, false /* isAppPage */)); return new ArrayList<>(mControllers); } } diff --git a/src/com/android/settings/notification/ChargingSoundPreferenceController.java b/src/com/android/settings/notification/ChargingSoundPreferenceController.java index e280177ce6..c7cd232a03 100644 --- a/src/com/android/settings/notification/ChargingSoundPreferenceController.java +++ b/src/com/android/settings/notification/ChargingSoundPreferenceController.java @@ -16,11 +16,11 @@ package com.android.settings.notification; -import static com.android.settings.notification.SettingPref.TYPE_GLOBAL; +import static com.android.settings.notification.SettingPref.TYPE_SECURE; import android.content.Context; +import android.provider.Settings.Secure; -import android.provider.Settings.Global; import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; import com.android.settingslib.core.lifecycle.Lifecycle; @@ -33,7 +33,7 @@ public class ChargingSoundPreferenceController extends SettingPrefController { Lifecycle lifecycle) { super(context, parent, lifecycle); mPreference = new SettingPref( - TYPE_GLOBAL, KEY_CHARGING_SOUNDS, Global.CHARGING_SOUNDS_ENABLED, DEFAULT_ON); + TYPE_SECURE, KEY_CHARGING_SOUNDS, Secure.CHARGING_SOUNDS_ENABLED, DEFAULT_ON); } @Override diff --git a/src/com/android/settings/notification/ConfigureNotificationSettings.java b/src/com/android/settings/notification/ConfigureNotificationSettings.java index bd7a82ae8e..d21be1607b 100644 --- a/src/com/android/settings/notification/ConfigureNotificationSettings.java +++ b/src/com/android/settings/notification/ConfigureNotificationSettings.java @@ -16,56 +16,61 @@ package com.android.settings.notification; +import static com.android.settings.SettingsActivity.EXTRA_FRAGMENT_ARG_KEY; + import android.app.Activity; import android.app.Application; -import android.app.Fragment; +import android.app.settings.SettingsEnums; +import android.app.usage.IUsageStatsManager; import android.content.Context; import android.content.Intent; import android.os.Bundle; +import android.os.ServiceManager; import android.os.UserHandle; +import android.os.UserManager; import android.provider.SearchIndexableResource; +import android.text.TextUtils; + import androidx.annotation.VisibleForTesting; +import androidx.fragment.app.Fragment; import androidx.preference.Preference; -import android.text.TextUtils; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceScreen; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.RingtonePreference; +import com.android.settings.core.OnActivityResultListener; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.dashboard.SummaryLoader; -import com.android.settings.gestures.SwipeToNotificationPreferenceController; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -public class ConfigureNotificationSettings extends DashboardFragment { +@SearchIndexable +public class ConfigureNotificationSettings extends DashboardFragment implements + OnActivityResultListener { private static final String TAG = "ConfigNotiSettings"; @VisibleForTesting - static final String KEY_LOCKSCREEN = "lock_screen_notifications"; - @VisibleForTesting - static final String KEY_LOCKSCREEN_WORK_PROFILE_HEADER = - "lock_screen_notifications_profile_header"; - @VisibleForTesting - static final String KEY_LOCKSCREEN_WORK_PROFILE = "lock_screen_notifications_profile"; - @VisibleForTesting static final String KEY_SWIPE_DOWN = "gesture_swipe_down_fingerprint_notifications"; + static final String KEY_LOCKSCREEN = "lock_screen_notifications"; private static final String KEY_NOTI_DEFAULT_RINGTONE = "notification_default_ringtone"; - private static final String KEY_ZEN_MODE = "zen_mode_notifications"; - - private RingtonePreference mRequestPreference; private static final int REQUEST_CODE = 200; private static final String SELECTED_PREFERENCE_KEY = "selected_preference"; + private static final String KEY_ADVANCED_CATEGORY = "configure_notifications_advanced"; + + private RingtonePreference mRequestPreference; @Override public int getMetricsCategory() { - return MetricsEvent.CONFIGURE_NOTIFICATION; + return SettingsEnums.CONFIGURE_NOTIFICATION; } @Override @@ -87,27 +92,18 @@ public class ConfigureNotificationSettings extends DashboardFragment { } else { app = null; } - return buildPreferenceControllers(context, getLifecycle(), app, this); + return buildPreferenceControllers(context, app, this); } private static List<AbstractPreferenceController> buildPreferenceControllers(Context context, - Lifecycle lifecycle, Application app, Fragment host) { + Application app, Fragment host) { final List<AbstractPreferenceController> controllers = new ArrayList<>(); - final PulseNotificationPreferenceController pulseController = - new PulseNotificationPreferenceController(context); - final LockScreenNotificationPreferenceController lockScreenNotificationController = - new LockScreenNotificationPreferenceController(context, - KEY_LOCKSCREEN, - KEY_LOCKSCREEN_WORK_PROFILE_HEADER, - KEY_LOCKSCREEN_WORK_PROFILE); - if (lifecycle != null) { - lifecycle.addObserver(pulseController); - lifecycle.addObserver(lockScreenNotificationController); - } controllers.add(new RecentNotifyingAppsPreferenceController( - context, new NotificationBackend(), app, host)); - controllers.add(pulseController); - controllers.add(lockScreenNotificationController); + context, new NotificationBackend(), IUsageStatsManager.Stub.asInterface( + ServiceManager.getService(Context.USAGE_STATS_SERVICE)), + context.getSystemService(UserManager.class), app, host)); + controllers.add(new ShowOnLockScreenNotificationPreferenceController( + context, KEY_LOCKSCREEN)); controllers.add(new NotificationRingtonePreferenceController(context) { @Override public String getPreferenceKey() { @@ -115,16 +111,36 @@ public class ConfigureNotificationSettings extends DashboardFragment { } }); - controllers.add(new ZenModePreferenceController(context, lifecycle, KEY_ZEN_MODE)); return controllers; } @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + final PreferenceScreen screen = getPreferenceScreen(); + final Bundle arguments = getArguments(); + + if (screen == null) { + return; + } + if (arguments != null) { + final String highlightKey = arguments.getString(EXTRA_FRAGMENT_ARG_KEY); + if (!TextUtils.isEmpty(highlightKey)) { + final PreferenceCategory advancedCategory = + screen.findPreference(KEY_ADVANCED_CATEGORY); + // Has highlight row - expand everything + advancedCategory.setInitialExpandedChildrenCount(Integer.MAX_VALUE); + scrollToPreference(advancedCategory); + } + } + } + + @Override public boolean onPreferenceTreeClick(Preference preference) { if (preference instanceof RingtonePreference) { mRequestPreference = (RingtonePreference) preference; mRequestPreference.onPrepareRingtonePickerIntent(mRequestPreference.getIntent()); - startActivityForResultAsUser( + getActivity().startActivityForResultAsUser( mRequestPreference.getIntent(), REQUEST_CODE, null, @@ -214,17 +230,13 @@ public class ConfigureNotificationSettings extends DashboardFragment { @Override public List<AbstractPreferenceController> createPreferenceControllers( Context context) { - return buildPreferenceControllers(context, null, null, null); + return buildPreferenceControllers(context, null, null); } @Override public List<String> getNonIndexableKeys(Context context) { final List<String> keys = super.getNonIndexableKeys(context); keys.add(KEY_SWIPE_DOWN); - keys.add(KEY_LOCKSCREEN); - keys.add(KEY_LOCKSCREEN_WORK_PROFILE); - keys.add(KEY_LOCKSCREEN_WORK_PROFILE_HEADER); - keys.add(KEY_ZEN_MODE); return keys; } }; diff --git a/src/com/android/settings/notification/DefaultNotificationTonePreference.java b/src/com/android/settings/notification/DefaultNotificationTonePreference.java index 3a7d3b1922..4a2e0b610d 100644 --- a/src/com/android/settings/notification/DefaultNotificationTonePreference.java +++ b/src/com/android/settings/notification/DefaultNotificationTonePreference.java @@ -17,25 +17,15 @@ package com.android.settings.notification; -import com.android.settings.DefaultRingtonePreference; -import com.android.settings.Utils; - -import android.content.ContentResolver; import android.content.Context; import android.content.Intent; -import android.database.Cursor; -import android.database.sqlite.SQLiteException; import android.media.Ringtone; import android.media.RingtoneManager; -import android.os.AsyncTask; -import android.os.UserHandle; -import android.os.UserManager; import android.net.Uri; -import android.provider.MediaStore; -import android.provider.OpenableColumns; +import android.os.AsyncTask; import android.util.AttributeSet; -import static android.content.ContentProvider.getUriWithoutUserId; +import com.android.settings.DefaultRingtonePreference; public class DefaultNotificationTonePreference extends DefaultRingtonePreference { private Uri mRingtone; diff --git a/src/com/android/settings/notification/DeletedChannelsPreferenceController.java b/src/com/android/settings/notification/DeletedChannelsPreferenceController.java index 1ebc01c133..14e0c84e2f 100644 --- a/src/com/android/settings/notification/DeletedChannelsPreferenceController.java +++ b/src/com/android/settings/notification/DeletedChannelsPreferenceController.java @@ -17,6 +17,7 @@ package com.android.settings.notification; import android.content.Context; + import androidx.preference.Preference; import com.android.settings.R; diff --git a/src/com/android/settings/notification/DescriptionPreferenceController.java b/src/com/android/settings/notification/DescriptionPreferenceController.java index 61a9ddb96c..1e996463aa 100644 --- a/src/com/android/settings/notification/DescriptionPreferenceController.java +++ b/src/com/android/settings/notification/DescriptionPreferenceController.java @@ -17,9 +17,10 @@ package com.android.settings.notification; import android.content.Context; -import androidx.preference.Preference; import android.text.TextUtils; +import androidx.preference.Preference; + import com.android.settings.core.PreferenceControllerMixin; public class DescriptionPreferenceController extends NotificationPreferenceController diff --git a/src/com/android/settings/notification/DialPadTonePreferenceController.java b/src/com/android/settings/notification/DialPadTonePreferenceController.java index e0ca299683..1b05ce2118 100644 --- a/src/com/android/settings/notification/DialPadTonePreferenceController.java +++ b/src/com/android/settings/notification/DialPadTonePreferenceController.java @@ -19,8 +19,8 @@ package com.android.settings.notification; import static com.android.settings.notification.SettingPref.TYPE_SYSTEM; import android.content.Context; - import android.provider.Settings.System; + import com.android.settings.SettingsPreferenceFragment; import com.android.settings.Utils; import com.android.settingslib.core.lifecycle.Lifecycle; diff --git a/src/com/android/settings/notification/DndPreferenceController.java b/src/com/android/settings/notification/DndPreferenceController.java index acbc0704bb..4ec76f1c6d 100644 --- a/src/com/android/settings/notification/DndPreferenceController.java +++ b/src/com/android/settings/notification/DndPreferenceController.java @@ -17,15 +17,12 @@ package com.android.settings.notification; import android.app.NotificationChannel; -import android.app.NotificationManager; import android.content.Context; + import androidx.preference.Preference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.RestrictedSwitchPreference; -import com.android.settingslib.core.lifecycle.Lifecycle; -import com.android.settingslib.core.lifecycle.LifecycleObserver; -import com.android.settingslib.core.lifecycle.events.OnResume; public class DndPreferenceController extends NotificationPreferenceController implements PreferenceControllerMixin, Preference.OnPreferenceChangeListener { @@ -53,7 +50,7 @@ public class DndPreferenceController extends NotificationPreferenceController if (mChannel != null) { RestrictedSwitchPreference pref = (RestrictedSwitchPreference) preference; pref.setDisabledByAdmin(mAdmin); - pref.setEnabled(isChannelConfigurable() && !pref.isDisabledByAdmin()); + pref.setEnabled(!pref.isDisabledByAdmin()); pref.setChecked(mChannel.canBypassDnd()); } } diff --git a/src/com/android/settings/notification/DockAudioMediaPreferenceController.java b/src/com/android/settings/notification/DockAudioMediaPreferenceController.java index f7f13c58ff..d9367d58ab 100644 --- a/src/com/android/settings/notification/DockAudioMediaPreferenceController.java +++ b/src/com/android/settings/notification/DockAudioMediaPreferenceController.java @@ -19,10 +19,9 @@ package com.android.settings.notification; import static com.android.settings.notification.SettingPref.TYPE_GLOBAL; import android.content.Context; - import android.content.res.Resources; import android.provider.Settings.Global; -import android.telephony.TelephonyManager; + import com.android.settings.SettingsPreferenceFragment; import com.android.settingslib.core.lifecycle.Lifecycle; diff --git a/src/com/android/settings/notification/DockingSoundPreferenceController.java b/src/com/android/settings/notification/DockingSoundPreferenceController.java index 476fd6a990..f3a281ec4c 100644 --- a/src/com/android/settings/notification/DockingSoundPreferenceController.java +++ b/src/com/android/settings/notification/DockingSoundPreferenceController.java @@ -20,6 +20,7 @@ import static com.android.settings.notification.SettingPref.TYPE_GLOBAL; import android.content.Context; import android.provider.Settings.Global; + import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; import com.android.settingslib.core.lifecycle.Lifecycle; diff --git a/src/com/android/settings/notification/EmergencyBroadcastPreferenceController.java b/src/com/android/settings/notification/EmergencyBroadcastPreferenceController.java index e67de2b48c..7add167ab8 100644 --- a/src/com/android/settings/notification/EmergencyBroadcastPreferenceController.java +++ b/src/com/android/settings/notification/EmergencyBroadcastPreferenceController.java @@ -20,6 +20,7 @@ import android.content.Context; import android.content.pm.PackageManager; import android.os.UserHandle; import android.os.UserManager; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; diff --git a/src/com/android/settings/notification/EmergencyTonePreferenceController.java b/src/com/android/settings/notification/EmergencyTonePreferenceController.java index a67ec8aac3..9c2a9584eb 100644 --- a/src/com/android/settings/notification/EmergencyTonePreferenceController.java +++ b/src/com/android/settings/notification/EmergencyTonePreferenceController.java @@ -19,10 +19,10 @@ package com.android.settings.notification; import static com.android.settings.notification.SettingPref.TYPE_GLOBAL; import android.content.Context; - import android.content.res.Resources; import android.provider.Settings.Global; import android.telephony.TelephonyManager; + import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; import com.android.settingslib.core.lifecycle.Lifecycle; diff --git a/src/com/android/settings/notification/GentleNotificationsPreferenceController.java b/src/com/android/settings/notification/GentleNotificationsPreferenceController.java new file mode 100644 index 0000000000..ea16e72684 --- /dev/null +++ b/src/com/android/settings/notification/GentleNotificationsPreferenceController.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2019 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.notification; + +import static android.provider.Settings.Secure.NOTIFICATION_BUBBLES; + +import android.content.Context; +import android.provider.Settings; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; + +import androidx.annotation.VisibleForTesting; + +public class GentleNotificationsPreferenceController extends BasePreferenceController { + + @VisibleForTesting + static final int ON = 1; + + private NotificationBackend mBackend; + + public GentleNotificationsPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + mBackend = new NotificationBackend(); + } + + @VisibleForTesting + void setBackend(NotificationBackend backend) { + mBackend = backend; + } + + @Override + public CharSequence getSummary() { + boolean showOnLockscreen = showOnLockscreen(); + boolean showOnStatusBar = showOnStatusBar(); + + if (showOnLockscreen) { + if (showOnStatusBar) { + return mContext.getString( + R.string.gentle_notifications_display_summary_shade_status_lock); + } else { + return mContext.getString(R.string.gentle_notifications_display_summary_shade_lock); + } + } else if (showOnStatusBar) { + return mContext.getString(R.string.gentle_notifications_display_summary_shade_status); + } else { + return mContext.getString(R.string.gentle_notifications_display_summary_shade); + } + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } + + private boolean showOnLockscreen() { + return Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, ON) == ON; + } + + private boolean showOnStatusBar() { + return !mBackend.shouldHideSilentStatusBarIcons(mContext); + } +} diff --git a/src/com/android/settings/notification/GlobalBubblePermissionObserverMixin.java b/src/com/android/settings/notification/GlobalBubblePermissionObserverMixin.java new file mode 100644 index 0000000000..398931dc51 --- /dev/null +++ b/src/com/android/settings/notification/GlobalBubblePermissionObserverMixin.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2019 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.notification; + +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.provider.Settings; + +public class GlobalBubblePermissionObserverMixin extends ContentObserver { + + public interface Listener { + void onGlobalBubblePermissionChanged(); + } + + private final Context mContext; + private final Listener mListener; + + public GlobalBubblePermissionObserverMixin(Context context, Listener listener) { + super(new Handler(Looper.getMainLooper())); + mContext = context; + mListener = listener; + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + if (mListener != null) { + mListener.onGlobalBubblePermissionChanged(); + } + } + + public void onStart() { + mContext.getContentResolver().registerContentObserver( + Settings.Secure.getUriFor( + Settings.Secure.NOTIFICATION_BUBBLES), + false /* notifyForDescendants */, + this /* observer */); + } + + public void onStop() { + mContext.getContentResolver().unregisterContentObserver(this /* observer */); + } +}
\ No newline at end of file diff --git a/src/com/android/settings/notification/HeaderPreferenceController.java b/src/com/android/settings/notification/HeaderPreferenceController.java index 29b8670a6a..be5f45eacc 100644 --- a/src/com/android/settings/notification/HeaderPreferenceController.java +++ b/src/com/android/settings/notification/HeaderPreferenceController.java @@ -19,31 +19,32 @@ package com.android.settings.notification; import static com.android.settings.widget.EntityHeaderController.PREF_KEY_APP_HEADER; import android.app.Activity; -import androidx.lifecycle.LifecycleObserver; -import androidx.lifecycle.OnLifecycleEvent; import android.content.Context; -import androidx.preference.PreferenceFragment; -import androidx.preference.Preference; import android.text.BidiFormatter; import android.text.SpannableStringBuilder; import android.text.TextUtils; -import android.util.Slog; import android.view.View; +import androidx.annotation.VisibleForTesting; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.OnLifecycleEvent; +import androidx.preference.Preference; + import com.android.settings.R; -import com.android.settings.applications.LayoutPreference; import com.android.settings.core.PreferenceControllerMixin; +import com.android.settings.dashboard.DashboardFragment; import com.android.settings.widget.EntityHeaderController; import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.widget.LayoutPreference; public class HeaderPreferenceController extends NotificationPreferenceController implements PreferenceControllerMixin, LifecycleObserver { - private final PreferenceFragment mFragment; + private final DashboardFragment mFragment; private EntityHeaderController mHeaderController; private boolean mStarted = false; - public HeaderPreferenceController(Context context, PreferenceFragment fragment) { + public HeaderPreferenceController(Context context, DashboardFragment fragment) { super(context, null); mFragment = fragment; } @@ -68,9 +69,13 @@ public class HeaderPreferenceController extends NotificationPreferenceController activity = mFragment.getActivity(); } + if (activity == null) { + return; + } + LayoutPreference pref = (LayoutPreference) preference; mHeaderController = EntityHeaderController.newInstance( - mFragment.getActivity(), mFragment, pref.findViewById(R.id.entity_header)); + activity, mFragment, pref.findViewById(R.id.entity_header)); pref = mHeaderController.setIcon(mAppRow.icon) .setLabel(getLabel()) .setSummary(getSummary()) @@ -79,18 +84,12 @@ public class HeaderPreferenceController extends NotificationPreferenceController .setButtonActions(EntityHeaderController.ActionType.ACTION_NOTIF_PREFERENCE, EntityHeaderController.ActionType.ACTION_NONE) .setHasAppInfoLink(true) + .setRecyclerView(mFragment.getListView(), mFragment.getSettingsLifecycle()) .done(activity, mContext); pref.findViewById(R.id.entity_header).setVisibility(View.VISIBLE); } } - CharSequence getLabel() { - return (mChannel != null && !isDefaultChannel()) ? mChannel.getName() - : mChannelGroup != null - ? mChannelGroup.getName() - : mAppRow.label; - } - @Override public CharSequence getSummary() { if (mChannel != null && !isDefaultChannel()) { @@ -120,4 +119,12 @@ public class HeaderPreferenceController extends NotificationPreferenceController mHeaderController.styleActionBar(mFragment.getActivity()); } } + + @VisibleForTesting + CharSequence getLabel() { + return (mChannel != null && !isDefaultChannel()) ? mChannel.getName() + : mChannelGroup != null + ? mChannelGroup.getName() + : mAppRow.label; + } } diff --git a/src/com/android/settings/notification/HighImportancePreferenceController.java b/src/com/android/settings/notification/HighImportancePreferenceController.java new file mode 100644 index 0000000000..da9b3b4653 --- /dev/null +++ b/src/com/android/settings/notification/HighImportancePreferenceController.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2019 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.notification; + +import static android.app.NotificationManager.IMPORTANCE_DEFAULT; +import static android.app.NotificationManager.IMPORTANCE_HIGH; + +import android.app.NotificationChannel; +import android.content.Context; + +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settingslib.RestrictedSwitchPreference; + +import androidx.preference.Preference; + +public class HighImportancePreferenceController extends NotificationPreferenceController + implements PreferenceControllerMixin, Preference.OnPreferenceChangeListener { + + private static final String KEY_IMPORTANCE = "high_importance"; + private NotificationSettingsBase.ImportanceListener mImportanceListener; + + public HighImportancePreferenceController(Context context, + NotificationSettingsBase.ImportanceListener importanceListener, + NotificationBackend backend) { + super(context, backend); + mImportanceListener = importanceListener; + } + + @Override + public String getPreferenceKey() { + return KEY_IMPORTANCE; + } + + @Override + public boolean isAvailable() { + if (!super.isAvailable()) { + return false; + } + if (mChannel == null) { + return false; + } + if (isDefaultChannel()) { + return false; + } + return mChannel.getImportance() >= IMPORTANCE_DEFAULT; + } + + @Override + public void updateState(Preference preference) { + if (mAppRow != null && mChannel != null) { + preference.setEnabled(mAdmin == null && !mChannel.isImportanceLockedByOEM()); + + RestrictedSwitchPreference pref = (RestrictedSwitchPreference) preference; + pref.setChecked(mChannel.getImportance() >= IMPORTANCE_HIGH); + } + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + if (mChannel != null) { + final boolean checked = (boolean) newValue; + + mChannel.setImportance(checked ? IMPORTANCE_HIGH : IMPORTANCE_DEFAULT); + mChannel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE); + saveChannel(); + mImportanceListener.onImportanceChanged(); + } + return true; + } +} diff --git a/src/com/android/settings/notification/ImportancePreference.java b/src/com/android/settings/notification/ImportancePreference.java new file mode 100644 index 0000000000..b059f9165e --- /dev/null +++ b/src/com/android/settings/notification/ImportancePreference.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2019 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.notification; + +import static android.app.NotificationManager.IMPORTANCE_DEFAULT; +import static android.app.NotificationManager.IMPORTANCE_HIGH; +import static android.app.NotificationManager.IMPORTANCE_LOW; +import static android.app.NotificationManager.IMPORTANCE_MIN; +import static android.view.View.GONE; +import static android.view.View.VISIBLE; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.drawable.Drawable; +import android.transition.AutoTransition; +import android.transition.Transition; +import android.transition.TransitionManager; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.settings.Utils; +import com.android.settingslib.R; + +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + +public class ImportancePreference extends Preference { + + private boolean mIsConfigurable = true; + private int mImportance; + private boolean mDisplayInStatusBar; + private boolean mDisplayOnLockscreen; + private View mSilenceButton; + private View mAlertButton; + private Context mContext; + Drawable selectedBackground; + Drawable unselectedBackground; + private static final int BUTTON_ANIM_TIME_MS = 100; + private static final boolean SHOW_BUTTON_SUMMARY = false; + + public ImportancePreference(Context context, AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context); + } + + public ImportancePreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context); + } + + public ImportancePreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public ImportancePreference(Context context) { + super(context); + init(context); + } + + private void init(Context context) { + mContext = context; + selectedBackground = mContext.getDrawable(R.drawable.button_border_selected); + unselectedBackground = mContext.getDrawable(R.drawable.button_border_unselected); + setLayoutResource(R.layout.notif_importance_preference); + } + + public void setImportance(int importance) { + mImportance = importance; + } + + public void setConfigurable(boolean configurable) { + mIsConfigurable = configurable; + } + + public void setDisplayInStatusBar(boolean display) { + mDisplayInStatusBar = display; + } + + public void setDisplayOnLockscreen(boolean display) { + mDisplayOnLockscreen = display; + } + + @Override + public void onBindViewHolder(final PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + holder.itemView.setClickable(false); + + mSilenceButton = holder.findViewById(R.id.silence); + mAlertButton = holder.findViewById(R.id.alert); + + if (!mIsConfigurable) { + mSilenceButton.setEnabled(false); + mAlertButton.setEnabled(false); + } + + setImportanceSummary((ViewGroup) holder.itemView, mImportance, false); + switch (mImportance) { + case IMPORTANCE_MIN: + case IMPORTANCE_LOW: + mAlertButton.setBackground(unselectedBackground); + mSilenceButton.setBackground(selectedBackground); + mSilenceButton.setSelected(true); + break; + case IMPORTANCE_HIGH: + default: + mSilenceButton.setBackground(unselectedBackground); + mAlertButton.setBackground(selectedBackground); + mAlertButton.setSelected(true); + break; + } + + mSilenceButton.setOnClickListener(v -> { + callChangeListener(IMPORTANCE_LOW); + mAlertButton.setBackground(unselectedBackground); + mSilenceButton.setBackground(selectedBackground); + setImportanceSummary((ViewGroup) holder.itemView, IMPORTANCE_LOW, true); + // a11y service won't always read the newly appearing text in the right order if the + // selection happens too soon (readback happens on a different thread as layout). post + // the selection to make that conflict less likely + holder.itemView.post(() -> { + mAlertButton.setSelected(false); + mSilenceButton.setSelected(true); + }); + }); + mAlertButton.setOnClickListener(v -> { + callChangeListener(IMPORTANCE_DEFAULT); + mSilenceButton.setBackground(unselectedBackground); + mAlertButton.setBackground(selectedBackground); + setImportanceSummary((ViewGroup) holder.itemView, IMPORTANCE_DEFAULT, true); + holder.itemView.post(() -> { + mSilenceButton.setSelected(false); + mAlertButton.setSelected(true); + }); + }); + } + + private ColorStateList getAccentTint() { + return Utils.getColorAccent(getContext()); + } + + private ColorStateList getRegularTint() { + return Utils.getColorAttr(getContext(), android.R.attr.textColorPrimary); + } + + void setImportanceSummary(ViewGroup parent, int importance, boolean fromUser) { + if (fromUser) { + AutoTransition transition = new AutoTransition(); + transition.setDuration(BUTTON_ANIM_TIME_MS); + TransitionManager.beginDelayedTransition(parent, transition); + } + + ColorStateList colorAccent = getAccentTint(); + ColorStateList colorNormal = getRegularTint(); + + if (importance >= IMPORTANCE_DEFAULT) { + parent.findViewById(R.id.silence_summary).setVisibility(GONE); + ((ImageView) parent.findViewById(R.id.silence_icon)).setImageTintList(colorNormal); + ((TextView) parent.findViewById(R.id.silence_label)).setTextColor(colorNormal); + + ((ImageView) parent.findViewById(R.id.alert_icon)).setImageTintList(colorAccent); + ((TextView) parent.findViewById(R.id.alert_label)).setTextColor(colorAccent); + + parent.findViewById(R.id.alert_summary).setVisibility(VISIBLE); + } else { + parent.findViewById(R.id.alert_summary).setVisibility(GONE); + ((ImageView) parent.findViewById(R.id.alert_icon)).setImageTintList(colorNormal); + ((TextView) parent.findViewById(R.id.alert_label)).setTextColor(colorNormal); + + ((ImageView) parent.findViewById(R.id.silence_icon)).setImageTintList(colorAccent); + ((TextView) parent.findViewById(R.id.silence_label)).setTextColor(colorAccent); + parent.findViewById(R.id.silence_summary).setVisibility(VISIBLE); + } + } +} diff --git a/src/com/android/settings/notification/ImportancePreferenceController.java b/src/com/android/settings/notification/ImportancePreferenceController.java index c05647240f..4ef459370d 100644 --- a/src/com/android/settings/notification/ImportancePreferenceController.java +++ b/src/com/android/settings/notification/ImportancePreferenceController.java @@ -18,20 +18,16 @@ package com.android.settings.notification; import static android.app.NotificationChannel.USER_LOCKED_SOUND; import static android.app.NotificationManager.IMPORTANCE_DEFAULT; -import static android.app.NotificationManager.IMPORTANCE_HIGH; -import static android.app.NotificationManager.IMPORTANCE_MIN; -import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED; import android.app.NotificationChannel; -import android.app.NotificationManager; import android.content.Context; import android.media.RingtoneManager; -import androidx.preference.Preference; +import android.provider.Settings; -import com.android.settings.R; -import com.android.settings.RestrictedListPreference; import com.android.settings.core.PreferenceControllerMixin; +import androidx.preference.Preference; + public class ImportancePreferenceController extends NotificationPreferenceController implements PreferenceControllerMixin, Preference.OnPreferenceChangeListener { @@ -64,32 +60,20 @@ public class ImportancePreferenceController extends NotificationPreferenceContro @Override public void updateState(Preference preference) { if (mAppRow!= null && mChannel != null) { - preference.setEnabled(mAdmin == null && isChannelConfigurable()); - preference.setSummary(getImportanceSummary(mChannel)); - - int importances = IMPORTANCE_HIGH - IMPORTANCE_MIN + 1; - CharSequence[] entries = new CharSequence[importances]; - CharSequence[] values = new CharSequence[importances]; - - int index = 0; - for (int i = IMPORTANCE_HIGH; i >= IMPORTANCE_MIN; i--) { - NotificationChannel channel = new NotificationChannel("", "", i); - entries[index] = getImportanceSummary(channel); - values[index] = String.valueOf(i); - index++; - } - - RestrictedListPreference pref = (RestrictedListPreference) preference; - pref.setEntries(entries); - pref.setEntryValues(values); - pref.setValue(String.valueOf(mChannel.getImportance())); + preference.setEnabled(mAdmin == null && !mChannel.isImportanceLockedByOEM()); + ImportancePreference pref = (ImportancePreference) preference; + pref.setConfigurable(!mChannel.isImportanceLockedByOEM()); + pref.setImportance(mChannel.getImportance()); + pref.setDisplayInStatusBar(mBackend.showSilentInStatusBar(mContext.getPackageName())); + pref.setDisplayOnLockscreen(Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, 1) == 1); } } @Override public boolean onPreferenceChange(Preference preference, Object newValue) { if (mChannel != null) { - final int importance = Integer.parseInt((String) newValue); + final int importance = (Integer) newValue; // If you are moving from an importance level without sound to one with sound, // but the sound you had selected was "Silence", @@ -110,39 +94,4 @@ public class ImportancePreferenceController extends NotificationPreferenceContro } return true; } - - protected String getImportanceSummary(NotificationChannel channel) { - String summary = ""; - int importance = channel.getImportance(); - switch (importance) { - case IMPORTANCE_UNSPECIFIED: - summary = mContext.getString(R.string.notification_importance_unspecified); - break; - case NotificationManager.IMPORTANCE_MIN: - summary = mContext.getString(R.string.notification_importance_min); - break; - case NotificationManager.IMPORTANCE_LOW: - summary = mContext.getString(R.string.notification_importance_low); - break; - case NotificationManager.IMPORTANCE_DEFAULT: - if (SoundPreferenceController.hasValidSound(channel)) { - summary = mContext.getString(R.string.notification_importance_default); - } else { - summary = mContext.getString(R.string.notification_importance_low); - } - break; - case NotificationManager.IMPORTANCE_HIGH: - case NotificationManager.IMPORTANCE_MAX: - if (SoundPreferenceController.hasValidSound(channel)) { - summary = mContext.getString(R.string.notification_importance_high); - } else { - summary = mContext.getString(R.string.notification_importance_high_silent); - } - break; - default: - return ""; - } - - return summary; - } } diff --git a/src/com/android/settings/notification/LightsPreferenceController.java b/src/com/android/settings/notification/LightsPreferenceController.java index 3710cc5a99..21dabbf74a 100644 --- a/src/com/android/settings/notification/LightsPreferenceController.java +++ b/src/com/android/settings/notification/LightsPreferenceController.java @@ -16,17 +16,14 @@ package com.android.settings.notification; -import android.app.NotificationChannel; import android.app.NotificationManager; import android.content.Context; import android.provider.Settings; + import androidx.preference.Preference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.RestrictedSwitchPreference; -import com.android.settingslib.core.lifecycle.Lifecycle; -import com.android.settingslib.core.lifecycle.LifecycleObserver; -import com.android.settingslib.core.lifecycle.events.OnResume; public class LightsPreferenceController extends NotificationPreferenceController implements PreferenceControllerMixin, Preference.OnPreferenceChangeListener { @@ -59,7 +56,7 @@ public class LightsPreferenceController extends NotificationPreferenceController if (mChannel != null) { RestrictedSwitchPreference pref = (RestrictedSwitchPreference) preference; pref.setDisabledByAdmin(mAdmin); - pref.setEnabled(isChannelConfigurable() && !pref.isDisabledByAdmin()); + pref.setEnabled(!pref.isDisabledByAdmin()); pref.setChecked(mChannel.shouldShowLights()); } } diff --git a/src/com/android/settings/notification/LockScreenNotificationPreferenceController.java b/src/com/android/settings/notification/LockScreenNotificationPreferenceController.java index 518d2a949b..1c024e362c 100644 --- a/src/com/android/settings/notification/LockScreenNotificationPreferenceController.java +++ b/src/com/android/settings/notification/LockScreenNotificationPreferenceController.java @@ -28,11 +28,12 @@ import android.os.Handler; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; import android.text.TextUtils; import android.util.Log; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + import com.android.internal.widget.LockPatternUtils; import com.android.settings.R; import com.android.settings.RestrictedListPreference; @@ -40,6 +41,7 @@ import com.android.settings.Utils; import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.overlay.FeatureFactory; import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnPause; @@ -90,13 +92,13 @@ public class LockScreenNotificationPreferenceController extends AbstractPreferen @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - mLockscreen = (RestrictedListPreference) screen.findPreference(mSettingKey); + mLockscreen = screen.findPreference(mSettingKey); if (mLockscreen == null) { Log.i(TAG, "Preference not found: " + mSettingKey); return; } if (mProfileUserId != UserHandle.USER_NULL) { - mLockscreenProfile = (RestrictedListPreference) screen.findPreference(mWorkSettingKey); + mLockscreenProfile = screen.findPreference(mWorkSettingKey); mLockscreenProfile.setRequiresActiveUnlockedProfile(true); mLockscreenProfile.setProfileUserId(mProfileUserId); } else { @@ -245,7 +247,7 @@ public class LockScreenNotificationPreferenceController extends AbstractPreferen private void setRestrictedIfNotificationFeaturesDisabled(CharSequence entry, CharSequence entryValue, int keyguardNotificationFeatures) { RestrictedLockUtils.EnforcedAdmin admin = - RestrictedLockUtils.checkIfKeyguardFeaturesDisabled( + RestrictedLockUtilsInternal.checkIfKeyguardFeaturesDisabled( mContext, keyguardNotificationFeatures, UserHandle.myUserId()); if (admin != null && mLockscreen != null) { RestrictedListPreference.RestrictedItem item = @@ -254,7 +256,7 @@ public class LockScreenNotificationPreferenceController extends AbstractPreferen } if (mProfileUserId != UserHandle.USER_NULL) { RestrictedLockUtils.EnforcedAdmin profileAdmin = - RestrictedLockUtils.checkIfKeyguardFeaturesDisabled( + RestrictedLockUtilsInternal.checkIfKeyguardFeaturesDisabled( mContext, keyguardNotificationFeatures, mProfileUserId); if (profileAdmin != null && mLockscreenProfile != null) { RestrictedListPreference.RestrictedItem item = diff --git a/src/com/android/settings/notification/MediaVolumePreferenceController.java b/src/com/android/settings/notification/MediaVolumePreferenceController.java index 74ce7695f3..ae3146cbe5 100644 --- a/src/com/android/settings/notification/MediaVolumePreferenceController.java +++ b/src/com/android/settings/notification/MediaVolumePreferenceController.java @@ -22,8 +22,7 @@ import android.text.TextUtils; import com.android.settings.R; -public class MediaVolumePreferenceController extends - VolumeSeekBarPreferenceController { +public class MediaVolumePreferenceController extends VolumeSeekBarPreferenceController { private static final String KEY_MEDIA_VOLUME = "media_volume"; @@ -44,6 +43,11 @@ public class MediaVolumePreferenceController extends } @Override + public boolean useDynamicSliceSummary() { + return true; + } + + @Override public String getPreferenceKey() { return KEY_MEDIA_VOLUME; } diff --git a/src/com/android/settings/notification/MinImportancePreferenceController.java b/src/com/android/settings/notification/MinImportancePreferenceController.java new file mode 100644 index 0000000000..0af0c8dd33 --- /dev/null +++ b/src/com/android/settings/notification/MinImportancePreferenceController.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2017 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.notification; + +import static android.app.NotificationManager.IMPORTANCE_LOW; +import static android.app.NotificationManager.IMPORTANCE_MIN; + +import android.app.NotificationChannel; +import android.content.Context; + +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settingslib.RestrictedSwitchPreference; + +import androidx.preference.Preference; + +public class MinImportancePreferenceController extends NotificationPreferenceController + implements PreferenceControllerMixin, Preference.OnPreferenceChangeListener { + + private static final String KEY_IMPORTANCE = "min_importance"; + private NotificationSettingsBase.ImportanceListener mImportanceListener; + + public MinImportancePreferenceController(Context context, + NotificationSettingsBase.ImportanceListener importanceListener, + NotificationBackend backend) { + super(context, backend); + mImportanceListener = importanceListener; + } + + @Override + public String getPreferenceKey() { + return KEY_IMPORTANCE; + } + + @Override + public boolean isAvailable() { + if (!super.isAvailable()) { + return false; + } + if (mChannel == null) { + return false; + } + if (isDefaultChannel()) { + return false; + } + return mChannel.getImportance() <= IMPORTANCE_LOW; + } + + @Override + public void updateState(Preference preference) { + if (mAppRow != null && mChannel != null) { + preference.setEnabled(mAdmin == null && !mChannel.isImportanceLockedByOEM()); + + RestrictedSwitchPreference pref = (RestrictedSwitchPreference) preference; + pref.setChecked(mChannel.getImportance() == IMPORTANCE_MIN); + } + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + if (mChannel != null) { + final boolean checked = (boolean) newValue; + + mChannel.setImportance(checked ? IMPORTANCE_MIN : IMPORTANCE_LOW); + mChannel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE); + saveChannel(); + mImportanceListener.onImportanceChanged(); + } + return true; + } +} diff --git a/src/com/android/settings/notification/NotificationAccessConfirmationActivity.java b/src/com/android/settings/notification/NotificationAccessConfirmationActivity.java index 16d255b0c1..8677c0226d 100644 --- a/src/com/android/settings/notification/NotificationAccessConfirmationActivity.java +++ b/src/com/android/settings/notification/NotificationAccessConfirmationActivity.java @@ -17,12 +17,9 @@ package com.android.settings.notification; -import static com.android.internal.notification.NotificationAccessConfirmationActivityContract - .EXTRA_COMPONENT_NAME; -import static com.android.internal.notification.NotificationAccessConfirmationActivityContract - .EXTRA_PACKAGE_TITLE; -import static com.android.internal.notification.NotificationAccessConfirmationActivityContract - .EXTRA_USER_ID; +import static com.android.internal.notification.NotificationAccessConfirmationActivityContract.EXTRA_COMPONENT_NAME; +import static com.android.internal.notification.NotificationAccessConfirmationActivityContract.EXTRA_PACKAGE_TITLE; +import static com.android.internal.notification.NotificationAccessConfirmationActivityContract.EXTRA_USER_ID; import android.Manifest; import android.annotation.Nullable; @@ -87,13 +84,13 @@ public class NotificationAccessConfirmationActivity extends Activity public void onResume() { super.onResume(); getWindow().addFlags( - WindowManager.LayoutParams.PRIVATE_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); + WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); } @Override public void onPause() { getWindow().clearFlags( - WindowManager.LayoutParams.PRIVATE_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); + WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); super.onPause(); } diff --git a/src/com/android/settings/notification/NotificationAccessSettings.java b/src/com/android/settings/notification/NotificationAccessSettings.java index 4180a53721..81edc5b65d 100644 --- a/src/com/android/settings/notification/NotificationAccessSettings.java +++ b/src/com/android/settings/notification/NotificationAccessSettings.java @@ -16,30 +16,41 @@ package com.android.settings.notification; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.Fragment; import android.app.NotificationManager; +import android.app.settings.SettingsEnums; import android.content.ComponentName; import android.content.Context; import android.os.AsyncTask; import android.os.Bundle; +import android.os.UserManager; +import android.provider.SearchIndexableResource; import android.provider.Settings; import android.service.notification.NotificationListenerService; +import android.widget.Toast; + +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; import com.android.settings.overlay.FeatureFactory; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; import com.android.settings.utils.ManagedServiceSettings; +import com.android.settingslib.search.SearchIndexable; + +import java.util.ArrayList; +import java.util.List; /** * Settings screen for managing notification listener permissions */ +@SearchIndexable public class NotificationAccessSettings extends ManagedServiceSettings { - private static final String TAG = NotificationAccessSettings.class.getSimpleName(); - private static final Config CONFIG = new Config.Builder() + private static final String TAG = "NotificationAccessSettings"; + private static final Config CONFIG = new Config.Builder() .setTag(TAG) .setSetting(Settings.Secure.ENABLED_NOTIFICATION_LISTENERS) .setIntentAction(NotificationListenerService.SERVICE_INTERFACE) @@ -53,8 +64,20 @@ public class NotificationAccessSettings extends ManagedServiceSettings { private NotificationManager mNm; @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + final Context ctx = getContext(); + if (UserManager.get(ctx).isManagedProfile()) { + // Apps in the work profile do not support notification listeners. + Toast.makeText(ctx, R.string.notification_settings_work_profile, Toast.LENGTH_SHORT) + .show(); + finish(); + } + } + + @Override public int getMetricsCategory() { - return MetricsEvent.NOTIFICATION_ACCESS; + return SettingsEnums.NOTIFICATION_ACCESS; } @Override @@ -109,8 +132,8 @@ public class NotificationAccessSettings extends ManagedServiceSettings { @VisibleForTesting void logSpecialPermissionChange(boolean enable, String packageName) { - int logCategory = enable ? MetricsEvent.APP_SPECIAL_PERMISSION_NOTIVIEW_ALLOW - : MetricsEvent.APP_SPECIAL_PERMISSION_NOTIVIEW_DENY; + int logCategory = enable ? SettingsEnums.APP_SPECIAL_PERMISSION_NOTIVIEW_ALLOW + : SettingsEnums.APP_SPECIAL_PERMISSION_NOTIVIEW_DENY; FeatureFactory.getFactory(getContext()).getMetricsFeatureProvider().action(getContext(), logCategory, packageName); } @@ -141,7 +164,7 @@ public class NotificationAccessSettings extends ManagedServiceSettings { @Override public int getMetricsCategory() { - return MetricsEvent.DIALOG_DISABLE_NOTIFICATION_ACCESS; + return SettingsEnums.DIALOG_DISABLE_NOTIFICATION_ACCESS; } @Override @@ -166,4 +189,18 @@ public class NotificationAccessSettings extends ManagedServiceSettings { .create(); } } + + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, + boolean enabled) { + final List<SearchIndexableResource> result = new ArrayList<>(); + + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.notification_access_settings; + result.add(sir); + return result; + } + }; } diff --git a/src/com/android/settings/notification/NotificationAppPreference.java b/src/com/android/settings/notification/NotificationAppPreference.java index bb015ca3f2..dfa3e36800 100644 --- a/src/com/android/settings/notification/NotificationAppPreference.java +++ b/src/com/android/settings/notification/NotificationAppPreference.java @@ -16,11 +16,12 @@ package com.android.settings.notification; import android.content.Context; -import androidx.preference.PreferenceViewHolder; import android.util.AttributeSet; import android.view.View; import android.widget.Switch; +import androidx.preference.PreferenceViewHolder; + import com.android.settings.R; import com.android.settings.widget.MasterSwitchPreference; import com.android.settingslib.RestrictedLockUtils; diff --git a/src/com/android/settings/notification/NotificationAssistantPicker.java b/src/com/android/settings/notification/NotificationAssistantPicker.java new file mode 100644 index 0000000000..868e0a9812 --- /dev/null +++ b/src/com/android/settings/notification/NotificationAssistantPicker.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2019 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.notification; + +import android.app.settings.SettingsEnums; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.PackageItemInfo; +import android.content.pm.PackageManager; +import android.content.pm.ServiceInfo; +import android.graphics.drawable.Drawable; +import android.os.RemoteException; +import android.provider.SearchIndexableResource; +import android.provider.Settings; +import android.service.notification.NotificationAssistantService; +import android.text.TextUtils; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.settings.R; +import com.android.settings.applications.defaultapps.DefaultAppPickerFragment; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; +import com.android.settingslib.applications.DefaultAppInfo; +import com.android.settingslib.applications.ServiceListing; +import com.android.settingslib.widget.CandidateInfo; + +import java.util.ArrayList; +import java.util.List; + +public class NotificationAssistantPicker extends DefaultAppPickerFragment implements + ServiceListing.Callback { + + private static final String TAG = "NotiAssistantPicker"; + + @VisibleForTesting + protected NotificationBackend mNotificationBackend; + private List<CandidateInfo> mCandidateInfos = new ArrayList<>(); + @VisibleForTesting + protected Context mContext; + private ServiceListing mServiceListing; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + mContext = context; + mNotificationBackend = new NotificationBackend(); + mServiceListing = new ServiceListing.Builder(context) + .setTag(TAG) + .setSetting(Settings.Secure.ENABLED_NOTIFICATION_ASSISTANT) + .setIntentAction(NotificationAssistantService.SERVICE_INTERFACE) + .setPermission(android.Manifest.permission.BIND_NOTIFICATION_ASSISTANT_SERVICE) + .setNoun("notification assistant") + .build(); + mServiceListing.addCallback(this); + mServiceListing.reload(); + } + + @Override + public void onDetach() { + super.onDetach(); + mServiceListing.removeCallback(this); + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.notification_assistant_settings; + } + + @Override + protected List<? extends CandidateInfo> getCandidates() { + return mCandidateInfos; + } + + @Override + protected String getDefaultKey() { + ComponentName cn = mNotificationBackend.getAllowedNotificationAssistant(); + return (cn != null) ? cn.flattenToString() : ""; + } + + @Override + protected boolean setDefaultKey(String key) { + return mNotificationBackend.setNotificationAssistantGranted( + ComponentName.unflattenFromString(key)); + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.DEFAULT_NOTIFICATION_ASSISTANT; + } + + @Override + protected CharSequence getConfirmationMessage(CandidateInfo info) { + if (TextUtils.isEmpty(info.getKey())) { + return null; + } + return mContext.getString(R.string.notification_assistant_security_warning_summary, + info.loadLabel()); + } + + @Override + public void onServicesReloaded(List<ServiceInfo> services) { + List<CandidateInfo> list = new ArrayList<>(); + services.sort(new PackageItemInfo.DisplayNameComparator(mPm)); + for (ServiceInfo service : services) { + if (mContext.getPackageManager().checkPermission( + android.Manifest.permission.REQUEST_NOTIFICATION_ASSISTANT_SERVICE, + service.packageName) == PackageManager.PERMISSION_GRANTED) { + final ComponentName cn = new ComponentName(service.packageName, service.name); + list.add(new DefaultAppInfo(mContext, mPm, mUserId, cn)); + } + } + list.add(new CandidateNone(mContext)); + mCandidateInfos = list; + } + + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, + boolean enabled) { + final List<SearchIndexableResource> result = new ArrayList<>(); + + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.notification_assistant_settings; + result.add(sir); + return result; + } + }; + + public static class CandidateNone extends CandidateInfo { + + public Context mContext; + + public CandidateNone(Context context) { + super(true); + mContext = context; + } + + @Override + public CharSequence loadLabel() { + return mContext.getString(R.string.no_notification_assistant); + } + + @Override + public Drawable loadIcon() { + return null; + } + + @Override + public String getKey() { + return ""; + } + } +} diff --git a/src/com/android/settings/notification/NotificationAssistantPreferenceController.java b/src/com/android/settings/notification/NotificationAssistantPreferenceController.java new file mode 100644 index 0000000000..66f27fef95 --- /dev/null +++ b/src/com/android/settings/notification/NotificationAssistantPreferenceController.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2019 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.notification; + +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.UserHandle; + +import com.android.settings.core.BasePreferenceController; +import com.android.settingslib.applications.DefaultAppInfo; +import com.android.settingslib.widget.CandidateInfo; + +import com.google.common.annotations.VisibleForTesting; + +public class NotificationAssistantPreferenceController extends BasePreferenceController { + + @VisibleForTesting + protected NotificationBackend mNotificationBackend; + private PackageManager mPackageManager; + + public NotificationAssistantPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + mNotificationBackend = new NotificationBackend(); + mPackageManager = mContext.getPackageManager(); + } + + @Override + public int getAvailabilityStatus() { + return BasePreferenceController.AVAILABLE; + } + + @Override + public CharSequence getSummary() { + CandidateInfo appSelected = new NotificationAssistantPicker.CandidateNone(mContext); + ComponentName assistant = mNotificationBackend.getAllowedNotificationAssistant(); + if (assistant != null) { + appSelected = createCandidateInfo(assistant); + } + return appSelected.loadLabel(); + } + + @VisibleForTesting + protected CandidateInfo createCandidateInfo(ComponentName cn) { + return new DefaultAppInfo(mContext, mPackageManager, UserHandle.myUserId(), cn); + } +} diff --git a/src/com/android/settings/notification/NotificationBackend.java b/src/com/android/settings/notification/NotificationBackend.java index 5e8ef19d97..a8396a1492 100644 --- a/src/com/android/settings/notification/NotificationBackend.java +++ b/src/com/android/settings/notification/NotificationBackend.java @@ -21,6 +21,10 @@ import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED; import android.app.INotificationManager; import android.app.NotificationChannel; import android.app.NotificationChannelGroup; +import android.app.role.RoleManager; +import android.app.usage.IUsageStatsManager; +import android.app.usage.UsageEvents; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; @@ -28,21 +32,31 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; import android.graphics.drawable.Drawable; +import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; import android.service.notification.NotifyingApp; +import android.text.format.DateUtils; import android.util.IconDrawableFactory; import android.util.Log; -import com.android.internal.annotations.VisibleForTesting; +import androidx.annotation.VisibleForTesting; + +import com.android.settingslib.R; import com.android.settingslib.Utils; +import com.android.settingslib.utils.StringUtil; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; public class NotificationBackend { private static final String TAG = "NotificationBackend"; + static IUsageStatsManager sUsageStatsManager = IUsageStatsManager.Stub.asInterface( + ServiceManager.getService(Context.USAGE_STATS_SERVICE)); + private static final int DAYS_TO_CHECK = 7; static INotificationManager sINM = INotificationManager.Stub.asInterface( ServiceManager.getService(Context.NOTIFICATION_SERVICE)); @@ -59,20 +73,35 @@ public class NotificationBackend { row.icon = IconDrawableFactory.newInstance(context).getBadgedIcon(app); row.banned = getNotificationsBanned(row.pkg, row.uid); row.showBadge = canShowBadge(row.pkg, row.uid); + row.allowBubbles = canBubble(row.pkg, row.uid); row.userId = UserHandle.getUserId(row.uid); row.blockedChannelCount = getBlockedChannelCount(row.pkg, row.uid); row.channelCount = getChannelCount(row.pkg, row.uid); + recordAggregatedUsageEvents(context, row); return row; } - public AppRow loadAppRow(Context context, PackageManager pm, PackageInfo app) { + public boolean isBlockable(Context context, ApplicationInfo info) { + final boolean blocked = getNotificationsBanned(info.packageName, info.uid); + final boolean systemApp = isSystemApp(context, info); + return !systemApp || (systemApp && blocked); + } + + public AppRow loadAppRow(Context context, PackageManager pm, + RoleManager roleManager, PackageInfo app) { final AppRow row = loadAppRow(context, pm, app.applicationInfo); - recordCanBeBlocked(context, pm, app, row); + recordCanBeBlocked(context, pm, roleManager, app, row); return row; } - void recordCanBeBlocked(Context context, PackageManager pm, PackageInfo app, AppRow row) { + void recordCanBeBlocked(Context context, PackageManager pm, RoleManager rm, PackageInfo app, + AppRow row) { row.systemApp = Utils.isSystemPackage(context.getResources(), pm, app); + List<String> roles = rm.getHeldRolesFromController(app.packageName); + if (roles.contains(RoleManager.ROLE_DIALER) + || roles.contains(RoleManager.ROLE_EMERGENCY)) { + row.systemApp = true; + } final String[] nonBlockablePkgs = context.getResources().getStringArray( com.android.internal.R.array.config_nonBlockableNotificationPackages); markAppRowWithBlockables(nonBlockablePkgs, row, app.packageName); @@ -87,10 +116,8 @@ public class NotificationBackend { if (pkg == null) { continue; } else if (pkg.contains(":")) { - // Interpret as channel; lock only this channel for this app. - if (packageName.equals(pkg.split(":", 2)[0])) { - row.lockedChannelId = pkg.split(":", 2 )[1]; - } + // handled by NotificationChannel.isImportanceLockedByOEM() + continue; } else if (packageName.equals(nonBlockablePkgs[i])) { row.systemApp = row.lockedImportance = true; } @@ -102,8 +129,9 @@ public class NotificationBackend { try { PackageInfo info = context.getPackageManager().getPackageInfo( app.packageName, PackageManager.GET_SIGNATURES); + RoleManager rm = context.getSystemService(RoleManager.class); final AppRow row = new AppRow(); - recordCanBeBlocked(context, context.getPackageManager(), info, row); + recordCanBeBlocked(context, context.getPackageManager(), rm, info, row); return row.systemApp; } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); @@ -156,6 +184,26 @@ public class NotificationBackend { } } + public boolean canBubble(String pkg, int uid) { + try { + return sINM.areBubblesAllowedForPackage(pkg, uid); + } catch (Exception e) { + Log.w(TAG, "Error calling NoMan", e); + return false; + } + } + + public boolean setAllowBubbles(String pkg, int uid, boolean allow) { + try { + sINM.setBubblesAllowed(pkg, uid, allow); + return true; + } catch (Exception e) { + Log.w(TAG, "Error calling NoMan", e); + return false; + } + } + + public NotificationChannel getChannel(String pkg, int uid, String channelId) { if (channelId == null) { return null; @@ -189,6 +237,19 @@ public class NotificationBackend { } } + /** + * Returns all notification channels associated with the package and uid that will bypass DND + */ + public ParceledListSlice<NotificationChannel> getNotificationChannelsBypassingDnd(String pkg, + int uid) { + try { + return sINM.getNotificationChannelsBypassingDnd(pkg, uid); + } catch (Exception e) { + Log.w(TAG, "Error calling NoMan", e); + return ParceledListSlice.emptyList(); + } + } + public void updateChannel(String pkg, int uid, NotificationChannel channel) { try { sINM.updateNotificationChannelForPackage(pkg, uid, channel); @@ -241,12 +302,12 @@ public class NotificationBackend { } } - public List<NotifyingApp> getRecentApps() { + public int getNumAppsBypassingDnd(int uid) { try { - return sINM.getRecentNotifyingAppsForUser(UserHandle.myUserId()).getList(); + return sINM.getAppsBypassingDndCount(uid); } catch (Exception e) { Log.w(TAG, "Error calling NoMan", e); - return new ArrayList<>(); + return 0; } } @@ -259,6 +320,161 @@ public class NotificationBackend { } } + public boolean shouldHideSilentStatusBarIcons(Context context) { + try { + return sINM.shouldHideSilentStatusIcons(context.getPackageName()); + } catch (Exception e) { + Log.w(TAG, "Error calling NoMan", e); + return false; + } + } + + public void setHideSilentStatusIcons(boolean hide) { + try { + sINM.setHideSilentStatusIcons(hide); + } catch (Exception e) { + Log.w(TAG, "Error calling NoMan", e); + } + } + + public void allowAssistantAdjustment(String capability, boolean allowed) { + try { + if (allowed) { + sINM.allowAssistantAdjustment(capability); + } else { + sINM.disallowAssistantAdjustment(capability); + } + } catch (Exception e) { + Log.w(TAG, "Error calling NoMan", e); + } + } + + public List<String> getAssistantAdjustments(String pkg) { + try { + return sINM.getAllowedAssistantAdjustments(pkg); + } catch (Exception e) { + Log.w(TAG, "Error calling NoMan", e); + } + return new ArrayList<>(); + } + + public boolean showSilentInStatusBar(String pkg) { + try { + return !sINM.shouldHideSilentStatusIcons(pkg); + } catch (Exception e) { + Log.w(TAG, "Error calling NoMan", e); + } + return false; + } + + protected void recordAggregatedUsageEvents(Context context, AppRow appRow) { + long now = System.currentTimeMillis(); + long startTime = now - (DateUtils.DAY_IN_MILLIS * DAYS_TO_CHECK); + UsageEvents events = null; + try { + events = sUsageStatsManager.queryEventsForPackageForUser( + startTime, now, appRow.userId, appRow.pkg, context.getPackageName()); + } catch (RemoteException e) { + e.printStackTrace(); + } + recordAggregatedUsageEvents(events, appRow); + } + + protected void recordAggregatedUsageEvents(UsageEvents events, AppRow appRow) { + appRow.sentByChannel = new HashMap<>(); + appRow.sentByApp = new NotificationsSentState(); + if (events != null) { + UsageEvents.Event event = new UsageEvents.Event(); + while (events.hasNextEvent()) { + events.getNextEvent(event); + + if (event.getEventType() == UsageEvents.Event.NOTIFICATION_INTERRUPTION) { + String channelId = event.mNotificationChannelId; + if (channelId != null) { + NotificationsSentState stats = appRow.sentByChannel.get(channelId); + if (stats == null) { + stats = new NotificationsSentState(); + appRow.sentByChannel.put(channelId, stats); + } + if (event.getTimeStamp() > stats.lastSent) { + stats.lastSent = event.getTimeStamp(); + appRow.sentByApp.lastSent = event.getTimeStamp(); + } + stats.sentCount++; + appRow.sentByApp.sentCount++; + calculateAvgSentCounts(stats); + } + } + + } + calculateAvgSentCounts(appRow.sentByApp); + } + } + + public static CharSequence getSentSummary(Context context, NotificationsSentState state, + boolean sortByRecency) { + if (state == null) { + return null; + } + if (sortByRecency) { + if (state.lastSent == 0) { + return context.getString(R.string.notifications_sent_never); + } + return StringUtil.formatRelativeTime( + context, System.currentTimeMillis() - state.lastSent, true); + } else { + if (state.avgSentDaily > 0) { + return context.getResources().getQuantityString(R.plurals.notifications_sent_daily, + state.avgSentDaily, state.avgSentDaily); + } + return context.getResources().getQuantityString(R.plurals.notifications_sent_weekly, + state.avgSentWeekly, state.avgSentWeekly); + } + } + + private void calculateAvgSentCounts(NotificationsSentState stats) { + if (stats != null) { + stats.avgSentDaily = Math.round((float) stats.sentCount / DAYS_TO_CHECK); + if (stats.sentCount < DAYS_TO_CHECK) { + stats.avgSentWeekly = stats.sentCount; + } + } + } + + public ComponentName getAllowedNotificationAssistant() { + try { + return sINM.getAllowedNotificationAssistant(); + } catch (Exception e) { + Log.w(TAG, "Error calling NoMan", e); + return null; + } + } + + public boolean setNotificationAssistantGranted(ComponentName cn) { + try { + sINM.setNotificationAssistantAccessGranted(cn, true); + if (cn == null) { + return sINM.getAllowedNotificationAssistant() == null; + } else { + return cn.equals(sINM.getAllowedNotificationAssistant()); + } + } catch (Exception e) { + Log.w(TAG, "Error calling NoMan", e); + return false; + } + } + + /** + * NotificationsSentState contains how often an app sends notifications and how recently it sent + * one. + */ + public static class NotificationsSentState { + public int avgSentDaily = 0; + public int avgSentWeekly = 0; + public long lastSent = 0; + public int sentCount = 0; + } + static class Row { public String section; } @@ -273,10 +489,12 @@ public class NotificationBackend { public boolean first; // first app in section public boolean systemApp; public boolean lockedImportance; - public String lockedChannelId; public boolean showBadge; + public boolean allowBubbles; public int userId; public int blockedChannelCount; public int channelCount; + public Map<String, NotificationsSentState> sentByChannel; + public NotificationsSentState sentByApp; } } diff --git a/src/com/android/settings/notification/NotificationButtonRelativeLayout.java b/src/com/android/settings/notification/NotificationButtonRelativeLayout.java new file mode 100644 index 0000000000..358cbf35ba --- /dev/null +++ b/src/com/android/settings/notification/NotificationButtonRelativeLayout.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2019 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.notification; + + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.Button; +import android.widget.RelativeLayout; + +public class NotificationButtonRelativeLayout extends RelativeLayout { + + public NotificationButtonRelativeLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public CharSequence getAccessibilityClassName() { + return Button.class.getName(); + } +}
\ No newline at end of file diff --git a/src/com/android/settings/notification/NotificationFooterPreference.java b/src/com/android/settings/notification/NotificationFooterPreference.java index 2b41281c51..abaffc8594 100644 --- a/src/com/android/settings/notification/NotificationFooterPreference.java +++ b/src/com/android/settings/notification/NotificationFooterPreference.java @@ -17,13 +17,14 @@ package com.android.settings.notification; import android.content.Context; -import androidx.core.content.res.TypedArrayUtils; -import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; import android.text.method.LinkMovementMethod; import android.util.AttributeSet; import android.widget.TextView; +import androidx.core.content.res.TypedArrayUtils; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + import com.android.settingslib.R; /** diff --git a/src/com/android/settings/notification/NotificationLockscreenPreference.java b/src/com/android/settings/notification/NotificationLockscreenPreference.java index 01eecf52ed..b236014802 100644 --- a/src/com/android/settings/notification/NotificationLockscreenPreference.java +++ b/src/com/android/settings/notification/NotificationLockscreenPreference.java @@ -16,12 +16,6 @@ package com.android.settings.notification; -import com.android.settings.R; -import com.android.settings.RestrictedListPreference; -import com.android.settings.Utils; -import com.android.settingslib.RestrictedLockUtils; - -import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; @@ -33,10 +27,16 @@ import android.util.AttributeSet; import android.view.View; import android.widget.CheckBox; import android.widget.CompoundButton; -import android.widget.ImageView; -import android.widget.ListAdapter; import android.widget.ListView; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AlertDialog.Builder; + +import com.android.settings.R; +import com.android.settings.RestrictedListPreference; +import com.android.settings.Utils; +import com.android.settingslib.RestrictedLockUtils; + public class NotificationLockscreenPreference extends RestrictedListPreference { private boolean mAllowRemoteInput; @@ -73,11 +73,12 @@ public class NotificationLockscreenPreference extends RestrictedListPreference { } @Override - protected void onPrepareDialogBuilder(AlertDialog.Builder builder, + protected void onPrepareDialogBuilder(Builder builder, DialogInterface.OnClickListener innerListener) { mListener = new Listener(innerListener); - builder.setSingleChoiceItems(createListAdapter(), getSelectedValuePos(), mListener); + builder.setSingleChoiceItems(createListAdapter(builder.getContext()), getSelectedValuePos(), + mListener); mShowRemoteInput = getEntryValues().length == 3; mAllowRemoteInput = Settings.Secure.getInt(getContext().getContentResolver(), Settings.Secure.LOCK_SCREEN_ALLOW_REMOTE_INPUT, 0) != 0; @@ -116,11 +117,6 @@ public class NotificationLockscreenPreference extends RestrictedListPreference { } @Override - protected ListAdapter createListAdapter() { - return new RestrictedArrayAdapter(getContext(), getEntries(), -1); - } - - @Override protected void onDialogClosed(boolean positiveResult) { super.onDialogClosed(positiveResult); Settings.Secure.putInt(getContext().getContentResolver(), diff --git a/src/com/android/settings/notification/NotificationPreferenceController.java b/src/com/android/settings/notification/NotificationPreferenceController.java index 49bb08ef15..2ae7019ea3 100644 --- a/src/com/android/settings/notification/NotificationPreferenceController.java +++ b/src/com/android/settings/notification/NotificationPreferenceController.java @@ -25,11 +25,10 @@ import android.app.NotificationManager; import android.content.Context; import android.content.pm.PackageManager; import android.os.UserManager; -import androidx.preference.Preference; -import androidx.preference.PreferenceGroup; -import androidx.preference.PreferenceScreen; import android.util.Log; +import androidx.preference.Preference; + import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.core.AbstractPreferenceController; @@ -74,33 +73,17 @@ public abstract class NotificationPreferenceController extends AbstractPreferenc if (mAppRow.banned) { return false; } + if (mChannelGroup != null) { + if (mChannelGroup.isBlocked()) { + return false; + } + } if (mChannel != null) { return mChannel.getImportance() != IMPORTANCE_NONE; } - if (mChannelGroup != null) { - return !mChannelGroup.isBlocked(); - } return true; } - // finds the preference recursively and removes it from its parent - private void findAndRemovePreference(PreferenceGroup prefGroup, String key) { - final int preferenceCount = prefGroup.getPreferenceCount(); - for (int i = preferenceCount - 1; i >= 0; i--) { - final Preference preference = prefGroup.getPreference(i); - final String curKey = preference.getKey(); - - if (curKey != null && curKey.equals(key)) { - mPreference = preference; - prefGroup.removePreference(preference); - } - - if (preference instanceof PreferenceGroup) { - findAndRemovePreference((PreferenceGroup) preference, key); - } - } - } - protected void onResume(NotificationBackend.AppRow appRow, @Nullable NotificationChannel channel, @Nullable NotificationChannelGroup group, RestrictedLockUtils.EnforcedAdmin admin) { @@ -129,20 +112,14 @@ public abstract class NotificationPreferenceController extends AbstractPreferenc } } - protected boolean isChannelConfigurable() { - if (mChannel != null && mAppRow != null) { - return !Objects.equals(mChannel.getId(), mAppRow.lockedChannelId); - } - return false; - } - protected boolean isChannelBlockable() { if (mChannel != null && mAppRow != null) { - if (!mAppRow.systemApp) { - return true; + if (mChannel.isImportanceLockedByCriticalDeviceFunction() + || mChannel.isImportanceLockedByOEM()) { + return mChannel.getImportance() == IMPORTANCE_NONE; } - return mChannel.isBlockableSystem() + return mChannel.isBlockableSystem() || !mAppRow.systemApp || mChannel.getImportance() == IMPORTANCE_NONE; } return false; diff --git a/src/com/android/settings/notification/NotificationSettingsBase.java b/src/com/android/settings/notification/NotificationSettingsBase.java index 6006162955..2a728c5951 100644 --- a/src/com/android/settings/notification/NotificationSettingsBase.java +++ b/src/com/android/settings/notification/NotificationSettingsBase.java @@ -18,13 +18,14 @@ package com.android.settings.notification; import static android.app.NotificationManager.IMPORTANCE_LOW; import static android.app.NotificationManager.IMPORTANCE_NONE; + import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; -import android.app.Activity; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationChannelGroup; import android.app.NotificationManager; +import android.app.role.RoleManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -34,23 +35,30 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; +import android.graphics.BlendMode; +import android.graphics.BlendModeColorFilter; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.LayerDrawable; import android.os.Bundle; import android.os.UserHandle; import android.provider.Settings; -import androidx.preference.Preference; -import androidx.preference.PreferenceGroup; -import androidx.preference.PreferenceScreen; import android.text.TextUtils; import android.util.Log; import android.widget.Toast; +import androidx.preference.Preference; +import androidx.preference.PreferenceGroup; +import androidx.preference.PreferenceScreen; + import com.android.settings.R; import com.android.settings.SettingsActivity; +import com.android.settings.Utils; import com.android.settings.applications.AppInfoBase; import com.android.settings.core.SubSettingLauncher; import com.android.settings.dashboard.DashboardFragment; -import com.android.settings.widget.MasterCheckBoxPreference; -import com.android.settingslib.RestrictedLockUtils; +import com.android.settings.widget.MasterSwitchPreference; +import com.android.settingslib.RestrictedLockUtilsInternal; import java.util.ArrayList; import java.util.Comparator; @@ -59,11 +67,12 @@ import java.util.List; abstract public class NotificationSettingsBase extends DashboardFragment { private static final String TAG = "NotifiSettingsBase"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - protected static final String ARG_FROM_SETTINGS = "fromSettings"; + public static final String ARG_FROM_SETTINGS = "fromSettings"; protected PackageManager mPm; protected NotificationBackend mBackend = new NotificationBackend(); protected NotificationManager mNm; + protected RoleManager mRm; protected Context mContext; protected int mUid; @@ -94,6 +103,7 @@ abstract public class NotificationSettingsBase extends DashboardFragment { mPm = getPackageManager(); mNm = NotificationManager.from(mContext); + mRm = mContext.getSystemService(RoleManager.class); mPkg = mArgs != null && mArgs.containsKey(AppInfoBase.ARG_PACKAGE_NAME) ? mArgs.getString(AppInfoBase.ARG_PACKAGE_NAME) @@ -111,19 +121,22 @@ abstract public class NotificationSettingsBase extends DashboardFragment { mPkgInfo = findPackageInfo(mPkg, mUid); - mUserId = UserHandle.getUserId(mUid); - mSuspendedAppsAdmin = RestrictedLockUtils.checkIfApplicationIsSuspended( - mContext, mPkg, mUserId); + if (mPkgInfo != null) { + mUserId = UserHandle.getUserId(mUid); + mSuspendedAppsAdmin = RestrictedLockUtilsInternal.checkIfApplicationIsSuspended( + mContext, mPkg, mUserId); - loadChannel(); - loadAppRow(); - loadChannelGroup(); - collectConfigActivities(); - getLifecycle().addObserver(use(HeaderPreferenceController.class)); + loadChannel(); + loadAppRow(); + loadChannelGroup(); + collectConfigActivities(); - for (NotificationPreferenceController controller : mControllers) { - controller.onResume(mAppRow, mChannel, mChannelGroup, mSuspendedAppsAdmin); + getSettingsLifecycle().addObserver(use(HeaderPreferenceController.class)); + + for (NotificationPreferenceController controller : mControllers) { + controller.onResume(mAppRow, mChannel, mChannelGroup, mSuspendedAppsAdmin); + } } } @@ -185,7 +198,7 @@ abstract public class NotificationSettingsBase extends DashboardFragment { } private void loadAppRow() { - mAppRow = mBackend.loadAppRow(mContext, mPm, mPkgInfo); + mAppRow = mBackend.loadAppRow(mContext, mPm, mRm, mPkgInfo); } private void loadChannelGroup() { @@ -267,16 +280,28 @@ abstract public class NotificationSettingsBase extends DashboardFragment { return null; } + private Drawable getAlertingIcon() { + Drawable icon = getContext().getDrawable(R.drawable.ic_notifications_alert); + icon.setTintList(Utils.getColorAccent(getContext())); + return icon; + } + protected Preference populateSingleChannelPrefs(PreferenceGroup parent, final NotificationChannel channel, final boolean groupBlocked) { - MasterCheckBoxPreference channelPref = new MasterCheckBoxPreference( - getPrefContext()); - channelPref.setCheckBoxEnabled(mSuspendedAppsAdmin == null + MasterSwitchPreference channelPref = new MasterSwitchPreference(getPrefContext()); + channelPref.setSwitchEnabled(mSuspendedAppsAdmin == null && isChannelBlockable(channel) && isChannelConfigurable(channel) && !groupBlocked); + channelPref.setIcon(null); + if (channel.getImportance() > IMPORTANCE_LOW) { + channelPref.setIcon(getAlertingIcon()); + } + channelPref.setIconSize(MasterSwitchPreference.ICON_SIZE_SMALL); channelPref.setKey(channel.getId()); channelPref.setTitle(channel.getName()); + channelPref.setSummary(NotificationBackend.getSentSummary( + mContext, mAppRow.sentByChannel.get(channel.getId()), false)); channelPref.setChecked(channel.getImportance() != IMPORTANCE_NONE); Bundle channelArgs = new Bundle(); channelArgs.putInt(AppInfoBase.ARG_PACKAGE_UID, mUid); @@ -286,32 +311,55 @@ abstract public class NotificationSettingsBase extends DashboardFragment { channelPref.setIntent(new SubSettingLauncher(getActivity()) .setDestination(ChannelNotificationSettings.class.getName()) .setArguments(channelArgs) - .setTitle(R.string.notification_channel_title) + .setTitleRes(R.string.notification_channel_title) .setSourceMetricsCategory(getMetricsCategory()) .toIntent()); channelPref.setOnPreferenceChangeListener( - new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, - Object o) { - boolean value = (Boolean) o; - int importance = value ? IMPORTANCE_LOW : IMPORTANCE_NONE; - channel.setImportance(importance); - channel.lockFields( - NotificationChannel.USER_LOCKED_IMPORTANCE); - mBackend.updateChannel(mPkg, mUid, channel); - - return true; + (preference, o) -> { + boolean value = (Boolean) o; + int importance = value ? IMPORTANCE_LOW : IMPORTANCE_NONE; + channel.setImportance(importance); + channel.lockFields( + NotificationChannel.USER_LOCKED_IMPORTANCE); + MasterSwitchPreference channelPref1 = (MasterSwitchPreference) preference; + channelPref1.setIcon(null); + if (channel.getImportance() > IMPORTANCE_LOW) { + channelPref1.setIcon(getAlertingIcon()); } + toggleBehaviorIconState(channelPref1.getIcon(), + importance != IMPORTANCE_NONE); + mBackend.updateChannel(mPkg, mUid, channel); + + return true; }); - parent.addPreference(channelPref); + if (parent.findPreference(channelPref.getKey()) == null) { + parent.addPreference(channelPref); + } return channelPref; } + private void toggleBehaviorIconState(Drawable icon, boolean enabled) { + if (icon == null) return; + + LayerDrawable layerDrawable = (LayerDrawable) icon; + GradientDrawable background = + (GradientDrawable) layerDrawable.findDrawableByLayerId(R.id.back); + + if (background == null) return; + + if (enabled) { + background.clearColorFilter(); + } else { + background.setColorFilter(new BlendModeColorFilter( + mContext.getColor(R.color.material_grey_300), + BlendMode.SRC_IN)); + } + } + protected boolean isChannelConfigurable(NotificationChannel channel) { if (channel != null && mAppRow != null) { - return !channel.getId().equals(mAppRow.lockedChannelId); + return !channel.isImportanceLockedByOEM(); } return false; } @@ -322,6 +370,14 @@ abstract public class NotificationSettingsBase extends DashboardFragment { return true; } + if (channel.isImportanceLockedByCriticalDeviceFunction()) { + return false; + } + + if (channel.isImportanceLockedByOEM()) { + return false; + } + return channel.isBlockableSystem() || channel.getImportance() == NotificationManager.IMPORTANCE_NONE; } diff --git a/src/com/android/settings/notification/NotificationStation.java b/src/com/android/settings/notification/NotificationStation.java index e63e588646..80b2d453ff 100644 --- a/src/com/android/settings/notification/NotificationStation.java +++ b/src/com/android/settings/notification/NotificationStation.java @@ -16,11 +16,15 @@ package com.android.settings.notification; +import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED; + import android.app.Activity; import android.app.ActivityManager; import android.app.INotificationManager; import android.app.Notification; +import android.app.NotificationChannel; import android.app.PendingIntent; +import android.app.settings.SettingsEnums; import android.content.ComponentName; import android.content.Context; import android.content.IntentSender; @@ -39,9 +43,6 @@ import android.service.notification.NotificationListenerService; import android.service.notification.NotificationListenerService.Ranking; import android.service.notification.NotificationListenerService.RankingMap; import android.service.notification.StatusBarNotification; -import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; -import androidx.recyclerview.widget.RecyclerView; import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.TextUtils; @@ -52,7 +53,10 @@ import android.widget.DateTimeView; import android.widget.ImageView; import android.widget.TextView; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; +import androidx.recyclerview.widget.RecyclerView; + import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; import com.android.settings.Utils; @@ -60,13 +64,12 @@ import com.android.settings.Utils; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; -import java.util.Date; import java.util.List; public class NotificationStation extends SettingsPreferenceFragment { private static final String TAG = NotificationStation.class.getSimpleName(); - private static final boolean DEBUG = true; + private static final boolean DEBUG = false; private static final boolean DUMP_EXTRAS = true; private static final boolean DUMP_PARCEL = true; private Handler mHandler; @@ -180,7 +183,7 @@ public class NotificationStation extends SettingsPreferenceFragment { @Override public int getMetricsCategory() { - return MetricsEvent.NOTIFICATION_STATION; + return SettingsEnums.NOTIFICATION_STATION; } @Override @@ -354,28 +357,53 @@ public class NotificationStation extends SettingsPreferenceFragment { getString(R.string.notification_log_details_group_summary))); } } - sb.append("\n") - .append(bold(getString(R.string.notification_log_details_sound))) - .append(delim); - if (0 != (n.defaults & Notification.DEFAULT_SOUND)) { - sb.append(getString(R.string.notification_log_details_default)); - } else if (n.sound != null) { - sb.append(n.sound.toString()); - } else { - sb.append(getString(R.string.notification_log_details_none)); + if (info.active) { + // mRanking only applies to active notifications + if (mRanking != null && mRanking.getRanking(sbn.getKey(), rank)) { + if (rank.getLastAudiblyAlertedMillis() > 0) { + sb.append("\n") + .append(bold(getString(R.string.notification_log_details_alerted))); + } + } } - sb.append("\n") - .append(bold(getString(R.string.notification_log_details_vibrate))) - .append(delim); - if (0 != (n.defaults & Notification.DEFAULT_VIBRATE)) { - sb.append(getString(R.string.notification_log_details_default)); - } else if (n.vibrate != null) { - for (int vi=0;vi<n.vibrate.length;vi++) { - if (vi > 0) sb.append(','); - sb.append(String.valueOf(n.vibrate[vi])); + try { + NotificationChannel channel = mNoMan.getNotificationChannelForPackage( + sbn.getPackageName(), sbn.getUid(), n.getChannelId(), false); + sb.append("\n") + .append(bold(getString(R.string.notification_log_details_sound))) + .append(delim); + if (channel == null || channel.getImportance() == IMPORTANCE_UNSPECIFIED) { + + if (0 != (n.defaults & Notification.DEFAULT_SOUND)) { + sb.append(getString(R.string.notification_log_details_default)); + } else if (n.sound != null) { + sb.append(n.sound.toString()); + } else { + sb.append(getString(R.string.notification_log_details_none)); + } + } else { + sb.append(String.valueOf(channel.getSound())); } - } else { - sb.append(getString(R.string.notification_log_details_none)); + sb.append("\n") + .append(bold(getString(R.string.notification_log_details_vibrate))) + .append(delim); + if (channel == null || channel.getImportance() == IMPORTANCE_UNSPECIFIED) { + if (0 != (n.defaults & Notification.DEFAULT_VIBRATE)) { + sb.append(getString(R.string.notification_log_details_default)); + } else if (n.vibrate != null) { + sb.append(getString(R.string.notification_log_details_vibrate_pattern)); + } else { + sb.append(getString(R.string.notification_log_details_none)); + } + } else { + if (channel.getVibrationPattern() != null) { + sb.append(getString(R.string.notification_log_details_vibrate_pattern)); + } else { + sb.append(getString(R.string.notification_log_details_none)); + } + } + } catch (RemoteException e) { + Log.d(TAG, "cannot read channel info", e); } sb.append("\n") .append(bold(getString(R.string.notification_log_details_visibility))) diff --git a/src/com/android/settings/notification/NotificationSwitchBarPreference.java b/src/com/android/settings/notification/NotificationSwitchBarPreference.java index ede7d881f9..01c4f6ab46 100644 --- a/src/com/android/settings/notification/NotificationSwitchBarPreference.java +++ b/src/com/android/settings/notification/NotificationSwitchBarPreference.java @@ -17,13 +17,14 @@ package com.android.settings.notification; import android.content.Context; -import androidx.preference.PreferenceViewHolder; import android.util.AttributeSet; import android.view.View; -import com.android.settings.applications.LayoutPreference; +import androidx.preference.PreferenceViewHolder; + import com.android.settings.widget.ToggleSwitch; import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.widget.LayoutPreference; public class NotificationSwitchBarPreference extends LayoutPreference { private ToggleSwitch mSwitch; diff --git a/src/com/android/settings/notification/NotificationsOffPreferenceController.java b/src/com/android/settings/notification/NotificationsOffPreferenceController.java index 4dbf491764..8762f91c27 100644 --- a/src/com/android/settings/notification/NotificationsOffPreferenceController.java +++ b/src/com/android/settings/notification/NotificationsOffPreferenceController.java @@ -17,6 +17,7 @@ package com.android.settings.notification; import android.content.Context; + import androidx.preference.Preference; import com.android.settings.R; diff --git a/src/com/android/settings/notification/OWNERS b/src/com/android/settings/notification/OWNERS index 0d73685a87..edf266eebb 100644 --- a/src/com/android/settings/notification/OWNERS +++ b/src/com/android/settings/notification/OWNERS @@ -1,4 +1,4 @@ # Default reviewers for this and subdirectories. asc@google.com -dsandler@google.com +dsandler@android.com juliacr@google.com
\ No newline at end of file diff --git a/src/com/android/settings/notification/PhoneRingtonePreferenceController.java b/src/com/android/settings/notification/PhoneRingtonePreferenceController.java index cb1115165f..049dfe51b6 100644 --- a/src/com/android/settings/notification/PhoneRingtonePreferenceController.java +++ b/src/com/android/settings/notification/PhoneRingtonePreferenceController.java @@ -18,6 +18,7 @@ package com.android.settings.notification; import android.content.Context; import android.media.RingtoneManager; + import com.android.settings.Utils; public class PhoneRingtonePreferenceController extends RingtonePreferenceControllerBase { diff --git a/src/com/android/settings/notification/PulseNotificationPreferenceController.java b/src/com/android/settings/notification/PulseNotificationPreferenceController.java index 15bbf6d115..78855208f7 100644 --- a/src/com/android/settings/notification/PulseNotificationPreferenceController.java +++ b/src/com/android/settings/notification/PulseNotificationPreferenceController.java @@ -16,41 +16,38 @@ package com.android.settings.notification; +import static android.provider.Settings.System.NOTIFICATION_LIGHT_PULSE; + import android.content.ContentResolver; import android.content.Context; import android.database.ContentObserver; import android.net.Uri; import android.os.Handler; import android.provider.Settings; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import androidx.preference.TwoStatePreference; -import android.util.Log; -import com.android.settings.core.PreferenceControllerMixin; -import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settings.core.TogglePreferenceController; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnPause; import com.android.settingslib.core.lifecycle.events.OnResume; -import static android.provider.Settings.System.NOTIFICATION_LIGHT_PULSE; - -public class PulseNotificationPreferenceController extends AbstractPreferenceController - implements PreferenceControllerMixin, Preference.OnPreferenceChangeListener, - LifecycleObserver, OnResume, OnPause { +public class PulseNotificationPreferenceController extends TogglePreferenceController + implements LifecycleObserver, OnResume, OnPause { - private static final String TAG = "PulseNotifPrefContr"; - private static final String KEY_NOTIFICATION_PULSE = "notification_pulse"; + private static final int ON = 1; + private static final int OFF = 0; private SettingObserver mSettingObserver; - public PulseNotificationPreferenceController(Context context) { - super(context); + public PulseNotificationPreferenceController(Context context, String key) { + super(context, key); } @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - Preference preference = screen.findPreference(KEY_NOTIFICATION_PULSE); + Preference preference = screen.findPreference(getPreferenceKey()); if (preference != null) { mSettingObserver = new SettingObserver(preference); } @@ -71,32 +68,22 @@ public class PulseNotificationPreferenceController extends AbstractPreferenceCon } @Override - public String getPreferenceKey() { - return KEY_NOTIFICATION_PULSE; + public int getAvailabilityStatus() { + return mContext.getResources().getBoolean( + com.android.internal.R.bool.config_intrusiveNotificationLed) ? AVAILABLE + : UNSUPPORTED_ON_DEVICE; } @Override - public boolean isAvailable() { - return mContext.getResources() - .getBoolean(com.android.internal.R.bool.config_intrusiveNotificationLed); - } - - @Override - public void updateState(Preference preference) { - try { - final boolean checked = Settings.System.getInt(mContext.getContentResolver(), - NOTIFICATION_LIGHT_PULSE) == 1; - ((TwoStatePreference) preference).setChecked(checked); - } catch (Settings.SettingNotFoundException snfe) { - Log.e(TAG, NOTIFICATION_LIGHT_PULSE + " not found"); - } + public boolean isChecked() { + return Settings.System.getInt(mContext.getContentResolver(), NOTIFICATION_LIGHT_PULSE, OFF) + == ON; } @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - final boolean val = (Boolean) newValue; - return Settings.System.putInt(mContext.getContentResolver(), - NOTIFICATION_LIGHT_PULSE, val ? 1 : 0); + public boolean setChecked(boolean isChecked) { + return Settings.System.putInt(mContext.getContentResolver(), NOTIFICATION_LIGHT_PULSE, + isChecked ? ON : OFF); } class SettingObserver extends ContentObserver { diff --git a/src/com/android/settings/notification/RecentNotifyingAppsPreferenceController.java b/src/com/android/settings/notification/RecentNotifyingAppsPreferenceController.java index 8081e4e943..8b5b7617fe 100644 --- a/src/com/android/settings/notification/RecentNotifyingAppsPreferenceController.java +++ b/src/com/android/settings/notification/RecentNotifyingAppsPreferenceController.java @@ -17,25 +17,25 @@ package com.android.settings.notification; import android.app.Application; -import android.app.Fragment; +import android.app.settings.SettingsEnums; +import android.app.usage.IUsageStatsManager; +import android.app.usage.UsageEvents; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.os.Bundle; +import android.os.RemoteException; import android.os.UserHandle; +import android.os.UserManager; import android.service.notification.NotifyingApp; -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; -import androidx.preference.PreferenceCategory; -import androidx.preference.PreferenceScreen; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.IconDrawableFactory; import android.util.Log; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; +import com.android.settings.Utils; import com.android.settings.applications.AppInfoBase; import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.core.SubSettingLauncher; @@ -46,12 +46,18 @@ import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.utils.StringUtil; import java.util.ArrayList; -import java.util.Arrays; +import java.util.Calendar; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; +import androidx.annotation.VisibleForTesting; +import androidx.fragment.app.Fragment; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceScreen; + /** * This controller displays a list of recently used apps and a "See all" button. If there is * no recently used app, "See all" will be displayed as "Notifications". @@ -65,48 +71,49 @@ public class RecentNotifyingAppsPreferenceController extends AbstractPreferenceC static final String KEY_DIVIDER = "all_notifications_divider"; @VisibleForTesting static final String KEY_SEE_ALL = "all_notifications"; - private static final int SHOW_RECENT_APP_COUNT = 5; + private static final int SHOW_RECENT_APP_COUNT = 3; + private static final int DAYS = 3; private static final Set<String> SKIP_SYSTEM_PACKAGES = new ArraySet<>(); private final Fragment mHost; private final PackageManager mPm; private final NotificationBackend mNotificationBackend; - private final int mUserId; + private IUsageStatsManager mUsageStatsManager; private final IconDrawableFactory mIconDrawableFactory; - private List<NotifyingApp> mApps; + private Calendar mCal; + List<NotifyingApp> mApps; private final ApplicationsState mApplicationsState; private PreferenceCategory mCategory; private Preference mSeeAllPref; private Preference mDivider; - - static { - SKIP_SYSTEM_PACKAGES.addAll(Arrays.asList( - "android", - "com.android.phone", - "com.android.settings", - "com.android.systemui", - "com.android.providers.calendar", - "com.android.providers.media" - )); - } + protected List<Integer> mUserIds; public RecentNotifyingAppsPreferenceController(Context context, NotificationBackend backend, + IUsageStatsManager usageStatsManager, UserManager userManager, Application app, Fragment host) { - this(context, backend, app == null ? null : ApplicationsState.getInstance(app), host); + this(context, backend, usageStatsManager, userManager, + app == null ? null : ApplicationsState.getInstance(app), host); } @VisibleForTesting(otherwise = VisibleForTesting.NONE) RecentNotifyingAppsPreferenceController(Context context, NotificationBackend backend, + IUsageStatsManager usageStatsManager, UserManager userManager, ApplicationsState appState, Fragment host) { super(context); mIconDrawableFactory = IconDrawableFactory.newInstance(context); - mUserId = UserHandle.myUserId(); mPm = context.getPackageManager(); mHost = host; mApplicationsState = appState; mNotificationBackend = backend; + mUsageStatsManager = usageStatsManager; + mUserIds = new ArrayList<>(); + mUserIds.add(mContext.getUserId()); + int workUserId = Utils.getManagedProfileId(userManager, mContext.getUserId()); + if (workUserId != UserHandle.USER_NULL) { + mUserIds.add(workUserId); + } } @Override @@ -129,7 +136,7 @@ public class RecentNotifyingAppsPreferenceController extends AbstractPreferenceC @Override public void displayPreference(PreferenceScreen screen) { - mCategory = (PreferenceCategory) screen.findPreference(getPreferenceKey()); + mCategory = screen.findPreference(getPreferenceKey()); mSeeAllPref = screen.findPreference(KEY_SEE_ALL); mDivider = screen.findPreference(KEY_DIVIDER); super.displayPreference(screen); @@ -156,7 +163,48 @@ public class RecentNotifyingAppsPreferenceController extends AbstractPreferenceC @VisibleForTesting void reloadData() { - mApps = mNotificationBackend.getRecentApps(); + mApps = new ArrayList<>(); + mCal = Calendar.getInstance(); + mCal.add(Calendar.DAY_OF_YEAR, -DAYS); + for (int userId : mUserIds) { + UsageEvents events = null; + try { + events = mUsageStatsManager.queryEventsForUser(mCal.getTimeInMillis(), + System.currentTimeMillis(), userId, mContext.getPackageName()); + } catch (RemoteException e) { + e.printStackTrace(); + } + if (events != null) { + ArrayMap<String, NotifyingApp> aggregatedStats = new ArrayMap<>(); + + UsageEvents.Event event = new UsageEvents.Event(); + while (events.hasNextEvent()) { + events.getNextEvent(event); + + if (event.getEventType() == UsageEvents.Event.NOTIFICATION_INTERRUPTION) { + NotifyingApp app = + aggregatedStats.get(getKey(userId, event.getPackageName())); + if (app == null) { + app = new NotifyingApp(); + aggregatedStats.put(getKey(userId, event.getPackageName()), app); + app.setPackage(event.getPackageName()); + app.setUserId(userId); + } + if (event.getTimeStamp() > app.getLastNotified()) { + app.setLastNotified(event.getTimeStamp()); + } + } + + } + + mApps.addAll(aggregatedStats.values()); + } + } + } + + @VisibleForTesting + static String getKey(int userId, String pkg) { + return userId + "|" + pkg; } private void displayOnlyAllAppsLink() { @@ -196,18 +244,19 @@ public class RecentNotifyingAppsPreferenceController extends AbstractPreferenceC // Bind recent apps to existing prefs if possible, or create a new pref. final String pkgName = app.getPackage(); final ApplicationsState.AppEntry appEntry = - mApplicationsState.getEntry(app.getPackage(), mUserId); + mApplicationsState.getEntry(app.getPackage(), app.getUserId()); if (appEntry == null) { continue; } boolean rebindPref = true; - NotificationAppPreference pref = appPreferences.remove(pkgName); + NotificationAppPreference pref = appPreferences.remove(getKey(app.getUserId(), + pkgName)); if (pref == null) { pref = new NotificationAppPreference(prefContext); rebindPref = false; } - pref.setKey(pkgName); + pref.setKey(getKey(app.getUserId(), pkgName)); pref.setTitle(appEntry.label); pref.setIcon(mIconDrawableFactory.getBadgedIcon(appEntry.info)); pref.setIconSize(TwoTargetPreference.ICON_SIZE_SMALL); @@ -217,14 +266,15 @@ public class RecentNotifyingAppsPreferenceController extends AbstractPreferenceC Bundle args = new Bundle(); args.putString(AppInfoBase.ARG_PACKAGE_NAME, pkgName); args.putInt(AppInfoBase.ARG_PACKAGE_UID, appEntry.info.uid); - pref.setIntent(new SubSettingLauncher(mHost.getActivity()) .setDestination(AppNotificationSettings.class.getName()) - .setTitle(R.string.notifications_title) + .setTitleRes(R.string.notifications_title) .setArguments(args) + .setUserHandle(new UserHandle(UserHandle.getUserId(appEntry.info.uid))) .setSourceMetricsCategory( - MetricsProto.MetricsEvent.MANAGE_APPLICATIONS_NOTIFICATIONS) + SettingsEnums.MANAGE_APPLICATIONS_NOTIFICATIONS) .toIntent()); + pref.setSwitchEnabled(mNotificationBackend.isBlockable(mContext, appEntry.info)); pref.setOnPreferenceChangeListener((preference, newValue) -> { boolean blocked = !(Boolean) newValue; mNotificationBackend.setNotificationsEnabledForPackage( @@ -250,11 +300,11 @@ public class RecentNotifyingAppsPreferenceController extends AbstractPreferenceC int count = 0; for (NotifyingApp app : mApps) { final ApplicationsState.AppEntry appEntry = mApplicationsState.getEntry( - app.getPackage(), mUserId); + app.getPackage(), app.getUserId()); if (appEntry == null) { continue; } - if (!shouldIncludePkgInRecents(app.getPackage())) { + if (!shouldIncludePkgInRecents(app.getPackage(), app.getUserId())) { continue; } displayableApps.add(app); @@ -270,18 +320,14 @@ public class RecentNotifyingAppsPreferenceController extends AbstractPreferenceC /** * Whether or not the app should be included in recent list. */ - private boolean shouldIncludePkgInRecents(String pkgName) { - if (SKIP_SYSTEM_PACKAGES.contains(pkgName)) { - Log.d(TAG, "System package, skipping " + pkgName); - return false; - } + private boolean shouldIncludePkgInRecents(String pkgName, int userId) { final Intent launchIntent = new Intent().addCategory(Intent.CATEGORY_LAUNCHER) .setPackage(pkgName); if (mPm.resolveActivity(launchIntent, 0) == null) { // Not visible on launcher -> likely not a user visible app, skip if non-instant. final ApplicationsState.AppEntry appEntry = - mApplicationsState.getEntry(pkgName, mUserId); + mApplicationsState.getEntry(pkgName, userId); if (appEntry == null || appEntry.info == null || !AppUtils.isInstant(appEntry.info)) { Log.d(TAG, "Not a user visible or instant app, skipping " + pkgName); return false; diff --git a/src/com/android/settings/notification/RedactNotificationPreferenceController.java b/src/com/android/settings/notification/RedactNotificationPreferenceController.java new file mode 100644 index 0000000000..94d7fc1f0d --- /dev/null +++ b/src/com/android/settings/notification/RedactNotificationPreferenceController.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2019 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.notification; + +import static android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_SECURE_NOTIFICATIONS; +import static android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_UNREDACTED_NOTIFICATIONS; +import static android.provider.Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS; + +import android.app.KeyguardManager; +import android.app.admin.DevicePolicyManager; +import android.content.Context; +import android.os.UserHandle; +import android.os.UserManager; +import android.provider.Settings; + +import com.android.internal.widget.LockPatternUtils; +import com.android.settings.Utils; +import com.android.settings.core.TogglePreferenceController; +import com.android.settings.overlay.FeatureFactory; + +public class RedactNotificationPreferenceController extends TogglePreferenceController { + + private static final String TAG = "LockScreenNotifPref"; + + static final String KEY_LOCKSCREEN_REDACT = "lock_screen_redact"; + static final String KEY_LOCKSCREEN_WORK_PROFILE_REDACT = "lock_screen_work_redact"; + + private DevicePolicyManager mDpm; + private UserManager mUm; + private KeyguardManager mKm; + private final int mProfileUserId; + + public RedactNotificationPreferenceController(Context context, String settingKey) { + super(context, settingKey); + + mUm = context.getSystemService(UserManager.class); + mDpm = context.getSystemService(DevicePolicyManager.class); + mKm = context.getSystemService(KeyguardManager.class); + + mProfileUserId = Utils.getManagedProfileId(mUm, UserHandle.myUserId()); + } + + @Override + public boolean isChecked() { + int userId = KEY_LOCKSCREEN_REDACT.equals(getPreferenceKey()) + ? UserHandle.myUserId() : mProfileUserId; + + return getAllowPrivateNotifications(userId); + } + + @Override + public boolean setChecked(boolean isChecked) { + int userId = KEY_LOCKSCREEN_REDACT.equals(getPreferenceKey()) + ? UserHandle.myUserId() : mProfileUserId; + + Settings.Secure.putIntForUser(mContext.getContentResolver(), + Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, isChecked ? 1 : 0, userId); + return true; + } + + @Override + public int getAvailabilityStatus() { + // hide work profile setting if no work profile + if (KEY_LOCKSCREEN_WORK_PROFILE_REDACT.equals(getPreferenceKey()) + && mProfileUserId == UserHandle.USER_NULL) { + return CONDITIONALLY_UNAVAILABLE; + } + + int userId = KEY_LOCKSCREEN_REDACT.equals(getPreferenceKey()) + ? UserHandle.myUserId() : mProfileUserId; + + // hide if lockscreen isn't secure for this user + final LockPatternUtils utils = FeatureFactory.getFactory(mContext) + .getSecurityFeatureProvider() + .getLockPatternUtils(mContext); + if (!utils.isSecure(userId)) { + return CONDITIONALLY_UNAVAILABLE; + } + + // all notifs hidden? admin doesn't allow notifs or redacted notifs? disabled + if (!getLockscreenNotificationsEnabled(userId) + || !adminAllowsNotifications(userId) + || !adminAllowsUnredactedNotifications(userId)) { + return DISABLED_DEPENDENT_SETTING; + } + + // specifically the work profile setting requires the work profile to be unlocked + if (KEY_LOCKSCREEN_WORK_PROFILE_REDACT.equals(getPreferenceKey())) { + if (mKm.isDeviceLocked(mProfileUserId)) { + return DISABLED_DEPENDENT_SETTING; + } + } + + return AVAILABLE; + } + + private boolean adminAllowsNotifications(int userId) { + final int dpmFlags = mDpm.getKeyguardDisabledFeatures(null/* admin */, userId); + return (dpmFlags & KEYGUARD_DISABLE_SECURE_NOTIFICATIONS) == 0; + } + + private boolean adminAllowsUnredactedNotifications(int userId) { + final int dpmFlags = mDpm.getKeyguardDisabledFeatures(null/* admin */, userId); + return (dpmFlags & KEYGUARD_DISABLE_UNREDACTED_NOTIFICATIONS) == 0; + } + + private boolean getAllowPrivateNotifications(int userId) { + return Settings.Secure.getIntForUser(mContext.getContentResolver(), + LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 1, userId) != 0; + } + + private boolean getLockscreenNotificationsEnabled(int userId) { + return Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, 1, userId) != 0; + } +} diff --git a/src/com/android/settings/notification/RedactionInterstitial.java b/src/com/android/settings/notification/RedactionInterstitial.java index 517c0410b3..14f99f38b3 100644 --- a/src/com/android/settings/notification/RedactionInterstitial.java +++ b/src/com/android/settings/notification/RedactionInterstitial.java @@ -23,6 +23,7 @@ import static android.provider.Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS; import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; import android.content.res.Resources; @@ -32,13 +33,10 @@ import android.provider.Settings; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Button; -import android.widget.LinearLayout; import android.widget.RadioButton; import android.widget.RadioGroup; import android.widget.TextView; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.RestrictedRadioButton; import com.android.settings.SettingsActivity; @@ -46,7 +44,11 @@ import com.android.settings.SettingsPreferenceFragment; import com.android.settings.SetupRedactionInterstitial; import com.android.settings.SetupWizardUtils; import com.android.settings.Utils; -import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; + +import com.google.android.setupcompat.template.FooterBarMixin; +import com.google.android.setupcompat.template.FooterButton; +import com.google.android.setupdesign.GlifLayout; public class RedactionInterstitial extends SettingsActivity { @@ -71,12 +73,12 @@ public class RedactionInterstitial extends SettingsActivity { @Override protected void onCreate(Bundle savedInstance) { super.onCreate(savedInstance); - LinearLayout layout = (LinearLayout) findViewById(R.id.content_parent); - layout.setFitsSystemWindows(false); + findViewById(R.id.content_parent).setFitsSystemWindows(false); } /** * Create an intent for launching RedactionInterstitial. + * * @return An intent to launch the activity is if is available, @null if the activity is not * available to be launched. */ @@ -84,13 +86,13 @@ public class RedactionInterstitial extends SettingsActivity { return new Intent(ctx, RedactionInterstitial.class) .putExtra(EXTRA_SHOW_FRAGMENT_TITLE_RESID, UserManager.get(ctx).isManagedProfile(userId) - ? R.string.lock_screen_notifications_interstitial_title_profile - : R.string.lock_screen_notifications_interstitial_title) + ? R.string.lock_screen_notifications_interstitial_title_profile + : R.string.lock_screen_notifications_interstitial_title) .putExtra(Intent.EXTRA_USER_ID, userId); } public static class RedactionInterstitialFragment extends SettingsPreferenceFragment - implements RadioGroup.OnCheckedChangeListener, View.OnClickListener { + implements RadioGroup.OnCheckedChangeListener { private RadioGroup mRadioGroup; private RestrictedRadioButton mShowAllButton; @@ -99,7 +101,7 @@ public class RedactionInterstitial extends SettingsActivity { @Override public int getMetricsCategory() { - return MetricsEvent.NOTIFICATION_REDACTION; + return SettingsEnums.NOTIFICATION_REDACTION; } @Override @@ -120,7 +122,7 @@ public class RedactionInterstitial extends SettingsActivity { mUserId = Utils.getUserIdFromBundle( getContext(), getActivity().getIntent().getExtras()); if (UserManager.get(getContext()).isManagedProfile(mUserId)) { - ((TextView) view.findViewById(R.id.message)) + ((TextView) view.findViewById(R.id.sud_layout_description)) .setText(R.string.lock_screen_notifications_interstitial_message_profile); mShowAllButton.setText(R.string.lock_screen_notifications_summary_show_profile); mRedactSensitiveButton @@ -129,19 +131,24 @@ public class RedactionInterstitial extends SettingsActivity { ((RadioButton) view.findViewById(R.id.hide_all)).setVisibility(View.GONE); } - final Button button = (Button) view.findViewById(R.id.redaction_done_button); - button.setOnClickListener(this); + final GlifLayout layout = view.findViewById(R.id.setup_wizard_layout); + final FooterBarMixin mixin = layout.getMixin(FooterBarMixin.class); + mixin.setPrimaryButton( + new FooterButton.Builder(getContext()) + .setText(R.string.app_notifications_dialog_done) + .setListener(this::onDoneButtonClicked) + .setButtonType(FooterButton.ButtonType.NEXT) + .setTheme(R.style.SudGlifButton_Primary) + .build() + ); } - @Override - public void onClick(View v) { - if (v.getId() == R.id.redaction_done_button) { - SetupRedactionInterstitial.setEnabled(getContext(), false); - final RedactionInterstitial activity = (RedactionInterstitial) getActivity(); - if (activity != null) { - activity.setResult(RESULT_OK, null); - finish(); - } + private void onDoneButtonClicked(View view) { + SetupRedactionInterstitial.setEnabled(getContext(), false); + final RedactionInterstitial activity = (RedactionInterstitial) getActivity(); + if (activity != null) { + activity.setResult(RESULT_OK, null); + finish(); } } @@ -152,7 +159,7 @@ public class RedactionInterstitial extends SettingsActivity { checkNotificationFeaturesAndSetDisabled(mShowAllButton, KEYGUARD_DISABLE_SECURE_NOTIFICATIONS | - KEYGUARD_DISABLE_UNREDACTED_NOTIFICATIONS); + KEYGUARD_DISABLE_UNREDACTED_NOTIFICATIONS); checkNotificationFeaturesAndSetDisabled(mRedactSensitiveButton, KEYGUARD_DISABLE_SECURE_NOTIFICATIONS); loadFromSettings(); @@ -160,7 +167,7 @@ public class RedactionInterstitial extends SettingsActivity { private void checkNotificationFeaturesAndSetDisabled(RestrictedRadioButton button, int keyguardNotifications) { - EnforcedAdmin admin = RestrictedLockUtils.checkIfKeyguardFeaturesDisabled( + EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfKeyguardFeaturesDisabled( getActivity(), keyguardNotifications, mUserId); button.setDisabledByAdmin(admin); } diff --git a/src/com/android/settings/notification/RemoteVolumePreferenceController.java b/src/com/android/settings/notification/RemoteVolumePreferenceController.java new file mode 100644 index 0000000000..816d80f2be --- /dev/null +++ b/src/com/android/settings/notification/RemoteVolumePreferenceController.java @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2019 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.notification; + +import android.content.Context; +import android.media.session.MediaController; +import android.media.session.MediaSession; +import android.media.session.MediaSessionManager; +import android.net.Uri; +import android.os.Looper; +import android.text.TextUtils; + +import androidx.annotation.VisibleForTesting; +import androidx.lifecycle.OnLifecycleEvent; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.slices.SliceBackgroundWorker; +import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.volume.MediaSessions; + +import java.io.IOException; +import java.util.List; +import java.util.Objects; + +public class RemoteVolumePreferenceController extends VolumeSeekBarPreferenceController { + + private static final String KEY_REMOTE_VOLUME = "remote_volume"; + @VisibleForTesting + static final int REMOTE_VOLUME = 100; + + private MediaSessions mMediaSessions; + @VisibleForTesting + MediaSession.Token mActiveToken; + @VisibleForTesting + MediaController mMediaController; + + @VisibleForTesting + MediaSessions.Callbacks mCallbacks = new MediaSessions.Callbacks() { + @Override + public void onRemoteUpdate(MediaSession.Token token, String name, + MediaController.PlaybackInfo pi) { + if (mActiveToken == null) { + updateToken(token); + } + if (Objects.equals(mActiveToken, token)) { + updatePreference(mPreference, mActiveToken, pi); + } + } + + @Override + public void onRemoteRemoved(MediaSession.Token t) { + if (Objects.equals(mActiveToken, t)) { + updateToken(null); + if (mPreference != null) { + mPreference.setVisible(false); + } + } + } + + @Override + public void onRemoteVolumeChanged(MediaSession.Token token, int flags) { + if (Objects.equals(mActiveToken, token)) { + final MediaController.PlaybackInfo pi = mMediaController.getPlaybackInfo(); + if (pi != null) { + setSliderPosition(pi.getCurrentVolume()); + } + } + } + }; + + public RemoteVolumePreferenceController(Context context) { + super(context, KEY_REMOTE_VOLUME); + mMediaSessions = new MediaSessions(context, Looper.getMainLooper(), mCallbacks); + updateToken(getActiveRemoteToken(mContext)); + } + + @Override + public int getAvailabilityStatus() { + // Always return true to make it indexed in database + return AVAILABLE_UNSEARCHABLE; + } + + /** + * Return {@link android.media.session.MediaSession.Token} for active remote token, or + * {@code null} if there is no active remote token. + */ + public static MediaSession.Token getActiveRemoteToken(Context context) { + final MediaSessionManager sessionManager = context.getSystemService( + MediaSessionManager.class); + final List<MediaController> controllers = sessionManager.getActiveSessions(null); + for (MediaController mediaController : controllers) { + final MediaController.PlaybackInfo pi = mediaController.getPlaybackInfo(); + if (isRemote(pi)) { + return mediaController.getSessionToken(); + } + } + + // No active remote media at this point + return null; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference.setVisible(mActiveToken != null); + if (mMediaController != null) { + updatePreference(mPreference, mActiveToken, mMediaController.getPlaybackInfo()); + } + } + + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) + public void onResume() { + super.onResume(); + mMediaSessions.init(); + } + + @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) + public void onPause() { + super.onPause(); + mMediaSessions.destroy(); + } + + @Override + public int getSliderPosition() { + if (mPreference != null) { + return mPreference.getProgress(); + } + if (mMediaController == null) { + return 0; + } + final MediaController.PlaybackInfo playbackInfo = mMediaController.getPlaybackInfo(); + return playbackInfo != null ? playbackInfo.getCurrentVolume() : 0; + } + + @Override + public boolean setSliderPosition(int position) { + if (mPreference != null) { + mPreference.setProgress(position); + } + if (mMediaController == null) { + return false; + } + mMediaController.setVolumeTo(position, 0); + return true; + } + + @Override + public int getMax() { + if (mPreference != null) { + return mPreference.getMax(); + } + if (mMediaController == null) { + return 0; + } + final MediaController.PlaybackInfo playbackInfo = mMediaController.getPlaybackInfo(); + return playbackInfo != null ? playbackInfo.getMaxVolume() : 0; + } + + @Override + public int getMin() { + if (mPreference != null) { + return mPreference.getMin(); + } + return 0; + } + + @Override + public boolean isSliceable() { + return TextUtils.equals(getPreferenceKey(), KEY_REMOTE_VOLUME); + } + + @Override + public boolean useDynamicSliceSummary() { + return true; + } + + @Override + public String getPreferenceKey() { + return KEY_REMOTE_VOLUME; + } + + @Override + public int getAudioStream() { + // This can be anything because remote volume controller doesn't rely on it. + return REMOTE_VOLUME; + } + + @Override + public int getMuteIcon() { + return R.drawable.ic_volume_remote_mute; + } + + public static boolean isRemote(MediaController.PlaybackInfo pi) { + return pi != null + && pi.getPlaybackType() == MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE; + } + + @Override + public Class<? extends SliceBackgroundWorker> getBackgroundWorkerClass() { + return RemoteVolumeSliceWorker.class; + } + + private void updatePreference(VolumeSeekBarPreference seekBarPreference, + MediaSession.Token token, MediaController.PlaybackInfo playbackInfo) { + if (seekBarPreference == null || token == null || playbackInfo == null) { + return; + } + + seekBarPreference.setMax(playbackInfo.getMaxVolume()); + seekBarPreference.setVisible(true); + setSliderPosition(playbackInfo.getCurrentVolume()); + } + + private void updateToken(MediaSession.Token token) { + mActiveToken = token; + if (token != null) { + mMediaController = new MediaController(mContext, mActiveToken); + } else { + mMediaController = null; + } + } + + /** + * Listener for background change to remote volume, which listens callback + * from {@code MediaSessions} + */ + public static class RemoteVolumeSliceWorker extends SliceBackgroundWorker<Void> implements + MediaSessions.Callbacks { + + private MediaSessions mMediaSessions; + + public RemoteVolumeSliceWorker(Context context, Uri uri) { + super(context, uri); + mMediaSessions = new MediaSessions(context, Looper.getMainLooper(), this); + } + + @Override + protected void onSlicePinned() { + mMediaSessions.init(); + } + + @Override + protected void onSliceUnpinned() { + mMediaSessions.destroy(); + } + + @Override + public void close() throws IOException { + mMediaSessions = null; + } + + @Override + public void onRemoteUpdate(MediaSession.Token token, String name, + MediaController.PlaybackInfo pi) { + notifySliceChange(); + } + + @Override + public void onRemoteRemoved(MediaSession.Token t) { + notifySliceChange(); + } + + @Override + public void onRemoteVolumeChanged(MediaSession.Token token, int flags) { + notifySliceChange(); + } + } +} diff --git a/src/com/android/settings/notification/RemoteVolumeSeekBarPreference.java b/src/com/android/settings/notification/RemoteVolumeSeekBarPreference.java new file mode 100644 index 0000000000..e99af6a42c --- /dev/null +++ b/src/com/android/settings/notification/RemoteVolumeSeekBarPreference.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2019 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.notification; + +import android.content.Context; +import android.util.AttributeSet; + +/** + * A slider preference that controls remote volume, which doesn't go through + * {@link android.media.AudioManager} + **/ +public class RemoteVolumeSeekBarPreference extends VolumeSeekBarPreference { + + public RemoteVolumeSeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public RemoteVolumeSeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public RemoteVolumeSeekBarPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public RemoteVolumeSeekBarPreference(Context context) { + super(context); + } + + @Override + public void setStream(int stream) { + // Do nothing here, volume is not controlled by AudioManager + } + + @Override + protected void init() { + if (mSeekBar == null) return; + updateIconView(); + updateSuppressionText(); + } +} diff --git a/src/com/android/settings/notification/RingVolumePreferenceController.java b/src/com/android/settings/notification/RingVolumePreferenceController.java index becf95fece..08efc931a0 100644 --- a/src/com/android/settings/notification/RingVolumePreferenceController.java +++ b/src/com/android/settings/notification/RingVolumePreferenceController.java @@ -17,7 +17,6 @@ package com.android.settings.notification; import android.app.NotificationManager; -import androidx.lifecycle.OnLifecycleEvent; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; @@ -30,6 +29,8 @@ import android.os.Message; import android.os.Vibrator; import android.text.TextUtils; +import androidx.lifecycle.OnLifecycleEvent; + import com.android.settings.R; import com.android.settings.Utils; import com.android.settingslib.core.lifecycle.Lifecycle; @@ -95,6 +96,11 @@ public class RingVolumePreferenceController extends VolumeSeekBarPreferenceContr } @Override + public boolean useDynamicSliceSummary() { + return true; + } + + @Override public int getAudioStream() { return AudioManager.STREAM_RING; } diff --git a/src/com/android/settings/notification/RingtonePreferenceControllerBase.java b/src/com/android/settings/notification/RingtonePreferenceControllerBase.java index 21b7e539a1..733d0d937d 100644 --- a/src/com/android/settings/notification/RingtonePreferenceControllerBase.java +++ b/src/com/android/settings/notification/RingtonePreferenceControllerBase.java @@ -20,10 +20,12 @@ import android.content.Context; import android.media.Ringtone; import android.media.RingtoneManager; import android.net.Uri; + import androidx.preference.Preference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.utils.ThreadUtils; public abstract class RingtonePreferenceControllerBase extends AbstractPreferenceController implements PreferenceControllerMixin { @@ -44,11 +46,22 @@ public abstract class RingtonePreferenceControllerBase extends AbstractPreferenc @Override public void updateState(Preference preference) { - Uri ringtoneUri = RingtoneManager.getActualDefaultRingtoneUri(mContext, getRingtoneType()); - final CharSequence summary = Ringtone.getTitle( - mContext, ringtoneUri, false /* followSettingsUri */, true /* allowRemote */); + ThreadUtils.postOnBackgroundThread(() -> updateSummary(preference)); + } + + private void updateSummary(Preference preference) { + final Uri ringtoneUri = RingtoneManager.getActualDefaultRingtoneUri( + mContext, getRingtoneType()); + + final CharSequence summary; + if (ringtoneUri == null) { + summary = null; + } else { + summary = Ringtone.getTitle( + mContext, ringtoneUri, false /* followSettingsUri */, true /* allowRemote */); + } if (summary != null) { - preference.setSummary(summary); + ThreadUtils.postOnMainThread(() -> preference.setSummary(summary)); } } diff --git a/src/com/android/settings/notification/ScreenLockSoundPreferenceController.java b/src/com/android/settings/notification/ScreenLockSoundPreferenceController.java index 11aaa92dd0..896d99e1ed 100644 --- a/src/com/android/settings/notification/ScreenLockSoundPreferenceController.java +++ b/src/com/android/settings/notification/ScreenLockSoundPreferenceController.java @@ -19,8 +19,8 @@ package com.android.settings.notification; import static com.android.settings.notification.SettingPref.TYPE_SYSTEM; import android.content.Context; - import android.provider.Settings.System; + import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; import com.android.settingslib.core.lifecycle.Lifecycle; diff --git a/src/com/android/settings/notification/SettingPref.java b/src/com/android/settings/notification/SettingPref.java index 08031b12fb..9a37455273 100644 --- a/src/com/android/settings/notification/SettingPref.java +++ b/src/com/android/settings/notification/SettingPref.java @@ -21,7 +21,9 @@ import android.content.Context; import android.content.res.Resources; import android.net.Uri; import android.provider.Settings.Global; +import android.provider.Settings.Secure; import android.provider.Settings.System; + import androidx.preference.DropDownPreference; import androidx.preference.Preference; import androidx.preference.Preference.OnPreferenceChangeListener; @@ -33,6 +35,7 @@ import com.android.settings.SettingsPreferenceFragment; public class SettingPref { public static final int TYPE_GLOBAL = 1; public static final int TYPE_SYSTEM = 2; + public static final int TYPE_SECURE = 3; protected final int mType; private final String mKey; @@ -131,6 +134,8 @@ public class SettingPref { return Global.getUriFor(setting); case TYPE_SYSTEM: return System.getUriFor(setting); + case TYPE_SECURE: + return Secure.getUriFor(setting); } throw new IllegalArgumentException(); } @@ -141,6 +146,8 @@ public class SettingPref { return Global.putInt(cr, setting, value); case TYPE_SYSTEM: return System.putInt(cr, setting, value); + case TYPE_SECURE: + return Secure.putInt(cr, setting, value); } throw new IllegalArgumentException(); } @@ -151,6 +158,8 @@ public class SettingPref { return Global.getInt(cr, setting, def); case TYPE_SYSTEM: return System.getInt(cr, setting, def); + case TYPE_SECURE: + return Secure.getInt(cr, setting, def); } throw new IllegalArgumentException(); } diff --git a/src/com/android/settings/notification/SettingPrefController.java b/src/com/android/settings/notification/SettingPrefController.java index 891c7b3f8f..8d48d53196 100644 --- a/src/com/android/settings/notification/SettingPrefController.java +++ b/src/com/android/settings/notification/SettingPrefController.java @@ -18,14 +18,14 @@ package com.android.settings.notification; import android.content.ContentResolver; import android.content.Context; - import android.database.ContentObserver; import android.net.Uri; import android.os.Handler; + +import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import com.android.internal.annotations.VisibleForTesting; import com.android.settings.SettingsPreferenceFragment; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.core.AbstractPreferenceController; diff --git a/src/com/android/settings/notification/SettingsEnableZenModeDialog.java b/src/com/android/settings/notification/SettingsEnableZenModeDialog.java index dccdc26e88..880db35b59 100644 --- a/src/com/android/settings/notification/SettingsEnableZenModeDialog.java +++ b/src/com/android/settings/notification/SettingsEnableZenModeDialog.java @@ -17,9 +17,9 @@ package com.android.settings.notification; import android.app.Dialog; +import android.app.settings.SettingsEnums; import android.os.Bundle; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; public class SettingsEnableZenModeDialog extends InstrumentedDialogFragment { @@ -32,6 +32,6 @@ public class SettingsEnableZenModeDialog extends InstrumentedDialogFragment { @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.NOTIFICATION_ZEN_MODE_ENABLE_DIALOG; + return SettingsEnums.NOTIFICATION_ZEN_MODE_ENABLE_DIALOG; } } diff --git a/src/com/android/settings/notification/SettingsZenDurationDialog.java b/src/com/android/settings/notification/SettingsZenDurationDialog.java index 23bf1a971f..355d4836b0 100644 --- a/src/com/android/settings/notification/SettingsZenDurationDialog.java +++ b/src/com/android/settings/notification/SettingsZenDurationDialog.java @@ -17,9 +17,9 @@ package com.android.settings.notification; import android.app.Dialog; +import android.app.settings.SettingsEnums; import android.os.Bundle; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; public class SettingsZenDurationDialog extends InstrumentedDialogFragment { @@ -32,6 +32,6 @@ public class SettingsZenDurationDialog extends InstrumentedDialogFragment { @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.NOTIFICATION_ZEN_MODE_DURATION_DIALOG; + return SettingsEnums.NOTIFICATION_ZEN_MODE_DURATION_DIALOG; } } diff --git a/src/com/android/settings/notification/ShowOnLockScreenNotificationPreferenceController.java b/src/com/android/settings/notification/ShowOnLockScreenNotificationPreferenceController.java new file mode 100644 index 0000000000..df1f4a21eb --- /dev/null +++ b/src/com/android/settings/notification/ShowOnLockScreenNotificationPreferenceController.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2019 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.notification; + +import static android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_SECURE_NOTIFICATIONS; + +import android.app.admin.DevicePolicyManager; +import android.content.Context; +import android.os.UserHandle; +import android.provider.Settings; + +import com.android.settings.R; +import com.android.settings.RestrictedListPreference; +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; +import com.android.settingslib.core.AbstractPreferenceController; + +import com.google.common.annotations.VisibleForTesting; + +import java.util.ArrayList; + +import androidx.preference.Preference; + +public class ShowOnLockScreenNotificationPreferenceController extends AbstractPreferenceController + implements PreferenceControllerMixin, Preference.OnPreferenceChangeListener { + + private static final String TAG = "LockScreenNotifPref"; + + private final String mSettingKey; + private DevicePolicyManager mDpm; + + public ShowOnLockScreenNotificationPreferenceController(Context context, String settingKey) { + super(context); + mSettingKey = settingKey; + mDpm = context.getSystemService(DevicePolicyManager.class); + } + + @VisibleForTesting + void setDpm(DevicePolicyManager dpm) { + mDpm = dpm; + } + + @Override + public String getPreferenceKey() { + return mSettingKey; + } + + @Override + public boolean isAvailable() { + return true; + } + + @Override + public void updateState(Preference preference) { + RestrictedListPreference pref = (RestrictedListPreference) preference; + pref.clearRestrictedItems(); + ArrayList<CharSequence> entries = new ArrayList<>(); + ArrayList<CharSequence> values = new ArrayList<>(); + + String showAllEntry = + mContext.getString(R.string.lock_screen_notifs_show_all); + String showAllEntryValue = + Integer.toString(R.string.lock_screen_notifs_show_all); + entries.add(showAllEntry); + values.add(showAllEntryValue); + setRestrictedIfNotificationFeaturesDisabled(pref, showAllEntry, showAllEntryValue, + KEYGUARD_DISABLE_SECURE_NOTIFICATIONS); + + String alertingEntry = mContext.getString(R.string.lock_screen_notifs_show_alerting); + String alertingEntryValue = Integer.toString(R.string.lock_screen_notifs_show_alerting); + entries.add(alertingEntry); + values.add(alertingEntryValue); + setRestrictedIfNotificationFeaturesDisabled(pref, alertingEntry, alertingEntryValue, + KEYGUARD_DISABLE_SECURE_NOTIFICATIONS); + + entries.add(mContext.getString(R.string.lock_screen_notifs_show_none)); + values.add(Integer.toString(R.string.lock_screen_notifs_show_none)); + + pref.setEntries(entries.toArray(new CharSequence[entries.size()])); + pref.setEntryValues(values.toArray(new CharSequence[values.size()])); + + if (!adminAllowsNotifications() || !getLockscreenNotificationsEnabled()) { + pref.setValue(Integer.toString(R.string.lock_screen_notifs_show_none)); + } else if (!getLockscreenSilentNotificationsEnabled()) { + pref.setValue(Integer.toString(R.string.lock_screen_notifs_show_alerting)); + } else { + pref.setValue(Integer.toString(R.string.lock_screen_notifs_show_all)); + } + + pref.setOnPreferenceChangeListener(this); + + refreshSummary(preference); + } + + @Override + public CharSequence getSummary() { + if (!adminAllowsNotifications() || !getLockscreenNotificationsEnabled()) { + return mContext.getString(R.string.lock_screen_notifs_show_none); + } else if (!getLockscreenSilentNotificationsEnabled()) { + return mContext.getString(R.string.lock_screen_notifs_show_alerting); + } else { + return mContext.getString(R.string.lock_screen_notifs_show_all); + } + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + final int val = Integer.parseInt((String) newValue); + final boolean enabled = val != R.string.lock_screen_notifs_show_none; + final boolean show = val == R.string.lock_screen_notifs_show_all; + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, show ? 1 : 0); + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, enabled ? 1 : 0); + refreshSummary(preference); + return true; + } + + private void setRestrictedIfNotificationFeaturesDisabled(RestrictedListPreference pref, + CharSequence entry, CharSequence entryValue, int keyguardNotificationFeatures) { + RestrictedLockUtils.EnforcedAdmin admin = + RestrictedLockUtilsInternal.checkIfKeyguardFeaturesDisabled( + mContext, keyguardNotificationFeatures, UserHandle.myUserId()); + if (admin != null && pref != null) { + RestrictedListPreference.RestrictedItem item = + new RestrictedListPreference.RestrictedItem(entry, entryValue, admin); + pref.addRestrictedItem(item); + } + } + + private boolean adminAllowsNotifications() { + final int dpmFlags = mDpm.getKeyguardDisabledFeatures(null/* admin */); + return (dpmFlags & KEYGUARD_DISABLE_SECURE_NOTIFICATIONS) == 0; + } + + private boolean getLockscreenNotificationsEnabled() { + return Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, 1) != 0; + } + + private boolean getLockscreenSilentNotificationsEnabled() { + return Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, 1) != 0; + } +} diff --git a/src/com/android/settings/notification/SnoozeNotificationPreferenceController.java b/src/com/android/settings/notification/SnoozeNotificationPreferenceController.java new file mode 100644 index 0000000000..03170e42e9 --- /dev/null +++ b/src/com/android/settings/notification/SnoozeNotificationPreferenceController.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2019 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.notification; + +import static android.provider.Settings.Secure.SHOW_NOTIFICATION_SNOOZE; + +import android.content.Context; +import android.provider.Settings; + +import com.android.settings.core.TogglePreferenceController; + +import androidx.annotation.VisibleForTesting; + +public class SnoozeNotificationPreferenceController extends TogglePreferenceController { + + private static final String TAG = "SnoozeNotifPrefContr"; + @VisibleForTesting + static final int ON = 1; + @VisibleForTesting + static final int OFF = 0; + + public SnoozeNotificationPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } + + @Override + public boolean isChecked() { + return Settings.Secure.getInt(mContext.getContentResolver(), + SHOW_NOTIFICATION_SNOOZE, OFF) == ON; + } + + @Override + public boolean setChecked(boolean isChecked) { + return Settings.Secure.putInt(mContext.getContentResolver(), + SHOW_NOTIFICATION_SNOOZE, isChecked ? ON : OFF); + } +} diff --git a/src/com/android/settings/notification/SoundPreferenceController.java b/src/com/android/settings/notification/SoundPreferenceController.java index 6c537ff0d2..566f3f2845 100644 --- a/src/com/android/settings/notification/SoundPreferenceController.java +++ b/src/com/android/settings/notification/SoundPreferenceController.java @@ -16,12 +16,17 @@ package com.android.settings.notification; +import static android.media.AudioAttributes.USAGE_ALARM; +import static android.media.AudioAttributes.USAGE_NOTIFICATION_RINGTONE; + import android.app.NotificationChannel; import android.app.NotificationManager; import android.content.Context; import android.content.Intent; +import android.media.RingtoneManager; import android.net.Uri; import android.preference.PreferenceManager; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; @@ -66,13 +71,13 @@ public class SoundPreferenceController extends NotificationPreferenceController public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - mPreference = (NotificationSoundPreference) screen.findPreference(getPreferenceKey()); + mPreference = screen.findPreference(getPreferenceKey()); } public void updateState(Preference preference) { if (mAppRow!= null && mChannel != null) { NotificationSoundPreference pref = (NotificationSoundPreference) preference; - pref.setEnabled(mAdmin == null && isChannelConfigurable()); + pref.setEnabled(mAdmin == null); pref.setRingtone(mChannel.getSound()); } } @@ -90,6 +95,16 @@ public class SoundPreferenceController extends NotificationPreferenceController public boolean handlePreferenceTreeClick(Preference preference) { if (KEY_SOUND.equals(preference.getKey()) && mFragment != null) { NotificationSoundPreference pref = (NotificationSoundPreference) preference; + if (mChannel != null && mChannel.getAudioAttributes() != null) { + if (USAGE_ALARM == mChannel.getAudioAttributes().getUsage()) { + pref.setRingtoneType(RingtoneManager.TYPE_ALARM); + } else if (USAGE_NOTIFICATION_RINGTONE + == mChannel.getAudioAttributes().getUsage()) { + pref.setRingtoneType(RingtoneManager.TYPE_RINGTONE); + } else { + pref.setRingtoneType(RingtoneManager.TYPE_NOTIFICATION); + } + } pref.onPrepareRingtonePickerIntent(pref.getIntent()); mFragment.startActivityForResult(preference.getIntent(), CODE); return true; diff --git a/src/com/android/settings/notification/SoundSettings.java b/src/com/android/settings/notification/SoundSettings.java index 4e8218fbb5..eec0fb8018 100644 --- a/src/com/android/settings/notification/SoundSettings.java +++ b/src/com/android/settings/notification/SoundSettings.java @@ -16,6 +16,7 @@ package com.android.settings.notification; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; import android.os.Bundle; @@ -25,35 +26,35 @@ import android.os.Message; import android.os.UserHandle; import android.preference.SeekBarVolumizer; import android.provider.SearchIndexableResource; +import android.text.TextUtils; + import androidx.annotation.VisibleForTesting; import androidx.preference.ListPreference; import androidx.preference.Preference; -import android.text.TextUtils; -import com.android.internal.logging.nano.MetricsProto; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.RingtonePreference; +import com.android.settings.core.OnActivityResultListener; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.sound.HandsFreeProfileOutputPreferenceController; -import com.android.settings.sound.MediaOutputPreferenceController; import com.android.settings.widget.PreferenceCategoryController; import com.android.settings.widget.UpdatableListPreferenceDialogFragment; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.instrumentation.Instrumentable; import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -public class SoundSettings extends DashboardFragment { +@SearchIndexable +public class SoundSettings extends DashboardFragment implements OnActivityResultListener { private static final String TAG = "SoundSettings"; private static final String SELECTED_PREFERENCE_KEY = "selected_preference"; private static final int REQUEST_CODE = 200; - private static final String KEY_ZEN_MODE = "zen_mode"; private static final int SAMPLE_CUTOFF = 2000; // manually cap sample playback at 2 seconds @VisibleForTesting @@ -75,12 +76,11 @@ public class SoundSettings extends DashboardFragment { private RingtonePreference mRequestPreference; private UpdatableListPreferenceDialogFragment mDialogFragment; - private String mMediaOutputControllerKey; private String mHfpOutputControllerKey; @Override public int getMetricsCategory() { - return MetricsEvent.SOUND; + return SettingsEnums.SOUND; } @Override @@ -115,7 +115,7 @@ public class SoundSettings extends DashboardFragment { if (preference instanceof RingtonePreference) { mRequestPreference = (RingtonePreference) preference; mRequestPreference.onPrepareRingtonePickerIntent(mRequestPreference.getIntent()); - startActivityForResultAsUser( + getActivity().startActivityForResultAsUser( mRequestPreference.getIntent(), REQUEST_CODE, null, @@ -129,9 +129,7 @@ public class SoundSettings extends DashboardFragment { public void onDisplayPreferenceDialog(Preference preference) { final int metricsCategory; if (mHfpOutputControllerKey.equals(preference.getKey())) { - metricsCategory = MetricsProto.MetricsEvent.DIALOG_SWITCH_HFP_DEVICES; - } else if (mMediaOutputControllerKey.equals(preference.getKey())) { - metricsCategory = MetricsProto.MetricsEvent.DIALOG_SWITCH_A2DP_DEVICES; + metricsCategory = SettingsEnums.DIALOG_SWITCH_HFP_DEVICES; } else { metricsCategory = Instrumentable.METRICS_CATEGORY_UNKNOWN; } @@ -154,7 +152,7 @@ public class SoundSettings extends DashboardFragment { @Override protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { - return buildPreferenceControllers(context, this, getLifecycle()); + return buildPreferenceControllers(context, this, getSettingsLifecycle()); } @Override @@ -182,18 +180,16 @@ public class SoundSettings extends DashboardFragment { volumeControllers.add(use(RingVolumePreferenceController.class)); volumeControllers.add(use(NotificationVolumePreferenceController.class)); volumeControllers.add(use(CallVolumePreferenceController.class)); + volumeControllers.add(use(RemoteVolumePreferenceController.class)); - use(MediaOutputPreferenceController.class).setCallback(listPreference -> - onPreferenceDataChanged(listPreference)); - mMediaOutputControllerKey = use(MediaOutputPreferenceController.class).getPreferenceKey(); use(HandsFreeProfileOutputPreferenceController.class).setCallback(listPreference -> - onPreferenceDataChanged(listPreference)); + onPreferenceDataChanged(listPreference)); mHfpOutputControllerKey = use(HandsFreeProfileOutputPreferenceController.class).getPreferenceKey(); for (VolumeSeekBarPreferenceController controller : volumeControllers) { controller.setCallback(mVolumeCallback); - getLifecycle().addObserver(controller); + getSettingsLifecycle().addObserver(controller); } } @@ -232,7 +228,6 @@ public class SoundSettings extends DashboardFragment { private static List<AbstractPreferenceController> buildPreferenceControllers(Context context, SoundSettings fragment, Lifecycle lifecycle) { final List<AbstractPreferenceController> controllers = new ArrayList<>(); - controllers.add(new ZenModePreferenceController(context, lifecycle, KEY_ZEN_MODE)); // Volumes are added via xml @@ -306,15 +301,6 @@ public class SoundSettings extends DashboardFragment { return buildPreferenceControllers(context, null /* fragment */, null /* lifecycle */); } - - @Override - public List<String> getNonIndexableKeys(Context context) { - List<String> keys = super.getNonIndexableKeys(context); - // Duplicate results - keys.add((new ZenModePreferenceController(context, null, KEY_ZEN_MODE)) - .getPreferenceKey()); - return keys; - } }; // === Work Sound Settings === diff --git a/src/com/android/settings/notification/SuppressorHelper.java b/src/com/android/settings/notification/SuppressorHelper.java index 837517ca82..d3a017c4c6 100644 --- a/src/com/android/settings/notification/SuppressorHelper.java +++ b/src/com/android/settings/notification/SuppressorHelper.java @@ -20,9 +20,10 @@ import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; -import androidx.annotation.VisibleForTesting; import android.util.Log; +import androidx.annotation.VisibleForTesting; + public class SuppressorHelper { private static final String TAG = "SuppressorHelper"; diff --git a/src/com/android/settings/notification/SwipeDirectionPreferenceController.java b/src/com/android/settings/notification/SwipeDirectionPreferenceController.java new file mode 100644 index 0000000000..0d122d7899 --- /dev/null +++ b/src/com/android/settings/notification/SwipeDirectionPreferenceController.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2019 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.notification; + +import android.content.Context; +import android.provider.Settings; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; + +import androidx.preference.ListPreference; +import androidx.preference.Preference; + +public class SwipeDirectionPreferenceController extends BasePreferenceController + implements Preference.OnPreferenceChangeListener { + + public SwipeDirectionPreferenceController(Context context, String key) { + super(context, key); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } + + @Override + public void updateState(Preference pref) { + ((ListPreference) pref).setValue(String.valueOf(Settings.Secure.getInt( + mContext.getContentResolver(), + Settings.Secure.NOTIFICATION_DISMISS_RTL, + 1))); + super.updateState(pref); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.NOTIFICATION_DISMISS_RTL, + Integer.valueOf((String) newValue)); + refreshSummary(preference); + return true; + } + + @Override + public CharSequence getSummary() { + int value = Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.NOTIFICATION_DISMISS_RTL, 1); + String[] values = mContext.getResources().getStringArray(R.array.swipe_direction_values); + String[] titles = mContext.getResources().getStringArray(R.array.swipe_direction_titles); + if (values == null) { + return null; + } + for (int i = 0; i < values.length; i++) { + int valueAt = Integer.parseInt(values[i]); + if (value == valueAt) { + return titles[i]; + } + } + return null; + } +} diff --git a/src/com/android/settings/notification/TouchSoundPreferenceController.java b/src/com/android/settings/notification/TouchSoundPreferenceController.java index 20c9cee0d0..608cf67796 100644 --- a/src/com/android/settings/notification/TouchSoundPreferenceController.java +++ b/src/com/android/settings/notification/TouchSoundPreferenceController.java @@ -19,10 +19,10 @@ package com.android.settings.notification; import static com.android.settings.notification.SettingPref.TYPE_SYSTEM; import android.content.Context; - import android.media.AudioManager; import android.os.AsyncTask; import android.provider.Settings.System; + import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; import com.android.settingslib.core.lifecycle.Lifecycle; diff --git a/src/com/android/settings/notification/VibrateOnTouchPreferenceController.java b/src/com/android/settings/notification/VibrateOnTouchPreferenceController.java index acb6b4d52c..0ae4c03040 100644 --- a/src/com/android/settings/notification/VibrateOnTouchPreferenceController.java +++ b/src/com/android/settings/notification/VibrateOnTouchPreferenceController.java @@ -21,6 +21,7 @@ import static com.android.settings.notification.SettingPref.TYPE_SYSTEM; import android.content.Context; import android.os.Vibrator; import android.provider.Settings.System; + import com.android.settings.SettingsPreferenceFragment; import com.android.settingslib.core.lifecycle.Lifecycle; diff --git a/src/com/android/settings/notification/VibrateWhenRingPreferenceController.java b/src/com/android/settings/notification/VibrateWhenRingPreferenceController.java index 3d9557e347..b043cb10f3 100644 --- a/src/com/android/settings/notification/VibrateWhenRingPreferenceController.java +++ b/src/com/android/settings/notification/VibrateWhenRingPreferenceController.java @@ -23,10 +23,12 @@ import android.content.Context; import android.database.ContentObserver; import android.net.Uri; import android.os.Handler; +import android.provider.DeviceConfig; import android.provider.Settings; +import android.text.TextUtils; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.text.TextUtils; import com.android.settings.Utils; import com.android.settings.core.TogglePreferenceController; @@ -37,6 +39,8 @@ import com.android.settingslib.core.lifecycle.events.OnResume; public class VibrateWhenRingPreferenceController extends TogglePreferenceController implements LifecycleObserver, OnResume, OnPause { + /** Flag for whether or not to apply ramping ringer on incoming phone calls. */ + private static final String RAMPING_RINGER_ENABLED = "ramping_ringer_enabled"; private static final String KEY_VIBRATE_WHEN_RINGING = "vibrate_when_ringing"; private final int DEFAULT_VALUE = 0; private final int NOTIFICATION_VIBRATE_WHEN_RINGING = 1; @@ -61,7 +65,11 @@ public class VibrateWhenRingPreferenceController extends TogglePreferenceControl @Override @AvailabilityStatus public int getAvailabilityStatus() { - return Utils.isVoiceCapable(mContext) ? AVAILABLE : UNSUPPORTED_ON_DEVICE; + // If ramping ringer is enabled then this setting will be injected + // with additional options. + return Utils.isVoiceCapable(mContext) && !isRampingRingerEnabled() + ? AVAILABLE + : UNSUPPORTED_ON_DEVICE; } @Override @@ -122,4 +130,10 @@ public class VibrateWhenRingPreferenceController extends TogglePreferenceControl } } } + + private boolean isRampingRingerEnabled() { + return DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_TELEPHONY, RAMPING_RINGER_ENABLED, false); + } + } diff --git a/src/com/android/settings/notification/VibrationPreferenceController.java b/src/com/android/settings/notification/VibrationPreferenceController.java index 04aa201a58..e6024d0ea9 100644 --- a/src/com/android/settings/notification/VibrationPreferenceController.java +++ b/src/com/android/settings/notification/VibrationPreferenceController.java @@ -16,10 +16,10 @@ package com.android.settings.notification; -import android.app.NotificationChannel; import android.app.NotificationManager; import android.content.Context; import android.os.Vibrator; + import androidx.preference.Preference; import com.android.settings.core.PreferenceControllerMixin; @@ -56,7 +56,7 @@ public class VibrationPreferenceController extends NotificationPreferenceControl if (mChannel != null) { RestrictedSwitchPreference pref = (RestrictedSwitchPreference) preference; pref.setDisabledByAdmin(mAdmin); - pref.setEnabled(!pref.isDisabledByAdmin() && isChannelConfigurable()); + pref.setEnabled(!pref.isDisabledByAdmin()); pref.setChecked(mChannel.shouldVibrate()); } } diff --git a/src/com/android/settings/notification/VisibilityPreferenceController.java b/src/com/android/settings/notification/VisibilityPreferenceController.java index 5fde67e8a9..fe036e9183 100644 --- a/src/com/android/settings/notification/VisibilityPreferenceController.java +++ b/src/com/android/settings/notification/VisibilityPreferenceController.java @@ -25,6 +25,7 @@ import android.content.pm.UserInfo; import android.os.UserHandle; import android.provider.Settings; import android.service.notification.NotificationListenerService; + import androidx.preference.Preference; import com.android.internal.widget.LockPatternUtils; @@ -32,6 +33,7 @@ import com.android.settings.R; import com.android.settings.RestrictedListPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; import java.util.ArrayList; @@ -127,7 +129,7 @@ public class VisibilityPreferenceController extends NotificationPreferenceContro private void setRestrictedIfNotificationFeaturesDisabled(RestrictedListPreference pref, CharSequence entry, CharSequence entryValue, int keyguardNotificationFeatures) { RestrictedLockUtils.EnforcedAdmin admin = - RestrictedLockUtils.checkIfKeyguardFeaturesDisabled( + RestrictedLockUtilsInternal.checkIfKeyguardFeaturesDisabled( mContext, keyguardNotificationFeatures, mAppRow.userId); if (admin != null) { RestrictedListPreference.RestrictedItem item = diff --git a/src/com/android/settings/notification/VolumeSeekBarPreference.java b/src/com/android/settings/notification/VolumeSeekBarPreference.java index ee02b628b0..92b3cae61b 100644 --- a/src/com/android/settings/notification/VolumeSeekBarPreference.java +++ b/src/com/android/settings/notification/VolumeSeekBarPreference.java @@ -21,16 +21,16 @@ import android.content.Context; import android.media.AudioManager; import android.net.Uri; import android.preference.SeekBarVolumizer; -import androidx.annotation.VisibleForTesting; -import androidx.preference.PreferenceViewHolder; import android.text.TextUtils; import android.util.AttributeSet; -import android.util.Log; import android.view.View; import android.widget.ImageView; import android.widget.SeekBar; import android.widget.TextView; +import androidx.annotation.VisibleForTesting; +import androidx.preference.PreferenceViewHolder; + import com.android.settings.R; import com.android.settings.widget.SeekBarPreference; @@ -40,8 +40,8 @@ import java.util.Objects; public class VolumeSeekBarPreference extends SeekBarPreference { private static final String TAG = "VolumeSeekBarPreference"; + protected SeekBar mSeekBar; private int mStream; - private SeekBar mSeekBar; private SeekBarVolumizer mVolumizer; private Callback mCallback; private ImageView mIconView; @@ -116,7 +116,7 @@ public class VolumeSeekBarPreference extends SeekBarPreference { init(); } - private void init() { + protected void init() { if (mSeekBar == null) return; final SeekBarVolumizer.Callback sbvc = new SeekBarVolumizer.Callback() { @Override @@ -153,7 +153,7 @@ public class VolumeSeekBarPreference extends SeekBarPreference { } } - private void updateIconView() { + protected void updateIconView() { if (mIconView == null) return; if (mIconResId != 0) { mIconView.setImageResource(mIconResId); @@ -190,7 +190,7 @@ public class VolumeSeekBarPreference extends SeekBarPreference { updateSuppressionText(); } - private void updateSuppressionText() { + protected void updateSuppressionText() { if (mSuppressionTextView != null && mSeekBar != null) { mSuppressionTextView.setText(mSuppressionText); final boolean showSuppression = !TextUtils.isEmpty(mSuppressionText); diff --git a/src/com/android/settings/notification/VolumeSeekBarPreferenceController.java b/src/com/android/settings/notification/VolumeSeekBarPreferenceController.java index 720fddd720..f7bf75f13c 100644 --- a/src/com/android/settings/notification/VolumeSeekBarPreferenceController.java +++ b/src/com/android/settings/notification/VolumeSeekBarPreferenceController.java @@ -16,12 +16,13 @@ package com.android.settings.notification; +import android.content.Context; + +import androidx.annotation.VisibleForTesting; import androidx.lifecycle.LifecycleObserver; import androidx.lifecycle.OnLifecycleEvent; -import android.content.Context; import androidx.preference.PreferenceScreen; -import com.android.internal.annotations.VisibleForTesting; import com.android.settings.notification.VolumeSeekBarPreference.Callback; import com.android.settingslib.core.lifecycle.Lifecycle; @@ -53,7 +54,7 @@ public abstract class VolumeSeekBarPreferenceController extends public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); if (isAvailable()) { - mPreference = (VolumeSeekBarPreference) screen.findPreference(getPreferenceKey()); + mPreference = screen.findPreference(getPreferenceKey()); mPreference.setCallback(mVolumePreferenceCallback); mPreference.setStream(getAudioStream()); mPreference.setMuteIcon(getMuteIcon()); @@ -91,13 +92,21 @@ public abstract class VolumeSeekBarPreferenceController extends } @Override - public int getMaxSteps() { + public int getMax() { if (mPreference != null) { return mPreference.getMax(); } return mHelper.getMaxVolume(getAudioStream()); } + @Override + public int getMin() { + if (mPreference != null) { + return mPreference.getMin(); + } + return mHelper.getMinVolume(getAudioStream()); + } + protected abstract int getAudioStream(); protected abstract int getMuteIcon(); diff --git a/src/com/android/settings/notification/WorkSoundPreferenceController.java b/src/com/android/settings/notification/WorkSoundPreferenceController.java index 108fbcaa2e..e23d9ea5b9 100644 --- a/src/com/android/settings/notification/WorkSoundPreferenceController.java +++ b/src/com/android/settings/notification/WorkSoundPreferenceController.java @@ -17,9 +17,8 @@ package com.android.settings.notification; import android.annotation.UserIdInt; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.FragmentManager; +import android.app.settings.SettingsEnums; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; @@ -32,14 +31,16 @@ import android.os.Bundle; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; + +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.FragmentManager; import androidx.preference.Preference; import androidx.preference.Preference.OnPreferenceChangeListener; import androidx.preference.PreferenceGroup; import androidx.preference.PreferenceScreen; import androidx.preference.TwoStatePreference; -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.DefaultRingtonePreference; import com.android.settings.R; import com.android.settings.Utils; @@ -51,6 +52,8 @@ import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnPause; import com.android.settingslib.core.lifecycle.events.OnResume; +import java.util.List; + public class WorkSoundPreferenceController extends AbstractPreferenceController implements PreferenceControllerMixin, OnPreferenceChangeListener, LifecycleObserver, OnResume, OnPause { @@ -96,10 +99,8 @@ public class WorkSoundPreferenceController extends AbstractPreferenceController @Override public void displayPreference(PreferenceScreen screen) { - mWorkPreferenceCategory = (PreferenceGroup) screen.findPreference(KEY_WORK_CATEGORY); - if (mWorkPreferenceCategory != null) { - mWorkPreferenceCategory.setVisible(isAvailable()); - } + super.displayPreference(screen); + mWorkPreferenceCategory = screen.findPreference(KEY_WORK_CATEGORY); } @Override @@ -157,6 +158,18 @@ public class WorkSoundPreferenceController extends AbstractPreferenceController return true; } + @Override + public void updateNonIndexableKeys(List<String> keys) { + if (isAvailable()) { + return; + } + keys.add(KEY_WORK_CATEGORY); + keys.add(KEY_WORK_USE_PERSONAL_SOUNDS); + keys.add(KEY_WORK_NOTIFICATION_RINGTONE); + keys.add(KEY_WORK_PHONE_RINGTONE); + keys.add(KEY_WORK_ALARM_RINGTONE); + } + // === Phone & notification ringtone === private boolean shouldShowRingtoneSettings() { @@ -318,7 +331,7 @@ public class WorkSoundPreferenceController extends AbstractPreferenceController @Override public int getMetricsCategory() { - return MetricsEvent.DIALOG_UNIFY_SOUND_SETTINGS; + return SettingsEnums.DIALOG_UNIFY_SOUND_SETTINGS; } @Override diff --git a/src/com/android/settings/notification/ZenAccessSettings.java b/src/com/android/settings/notification/ZenAccessSettings.java index 62c9315f8a..fca8255224 100644 --- a/src/com/android/settings/notification/ZenAccessSettings.java +++ b/src/com/android/settings/notification/ZenAccessSettings.java @@ -18,56 +18,47 @@ package com.android.settings.notification; import android.annotation.Nullable; import android.app.ActivityManager; -import android.app.AlertDialog; -import android.app.AppGlobals; -import android.app.Dialog; import android.app.NotificationManager; +import android.app.settings.SettingsEnums; import android.content.Context; -import android.content.DialogInterface; import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; import android.content.pm.PackageItemInfo; import android.content.pm.PackageManager; -import android.content.pm.ParceledListSlice; -import android.database.ContentObserver; -import android.net.Uri; -import android.os.AsyncTask; import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.os.RemoteException; -import android.provider.Settings.Secure; -import androidx.preference.SwitchPreference; -import androidx.preference.Preference; -import androidx.preference.Preference.OnPreferenceChangeListener; -import androidx.preference.PreferenceScreen; -import android.text.TextUtils; +import android.provider.SearchIndexableResource; import android.util.ArraySet; -import android.util.Log; import android.view.View; -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.preference.PreferenceScreen; + import com.android.settings.R; -import com.android.settings.core.instrumentation.InstrumentedDialogFragment; -import com.android.settings.overlay.FeatureFactory; -import com.android.settings.widget.AppSwitchPreference; +import com.android.settings.applications.AppInfoBase; +import com.android.settings.applications.specialaccess.zenaccess.ZenAccessController; +import com.android.settings.applications.specialaccess.zenaccess.ZenAccessDetails; +import com.android.settings.applications.specialaccess.zenaccess.ZenAccessSettingObserverMixin; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; +import com.android.settings.widget.EmptyTextSettings; +import com.android.settingslib.search.SearchIndexable; +import com.android.settingslib.widget.apppreference.AppPreference; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Set; -public class ZenAccessSettings extends EmptyTextSettings { +@SearchIndexable +public class ZenAccessSettings extends EmptyTextSettings implements + ZenAccessSettingObserverMixin.Listener { private final String TAG = "ZenAccessSettings"; - private final SettingObserver mObserver = new SettingObserver(); private Context mContext; private PackageManager mPkgMan; private NotificationManager mNoMan; @Override public int getMetricsCategory() { - return MetricsEvent.NOTIFICATION_ZEN_MODE_ACCESS; + return SettingsEnums.NOTIFICATION_ZEN_MODE_ACCESS; } @Override @@ -77,6 +68,8 @@ public class ZenAccessSettings extends EmptyTextSettings { mContext = getActivity(); mPkgMan = mContext.getPackageManager(); mNoMan = mContext.getSystemService(NotificationManager.class); + getSettingsLifecycle().addObserver( + new ZenAccessSettingObserverMixin(getContext(), this /* listener */)); } @Override @@ -95,30 +88,22 @@ public class ZenAccessSettings extends EmptyTextSettings { super.onResume(); if (!ActivityManager.isLowRamDeviceStatic()) { reloadList(); - getContentResolver().registerContentObserver( - Secure.getUriFor(Secure.ENABLED_NOTIFICATION_POLICY_ACCESS_PACKAGES), false, - mObserver); - getContentResolver().registerContentObserver( - Secure.getUriFor(Secure.ENABLED_NOTIFICATION_LISTENERS), false, - mObserver); } else { setEmptyText(R.string.disabled_low_ram_device); } } @Override - public void onPause() { - super.onPause(); - if (!ActivityManager.isLowRamDeviceStatic()) { - getContentResolver().unregisterContentObserver(mObserver); - } + public void onZenAccessPolicyChanged() { + reloadList(); } private void reloadList() { final PreferenceScreen screen = getPreferenceScreen(); screen.removeAll(); final ArrayList<ApplicationInfo> apps = new ArrayList<>(); - final ArraySet<String> requesting = getPackagesRequestingNotificationPolicyAccess(); + final Set<String> requesting = + ZenAccessController.getPackagesRequestingNotificationPolicyAccess(); if (!requesting.isEmpty()) { final List<ApplicationInfo> installed = mPkgMan.getInstalledApplications(0); if (installed != null) { @@ -136,203 +121,55 @@ public class ZenAccessSettings extends EmptyTextSettings { for (ApplicationInfo app : apps) { final String pkg = app.packageName; final CharSequence label = app.loadLabel(mPkgMan); - final SwitchPreference pref = new AppSwitchPreference(getPrefContext()); + final AppPreference pref = new AppPreference(getPrefContext()); pref.setKey(pkg); - pref.setPersistent(false); pref.setIcon(app.loadIcon(mPkgMan)); pref.setTitle(label); - pref.setChecked(hasAccess(pkg)); if (autoApproved.contains(pkg)) { + //Auto approved, user cannot do anything. Hard code summary and disable preference. pref.setEnabled(false); pref.setSummary(getString(R.string.zen_access_disabled_package_warning)); + } else { + // Not auto approved, update summary according to notification backend. + pref.setSummary(getPreferenceSummary(pkg)); } - pref.setOnPreferenceChangeListener(new OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - final boolean access = (Boolean) newValue; - if (access) { - new ScaryWarningDialogFragment() - .setPkgInfo(pkg, label) - .show(getFragmentManager(), "dialog"); - } else { - new FriendlyWarningDialogFragment() - .setPkgInfo(pkg, label) - .show(getFragmentManager(), "dialog"); - } - return false; - } + pref.setOnPreferenceClickListener(preference -> { + AppInfoBase.startAppInfoFragment( + ZenAccessDetails.class /* fragment */, + R.string.manage_zen_access_title /* titleRes */, + pkg, + app.uid, + this /* source */, + -1 /* requestCode */, + getMetricsCategory() /* sourceMetricsCategory */); + return true; }); - screen.addPreference(pref); - } - } - private ArraySet<String> getPackagesRequestingNotificationPolicyAccess() { - ArraySet<String> requestingPackages = new ArraySet<>(); - try { - final String[] PERM = { - android.Manifest.permission.ACCESS_NOTIFICATION_POLICY - }; - final ParceledListSlice list = AppGlobals.getPackageManager() - .getPackagesHoldingPermissions(PERM, 0 /*flags*/, - ActivityManager.getCurrentUser()); - final List<PackageInfo> pkgs = list.getList(); - if (pkgs != null) { - for (PackageInfo info : pkgs) { - requestingPackages.add(info.packageName); - } - } - } catch(RemoteException e) { - Log.e(TAG, "Cannot reach packagemanager", e); - } - return requestingPackages; - } - - private boolean hasAccess(String pkg) { - return mNoMan.isNotificationPolicyAccessGrantedForPackage(pkg); - } - - private static void setAccess(final Context context, final String pkg, final boolean access) { - logSpecialPermissionChange(access, pkg, context); - AsyncTask.execute(new Runnable() { - @Override - public void run() { - final NotificationManager mgr = context.getSystemService(NotificationManager.class); - mgr.setNotificationPolicyAccessGranted(pkg, access); - } - }); - } - - @VisibleForTesting - static void logSpecialPermissionChange(boolean enable, String packageName, Context context) { - int logCategory = enable ? MetricsEvent.APP_SPECIAL_PERMISSION_DND_ALLOW - : MetricsEvent.APP_SPECIAL_PERMISSION_DND_DENY; - FeatureFactory.getFactory(context).getMetricsFeatureProvider().action(context, - logCategory, packageName); - } - - - private static void deleteRules(final Context context, final String pkg) { - AsyncTask.execute(new Runnable() { - @Override - public void run() { - final NotificationManager mgr = context.getSystemService(NotificationManager.class); - mgr.removeAutomaticZenRules(pkg); - } - }); - } - - private final class SettingObserver extends ContentObserver { - public SettingObserver() { - super(new Handler(Looper.getMainLooper())); - } - - @Override - public void onChange(boolean selfChange, Uri uri) { - reloadList(); + screen.addPreference(pref); } } /** - * Warning dialog when allowing zen access warning about the privileges being granted. + * @return the summary for the current state of whether the app associated with the given + * {@param packageName} is allowed to enter picture-in-picture. */ - public static class ScaryWarningDialogFragment extends InstrumentedDialogFragment { - static final String KEY_PKG = "p"; - static final String KEY_LABEL = "l"; - - @Override - public int getMetricsCategory() { - return MetricsEvent.DIALOG_ZEN_ACCESS_GRANT; - } - - public ScaryWarningDialogFragment setPkgInfo(String pkg, CharSequence label) { - Bundle args = new Bundle(); - args.putString(KEY_PKG, pkg); - args.putString(KEY_LABEL, TextUtils.isEmpty(label) ? pkg : label.toString()); - setArguments(args); - return this; - } - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - final Bundle args = getArguments(); - final String pkg = args.getString(KEY_PKG); - final String label = args.getString(KEY_LABEL); - - final String title = getResources().getString(R.string.zen_access_warning_dialog_title, - label); - final String summary = getResources() - .getString(R.string.zen_access_warning_dialog_summary); - return new AlertDialog.Builder(getContext()) - .setMessage(summary) - .setTitle(title) - .setCancelable(true) - .setPositiveButton(R.string.allow, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - setAccess(getContext(), pkg, true); - } - }) - .setNegativeButton(R.string.deny, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - // pass - } - }) - .create(); - } + private int getPreferenceSummary(String packageName) { + final boolean enabled = ZenAccessController.hasAccess(getContext(), packageName); + return enabled ? R.string.app_permission_summary_allowed + : R.string.app_permission_summary_not_allowed; } - /** - * Warning dialog when revoking zen access warning that zen rule instances will be deleted. - */ - public static class FriendlyWarningDialogFragment extends InstrumentedDialogFragment { - static final String KEY_PKG = "p"; - static final String KEY_LABEL = "l"; - - - @Override - public int getMetricsCategory() { - return MetricsEvent.DIALOG_ZEN_ACCESS_REVOKE; - } - - public FriendlyWarningDialogFragment setPkgInfo(String pkg, CharSequence label) { - Bundle args = new Bundle(); - args.putString(KEY_PKG, pkg); - args.putString(KEY_LABEL, TextUtils.isEmpty(label) ? pkg : label.toString()); - setArguments(args); - return this; - } - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - final Bundle args = getArguments(); - final String pkg = args.getString(KEY_PKG); - final String label = args.getString(KEY_LABEL); - - final String title = getResources().getString( - R.string.zen_access_revoke_warning_dialog_title, label); - final String summary = getResources() - .getString(R.string.zen_access_revoke_warning_dialog_summary); - return new AlertDialog.Builder(getContext()) - .setMessage(summary) - .setTitle(title) - .setCancelable(true) - .setPositiveButton(R.string.okay, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - deleteRules(getContext(), pkg); - setAccess(getContext(), pkg, false); - } - }) - .setNegativeButton(R.string.cancel, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - // pass - } - }) - .create(); - } - } + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, + boolean enabled) { + final ArrayList<SearchIndexableResource> result = new ArrayList<>(); + + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.zen_access_settings; + result.add(sir); + return result; + } + }; } diff --git a/src/com/android/settings/notification/ZenAutomaticRuleHeaderPreferenceController.java b/src/com/android/settings/notification/ZenAutomaticRuleHeaderPreferenceController.java index 388b40d14b..73a03cbb3c 100644 --- a/src/com/android/settings/notification/ZenAutomaticRuleHeaderPreferenceController.java +++ b/src/com/android/settings/notification/ZenAutomaticRuleHeaderPreferenceController.java @@ -19,35 +19,34 @@ package com.android.settings.notification; import static com.android.settings.widget.EntityHeaderController.PREF_KEY_APP_HEADER; import android.app.AutomaticZenRule; -import android.app.Fragment; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; import android.service.notification.ZenModeConfig; -import androidx.preference.PreferenceFragment; -import androidx.preference.Preference; import android.util.Slog; import android.view.View; -import com.android.internal.logging.nano.MetricsProto; +import androidx.preference.Preference; +import androidx.preference.PreferenceFragmentCompat; + import com.android.settings.R; -import com.android.settings.applications.LayoutPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.widget.EntityHeaderController; import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.widget.LayoutPreference; public class ZenAutomaticRuleHeaderPreferenceController extends AbstractZenModePreferenceController implements PreferenceControllerMixin { private final String KEY = PREF_KEY_APP_HEADER; - private final PreferenceFragment mFragment; + private final PreferenceFragmentCompat mFragment; private AutomaticZenRule mRule; private String mId; private EntityHeaderController mController; - public ZenAutomaticRuleHeaderPreferenceController(Context context, PreferenceFragment fragment, - Lifecycle lifecycle) { + public ZenAutomaticRuleHeaderPreferenceController(Context context, + PreferenceFragmentCompat fragment, Lifecycle lifecycle) { super(context, PREF_KEY_APP_HEADER, lifecycle); mFragment = fragment; } @@ -74,14 +73,6 @@ public class ZenAutomaticRuleHeaderPreferenceController extends AbstractZenModeP mController = EntityHeaderController .newInstance(mFragment.getActivity(), mFragment, pref.findViewById(R.id.entity_header)); - - mController.setEditZenRuleNameListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - ZenRuleNameDialog.show(mFragment, mRule.getName(), null, - new RuleNameChangeListener()); - } - }); } pref = mController.setIcon(getIcon()) @@ -89,7 +80,7 @@ public class ZenAutomaticRuleHeaderPreferenceController extends AbstractZenModeP .setPackageName(mRule.getOwner().getPackageName()) .setUid(mContext.getUserId()) .setHasAppInfoLink(false) - .setButtonActions(EntityHeaderController.ActionType.ACTION_DND_RULE_PREFERENCE, + .setButtonActions(EntityHeaderController.ActionType.ACTION_EDIT_PREFERENCE, EntityHeaderController.ActionType.ACTION_NONE) .done(mFragment.getActivity(), mContext); @@ -121,16 +112,4 @@ public class ZenAutomaticRuleHeaderPreferenceController extends AbstractZenModeP mRule = rule; mId = id; } - - public class RuleNameChangeListener implements ZenRuleNameDialog.PositiveClickListener { - public RuleNameChangeListener() {} - - @Override - public void onOk(String ruleName, Fragment parent) { - mMetricsFeatureProvider.action(mContext, - MetricsProto.MetricsEvent.ACTION_ZEN_MODE_RULE_NAME_CHANGE_OK); - mRule.setName(ruleName); - mBackend.setZenRule(mId, mRule); - } - } } diff --git a/src/com/android/settings/notification/ZenAutomaticRuleSwitchPreferenceController.java b/src/com/android/settings/notification/ZenAutomaticRuleSwitchPreferenceController.java index 599af26824..b9ed734680 100644 --- a/src/com/android/settings/notification/ZenAutomaticRuleSwitchPreferenceController.java +++ b/src/com/android/settings/notification/ZenAutomaticRuleSwitchPreferenceController.java @@ -17,16 +17,17 @@ package com.android.settings.notification; import android.app.AutomaticZenRule; -import android.app.Fragment; import android.content.Context; +import android.widget.Switch; + +import androidx.fragment.app.Fragment; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.widget.Switch; import com.android.settings.R; -import com.android.settings.applications.LayoutPreference; import com.android.settings.widget.SwitchBar; import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.widget.LayoutPreference; public class ZenAutomaticRuleSwitchPreferenceController extends AbstractZenModeAutomaticRulePreferenceController implements @@ -55,13 +56,18 @@ public class ZenAutomaticRuleSwitchPreferenceController extends @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - LayoutPreference pref = (LayoutPreference) screen.findPreference(KEY); + LayoutPreference pref = screen.findPreference(KEY); mSwitchBar = pref.findViewById(R.id.switch_bar); if (mSwitchBar != null) { mSwitchBar.setSwitchBarText(R.string.zen_mode_use_automatic_rule, R.string.zen_mode_use_automatic_rule); try { + pref.setOnPreferenceClickListener(preference -> { + mRule.setEnabled(!mRule.isEnabled()); + mBackend.updateZenRule(mId, mRule); + return true; + }); mSwitchBar.addOnSwitchChangeListener(this); } catch (IllegalStateException e) { // an exception is thrown if you try to add the listener twice @@ -70,7 +76,6 @@ public class ZenAutomaticRuleSwitchPreferenceController extends } } - public void onResume(AutomaticZenRule rule, String id) { mRule = rule; mId = id; @@ -87,6 +92,6 @@ public class ZenAutomaticRuleSwitchPreferenceController extends final boolean enabled = isChecked; if (enabled == mRule.isEnabled()) return; mRule.setEnabled(enabled); - mBackend.setZenRule(mId, mRule); + mBackend.updateZenRule(mId, mRule); } } diff --git a/src/com/android/settings/notification/ZenCustomRadioButtonPreference.java b/src/com/android/settings/notification/ZenCustomRadioButtonPreference.java index c28b7b4456..000a756320 100644 --- a/src/com/android/settings/notification/ZenCustomRadioButtonPreference.java +++ b/src/com/android/settings/notification/ZenCustomRadioButtonPreference.java @@ -17,11 +17,12 @@ package com.android.settings.notification; import android.content.Context; -import androidx.preference.PreferenceViewHolder; import android.util.AttributeSet; import android.view.View; import android.widget.RadioButton; +import androidx.preference.PreferenceViewHolder; + import com.android.settings.R; import com.android.settingslib.TwoTargetPreference; diff --git a/src/com/android/settings/notification/ZenCustomRuleBlockedEffectsSettings.java b/src/com/android/settings/notification/ZenCustomRuleBlockedEffectsSettings.java new file mode 100644 index 0000000000..b407445152 --- /dev/null +++ b/src/com/android/settings/notification/ZenCustomRuleBlockedEffectsSettings.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2018 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.notification; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.os.Bundle; +import android.service.notification.ZenPolicy; + +import com.android.settings.R; +import com.android.settingslib.core.AbstractPreferenceController; + +import java.util.ArrayList; +import java.util.List; + +public class ZenCustomRuleBlockedEffectsSettings extends ZenCustomRuleSettingsBase { + + @Override + public void onCreate(Bundle bundle) { + super.onCreate(bundle); + mFooterPreferenceMixin.createFooterPreference().setTitle( + R.string.zen_mode_blocked_effects_footer); + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.zen_mode_block_settings; + } + + @Override + protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { + mControllers = new ArrayList<>(); + mControllers.add(new ZenRuleVisEffectPreferenceController(context, getSettingsLifecycle(), + "zen_effect_intent", ZenPolicy.VISUAL_EFFECT_FULL_SCREEN_INTENT, + SettingsEnums.ACTION_ZEN_BLOCK_FULL_SCREEN_INTENTS, null)); + mControllers.add(new ZenRuleVisEffectPreferenceController(context, getSettingsLifecycle(), + "zen_effect_light", ZenPolicy.VISUAL_EFFECT_LIGHTS, + SettingsEnums.ACTION_ZEN_BLOCK_LIGHT, null)); + mControllers.add(new ZenRuleVisEffectPreferenceController(context, getSettingsLifecycle(), + "zen_effect_peek", ZenPolicy.VISUAL_EFFECT_PEEK, + SettingsEnums.ACTION_ZEN_BLOCK_PEEK, null)); + mControllers.add(new ZenRuleVisEffectPreferenceController(context, getSettingsLifecycle(), + "zen_effect_status", ZenPolicy.VISUAL_EFFECT_STATUS_BAR, + SettingsEnums.ACTION_ZEN_BLOCK_STATUS, + new int[] {ZenPolicy.VISUAL_EFFECT_NOTIFICATION_LIST})); + mControllers.add(new ZenRuleVisEffectPreferenceController(context, getSettingsLifecycle(), + "zen_effect_badge", ZenPolicy.VISUAL_EFFECT_BADGE, + SettingsEnums.ACTION_ZEN_BLOCK_BADGE, null)); + mControllers.add(new ZenRuleVisEffectPreferenceController(context, getSettingsLifecycle(), + "zen_effect_ambient", ZenPolicy.VISUAL_EFFECT_AMBIENT, + SettingsEnums.ACTION_ZEN_BLOCK_AMBIENT, null)); + mControllers.add(new ZenRuleVisEffectPreferenceController(context, getSettingsLifecycle(), + "zen_effect_list", ZenPolicy.VISUAL_EFFECT_NOTIFICATION_LIST, + SettingsEnums.ACTION_ZEN_BLOCK_NOTIFICATION_LIST, null)); + return mControllers; + } + + @Override + String getPreferenceCategoryKey() { + return null; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.ZEN_CUSTOM_RULE_VIS_EFFECTS; + } +} diff --git a/src/com/android/settings/notification/ZenCustomRuleCallsSettings.java b/src/com/android/settings/notification/ZenCustomRuleCallsSettings.java new file mode 100644 index 0000000000..42e62c34db --- /dev/null +++ b/src/com/android/settings/notification/ZenCustomRuleCallsSettings.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2018 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.notification; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.os.Bundle; +import android.service.notification.ZenPolicy; + +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.widget.FooterPreference; + +import java.util.ArrayList; +import java.util.List; + +public class ZenCustomRuleCallsSettings extends ZenCustomRuleSettingsBase { + private static final String CALLS_KEY = "zen_mode_calls"; + private static final String REPEAT_CALLERS_KEY = "zen_mode_repeat_callers"; + private static final String STARRED_CONTACTS_KEY = "zen_mode_starred_contacts_callers"; + private static final String PREFERENCE_CATEGORY_KEY = "zen_mode_settings_category_calls"; + + @Override + public void onCreate(Bundle bundle) { + super.onCreate(bundle); + } + + @Override + protected int getPreferenceScreenResId() { + return com.android.settings.R.xml.zen_mode_calls_settings; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.ZEN_CUSTOM_RULE_CALLS; + } + + @Override + protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { + mControllers = new ArrayList<>(); + mControllers.add(new ZenRuleCallsPreferenceController(context, CALLS_KEY, + getSettingsLifecycle())); + mControllers.add(new ZenRuleRepeatCallersPreferenceController(context, + REPEAT_CALLERS_KEY, getSettingsLifecycle(), context.getResources() + .getInteger(com.android.internal.R.integer.config_zen_repeat_callers_threshold))); + mControllers.add(new ZenRuleStarredContactsPreferenceController(context, + getSettingsLifecycle(), ZenPolicy.PRIORITY_CATEGORY_CALLS, STARRED_CONTACTS_KEY)); + return mControllers; + } + + @Override + String getPreferenceCategoryKey() { + return PREFERENCE_CATEGORY_KEY; + } + + @Override + public void updatePreferences() { + super.updatePreferences(); + PreferenceScreen screen = getPreferenceScreen(); + Preference footerPreference = screen.findPreference(FooterPreference.KEY_FOOTER); + footerPreference.setTitle(mContext.getResources().getString( + R.string.zen_mode_custom_calls_footer, mRule.getName())); + } +} diff --git a/src/com/android/settings/notification/ZenCustomRuleConfigSettings.java b/src/com/android/settings/notification/ZenCustomRuleConfigSettings.java new file mode 100644 index 0000000000..c39b4d500a --- /dev/null +++ b/src/com/android/settings/notification/ZenCustomRuleConfigSettings.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2018 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.notification; + +import android.app.NotificationManager; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.os.Bundle; +import android.service.notification.ZenPolicy; + +import androidx.preference.Preference; + +import com.android.settings.R; +import com.android.settings.core.SubSettingLauncher; +import com.android.settingslib.core.AbstractPreferenceController; + +import java.util.ArrayList; +import java.util.List; + +public class ZenCustomRuleConfigSettings extends ZenCustomRuleSettingsBase { + private static final String CALLS_KEY = "zen_rule_calls_settings"; + private static final String MESSAGES_KEY = "zen_rule_messages_settings"; + private static final String ALARMS_KEY = "zen_rule_alarms"; + private static final String MEDIA_KEY = "zen_rule_media"; + private static final String SYSTEM_KEY = "zen_rule_system"; + private static final String REMINDERS_KEY = "zen_rule_reminders"; + private static final String EVENTS_KEY = "zen_rule_events"; + private static final String NOTIFICATIONS_KEY = "zen_rule_notifications"; + private static final String PREFERENCE_CATEGORY_KEY = "zen_custom_rule_configuration_category"; + + private Preference mCallsPreference; + private Preference mMessagesPreference; + private Preference mNotificationsPreference; + private ZenModeSettings.SummaryBuilder mSummaryBuilder; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + mSummaryBuilder = new ZenModeSettings.SummaryBuilder(mContext); + + mCallsPreference = getPreferenceScreen().findPreference(CALLS_KEY); + mCallsPreference.setOnPreferenceClickListener( + new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + new SubSettingLauncher(mContext) + .setDestination(ZenCustomRuleCallsSettings.class.getName()) + .setArguments(createZenRuleBundle()) + .setSourceMetricsCategory(SettingsEnums.ZEN_CUSTOM_RULE_CALLS) + .launch(); + return true; + } + }); + + mMessagesPreference = getPreferenceScreen().findPreference(MESSAGES_KEY); + mMessagesPreference.setOnPreferenceClickListener( + new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + new SubSettingLauncher(mContext) + .setDestination(ZenCustomRuleMessagesSettings.class.getName()) + .setArguments(createZenRuleBundle()) + .setSourceMetricsCategory(SettingsEnums.ZEN_CUSTOM_RULE_MESSAGES) + .launch(); + return true; + } + }); + + mNotificationsPreference = getPreferenceScreen().findPreference(NOTIFICATIONS_KEY); + mNotificationsPreference.setOnPreferenceClickListener( + new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + new SubSettingLauncher(mContext) + .setDestination(ZenCustomRuleNotificationsSettings.class.getName()) + .setArguments(createZenRuleBundle()) + .setSourceMetricsCategory + (SettingsEnums.ZEN_CUSTOM_RULE_NOTIFICATION_RESTRICTIONS) + .launch(); + return true; + } + }); + + updateSummaries(); + } + + @Override + public void onZenModeConfigChanged() { + super.onZenModeConfigChanged(); + updateSummaries(); + } + + /** + * Updates summaries of preferences without preference controllers + */ + private void updateSummaries() { + NotificationManager.Policy noManPolicy = mBackend.toNotificationPolicy( + mRule.getZenPolicy()); + + mCallsPreference.setSummary(mSummaryBuilder.getCallsSettingSummary(noManPolicy)); + mMessagesPreference.setSummary(mSummaryBuilder.getMessagesSettingSummary(noManPolicy)); + mNotificationsPreference.setSummary(mSummaryBuilder.getBlockedEffectsSummary(noManPolicy)); + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.zen_mode_custom_rule_configuration; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.ZEN_CUSTOM_RULE_SOUND_SETTINGS; + } + + @Override + protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { + mControllers = new ArrayList<>(); + mControllers.add(new ZenRuleCustomSwitchPreferenceController(context, + getSettingsLifecycle(), ALARMS_KEY, ZenPolicy.PRIORITY_CATEGORY_ALARMS, + SettingsEnums.ACTION_ZEN_ALLOW_ALARMS)); + mControllers.add(new ZenRuleCustomSwitchPreferenceController(context, + getSettingsLifecycle(), MEDIA_KEY, ZenPolicy.PRIORITY_CATEGORY_MEDIA, + SettingsEnums.ACTION_ZEN_ALLOW_MEDIA)); + mControllers.add(new ZenRuleCustomSwitchPreferenceController(context, + getSettingsLifecycle(), SYSTEM_KEY, ZenPolicy.PRIORITY_CATEGORY_SYSTEM, + SettingsEnums.ACTION_ZEN_ALLOW_SYSTEM)); + mControllers.add(new ZenRuleCustomSwitchPreferenceController(context, + getSettingsLifecycle(), REMINDERS_KEY, ZenPolicy.PRIORITY_CATEGORY_REMINDERS, + SettingsEnums.ACTION_ZEN_ALLOW_REMINDERS)); + mControllers.add(new ZenRuleCustomSwitchPreferenceController(context, + getSettingsLifecycle(), EVENTS_KEY, ZenPolicy.PRIORITY_CATEGORY_EVENTS, + SettingsEnums.ACTION_ZEN_ALLOW_EVENTS)); + return mControllers; + } + + @Override + String getPreferenceCategoryKey() { + return PREFERENCE_CATEGORY_KEY; + } + + @Override + public void onResume() { + super.onResume(); + updateSummaries(); + } +} diff --git a/src/com/android/settings/notification/ZenCustomRuleMessagesSettings.java b/src/com/android/settings/notification/ZenCustomRuleMessagesSettings.java new file mode 100644 index 0000000000..746dff3fab --- /dev/null +++ b/src/com/android/settings/notification/ZenCustomRuleMessagesSettings.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2018 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.notification; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.service.notification.ZenPolicy; + +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.widget.FooterPreference; + +import java.util.ArrayList; +import java.util.List; + +public class ZenCustomRuleMessagesSettings extends ZenCustomRuleSettingsBase { + private static final String MESSAGES_KEY = "zen_mode_messages"; + private static final String STARRED_CONTACTS_KEY = "zen_mode_starred_contacts_messages"; + private static final String PREFERENCE_CATEGORY_KEY = "zen_mode_settings_category_messages"; + + @Override + protected int getPreferenceScreenResId() { + return com.android.settings.R.xml.zen_mode_messages_settings; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.ZEN_CUSTOM_RULE_MESSAGES; + } + + @Override + protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { + mControllers = new ArrayList<>(); + mControllers.add(new ZenRuleMessagesPreferenceController(context, MESSAGES_KEY, + getSettingsLifecycle())); + mControllers.add(new ZenRuleStarredContactsPreferenceController(context, + getSettingsLifecycle(), ZenPolicy.PRIORITY_CATEGORY_MESSAGES, + STARRED_CONTACTS_KEY)); + return mControllers; + } + + @Override + String getPreferenceCategoryKey() { + return PREFERENCE_CATEGORY_KEY; + } + + @Override + public void updatePreferences() { + super.updatePreferences(); + PreferenceScreen screen = getPreferenceScreen(); + Preference footerPreference = screen.findPreference(FooterPreference.KEY_FOOTER); + footerPreference.setTitle(mContext.getResources().getString( + R.string.zen_mode_custom_messages_footer, mRule.getName())); + } +} diff --git a/src/com/android/settings/notification/ZenCustomRuleNotificationsSettings.java b/src/com/android/settings/notification/ZenCustomRuleNotificationsSettings.java new file mode 100644 index 0000000000..9d7d5a3e75 --- /dev/null +++ b/src/com/android/settings/notification/ZenCustomRuleNotificationsSettings.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2018 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.notification; + +import android.app.settings.SettingsEnums; +import android.content.Context; + +import com.android.settings.R; +import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.widget.FooterPreference; + +import java.util.ArrayList; +import java.util.List; + +public class ZenCustomRuleNotificationsSettings extends ZenCustomRuleSettingsBase { + private static final String VIS_EFFECTS_ALL_KEY = "zen_mute_notifications"; + private static final String VIS_EFFECTS_NONE_KEY = "zen_hide_notifications"; + private static final String VIS_EFFECTS_CUSTOM_KEY = "zen_custom"; + private static final String PREFERENCE_CATEGORY_KEY = "restrict_category"; + + @Override + protected int getPreferenceScreenResId() { + return R.xml.zen_mode_restrict_notifications_settings; + } + + @Override + protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { + mControllers = new ArrayList<>(); + mControllers.add(new ZenRuleVisEffectsAllPreferenceController( + context, getSettingsLifecycle(), VIS_EFFECTS_ALL_KEY)); + mControllers.add(new ZenRuleVisEffectsNonePreferenceController( + context, getSettingsLifecycle(), VIS_EFFECTS_NONE_KEY)); + mControllers.add(new ZenRuleVisEffectsCustomPreferenceController( + context, getSettingsLifecycle(), VIS_EFFECTS_CUSTOM_KEY)); + mControllers.add(new ZenRuleNotifFooterPreferenceController(context, getSettingsLifecycle(), + FooterPreference.KEY_FOOTER)); + return mControllers; + } + + @Override + String getPreferenceCategoryKey() { + return PREFERENCE_CATEGORY_KEY; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.ZEN_CUSTOM_RULE_NOTIFICATION_RESTRICTIONS; + } +} diff --git a/src/com/android/settings/notification/ZenCustomRuleSettings.java b/src/com/android/settings/notification/ZenCustomRuleSettings.java new file mode 100644 index 0000000000..d7ca8c01e1 --- /dev/null +++ b/src/com/android/settings/notification/ZenCustomRuleSettings.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2018 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.notification; + +import android.app.settings.SettingsEnums; +import android.content.Context; + +import com.android.settings.R; +import com.android.settingslib.core.AbstractPreferenceController; + +import java.util.ArrayList; +import java.util.List; + +public class ZenCustomRuleSettings extends ZenCustomRuleSettingsBase { + private static final String RULE_DEFAULT_POLICY_KEY = "zen_custom_rule_setting_default"; + private static final String CUSTOM_RULE_POLICY_KEY = "zen_custom_rule_setting"; + private static final String PREFERENCE_CATEGORY_KEY = "zen_custom_rule_category"; + + @Override + protected int getPreferenceScreenResId() { + return R.xml.zen_mode_custom_rule_settings; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.ZEN_CUSTOM_RULE_SETTINGS; + } + + @Override + protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { + mControllers = new ArrayList<>(); + mControllers.add(new ZenRuleDefaultPolicyPreferenceController( + context, getSettingsLifecycle(), RULE_DEFAULT_POLICY_KEY)); + mControllers.add(new ZenRuleCustomPolicyPreferenceController( + context, getSettingsLifecycle(), CUSTOM_RULE_POLICY_KEY)); + return mControllers; + } + + @Override + String getPreferenceCategoryKey() { + return PREFERENCE_CATEGORY_KEY; + } +} diff --git a/src/com/android/settings/notification/ZenCustomRuleSettingsBase.java b/src/com/android/settings/notification/ZenCustomRuleSettingsBase.java new file mode 100644 index 0000000000..94b0b7b990 --- /dev/null +++ b/src/com/android/settings/notification/ZenCustomRuleSettingsBase.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2018 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.notification; + +import android.app.AutomaticZenRule; +import android.content.Context; +import android.os.Bundle; +import android.util.Log; + +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settingslib.core.AbstractPreferenceController; + +import java.util.ArrayList; +import java.util.List; + +abstract class ZenCustomRuleSettingsBase extends ZenModeSettingsBase { + static final String TAG = "ZenCustomRuleSettings"; + static final String RULE_ID = "RULE_ID"; + + String mId; + AutomaticZenRule mRule; + List<AbstractPreferenceController> mControllers = new ArrayList<>(); + + /** + * @return null if no preference category exists + */ + abstract String getPreferenceCategoryKey(); + + @Override + public void onAttach(Context context) { + super.onAttach(context); + Bundle bundle = getArguments(); + if (bundle != null && bundle.containsKey(RULE_ID)) { + mId = bundle.getString(RULE_ID); + mRule = mBackend.getAutomaticZenRule(mId); + } else { + Log.d(TAG, "Rule id required to set custom dnd rule config settings"); + this.finish(); + } + } + + @Override + public void onZenModeConfigChanged() { + super.onZenModeConfigChanged(); + updatePreferences(); + } + + public void updatePreferences() { + mRule = mBackend.getAutomaticZenRule(mId); + final PreferenceScreen screen = getPreferenceScreen(); + String categoryKey = getPreferenceCategoryKey(); + if (categoryKey != null) { + Preference prefCategory = screen.findPreference(categoryKey); + if (prefCategory != null) { + prefCategory.setTitle(mContext.getResources().getString( + com.android.settings.R.string.zen_mode_custom_behavior_category_title, + mRule.getName())); + } + } + + for (AbstractPreferenceController controller : mControllers) { + AbstractZenCustomRulePreferenceController zenRuleController = + (AbstractZenCustomRulePreferenceController) controller; + zenRuleController.onResume(mRule, mId); + zenRuleController.displayPreference(screen); + updatePreference(zenRuleController); + } + } + + @Override + public int getHelpResource() { + return R.string.help_uri_interruptions; + } + + @Override + public void onResume() { + super.onResume(); + updatePreferences(); + } + + Bundle createZenRuleBundle() { + Bundle bundle = new Bundle(); + bundle.putString(RULE_ID, mId); + return bundle; + } +} diff --git a/src/com/android/settings/notification/ZenDeleteRuleDialog.java b/src/com/android/settings/notification/ZenDeleteRuleDialog.java index d9061d3b0a..694fcbac0a 100644 --- a/src/com/android/settings/notification/ZenDeleteRuleDialog.java +++ b/src/com/android/settings/notification/ZenDeleteRuleDialog.java @@ -16,14 +16,16 @@ package com.android.settings.notification; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.Fragment; +import android.app.settings.SettingsEnums; import android.content.DialogInterface; import android.os.Bundle; +import android.text.BidiFormatter; import android.view.View; -import com.android.internal.logging.nano.MetricsProto; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; + import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; @@ -42,8 +44,9 @@ public class ZenDeleteRuleDialog extends InstrumentedDialogFragment { public static void show(Fragment parent, String ruleName, String id, PositiveClickListener listener) { + final BidiFormatter bidi = BidiFormatter.getInstance(); final Bundle args = new Bundle(); - args.putString(EXTRA_ZEN_RULE_NAME, ruleName); + args.putString(EXTRA_ZEN_RULE_NAME, bidi.unicodeWrap(ruleName)); args.putString(EXTRA_ZEN_RULE_ID, id); mPositiveClickListener = listener; @@ -55,7 +58,7 @@ public class ZenDeleteRuleDialog extends InstrumentedDialogFragment { @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.NOTIFICATION_ZEN_MODE_DELETE_RULE_DIALOG; + return SettingsEnums.NOTIFICATION_ZEN_MODE_DELETE_RULE_DIALOG; } @Override diff --git a/src/com/android/settings/notification/ZenDurationDialogPreference.java b/src/com/android/settings/notification/ZenDurationDialogPreference.java new file mode 100644 index 0000000000..6d6d085cf9 --- /dev/null +++ b/src/com/android/settings/notification/ZenDurationDialogPreference.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2018 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.notification; + +import android.content.Context; +import android.content.DialogInterface; +import android.util.AttributeSet; + +import androidx.appcompat.app.AlertDialog; + +import com.android.settingslib.CustomDialogPreferenceCompat; +import com.android.settingslib.notification.ZenDurationDialog; + +public class ZenDurationDialogPreference extends CustomDialogPreferenceCompat { + + public ZenDurationDialogPreference(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public ZenDurationDialogPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public ZenDurationDialogPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + + @Override + protected void onPrepareDialogBuilder(AlertDialog.Builder builder, + DialogInterface.OnClickListener listener) { + super.onPrepareDialogBuilder(builder, listener); + + ZenDurationDialog zenDialog = new ZenDurationDialog(getContext()); + zenDialog.setupDialog(builder); + } +} diff --git a/src/com/android/settings/notification/ZenFooterPreferenceController.java b/src/com/android/settings/notification/ZenFooterPreferenceController.java index a00ac6a346..15a2252397 100644 --- a/src/com/android/settings/notification/ZenFooterPreferenceController.java +++ b/src/com/android/settings/notification/ZenFooterPreferenceController.java @@ -18,6 +18,7 @@ package com.android.settings.notification; import android.app.NotificationManager.Policy; import android.content.Context; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; diff --git a/src/com/android/settings/notification/ZenModeAddAutomaticRulePreferenceController.java b/src/com/android/settings/notification/ZenModeAddAutomaticRulePreferenceController.java index 8898b055c2..c5ddf48581 100644 --- a/src/com/android/settings/notification/ZenModeAddAutomaticRulePreferenceController.java +++ b/src/com/android/settings/notification/ZenModeAddAutomaticRulePreferenceController.java @@ -16,14 +16,15 @@ package com.android.settings.notification; -import android.app.Fragment; import android.content.Context; import android.content.Intent; + +import androidx.fragment.app.Fragment; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settings.utils.ZenServiceListing; +import com.android.settingslib.core.lifecycle.Lifecycle; public class ZenModeAddAutomaticRulePreferenceController extends AbstractZenModeAutomaticRulePreferenceController implements diff --git a/src/com/android/settings/notification/ZenModeAlarmsPreferenceController.java b/src/com/android/settings/notification/ZenModeAlarmsPreferenceController.java index 70c1ae905e..00e9736499 100644 --- a/src/com/android/settings/notification/ZenModeAlarmsPreferenceController.java +++ b/src/com/android/settings/notification/ZenModeAlarmsPreferenceController.java @@ -17,22 +17,24 @@ package com.android.settings.notification; import android.app.NotificationManager.Policy; +import android.app.settings.SettingsEnums; import android.content.Context; import android.provider.Settings; -import androidx.preference.SwitchPreference; -import androidx.preference.Preference; import android.util.Log; -import com.android.internal.logging.nano.MetricsProto; +import androidx.preference.Preference; +import androidx.preference.SwitchPreference; + import com.android.settingslib.core.lifecycle.Lifecycle; public class ZenModeAlarmsPreferenceController extends AbstractZenModePreferenceController implements Preference.OnPreferenceChangeListener { - protected static final String KEY = "zen_mode_alarms"; + private final String KEY; - public ZenModeAlarmsPreferenceController(Context context, Lifecycle lifecycle) { - super(context, KEY, lifecycle); + public ZenModeAlarmsPreferenceController(Context context, Lifecycle lifecycle, String key) { + super(context, key, lifecycle); + KEY = key; } @Override @@ -73,9 +75,10 @@ public class ZenModeAlarmsPreferenceController extends Log.d(TAG, "onPrefChange allowAlarms=" + allowAlarms); } - mMetricsFeatureProvider.action(mContext, MetricsProto.MetricsEvent.ACTION_ZEN_ALLOW_ALARMS, + mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_ZEN_ALLOW_ALARMS, allowAlarms); mBackend.saveSoundPolicy(Policy.PRIORITY_CATEGORY_ALARMS, allowAlarms); + return true; } } diff --git a/src/com/android/settings/notification/ZenModeAllBypassingAppsPreferenceController.java b/src/com/android/settings/notification/ZenModeAllBypassingAppsPreferenceController.java new file mode 100644 index 0000000000..ee7868cfe5 --- /dev/null +++ b/src/com/android/settings/notification/ZenModeAllBypassingAppsPreferenceController.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2018 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.notification; + +import android.app.Application; +import android.app.NotificationChannel; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.os.Bundle; +import android.provider.Settings; + +import androidx.annotation.VisibleForTesting; +import androidx.core.text.BidiFormatter; +import androidx.fragment.app.Fragment; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.applications.AppInfoBase; +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settings.core.SubSettingLauncher; +import com.android.settingslib.applications.ApplicationsState; +import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.widget.apppreference.AppPreference; + +import java.util.ArrayList; +import java.util.List; + +/** + * Adds a preference to the PreferenceScreen for each notification channel that can bypass DND. + */ +public class ZenModeAllBypassingAppsPreferenceController extends AbstractPreferenceController + implements PreferenceControllerMixin { + + private final String KEY = "zen_mode_bypassing_apps_category"; + + @VisibleForTesting ApplicationsState mApplicationsState; + @VisibleForTesting PreferenceScreen mPreferenceScreen; + @VisibleForTesting Context mPrefContext; + + private ApplicationsState.Session mAppSession; + private NotificationBackend mNotificationBackend = new NotificationBackend(); + private Fragment mHostFragment; + + public ZenModeAllBypassingAppsPreferenceController(Context context, Application app, + Fragment host) { + + this(context, app == null ? null : ApplicationsState.getInstance(app), host); + } + + private ZenModeAllBypassingAppsPreferenceController(Context context, ApplicationsState appState, + Fragment host) { + super(context); + mApplicationsState = appState; + mHostFragment = host; + + if (mApplicationsState != null && host != null) { + mAppSession = mApplicationsState.newSession(mAppSessionCallbacks, host.getLifecycle()); + } + } + + @Override + public void displayPreference(PreferenceScreen screen) { + mPreferenceScreen = screen; + mPrefContext = mPreferenceScreen.getContext(); + updateNotificationChannelList(); + super.displayPreference(screen); + } + + @Override + public boolean isAvailable() { + return true; + } + + @Override + public String getPreferenceKey() { + return KEY; + } + + /** + * Call this method to trigger the notification channels list to refresh. + */ + public void updateNotificationChannelList() { + if (mAppSession == null) { + return; + } + + ApplicationsState.AppFilter filter = ApplicationsState.FILTER_ALL_ENABLED; + List<ApplicationsState.AppEntry> apps = mAppSession.rebuild(filter, + ApplicationsState.ALPHA_COMPARATOR); + if (apps != null) { + updateNotificationChannelList(apps); + } + } + + @VisibleForTesting + void updateNotificationChannelList(List<ApplicationsState.AppEntry> apps) { + if (mPreferenceScreen == null || apps == null) { + return; + } + + List<Preference> channelsBypassingDnd = new ArrayList<>(); + for (ApplicationsState.AppEntry entry : apps) { + String pkg = entry.info.packageName; + mApplicationsState.ensureIcon(entry); + for (NotificationChannel channel : mNotificationBackend + .getNotificationChannelsBypassingDnd(pkg, entry.info.uid).getList()) { + Preference pref = new AppPreference(mPrefContext); + pref.setKey(pkg + "|" + channel.getId()); + pref.setTitle(BidiFormatter.getInstance().unicodeWrap(entry.label)); + pref.setIcon(entry.icon); + pref.setSummary(BidiFormatter.getInstance().unicodeWrap(channel.getName())); + + pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + Bundle args = new Bundle(); + args.putString(AppInfoBase.ARG_PACKAGE_NAME, entry.info.packageName); + args.putInt(AppInfoBase.ARG_PACKAGE_UID, entry.info.uid); + args.putString(Settings.EXTRA_CHANNEL_ID, channel.getId()); + new SubSettingLauncher(mContext) + .setDestination(ChannelNotificationSettings.class.getName()) + .setArguments(args) + .setTitleRes(R.string.notification_channel_title) + .setResultListener(mHostFragment, 0) + .setSourceMetricsCategory( + SettingsEnums.NOTIFICATION_ZEN_MODE_OVERRIDING_APP) + .launch(); + return true; + } + }); + channelsBypassingDnd.add(pref); + } + + mPreferenceScreen.removeAll(); + if (channelsBypassingDnd.size() > 0) { + for (Preference prefToAdd : channelsBypassingDnd) { + mPreferenceScreen.addPreference(prefToAdd); + } + } + } + } + + private final ApplicationsState.Callbacks mAppSessionCallbacks = + new ApplicationsState.Callbacks() { + + @Override + public void onRunningStateChanged(boolean running) { + updateNotificationChannelList(); + } + + @Override + public void onPackageListChanged() { + updateNotificationChannelList(); + } + + @Override + public void onRebuildComplete(ArrayList<ApplicationsState.AppEntry> apps) { + updateNotificationChannelList(apps); + } + + @Override + public void onPackageIconChanged() { + updateNotificationChannelList(); + } + + @Override + public void onPackageSizeChanged(String packageName) { + updateNotificationChannelList(); + } + + @Override + public void onAllSizesComputed() { } + + @Override + public void onLauncherInfoChanged() { + updateNotificationChannelList(); + } + + @Override + public void onLoadEntriesCompleted() { + updateNotificationChannelList(); + } + }; +} diff --git a/src/com/android/settings/notification/ZenModeAutomaticRulesPreferenceController.java b/src/com/android/settings/notification/ZenModeAutomaticRulesPreferenceController.java index 2ea46392c7..2f62f4539b 100644 --- a/src/com/android/settings/notification/ZenModeAutomaticRulesPreferenceController.java +++ b/src/com/android/settings/notification/ZenModeAutomaticRulesPreferenceController.java @@ -17,9 +17,10 @@ package com.android.settings.notification; import android.app.AutomaticZenRule; -import android.app.Fragment; import android.content.Context; + import androidx.annotation.VisibleForTesting; +import androidx.fragment.app.Fragment; import androidx.preference.Preference; import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceScreen; @@ -27,6 +28,7 @@ import androidx.preference.PreferenceScreen; import com.android.settingslib.core.lifecycle.Lifecycle; import java.util.Map; +import java.util.Objects; public class ZenModeAutomaticRulesPreferenceController extends AbstractZenModeAutomaticRulePreferenceController { @@ -54,23 +56,46 @@ public class ZenModeAutomaticRulesPreferenceController extends @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - mPreferenceCategory = (PreferenceCategory) screen.findPreference(getPreferenceKey()); + mPreferenceCategory = screen.findPreference(getPreferenceKey()); mPreferenceCategory.setPersistent(false); } @Override public void updateState(Preference preference) { super.updateState(preference); + Map.Entry<String, AutomaticZenRule>[] sortedRules = getRules(); + final int currNumPreferences = mPreferenceCategory.getPreferenceCount(); + if (currNumPreferences == sortedRules.length) { + for (int i = 0; i < sortedRules.length; i++) { + ZenRulePreference pref = (ZenRulePreference) mPreferenceCategory.getPreference(i); + // we are either: + // 1. updating everything about the rule + // 2. rule was added or deleted, so reload the entire list + if (Objects.equals(pref.mId, sortedRules[i].getKey())) { + AutomaticZenRule rule = sortedRules[i].getValue(); + pref.updatePreference(rule); + } else { + reloadAllRules(sortedRules); + break; + } + } + } else { + reloadAllRules(sortedRules); + } + } + @VisibleForTesting + void reloadAllRules(Map.Entry<String, AutomaticZenRule>[] rules) { mPreferenceCategory.removeAll(); - Map.Entry<String, AutomaticZenRule>[] sortedRules = sortedRules(); - for (Map.Entry<String, AutomaticZenRule> sortedRule : sortedRules) { - ZenRulePreference pref = new ZenRulePreference(mPreferenceCategory.getContext(), - sortedRule, mParent, mMetricsFeatureProvider); + for (Map.Entry<String, AutomaticZenRule> rule : rules) { + ZenRulePreference pref = createZenRulePreference(rule); mPreferenceCategory.addPreference(pref); } } -} - - + @VisibleForTesting + ZenRulePreference createZenRulePreference(Map.Entry<String, AutomaticZenRule> rule) { + return new ZenRulePreference(mPreferenceCategory.getContext(), + rule, mParent, mMetricsFeatureProvider); + } +} diff --git a/src/com/android/settings/notification/ZenModeAutomationPreferenceController.java b/src/com/android/settings/notification/ZenModeAutomationPreferenceController.java index faa0f8a196..4220a42c7f 100644 --- a/src/com/android/settings/notification/ZenModeAutomationPreferenceController.java +++ b/src/com/android/settings/notification/ZenModeAutomationPreferenceController.java @@ -17,6 +17,7 @@ package com.android.settings.notification; import android.content.Context; + import androidx.preference.Preference; import com.android.settings.core.PreferenceControllerMixin; diff --git a/src/com/android/settings/notification/ZenModeAutomationSettings.java b/src/com/android/settings/notification/ZenModeAutomationSettings.java index 1bb6fc678c..0f879071de 100644 --- a/src/com/android/settings/notification/ZenModeAutomationSettings.java +++ b/src/com/android/settings/notification/ZenModeAutomationSettings.java @@ -16,12 +16,21 @@ package com.android.settings.notification; -import android.app.Fragment; +import android.app.AlertDialog; +import android.app.AutomaticZenRule; +import android.app.NotificationManager; +import android.app.settings.SettingsEnums; import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; import android.provider.SearchIndexableResource; import android.service.notification.ConditionProviderService; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; + +import androidx.fragment.app.Fragment; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; @@ -29,18 +38,35 @@ import com.android.settings.utils.ManagedServiceSettings; import com.android.settings.utils.ZenServiceListing; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.List; +import java.util.Map; +@SearchIndexable public class ZenModeAutomationSettings extends ZenModeSettingsBase { + public static final String DELETE = "DELETE_RULE"; protected final ManagedServiceSettings.Config CONFIG = getConditionProviderConfig(); + private CharSequence[] mDeleteDialogRuleNames; + private String[] mDeleteDialogRuleIds; + private boolean[] mDeleteDialogChecked; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + Bundle bundle = getArguments(); + if (bundle != null && bundle.containsKey(DELETE)) { + mBackend.removeZenRule(bundle.getString(DELETE)); + bundle.remove(DELETE); + } + } @Override protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { ZenServiceListing serviceListing = new ZenServiceListing(getContext(), CONFIG); serviceListing.reloadApprovedServices(); - return buildPreferenceControllers(context, this, serviceListing, getLifecycle()); + return buildPreferenceControllers(context, this, serviceListing, getSettingsLifecycle()); } private static List<AbstractPreferenceController> buildPreferenceControllers(Context context, @@ -60,17 +86,64 @@ public class ZenModeAutomationSettings extends ZenModeSettingsBase { @Override public int getMetricsCategory() { - return MetricsEvent.NOTIFICATION_ZEN_MODE_AUTOMATION; + return SettingsEnums.NOTIFICATION_ZEN_MODE_AUTOMATION; } protected static ManagedServiceSettings.Config getConditionProviderConfig() { return new ManagedServiceSettings.Config.Builder() .setTag(TAG) .setIntentAction(ConditionProviderService.SERVICE_INTERFACE) + .setConfigurationIntentAction(NotificationManager.ACTION_AUTOMATIC_ZEN_RULE) .setPermission(android.Manifest.permission.BIND_CONDITION_PROVIDER_SERVICE) .setNoun("condition provider") .build(); } + private final int DELETE_RULES = 1; + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + menu.add(Menu.NONE, DELETE_RULES, Menu.NONE, R.string.zen_mode_delete_automatic_rules); + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case DELETE_RULES: + Map.Entry<String, AutomaticZenRule>[] rules = mBackend.getAutomaticZenRules(); + mDeleteDialogRuleNames = new CharSequence[rules.length]; + mDeleteDialogRuleIds = new String[rules.length]; + mDeleteDialogChecked = new boolean[rules.length]; + for (int i = 0; i < rules.length; i++) { + mDeleteDialogRuleNames[i] = rules[i].getValue().getName(); + mDeleteDialogRuleIds[i] = rules[i].getKey(); + } + new AlertDialog.Builder(mContext) + .setTitle(R.string.zen_mode_delete_automatic_rules) + .setMultiChoiceItems(mDeleteDialogRuleNames, null, + new DialogInterface.OnMultiChoiceClickListener() { + @Override + public void onClick(DialogInterface dialog, int which, + boolean isChecked) { + mDeleteDialogChecked[which] = isChecked; + } + }) + .setPositiveButton(R.string.zen_mode_schedule_delete, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + for (int i = 0; i < rules.length; i++) { + if (mDeleteDialogChecked[i]) { + mBackend.removeZenRule(mDeleteDialogRuleIds[i]); + } + } + } + }).show(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } /** * For Search. diff --git a/src/com/android/settings/notification/ZenModeBackend.java b/src/com/android/settings/notification/ZenModeBackend.java index 8c8f14df67..4c9cebe845 100644 --- a/src/com/android/settings/notification/ZenModeBackend.java +++ b/src/com/android/settings/notification/ZenModeBackend.java @@ -23,14 +23,25 @@ import android.app.ActivityManager; import android.app.AutomaticZenRule; import android.app.NotificationManager; import android.content.Context; +import android.database.Cursor; +import android.icu.text.ListFormatter; import android.net.Uri; +import android.provider.ContactsContract; import android.provider.Settings; import android.service.notification.ZenModeConfig; -import androidx.annotation.VisibleForTesting; +import android.service.notification.ZenPolicy; import android.util.Log; +import androidx.annotation.VisibleForTesting; + import com.android.settings.R; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Map; + public class ZenModeBackend { @VisibleForTesting protected static final String ZEN_MODE_FROM_ANYONE = "zen_mode_from_anyone"; @@ -41,6 +52,7 @@ public class ZenModeBackend { @VisibleForTesting protected static final String ZEN_MODE_FROM_NONE = "zen_mode_from_none"; protected static final int SOURCE_NONE = -1; + private static List<String> mDefaultRuleIds; private static ZenModeBackend sInstance; @@ -78,7 +90,7 @@ public class ZenModeBackend { Settings.Global.ZEN_MODE, mZenMode); } - protected boolean setZenRule(String id, AutomaticZenRule rule) { + protected boolean updateZenRule(String id, AutomaticZenRule rule) { return NotificationManager.from(mContext).updateAutomaticZenRule(id, rule); } @@ -109,7 +121,7 @@ public class ZenModeBackend { return (mPolicy.priorityCategories & categoryType) != 0; } - protected int getNewPriorityCategories(boolean allow, int categoryType) { + protected int getNewDefaultPriorityCategories(boolean allow, int categoryType) { int priorityCategories = mPolicy.priorityCategories; if (allow) { priorityCategories |= categoryType; @@ -128,15 +140,16 @@ public class ZenModeBackend { } protected int getPriorityMessageSenders() { - if (isPriorityCategoryEnabled(NotificationManager.Policy.PRIORITY_CATEGORY_MESSAGES)) { + if (isPriorityCategoryEnabled( + NotificationManager.Policy.PRIORITY_CATEGORY_MESSAGES)) { return mPolicy.priorityMessageSenders; } return SOURCE_NONE; } protected void saveVisualEffectsPolicy(int category, boolean suppress) { - Settings.Global.putInt(mContext.getContentResolver(), - Settings.Global.ZEN_SETTINGS_UPDATED, 1); + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.ZEN_SETTINGS_UPDATED, 1); int suppressedEffects = getNewSuppressedEffects(suppress, category); savePolicy(mPolicy.priorityCategories, mPolicy.priorityCallSenders, @@ -144,7 +157,7 @@ public class ZenModeBackend { } protected void saveSoundPolicy(int category, boolean allow) { - int priorityCategories = getNewPriorityCategories(allow, category); + int priorityCategories = getNewDefaultPriorityCategories(allow, category); savePolicy(priorityCategories, mPolicy.priorityCallSenders, mPolicy.priorityMessageSenders, mPolicy.suppressedVisualEffects); } @@ -156,6 +169,7 @@ public class ZenModeBackend { mNotificationManager.setNotificationPolicy(mPolicy); } + private int getNewSuppressedEffects(boolean suppress, int effectType) { int effects = mPolicy.suppressedVisualEffects; @@ -195,7 +209,7 @@ public class ZenModeBackend { priorityMessagesSenders = allowSendersFrom; } - savePolicy(getNewPriorityCategories(allowSenders, category), + savePolicy(getNewDefaultPriorityCategories(allowSenders, category), priorityCallSenders, priorityMessagesSenders, mPolicy.suppressedVisualEffects); if (ZenModeSettingsBase.DEBUG) Log.d(TAG, "onPrefChange allow" + @@ -229,6 +243,20 @@ public class ZenModeBackend { return categorySenders; } + protected static String getKeyFromZenPolicySetting(int contactType) { + switch (contactType) { + case ZenPolicy.PEOPLE_TYPE_ANYONE: + return ZEN_MODE_FROM_ANYONE; + case ZenPolicy.PEOPLE_TYPE_CONTACTS: + return ZEN_MODE_FROM_CONTACTS; + case ZenPolicy.PEOPLE_TYPE_STARRED: + return ZEN_MODE_FROM_STARRED; + case ZenPolicy.PEOPLE_TYPE_NONE: + default: + return ZEN_MODE_FROM_NONE; + } + } + protected static String getKeyFromSetting(int contactType) { switch (contactType) { case NotificationManager.Policy.PRIORITY_SENDERS_ANY: @@ -243,15 +271,17 @@ public class ZenModeBackend { } } - protected int getContactsSummary(int category) { - int contactType = -1; - - // SOURCE_NONE can be used when in total silence or alarms only - // (policy is based on user's preferences but the UI displayed is based on zenMode) - if (category == SOURCE_NONE) { - return R.string.zen_mode_from_none; + protected int getAlarmsTotalSilenceCallsMessagesSummary(int category) { + if (category == NotificationManager.Policy.PRIORITY_CATEGORY_MESSAGES) { + return R.string.zen_mode_from_none_messages; + } else if (category == NotificationManager.Policy.PRIORITY_CATEGORY_CALLS){ + return R.string.zen_mode_from_none_calls; } + return 0; + } + protected int getContactsSummary(int category) { + int contactType = -1; if (category == NotificationManager.Policy.PRIORITY_CATEGORY_MESSAGES) { if (isPriorityCategoryEnabled(category)) { contactType = getPriorityMessageSenders(); @@ -266,12 +296,60 @@ public class ZenModeBackend { case NotificationManager.Policy.PRIORITY_SENDERS_ANY: return R.string.zen_mode_from_anyone; case NotificationManager.Policy.PRIORITY_SENDERS_CONTACTS: - return R.string.zen_mode_from_contacts; + return R.string.zen_mode_from_contacts; case NotificationManager.Policy.PRIORITY_SENDERS_STARRED: - return R.string.zen_mode_from_starred; + return R.string.zen_mode_from_starred; case SOURCE_NONE: default: - return R.string.zen_mode_from_none; + if (category == NotificationManager.Policy.PRIORITY_CATEGORY_MESSAGES) { + return R.string.zen_mode_from_none_messages; + } else { + return R.string.zen_mode_from_none_calls; + } + } + } + + protected int getContactsCallsSummary(ZenPolicy policy) { + int peopleType = policy.getPriorityCallSenders(); + switch (peopleType) { + case ZenPolicy.PEOPLE_TYPE_ANYONE: + return R.string.zen_mode_from_anyone; + case ZenPolicy.PEOPLE_TYPE_CONTACTS: + return R.string.zen_mode_from_contacts; + case ZenPolicy.PEOPLE_TYPE_STARRED: + return R.string.zen_mode_from_starred; + case ZenPolicy.PEOPLE_TYPE_NONE: + default: + return R.string.zen_mode_from_none_calls; + } + } + + protected int getContactsMessagesSummary(ZenPolicy policy) { + int peopleType = policy.getPriorityMessageSenders(); + switch (peopleType) { + case ZenPolicy.PEOPLE_TYPE_ANYONE: + return R.string.zen_mode_from_anyone; + case ZenPolicy.PEOPLE_TYPE_CONTACTS: + return R.string.zen_mode_from_contacts; + case ZenPolicy.PEOPLE_TYPE_STARRED: + return R.string.zen_mode_from_starred; + case ZenPolicy.PEOPLE_TYPE_NONE: + default: + return R.string.zen_mode_from_none_messages; + } + } + + protected static int getZenPolicySettingFromPrefKey(String key) { + switch (key) { + case ZEN_MODE_FROM_ANYONE: + return ZenPolicy.PEOPLE_TYPE_ANYONE; + case ZEN_MODE_FROM_CONTACTS: + return ZenPolicy.PEOPLE_TYPE_CONTACTS; + case ZEN_MODE_FROM_STARRED: + return ZenPolicy.PEOPLE_TYPE_STARRED; + case ZEN_MODE_FROM_NONE: + default: + return ZenPolicy.PEOPLE_TYPE_NONE; } } @@ -293,13 +371,163 @@ public class ZenModeBackend { return NotificationManager.from(mContext).removeAutomaticZenRule(ruleId); } + public NotificationManager.Policy getConsolidatedPolicy() { + return NotificationManager.from(mContext).getConsolidatedNotificationPolicy(); + } + protected String addZenRule(AutomaticZenRule rule) { try { - String id = NotificationManager.from(mContext).addAutomaticZenRule(rule); - NotificationManager.from(mContext).getAutomaticZenRule(id); - return id; + return NotificationManager.from(mContext).addAutomaticZenRule(rule); } catch (Exception e) { return null; } } + + ZenPolicy setDefaultZenPolicy(ZenPolicy zenPolicy) { + int calls; + if (mPolicy.allowCalls()) { + calls = ZenModeConfig.getZenPolicySenders(mPolicy.allowCallsFrom()); + } else { + calls = ZenPolicy.PEOPLE_TYPE_NONE; + } + + int messages; + if (mPolicy.allowMessages()) { + messages = ZenModeConfig.getZenPolicySenders(mPolicy.allowMessagesFrom()); + } else { + messages = ZenPolicy.PEOPLE_TYPE_NONE; + } + + return new ZenPolicy.Builder(zenPolicy) + .allowAlarms(mPolicy.allowAlarms()) + .allowCalls(calls) + .allowEvents(mPolicy.allowEvents()) + .allowMedia(mPolicy.allowMedia()) + .allowMessages(messages) + .allowReminders(mPolicy.allowReminders()) + .allowRepeatCallers(mPolicy.allowRepeatCallers()) + .allowSystem(mPolicy.allowSystem()) + .showFullScreenIntent(mPolicy.showFullScreenIntents()) + .showLights(mPolicy.showLights()) + .showInAmbientDisplay(mPolicy.showAmbient()) + .showInNotificationList(mPolicy.showInNotificationList()) + .showBadges(mPolicy.showBadges()) + .showPeeking(mPolicy.showPeeking()) + .showStatusBarIcons(mPolicy.showStatusBarIcons()) + .build(); + } + + protected Map.Entry<String, AutomaticZenRule>[] getAutomaticZenRules() { + Map<String, AutomaticZenRule> ruleMap = + NotificationManager.from(mContext).getAutomaticZenRules(); + final Map.Entry<String, AutomaticZenRule>[] rt = ruleMap.entrySet().toArray( + new Map.Entry[ruleMap.size()]); + Arrays.sort(rt, RULE_COMPARATOR); + return rt; + } + + protected AutomaticZenRule getAutomaticZenRule(String id) { + return NotificationManager.from(mContext).getAutomaticZenRule(id); + } + + private static List<String> getDefaultRuleIds() { + if (mDefaultRuleIds == null) { + mDefaultRuleIds = ZenModeConfig.DEFAULT_RULE_IDS; + } + return mDefaultRuleIds; + } + + NotificationManager.Policy toNotificationPolicy(ZenPolicy policy) { + ZenModeConfig config = new ZenModeConfig(); + return config.toNotificationPolicy(policy); + } + + @VisibleForTesting + List<String> getStarredContacts(Cursor cursor) { + List<String> starredContacts = new ArrayList<>(); + if (cursor != null && cursor.moveToFirst()) { + do { + String contact = cursor.getString(0); + if (contact != null) { + starredContacts.add(contact); + } + } while (cursor.moveToNext()); + } + return starredContacts; + } + + private List<String> getStarredContacts() { + Cursor cursor = null; + try { + cursor = queryData(); + return getStarredContacts(cursor); + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + + public String getStarredContactsSummary() { + List<String> starredContacts = getStarredContacts(); + int numStarredContacts = starredContacts.size(); + + List<String> displayContacts = new ArrayList<>(); + + if (numStarredContacts == 0) { + displayContacts.add(mContext.getString(R.string.zen_mode_from_none)); + } else { + for (int i = 0; i < 2 && i < numStarredContacts; i++) { + displayContacts.add(starredContacts.get(i)); + } + + if (numStarredContacts == 3) { + displayContacts.add(starredContacts.get(2)); + } else if (numStarredContacts > 2) { + displayContacts.add(mContext.getResources().getQuantityString( + R.plurals.zen_mode_starred_contacts_summary_additional_contacts, + numStarredContacts - 2, numStarredContacts - 2)); + } + } + + // values in displayContacts must not be null + return ListFormatter.getInstance().format(displayContacts); + } + + private Cursor queryData() { + return mContext.getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, + new String[]{ContactsContract.Contacts.DISPLAY_NAME_PRIMARY}, + ContactsContract.Data.STARRED + "=1", null, + ContactsContract.Data.TIMES_CONTACTED); + } + + @VisibleForTesting + public static final Comparator<Map.Entry<String, AutomaticZenRule>> RULE_COMPARATOR = + new Comparator<Map.Entry<String, AutomaticZenRule>>() { + @Override + public int compare(Map.Entry<String, AutomaticZenRule> lhs, + Map.Entry<String, AutomaticZenRule> rhs) { + // if it's a default rule, should be at the top of automatic rules + boolean lhsIsDefaultRule = getDefaultRuleIds().contains(lhs.getKey()); + boolean rhsIsDefaultRule = getDefaultRuleIds().contains(rhs.getKey()); + if (lhsIsDefaultRule != rhsIsDefaultRule) { + return lhsIsDefaultRule ? -1 : 1; + } + + int byDate = Long.compare(lhs.getValue().getCreationTime(), + rhs.getValue().getCreationTime()); + if (byDate != 0) { + return byDate; + } else { + return key(lhs.getValue()).compareTo(key(rhs.getValue())); + } + } + + private String key(AutomaticZenRule rule) { + final int type = ZenModeConfig.isValidScheduleConditionId(rule.getConditionId()) + ? 1 : ZenModeConfig.isValidEventConditionId(rule.getConditionId()) + ? 2 : 3; + return type + rule.getName().toString(); + } + }; } diff --git a/src/com/android/settings/notification/ZenModeBehaviorCallsPreferenceController.java b/src/com/android/settings/notification/ZenModeBehaviorCallsPreferenceController.java deleted file mode 100644 index 4ab14379e9..0000000000 --- a/src/com/android/settings/notification/ZenModeBehaviorCallsPreferenceController.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (C) 2017 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.notification; - -import android.content.Context; -import androidx.preference.Preference; - -import com.android.settings.core.PreferenceControllerMixin; -import com.android.settingslib.core.lifecycle.Lifecycle; - -public class ZenModeBehaviorCallsPreferenceController extends - AbstractZenModePreferenceController implements PreferenceControllerMixin { - - protected static final String KEY_BEHAVIOR_SETTINGS = "zen_mode_calls_settings"; - private final ZenModeSettings.SummaryBuilder mSummaryBuilder; - - public ZenModeBehaviorCallsPreferenceController(Context context, Lifecycle lifecycle) { - super(context, KEY_BEHAVIOR_SETTINGS, lifecycle); - mSummaryBuilder = new ZenModeSettings.SummaryBuilder(context); - } - - @Override - public String getPreferenceKey() { - return KEY_BEHAVIOR_SETTINGS; - } - - @Override - public boolean isAvailable() { - return true; - } - - @Override - public void updateState(Preference preference) { - super.updateState(preference); - - preference.setSummary(mSummaryBuilder.getCallsSettingSummary(getPolicy())); - } -} diff --git a/src/com/android/settings/notification/ZenModeBehaviorFooterPreferenceController.java b/src/com/android/settings/notification/ZenModeBehaviorFooterPreferenceController.java index 6a0b657b23..e9f74d2880 100644 --- a/src/com/android/settings/notification/ZenModeBehaviorFooterPreferenceController.java +++ b/src/com/android/settings/notification/ZenModeBehaviorFooterPreferenceController.java @@ -21,6 +21,7 @@ import android.content.Context; import android.net.Uri; import android.provider.Settings; import android.service.notification.ZenModeConfig; + import androidx.preference.Preference; import com.android.settings.R; diff --git a/src/com/android/settings/notification/ZenModeBehaviorMsgEventReminderPreferenceController.java b/src/com/android/settings/notification/ZenModeBehaviorMsgEventReminderPreferenceController.java deleted file mode 100644 index d11baee8e8..0000000000 --- a/src/com/android/settings/notification/ZenModeBehaviorMsgEventReminderPreferenceController.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2017 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.notification; - -import android.content.Context; -import androidx.preference.Preference; - -import com.android.settings.core.PreferenceControllerMixin; -import com.android.settingslib.core.lifecycle.Lifecycle; - -public class ZenModeBehaviorMsgEventReminderPreferenceController extends - AbstractZenModePreferenceController implements PreferenceControllerMixin { - - protected static final String KEY_BEHAVIOR_SETTINGS = "zen_mode_msg_event_reminder_settings"; - private final ZenModeSettings.SummaryBuilder mSummaryBuilder; - - public ZenModeBehaviorMsgEventReminderPreferenceController(Context context, - Lifecycle lifecycle) { - super(context, KEY_BEHAVIOR_SETTINGS, lifecycle); - mSummaryBuilder = new ZenModeSettings.SummaryBuilder(context); - } - - @Override - public String getPreferenceKey() { - return KEY_BEHAVIOR_SETTINGS; - } - - @Override - public boolean isAvailable() { - return true; - } - - @Override - public void updateState(Preference preference) { - super.updateState(preference); - - preference.setSummary(mSummaryBuilder.getMsgEventReminderSettingSummary(getPolicy())); - } -} diff --git a/src/com/android/settings/notification/ZenModeBlockedEffectsPreferenceController.java b/src/com/android/settings/notification/ZenModeBlockedEffectsPreferenceController.java index b017cac315..08c4ca7975 100644 --- a/src/com/android/settings/notification/ZenModeBlockedEffectsPreferenceController.java +++ b/src/com/android/settings/notification/ZenModeBlockedEffectsPreferenceController.java @@ -17,7 +17,6 @@ package com.android.settings.notification; import android.content.Context; -import androidx.preference.Preference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.core.lifecycle.Lifecycle; diff --git a/src/com/android/settings/notification/ZenModeBlockedEffectsSettings.java b/src/com/android/settings/notification/ZenModeBlockedEffectsSettings.java index aa2b55ccbf..613bd355c0 100644 --- a/src/com/android/settings/notification/ZenModeBlockedEffectsSettings.java +++ b/src/com/android/settings/notification/ZenModeBlockedEffectsSettings.java @@ -24,21 +24,22 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_NOTIFICAT import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_STATUS_BAR; +import android.app.settings.SettingsEnums; import android.content.Context; import android.os.Bundle; import android.provider.SearchIndexableResource; -import androidx.preference.CheckBoxPreference; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.List; +@SearchIndexable public class ZenModeBlockedEffectsSettings extends ZenModeSettingsBase implements Indexable { @Override @@ -50,7 +51,7 @@ public class ZenModeBlockedEffectsSettings extends ZenModeSettingsBase implement @Override protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { - return buildPreferenceControllers(context, getLifecycle()); + return buildPreferenceControllers(context, getSettingsLifecycle()); } private static List<AbstractPreferenceController> buildPreferenceControllers(Context context, @@ -58,26 +59,26 @@ public class ZenModeBlockedEffectsSettings extends ZenModeSettingsBase implement List<AbstractPreferenceController> controllers = new ArrayList<>(); controllers.add(new ZenModeVisEffectPreferenceController(context, lifecycle, "zen_effect_intent", SUPPRESSED_EFFECT_FULL_SCREEN_INTENT, - MetricsEvent.ACTION_ZEN_BLOCK_FULL_SCREEN_INTENTS, null)); + SettingsEnums.ACTION_ZEN_BLOCK_FULL_SCREEN_INTENTS, null)); controllers.add(new ZenModeVisEffectPreferenceController(context, lifecycle, "zen_effect_light", SUPPRESSED_EFFECT_LIGHTS, - MetricsEvent.ACTION_ZEN_BLOCK_LIGHT, null)); + SettingsEnums.ACTION_ZEN_BLOCK_LIGHT, null)); controllers.add(new ZenModeVisEffectPreferenceController(context, lifecycle, "zen_effect_peek", SUPPRESSED_EFFECT_PEEK, - MetricsEvent.ACTION_ZEN_BLOCK_PEEK, null)); + SettingsEnums.ACTION_ZEN_BLOCK_PEEK, null)); controllers.add(new ZenModeVisEffectPreferenceController(context, lifecycle, "zen_effect_status", SUPPRESSED_EFFECT_STATUS_BAR, - MetricsEvent.ACTION_ZEN_BLOCK_STATUS, + SettingsEnums.ACTION_ZEN_BLOCK_STATUS, new int[] {SUPPRESSED_EFFECT_NOTIFICATION_LIST})); controllers.add(new ZenModeVisEffectPreferenceController(context, lifecycle, "zen_effect_badge", SUPPRESSED_EFFECT_BADGE, - MetricsEvent.ACTION_ZEN_BLOCK_BADGE, null)); + SettingsEnums.ACTION_ZEN_BLOCK_BADGE, null)); controllers.add(new ZenModeVisEffectPreferenceController(context, lifecycle, "zen_effect_ambient", SUPPRESSED_EFFECT_AMBIENT, - MetricsEvent.ACTION_ZEN_BLOCK_AMBIENT, null)); + SettingsEnums.ACTION_ZEN_BLOCK_AMBIENT, null)); controllers.add(new ZenModeVisEffectPreferenceController(context, lifecycle, "zen_effect_list", SUPPRESSED_EFFECT_NOTIFICATION_LIST, - MetricsEvent.ACTION_ZEN_BLOCK_NOTIFICATION_LIST, null)); + SettingsEnums.ACTION_ZEN_BLOCK_NOTIFICATION_LIST, null)); return controllers; } @@ -88,7 +89,7 @@ public class ZenModeBlockedEffectsSettings extends ZenModeSettingsBase implement @Override public int getMetricsCategory() { - return MetricsEvent.ZEN_WHAT_TO_BLOCK; + return SettingsEnums.ZEN_WHAT_TO_BLOCK; } /** @@ -107,12 +108,6 @@ public class ZenModeBlockedEffectsSettings extends ZenModeSettingsBase implement return result; } - @Override - public List<String> getNonIndexableKeys(Context context) { - final List<String> keys = super.getNonIndexableKeys(context); - return keys; - } - @Override public List<AbstractPreferenceController> createPreferenceControllers(Context context) { return buildPreferenceControllers(context, null); diff --git a/src/com/android/settings/notification/ZenModeButtonPreferenceController.java b/src/com/android/settings/notification/ZenModeButtonPreferenceController.java index ac9497dbe3..3a9bcb7b17 100644 --- a/src/com/android/settings/notification/ZenModeButtonPreferenceController.java +++ b/src/com/android/settings/notification/ZenModeButtonPreferenceController.java @@ -16,27 +16,29 @@ package com.android.settings.notification; -import android.app.FragmentManager; +import android.app.settings.SettingsEnums; import android.content.Context; import android.provider.Settings; -import androidx.preference.Preference; import android.view.View; import android.widget.Button; -import com.android.internal.logging.nano.MetricsProto; +import androidx.fragment.app.FragmentManager; +import androidx.preference.Preference; + import com.android.settings.R; -import com.android.settings.applications.LayoutPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.widget.LayoutPreference; public class ZenModeButtonPreferenceController extends AbstractZenModePreferenceController implements PreferenceControllerMixin { + public static final String KEY = "zen_mode_toggle"; + private static final String TAG = "EnableZenModeButton"; - protected static final String KEY = "zen_mode_settings_button_container"; + private final FragmentManager mFragment; private Button mZenButtonOn; private Button mZenButtonOff; - private FragmentManager mFragment; public ZenModeButtonPreferenceController(Context context, Lifecycle lifecycle, FragmentManager fragment) { @@ -59,17 +61,17 @@ public class ZenModeButtonPreferenceController extends AbstractZenModePreference super.updateState(preference); if (null == mZenButtonOn) { - mZenButtonOn = (Button) ((LayoutPreference) preference) + mZenButtonOn = ((LayoutPreference) preference) .findViewById(R.id.zen_mode_settings_turn_on_button); updateZenButtonOnClickListener(); } if (null == mZenButtonOff) { - mZenButtonOff = (Button) ((LayoutPreference) preference) + mZenButtonOff = ((LayoutPreference) preference) .findViewById(R.id.zen_mode_settings_turn_off_button); mZenButtonOff.setOnClickListener(v -> { mMetricsFeatureProvider.action(mContext, - MetricsProto.MetricsEvent.ACTION_ZEN_TOGGLE_DND_BUTTON, false); + SettingsEnums.ACTION_ZEN_TOGGLE_DND_BUTTON, false); mBackend.setZenMode(Settings.Global.ZEN_MODE_OFF); }); } @@ -96,24 +98,24 @@ public class ZenModeButtonPreferenceController extends AbstractZenModePreference private void updateZenButtonOnClickListener() { int zenDuration = getZenDuration(); switch (zenDuration) { - case Settings.Global.ZEN_DURATION_PROMPT: + case Settings.Secure.ZEN_DURATION_PROMPT: mZenButtonOn.setOnClickListener(v -> { mMetricsFeatureProvider.action(mContext, - MetricsProto.MetricsEvent.ACTION_ZEN_TOGGLE_DND_BUTTON, false); + SettingsEnums.ACTION_ZEN_TOGGLE_DND_BUTTON, false); new SettingsEnableZenModeDialog().show(mFragment, TAG); }); break; - case Settings.Global.ZEN_DURATION_FOREVER: + case Settings.Secure.ZEN_DURATION_FOREVER: mZenButtonOn.setOnClickListener(v -> { mMetricsFeatureProvider.action(mContext, - MetricsProto.MetricsEvent.ACTION_ZEN_TOGGLE_DND_BUTTON, false); + SettingsEnums.ACTION_ZEN_TOGGLE_DND_BUTTON, false); mBackend.setZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS); }); break; default: mZenButtonOn.setOnClickListener(v -> { mMetricsFeatureProvider.action(mContext, - MetricsProto.MetricsEvent.ACTION_ZEN_TOGGLE_DND_BUTTON, false); + SettingsEnums.ACTION_ZEN_TOGGLE_DND_BUTTON, false); mBackend.setZenModeForDuration(zenDuration); }); } diff --git a/src/com/android/settings/notification/ZenModeBypassingAppsPreferenceController.java b/src/com/android/settings/notification/ZenModeBypassingAppsPreferenceController.java new file mode 100644 index 0000000000..33e03d954d --- /dev/null +++ b/src/com/android/settings/notification/ZenModeBypassingAppsPreferenceController.java @@ -0,0 +1,30 @@ +package com.android.settings.notification; + +import android.content.Context; +import android.os.UserHandle; + +import com.android.settings.R; +import com.android.settingslib.core.lifecycle.Lifecycle; + +public class ZenModeBypassingAppsPreferenceController extends AbstractZenModePreferenceController { + + protected static final String KEY = "zen_mode_bypassing_apps"; + private NotificationBackend mNotificationBackend = new NotificationBackend(); + + public ZenModeBypassingAppsPreferenceController(Context context, Lifecycle lifecycle) { + super(context, KEY, lifecycle); + } + + @Override + public boolean isAvailable() { + return mNotificationBackend.getNumAppsBypassingDnd(UserHandle.getCallingUserId()) != 0; + } + + @Override + public String getSummary() { + final int channelsBypassing = + mNotificationBackend.getNumAppsBypassingDnd(UserHandle.getCallingUserId()); + return mContext.getResources().getQuantityString(R.plurals.zen_mode_bypassing_apps_subtext, + channelsBypassing, channelsBypassing); + } +} diff --git a/src/com/android/settings/notification/ZenModeBypassingAppsSettings.java b/src/com/android/settings/notification/ZenModeBypassingAppsSettings.java new file mode 100644 index 0000000000..455de427ce --- /dev/null +++ b/src/com/android/settings/notification/ZenModeBypassingAppsSettings.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2018 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.notification; + +import android.app.Activity; +import android.app.Application; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.provider.SearchIndexableResource; + +import androidx.fragment.app.Fragment; + +import com.android.settings.R; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; +import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.search.SearchIndexable; + +import java.util.ArrayList; +import java.util.List; + +@SearchIndexable +public class ZenModeBypassingAppsSettings extends ZenModeSettingsBase implements + Indexable { + private final String TAG = "ZenBypassingApps"; + + @Override + protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { + final Activity activity = getActivity(); + final Application app; + if (activity != null) { + app = activity.getApplication(); + } else { + app = null; + } + return buildPreferenceControllers(context, app, this); + } + + private static List<AbstractPreferenceController> buildPreferenceControllers(Context context, + Application app, Fragment host) { + final List<AbstractPreferenceController> controllers = new ArrayList<>(); + controllers.add(new ZenModeAllBypassingAppsPreferenceController(context, app, host)); + return controllers; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.zen_mode_bypassing_apps; + } + + @Override + protected String getLogTag() { + return TAG; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.NOTIFICATION_ZEN_MODE_OVERRIDING_APPS; + } + + /** + * For Search. + */ + public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, + boolean enabled) { + final ArrayList<SearchIndexableResource> result = new ArrayList<>(); + + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.zen_mode_bypassing_apps; + result.add(sir); + return result; + } + + @Override + public List<AbstractPreferenceController> createPreferenceControllers( + Context context) { + return buildPreferenceControllers(context, null, null); + } + }; +} diff --git a/src/com/android/settings/notification/ZenModeCallsPreferenceController.java b/src/com/android/settings/notification/ZenModeCallsPreferenceController.java index 1b6c122be4..d931f6c6fe 100644 --- a/src/com/android/settings/notification/ZenModeCallsPreferenceController.java +++ b/src/com/android/settings/notification/ZenModeCallsPreferenceController.java @@ -19,32 +19,28 @@ package com.android.settings.notification; import android.app.NotificationManager; import android.content.Context; import android.provider.Settings; -import androidx.preference.ListPreference; + import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; -import android.text.TextUtils; -import com.android.internal.annotations.VisibleForTesting; -import com.android.settings.R; +import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.core.lifecycle.Lifecycle; -public class ZenModeCallsPreferenceController extends AbstractZenModePreferenceController implements - Preference.OnPreferenceChangeListener { +public class ZenModeCallsPreferenceController extends + AbstractZenModePreferenceController implements PreferenceControllerMixin { - protected static final String KEY = "zen_mode_calls"; - private final ZenModeBackend mBackend; - private ListPreference mPreference; - private final String[] mListValues; + private final String KEY_BEHAVIOR_SETTINGS; + private final ZenModeSettings.SummaryBuilder mSummaryBuilder; - public ZenModeCallsPreferenceController(Context context, Lifecycle lifecycle) { - super(context, KEY, lifecycle); - mBackend = ZenModeBackend.getInstance(context); - mListValues = context.getResources().getStringArray(R.array.zen_mode_contacts_values); + public ZenModeCallsPreferenceController(Context context, Lifecycle lifecycle, + String key) { + super(context, key, lifecycle); + KEY_BEHAVIOR_SETTINGS = key; + mSummaryBuilder = new ZenModeSettings.SummaryBuilder(context); } @Override public String getPreferenceKey() { - return KEY; + return KEY_BEHAVIOR_SETTINGS; } @Override @@ -53,54 +49,19 @@ public class ZenModeCallsPreferenceController extends AbstractZenModePreferenceC } @Override - public void displayPreference(PreferenceScreen screen) { - super.displayPreference(screen); - mPreference = (ListPreference) screen.findPreference(KEY); - } - - @Override public void updateState(Preference preference) { super.updateState(preference); - updateFromContactsValue(preference); - } - - @Override - public boolean onPreferenceChange(Preference preference, Object selectedContactsFrom) { - mBackend.saveSenders(NotificationManager.Policy.PRIORITY_CATEGORY_CALLS, - ZenModeBackend.getSettingFromPrefKey(selectedContactsFrom.toString())); - updateFromContactsValue(preference); - return true; - } - private void updateFromContactsValue(Preference preference) { - mPreference = (ListPreference) preference; switch (getZenMode()) { case Settings.Global.ZEN_MODE_NO_INTERRUPTIONS: case Settings.Global.ZEN_MODE_ALARMS: - mPreference.setEnabled(false); - mPreference.setValue(ZenModeBackend.ZEN_MODE_FROM_NONE); - mPreference.setSummary(mBackend.getContactsSummary(ZenModeBackend.SOURCE_NONE)); + preference.setEnabled(false); + preference.setSummary(mBackend.getAlarmsTotalSilenceCallsMessagesSummary( + NotificationManager.Policy.PRIORITY_CATEGORY_CALLS)); break; default: preference.setEnabled(true); - preference.setSummary(mBackend.getContactsSummary( - NotificationManager.Policy.PRIORITY_CATEGORY_CALLS)); - - final String currentVal = ZenModeBackend.getKeyFromSetting( - mBackend.getPriorityCallSenders()); - mPreference.setValue(mListValues[getIndexOfSendersValue(currentVal)]); + preference.setSummary(mSummaryBuilder.getCallsSettingSummary(getPolicy())); } } - - @VisibleForTesting - protected int getIndexOfSendersValue(String currentVal) { - int index = 3; // defaults to "none" based on R.array.zen_mode_contacts_values - for (int i = 0; i < mListValues.length; i++) { - if (TextUtils.equals(currentVal, mListValues[i])) { - return i; - } - } - - return index; - } } diff --git a/src/com/android/settings/notification/ZenModeCallsSettings.java b/src/com/android/settings/notification/ZenModeCallsSettings.java index cd56d7b5e2..a1d0ec7e40 100644 --- a/src/com/android/settings/notification/ZenModeCallsSettings.java +++ b/src/com/android/settings/notification/ZenModeCallsSettings.java @@ -18,32 +18,34 @@ package com.android.settings.notification; import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_CALLS; +import android.app.settings.SettingsEnums; import android.content.Context; import android.provider.SearchIndexableResource; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.List; +@SearchIndexable public class ZenModeCallsSettings extends ZenModeSettingsBase implements Indexable { @Override protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { - return buildPreferenceControllers(context, getLifecycle()); + return buildPreferenceControllers(context, getSettingsLifecycle()); } private static List<AbstractPreferenceController> buildPreferenceControllers(Context context, Lifecycle lifecycle) { List<AbstractPreferenceController> controllers = new ArrayList<>(); - controllers.add(new ZenModeCallsPreferenceController(context, lifecycle)); + controllers.add(new ZenModePriorityCallsPreferenceController(context, lifecycle)); controllers.add(new ZenModeStarredContactsPreferenceController(context, lifecycle, - PRIORITY_CATEGORY_CALLS)); + PRIORITY_CATEGORY_CALLS, "zen_mode_starred_contacts_callers")); controllers.add(new ZenModeRepeatCallersPreferenceController(context, lifecycle, context.getResources().getInteger(com.android.internal.R.integer .config_zen_repeat_callers_threshold))); @@ -59,7 +61,7 @@ public class ZenModeCallsSettings extends ZenModeSettingsBase implements Indexab @Override public int getMetricsCategory() { - return MetricsEvent.NOTIFICATION_ZEN_MODE_PRIORITY; + return SettingsEnums.NOTIFICATION_ZEN_MODE_PRIORITY; } /** @@ -80,14 +82,9 @@ public class ZenModeCallsSettings extends ZenModeSettingsBase implements Indexab } @Override - public List<String> getNonIndexableKeys(Context context) { - final List<String> keys = super.getNonIndexableKeys(context); - return keys; + public List<AbstractPreferenceController> createPreferenceControllers( + Context context) { + return buildPreferenceControllers(context, null); } - - @Override - public List<AbstractPreferenceController> createPreferenceControllers(Context context) { - return buildPreferenceControllers(context, null); - } - }; + }; } diff --git a/src/com/android/settings/notification/ZenModeDurationPreferenceController.java b/src/com/android/settings/notification/ZenModeDurationPreferenceController.java index 5e58444efb..3972bb106d 100644 --- a/src/com/android/settings/notification/ZenModeDurationPreferenceController.java +++ b/src/com/android/settings/notification/ZenModeDurationPreferenceController.java @@ -16,26 +16,19 @@ package com.android.settings.notification; -import android.app.FragmentManager; import android.content.Context; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.core.lifecycle.Lifecycle; public class ZenModeDurationPreferenceController extends AbstractZenModePreferenceController - implements PreferenceControllerMixin, Preference.OnPreferenceClickListener { + implements PreferenceControllerMixin { - private static final String TAG = "ZenModeDurationDialog"; protected static final String KEY = "zen_mode_duration_settings"; - private FragmentManager mFragment; - public ZenModeDurationPreferenceController(Context context, Lifecycle lifecycle, FragmentManager - fragment) { + public ZenModeDurationPreferenceController(Context context, Lifecycle lifecycle) { super(context, KEY, lifecycle); - mFragment = fragment; } @Override @@ -49,16 +42,8 @@ public class ZenModeDurationPreferenceController extends AbstractZenModePreferen } @Override - public void displayPreference(PreferenceScreen screen) { - super.displayPreference(screen); - screen.findPreference(KEY).setOnPreferenceClickListener(this); - } - - @Override - public void updateState(Preference preference) { - super.updateState(preference); - - String summary = ""; + public CharSequence getSummary() { + String summary; int zenDuration = getZenDuration(); if (zenDuration < 0) { summary = mContext.getString(R.string.zen_mode_duration_summary_always_prompt); @@ -75,12 +60,6 @@ public class ZenModeDurationPreferenceController extends AbstractZenModePreferen } } - preference.setSummary(summary); - } - - @Override - public boolean onPreferenceClick(Preference preference) { - new SettingsZenDurationDialog().show(mFragment, TAG); - return true; + return summary; } -}
\ No newline at end of file +} diff --git a/src/com/android/settings/notification/ZenModeEventRuleSettings.java b/src/com/android/settings/notification/ZenModeEventRuleSettings.java index 51ea51780d..4a1a34290b 100644 --- a/src/com/android/settings/notification/ZenModeEventRuleSettings.java +++ b/src/com/android/settings/notification/ZenModeEventRuleSettings.java @@ -17,6 +17,7 @@ package com.android.settings.notification; import android.app.AutomaticZenRule; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.pm.PackageManager.NameNotFoundException; import android.database.Cursor; @@ -26,12 +27,13 @@ import android.provider.CalendarContract.Calendars; import android.provider.Settings; import android.service.notification.ZenModeConfig; import android.service.notification.ZenModeConfig.EventInfo; + import androidx.preference.DropDownPreference; import androidx.preference.Preference; import androidx.preference.Preference.OnPreferenceChangeListener; import androidx.preference.PreferenceScreen; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.internal.annotations.VisibleForTesting; import com.android.settings.R; import com.android.settingslib.core.AbstractPreferenceController; @@ -39,6 +41,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.Objects; public class ZenModeEventRuleSettings extends ZenModeRuleSettingsBase { private static final String KEY_CALENDAR = "calendar"; @@ -50,7 +53,7 @@ public class ZenModeEventRuleSettings extends ZenModeRuleSettingsBase { private DropDownPreference mReply; private EventInfo mEvent; - private List<CalendarInfo> mCalendars; + private boolean mCreate; @Override @@ -81,34 +84,42 @@ public class ZenModeEventRuleSettings extends ZenModeRuleSettingsBase { protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { List<AbstractPreferenceController> controllers = new ArrayList<>(); mHeader = new ZenAutomaticRuleHeaderPreferenceController(context, this, - getLifecycle()); - mSwitch = new ZenAutomaticRuleSwitchPreferenceController(context, this, getLifecycle()); + getSettingsLifecycle()); + mActionButtons = new ZenRuleButtonsPreferenceController(context, this, + getSettingsLifecycle()); + mSwitch = new ZenAutomaticRuleSwitchPreferenceController(context, this, + getSettingsLifecycle()); controllers.add(mHeader); + controllers.add(mActionButtons); controllers.add(mSwitch); return controllers; } private void reloadCalendar() { - mCalendars = getCalendars(mContext); + List<CalendarInfo> calendars = getCalendars(mContext); ArrayList<CharSequence> entries = new ArrayList<>(); ArrayList<CharSequence> values = new ArrayList<>(); entries.add(getString(R.string.zen_mode_event_rule_calendar_any)); - values.add(key(0, null)); - final String eventCalendar = mEvent != null ? mEvent.calendar : null; - boolean found = false; - for (CalendarInfo calendar : mCalendars) { + values.add(key(0, null, "")); + final String eventCalendar = mEvent != null ? mEvent.calName : null; + for (CalendarInfo calendar : calendars) { entries.add(calendar.name); values.add(key(calendar)); - if (eventCalendar != null && eventCalendar.equals(calendar.name)) { - found = true; + if (eventCalendar != null && (mEvent.calendarId == null + && eventCalendar.equals(calendar.name))) { + mEvent.calendarId = calendar.calendarId; } } - if (eventCalendar != null && !found) { - entries.add(eventCalendar); - values.add(key(mEvent.userId, eventCalendar)); + + CharSequence[] entriesArr = entries.toArray(new CharSequence[entries.size()]); + CharSequence[] valuesArr = values.toArray(new CharSequence[values.size()]); + if (!Objects.equals(mCalendar.getEntries(), entriesArr)) { + mCalendar.setEntries(entriesArr); + } + + if (!Objects.equals(mCalendar.getEntryValues(), valuesArr)) { + mCalendar.setEntryValues(valuesArr); } - mCalendar.setEntries(entries.toArray(new CharSequence[entries.size()])); - mCalendar.setEntryValues(values.toArray(new CharSequence[values.size()])); } @Override @@ -122,12 +133,10 @@ public class ZenModeEventRuleSettings extends ZenModeRuleSettingsBase { public boolean onPreferenceChange(Preference preference, Object newValue) { final String calendarKey = (String) newValue; if (calendarKey.equals(key(mEvent))) return false; - final int i = calendarKey.indexOf(':'); - mEvent.userId = Integer.parseInt(calendarKey.substring(0, i)); - mEvent.calendar = calendarKey.substring(i + 1); - if (mEvent.calendar.isEmpty()) { - mEvent.calendar = null; - } + String[] key = calendarKey.split(":", 3); + mEvent.userId = Integer.parseInt(key[0]); + mEvent.calendarId = key[1].equals("") ? null : Long.parseLong(key[1]); + mEvent.calName = key[2].equals("") ? null : key[2]; updateRule(ZenModeConfig.toEventConditionId(mEvent)); return true; } @@ -161,27 +170,20 @@ public class ZenModeEventRuleSettings extends ZenModeRuleSettingsBase { @Override protected void updateControlsInternal() { - mCalendar.setValue(key(mEvent)); - mReply.setValue(Integer.toString(mEvent.reply)); + if (!Objects.equals(mCalendar.getValue(), key(mEvent))) { + mCalendar.setValue(key(mEvent)); + } + if (!Objects.equals(mReply.getValue(), Integer.toString(mEvent.reply))) { + mReply.setValue(Integer.toString(mEvent.reply)); + } } @Override public int getMetricsCategory() { - return MetricsEvent.NOTIFICATION_ZEN_MODE_EVENT_RULE; - } - - public static CalendarInfo findCalendar(Context context, EventInfo event) { - if (context == null || event == null) return null; - final String eventKey = key(event); - for (CalendarInfo calendar : getCalendars(context)) { - if (eventKey.equals(key(calendar))) { - return calendar; - } - } - return null; + return SettingsEnums.NOTIFICATION_ZEN_MODE_EVENT_RULE; } - private static List<CalendarInfo> getCalendars(Context context) { + private List<CalendarInfo> getCalendars(Context context) { final List<CalendarInfo> calendars = new ArrayList<>(); for (UserHandle user : UserManager.get(context).getUserProfiles()) { final Context userContext = getContextForUser(context, user); @@ -201,11 +203,11 @@ public class ZenModeEventRuleSettings extends ZenModeRuleSettingsBase { } } - public static void addCalendars(Context context, List<CalendarInfo> outCalendars) { - final String primary = "\"primary\""; - final String[] projection = { Calendars._ID, Calendars.CALENDAR_DISPLAY_NAME, - "(" + Calendars.ACCOUNT_NAME + "=" + Calendars.OWNER_ACCOUNT + ") AS " + primary }; - final String selection = primary + " = 1"; + private void addCalendars(Context context, List<CalendarInfo> outCalendars) { + final String[] projection = { Calendars._ID, Calendars.CALENDAR_DISPLAY_NAME }; + final String selection = Calendars.CALENDAR_ACCESS_LEVEL + " >= " + + Calendars.CAL_ACCESS_CONTRIBUTOR + + " AND " + Calendars.SYNC_EVENTS + " = 1"; Cursor cursor = null; try { cursor = context.getContentResolver().query(Calendars.CONTENT_URI, projection, @@ -214,10 +216,8 @@ public class ZenModeEventRuleSettings extends ZenModeRuleSettingsBase { return; } while (cursor.moveToNext()) { - final CalendarInfo ci = new CalendarInfo(); - ci.name = cursor.getString(1); - ci.userId = context.getUserId(); - outCalendars.add(ci); + addCalendar(cursor.getLong(0), cursor.getString(1), + context.getUserId(), outCalendars); } } finally { if (cursor != null) { @@ -226,16 +226,29 @@ public class ZenModeEventRuleSettings extends ZenModeRuleSettingsBase { } } + @VisibleForTesting + void addCalendar(long calendarId, String calName, int userId, List<CalendarInfo> + outCalendars) { + final CalendarInfo ci = new CalendarInfo(); + ci.calendarId = calendarId; + ci.name = calName; + ci.userId = userId; + if (!outCalendars.contains(ci)) { + outCalendars.add(ci); + } + } + private static String key(CalendarInfo calendar) { - return key(calendar.userId, calendar.name); + return key(calendar.userId, calendar.calendarId, calendar.name); } private static String key(EventInfo event) { - return key(event.userId, event.calendar); + return key(event.userId, event.calendarId, event.calName); } - private static String key(int userId, String calendar) { - return EventInfo.resolveUserId(userId) + ":" + (calendar == null ? "" : calendar); + private static String key(int userId, Long calendarId, String displayName) { + return EventInfo.resolveUserId(userId) + ":" + (calendarId == null ? "" : calendarId) + + ":" + (displayName == null ? "" : displayName); } private static final Comparator<CalendarInfo> CALENDAR_NAME = new Comparator<CalendarInfo>() { @@ -248,5 +261,20 @@ public class ZenModeEventRuleSettings extends ZenModeRuleSettingsBase { public static class CalendarInfo { public String name; public int userId; + public Long calendarId; + + @Override + public boolean equals(Object o) { + if (!(o instanceof CalendarInfo)) return false; + if (o == this) return true; + final CalendarInfo other = (CalendarInfo) o; + return Objects.equals(other.name, name) + && Objects.equals(other.calendarId, calendarId); + } + + @Override + public int hashCode() { + return Objects.hash(name, calendarId); + } } } diff --git a/src/com/android/settings/notification/ZenModeEventsPreferenceController.java b/src/com/android/settings/notification/ZenModeEventsPreferenceController.java index 8ca6cf63df..9a45a90347 100644 --- a/src/com/android/settings/notification/ZenModeEventsPreferenceController.java +++ b/src/com/android/settings/notification/ZenModeEventsPreferenceController.java @@ -17,14 +17,14 @@ package com.android.settings.notification; import android.app.NotificationManager.Policy; +import android.app.settings.SettingsEnums; import android.content.Context; import android.provider.Settings; -import androidx.preference.SwitchPreference; -import androidx.preference.Preference; import android.util.Log; +import androidx.preference.Preference; +import androidx.preference.SwitchPreference; -import com.android.internal.logging.nano.MetricsProto; import com.android.settingslib.core.lifecycle.Lifecycle; public class ZenModeEventsPreferenceController extends AbstractZenModePreferenceController @@ -70,7 +70,7 @@ public class ZenModeEventsPreferenceController extends AbstractZenModePreference if (ZenModeSettingsBase.DEBUG) { Log.d(TAG, "onPrefChange allowEvents=" + allowEvents); } - mMetricsFeatureProvider.action(mContext, MetricsProto.MetricsEvent.ACTION_ZEN_ALLOW_EVENTS, + mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_ZEN_ALLOW_EVENTS, allowEvents); mBackend.saveSoundPolicy(Policy.PRIORITY_CATEGORY_EVENTS, allowEvents); return true; diff --git a/src/com/android/settings/notification/ZenModeMediaPreferenceController.java b/src/com/android/settings/notification/ZenModeMediaPreferenceController.java index d8099bed21..2bec84f9c7 100644 --- a/src/com/android/settings/notification/ZenModeMediaPreferenceController.java +++ b/src/com/android/settings/notification/ZenModeMediaPreferenceController.java @@ -19,10 +19,11 @@ package com.android.settings.notification; import android.app.NotificationManager.Policy; import android.content.Context; import android.provider.Settings; -import androidx.preference.SwitchPreference; -import androidx.preference.Preference; import android.util.Log; +import androidx.preference.Preference; +import androidx.preference.SwitchPreference; + import com.android.settingslib.core.lifecycle.Lifecycle; public class ZenModeMediaPreferenceController extends AbstractZenModePreferenceController diff --git a/src/com/android/settings/notification/ZenModeMessagesPreferenceController.java b/src/com/android/settings/notification/ZenModeMessagesPreferenceController.java index 5939abbce5..d51be27a20 100644 --- a/src/com/android/settings/notification/ZenModeMessagesPreferenceController.java +++ b/src/com/android/settings/notification/ZenModeMessagesPreferenceController.java @@ -1,30 +1,40 @@ +/* + * Copyright (C) 2018 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.notification; import android.app.NotificationManager; import android.content.Context; import android.provider.Settings; -import androidx.preference.ListPreference; + import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; -import android.text.TextUtils; -import com.android.internal.annotations.VisibleForTesting; -import com.android.settings.R; +import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.core.lifecycle.Lifecycle; -public class ZenModeMessagesPreferenceController extends AbstractZenModePreferenceController - implements Preference.OnPreferenceChangeListener { - - protected static final String KEY = "zen_mode_messages"; +public class ZenModeMessagesPreferenceController extends + AbstractZenModePreferenceController implements PreferenceControllerMixin { - private final ZenModeBackend mBackend; - private ListPreference mPreference; - private final String[] mListValues; + private final String KEY; + private final ZenModeSettings.SummaryBuilder mSummaryBuilder; - public ZenModeMessagesPreferenceController(Context context, Lifecycle lifecycle) { - super(context, KEY, lifecycle); - mBackend = ZenModeBackend.getInstance(context); - mListValues = context.getResources().getStringArray(R.array.zen_mode_contacts_values); + public ZenModeMessagesPreferenceController(Context context, Lifecycle lifecycle, String key) { + super(context, key, lifecycle); + KEY = key; + mSummaryBuilder = new ZenModeSettings.SummaryBuilder(context); } @Override @@ -38,54 +48,19 @@ public class ZenModeMessagesPreferenceController extends AbstractZenModePreferen } @Override - public void displayPreference(PreferenceScreen screen) { - super.displayPreference(screen); - mPreference = (ListPreference) screen.findPreference(KEY); - } - - @Override public void updateState(Preference preference) { super.updateState(preference); - updateFromContactsValue(preference); - } - - @Override - public boolean onPreferenceChange(Preference preference, Object selectedContactsFrom) { - mBackend.saveSenders(NotificationManager.Policy.PRIORITY_CATEGORY_MESSAGES, - ZenModeBackend.getSettingFromPrefKey(selectedContactsFrom.toString())); - updateFromContactsValue(preference); - return true; - } - private void updateFromContactsValue(Preference preference) { - mPreference = (ListPreference) preference; switch (getZenMode()) { case Settings.Global.ZEN_MODE_NO_INTERRUPTIONS: case Settings.Global.ZEN_MODE_ALARMS: - mPreference.setEnabled(false); - mPreference.setValue(ZenModeBackend.ZEN_MODE_FROM_NONE); - mPreference.setSummary(mBackend.getContactsSummary(ZenModeBackend.SOURCE_NONE)); + preference.setEnabled(false); + preference.setSummary(mBackend.getAlarmsTotalSilenceCallsMessagesSummary( + NotificationManager.Policy.PRIORITY_CATEGORY_MESSAGES)); break; default: preference.setEnabled(true); - preference.setSummary(mBackend.getContactsSummary( - NotificationManager.Policy.PRIORITY_CATEGORY_MESSAGES)); - - final String currentVal = ZenModeBackend.getKeyFromSetting( - mBackend.getPriorityMessageSenders()); - mPreference.setValue(mListValues[getIndexOfSendersValue(currentVal)]); + preference.setSummary(mSummaryBuilder.getMessagesSettingSummary(getPolicy())); } } - - @VisibleForTesting - protected int getIndexOfSendersValue(String currentVal) { - int index = 3; // defaults to "none" based on R.array.zen_mode_contacts_values - for (int i = 0; i < mListValues.length; i++) { - if (TextUtils.equals(currentVal, mListValues[i])) { - return i; - } - } - - return index; - } } diff --git a/src/com/android/settings/notification/ZenModeMsgEventReminderSettings.java b/src/com/android/settings/notification/ZenModeMessagesSettings.java index 95b9f7e30d..a03d088f30 100644 --- a/src/com/android/settings/notification/ZenModeMsgEventReminderSettings.java +++ b/src/com/android/settings/notification/ZenModeMessagesSettings.java @@ -18,53 +18,53 @@ package com.android.settings.notification; import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_MESSAGES; +import android.app.settings.SettingsEnums; import android.content.Context; import android.provider.SearchIndexableResource; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.List; -public class ZenModeMsgEventReminderSettings extends ZenModeSettingsBase implements Indexable { +@SearchIndexable +public class ZenModeMessagesSettings extends ZenModeSettingsBase implements Indexable { @Override protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { - return buildPreferenceControllers(context, getLifecycle()); + return buildPreferenceControllers(context, getSettingsLifecycle()); } private static List<AbstractPreferenceController> buildPreferenceControllers(Context context, Lifecycle lifecycle) { List<AbstractPreferenceController> controllers = new ArrayList<>(); - controllers.add(new ZenModeEventsPreferenceController(context, lifecycle)); - controllers.add(new ZenModeRemindersPreferenceController(context, lifecycle)); - controllers.add(new ZenModeMessagesPreferenceController(context, lifecycle)); + controllers.add(new ZenModePriorityMessagesPreferenceController(context, lifecycle)); controllers.add(new ZenModeStarredContactsPreferenceController(context, lifecycle, - PRIORITY_CATEGORY_MESSAGES)); - controllers.add(new ZenModeBehaviorFooterPreferenceController(context, lifecycle, - R.string.zen_msg_event_reminder_footer)); + PRIORITY_CATEGORY_MESSAGES, "zen_mode_starred_contacts_messages")); + controllers.add(new ZenModeBehaviorFooterPreferenceController( + context, lifecycle, R.string.zen_mode_messages_footer)); return controllers; } @Override protected int getPreferenceScreenResId() { - return R.xml.zen_mode_msg_event_reminder_settings; + return R.xml.zen_mode_messages_settings; } @Override public int getMetricsCategory() { - return MetricsEvent.NOTIFICATION_ZEN_MODE_PRIORITY; + return SettingsEnums.NOTIFICATION_ZEN_MODE_PRIORITY; } /** * For Search. */ - public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = new BaseSearchIndexProvider() { @Override @@ -73,20 +73,15 @@ public class ZenModeMsgEventReminderSettings extends ZenModeSettingsBase impleme final ArrayList<SearchIndexableResource> result = new ArrayList<>(); final SearchIndexableResource sir = new SearchIndexableResource(context); - sir.xmlResId = R.xml.zen_mode_msg_event_reminder_settings; + sir.xmlResId = R.xml.zen_mode_messages_settings; result.add(sir); return result; } @Override - public List<String> getNonIndexableKeys(Context context) { - final List<String> keys = super.getNonIndexableKeys(context); - return keys; + public List<AbstractPreferenceController> createPreferenceControllers( + Context context) { + return buildPreferenceControllers(context, null); } - - @Override - public List<AbstractPreferenceController> createPreferenceControllers(Context context) { - return buildPreferenceControllers(context, null); - } - }; + }; } diff --git a/src/com/android/settings/notification/ZenModePreferenceController.java b/src/com/android/settings/notification/ZenModePreferenceController.java index 0d94029f47..44ad2ffb6d 100644 --- a/src/com/android/settings/notification/ZenModePreferenceController.java +++ b/src/com/android/settings/notification/ZenModePreferenceController.java @@ -23,38 +23,30 @@ import android.net.Uri; import android.os.Handler; import android.os.UserHandle; import android.provider.Settings; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.util.Slog; -import com.android.settings.core.PreferenceControllerMixin; -import com.android.settingslib.core.AbstractPreferenceController; -import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settings.core.BasePreferenceController; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnPause; import com.android.settingslib.core.lifecycle.events.OnResume; -public class ZenModePreferenceController extends AbstractPreferenceController - implements LifecycleObserver, OnResume, OnPause, PreferenceControllerMixin { +public class ZenModePreferenceController extends BasePreferenceController + implements LifecycleObserver, OnResume, OnPause { - private final String mKey; private SettingObserver mSettingObserver; private ZenModeSettings.SummaryBuilder mSummaryBuilder; - public ZenModePreferenceController(Context context, Lifecycle lifecycle, String key) { - super(context); + public ZenModePreferenceController(Context context, String key) { + super(context, key); mSummaryBuilder = new ZenModeSettings.SummaryBuilder(context); - - if (lifecycle != null) { - lifecycle.addObserver(this); - } - mKey = key; } @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - mSettingObserver = new SettingObserver(screen.findPreference(mKey)); + mSettingObserver = new SettingObserver(screen.findPreference(getPreferenceKey())); } @Override @@ -72,13 +64,8 @@ public class ZenModePreferenceController extends AbstractPreferenceController } @Override - public String getPreferenceKey() { - return mKey; - } - - @Override - public boolean isAvailable() { - return true; + public int getAvailabilityStatus() { + return AVAILABLE_UNSEARCHABLE; } @Override diff --git a/src/com/android/settings/notification/ZenModePriorityCallsPreferenceController.java b/src/com/android/settings/notification/ZenModePriorityCallsPreferenceController.java new file mode 100644 index 0000000000..cec97f5c5d --- /dev/null +++ b/src/com/android/settings/notification/ZenModePriorityCallsPreferenceController.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2018 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.notification; + +import android.app.NotificationManager; +import android.content.Context; +import android.provider.Settings; +import android.text.TextUtils; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.ListPreference; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settingslib.core.lifecycle.Lifecycle; + +public class ZenModePriorityCallsPreferenceController extends AbstractZenModePreferenceController + implements Preference.OnPreferenceChangeListener { + + protected static final String KEY = "zen_mode_calls"; + private final ZenModeBackend mBackend; + private ListPreference mPreference; + private final String[] mListValues; + + public ZenModePriorityCallsPreferenceController(Context context, Lifecycle lifecycle) { + super(context, KEY, lifecycle); + mBackend = ZenModeBackend.getInstance(context); + mListValues = context.getResources().getStringArray(R.array.zen_mode_contacts_values); + } + + @Override + public String getPreferenceKey() { + return KEY; + } + + @Override + public boolean isAvailable() { + return true; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(KEY); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + updateFromContactsValue(preference); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object selectedContactsFrom) { + mBackend.saveSenders(NotificationManager.Policy.PRIORITY_CATEGORY_CALLS, + ZenModeBackend.getSettingFromPrefKey(selectedContactsFrom.toString())); + updateFromContactsValue(preference); + return true; + } + + private void updateFromContactsValue(Preference preference) { + mPreference = (ListPreference) preference; + switch (getZenMode()) { + case Settings.Global.ZEN_MODE_NO_INTERRUPTIONS: + case Settings.Global.ZEN_MODE_ALARMS: + mPreference.setEnabled(false); + mPreference.setValue(ZenModeBackend.ZEN_MODE_FROM_NONE); + mPreference.setSummary(mBackend.getAlarmsTotalSilenceCallsMessagesSummary( + NotificationManager.Policy.PRIORITY_CATEGORY_CALLS)); + break; + default: + preference.setEnabled(true); + preference.setSummary(mBackend.getContactsSummary( + NotificationManager.Policy.PRIORITY_CATEGORY_CALLS)); + + final String currentVal = ZenModeBackend.getKeyFromSetting( + mBackend.getPriorityCallSenders()); + mPreference.setValue(mListValues[getIndexOfSendersValue(currentVal)]); + } + } + + @VisibleForTesting + protected int getIndexOfSendersValue(String currentVal) { + int index = 3; // defaults to "none" based on R.array.zen_mode_contacts_values + for (int i = 0; i < mListValues.length; i++) { + if (TextUtils.equals(currentVal, mListValues[i])) { + return i; + } + } + + return index; + } +} diff --git a/src/com/android/settings/notification/ZenModePriorityMessagesPreferenceController.java b/src/com/android/settings/notification/ZenModePriorityMessagesPreferenceController.java new file mode 100644 index 0000000000..752edf2f69 --- /dev/null +++ b/src/com/android/settings/notification/ZenModePriorityMessagesPreferenceController.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2018 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.notification; + +import android.app.NotificationManager; +import android.content.Context; +import android.provider.Settings; +import android.text.TextUtils; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.ListPreference; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settingslib.core.lifecycle.Lifecycle; + +public class ZenModePriorityMessagesPreferenceController extends AbstractZenModePreferenceController + implements Preference.OnPreferenceChangeListener { + + protected static final String KEY = "zen_mode_messages"; + private final ZenModeBackend mBackend; + private ListPreference mPreference; + private final String[] mListValues; + + public ZenModePriorityMessagesPreferenceController(Context context, Lifecycle lifecycle) { + super(context, KEY, lifecycle); + mBackend = ZenModeBackend.getInstance(context); + mListValues = context.getResources().getStringArray(R.array.zen_mode_contacts_values); + } + + @Override + public String getPreferenceKey() { + return KEY; + } + + @Override + public boolean isAvailable() { + return true; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(KEY); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + updateFromContactsValue(preference); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object selectedContactsFrom) { + mBackend.saveSenders(NotificationManager.Policy.PRIORITY_CATEGORY_MESSAGES, + ZenModeBackend.getSettingFromPrefKey(selectedContactsFrom.toString())); + updateFromContactsValue(preference); + return true; + } + + private void updateFromContactsValue(Preference preference) { + mPreference = (ListPreference) preference; + switch (getZenMode()) { + case Settings.Global.ZEN_MODE_NO_INTERRUPTIONS: + case Settings.Global.ZEN_MODE_ALARMS: + mPreference.setEnabled(false); + mPreference.setValue(ZenModeBackend.ZEN_MODE_FROM_NONE); + mPreference.setSummary(mBackend.getAlarmsTotalSilenceCallsMessagesSummary( + NotificationManager.Policy.PRIORITY_CATEGORY_MESSAGES)); + break; + default: + preference.setEnabled(true); + preference.setSummary(mBackend.getContactsSummary( + NotificationManager.Policy.PRIORITY_CATEGORY_MESSAGES)); + + final String currentVal = ZenModeBackend.getKeyFromSetting( + mBackend.getPriorityMessageSenders()); + mPreference.setValue(mListValues[getIndexOfSendersValue(currentVal)]); + } + } + + @VisibleForTesting + protected int getIndexOfSendersValue(String currentVal) { + int index = 3; // defaults to "none" based on R.array.zen_mode_contacts_values + for (int i = 0; i < mListValues.length; i++) { + if (TextUtils.equals(currentVal, mListValues[i])) { + return i; + } + } + + return index; + } +} diff --git a/src/com/android/settings/notification/ZenModeRemindersPreferenceController.java b/src/com/android/settings/notification/ZenModeRemindersPreferenceController.java index c0f319afaf..8e64be3d4d 100644 --- a/src/com/android/settings/notification/ZenModeRemindersPreferenceController.java +++ b/src/com/android/settings/notification/ZenModeRemindersPreferenceController.java @@ -17,13 +17,14 @@ package com.android.settings.notification; import android.app.NotificationManager; +import android.app.settings.SettingsEnums; import android.content.Context; import android.provider.Settings; -import androidx.preference.SwitchPreference; -import androidx.preference.Preference; import android.util.Log; -import com.android.internal.logging.nano.MetricsProto; +import androidx.preference.Preference; +import androidx.preference.SwitchPreference; + import com.android.settingslib.core.lifecycle.Lifecycle; public class ZenModeRemindersPreferenceController extends AbstractZenModePreferenceController @@ -70,7 +71,7 @@ public class ZenModeRemindersPreferenceController extends AbstractZenModePrefere Log.d(TAG, "onPrefChange allowReminders=" + allowReminders); } mMetricsFeatureProvider.action(mContext, - MetricsProto.MetricsEvent.ACTION_ZEN_ALLOW_REMINDERS, allowReminders); + SettingsEnums.ACTION_ZEN_ALLOW_REMINDERS, allowReminders); mBackend.saveSoundPolicy(NotificationManager.Policy.PRIORITY_CATEGORY_REMINDERS, allowReminders); return true; diff --git a/src/com/android/settings/notification/ZenModeRepeatCallersPreferenceController.java b/src/com/android/settings/notification/ZenModeRepeatCallersPreferenceController.java index e3f5c2f7ac..d658d5fbb3 100644 --- a/src/com/android/settings/notification/ZenModeRepeatCallersPreferenceController.java +++ b/src/com/android/settings/notification/ZenModeRepeatCallersPreferenceController.java @@ -17,14 +17,15 @@ package com.android.settings.notification; import android.app.NotificationManager.Policy; +import android.app.settings.SettingsEnums; import android.content.Context; import android.provider.Settings; -import androidx.preference.SwitchPreference; +import android.util.Log; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.util.Log; +import androidx.preference.SwitchPreference; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settingslib.core.lifecycle.Lifecycle; @@ -94,7 +95,7 @@ public class ZenModeRepeatCallersPreferenceController extends AbstractZenModePre Log.d(TAG, "onPrefChange allowRepeatCallers=" + allowRepeatCallers); } mMetricsFeatureProvider.action(mContext, - MetricsProto.MetricsEvent.ACTION_ZEN_ALLOW_REPEAT_CALLS, allowRepeatCallers); + SettingsEnums.ACTION_ZEN_ALLOW_REPEAT_CALLS, allowRepeatCallers); mBackend.saveSoundPolicy(Policy.PRIORITY_CATEGORY_REPEAT_CALLERS, allowRepeatCallers); return true; } diff --git a/src/com/android/settings/notification/ZenModeRestrictNotificationsSettings.java b/src/com/android/settings/notification/ZenModeRestrictNotificationsSettings.java index 8d0cd0eb5c..90d97f0461 100644 --- a/src/com/android/settings/notification/ZenModeRestrictNotificationsSettings.java +++ b/src/com/android/settings/notification/ZenModeRestrictNotificationsSettings.java @@ -16,21 +16,23 @@ package com.android.settings.notification; +import android.app.settings.SettingsEnums; import android.content.Context; import android.os.Bundle; import android.provider.SearchIndexableResource; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.search.SearchIndexable; import com.android.settingslib.widget.FooterPreference; import java.util.ArrayList; import java.util.List; +@SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC) public class ZenModeRestrictNotificationsSettings extends ZenModeSettingsBase implements Indexable { @Override @@ -40,7 +42,7 @@ public class ZenModeRestrictNotificationsSettings extends ZenModeSettingsBase im @Override protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { - return buildPreferenceControllers(context, getLifecycle()); + return buildPreferenceControllers(context, getSettingsLifecycle()); } @Override @@ -69,7 +71,7 @@ public class ZenModeRestrictNotificationsSettings extends ZenModeSettingsBase im @Override public int getMetricsCategory() { - return MetricsEvent.SETTINGS_ZEN_NOTIFICATIONS; + return SettingsEnums.SETTINGS_ZEN_NOTIFICATIONS; } /** @@ -88,12 +90,6 @@ public class ZenModeRestrictNotificationsSettings extends ZenModeSettingsBase im return result; } - @Override - public List<String> getNonIndexableKeys(Context context) { - final List<String> keys = super.getNonIndexableKeys(context); - return keys; - } - @Override public List<AbstractPreferenceController> createPreferenceControllers(Context context) { return buildPreferenceControllers(context, null); diff --git a/src/com/android/settings/notification/ZenModeRuleSettingsBase.java b/src/com/android/settings/notification/ZenModeRuleSettingsBase.java index 576da16729..46162a8f17 100644 --- a/src/com/android/settings/notification/ZenModeRuleSettingsBase.java +++ b/src/com/android/settings/notification/ZenModeRuleSettingsBase.java @@ -23,26 +23,31 @@ import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.service.notification.ConditionProviderService; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; import android.util.Log; import android.widget.Toast; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + import com.android.settings.R; -import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settings.core.SubSettingLauncher; public abstract class ZenModeRuleSettingsBase extends ZenModeSettingsBase { protected static final String TAG = ZenModeSettingsBase.TAG; protected static final boolean DEBUG = ZenModeSettingsBase.DEBUG; + private final String CUSTOM_BEHAVIOR_KEY = "zen_custom_setting"; + protected Context mContext; protected boolean mDisableListeners; protected AutomaticZenRule mRule; protected String mId; protected ZenAutomaticRuleHeaderPreferenceController mHeader; + protected ZenRuleButtonsPreferenceController mActionButtons; protected ZenAutomaticRuleSwitchPreferenceController mSwitch; + protected Preference mCustomBehaviorPreference; abstract protected void onCreateInternal(); abstract protected boolean setRule(AutomaticZenRule rule); @@ -73,6 +78,21 @@ public abstract class ZenModeRuleSettingsBase extends ZenModeSettingsBase { } super.onCreate(icicle); + mCustomBehaviorPreference = getPreferenceScreen().findPreference(CUSTOM_BEHAVIOR_KEY); + mCustomBehaviorPreference.setOnPreferenceClickListener( + new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + Bundle bundle = new Bundle(); + bundle.putString(ZenCustomRuleSettings.RULE_ID, mId); + new SubSettingLauncher(mContext) + .setDestination(ZenCustomRuleSettings.class.getName()) + .setArguments(bundle) + .setSourceMetricsCategory(0) // TODO + .launch(); + return true; + } + }); onCreateInternal(); } @@ -82,7 +102,9 @@ public abstract class ZenModeRuleSettingsBase extends ZenModeSettingsBase { if (isUiRestricted()) { return; } - updateControls(); + if (!refreshRuleOrFinish()) { + updateControls(); + } } @Override @@ -103,27 +125,15 @@ public abstract class ZenModeRuleSettingsBase extends ZenModeSettingsBase { mHeader.onResume(mRule, mId); mHeader.displayPreference(screen); updatePreference(mHeader); - } - - private void updatePreference(AbstractPreferenceController controller) { - final PreferenceScreen screen = getPreferenceScreen(); - if (!controller.isAvailable()) { - return; - } - final String key = controller.getPreferenceKey(); - final Preference preference = screen.findPreference(key); - if (preference == null) { - Log.d(TAG, String.format("Cannot find preference with key %s in Controller %s", - key, controller.getClass().getSimpleName())); - return; - } - controller.updateState(preference); + mActionButtons.onResume(mRule, mId); + mActionButtons.displayPreference(screen); + updatePreference(mActionButtons); } protected void updateRule(Uri newConditionId) { mRule.setConditionId(newConditionId); - mBackend.setZenRule(mId, mRule); + mBackend.updateZenRule(mId, mRule); } @Override @@ -146,7 +156,8 @@ public abstract class ZenModeRuleSettingsBase extends ZenModeSettingsBase { private void toastAndFinish() { Toast.makeText(mContext, R.string.zen_mode_rule_not_found_text, Toast.LENGTH_SHORT) - .show(); + .show(); + getActivity().finish(); } @@ -158,6 +169,11 @@ public abstract class ZenModeRuleSettingsBase extends ZenModeSettingsBase { mDisableListeners = true; updateControlsInternal(); updateHeader(); + if (mRule.getZenPolicy() == null) { + mCustomBehaviorPreference.setSummary(R.string.zen_mode_custom_behavior_summary_default); + } else { + mCustomBehaviorPreference.setSummary(R.string.zen_mode_custom_behavior_summary); + } mDisableListeners = false; } } diff --git a/src/com/android/settings/notification/ZenModeScheduleRuleSettings.java b/src/com/android/settings/notification/ZenModeScheduleRuleSettings.java index 50ecd8bd09..e879ece78b 100644 --- a/src/com/android/settings/notification/ZenModeScheduleRuleSettings.java +++ b/src/com/android/settings/notification/ZenModeScheduleRuleSettings.java @@ -16,11 +16,10 @@ package com.android.settings.notification; -import android.app.AlertDialog; import android.app.AutomaticZenRule; import android.app.Dialog; -import android.app.FragmentManager; import android.app.TimePickerDialog; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnDismissListener; @@ -28,15 +27,17 @@ import android.os.Bundle; import android.provider.Settings; import android.service.notification.ZenModeConfig; import android.service.notification.ZenModeConfig.ScheduleInfo; -import androidx.preference.SwitchPreference; -import androidx.preference.Preference; -import androidx.preference.Preference.OnPreferenceClickListener; -import androidx.preference.PreferenceScreen; import android.text.format.DateFormat; import android.util.Log; import android.widget.TimePicker; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.FragmentManager; +import androidx.preference.Preference; +import androidx.preference.Preference.OnPreferenceClickListener; +import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreference; + import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; import com.android.settingslib.core.AbstractPreferenceController; @@ -62,7 +63,7 @@ public class ZenModeScheduleRuleSettings extends ZenModeRuleSettingsBase { private TimePickerPreference mStart; private TimePickerPreference mEnd; private SwitchPreference mExitAtAlarm; - + private AlertDialog mDayDialog; private ScheduleInfo mSchedule; @Override @@ -184,7 +185,6 @@ public class ZenModeScheduleRuleSettings extends ZenModeRuleSettingsBase { final int summaryFormat = nextDay ? R.string.zen_mode_end_time_next_day_summary_format : 0; mEnd.setSummaryFormat(summaryFormat); } - @Override protected void updateControlsInternal() { updateDays(); @@ -194,36 +194,47 @@ public class ZenModeScheduleRuleSettings extends ZenModeRuleSettingsBase { updateEndSummary(); } - @Override protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { List<AbstractPreferenceController> controllers = new ArrayList<>(); mHeader = new ZenAutomaticRuleHeaderPreferenceController(context, this, - getLifecycle()); - mSwitch = new ZenAutomaticRuleSwitchPreferenceController(context, this, getLifecycle()); - + getSettingsLifecycle()); + mActionButtons = new ZenRuleButtonsPreferenceController(context, this, + getSettingsLifecycle()); + mSwitch = new ZenAutomaticRuleSwitchPreferenceController(context, this, + getSettingsLifecycle()); controllers.add(mHeader); + controllers.add(mActionButtons); controllers.add(mSwitch); return controllers; } @Override public int getMetricsCategory() { - return MetricsEvent.NOTIFICATION_ZEN_MODE_SCHEDULE_RULE; + return SettingsEnums.NOTIFICATION_ZEN_MODE_SCHEDULE_RULE; + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (mDayDialog != null && mDayDialog.isShowing()) { + mDayDialog.dismiss(); + mDayDialog = null; + } } private void showDaysDialog() { - new AlertDialog.Builder(mContext) + mDayDialog = new AlertDialog.Builder(mContext) .setTitle(R.string.zen_mode_schedule_rule_days) .setView(new ZenModeScheduleDaysSelection(mContext, mSchedule.days) { - @Override - protected void onChanged(final int[] days) { - if (mDisableListeners) return; - if (Arrays.equals(days, mSchedule.days)) return; - if (DEBUG) Log.d(TAG, "days.onChanged days=" + Arrays.asList(days)); - mSchedule.days = days; - updateRule(ZenModeConfig.toScheduleConditionId(mSchedule)); - } + @Override + protected void onChanged(final int[] days) { + if (mDisableListeners) return; + if (Arrays.equals(days, mSchedule.days)) return; + if (DEBUG) Log.d(TAG, "days.onChanged days=" + Arrays.asList(days)); + mSchedule.days = days; + updateRule(ZenModeConfig.toScheduleConditionId(mSchedule)); + } }) .setOnDismissListener(new OnDismissListener() { @Override @@ -247,7 +258,7 @@ public class ZenModeScheduleRuleSettings extends ZenModeRuleSettingsBase { super(context); mContext = context; setPersistent(false); - setOnPreferenceClickListener(new OnPreferenceClickListener(){ + setOnPreferenceClickListener(new OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { final TimePickerFragment frag = new TimePickerFragment(); @@ -291,7 +302,7 @@ public class ZenModeScheduleRuleSettings extends ZenModeRuleSettingsBase { @Override public int getMetricsCategory() { - return MetricsEvent.DIALOG_ZEN_TIMEPICKER; + return SettingsEnums.DIALOG_ZEN_TIMEPICKER; } @Override diff --git a/src/com/android/settings/notification/ZenModeSettings.java b/src/com/android/settings/notification/ZenModeSettings.java index 232502ec22..f35c649e0b 100644 --- a/src/com/android/settings/notification/ZenModeSettings.java +++ b/src/com/android/settings/notification/ZenModeSettings.java @@ -26,22 +26,23 @@ import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_REPEAT_CA import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_SYSTEM; import android.app.AutomaticZenRule; -import android.app.FragmentManager; import android.app.NotificationManager; import android.app.NotificationManager.Policy; +import android.app.settings.SettingsEnums; import android.content.Context; -import android.icu.text.ListFormatter; import android.provider.SearchIndexableResource; import android.provider.Settings; import android.service.notification.ZenModeConfig; + import androidx.annotation.VisibleForTesting; +import androidx.fragment.app.FragmentManager; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.Arrays; @@ -50,6 +51,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.function.Predicate; +@SearchIndexable public class ZenModeSettings extends ZenModeSettingsBase { @Override public void onResume() { @@ -63,12 +65,12 @@ public class ZenModeSettings extends ZenModeSettingsBase { @Override public int getMetricsCategory() { - return MetricsEvent.NOTIFICATION_ZEN_MODE; + return SettingsEnums.NOTIFICATION_ZEN_MODE; } @Override protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { - return buildPreferenceControllers(context, getLifecycle(), getFragmentManager()); + return buildPreferenceControllers(context, getSettingsLifecycle(), getFragmentManager()); } @Override @@ -79,15 +81,16 @@ public class ZenModeSettings extends ZenModeSettingsBase { private static List<AbstractPreferenceController> buildPreferenceControllers(Context context, Lifecycle lifecycle, FragmentManager fragmentManager) { List<AbstractPreferenceController> controllers = new ArrayList<>(); - controllers.add(new ZenModeBehaviorMsgEventReminderPreferenceController(context, lifecycle)); - controllers.add(new ZenModeBehaviorSoundPreferenceController(context, lifecycle)); - controllers.add(new ZenModeBehaviorCallsPreferenceController(context, lifecycle)); + controllers.add(new ZenModeCallsPreferenceController(context, lifecycle, + "zen_mode_behavior_calls")); + controllers.add(new ZenModeMessagesPreferenceController(context, lifecycle, + "zen_mode_behavior_messages")); controllers.add(new ZenModeBlockedEffectsPreferenceController(context, lifecycle)); - controllers.add(new ZenModeDurationPreferenceController(context, lifecycle, - fragmentManager)); + controllers.add(new ZenModeDurationPreferenceController(context, lifecycle)); controllers.add(new ZenModeAutomationPreferenceController(context)); controllers.add(new ZenModeButtonPreferenceController(context, lifecycle, fragmentManager)); - controllers.add(new ZenModeSettingsFooterPreferenceController(context, lifecycle)); + controllers.add(new ZenModeSettingsFooterPreferenceController(context, lifecycle, + fragmentManager)); return controllers; } @@ -115,22 +118,22 @@ public class ZenModeSettings extends ZenModeSettingsBase { List<String> enabledCategories = getEnabledCategories(policy, category -> PRIORITY_CATEGORY_ALARMS == category || PRIORITY_CATEGORY_MEDIA == category - || PRIORITY_CATEGORY_SYSTEM == category); + || PRIORITY_CATEGORY_SYSTEM == category, false); int numCategories = enabledCategories.size(); if (numCategories == 0) { return mContext.getString(R.string.zen_sound_all_muted); } else if (numCategories == 1) { return mContext.getString(R.string.zen_sound_one_allowed, - enabledCategories.get(0).toLowerCase()); + enabledCategories.get(0)); } else if (numCategories == 2) { return mContext.getString(R.string.zen_sound_two_allowed, - enabledCategories.get(0).toLowerCase(), - enabledCategories.get(1).toLowerCase()); + enabledCategories.get(0), + enabledCategories.get(1)); } else if (numCategories == 3) { return mContext.getString(R.string.zen_sound_three_allowed, - enabledCategories.get(0).toLowerCase(), - enabledCategories.get(1).toLowerCase(), - enabledCategories.get(2).toLowerCase()); + enabledCategories.get(0), + enabledCategories.get(1), + enabledCategories.get(2)); } else { return mContext.getString(R.string.zen_sound_none_muted); } @@ -139,48 +142,28 @@ public class ZenModeSettings extends ZenModeSettingsBase { String getCallsSettingSummary(Policy policy) { List<String> enabledCategories = getEnabledCategories(policy, category -> PRIORITY_CATEGORY_CALLS == category - || PRIORITY_CATEGORY_REPEAT_CALLERS == category); + || PRIORITY_CATEGORY_REPEAT_CALLERS == category, false); int numCategories = enabledCategories.size(); if (numCategories == 0) { - return mContext.getString(R.string.zen_mode_no_exceptions); + return mContext.getString(R.string.zen_mode_from_none_calls); } else if (numCategories == 1) { return mContext.getString(R.string.zen_mode_calls_summary_one, - enabledCategories.get(0).toLowerCase()); + enabledCategories.get(0)); } else { return mContext.getString(R.string.zen_mode_calls_summary_two, - enabledCategories.get(0).toLowerCase(), - enabledCategories.get(1).toLowerCase()); + enabledCategories.get(0), + enabledCategories.get(1)); } } - String getMsgEventReminderSettingSummary(Policy policy) { + String getMessagesSettingSummary(Policy policy) { List<String> enabledCategories = getEnabledCategories(policy, - category -> PRIORITY_CATEGORY_EVENTS == category - || PRIORITY_CATEGORY_REMINDERS == category - || PRIORITY_CATEGORY_MESSAGES == category); + category -> PRIORITY_CATEGORY_MESSAGES == category, false); int numCategories = enabledCategories.size(); if (numCategories == 0) { - return mContext.getString(R.string.zen_mode_no_exceptions); - } else if (numCategories == 1) { - return enabledCategories.get(0); - } else if (numCategories == 2) { - return mContext.getString(R.string.join_two_items, enabledCategories.get(0), - enabledCategories.get(1).toLowerCase()); - } else if (numCategories == 3){ - final List<String> summaries = new ArrayList<>(); - summaries.add(enabledCategories.get(0)); - summaries.add(enabledCategories.get(1).toLowerCase()); - summaries.add(enabledCategories.get(2).toLowerCase()); - - return ListFormatter.getInstance().format(summaries); + return mContext.getString(R.string.zen_mode_from_none_messages); } else { - final List<String> summaries = new ArrayList<>(); - summaries.add(enabledCategories.get(0)); - summaries.add(enabledCategories.get(1).toLowerCase()); - summaries.add(enabledCategories.get(2).toLowerCase()); - summaries.add(mContext.getString(R.string.zen_mode_other_options)); - - return ListFormatter.getInstance().format(summaries); + return enabledCategories.get(0); } } @@ -226,15 +209,15 @@ public class ZenModeSettings extends ZenModeSettingsBase { String getAutomaticRulesSummary() { final int count = getEnabledAutomaticRulesCount(); return count == 0 ? mContext.getString(R.string.zen_mode_settings_summary_off) - : mContext.getResources().getQuantityString( - R.plurals.zen_mode_settings_summary_on, count, count); + : mContext.getResources().getQuantityString( + R.plurals.zen_mode_settings_summary_on, count, count); } @VisibleForTesting int getEnabledAutomaticRulesCount() { int count = 0; final Map<String, AutomaticZenRule> ruleMap = - NotificationManager.from(mContext).getAutomaticZenRules(); + NotificationManager.from(mContext).getAutomaticZenRules(); if (ruleMap != null) { for (Entry<String, AutomaticZenRule> ruleEntry : ruleMap.entrySet()) { final AutomaticZenRule rule = ruleEntry.getValue(); @@ -247,48 +230,18 @@ public class ZenModeSettings extends ZenModeSettingsBase { } private List<String> getEnabledCategories(Policy policy, - Predicate<Integer> filteredCategories) { + Predicate<Integer> filteredCategories, boolean capitalizeFirstInList) { List<String> enabledCategories = new ArrayList<>(); for (int category : ALL_PRIORITY_CATEGORIES) { + boolean isFirst = capitalizeFirstInList && enabledCategories.isEmpty(); if (filteredCategories.test(category) && isCategoryEnabled(policy, category)) { - if (category == PRIORITY_CATEGORY_ALARMS) { - enabledCategories.add(mContext.getString(R.string.zen_mode_alarms)); - } else if (category == PRIORITY_CATEGORY_MEDIA) { - enabledCategories.add(mContext.getString( - R.string.zen_mode_media)); - } else if (category == PRIORITY_CATEGORY_SYSTEM) { - enabledCategories.add(mContext.getString( - R.string.zen_mode_system)); - } else if (category == Policy.PRIORITY_CATEGORY_MESSAGES) { - if (policy.priorityMessageSenders == Policy.PRIORITY_SENDERS_ANY) { - enabledCategories.add(mContext.getString( - R.string.zen_mode_all_messages)); - } else { - enabledCategories.add(mContext.getString( - R.string.zen_mode_selected_messages)); - } - } else if (category == Policy.PRIORITY_CATEGORY_EVENTS) { - enabledCategories.add(mContext.getString(R.string.zen_mode_events)); - } else if (category == Policy.PRIORITY_CATEGORY_REMINDERS) { - enabledCategories.add(mContext.getString(R.string.zen_mode_reminders)); - } else if (category == Policy.PRIORITY_CATEGORY_CALLS) { - if (policy.priorityCallSenders == Policy.PRIORITY_SENDERS_ANY) { - enabledCategories.add(mContext.getString( - R.string.zen_mode_all_callers)); - } else if (policy.priorityCallSenders == Policy.PRIORITY_SENDERS_CONTACTS){ - enabledCategories.add(mContext.getString( - R.string.zen_mode_contacts_callers)); - } else { - enabledCategories.add(mContext.getString( - R.string.zen_mode_starred_callers)); - } - } else if (category == Policy.PRIORITY_CATEGORY_REPEAT_CALLERS) { - if (!enabledCategories.contains(mContext.getString( - R.string.zen_mode_all_callers))) { - enabledCategories.add(mContext.getString( - R.string.zen_mode_repeat_callers)); - } + if (category == Policy.PRIORITY_CATEGORY_REPEAT_CALLERS + && isCategoryEnabled(policy, Policy.PRIORITY_CATEGORY_CALLS) + && policy.priorityCallSenders == Policy.PRIORITY_SENDERS_ANY) { + continue; } + + enabledCategories.add(getCategory(category, policy, isFirst)); } } return enabledCategories; @@ -297,6 +250,64 @@ public class ZenModeSettings extends ZenModeSettingsBase { private boolean isCategoryEnabled(Policy policy, int categoryType) { return (policy.priorityCategories & categoryType) != 0; } + + private String getCategory(int category, Policy policy, boolean isFirst) { + if (category == PRIORITY_CATEGORY_ALARMS) { + if (isFirst) { + return mContext.getString(R.string.zen_mode_alarms); + } else { + return mContext.getString(R.string.zen_mode_alarms_list); + } + } else if (category == PRIORITY_CATEGORY_MEDIA) { + if (isFirst) { + return mContext.getString(R.string.zen_mode_media); + } else { + return mContext.getString(R.string.zen_mode_media_list); + } + } else if (category == PRIORITY_CATEGORY_SYSTEM) { + if (isFirst) { + return mContext.getString(R.string.zen_mode_system); + } else { + return mContext.getString(R.string.zen_mode_system_list); + } + } else if (category == Policy.PRIORITY_CATEGORY_MESSAGES) { + if (policy.priorityMessageSenders == Policy.PRIORITY_SENDERS_ANY) { + return mContext.getString(R.string.zen_mode_from_anyone); + } else if (policy.priorityMessageSenders == Policy.PRIORITY_SENDERS_CONTACTS){ + return mContext.getString(R.string.zen_mode_from_contacts); + } else { + return mContext.getString(R.string.zen_mode_from_starred); + } + } else if (category == Policy.PRIORITY_CATEGORY_EVENTS) { + if (isFirst) { + return mContext.getString(R.string.zen_mode_events); + } else { + return mContext.getString(R.string.zen_mode_events_list); + } + } else if (category == Policy.PRIORITY_CATEGORY_REMINDERS) { + if (isFirst) { + return mContext.getString(R.string.zen_mode_reminders); + } else { + return mContext.getString(R.string.zen_mode_reminders_list); + } + } else if (category == Policy.PRIORITY_CATEGORY_CALLS) { + if (policy.priorityCallSenders == Policy.PRIORITY_SENDERS_ANY) { + return mContext.getString(R.string.zen_mode_all_callers); + } else if (policy.priorityCallSenders == Policy.PRIORITY_SENDERS_CONTACTS){ + return mContext.getString(R.string.zen_mode_contacts_callers); + } else { + return mContext.getString(R.string.zen_mode_starred_callers); + } + } else if (category == Policy.PRIORITY_CATEGORY_REPEAT_CALLERS) { + if (isFirst) { + return mContext.getString(R.string.zen_mode_repeat_callers); + } else { + return mContext.getString(R.string.zen_mode_repeat_callers_list); + } + } + + return ""; + } } /** @@ -317,7 +328,6 @@ public class ZenModeSettings extends ZenModeSettingsBase { public List<String> getNonIndexableKeys(Context context) { List<String> keys = super.getNonIndexableKeys(context); keys.add(ZenModeDurationPreferenceController.KEY); - keys.add(ZenModeButtonPreferenceController.KEY); return keys; } diff --git a/src/com/android/settings/notification/ZenModeSettingsBase.java b/src/com/android/settings/notification/ZenModeSettingsBase.java index 2aecae4cc0..3f53879483 100644 --- a/src/com/android/settings/notification/ZenModeSettingsBase.java +++ b/src/com/android/settings/notification/ZenModeSettingsBase.java @@ -26,7 +26,11 @@ import android.provider.Settings; import android.provider.Settings.Global; import android.util.Log; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + import com.android.settings.dashboard.RestrictedDashboardFragment; +import com.android.settingslib.core.AbstractPreferenceController; abstract public class ZenModeSettingsBase extends RestrictedDashboardFragment { protected static final String TAG = "ZenModeSettings"; @@ -52,9 +56,14 @@ abstract public class ZenModeSettingsBase extends RestrictedDashboardFragment { } @Override - public void onCreate(Bundle icicle) { - mContext = getActivity(); + public void onAttach(Context context) { + super.onAttach(context); + mContext = context; mBackend = ZenModeBackend.getInstance(mContext); + } + + @Override + public void onCreate(Bundle icicle) { super.onCreate(icicle); updateZenMode(false /*fireChanged*/); } @@ -116,4 +125,20 @@ abstract public class ZenModeSettingsBase extends RestrictedDashboardFragment { } } } + + void updatePreference(AbstractPreferenceController controller) { + final PreferenceScreen screen = getPreferenceScreen(); + if (!controller.isAvailable()) { + return; + } + final String key = controller.getPreferenceKey(); + + final Preference preference = screen.findPreference(key); + if (preference == null) { + Log.d(TAG, String.format("Cannot find preference with key %s in Controller %s", + key, controller.getClass().getSimpleName())); + return; + } + controller.updateState(preference); + } } diff --git a/src/com/android/settings/notification/ZenModeSettingsFooterPreferenceController.java b/src/com/android/settings/notification/ZenModeSettingsFooterPreferenceController.java index c21f23dac6..e7ce1ddddf 100644 --- a/src/com/android/settings/notification/ZenModeSettingsFooterPreferenceController.java +++ b/src/com/android/settings/notification/ZenModeSettingsFooterPreferenceController.java @@ -16,21 +16,43 @@ package com.android.settings.notification; +import android.app.Dialog; +import android.app.NotificationManager; +import android.app.settings.SettingsEnums; import android.content.Context; +import android.content.DialogInterface; +import android.icu.text.ListFormatter; import android.net.Uri; +import android.os.Bundle; import android.provider.Settings; import android.service.notification.ZenModeConfig; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; + +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.FragmentManager; import androidx.preference.Preference; import com.android.settings.R; +import com.android.settings.core.SubSettingLauncher; +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; +import com.android.settings.utils.AnnotationSpan; import com.android.settingslib.core.lifecycle.Lifecycle; -public class ZenModeSettingsFooterPreferenceController extends AbstractZenModePreferenceController { +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; - protected static final String KEY = "footer_preference"; +public class ZenModeSettingsFooterPreferenceController extends AbstractZenModePreferenceController { + static final String KEY = "footer_preference"; + private FragmentManager mFragment; - public ZenModeSettingsFooterPreferenceController(Context context, Lifecycle lifecycle) { + public ZenModeSettingsFooterPreferenceController(Context context, Lifecycle lifecycle, + FragmentManager fragment) { super(context, KEY, lifecycle); + mFragment = fragment; } @Override @@ -62,8 +84,43 @@ public class ZenModeSettingsFooterPreferenceController extends AbstractZenModePr } } - protected String getFooterText() { + protected CharSequence getFooterText() { ZenModeConfig config = getZenModeConfig(); + + NotificationManager.Policy appliedPolicy = mBackend.getConsolidatedPolicy(); + NotificationManager.Policy defaultPolicy = config.toNotificationPolicy(); + final boolean usingCustomPolicy = !Objects.equals(appliedPolicy, defaultPolicy); + + if (usingCustomPolicy) { + final List<ZenModeConfig.ZenRule> activeRules = getActiveRules(config); + final List<String> rulesNames = new ArrayList<>(); + for (ZenModeConfig.ZenRule rule : activeRules) { + if (rule.name != null) { + rulesNames.add(rule.name); + } + } + if (rulesNames.size() > 0) { + String rules = ListFormatter.getInstance().format(rulesNames); + if (!rules.isEmpty()) { + final AnnotationSpan.LinkInfo linkInfo = new AnnotationSpan.LinkInfo( + AnnotationSpan.LinkInfo.DEFAULT_ANNOTATION, new View.OnClickListener() { + @Override + public void onClick(View v) { + showCustomSettingsDialog(); + } + }); + return TextUtils.concat(mContext.getResources().getString( + R.string.zen_mode_settings_dnd_custom_settings_footer, rules), + AnnotationSpan.linkify(mContext.getResources().getText( + R.string.zen_mode_settings_dnd_custom_settings_footer_link), + linkInfo)); + } + } + } + return getDefaultPolicyFooter(config); + } + + private String getDefaultPolicyFooter(ZenModeConfig config) { String footerText = ""; long latestEndTime = -1; @@ -115,4 +172,115 @@ public class ZenModeSettingsFooterPreferenceController extends AbstractZenModePr } return footerText; } + + private List<ZenModeConfig.ZenRule> getActiveRules(ZenModeConfig config) { + List<ZenModeConfig.ZenRule> zenRules = new ArrayList<>(); + if (config.manualRule != null) { + zenRules.add(config.manualRule); + } + + for (ZenModeConfig.ZenRule automaticRule : config.automaticRules.values()) { + if (automaticRule.isAutomaticActive()) { + zenRules.add(automaticRule); + } + } + return zenRules; + } + + private void showCustomSettingsDialog() { + ZenCustomSettingsDialog dialog = new ZenCustomSettingsDialog(); + dialog.setNotificationPolicy(mBackend.getConsolidatedPolicy()); + dialog.show(mFragment, ZenCustomSettingsDialog.class.getName()); + } + + public static class ZenCustomSettingsDialog extends InstrumentedDialogFragment { + private String KEY_POLICY = "policy"; + private NotificationManager.Policy mPolicy; + private ZenModeSettings.SummaryBuilder mSummaryBuilder; + + public void setNotificationPolicy(NotificationManager.Policy policy) { + mPolicy = policy; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + Context context = getActivity(); + if (savedInstanceState != null) { + NotificationManager.Policy policy = savedInstanceState.getParcelable(KEY_POLICY); + if (policy != null) { + mPolicy = policy; + } + } + + mSummaryBuilder = new ZenModeSettings.SummaryBuilder(context); + + AlertDialog customSettingsDialog = new AlertDialog.Builder(context) + .setTitle(R.string.zen_custom_settings_dialog_title) + .setNeutralButton(R.string.zen_custom_settings_dialog_review_schedule, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, + int which) { + new SubSettingLauncher(context) + .setDestination( + ZenModeAutomationSettings.class.getName()) + .setSourceMetricsCategory( + SettingsEnums.NOTIFICATION_ZEN_MODE_AUTOMATION) + .launch(); + } + }) + .setPositiveButton(R.string.zen_custom_settings_dialog_ok, null) + .setView(LayoutInflater.from(context).inflate(context.getResources().getLayout( + R.layout.zen_custom_settings_dialog), null, false)) + .create(); + + customSettingsDialog.setOnShowListener(new DialogInterface.OnShowListener() { + @Override + public void onShow(DialogInterface dialog) { + TextView allowCallsText = customSettingsDialog.findViewById( + R.id.zen_custom_settings_dialog_calls_allow); + TextView allowMessagesText = customSettingsDialog.findViewById( + R.id.zen_custom_settings_dialog_messages_allow); + TextView allowAlarmsText = customSettingsDialog.findViewById( + R.id.zen_custom_settings_dialog_alarms_allow); + TextView allowMediaText = customSettingsDialog.findViewById( + R.id.zen_custom_settings_dialog_media_allow); + TextView allowSystemText = customSettingsDialog.findViewById( + R.id.zen_custom_settings_dialog_system_allow); + TextView allowRemindersText = customSettingsDialog.findViewById( + R.id.zen_custom_settings_dialog_reminders_allow); + TextView allowEventsText = customSettingsDialog.findViewById( + R.id.zen_custom_settings_dialog_events_allow); + TextView notificationsText = customSettingsDialog.findViewById( + R.id.zen_custom_settings_dialog_show_notifications); + + allowCallsText.setText(mSummaryBuilder.getCallsSettingSummary(mPolicy)); + allowMessagesText.setText(mSummaryBuilder.getMessagesSettingSummary(mPolicy)); + allowAlarmsText.setText(getAllowRes(mPolicy.allowAlarms())); + allowMediaText.setText(getAllowRes(mPolicy.allowMedia())); + allowSystemText.setText(getAllowRes(mPolicy.allowSystem())); + allowRemindersText.setText(getAllowRes(mPolicy.allowReminders())); + allowEventsText.setText(getAllowRes(mPolicy.allowEvents())); + notificationsText.setText(mSummaryBuilder.getBlockedEffectsSummary(mPolicy)); + } + }); + + return customSettingsDialog; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.ZEN_CUSTOM_SETTINGS_DIALOG; + } + + private int getAllowRes(boolean allow) { + return allow ? R.string.zen_mode_sound_summary_on : R.string.zen_mode_sound_summary_off; + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putParcelable(KEY_POLICY, mPolicy); + } + } } diff --git a/src/com/android/settings/notification/ZenModeSliceBuilder.java b/src/com/android/settings/notification/ZenModeSliceBuilder.java index bf47154a1d..1109404284 100644 --- a/src/com/android/settings/notification/ZenModeSliceBuilder.java +++ b/src/com/android/settings/notification/ZenModeSliceBuilder.java @@ -18,49 +18,34 @@ package com.android.settings.notification; import static android.app.slice.Slice.EXTRA_TOGGLE_STATE; -import static androidx.slice.builders.ListBuilder.ICON_IMAGE; - import android.annotation.ColorInt; import android.app.NotificationManager; import android.app.PendingIntent; -import android.content.ContentResolver; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.Uri; import android.provider.Settings; -import android.provider.SettingsSlicesContract; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.core.graphics.drawable.IconCompat; +import androidx.slice.Slice; +import androidx.slice.builders.ListBuilder; +import androidx.slice.builders.ListBuilder.RowBuilder; +import androidx.slice.builders.SliceAction; + import com.android.settings.R; import com.android.settings.SubSettings; import com.android.settings.Utils; -import com.android.settings.search.DatabaseIndexingUtils; -import com.android.settings.slices.SettingsSliceProvider; +import com.android.settings.slices.CustomSliceRegistry; import com.android.settings.slices.SliceBroadcastReceiver; import com.android.settings.slices.SliceBuilderUtils; -import androidx.slice.Slice; -import androidx.slice.builders.ListBuilder; -import androidx.slice.builders.SliceAction; - -import androidx.core.graphics.drawable.IconCompat; - public class ZenModeSliceBuilder { private static final String TAG = "ZenModeSliceBuilder"; - private static final String ZEN_MODE_KEY = "zen_mode"; - - /** - * Backing Uri for the Zen Mode Slice. - */ - public static final Uri ZEN_MODE_URI = new Uri.Builder() - .scheme(ContentResolver.SCHEME_CONTENT) - .authority(SettingsSliceProvider.SLICE_AUTHORITY) - .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) - .appendPath(ZEN_MODE_KEY) - .build(); + private static final String ZEN_MODE_SLICE_KEY = ZenModeButtonPreferenceController.KEY; /** * Action notifying a change on the Zen Mode Slice. @@ -80,7 +65,7 @@ public class ZenModeSliceBuilder { } /** - * Return a ZenMode Slice bound to {@link #ZEN_MODE_URI}. + * Return a ZenMode Slice bound to {@link CustomSliceRegistry#ZEN_MODE_URI}. * <p> * Note that you should register a listener for {@link #INTENT_FILTER} to get changes for * ZenMode. @@ -88,18 +73,22 @@ public class ZenModeSliceBuilder { public static Slice getSlice(Context context) { final boolean isZenModeEnabled = isZenModeEnabled(context); final CharSequence title = context.getText(R.string.zen_mode_settings_title); - @ColorInt final int color = Utils.getColorAccent(context); + final CharSequence subtitle = context.getText(R.string.zen_mode_slice_subtitle); + @ColorInt final int color = Utils.getColorAccentDefaultColor(context); final PendingIntent toggleAction = getBroadcastIntent(context); final PendingIntent primaryAction = getPrimaryAction(context); - final SliceAction primarySliceAction = new SliceAction(primaryAction, - (IconCompat) null /* icon */, title); - final SliceAction toggleSliceAction = new SliceAction(toggleAction, null /* actionTitle */, + final SliceAction primarySliceAction = SliceAction.createDeeplink(primaryAction, + (IconCompat) null /* icon */, ListBuilder.ICON_IMAGE, title); + final SliceAction toggleSliceAction = SliceAction.createToggle(toggleAction, + null /* actionTitle */, isZenModeEnabled); - return new ListBuilder(context, ZEN_MODE_URI, ListBuilder.INFINITY) + return new ListBuilder(context, CustomSliceRegistry.ZEN_MODE_SLICE_URI, + ListBuilder.INFINITY) .setAccentColor(color) - .addRow(b -> b + .addRow(new RowBuilder() .setTitle(title) + .setSubtitle(subtitle) .addEndItem(toggleSliceAction) .setPrimaryAction(primarySliceAction)) .build(); @@ -124,11 +113,11 @@ public class ZenModeSliceBuilder { } public static Intent getIntent(Context context) { - final Uri contentUri = new Uri.Builder().appendPath(ZEN_MODE_KEY).build(); + final Uri contentUri = new Uri.Builder().appendPath(ZEN_MODE_SLICE_KEY).build(); final String screenTitle = context.getText(R.string.zen_mode_settings_title).toString(); - return DatabaseIndexingUtils.buildSearchResultPageIntent(context, - ZenModeSettings.class.getName(), ZEN_MODE_KEY, screenTitle, - MetricsEvent.NOTIFICATION_ZEN_MODE) + return SliceBuilderUtils.buildSearchResultPageIntent(context, + ZenModeSettings.class.getName(), ZEN_MODE_SLICE_KEY, screenTitle, + SettingsEnums.NOTIFICATION_ZEN_MODE) .setClassName(context.getPackageName(), SubSettings.class.getName()) .setData(contentUri); } diff --git a/src/com/android/settings/notification/ZenModeSoundVibrationSettings.java b/src/com/android/settings/notification/ZenModeSoundVibrationSettings.java index d100e12798..9f30759cc8 100644 --- a/src/com/android/settings/notification/ZenModeSoundVibrationSettings.java +++ b/src/com/android/settings/notification/ZenModeSoundVibrationSettings.java @@ -16,34 +16,44 @@ package com.android.settings.notification; +import android.app.settings.SettingsEnums; import android.content.Context; import android.provider.SearchIndexableResource; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.List; +@SearchIndexable public class ZenModeSoundVibrationSettings extends ZenModeSettingsBase implements Indexable { @Override protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { - return buildPreferenceControllers(context, getLifecycle()); + return buildPreferenceControllers(context, getSettingsLifecycle()); } private static List<AbstractPreferenceController> buildPreferenceControllers(Context context, Lifecycle lifecycle) { List<AbstractPreferenceController> controllers = new ArrayList<>(); - controllers.add(new ZenModeAlarmsPreferenceController(context, lifecycle)); + controllers.add(new ZenModeCallsPreferenceController(context, lifecycle, + "zen_mode_calls_settings")); + controllers.add(new ZenModeMessagesPreferenceController(context, lifecycle, + "zen_mode_messages_settings")); + controllers.add(new ZenModeAlarmsPreferenceController(context, lifecycle, + "zen_mode_alarms")); controllers.add(new ZenModeMediaPreferenceController(context, lifecycle)); controllers.add(new ZenModeSystemPreferenceController(context, lifecycle)); + controllers.add(new ZenModeRemindersPreferenceController(context, lifecycle)); + controllers.add(new ZenModeEventsPreferenceController(context, lifecycle)); controllers.add(new ZenModeBehaviorFooterPreferenceController(context, lifecycle, R.string.zen_sound_footer)); + controllers.add(new ZenModeBypassingAppsPreferenceController(context, lifecycle)); return controllers; } @@ -54,7 +64,7 @@ public class ZenModeSoundVibrationSettings extends ZenModeSettingsBase implement @Override public int getMetricsCategory() { - return MetricsEvent.NOTIFICATION_ZEN_MODE_PRIORITY; + return SettingsEnums.NOTIFICATION_ZEN_MODE_PRIORITY; } /** diff --git a/src/com/android/settings/notification/ZenModeStarredContactsPreferenceController.java b/src/com/android/settings/notification/ZenModeStarredContactsPreferenceController.java index bab16619ea..e5982ebd1a 100644 --- a/src/com/android/settings/notification/ZenModeStarredContactsPreferenceController.java +++ b/src/com/android/settings/notification/ZenModeStarredContactsPreferenceController.java @@ -23,36 +23,25 @@ import static android.app.NotificationManager.Policy.PRIORITY_SENDERS_STARRED; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; -import android.database.Cursor; -import android.icu.text.ListFormatter; import android.provider.Contacts; -import android.provider.ContactsContract; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import com.android.internal.annotations.VisibleForTesting; -import com.android.settings.R; import com.android.settingslib.core.lifecycle.Lifecycle; -import java.util.ArrayList; -import java.util.List; - public class ZenModeStarredContactsPreferenceController extends AbstractZenModePreferenceController implements Preference.OnPreferenceClickListener { - - protected static final String KEY = "zen_mode_starred_contacts"; private Preference mPreference; private final int mPriorityCategory; private final PackageManager mPackageManager; - @VisibleForTesting - Intent mStarredContactsIntent; - @VisibleForTesting - Intent mFallbackIntent; + private Intent mStarredContactsIntent; + private Intent mFallbackIntent; public ZenModeStarredContactsPreferenceController(Context context, Lifecycle lifecycle, int - priorityCategory) { - super(context, KEY, lifecycle); + priorityCategory, String key) { + super(context, key, lifecycle); mPriorityCategory = priorityCategory; mPackageManager = mContext.getPackageManager(); @@ -66,7 +55,10 @@ public class ZenModeStarredContactsPreferenceController extends public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); mPreference = screen.findPreference(KEY); - mPreference.setOnPreferenceClickListener(this); + + if (mPreference != null) { + mPreference.setOnPreferenceClickListener(this); + } } @Override @@ -91,31 +83,8 @@ public class ZenModeStarredContactsPreferenceController extends } @Override - public void updateState(Preference preference) { - super.updateState(preference); - - List<String> starredContacts = getStarredContacts(); - int numStarredContacts = starredContacts.size(); - - List<String> displayContacts = new ArrayList<>(); - - if (numStarredContacts == 0) { - displayContacts.add(mContext.getString(R.string.zen_mode_from_none)); - } else { - for (int i = 0; i < 2 && i < numStarredContacts; i++) { - displayContacts.add(starredContacts.get(i)); - } - - if (numStarredContacts == 3) { - displayContacts.add(starredContacts.get(2)); - } else if (numStarredContacts > 2) { - displayContacts.add(mContext.getResources().getQuantityString( - R.plurals.zen_mode_starred_contacts_summary_additional_contacts, - numStarredContacts - 2, numStarredContacts - 2)); - } - } - - mPreference.setSummary(ListFormatter.getInstance().format(displayContacts)); + public CharSequence getSummary() { + return mBackend.getStarredContactsSummary(); } @Override @@ -128,22 +97,6 @@ public class ZenModeStarredContactsPreferenceController extends return true; } - private List<String> getStarredContacts() { - List<String> starredContacts = new ArrayList<>(); - - Cursor cursor = mContext.getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, - new String[]{ContactsContract.Contacts.DISPLAY_NAME_PRIMARY}, - ContactsContract.Data.STARRED + "=1", null, - ContactsContract.Data.TIMES_CONTACTED); - - if (cursor.moveToFirst()) { - do { - starredContacts.add(cursor.getString(0)); - } while (cursor.moveToNext()); - } - return starredContacts; - } - private boolean isIntentValid() { return mStarredContactsIntent.resolveActivity(mPackageManager) != null || mFallbackIntent.resolveActivity(mPackageManager) != null; diff --git a/src/com/android/settings/notification/ZenModeSystemPreferenceController.java b/src/com/android/settings/notification/ZenModeSystemPreferenceController.java index b41f11e3cf..c2f4060286 100644 --- a/src/com/android/settings/notification/ZenModeSystemPreferenceController.java +++ b/src/com/android/settings/notification/ZenModeSystemPreferenceController.java @@ -17,13 +17,14 @@ package com.android.settings.notification; import android.app.NotificationManager.Policy; +import android.app.settings.SettingsEnums; import android.content.Context; import android.provider.Settings; -import androidx.preference.SwitchPreference; -import androidx.preference.Preference; import android.util.Log; -import com.android.internal.logging.nano.MetricsProto; +import androidx.preference.Preference; +import androidx.preference.SwitchPreference; + import com.android.settingslib.core.lifecycle.Lifecycle; public class ZenModeSystemPreferenceController extends @@ -73,7 +74,7 @@ public class ZenModeSystemPreferenceController extends Log.d(TAG, "onPrefChange allowSystem=" + allowSystem); } - mMetricsFeatureProvider.action(mContext, MetricsProto.MetricsEvent.ACTION_ZEN_ALLOW_SYSTEM, + mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_ZEN_ALLOW_SYSTEM, allowSystem); mBackend.saveSoundPolicy(Policy.PRIORITY_CATEGORY_SYSTEM, allowSystem); return true; diff --git a/src/com/android/settings/notification/ZenModeVisEffectPreferenceController.java b/src/com/android/settings/notification/ZenModeVisEffectPreferenceController.java index 4c49224bc7..e3098f0e22 100644 --- a/src/com/android/settings/notification/ZenModeVisEffectPreferenceController.java +++ b/src/com/android/settings/notification/ZenModeVisEffectPreferenceController.java @@ -18,13 +18,14 @@ package com.android.settings.notification; import android.app.NotificationManager; import android.content.Context; + import androidx.preference.CheckBoxPreference; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; import com.android.settings.core.PreferenceControllerMixin; -import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settings.widget.DisabledCheckBoxPreference; +import com.android.settingslib.core.lifecycle.Lifecycle; public class ZenModeVisEffectPreferenceController extends AbstractZenModePreferenceController diff --git a/src/com/android/settings/notification/ZenModeVisEffectsAllPreferenceController.java b/src/com/android/settings/notification/ZenModeVisEffectsAllPreferenceController.java index 2af7866450..14285d664a 100644 --- a/src/com/android/settings/notification/ZenModeVisEffectsAllPreferenceController.java +++ b/src/com/android/settings/notification/ZenModeVisEffectsAllPreferenceController.java @@ -17,18 +17,18 @@ package com.android.settings.notification; import android.app.NotificationManager.Policy; +import android.app.settings.SettingsEnums; import android.content.Context; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import com.android.internal.logging.nano.MetricsProto; import com.android.settingslib.core.lifecycle.Lifecycle; public class ZenModeVisEffectsAllPreferenceController extends AbstractZenModePreferenceController implements ZenCustomRadioButtonPreference.OnRadioButtonClickListener { - private final String KEY; private ZenCustomRadioButtonPreference mPreference; protected static final int EFFECTS = Policy.SUPPRESSED_EFFECT_SCREEN_OFF @@ -44,13 +44,12 @@ public class ZenModeVisEffectsAllPreferenceController public ZenModeVisEffectsAllPreferenceController(Context context, Lifecycle lifecycle, String key) { super(context, key, lifecycle); - KEY = key; } @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - mPreference = (ZenCustomRadioButtonPreference) screen.findPreference(KEY); + mPreference = screen.findPreference(getPreferenceKey()); mPreference.setOnRadioButtonClickListener(this); } @@ -71,7 +70,7 @@ public class ZenModeVisEffectsAllPreferenceController @Override public void onRadioButtonClick(ZenCustomRadioButtonPreference p) { mMetricsFeatureProvider.action(mContext, - MetricsProto.MetricsEvent.ACTION_ZEN_SOUND_AND_VIS_EFFECTS, true); + SettingsEnums.ACTION_ZEN_SOUND_AND_VIS_EFFECTS, true); mBackend.saveVisualEffectsPolicy(EFFECTS, true); } } diff --git a/src/com/android/settings/notification/ZenModeVisEffectsCustomPreferenceController.java b/src/com/android/settings/notification/ZenModeVisEffectsCustomPreferenceController.java index c015d9d8f2..837f999bd8 100644 --- a/src/com/android/settings/notification/ZenModeVisEffectsCustomPreferenceController.java +++ b/src/com/android/settings/notification/ZenModeVisEffectsCustomPreferenceController.java @@ -16,12 +16,13 @@ package com.android.settings.notification; -import android.app.NotificationManager.Policy; +import android.app.NotificationManager; +import android.app.settings.SettingsEnums; import android.content.Context; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.core.SubSettingLauncher; import com.android.settingslib.core.lifecycle.Lifecycle; @@ -29,18 +30,17 @@ import com.android.settingslib.core.lifecycle.Lifecycle; public class ZenModeVisEffectsCustomPreferenceController extends AbstractZenModePreferenceController { - private final String KEY; private ZenCustomRadioButtonPreference mPreference; - protected static final int INTERRUPTIVE_EFFECTS = Policy.SUPPRESSED_EFFECT_AMBIENT - | Policy.SUPPRESSED_EFFECT_PEEK - | Policy.SUPPRESSED_EFFECT_LIGHTS - | Policy.SUPPRESSED_EFFECT_FULL_SCREEN_INTENT; + protected static final int INTERRUPTIVE_EFFECTS = + NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT + | NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK + | NotificationManager.Policy.SUPPRESSED_EFFECT_LIGHTS + | NotificationManager.Policy.SUPPRESSED_EFFECT_FULL_SCREEN_INTENT; public ZenModeVisEffectsCustomPreferenceController(Context context, Lifecycle lifecycle, String key) { super(context, key, lifecycle); - KEY = key; } @Override @@ -51,10 +51,11 @@ public class ZenModeVisEffectsCustomPreferenceController @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - mPreference = (ZenCustomRadioButtonPreference) screen.findPreference(KEY); + mPreference = screen.findPreference(getPreferenceKey()); mPreference.setOnGearClickListener(p -> { launchCustomSettings(); + }); mPreference.setOnRadioButtonClickListener(p -> { @@ -71,7 +72,8 @@ public class ZenModeVisEffectsCustomPreferenceController protected boolean areCustomOptionsSelected() { boolean allEffectsSuppressed = - Policy.areAllVisualEffectsSuppressed(mBackend.mPolicy.suppressedVisualEffects); + NotificationManager.Policy.areAllVisualEffectsSuppressed( + mBackend.mPolicy.suppressedVisualEffects); boolean noEffectsSuppressed = mBackend.mPolicy.suppressedVisualEffects == 0; return !(allEffectsSuppressed || noEffectsSuppressed); @@ -79,15 +81,15 @@ public class ZenModeVisEffectsCustomPreferenceController protected void select() { mMetricsFeatureProvider.action(mContext, - MetricsProto.MetricsEvent.ACTION_ZEN_CUSTOM, true); + SettingsEnums.ACTION_ZEN_CUSTOM, true); } private void launchCustomSettings() { select(); new SubSettingLauncher(mContext) .setDestination(ZenModeBlockedEffectsSettings.class.getName()) - .setTitle(R.string.zen_mode_what_to_block_title) - .setSourceMetricsCategory(MetricsProto.MetricsEvent.SETTINGS_ZEN_NOTIFICATIONS) + .setTitleRes(R.string.zen_mode_what_to_block_title) + .setSourceMetricsCategory(SettingsEnums.SETTINGS_ZEN_NOTIFICATIONS) .launch(); } }
\ No newline at end of file diff --git a/src/com/android/settings/notification/ZenModeVisEffectsNonePreferenceController.java b/src/com/android/settings/notification/ZenModeVisEffectsNonePreferenceController.java index 3fd5420f6a..4a58c6241e 100644 --- a/src/com/android/settings/notification/ZenModeVisEffectsNonePreferenceController.java +++ b/src/com/android/settings/notification/ZenModeVisEffectsNonePreferenceController.java @@ -17,18 +17,18 @@ package com.android.settings.notification; import android.app.NotificationManager.Policy; +import android.app.settings.SettingsEnums; import android.content.Context; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import com.android.internal.logging.nano.MetricsProto; import com.android.settingslib.core.lifecycle.Lifecycle; public class ZenModeVisEffectsNonePreferenceController extends AbstractZenModePreferenceController implements ZenCustomRadioButtonPreference.OnRadioButtonClickListener { - private final String KEY; private ZenCustomRadioButtonPreference mPreference; protected static final int EFFECTS = Policy.SUPPRESSED_EFFECT_SCREEN_OFF @@ -44,13 +44,12 @@ public class ZenModeVisEffectsNonePreferenceController public ZenModeVisEffectsNonePreferenceController(Context context, Lifecycle lifecycle, String key) { super(context, key, lifecycle); - KEY = key; } @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - mPreference = (ZenCustomRadioButtonPreference) screen.findPreference(KEY); + mPreference = screen.findPreference(getPreferenceKey()); mPreference.setOnRadioButtonClickListener(this); } @@ -70,7 +69,7 @@ public class ZenModeVisEffectsNonePreferenceController @Override public void onRadioButtonClick(ZenCustomRadioButtonPreference preference) { mMetricsFeatureProvider.action(mContext, - MetricsProto.MetricsEvent.ACTION_ZEN_SOUND_ONLY, true); + SettingsEnums.ACTION_ZEN_SOUND_ONLY, true); mBackend.saveVisualEffectsPolicy(EFFECTS, false); } } diff --git a/src/com/android/settings/notification/ZenModeVoiceActivity.java b/src/com/android/settings/notification/ZenModeVoiceActivity.java index db8de69f86..0a52c3da0b 100644 --- a/src/com/android/settings/notification/ZenModeVoiceActivity.java +++ b/src/com/android/settings/notification/ZenModeVoiceActivity.java @@ -16,6 +16,9 @@ package com.android.settings.notification; +import static android.provider.Settings.EXTRA_DO_NOT_DISTURB_MODE_ENABLED; +import static android.provider.Settings.EXTRA_DO_NOT_DISTURB_MODE_MINUTES; + import android.app.NotificationManager; import android.content.Context; import android.content.Intent; @@ -33,9 +36,6 @@ import com.android.settings.utils.VoiceSettingsActivity; import java.util.Locale; -import static android.provider.Settings.EXTRA_DO_NOT_DISTURB_MODE_ENABLED; -import static android.provider.Settings.EXTRA_DO_NOT_DISTURB_MODE_MINUTES; - /** * Activity for modifying the Zen mode (Do not disturb) by voice * using the Voice Interaction API. diff --git a/src/com/android/settings/notification/ZenOnboardingActivity.java b/src/com/android/settings/notification/ZenOnboardingActivity.java index 798b081592..87411c2b9a 100644 --- a/src/com/android/settings/notification/ZenOnboardingActivity.java +++ b/src/com/android/settings/notification/ZenOnboardingActivity.java @@ -19,6 +19,7 @@ package com.android.settings.notification; import android.app.Activity; import android.app.NotificationManager; import android.app.NotificationManager.Policy; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -29,9 +30,9 @@ import android.util.Log; import android.view.View; import android.widget.RadioButton; -import com.android.internal.annotations.VisibleForTesting; +import androidx.annotation.VisibleForTesting; + import com.android.internal.logging.MetricsLogger; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider; import com.android.settings.overlay.FeatureFactory; @@ -61,8 +62,8 @@ public class ZenOnboardingActivity extends Activity { setMetricsLogger(new MetricsLogger()); Context context = getApplicationContext(); - Settings.Global.putInt(context.getContentResolver(), - Settings.Global.ZEN_SETTINGS_SUGGESTION_VIEWED, 1); + Settings.Secure.putInt(context.getContentResolver(), + Settings.Secure.ZEN_SETTINGS_SUGGESTION_VIEWED, 1); setupUI(); } @@ -99,7 +100,7 @@ public class ZenOnboardingActivity extends Activity { mKeepCurrentSettingButton.setOnClickListener(currentSettingClickListener); mKeepCurrentSettingButton.setChecked(true); - mMetrics.visible(MetricsEvent.SETTINGS_ZEN_ONBOARDING); + mMetrics.visible(SettingsEnums.SETTINGS_ZEN_ONBOARDING); } @VisibleForTesting @@ -113,7 +114,7 @@ public class ZenOnboardingActivity extends Activity { } public void launchSettings(View button) { - mMetrics.action(MetricsEvent.ACTION_ZEN_ONBOARDING_SETTINGS); + mMetrics.action(SettingsEnums.ACTION_ZEN_ONBOARDING_SETTINGS); Intent settings = new Intent(Settings.ACTION_ZEN_MODE_SETTINGS); settings.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); startActivity(settings); @@ -129,13 +130,13 @@ public class ZenOnboardingActivity extends Activity { policy.priorityMessageSenders, NotificationManager.Policy.getAllSuppressedVisualEffects()); mNm.setNotificationPolicy(newPolicy); - mMetrics.action(MetricsEvent.ACTION_ZEN_ONBOARDING_OK); + mMetrics.action(SettingsEnums.ACTION_ZEN_ONBOARDING_OK); } else { - mMetrics.action(MetricsEvent.ACTION_ZEN_ONBOARDING_KEEP_CURRENT_SETTINGS); + mMetrics.action(SettingsEnums.ACTION_ZEN_ONBOARDING_KEEP_CURRENT_SETTINGS); } - Settings.Global.putInt(getApplicationContext().getContentResolver(), - Settings.Global.ZEN_SETTINGS_UPDATED, 1); + Settings.Secure.putInt(getApplicationContext().getContentResolver(), + Settings.Secure.ZEN_SETTINGS_UPDATED, 1); finishAndRemoveTask(); } @@ -159,11 +160,11 @@ public class ZenOnboardingActivity extends Activity { NotificationManager nm = context.getSystemService(NotificationManager.class); if (NotificationManager.Policy.areAllVisualEffectsSuppressed( nm.getNotificationPolicy().suppressedVisualEffects)) { - Settings.Global.putInt(context.getContentResolver(), - Settings.Global.ZEN_SETTINGS_UPDATED, 1); + Settings.Secure.putInt(context.getContentResolver(), + Settings.Secure.ZEN_SETTINGS_UPDATED, 1); } - return Settings.Global.getInt(context.getContentResolver(), - Settings.Global.ZEN_SETTINGS_UPDATED, 0) != 0; + return Settings.Secure.getInt(context.getContentResolver(), + Settings.Secure.ZEN_SETTINGS_UPDATED, 0) != 0; } private static boolean showSuggestion(Context context) { @@ -172,8 +173,8 @@ public class ZenOnboardingActivity extends Activity { // SHOW_ZEN_SETTINGS_SUGGESTION is also true when: // - automatic rule has started DND and user has not seen the first use dialog - return Settings.Global.getInt(context.getContentResolver(), - Settings.Global.SHOW_ZEN_SETTINGS_SUGGESTION, 0) != 0; + return Settings.Secure.getInt(context.getContentResolver(), + Settings.Secure.SHOW_ZEN_SETTINGS_SUGGESTION, 0) != 0; } @@ -194,7 +195,8 @@ public class ZenOnboardingActivity extends Activity { final long showTimeMs = firstDisplayTimeMs + ALWAYS_SHOW_THRESHOLD; final boolean stillShow = currentTimeMs < showTimeMs; - Log.d(TAG, "still show zen suggestion based on time: " + stillShow); + Log.d(TAG, "still show zen suggestion based on time: " + stillShow + " showTimeMs=" + + showTimeMs); return stillShow; } } diff --git a/src/com/android/settings/notification/ZenRuleButtonsPreferenceController.java b/src/com/android/settings/notification/ZenRuleButtonsPreferenceController.java new file mode 100644 index 0000000000..a014ec9068 --- /dev/null +++ b/src/com/android/settings/notification/ZenRuleButtonsPreferenceController.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2018 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.notification; + +import android.app.AutomaticZenRule; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.View; + +import androidx.fragment.app.Fragment; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceScreen; + +import com.android.internal.logging.nano.MetricsProto; +import com.android.settings.R; +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settings.core.SubSettingLauncher; +import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.widget.ActionButtonsPreference; + +public class ZenRuleButtonsPreferenceController extends AbstractZenModePreferenceController + implements PreferenceControllerMixin { + public static final String KEY = "zen_action_buttons"; + + private AutomaticZenRule mRule; + private String mId; + private PreferenceFragmentCompat mFragment; + private ActionButtonsPreference mButtonsPref; + + + public ZenRuleButtonsPreferenceController(Context context, PreferenceFragmentCompat fragment, + Lifecycle lc) { + super(context, KEY, lc); + mFragment = fragment; + } + + + @Override + public boolean isAvailable() { + return mRule != null; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + if (isAvailable()) { + mButtonsPref = ((ActionButtonsPreference) screen.findPreference(KEY)) + .setButton1Text(R.string.zen_mode_rule_name_edit) + .setButton1Icon(com.android.internal.R.drawable.ic_mode_edit) + .setButton1OnClickListener(new EditRuleNameClickListener()) + .setButton2Text(R.string.zen_mode_delete_rule_button) + .setButton2Icon(R.drawable.ic_settings_delete) + .setButton2OnClickListener(new DeleteRuleClickListener()); + } + } + + public class EditRuleNameClickListener implements View.OnClickListener { + public EditRuleNameClickListener() {} + + @Override + public void onClick(View v) { + ZenRuleNameDialog.show(mFragment, mRule.getName(), null, + new ZenRuleNameDialog.PositiveClickListener() { + @Override + public void onOk(String ruleName, Fragment parent) { + if (TextUtils.equals(ruleName, mRule.getName())) { + return; + } + mMetricsFeatureProvider.action(mContext, + SettingsEnums.ACTION_ZEN_MODE_RULE_NAME_CHANGE_OK); + mRule.setName(ruleName); + mRule.setModified(true); + mBackend.updateZenRule(mId, mRule); + } + }); + } + } + + public class DeleteRuleClickListener implements View.OnClickListener { + public DeleteRuleClickListener() {} + + @Override + public void onClick(View v) { + ZenDeleteRuleDialog.show(mFragment, mRule.getName(), mId, + new ZenDeleteRuleDialog.PositiveClickListener() { + @Override + public void onOk(String id) { + Bundle bundle = new Bundle(); + bundle.putString(ZenModeAutomationSettings.DELETE, id); + mMetricsFeatureProvider.action(mContext, + SettingsEnums.ACTION_ZEN_DELETE_RULE_OK); + new SubSettingLauncher(mContext) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + .setDestination(ZenModeAutomationSettings.class.getName()) + .setSourceMetricsCategory(MetricsProto.MetricsEvent + .NOTIFICATION_ZEN_MODE_AUTOMATION) + .setArguments(bundle) + .launch(); + } + }); + } + } + + protected void onResume(AutomaticZenRule rule, String id) { + mRule = rule; + mId = id; + } +} diff --git a/src/com/android/settings/notification/ZenRuleCallsPreferenceController.java b/src/com/android/settings/notification/ZenRuleCallsPreferenceController.java new file mode 100644 index 0000000000..87f2db2e2c --- /dev/null +++ b/src/com/android/settings/notification/ZenRuleCallsPreferenceController.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2018 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.notification; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.service.notification.ZenPolicy; +import android.text.TextUtils; +import android.util.Pair; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.ListPreference; +import androidx.preference.Preference; + +import com.android.internal.logging.nano.MetricsProto; +import com.android.settingslib.core.lifecycle.Lifecycle; + +public class ZenRuleCallsPreferenceController extends AbstractZenCustomRulePreferenceController + implements Preference.OnPreferenceChangeListener { + + private final String[] mListValues; + + public ZenRuleCallsPreferenceController(Context context, String key, Lifecycle lifecycle) { + super(context, key, lifecycle); + mListValues = context.getResources().getStringArray( + com.android.settings.R.array.zen_mode_contacts_values); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + updateFromContactsValue(preference); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object selectedContactsFrom) { + int allowCalls = ZenModeBackend.getZenPolicySettingFromPrefKey( + selectedContactsFrom.toString()); + mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_ZEN_ALLOW_CALLS, + Pair.create(MetricsProto.MetricsEvent.FIELD_ZEN_TOGGLE_EXCEPTION, allowCalls), + Pair.create(MetricsProto.MetricsEvent.FIELD_ZEN_RULE_ID, mId)); + mRule.setZenPolicy(new ZenPolicy.Builder(mRule.getZenPolicy()) + .allowCalls(allowCalls) + .build()); + mBackend.updateZenRule(mId, mRule); + updateFromContactsValue(preference); + return true; + } + + private void updateFromContactsValue(Preference preference) { + if (mRule == null || mRule.getZenPolicy() == null) { + return; + } + ListPreference listPreference = (ListPreference) preference; + listPreference.setSummary(mBackend.getContactsCallsSummary(mRule.getZenPolicy())); + final String currentVal = ZenModeBackend.getKeyFromZenPolicySetting( + mRule.getZenPolicy().getPriorityCallSenders()); + listPreference.setValue(mListValues[getIndexOfSendersValue(currentVal)]); + + } + + @VisibleForTesting + protected int getIndexOfSendersValue(String currentVal) { + int index = 3; // defaults to "none" based on R.array.zen_mode_contacts_values + for (int i = 0; i < mListValues.length; i++) { + if (TextUtils.equals(currentVal, mListValues[i])) { + return i; + } + } + + return index; + } +} diff --git a/src/com/android/settings/notification/ZenRuleCustomPolicyPreferenceController.java b/src/com/android/settings/notification/ZenRuleCustomPolicyPreferenceController.java new file mode 100644 index 0000000000..527803df47 --- /dev/null +++ b/src/com/android/settings/notification/ZenRuleCustomPolicyPreferenceController.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2018 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.notification; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.service.notification.ZenPolicy; + +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.core.SubSettingLauncher; +import com.android.settingslib.core.lifecycle.Lifecycle; + +public class ZenRuleCustomPolicyPreferenceController extends + AbstractZenCustomRulePreferenceController { + + private ZenCustomRadioButtonPreference mPreference; + + public ZenRuleCustomPolicyPreferenceController(Context context, Lifecycle lifecycle, + String key) { + super(context, key, lifecycle); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + + mPreference.setOnGearClickListener(p -> { + setCustomPolicy(); + launchCustomSettings(); + + }); + + mPreference.setOnRadioButtonClickListener(p -> { + setCustomPolicy(); + launchCustomSettings(); + }); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + if (mId == null || mRule == null) { + return; + } + + mPreference.setChecked(mRule.getZenPolicy() != null); + } + + private void setCustomPolicy() { + if (mRule.getZenPolicy() == null) { + mRule.setZenPolicy(mBackend.setDefaultZenPolicy(new ZenPolicy())); + mBackend.updateZenRule(mId, mRule); + } + } + + private void launchCustomSettings() { + new SubSettingLauncher(mContext) + .setDestination(ZenCustomRuleConfigSettings.class.getName()) + .setArguments(createBundle()) + .setSourceMetricsCategory(SettingsEnums.ZEN_CUSTOM_RULE_SOUND_SETTINGS) + .launch(); + } +}
\ No newline at end of file diff --git a/src/com/android/settings/notification/ZenRuleCustomSwitchPreferenceController.java b/src/com/android/settings/notification/ZenRuleCustomSwitchPreferenceController.java new file mode 100644 index 0000000000..69aa81e5a6 --- /dev/null +++ b/src/com/android/settings/notification/ZenRuleCustomSwitchPreferenceController.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2017 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.notification; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.service.notification.ZenPolicy; +import android.util.Log; +import android.util.Pair; + +import androidx.preference.Preference; +import androidx.preference.SwitchPreference; + +import com.android.internal.logging.nano.MetricsProto; +import com.android.settingslib.core.lifecycle.Lifecycle; + +public class ZenRuleCustomSwitchPreferenceController extends + AbstractZenCustomRulePreferenceController implements Preference.OnPreferenceChangeListener { + + private @ZenPolicy.PriorityCategory int mCategory; + private int mMetricsCategory; + + public ZenRuleCustomSwitchPreferenceController(Context context, Lifecycle lifecycle, + String key, @ZenPolicy.PriorityCategory int category, int metricsCategory) { + super(context, key, lifecycle); + mCategory = category; + mMetricsCategory = metricsCategory; + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + if (mRule == null || mRule.getZenPolicy() == null) { + return; + } + + SwitchPreference pref = (SwitchPreference) preference; + pref.setChecked(mRule.getZenPolicy().isCategoryAllowed(mCategory, false)); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + final boolean allow = (Boolean) newValue; + if (ZenModeSettingsBase.DEBUG) { + Log.d(TAG, KEY + " onPrefChange mRule=" + mRule + " mCategory=" + mCategory + + " allow=" + allow); + } + mMetricsFeatureProvider.action(mContext, mMetricsCategory, + Pair.create(MetricsProto.MetricsEvent.FIELD_ZEN_TOGGLE_EXCEPTION, allow ? 1 : 0), + Pair.create(MetricsProto.MetricsEvent.FIELD_ZEN_RULE_ID, mId)); + mRule.setZenPolicy(new ZenPolicy.Builder(mRule.getZenPolicy()) + .allowCategory(mCategory, allow) + .build()); + mBackend.updateZenRule(mId, mRule); + return true; + } +} diff --git a/src/com/android/settings/notification/ZenRuleDefaultPolicyPreferenceController.java b/src/com/android/settings/notification/ZenRuleDefaultPolicyPreferenceController.java new file mode 100644 index 0000000000..b4036fad52 --- /dev/null +++ b/src/com/android/settings/notification/ZenRuleDefaultPolicyPreferenceController.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2018 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.notification; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.util.Pair; + +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.internal.logging.nano.MetricsProto; +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settingslib.core.lifecycle.Lifecycle; + +public class ZenRuleDefaultPolicyPreferenceController extends + AbstractZenCustomRulePreferenceController implements PreferenceControllerMixin { + + private ZenCustomRadioButtonPreference mPreference; + + public ZenRuleDefaultPolicyPreferenceController(Context context, Lifecycle lifecycle, + String key) { + super(context, key, lifecycle); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + + mPreference.setOnRadioButtonClickListener(p -> { + mRule.setZenPolicy(null); + mBackend.updateZenRule(mId, mRule); + }); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + if (mId == null || mRule == null) { + return; + } + mMetricsFeatureProvider.action(mContext, SettingsEnums.ZEN_CUSTOM_RULE_DEFAULT_SETTINGS, + Pair.create(MetricsProto.MetricsEvent.FIELD_ZEN_RULE_ID, mId)); + mPreference.setChecked(mRule.getZenPolicy() == null); + } +}
\ No newline at end of file diff --git a/src/com/android/settings/notification/ZenRuleInfo.java b/src/com/android/settings/notification/ZenRuleInfo.java index 2d7abf81bc..1a1539b6a3 100644 --- a/src/com/android/settings/notification/ZenRuleInfo.java +++ b/src/com/android/settings/notification/ZenRuleInfo.java @@ -24,6 +24,8 @@ public class ZenRuleInfo { that.defaultConditionId) : that.defaultConditionId != null) return false; if (serviceComponent != null ? !serviceComponent.equals( that.serviceComponent) : that.serviceComponent != null) return false; + if (id != null ? !id.equals(that.id) : that.id != null) + return false; return packageLabel != null ? packageLabel.equals( that.packageLabel) : that.packageLabel == null; @@ -38,4 +40,5 @@ public class ZenRuleInfo { public boolean isSystem; public CharSequence packageLabel; public int ruleInstanceLimit = -1; + public String id; } diff --git a/src/com/android/settings/notification/ZenRuleMessagesPreferenceController.java b/src/com/android/settings/notification/ZenRuleMessagesPreferenceController.java new file mode 100644 index 0000000000..59ad3a89eb --- /dev/null +++ b/src/com/android/settings/notification/ZenRuleMessagesPreferenceController.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2018 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.notification; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.service.notification.ZenPolicy; +import android.text.TextUtils; +import android.util.Pair; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.ListPreference; +import androidx.preference.Preference; + +import com.android.internal.logging.nano.MetricsProto; +import com.android.settingslib.core.lifecycle.Lifecycle; + +public class ZenRuleMessagesPreferenceController extends AbstractZenCustomRulePreferenceController + implements Preference.OnPreferenceChangeListener { + + private final String[] mListValues; + + public ZenRuleMessagesPreferenceController(Context context, String key, Lifecycle lifecycle) { + super(context, key, lifecycle); + mListValues = context.getResources().getStringArray( + com.android.settings.R.array.zen_mode_contacts_values); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + updateFromContactsValue(preference); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object selectedContactsFrom) { + int allowMessages = ZenModeBackend.getZenPolicySettingFromPrefKey( + selectedContactsFrom.toString()); + mMetricsFeatureProvider.action(mContext, + SettingsEnums.ACTION_ZEN_ALLOW_MESSAGES, + Pair.create(MetricsProto.MetricsEvent.FIELD_ZEN_TOGGLE_EXCEPTION, allowMessages), + Pair.create(MetricsProto.MetricsEvent.FIELD_ZEN_RULE_ID, mId)); + mRule.setZenPolicy(new ZenPolicy.Builder(mRule.getZenPolicy()) + .allowMessages(allowMessages) + .build()); + mBackend.updateZenRule(mId, mRule); + updateFromContactsValue(preference); + return true; + } + + private void updateFromContactsValue(Preference preference) { + if (mRule == null || mRule.getZenPolicy() == null) { + return; + } + ListPreference listPreference = (ListPreference) preference; + listPreference.setSummary(mBackend.getContactsMessagesSummary(mRule.getZenPolicy())); + final String currentVal = ZenModeBackend.getKeyFromZenPolicySetting( + mRule.getZenPolicy().getPriorityMessageSenders()); + listPreference.setValue(mListValues[getIndexOfSendersValue(currentVal)]); + + } + + @VisibleForTesting + protected int getIndexOfSendersValue(String currentVal) { + int index = 3; // defaults to "none" based on R.array.zen_mode_contacts_values + for (int i = 0; i < mListValues.length; i++) { + if (TextUtils.equals(currentVal, mListValues[i])) { + return i; + } + } + + return index; + } +} diff --git a/src/com/android/settings/notification/ZenRuleNameDialog.java b/src/com/android/settings/notification/ZenRuleNameDialog.java index 819ba5bfaa..edb303535f 100644 --- a/src/com/android/settings/notification/ZenRuleNameDialog.java +++ b/src/com/android/settings/notification/ZenRuleNameDialog.java @@ -16,9 +16,8 @@ package com.android.settings.notification; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.Fragment; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.DialogInterface; import android.net.Uri; @@ -29,7 +28,9 @@ import android.view.LayoutInflater; import android.view.View; import android.widget.EditText; -import com.android.internal.logging.nano.MetricsProto; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; + import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; @@ -41,7 +42,7 @@ public class ZenRuleNameDialog extends InstrumentedDialogFragment { @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.NOTIFICATION_ZEN_MODE_RULE_NAME_DIALOG; + return SettingsEnums.NOTIFICATION_ZEN_MODE_RULE_NAME_DIALOG; } /** diff --git a/src/com/android/settings/notification/ZenRuleNotifFooterPreferenceController.java b/src/com/android/settings/notification/ZenRuleNotifFooterPreferenceController.java new file mode 100644 index 0000000000..678bf90eda --- /dev/null +++ b/src/com/android/settings/notification/ZenRuleNotifFooterPreferenceController.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2018 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.notification; + +import android.content.Context; + +import androidx.preference.Preference; + +import com.android.settings.R; +import com.android.settingslib.core.lifecycle.Lifecycle; + +public class ZenRuleNotifFooterPreferenceController extends + AbstractZenCustomRulePreferenceController { + + public ZenRuleNotifFooterPreferenceController(Context context, Lifecycle lifecycle, + String key) { + super(context, key, lifecycle); + } + + @Override + public boolean isAvailable() { + if (!super.isAvailable() || mRule.getZenPolicy() == null) { + return false; + } + + + return mRule.getZenPolicy().shouldHideAllVisualEffects() + || mRule.getZenPolicy().shouldShowAllVisualEffects(); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + if (mRule == null || mRule.getZenPolicy() == null) { + return; + } + + if (mRule.getZenPolicy().shouldShowAllVisualEffects()) { + preference.setTitle(R.string.zen_mode_restrict_notifications_mute_footer); + } else if (mRule.getZenPolicy().shouldHideAllVisualEffects()) { + preference.setTitle(R.string.zen_mode_restrict_notifications_hide_footer); + } else { + preference.setTitle(null); + } + } +} diff --git a/src/com/android/settings/notification/ZenRulePreference.java b/src/com/android/settings/notification/ZenRulePreference.java index 1bb1538f01..fb6e92a634 100644 --- a/src/com/android/settings/notification/ZenRulePreference.java +++ b/src/com/android/settings/notification/ZenRulePreference.java @@ -17,18 +17,19 @@ package com.android.settings.notification; import android.app.AutomaticZenRule; -import android.app.Fragment; import android.content.ComponentName; import android.content.Context; -import android.content.pm.ApplicationInfo; +import android.content.Intent; +import android.content.pm.ComponentInfo; import android.content.pm.PackageManager; -import android.content.pm.ServiceInfo; import android.service.notification.ZenModeConfig; +import android.view.View; +import android.widget.CheckBox; + +import androidx.fragment.app.Fragment; import androidx.preference.Preference; import androidx.preference.PreferenceViewHolder; -import android.view.View; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.utils.ManagedServiceSettings; import com.android.settings.utils.ZenServiceListing; @@ -40,9 +41,7 @@ import java.util.Map; public class ZenRulePreference extends TwoTargetPreference { private static final ManagedServiceSettings.Config CONFIG = ZenModeAutomationSettings.getConditionProviderConfig(); - final CharSequence mName; final String mId; - boolean appExists; final Fragment mParent; final Preference mPref; final Context mContext; @@ -50,16 +49,22 @@ public class ZenRulePreference extends TwoTargetPreference { final ZenServiceListing mServiceListing; final PackageManager mPm; final MetricsFeatureProvider mMetricsFeatureProvider; + AutomaticZenRule mRule; + CharSequence mName; + + private Intent mIntent; + private boolean mChecked; + private CheckBox mCheckBox; public ZenRulePreference(Context context, final Map.Entry<String, AutomaticZenRule> ruleEntry, Fragment parent, MetricsFeatureProvider metricsProvider) { super(context); - + setLayoutResource(R.layout.preference_checkable_two_target); mBackend = ZenModeBackend.getInstance(context); mContext = context; - final AutomaticZenRule rule = ruleEntry.getValue(); - mName = rule.getName(); + mRule = ruleEntry.getValue(); + mName = mRule.getName(); mId = ruleEntry.getKey(); mParent = parent; mPm = mContext.getPackageManager(); @@ -67,80 +72,112 @@ public class ZenRulePreference extends TwoTargetPreference { mServiceListing.reloadApprovedServices(); mPref = this; mMetricsFeatureProvider = metricsProvider; - - setAttributes(rule); + mChecked = mRule.isEnabled(); + setAttributes(mRule); + setWidgetLayoutResource(getSecondTargetResId()); } - @Override protected int getSecondTargetResId() { - if (mId != null && ZenModeConfig.DEFAULT_RULE_IDS.contains(mId)) { - return 0; + if (mIntent != null) { + return R.layout.zen_rule_widget; } - - return R.layout.zen_rule_widget; + return 0; } @Override public void onBindViewHolder(PreferenceViewHolder view) { super.onBindViewHolder(view); + View settingsWidget = view.findViewById(android.R.id.widget_frame); + View divider = view.findViewById(R.id.two_target_divider); + if (mIntent != null) { + divider.setVisibility(View.VISIBLE); + settingsWidget.setVisibility(View.VISIBLE); + settingsWidget.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mContext.startActivity(mIntent); + } + }); + } else { + divider.setVisibility(View.GONE); + settingsWidget.setVisibility(View.GONE); + settingsWidget.setOnClickListener(null); + } - View v = view.findViewById(R.id.delete_zen_rule); - if (v != null) { - v.setOnClickListener(mDeleteListener); + View checkboxContainer = view.findViewById(R.id.checkbox_container); + if (checkboxContainer != null) { + checkboxContainer.setOnClickListener(mOnCheckBoxClickListener); + } + mCheckBox = (CheckBox) view.findViewById(com.android.internal.R.id.checkbox); + if (mCheckBox != null) { + mCheckBox.setChecked(mChecked); } } - private final View.OnClickListener mDeleteListener = new View.OnClickListener() { + public boolean isChecked() { + return mChecked; + } + + public void updatePreference(AutomaticZenRule rule) { + if (!mRule.getName().equals(rule.getName())) { + mName = rule.getName(); + setTitle(mName); + } + + if (mRule.isEnabled() != rule.isEnabled()) { + setChecked(mRule.isEnabled()); + setSummary(computeRuleSummary(mRule)); + } + + mRule = rule; + } + + @Override + public void onClick() { + mOnCheckBoxClickListener.onClick(null); + } + + private void setChecked(boolean checked) { + mChecked = checked; + if (mCheckBox != null) { + mCheckBox.setChecked(checked); + } + } + + private View.OnClickListener mOnCheckBoxClickListener = new View.OnClickListener() { @Override public void onClick(View v) { - showDeleteRuleDialog(mParent, mId, mName.toString()); + mRule.setEnabled(!mChecked); + mBackend.updateZenRule(mId, mRule); + setChecked(mRule.isEnabled()); + setAttributes(mRule); } }; - private void showDeleteRuleDialog(final Fragment parent, final String ruleId, - final String ruleName) { - ZenDeleteRuleDialog.show(parent, ruleName, ruleId, - new ZenDeleteRuleDialog.PositiveClickListener() { - @Override - public void onOk(String id) { - mMetricsFeatureProvider.action(mContext, - MetricsProto.MetricsEvent.ACTION_ZEN_DELETE_RULE_OK); - mBackend.removeZenRule(id); - } - }); - } - protected void setAttributes(AutomaticZenRule rule) { final boolean isSchedule = ZenModeConfig.isValidScheduleConditionId( - rule.getConditionId()); + rule.getConditionId(), true); final boolean isEvent = ZenModeConfig.isValidEventConditionId(rule.getConditionId()); - final boolean isSystemRule = isSchedule || isEvent; - - try { - ApplicationInfo info = mPm.getApplicationInfo(rule.getOwner().getPackageName(), 0); - setSummary(computeRuleSummary(rule, isSystemRule, info.loadLabel(mPm))); - } catch (PackageManager.NameNotFoundException e) { - appExists = false; - return; - } - appExists = true; - setTitle(rule.getName()); + setSummary(computeRuleSummary(rule)); + + setTitle(mName); setPersistent(false); final String action = isSchedule ? ZenModeScheduleRuleSettings.ACTION : isEvent ? ZenModeEventRuleSettings.ACTION : ""; - ServiceInfo si = mServiceListing.findService(rule.getOwner()); + ComponentInfo si = mServiceListing.findService(rule.getOwner()); ComponentName settingsActivity = AbstractZenModeAutomaticRulePreferenceController. - getSettingsActivity(si); - setIntent(AbstractZenModeAutomaticRulePreferenceController.getRuleIntent(action, - settingsActivity, mId)); - setSelectable(settingsActivity != null || isSystemRule); + getSettingsActivity(rule, si); + mIntent = AbstractZenModeAutomaticRulePreferenceController.getRuleIntent(action, + settingsActivity, mId); + if (mIntent.resolveActivity(mPm) == null) { + mIntent = null; + } setKey(mId); } - private String computeRuleSummary(AutomaticZenRule rule, boolean isSystemRule, - CharSequence providerLabel) { + private String computeRuleSummary(AutomaticZenRule rule) { return (rule == null || !rule.isEnabled()) ? mContext.getResources().getString(R.string.switch_off_text) : mContext.getResources().getString(R.string.switch_on_text); diff --git a/src/com/android/settings/notification/ZenRuleRepeatCallersPreferenceController.java b/src/com/android/settings/notification/ZenRuleRepeatCallersPreferenceController.java new file mode 100644 index 0000000000..7f3b0638de --- /dev/null +++ b/src/com/android/settings/notification/ZenRuleRepeatCallersPreferenceController.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2018 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.notification; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.service.notification.ZenPolicy; +import android.util.Log; +import android.util.Pair; + +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreference; + +import com.android.internal.logging.nano.MetricsProto; +import com.android.settingslib.core.lifecycle.Lifecycle; + +public class ZenRuleRepeatCallersPreferenceController extends + AbstractZenCustomRulePreferenceController implements Preference.OnPreferenceChangeListener { + + private final int mRepeatCallersThreshold; + + public ZenRuleRepeatCallersPreferenceController(Context context, + String key, Lifecycle lifecycle, int repeatCallersThreshold) { + super(context, key, lifecycle); + mRepeatCallersThreshold = repeatCallersThreshold; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + setRepeatCallerSummary(screen.findPreference(KEY)); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + if (mRule == null || mRule.getZenPolicy() == null) { + return; + } + + SwitchPreference pref = (SwitchPreference) preference; + boolean anyCallersCanBypassDnd = mRule.getZenPolicy().getPriorityCallSenders() + == ZenPolicy.PEOPLE_TYPE_ANYONE; + + // if any caller can bypass dnd then repeat callers preference is disabled + if (anyCallersCanBypassDnd) { + pref.setEnabled(false); + pref.setChecked(true); + } else { + pref.setEnabled(true); + pref.setChecked(mRule.getZenPolicy().getPriorityCategoryRepeatCallers() + == ZenPolicy.STATE_ALLOW); + } + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + final boolean allow = (Boolean) newValue; + if (ZenModeSettingsBase.DEBUG) { + Log.d(TAG, KEY + " onPrefChange mRule=" + mRule + " mCategory=" + + ZenPolicy.PRIORITY_CATEGORY_REPEAT_CALLERS + " allow=" + allow); + } + mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_ZEN_ALLOW_REPEAT_CALLS, + Pair.create(MetricsProto.MetricsEvent.FIELD_ZEN_TOGGLE_EXCEPTION, allow ? 1 : 0), + Pair.create(MetricsProto.MetricsEvent.FIELD_ZEN_RULE_ID, mId)); + mRule.setZenPolicy(new ZenPolicy.Builder(mRule.getZenPolicy()) + .allowRepeatCallers(allow) + .build()); + mBackend.updateZenRule(mId, mRule); + return true; + } + + private void setRepeatCallerSummary(Preference preference) { + preference.setSummary(mContext.getString( + com.android.settings.R.string.zen_mode_repeat_callers_summary, + mRepeatCallersThreshold)); + } +} diff --git a/src/com/android/settings/notification/ZenRuleSelectionDialog.java b/src/com/android/settings/notification/ZenRuleSelectionDialog.java index 9beac7c7a2..5b4b35ba74 100644 --- a/src/com/android/settings/notification/ZenRuleSelectionDialog.java +++ b/src/com/android/settings/notification/ZenRuleSelectionDialog.java @@ -16,17 +16,14 @@ package com.android.settings.notification; -import static com.android.internal.logging.nano.MetricsProto.MetricsEvent; - -import android.app.AlertDialog; import android.app.Dialog; -import android.app.Fragment; import android.app.NotificationManager; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.DialogInterface; import android.content.pm.ApplicationInfo; +import android.content.pm.ComponentInfo; import android.content.pm.PackageManager; -import android.content.pm.ServiceInfo; import android.graphics.drawable.Drawable; import android.os.AsyncTask; import android.os.Bundle; @@ -38,6 +35,9 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; + import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; import com.android.settings.utils.ZenServiceListing; @@ -70,7 +70,7 @@ public class ZenRuleSelectionDialog extends InstrumentedDialogFragment { @Override public int getMetricsCategory() { - return MetricsEvent.NOTIFICATION_ZEN_MODE_RULE_SELECTION_DIALOG; + return SettingsEnums.NOTIFICATION_ZEN_MODE_RULE_SELECTION_DIALOG; } public static void show(Context context, Fragment parent, PositiveClickListener @@ -169,7 +169,8 @@ public class ZenRuleSelectionDialog extends InstrumentedDialogFragment { private ZenRuleInfo defaultNewEvent() { final ZenModeConfig.EventInfo event = new ZenModeConfig.EventInfo(); - event.calendar = null; // any calendar + event.calName = null; // any calendar + event.calendarId = null; event.reply = ZenModeConfig.EventInfo.REPLY_ANY_EXCEPT_NO; final ZenRuleInfo rt = new ZenRuleInfo(); rt.settingsAction = ZenModeEventRuleSettings.ACTION; @@ -190,16 +191,17 @@ public class ZenRuleSelectionDialog extends InstrumentedDialogFragment { private final ZenServiceListing.Callback mServiceListingCallback = new ZenServiceListing.Callback() { @Override - public void onServicesReloaded(Set<ServiceInfo> services) { - if (DEBUG) Log.d(TAG, "Services reloaded: count=" + services.size()); + public void onComponentsReloaded(Set<ComponentInfo> componentInfos) { + if (DEBUG) Log.d(TAG, "Reloaded: count=" + componentInfos.size()); + Set<ZenRuleInfo> externalRuleTypes = new TreeSet<>(RULE_TYPE_COMPARATOR); - for (ServiceInfo serviceInfo : services) { + for (ComponentInfo ci : componentInfos) { final ZenRuleInfo ri = AbstractZenModeAutomaticRulePreferenceController. - getRuleInfo(mPm, serviceInfo); + getRuleInfo(mPm, ci); if (ri != null && ri.configurationActivity != null && mNm.isNotificationPolicyAccessGrantedForPackage(ri.packageName) && (ri.ruleInstanceLimit <= 0 || ri.ruleInstanceLimit - >= (mNm.getRuleInstanceCount(serviceInfo.getComponentName()) + 1))) { + >= (mNm.getRuleInstanceCount(ci.getComponentName()) + 1))) { externalRuleTypes.add(ri); } } diff --git a/src/com/android/settings/notification/ZenRuleStarredContactsPreferenceController.java b/src/com/android/settings/notification/ZenRuleStarredContactsPreferenceController.java new file mode 100644 index 0000000000..8a227a17ec --- /dev/null +++ b/src/com/android/settings/notification/ZenRuleStarredContactsPreferenceController.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2018 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.notification; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.provider.Contacts; +import android.service.notification.ZenPolicy; + +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settingslib.core.lifecycle.Lifecycle; + +public class ZenRuleStarredContactsPreferenceController extends + AbstractZenCustomRulePreferenceController implements Preference.OnPreferenceClickListener { + + private Preference mPreference; + private final @ZenPolicy.PriorityCategory int mPriorityCategory; + private final PackageManager mPackageManager; + + private Intent mStarredContactsIntent; + private Intent mFallbackIntent; + + public ZenRuleStarredContactsPreferenceController(Context context, Lifecycle lifecycle, + @ZenPolicy.PriorityCategory int priorityCategory, String key) { + super(context, key, lifecycle); + mPriorityCategory = priorityCategory; + mPackageManager = mContext.getPackageManager(); + + mStarredContactsIntent = new Intent(Contacts.Intents.UI.LIST_STARRED_ACTION); + + mFallbackIntent = new Intent(Intent.ACTION_MAIN); + mFallbackIntent.addCategory(Intent.CATEGORY_APP_CONTACTS); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(KEY); + + if (mPreference != null) { + mPreference.setOnPreferenceClickListener(this); + } + } + + @Override + public String getPreferenceKey() { + return KEY; + } + + @Override + public boolean isAvailable() { + if (!super.isAvailable() || mRule.getZenPolicy() == null || !isIntentValid()) { + return false; + } + + if (mPriorityCategory == ZenPolicy.PRIORITY_CATEGORY_CALLS) { + return mRule.getZenPolicy().getPriorityCallSenders() == ZenPolicy.PEOPLE_TYPE_STARRED; + } else if (mPriorityCategory == ZenPolicy.PRIORITY_CATEGORY_MESSAGES) { + return mRule.getZenPolicy().getPriorityMessageSenders() + == ZenPolicy.PEOPLE_TYPE_STARRED; + } else { + // invalid category + return false; + } + } + + @Override + public CharSequence getSummary() { + return mBackend.getStarredContactsSummary(); + } + + @Override + public boolean onPreferenceClick(Preference preference) { + if (mStarredContactsIntent.resolveActivity(mPackageManager) != null) { + mContext.startActivity(mStarredContactsIntent); + } else { + mContext.startActivity(mFallbackIntent); + } + return true; + } + + private boolean isIntentValid() { + return mStarredContactsIntent.resolveActivity(mPackageManager) != null + || mFallbackIntent.resolveActivity(mPackageManager) != null; + } +} diff --git a/src/com/android/settings/notification/ZenRuleVisEffectPreferenceController.java b/src/com/android/settings/notification/ZenRuleVisEffectPreferenceController.java new file mode 100644 index 0000000000..f5791510b6 --- /dev/null +++ b/src/com/android/settings/notification/ZenRuleVisEffectPreferenceController.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2018 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.notification; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.service.notification.ZenPolicy; +import android.util.Pair; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.CheckBoxPreference; +import androidx.preference.Preference; + +import com.android.internal.logging.nano.MetricsProto; +import com.android.settings.widget.DisabledCheckBoxPreference; +import com.android.settingslib.core.lifecycle.Lifecycle; + +public class ZenRuleVisEffectPreferenceController extends AbstractZenCustomRulePreferenceController + implements Preference.OnPreferenceChangeListener { + + private final int mMetricsCategory; + + @VisibleForTesting protected @ZenPolicy.VisualEffect int mEffect; + + // if any of these effects are suppressed, this effect must be too + @VisibleForTesting protected @ZenPolicy.VisualEffect int[] mParentSuppressedEffects; + + public ZenRuleVisEffectPreferenceController(Context context, Lifecycle lifecycle, String key, + @ZenPolicy.VisualEffect int visualEffect, int metricsCategory, + @ZenPolicy.VisualEffect int[] parentSuppressedEffects) { + super(context, key, lifecycle); + mEffect = visualEffect; + mMetricsCategory = metricsCategory; + mParentSuppressedEffects = parentSuppressedEffects; + } + + @Override + public boolean isAvailable() { + if (!super.isAvailable()) { + return false; + } + + if (mEffect == ZenPolicy.VISUAL_EFFECT_LIGHTS) { + return mContext.getResources() + .getBoolean(com.android.internal.R.bool.config_intrusiveNotificationLed); + } + return true; + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + if (mRule == null || mRule.getZenPolicy() == null) { + return; + } + + boolean suppressed = !mRule.getZenPolicy().isVisualEffectAllowed(mEffect, false); + boolean parentSuppressed = false; + if (mParentSuppressedEffects != null) { + for (@ZenPolicy.VisualEffect int parentEffect : mParentSuppressedEffects) { + if (!mRule.getZenPolicy().isVisualEffectAllowed(parentEffect, true)) { + parentSuppressed = true; + } + } + } + if (parentSuppressed) { + ((CheckBoxPreference) preference).setChecked(parentSuppressed); + onPreferenceChange(preference, parentSuppressed); + ((DisabledCheckBoxPreference) preference).enableCheckbox(false); + } else { + ((DisabledCheckBoxPreference) preference).enableCheckbox(true); + ((CheckBoxPreference) preference).setChecked(suppressed); + } + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + final boolean suppressEffect = (Boolean) newValue; + mMetricsFeatureProvider.action(mContext, mMetricsCategory, + Pair.create(MetricsProto.MetricsEvent.FIELD_ZEN_TOGGLE_EXCEPTION, + suppressEffect ? 1 : 0), + Pair.create(MetricsProto.MetricsEvent.FIELD_ZEN_RULE_ID, mId)); + + mRule.setZenPolicy(new ZenPolicy.Builder(mRule.getZenPolicy()) + .showVisualEffect(mEffect, !suppressEffect) + .build()); + mBackend.updateZenRule(mId, mRule); + return true; + } +} diff --git a/src/com/android/settings/notification/ZenRuleVisEffectsAllPreferenceController.java b/src/com/android/settings/notification/ZenRuleVisEffectsAllPreferenceController.java new file mode 100644 index 0000000000..7b7eae2b2a --- /dev/null +++ b/src/com/android/settings/notification/ZenRuleVisEffectsAllPreferenceController.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2018 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.notification; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.service.notification.ZenPolicy; +import android.util.Pair; + +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.internal.logging.nano.MetricsProto; +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settingslib.core.lifecycle.Lifecycle; + +public class ZenRuleVisEffectsAllPreferenceController extends + AbstractZenCustomRulePreferenceController implements PreferenceControllerMixin { + + private ZenCustomRadioButtonPreference mPreference; + + public ZenRuleVisEffectsAllPreferenceController(Context context, Lifecycle lifecycle, + String key) { + super(context, key, lifecycle); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + + mPreference.setOnRadioButtonClickListener(p -> { + mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_ZEN_SOUND_ONLY, + Pair.create(MetricsProto.MetricsEvent.FIELD_ZEN_RULE_ID, mId)); + mRule.setZenPolicy(new ZenPolicy.Builder(mRule.getZenPolicy()) + .showAllVisualEffects() + .build()); + mBackend.updateZenRule(mId, mRule); + }); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + if (mId == null || mRule == null || mRule.getZenPolicy() == null) { + return; + } + mPreference.setChecked(mRule.getZenPolicy().shouldShowAllVisualEffects()); + } +}
\ No newline at end of file diff --git a/src/com/android/settings/notification/ZenRuleVisEffectsCustomPreferenceController.java b/src/com/android/settings/notification/ZenRuleVisEffectsCustomPreferenceController.java new file mode 100644 index 0000000000..2314c5ab8f --- /dev/null +++ b/src/com/android/settings/notification/ZenRuleVisEffectsCustomPreferenceController.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2018 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.notification; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.util.Pair; + +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.internal.logging.nano.MetricsProto; +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settings.core.SubSettingLauncher; +import com.android.settingslib.core.lifecycle.Lifecycle; + +public class ZenRuleVisEffectsCustomPreferenceController extends + AbstractZenCustomRulePreferenceController implements PreferenceControllerMixin { + + private ZenCustomRadioButtonPreference mPreference; + + public ZenRuleVisEffectsCustomPreferenceController(Context context, Lifecycle lifecycle, + String key) { + super(context, key, lifecycle); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + + mPreference.setOnGearClickListener(p -> { + launchCustomSettings(); + + }); + + mPreference.setOnRadioButtonClickListener(p -> { + launchCustomSettings(); + }); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + if (mId == null || mRule == null || mRule.getZenPolicy() == null) { + return; + } + + mPreference.setChecked(!mRule.getZenPolicy().shouldHideAllVisualEffects() + && !mRule.getZenPolicy().shouldShowAllVisualEffects()); + } + + private void launchCustomSettings() { + mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_ZEN_SHOW_CUSTOM, + Pair.create(MetricsProto.MetricsEvent.FIELD_ZEN_RULE_ID, mId)); + new SubSettingLauncher(mContext) + .setDestination(ZenCustomRuleBlockedEffectsSettings.class.getName()) + .setArguments(createBundle()) + .setSourceMetricsCategory(SettingsEnums.ZEN_CUSTOM_RULE_VIS_EFFECTS) + .launch(); + } +}
\ No newline at end of file diff --git a/src/com/android/settings/notification/ZenRuleVisEffectsNonePreferenceController.java b/src/com/android/settings/notification/ZenRuleVisEffectsNonePreferenceController.java new file mode 100644 index 0000000000..875fb43e0b --- /dev/null +++ b/src/com/android/settings/notification/ZenRuleVisEffectsNonePreferenceController.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2018 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.notification; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.service.notification.ZenPolicy; +import android.util.Pair; + +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.internal.logging.nano.MetricsProto; +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settingslib.core.lifecycle.Lifecycle; + +public class ZenRuleVisEffectsNonePreferenceController extends + AbstractZenCustomRulePreferenceController implements PreferenceControllerMixin { + + private ZenCustomRadioButtonPreference mPreference; + + public ZenRuleVisEffectsNonePreferenceController(Context context, Lifecycle lifecycle, + String key) { + super(context, key, lifecycle); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + + mPreference.setOnRadioButtonClickListener(p -> { + mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_ZEN_SOUND_AND_VIS_EFFECTS, + Pair.create(MetricsProto.MetricsEvent.FIELD_ZEN_RULE_ID, mId)); + mRule.setZenPolicy(new ZenPolicy.Builder(mRule.getZenPolicy()) + .hideAllVisualEffects() + .build()); + mBackend.updateZenRule(mId, mRule); + }); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + if (mId == null || mRule == null || mRule.getZenPolicy() == null) { + return; + } + + mPreference.setChecked(mRule.getZenPolicy().shouldHideAllVisualEffects()); + } +}
\ No newline at end of file diff --git a/src/com/android/settings/overlay/FeatureFactory.java b/src/com/android/settings/overlay/FeatureFactory.java index 72bac99cc9..d9af345d65 100644 --- a/src/com/android/settings/overlay/FeatureFactory.java +++ b/src/com/android/settings/overlay/FeatureFactory.java @@ -20,17 +20,21 @@ import android.content.Context; import android.text.TextUtils; import android.util.Log; +import androidx.annotation.Nullable; + import com.android.settings.R; import com.android.settings.accounts.AccountFeatureProvider; import com.android.settings.applications.ApplicationFeatureProvider; +import com.android.settings.aware.AwareFeatureProvider; import com.android.settings.bluetooth.BluetoothFeatureProvider; import com.android.settings.dashboard.DashboardFeatureProvider; import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider; import com.android.settings.enterprise.EnterprisePrivacyFeatureProvider; import com.android.settings.fuelgauge.PowerUsageFeatureProvider; import com.android.settings.gestures.AssistGestureFeatureProvider; +import com.android.settings.homepage.contextualcards.ContextualCardFeatureProvider; import com.android.settings.localepicker.LocaleFeatureProvider; -import com.android.settings.search.DeviceIndexFeatureProvider; +import com.android.settings.panel.PanelFeatureProvider; import com.android.settings.search.SearchFeatureProvider; import com.android.settings.security.SecurityFeatureProvider; import com.android.settings.slices.SlicesFeatureProvider; @@ -48,6 +52,7 @@ public abstract class FeatureFactory { private static final boolean DEBUG = false; protected static FeatureFactory sFactory; + protected static Context sAppContext; /** * Returns a factory for creating feature controllers. Creates the factory if it does not @@ -58,6 +63,9 @@ public abstract class FeatureFactory { if (sFactory != null) { return sFactory; } + if (sAppContext == null) { + sAppContext = context.getApplicationContext(); + } if (DEBUG) Log.d(LOG_TAG, "getFactory"); final String clsName = context.getString(R.string.config_featureFactory); @@ -74,6 +82,16 @@ public abstract class FeatureFactory { return sFactory; } + /** + * Returns an application {@link Context} used to create this {@link FeatureFactory}. If the + * factory has not been properly created yet (aka {@link #getFactory} has not been called), this + * will return null. + */ + @Nullable + public static Context getAppContext() { + return sAppContext; + } + public abstract AssistGestureFeatureProvider getAssistGestureFeatureProvider(); public abstract SuggestionFeatureProvider getSuggestionFeatureProvider(Context context); @@ -103,13 +121,17 @@ public abstract class FeatureFactory { public abstract UserFeatureProvider getUserFeatureProvider(Context context); - public abstract BluetoothFeatureProvider getBluetoothFeatureProvider(Context context); - public abstract SlicesFeatureProvider getSlicesFeatureProvider(); public abstract AccountFeatureProvider getAccountFeatureProvider(); - public abstract DeviceIndexFeatureProvider getDeviceIndexFeatureProvider(); + public abstract PanelFeatureProvider getPanelFeatureProvider(); + + public abstract ContextualCardFeatureProvider getContextualCardFeatureProvider(Context context); + + public abstract BluetoothFeatureProvider getBluetoothFeatureProvider(Context context); + + public abstract AwareFeatureProvider getAwareFeatureProvider(); public static final class FactoryNotFoundException extends RuntimeException { public FactoryNotFoundException(Throwable throwable) { diff --git a/src/com/android/settings/overlay/FeatureFactoryImpl.java b/src/com/android/settings/overlay/FeatureFactoryImpl.java index 72714d563c..2f9626d8d3 100644 --- a/src/com/android/settings/overlay/FeatureFactoryImpl.java +++ b/src/com/android/settings/overlay/FeatureFactoryImpl.java @@ -21,15 +21,19 @@ import android.app.admin.DevicePolicyManager; import android.content.Context; import android.net.ConnectivityManager; import android.os.UserManager; + import androidx.annotation.Keep; import com.android.settings.accounts.AccountFeatureProvider; import com.android.settings.accounts.AccountFeatureProviderImpl; import com.android.settings.applications.ApplicationFeatureProvider; import com.android.settings.applications.ApplicationFeatureProviderImpl; +import com.android.settings.aware.AwareFeatureProvider; +import com.android.settings.aware.AwareFeatureProviderImpl; import com.android.settings.bluetooth.BluetoothFeatureProvider; import com.android.settings.bluetooth.BluetoothFeatureProviderImpl; import com.android.settings.connecteddevice.dock.DockUpdaterFeatureProviderImpl; +import com.android.settings.core.instrumentation.SettingsMetricsFeatureProvider; import com.android.settings.dashboard.DashboardFeatureProvider; import com.android.settings.dashboard.DashboardFeatureProviderImpl; import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider; @@ -40,10 +44,12 @@ import com.android.settings.fuelgauge.PowerUsageFeatureProvider; import com.android.settings.fuelgauge.PowerUsageFeatureProviderImpl; import com.android.settings.gestures.AssistGestureFeatureProvider; import com.android.settings.gestures.AssistGestureFeatureProviderImpl; +import com.android.settings.homepage.contextualcards.ContextualCardFeatureProvider; +import com.android.settings.homepage.contextualcards.ContextualCardFeatureProviderImpl; import com.android.settings.localepicker.LocaleFeatureProvider; import com.android.settings.localepicker.LocaleFeatureProviderImpl; -import com.android.settings.search.DeviceIndexFeatureProvider; -import com.android.settings.search.DeviceIndexFeatureProviderImpl; +import com.android.settings.panel.PanelFeatureProvider; +import com.android.settings.panel.PanelFeatureProviderImpl; import com.android.settings.search.SearchFeatureProvider; import com.android.settings.search.SearchFeatureProviderImpl; import com.android.settings.security.SecurityFeatureProvider; @@ -53,7 +59,6 @@ import com.android.settings.slices.SlicesFeatureProviderImpl; import com.android.settings.users.UserFeatureProvider; import com.android.settings.users.UserFeatureProviderImpl; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; -import com.android.settingslib.wrapper.PackageManagerWrapper; /** * {@link FeatureFactory} implementation for AOSP Settings. @@ -73,10 +78,12 @@ public class FeatureFactoryImpl extends FeatureFactory { private PowerUsageFeatureProvider mPowerUsageFeatureProvider; private AssistGestureFeatureProvider mAssistGestureFeatureProvider; private UserFeatureProvider mUserFeatureProvider; - private BluetoothFeatureProvider mBluetoothFeatureProvider; private SlicesFeatureProvider mSlicesFeatureProvider; private AccountFeatureProvider mAccountFeatureProvider; - private DeviceIndexFeatureProviderImpl mDeviceIndexFeatureProvider; + private PanelFeatureProvider mPanelFeatureProvider; + private ContextualCardFeatureProvider mContextualCardFeatureProvider; + private BluetoothFeatureProvider mBluetoothFeatureProvider; + private AwareFeatureProvider mAwareFeatureProvider; @Override public SupportFeatureProvider getSupportFeatureProvider(Context context) { @@ -86,7 +93,7 @@ public class FeatureFactoryImpl extends FeatureFactory { @Override public MetricsFeatureProvider getMetricsFeatureProvider() { if (mMetricsFeatureProvider == null) { - mMetricsFeatureProvider = new MetricsFeatureProvider(); + mMetricsFeatureProvider = new SettingsMetricsFeatureProvider(); } return mMetricsFeatureProvider; } @@ -122,7 +129,7 @@ public class FeatureFactoryImpl extends FeatureFactory { if (mApplicationFeatureProvider == null) { final Context appContext = context.getApplicationContext(); mApplicationFeatureProvider = new ApplicationFeatureProviderImpl(appContext, - new PackageManagerWrapper(appContext.getPackageManager()), + appContext.getPackageManager(), AppGlobals.getPackageManager(), (DevicePolicyManager) appContext .getSystemService(Context.DEVICE_POLICY_SERVICE)); @@ -143,9 +150,9 @@ public class FeatureFactoryImpl extends FeatureFactory { if (mEnterprisePrivacyFeatureProvider == null) { final Context appContext = context.getApplicationContext(); mEnterprisePrivacyFeatureProvider = new EnterprisePrivacyFeatureProviderImpl(appContext, - (DevicePolicyManager) appContext - .getSystemService(Context.DEVICE_POLICY_SERVICE), - new PackageManagerWrapper(appContext.getPackageManager()), + (DevicePolicyManager) appContext.getSystemService( + Context.DEVICE_POLICY_SERVICE), + appContext.getPackageManager(), UserManager.get(appContext), (ConnectivityManager) appContext.getSystemService(Context.CONNECTIVITY_SERVICE), appContext.getResources()); @@ -192,14 +199,6 @@ public class FeatureFactoryImpl extends FeatureFactory { } @Override - public BluetoothFeatureProvider getBluetoothFeatureProvider(Context context) { - if (mBluetoothFeatureProvider == null) { - mBluetoothFeatureProvider = new BluetoothFeatureProviderImpl(); - } - return mBluetoothFeatureProvider; - } - - @Override public AssistGestureFeatureProvider getAssistGestureFeatureProvider() { if (mAssistGestureFeatureProvider == null) { mAssistGestureFeatureProvider = new AssistGestureFeatureProviderImpl(); @@ -224,10 +223,36 @@ public class FeatureFactoryImpl extends FeatureFactory { } @Override - public DeviceIndexFeatureProvider getDeviceIndexFeatureProvider() { - if (mDeviceIndexFeatureProvider == null) { - mDeviceIndexFeatureProvider = new DeviceIndexFeatureProviderImpl(); + public PanelFeatureProvider getPanelFeatureProvider() { + if (mPanelFeatureProvider == null) { + mPanelFeatureProvider = new PanelFeatureProviderImpl(); + } + return mPanelFeatureProvider; + } + + @Override + public ContextualCardFeatureProvider getContextualCardFeatureProvider(Context context) { + if (mContextualCardFeatureProvider == null) { + mContextualCardFeatureProvider = new ContextualCardFeatureProviderImpl( + context.getApplicationContext()); + } + return mContextualCardFeatureProvider; + } + + @Override + public BluetoothFeatureProvider getBluetoothFeatureProvider(Context context) { + if (mBluetoothFeatureProvider == null) { + mBluetoothFeatureProvider = new BluetoothFeatureProviderImpl( + context.getApplicationContext()); + } + return mBluetoothFeatureProvider; + } + + @Override + public AwareFeatureProvider getAwareFeatureProvider() { + if (mAwareFeatureProvider == null) { + mAwareFeatureProvider = new AwareFeatureProviderImpl(); } - return mDeviceIndexFeatureProvider; + return mAwareFeatureProvider; } } diff --git a/src/com/android/settings/overlay/SupportFeatureProvider.java b/src/com/android/settings/overlay/SupportFeatureProvider.java index ad68a748e6..b22b4583fa 100644 --- a/src/com/android/settings/overlay/SupportFeatureProvider.java +++ b/src/com/android/settings/overlay/SupportFeatureProvider.java @@ -16,65 +16,17 @@ package com.android.settings.overlay; -import android.accounts.Account; -import android.annotation.IntDef; -import android.annotation.NonNull; -import android.annotation.StringRes; import android.app.Activity; -import android.app.FragmentManager; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; - -import com.android.settings.support.SupportPhone; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.List; /** * Feature provider for support tab. */ public interface SupportFeatureProvider { - @IntDef({SupportType.EMAIL, SupportType.PHONE, SupportType.CHAT}) - @Retention(RetentionPolicy.SOURCE) - @interface SupportType { - int EMAIL = 1; - int PHONE = 2; - int CHAT = 3; - } - - /** - * Refreshes all operation rules. - */ - void refreshOperationRules(); - - /** - * Returns the current country code if it has a operation config, otherwise returns null. - */ - String getCurrentCountryCodeIfHasConfig(@SupportType int type); - /** - * Returns a support phone for specified country. - */ - SupportPhone getSupportPhones(String countryCode, boolean isTollfree); - - /** - * Returns array of {@link Account} that's eligible for support options. - */ - @NonNull - Account[] getSupportEligibleAccounts(Context context); - - /** - * Starts support v2, invokes the support home page. Will no-op if support v2 is not enabled. + * Starts support, invokes the support home page. * * @param activity Calling activity. */ - void startSupportV2(Activity activity); - - /** - * Returns a url with information to introduce user to new device. - */ - String getNewDeviceIntroUrl(Context context); + void startSupport(Activity activity); } diff --git a/src/com/android/settings/overlay/SurveyFeatureProvider.java b/src/com/android/settings/overlay/SurveyFeatureProvider.java index d078d39247..44ee525060 100644 --- a/src/com/android/settings/overlay/SurveyFeatureProvider.java +++ b/src/com/android/settings/overlay/SurveyFeatureProvider.java @@ -18,7 +18,7 @@ package com.android.settings.overlay; import android.app.Activity; import android.content.BroadcastReceiver; import android.content.Context; -import android.content.IntentFilter; + import androidx.annotation.Nullable; import androidx.localbroadcastmanager.content.LocalBroadcastManager; diff --git a/src/com/android/settings/panel/InternetConnectivityPanel.java b/src/com/android/settings/panel/InternetConnectivityPanel.java new file mode 100644 index 0000000000..ae72427cbf --- /dev/null +++ b/src/com/android/settings/panel/InternetConnectivityPanel.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2018 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.panel; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.provider.Settings; + +import com.android.settings.R; +import com.android.settings.slices.CustomSliceRegistry; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents the Internet Connectivity Panel. + * + * <p> + * Displays Wifi (full Slice) and Airplane mode. + * </p> + */ +public class InternetConnectivityPanel implements PanelContent { + + private final Context mContext; + + public static InternetConnectivityPanel create(Context context) { + return new InternetConnectivityPanel(context); + } + + private InternetConnectivityPanel(Context context) { + mContext = context.getApplicationContext(); + } + + @Override + public CharSequence getTitle() { + return mContext.getText(R.string.internet_connectivity_panel_title); + } + + @Override + public List<Uri> getSlices() { + final List<Uri> uris = new ArrayList<>(); + uris.add(CustomSliceRegistry.WIFI_SLICE_URI); + uris.add(CustomSliceRegistry.MOBILE_DATA_SLICE_URI); + uris.add(CustomSliceRegistry.AIRPLANE_URI); + return uris; + } + + @Override + public Intent getSeeMoreIntent() { + return new Intent(Settings.ACTION_WIRELESS_SETTINGS) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.PANEL_INTERNET_CONNECTIVITY; + } +} diff --git a/src/com/android/settings/panel/MediaOutputPanel.java b/src/com/android/settings/panel/MediaOutputPanel.java new file mode 100644 index 0000000000..c42906d130 --- /dev/null +++ b/src/com/android/settings/panel/MediaOutputPanel.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2019 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.panel; + +import static com.android.settings.media.MediaOutputSlice.MEDIA_PACKAGE_NAME; +import static com.android.settings.slices.CustomSliceRegistry.MEDIA_OUTPUT_SLICE_URI; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; + +import com.android.settings.R; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents the Media output Panel. + * + * <p> + * Displays Media output item + * </p> + */ +public class MediaOutputPanel implements PanelContent { + + private final Context mContext; + private final String mPackageName; + + public static MediaOutputPanel create(Context context, String packageName) { + return new MediaOutputPanel(context, packageName); + } + + private MediaOutputPanel(Context context, String packageName) { + mContext = context.getApplicationContext(); + mPackageName = packageName; + } + + @Override + public CharSequence getTitle() { + return mContext.getText(R.string.media_output_panel_title); + } + + @Override + public List<Uri> getSlices() { + final List<Uri> uris = new ArrayList<>(); + MEDIA_OUTPUT_SLICE_URI = + MEDIA_OUTPUT_SLICE_URI + .buildUpon() + .clearQuery() + .appendQueryParameter(MEDIA_PACKAGE_NAME, mPackageName) + .build(); + uris.add(MEDIA_OUTPUT_SLICE_URI); + return uris; + } + + @Override + public Intent getSeeMoreIntent() { + return null; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.PANEL_MEDIA_OUTPUT; + } +} diff --git a/src/com/android/settings/panel/NfcPanel.java b/src/com/android/settings/panel/NfcPanel.java new file mode 100644 index 0000000000..c1e15e8406 --- /dev/null +++ b/src/com/android/settings/panel/NfcPanel.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2019 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.panel; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; + +import com.android.settings.R; +import com.android.settings.SubSettings; +import com.android.settings.connecteddevice.AdvancedConnectedDeviceDashboardFragment; +import com.android.settings.slices.CustomSliceRegistry; +import com.android.settings.slices.SliceBuilderUtils; + +import java.util.ArrayList; +import java.util.List; + +public class NfcPanel implements PanelContent { + + private final Context mContext; + + public static NfcPanel create(Context context) { + return new NfcPanel(context); + } + + private NfcPanel(Context context) { + mContext = context.getApplicationContext(); + } + + @Override + public CharSequence getTitle() { + return mContext.getText(R.string.nfc_quick_toggle_title); + } + + @Override + public List<Uri> getSlices() { + final List<Uri> uris = new ArrayList<>(); + uris.add(CustomSliceRegistry.NFC_SLICE_URI); + return uris; + } + + @Override + public Intent getSeeMoreIntent() { + final String screenTitle = + mContext.getText(R.string.connected_device_connections_title).toString(); + Intent intent = SliceBuilderUtils.buildSearchResultPageIntent(mContext, + AdvancedConnectedDeviceDashboardFragment.class.getName(), + null /* key */, + screenTitle, + SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY); + intent.setClassName(mContext.getPackageName(), SubSettings.class.getName()); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + return intent; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.PANEL_NFC; + } +} diff --git a/src/com/android/settings/panel/PanelContent.java b/src/com/android/settings/panel/PanelContent.java new file mode 100644 index 0000000000..496bac3f12 --- /dev/null +++ b/src/com/android/settings/panel/PanelContent.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2018 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.panel; + +import android.content.Intent; +import android.net.Uri; + +import com.android.settingslib.core.instrumentation.Instrumentable; + +import java.util.List; + +/** + * Represents the data class needed to create a Settings Panel. See {@link PanelFragment}. + */ +public interface PanelContent extends Instrumentable { + + /** + * @return a string for the title of the Panel. + */ + CharSequence getTitle(); + + /** + * @return an ordered list of the Slices to be displayed in the Panel. The first item in the + * list is shown on top of the Panel. + */ + List<Uri> getSlices(); + + + /** + * @return an {@link Intent} to the full content in Settings that is summarized by the Panel. + * + * <p> + * For example, for the connectivity panel you would intent to the Network & Internet page. + * </p> + */ + Intent getSeeMoreIntent(); +} diff --git a/src/com/android/settings/search/SearchIndexableResources.java b/src/com/android/settings/panel/PanelFeatureProvider.java index 5a0a131af6..85e098d603 100644 --- a/src/com/android/settings/search/SearchIndexableResources.java +++ b/src/com/android/settings/panel/PanelFeatureProvider.java @@ -14,16 +14,15 @@ * limitations under the License. */ -package com.android.settings.search; +package com.android.settings.panel; -import java.util.Collection; +import android.content.Context; -public interface SearchIndexableResources { +public interface PanelFeatureProvider { /** - * Returns a collection of classes that should be indexed for search. - * - * Each class should have the SEARCH_INDEX_DATA_PROVIDER public static member. + * Returns {@link PanelContent} as specified by the {@param panelType}, and + * {@param mediaPackageName}. */ - Collection<Class> getProviderValues(); + PanelContent getPanel(Context context, String panelType, String mediaPackageName); } diff --git a/src/com/android/settings/panel/PanelFeatureProviderImpl.java b/src/com/android/settings/panel/PanelFeatureProviderImpl.java new file mode 100644 index 0000000000..e6b0a23188 --- /dev/null +++ b/src/com/android/settings/panel/PanelFeatureProviderImpl.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2018 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.panel; + +import static com.android.settingslib.media.MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT; + +import android.content.Context; +import android.provider.Settings; + +public class PanelFeatureProviderImpl implements PanelFeatureProvider { + + @Override + public PanelContent getPanel(Context context, String panelType, String mediaPackageName) { + if (context == null) { + return null; + } + + switch (panelType) { + case Settings.Panel.ACTION_INTERNET_CONNECTIVITY: + return InternetConnectivityPanel.create(context); + case ACTION_MEDIA_OUTPUT: + return MediaOutputPanel.create(context, mediaPackageName); + case Settings.Panel.ACTION_NFC: + return NfcPanel.create(context); + case Settings.Panel.ACTION_WIFI: + return WifiPanel.create(context); + case Settings.Panel.ACTION_VOLUME: + return VolumePanel.create(context); + } + + throw new IllegalStateException("No matching panel for: " + panelType); + } +} diff --git a/src/com/android/settings/panel/PanelFragment.java b/src/com/android/settings/panel/PanelFragment.java new file mode 100644 index 0000000000..79d1ac592d --- /dev/null +++ b/src/com/android/settings/panel/PanelFragment.java @@ -0,0 +1,351 @@ +/* + * Copyright (C) 2018 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.panel; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.app.settings.SettingsEnums; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.animation.DecelerateInterpolator; +import android.widget.Button; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.lifecycle.LiveData; +import androidx.slice.Slice; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.slice.SliceMetadata; +import androidx.slice.widget.SliceLiveData; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.settings.R; +import com.android.settings.overlay.FeatureFactory; +import com.android.settings.panel.PanelLoggingContract.PanelClosedKeys; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; +import com.google.android.setupdesign.DividerItemDecoration; + +import java.util.ArrayList; +import java.util.List; + +public class PanelFragment extends Fragment { + + private static final String TAG = "PanelFragment"; + + /** + * Duration of the animation entering the screen, in milliseconds. + */ + private static final int DURATION_ANIMATE_PANEL_EXPAND_MS = 250; + + /** + * Duration of the animation exiting the screen, in milliseconds. + */ + private static final int DURATION_ANIMATE_PANEL_COLLAPSE_MS = 200; + + /** + * Duration of timeout waiting for Slice data to bind, in milliseconds. + */ + private static final int DURATION_SLICE_BINDING_TIMEOUT_MS = 250; + + private View mLayoutView; + private TextView mTitleView; + private Button mSeeMoreButton; + private Button mDoneButton; + private RecyclerView mPanelSlices; + + private PanelContent mPanel; + private MetricsFeatureProvider mMetricsProvider; + private String mPanelClosedKey; + + private final List<LiveData<Slice>> mSliceLiveData = new ArrayList<>(); + + @VisibleForTesting + PanelSlicesLoaderCountdownLatch mPanelSlicesLoaderCountdownLatch; + + private ViewTreeObserver.OnPreDrawListener mOnPreDrawListener = () -> { + return false; + }; + + private final ViewTreeObserver.OnGlobalLayoutListener mOnGlobalLayoutListener = + new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + animateIn(); + if (mPanelSlices != null) { + mPanelSlices.getViewTreeObserver().removeOnGlobalLayoutListener(this); + } + } + }; + + private PanelSlicesAdapter mAdapter; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + mLayoutView = inflater.inflate(R.layout.panel_layout, container, false); + createPanelContent(); + return mLayoutView; + } + + /** + * Animate the old panel out from the screen, then update the panel with new content once the + * animation is done. + * <p> + * Takes the entire panel and animates out from behind the navigation bar. + * <p> + * Call createPanelContent() once animation end. + */ + void updatePanelWithAnimation() { + final View panelContent = mLayoutView.findViewById(R.id.panel_container); + final AnimatorSet animatorSet = buildAnimatorSet(mLayoutView, + 0.0f /* startY */, panelContent.getHeight() /* endY */, + 1.0f /* startAlpha */, 0.0f /* endAlpha */, + DURATION_ANIMATE_PANEL_COLLAPSE_MS); + + final ValueAnimator animator = new ValueAnimator(); + animator.setFloatValues(0.0f, 1.0f); + animatorSet.play(animator); + animatorSet.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + createPanelContent(); + } + }); + animatorSet.start(); + } + + private void createPanelContent() { + final FragmentActivity activity = getActivity(); + if (mLayoutView == null) { + activity.finish(); + } + + mPanelSlices = mLayoutView.findViewById(R.id.panel_parent_layout); + mSeeMoreButton = mLayoutView.findViewById(R.id.see_more); + mDoneButton = mLayoutView.findViewById(R.id.done); + mTitleView = mLayoutView.findViewById(R.id.panel_title); + + // Make the panel layout gone here, to avoid janky animation when updating from old panel. + // We will make it visible once the panel is ready to load. + mPanelSlices.setVisibility(View.GONE); + + final Bundle arguments = getArguments(); + final String panelType = + arguments.getString(SettingsPanelActivity.KEY_PANEL_TYPE_ARGUMENT); + final String callingPackageName = + arguments.getString(SettingsPanelActivity.KEY_CALLING_PACKAGE_NAME); + final String mediaPackageName = + arguments.getString(SettingsPanelActivity.KEY_MEDIA_PACKAGE_NAME); + + // TODO (b/124399577) transform interface to take a context and bundle. + mPanel = FeatureFactory.getFactory(activity) + .getPanelFeatureProvider() + .getPanel(activity, panelType, mediaPackageName); + + if (mPanel == null) { + activity.finish(); + } + + mMetricsProvider = FeatureFactory.getFactory(activity).getMetricsFeatureProvider(); + + mPanelSlices.setLayoutManager(new LinearLayoutManager((activity))); + + // Add predraw listener to remove the animation and while we wait for Slices to load. + mLayoutView.getViewTreeObserver().addOnPreDrawListener(mOnPreDrawListener); + + // Start loading Slices. When finished, the Panel will animate in. + loadAllSlices(); + + mTitleView.setText(mPanel.getTitle()); + mSeeMoreButton.setOnClickListener(getSeeMoreListener()); + mDoneButton.setOnClickListener(getCloseListener()); + + // If getSeeMoreIntent() is null, hide the mSeeMoreButton. + if (mPanel.getSeeMoreIntent() == null) { + mSeeMoreButton.setVisibility(View.GONE); + } + + // Log panel opened. + mMetricsProvider.action( + 0 /* attribution */, + SettingsEnums.PAGE_VISIBLE /* opened panel - Action */, + mPanel.getMetricsCategory(), + callingPackageName, + 0 /* value */); + } + + private void loadAllSlices() { + mSliceLiveData.clear(); + final List<Uri> sliceUris = mPanel.getSlices(); + mPanelSlicesLoaderCountdownLatch = new PanelSlicesLoaderCountdownLatch(sliceUris.size()); + + for (Uri uri : sliceUris) { + final LiveData<Slice> sliceLiveData = SliceLiveData.fromUri(getActivity(), uri); + + // Add slice first to make it in order. Will remove it later if there's an error. + mSliceLiveData.add(sliceLiveData); + + sliceLiveData.observe(getViewLifecycleOwner(), slice -> { + // If the Slice has already loaded, do nothing. + if (mPanelSlicesLoaderCountdownLatch.isSliceLoaded(uri)) { + return; + } + + /** + * Watching for the {@link Slice} to load. + * <p> + * If the Slice comes back {@code null} or with the Error attribute, remove the + * Slice data from the list, and mark the Slice as loaded. + * <p> + * If the Slice has come back fully loaded, then mark the Slice as loaded. No + * other actions required since we already have the Slice data in the list. + * <p> + * If the Slice does not match the above condition, we will still want to mark + * it as loaded after 250ms timeout to avoid delay showing up the panel for + * too long. Since we are still having the Slice data in the list, the Slice + * will show up later once it is loaded. + */ + final SliceMetadata metadata = SliceMetadata.from(getActivity(), slice); + if (slice == null || metadata.isErrorSlice()) { + mSliceLiveData.remove(sliceLiveData); + mPanelSlicesLoaderCountdownLatch.markSliceLoaded(uri); + } else if (metadata.getLoadingState() == SliceMetadata.LOADED_ALL) { + mPanelSlicesLoaderCountdownLatch.markSliceLoaded(uri); + } else { + Handler handler = new Handler(); + handler.postDelayed(() -> { + mPanelSlicesLoaderCountdownLatch.markSliceLoaded(uri); + loadPanelWhenReady(); + }, DURATION_SLICE_BINDING_TIMEOUT_MS); + } + + loadPanelWhenReady(); + }); + } + } + + /** + * When all of the Slices have loaded for the first time, then we can setup the + * {@link RecyclerView}. + * <p> + * When the Recyclerview has been laid out, we can begin the animation with the + * {@link mOnGlobalLayoutListener}, which calls {@link #animateIn()}. + */ + private void loadPanelWhenReady() { + if (mPanelSlicesLoaderCountdownLatch.isPanelReadyToLoad()) { + mAdapter = new PanelSlicesAdapter( + this, mSliceLiveData, mPanel.getMetricsCategory()); + mPanelSlices.setAdapter(mAdapter); + mPanelSlices.getViewTreeObserver() + .addOnGlobalLayoutListener(mOnGlobalLayoutListener); + mPanelSlices.setVisibility(View.VISIBLE); + + DividerItemDecoration itemDecoration = new DividerItemDecoration(getActivity()); + itemDecoration + .setDividerCondition(DividerItemDecoration.DIVIDER_CONDITION_BOTH); + mPanelSlices.addItemDecoration(itemDecoration); + } + } + + /** + * Animate a Panel onto the screen. + * <p> + * Takes the entire panel and animates in from behind the navigation bar. + * <p> + * Relies on the Panel being having a fixed height to begin the animation. + */ + private void animateIn() { + final View panelContent = mLayoutView.findViewById(R.id.panel_container); + final AnimatorSet animatorSet = buildAnimatorSet(mLayoutView, + panelContent.getHeight() /* startY */, 0.0f /* endY */, + 0.0f /* startAlpha */, 1.0f /* endAlpha */, + DURATION_ANIMATE_PANEL_EXPAND_MS); + final ValueAnimator animator = new ValueAnimator(); + animator.setFloatValues(0.0f, 1.0f); + animatorSet.play(animator); + animatorSet.start(); + // Remove the predraw listeners on the Panel. + mLayoutView.getViewTreeObserver().removeOnPreDrawListener(mOnPreDrawListener); + } + + /** + * Build an {@link AnimatorSet} to animate the Panel, {@param parentView} in or out of the + * screen, based on the positional parameters {@param startY}, {@param endY}, the parameters + * for alpha changes {@param startAlpha}, {@param endAlpha}, and the {@param duration} in + * milliseconds. + */ + @NonNull + private static AnimatorSet buildAnimatorSet(@NonNull View parentView, float startY, float endY, + float startAlpha, float endAlpha, int duration) { + final View sheet = parentView.findViewById(R.id.panel_container); + final AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.setDuration(duration); + animatorSet.setInterpolator(new DecelerateInterpolator()); + animatorSet.playTogether( + ObjectAnimator.ofFloat(sheet, View.TRANSLATION_Y, startY, endY), + ObjectAnimator.ofFloat(sheet, View.ALPHA, startAlpha,endAlpha)); + return animatorSet; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + + if (TextUtils.isEmpty(mPanelClosedKey)) { + mPanelClosedKey = PanelClosedKeys.KEY_OTHERS; + } + + mMetricsProvider.action( + 0 /* attribution */, + SettingsEnums.PAGE_HIDE, + mPanel.getMetricsCategory(), + mPanelClosedKey, + 0 /* value */); + } + + @VisibleForTesting + View.OnClickListener getSeeMoreListener() { + return (v) -> { + mPanelClosedKey = PanelClosedKeys.KEY_SEE_MORE; + final FragmentActivity activity = getActivity(); + activity.startActivityForResult(mPanel.getSeeMoreIntent(), 0); + activity.finish(); + }; + } + + @VisibleForTesting + View.OnClickListener getCloseListener() { + return (v) -> { + mPanelClosedKey = PanelClosedKeys.KEY_DONE; + getActivity().finish(); + }; + } +} diff --git a/src/com/android/settings/panel/PanelLoggingContract.java b/src/com/android/settings/panel/PanelLoggingContract.java new file mode 100644 index 0000000000..e6e3012abe --- /dev/null +++ b/src/com/android/settings/panel/PanelLoggingContract.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2019 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.panel; + +/** + * Simple contract class to track keys in Panel logging. + * + * <p> + * Constants should only be removed if underlying panel, or use case is removed. + * </p> + */ +public class PanelLoggingContract { + + /** + * Keys tracking different ways users exit Panels. + */ + interface PanelClosedKeys { + /** + * The user clicked the See More button linking deeper into Settings. + */ + String KEY_SEE_MORE = "see_more"; + + /** + * The user clicked the Done button, closing the Panel. + */ + String KEY_DONE = "done"; + + /** + * The user closed the panel by other ways, for example: clicked outside of dialog, tapping + * on back button, etc. + */ + String KEY_OTHERS = "others"; + } +} diff --git a/src/com/android/settings/panel/PanelSlicesAdapter.java b/src/com/android/settings/panel/PanelSlicesAdapter.java new file mode 100644 index 0000000000..0f525cc39d --- /dev/null +++ b/src/com/android/settings/panel/PanelSlicesAdapter.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2018 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.panel; + +import static com.android.settings.slices.CustomSliceRegistry.MEDIA_OUTPUT_INDICATOR_SLICE_URI; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.lifecycle.LiveData; +import androidx.recyclerview.widget.RecyclerView; +import androidx.slice.Slice; +import androidx.slice.widget.SliceView; + +import com.android.settings.R; +import com.android.settings.overlay.FeatureFactory; + +import com.google.android.setupdesign.DividerItemDecoration; + +import java.util.ArrayList; +import java.util.List; + +/** + * RecyclerView adapter for Slices in Settings Panels. + */ +public class PanelSlicesAdapter + extends RecyclerView.Adapter<PanelSlicesAdapter.SliceRowViewHolder> { + + /** + * Maximum number of slices allowed on the panel view. + */ + @VisibleForTesting + static final int MAX_NUM_OF_SLICES = 5; + + private final List<LiveData<Slice>> mSliceLiveData; + private final int mMetricsCategory; + private final PanelFragment mPanelFragment; + + public PanelSlicesAdapter( + PanelFragment fragment, List<LiveData<Slice>> sliceLiveData, int metricsCategory) { + mPanelFragment = fragment; + mSliceLiveData = new ArrayList<>(sliceLiveData); + mMetricsCategory = metricsCategory; + } + + @NonNull + @Override + public SliceRowViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) { + final Context context = viewGroup.getContext(); + final LayoutInflater inflater = LayoutInflater.from(context); + final View view = inflater.inflate(R.layout.panel_slice_row, viewGroup, false); + + return new SliceRowViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull SliceRowViewHolder sliceRowViewHolder, int position) { + sliceRowViewHolder.onBind(mSliceLiveData.get(position)); + } + + /** + * Return the number of available items in the adapter with max number of slices enforced. + */ + @Override + public int getItemCount() { + return Math.min(mSliceLiveData.size(), MAX_NUM_OF_SLICES); + } + + /** + * Return the available data from the adapter. If the number of Slices over the max number + * allowed, the list will only have the first MAX_NUM_OF_SLICES of slices. + */ + @VisibleForTesting + List<LiveData<Slice>> getData() { + return mSliceLiveData.subList(0, getItemCount()); + } + + /** + * ViewHolder for binding Slices to SliceViews. + */ + public class SliceRowViewHolder extends RecyclerView.ViewHolder + implements DividerItemDecoration.DividedViewHolder { + + private boolean mDividerAllowedAbove = true; + + @VisibleForTesting + final SliceView sliceView; + + public SliceRowViewHolder(View view) { + super(view); + sliceView = view.findViewById(R.id.slice_view); + sliceView.setMode(SliceView.MODE_LARGE); + sliceView.showTitleItems(true); + } + + public void onBind(LiveData<Slice> sliceLiveData) { + sliceLiveData.observe(mPanelFragment.getViewLifecycleOwner(), sliceView); + + // Do not show the divider above media devices switcher slice per request + final Slice slice = sliceLiveData.getValue(); + if (slice != null && slice.getUri().equals(MEDIA_OUTPUT_INDICATOR_SLICE_URI)) { + mDividerAllowedAbove = false; + } + + // Log Panel interaction + sliceView.setOnSliceActionListener( + ((eventInfo, sliceItem) -> { + FeatureFactory.getFactory(sliceView.getContext()) + .getMetricsFeatureProvider() + .action(0 /* attribution */, + SettingsEnums.ACTION_PANEL_INTERACTION, + mMetricsCategory, + sliceLiveData.getValue().getUri().getLastPathSegment() + /* log key */, + eventInfo.actionType /* value */); + }) + ); + } + + @Override + public boolean isDividerAllowedAbove() { + return mDividerAllowedAbove; + } + + @Override + public boolean isDividerAllowedBelow() { + return true; + } + } +} diff --git a/src/com/android/settings/panel/PanelSlicesLoaderCountdownLatch.java b/src/com/android/settings/panel/PanelSlicesLoaderCountdownLatch.java new file mode 100644 index 0000000000..6137d6c564 --- /dev/null +++ b/src/com/android/settings/panel/PanelSlicesLoaderCountdownLatch.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2019 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.panel; + +import android.net.Uri; + +import androidx.slice.Slice; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.CountDownLatch; + +/** + * Helper class to isolate the work tracking all of the {@link Slice Slices} being loaded. + * <p> + * Uses a {@link CountDownLatch} and a {@link Set} of Slices to track how many + * Slices have been loaded. A Slice can only be counted as being loaded a single time, even + * when they get updated later. + * <p> + * To use the class, pass the number of expected Slices to load into the constructor. For + * every Slice that loads, call {@link #markSliceLoaded(Uri)} with the corresponding + * {@link Uri}. Then check if all of the Slices have loaded with + * {@link #isPanelReadyToLoad()}, which will return {@code true} the first time after all + * Slices have loaded. + */ +public class PanelSlicesLoaderCountdownLatch { + private final Set<Uri> mLoadedSlices; + private final CountDownLatch mCountDownLatch; + private boolean slicesReadyToLoad = false; + + public PanelSlicesLoaderCountdownLatch(int countdownSize) { + mLoadedSlices = new HashSet<>(); + mCountDownLatch = new CountDownLatch(countdownSize); + } + + /** + * Checks if the {@param sliceUri} has been loaded: if not, then decrement the countdown + * latch, and if so, then do nothing. + */ + public void markSliceLoaded(Uri sliceUri) { + if (mLoadedSlices.contains(sliceUri)) { + return; + } + mLoadedSlices.add(sliceUri); + mCountDownLatch.countDown(); + } + + /** + * @return {@code true} if the Slice has already been loaded. + */ + public boolean isSliceLoaded(Uri uri) { + return mLoadedSlices.contains(uri); + } + + /** + * @return {@code true} when all Slices have loaded, and the Panel has not yet been loaded. + */ + public boolean isPanelReadyToLoad() { + /** + * Use {@link slicesReadyToLoad} to track whether or not the Panel has been loaded. We + * only want to animate the Panel a single time. + */ + if ((mCountDownLatch.getCount() == 0) && !slicesReadyToLoad) { + slicesReadyToLoad = true; + return true; + } + return false; + } +}
\ No newline at end of file diff --git a/src/com/android/settings/panel/SettingsPanelActivity.java b/src/com/android/settings/panel/SettingsPanelActivity.java new file mode 100644 index 0000000000..749a46e0cd --- /dev/null +++ b/src/com/android/settings/panel/SettingsPanelActivity.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2018 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.panel; + +import static com.android.settingslib.media.MediaOutputSliceConstants.EXTRA_PACKAGE_NAME; + +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.view.Gravity; +import android.view.Window; +import android.view.WindowManager; + +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.settings.R; + +/** + * Dialog Activity to host Settings Slices. + */ +public class SettingsPanelActivity extends FragmentActivity { + + private final String TAG = "panel_activity"; + + @VisibleForTesting + final Bundle mBundle = new Bundle(); + + /** + * Key specifying which Panel the app is requesting. + */ + public static final String KEY_PANEL_TYPE_ARGUMENT = "PANEL_TYPE_ARGUMENT"; + + /** + * Key specifying the package which requested the Panel. + */ + public static final String KEY_CALLING_PACKAGE_NAME = "PANEL_CALLING_PACKAGE_NAME"; + + /** + * Key specifying the package name for which the + */ + public static final String KEY_MEDIA_PACKAGE_NAME = "PANEL_MEDIA_PACKAGE_NAME"; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + createOrUpdatePanel(true /* shouldForceCreation */); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + setIntent(intent); + createOrUpdatePanel(false /* shouldForceCreation */); + } + + private void createOrUpdatePanel(boolean shouldForceCreation) { + final Intent callingIntent = getIntent(); + if (callingIntent == null) { + Log.e(TAG, "Null intent, closing Panel Activity"); + finish(); + return; + } + + // We will use it once media output switch panel support remote device. + final String mediaPackageName = callingIntent.getStringExtra(EXTRA_PACKAGE_NAME); + + mBundle.putString(KEY_PANEL_TYPE_ARGUMENT, callingIntent.getAction()); + mBundle.putString(KEY_CALLING_PACKAGE_NAME, getCallingPackage()); + mBundle.putString(KEY_MEDIA_PACKAGE_NAME, mediaPackageName); + + final FragmentManager fragmentManager = getSupportFragmentManager(); + final Fragment fragment = fragmentManager.findFragmentById(R.id.main_content); + + // If fragment already exists, we will need to update panel with animation. + if (!shouldForceCreation && fragment != null && fragment instanceof PanelFragment) { + final PanelFragment panelFragment = (PanelFragment) fragment; + panelFragment.setArguments(mBundle); + ((PanelFragment) fragment).updatePanelWithAnimation(); + } else { + setContentView(R.layout.settings_panel); + + // Move the window to the bottom of screen, and make it take up the entire screen width. + final Window window = getWindow(); + window.setGravity(Gravity.BOTTOM); + window.setLayout(WindowManager.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.WRAP_CONTENT); + final PanelFragment panelFragment = new PanelFragment(); + panelFragment.setArguments(mBundle); + fragmentManager.beginTransaction().add(R.id.main_content, panelFragment).commit(); + } + } +} diff --git a/src/com/android/settings/panel/VolumePanel.java b/src/com/android/settings/panel/VolumePanel.java new file mode 100644 index 0000000000..1a166bae5c --- /dev/null +++ b/src/com/android/settings/panel/VolumePanel.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2018 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.panel; + +import static com.android.settings.slices.CustomSliceRegistry.MEDIA_OUTPUT_INDICATOR_SLICE_URI; +import static com.android.settings.slices.CustomSliceRegistry.VOLUME_ALARM_URI; +import static com.android.settings.slices.CustomSliceRegistry.VOLUME_CALL_URI; +import static com.android.settings.slices.CustomSliceRegistry.VOLUME_MEDIA_URI; +import static com.android.settings.slices.CustomSliceRegistry.VOLUME_REMOTE_MEDIA_URI; +import static com.android.settings.slices.CustomSliceRegistry.VOLUME_RINGER_URI; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.provider.Settings; + +import com.android.settings.R; +import com.android.settings.notification.RemoteVolumePreferenceController; + +import java.util.ArrayList; +import java.util.List; + +public class VolumePanel implements PanelContent { + + private final Context mContext; + + public static VolumePanel create(Context context) { + return new VolumePanel(context); + } + + private VolumePanel(Context context) { + mContext = context.getApplicationContext(); + } + + @Override + public CharSequence getTitle() { + return mContext.getText(R.string.volume_connectivity_panel_title); + } + + @Override + public List<Uri> getSlices() { + final List<Uri> uris = new ArrayList<>(); + if (RemoteVolumePreferenceController.getActiveRemoteToken(mContext) != null) { + uris.add(VOLUME_REMOTE_MEDIA_URI); + } + uris.add(VOLUME_MEDIA_URI); + uris.add(MEDIA_OUTPUT_INDICATOR_SLICE_URI); + uris.add(VOLUME_CALL_URI); + uris.add(VOLUME_RINGER_URI); + uris.add(VOLUME_ALARM_URI); + return uris; + } + + @Override + public Intent getSeeMoreIntent() { + return new Intent(Settings.ACTION_SOUND_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.PANEL_VOLUME; + } +}
\ No newline at end of file diff --git a/src/com/android/settings/panel/WifiPanel.java b/src/com/android/settings/panel/WifiPanel.java new file mode 100644 index 0000000000..36ee1178d0 --- /dev/null +++ b/src/com/android/settings/panel/WifiPanel.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2019 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.panel; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; + +import com.android.settings.R; +import com.android.settings.SubSettings; +import com.android.settings.slices.CustomSliceRegistry; +import com.android.settings.slices.SliceBuilderUtils; +import com.android.settings.wifi.WifiSettings; + +import java.util.ArrayList; +import java.util.List; + +/** + * Panel data class for Wifi settings. + */ +public class WifiPanel implements PanelContent { + + private final Context mContext; + + public static WifiPanel create(Context context) { + return new WifiPanel(context); + } + + private WifiPanel(Context context) { + mContext = context.getApplicationContext(); + } + + @Override + public CharSequence getTitle() { + return mContext.getText(R.string.wifi_settings); + } + + @Override + public List<Uri> getSlices() { + final List<Uri> uris = new ArrayList<>(); + uris.add(CustomSliceRegistry.WIFI_SLICE_URI); + return uris; + } + + @Override + public Intent getSeeMoreIntent() { + final String screenTitle = + mContext.getText(R.string.wifi_settings).toString(); + final Intent intent = SliceBuilderUtils.buildSearchResultPageIntent(mContext, + WifiSettings.class.getName(), + null /* key */, + screenTitle, + SettingsEnums.WIFI); + intent.setClassName(mContext.getPackageName(), SubSettings.class.getName()); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + return intent; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.PANEL_WIFI; + } +} diff --git a/src/com/android/settings/password/BiometricFragment.java b/src/com/android/settings/password/BiometricFragment.java new file mode 100644 index 0000000000..66b665b337 --- /dev/null +++ b/src/com/android/settings/password/BiometricFragment.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2018 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.password; + +import android.app.Activity; +import android.app.admin.DevicePolicyManager; +import android.app.settings.SettingsEnums; +import android.content.DialogInterface; +import android.hardware.biometrics.BiometricConstants; +import android.hardware.biometrics.BiometricPrompt; +import android.hardware.biometrics.BiometricPrompt.AuthenticationCallback; +import android.hardware.biometrics.BiometricPrompt.AuthenticationResult; +import android.hardware.biometrics.IBiometricConfirmDeviceCredentialCallback; +import android.os.Bundle; +import android.os.CancellationSignal; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.android.internal.widget.LockPatternUtils; +import com.android.settings.R; +import com.android.settings.core.InstrumentedFragment; +import com.android.settings.overlay.FeatureFactory; + +import java.util.concurrent.Executor; + +/** + * A fragment that wraps the BiometricPrompt and manages its lifecycle. + */ +public class BiometricFragment extends InstrumentedFragment { + + private static final String TAG = "ConfirmDeviceCredential/BiometricFragment"; + + // Re-set by the application. Should be done upon orientation changes, etc + private Executor mClientExecutor; + private AuthenticationCallback mClientCallback; + + // Re-settable by the application. + private int mUserId; + + // Created/Initialized once and retained + private Bundle mBundle; + private BiometricPrompt mBiometricPrompt; + private CancellationSignal mCancellationSignal; + private boolean mAuthenticating; + + private AuthenticationCallback mAuthenticationCallback = + new AuthenticationCallback() { + @Override + public void onAuthenticationError(int error, @NonNull CharSequence message) { + mAuthenticating = false; + mClientExecutor.execute(() -> { + mClientCallback.onAuthenticationError(error, message); + }); + cleanup(); + } + + @Override + public void onAuthenticationSucceeded(AuthenticationResult result) { + mAuthenticating = false; + mClientExecutor.execute(() -> { + mClientCallback.onAuthenticationSucceeded(result); + }); + cleanup(); + } + }; + + private final DialogInterface.OnClickListener mNegativeButtonListener = + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + mAuthenticationCallback.onAuthenticationError( + BiometricConstants.BIOMETRIC_ERROR_NEGATIVE_BUTTON, + mBundle.getString(BiometricPrompt.KEY_NEGATIVE_TEXT)); + } + }; + + // TODO(b/123378871): Remove when moved. + private final IBiometricConfirmDeviceCredentialCallback mCancelCallback + = new IBiometricConfirmDeviceCredentialCallback.Stub() { + @Override + public void cancel() { + final Activity activity = getActivity(); + if (activity != null) { + activity.finish(); + } else { + Log.e(TAG, "Activity null!"); + } + } + }; + + /** + * @param bundle Bundle passed from {@link BiometricPrompt.Builder#buildIntent()} + * @return + */ + public static BiometricFragment newInstance(Bundle bundle) { + BiometricFragment biometricFragment = new BiometricFragment(); + biometricFragment.setArguments(bundle); + return biometricFragment; + } + + public void setCallbacks(Executor executor, AuthenticationCallback callback) { + mClientExecutor = executor; + mClientCallback = callback; + } + + public void setUser(int userId) { + mUserId = userId; + } + + public void cancel() { + if (mCancellationSignal != null) { + mCancellationSignal.cancel(); + } + cleanup(); + } + + private void cleanup() { + if (getActivity() != null) { + getActivity().getSupportFragmentManager().beginTransaction().remove(this).commit(); + } + } + + boolean isAuthenticating() { + return mAuthenticating; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setRetainInstance(true); + + mBundle = getArguments(); + final BiometricPrompt.Builder builder = new BiometricPrompt.Builder(getContext()) + .setTitle(mBundle.getString(BiometricPrompt.KEY_TITLE)) + .setUseDefaultTitle() // use default title if title is null/empty + .setFromConfirmDeviceCredential() + .setSubtitle(mBundle.getString(BiometricPrompt.KEY_SUBTITLE)) + .setDescription(mBundle.getString(BiometricPrompt.KEY_DESCRIPTION)) + .setConfirmationRequired( + mBundle.getBoolean(BiometricPrompt.KEY_REQUIRE_CONFIRMATION, true)); + + final LockPatternUtils lockPatternUtils = FeatureFactory.getFactory( + getContext()) + .getSecurityFeatureProvider() + .getLockPatternUtils(getContext()); + + switch (lockPatternUtils.getKeyguardStoredPasswordQuality(mUserId)) { + case DevicePolicyManager.PASSWORD_QUALITY_SOMETHING: + builder.setNegativeButton(getResources().getString( + R.string.confirm_device_credential_pattern), + mClientExecutor, mNegativeButtonListener); + break; + case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC: + case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX: + builder.setNegativeButton(getResources().getString( + R.string.confirm_device_credential_pin), + mClientExecutor, mNegativeButtonListener); + break; + case DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC: + case DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC: + case DevicePolicyManager.PASSWORD_QUALITY_COMPLEX: + case DevicePolicyManager.PASSWORD_QUALITY_MANAGED: + builder.setNegativeButton(getResources().getString( + R.string.confirm_device_credential_password), + mClientExecutor, mNegativeButtonListener); + break; + } + + mBiometricPrompt = builder.build(); + mCancellationSignal = new CancellationSignal(); + + // TODO: CC doesn't use crypto for now + mAuthenticating = true; + mBiometricPrompt.authenticateUser(mCancellationSignal, mClientExecutor, + mAuthenticationCallback, mUserId, mCancelCallback); + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.BIOMETRIC_FRAGMENT; + } +} + diff --git a/src/com/android/settings/password/ChooseLockGeneric.java b/src/com/android/settings/password/ChooseLockGeneric.java index 4af83f3c98..2a7de05719 100644 --- a/src/com/android/settings/password/ChooseLockGeneric.java +++ b/src/com/android/settings/password/ChooseLockGeneric.java @@ -18,19 +18,27 @@ package com.android.settings.password; import static android.app.admin.DevicePolicyManager.ACTION_SET_NEW_PARENT_PROFILE_PASSWORD; import static android.app.admin.DevicePolicyManager.ACTION_SET_NEW_PASSWORD; +import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_HIGH; +import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_LOW; +import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_MEDIUM; +import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_NONE; import static com.android.settings.password.ChooseLockPassword.ChooseLockPasswordFragment.RESULT_FINISHED; +import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_CALLER_APP_NAME; +import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_IS_CALLING_APP_ADMIN; +import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_REQUESTED_MIN_COMPLEXITY; import android.accessibilityservice.AccessibilityServiceInfo; import android.app.Activity; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.Fragment; -import android.app.FragmentManager; import android.app.admin.DevicePolicyManager; +import android.app.admin.DevicePolicyManager.PasswordComplexity; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; import android.content.pm.UserInfo; +import android.hardware.face.Face; +import android.hardware.face.FaceManager; import android.hardware.fingerprint.Fingerprint; import android.hardware.fingerprint.FingerprintManager; import android.hardware.fingerprint.FingerprintManager.RemovalCallback; @@ -38,17 +46,20 @@ import android.os.Bundle; import android.os.UserHandle; import android.os.UserManager; import android.os.storage.StorageManager; -import android.security.KeyStore; -import androidx.annotation.StringRes; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; import android.text.TextUtils; import android.util.EventLog; import android.util.Log; import android.view.accessibility.AccessibilityManager; import android.widget.TextView; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.annotation.StringRes; +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + import com.android.internal.widget.LockPatternUtils; import com.android.settings.EncryptionInterstitial; import com.android.settings.EventLogTags; @@ -56,12 +67,15 @@ import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.SettingsPreferenceFragment; import com.android.settings.Utils; +import com.android.settings.biometrics.BiometricEnrollActivity; +import com.android.settings.biometrics.BiometricEnrollBase; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; -import com.android.settings.fingerprint.FingerprintEnrollBase; -import com.android.settings.fingerprint.FingerprintEnrollFindSensor; -import com.android.settingslib.RestrictedLockUtils; +import com.android.settings.search.SearchFeatureProvider; import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; +import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.RestrictedPreference; +import com.android.settingslib.widget.FooterPreference; +import com.android.settingslib.widget.FooterPreferenceMixinCompat; import java.util.List; @@ -72,12 +86,6 @@ public class ChooseLockGeneric extends SettingsActivity { public Intent getIntent() { Intent modIntent = new Intent(super.getIntent()); modIntent.putExtra(EXTRA_SHOW_FRAGMENT, getFragmentClass().getName()); - - String action = modIntent.getAction(); - if (ACTION_SET_NEW_PASSWORD.equals(action) - || ACTION_SET_NEW_PARENT_PROFILE_PASSWORD.equals(action)) { - modIntent.putExtra(EXTRA_HIDE_DRAWER, true); - } return modIntent; } @@ -97,14 +105,12 @@ public class ChooseLockGeneric extends SettingsActivity { public static class ChooseLockGenericFragment extends SettingsPreferenceFragment { private static final String TAG = "ChooseLockGenericFragment"; - private static final int MIN_PASSWORD_LENGTH = 4; private static final String KEY_SKIP_FINGERPRINT = "unlock_skip_fingerprint"; + private static final String KEY_SKIP_FACE = "unlock_skip_face"; private static final String PASSWORD_CONFIRMED = "password_confirmed"; private static final String WAITING_FOR_CONFIRMATION = "waiting_for_confirmation"; public static final String MINIMUM_QUALITY_KEY = "minimum_quality"; public static final String HIDE_DISABLED_PREFS = "hide_disabled_prefs"; - public static final String ENCRYPT_REQUESTED_QUALITY = "encrypt_requested_quality"; - public static final String ENCRYPT_REQUESTED_DISABLED = "encrypt_requested_disabled"; public static final String TAG_FRP_WARNING_DIALOG = "frp_warning_dialog"; /** @@ -128,37 +134,54 @@ public class ChooseLockGeneric extends SettingsActivity { */ public static final String EXTRA_CHOOSE_LOCK_GENERIC_EXTRAS = "choose_lock_generic_extras"; - private static final int CONFIRM_EXISTING_REQUEST = 100; - private static final int ENABLE_ENCRYPTION_REQUEST = 101; - private static final int CHOOSE_LOCK_REQUEST = 102; - private static final int CHOOSE_LOCK_BEFORE_FINGERPRINT_REQUEST = 103; - private static final int SKIP_FINGERPRINT_REQUEST = 104; + @VisibleForTesting + static final int CONFIRM_EXISTING_REQUEST = 100; + @VisibleForTesting + static final int ENABLE_ENCRYPTION_REQUEST = 101; + @VisibleForTesting + static final int CHOOSE_LOCK_REQUEST = 102; + @VisibleForTesting + static final int CHOOSE_LOCK_BEFORE_BIOMETRIC_REQUEST = 103; + @VisibleForTesting + static final int SKIP_FINGERPRINT_REQUEST = 104; private ChooseLockSettingsHelper mChooseLockSettingsHelper; private DevicePolicyManager mDPM; - private KeyStore mKeyStore; private boolean mHasChallenge = false; private long mChallenge; private boolean mPasswordConfirmed = false; private boolean mWaitingForConfirmation = false; - private int mEncryptionRequestQuality; - private boolean mEncryptionRequestDisabled; private boolean mForChangeCredRequiredForBoot = false; - private String mUserPassword; + private byte[] mUserPassword; private LockPatternUtils mLockPatternUtils; private FingerprintManager mFingerprintManager; + private FaceManager mFaceManager; private int mUserId; - private boolean mHideDrawer = false; private ManagedLockPasswordProvider mManagedPasswordProvider; private boolean mIsSetNewPassword = false; private UserManager mUserManager; private ChooseLockGenericController mController; + /** + * From intent extra {@link ChooseLockSettingsHelper#EXTRA_KEY_REQUESTED_MIN_COMPLEXITY}. + */ + @PasswordComplexity private int mRequestedMinComplexity; + + /** From intent extra {@link ChooseLockSettingsHelper#EXTRA_KEY_CALLER_APP_NAME}. */ + private String mCallerAppName = null; + + /** + * The value from the intent extra {@link + * ChooseLockSettingsHelper#EXTRA_KEY_IS_CALLING_APP_ADMIN}. + */ + private boolean mIsCallingAppAdmin; + protected boolean mForFingerprint = false; + protected boolean mForFace = false; @Override public int getMetricsCategory() { - return MetricsEvent.CHOOSE_LOCK_GENERIC; + return SettingsEnums.CHOOSE_LOCK_GENERIC; } @Override @@ -166,14 +189,15 @@ public class ChooseLockGeneric extends SettingsActivity { super.onCreate(savedInstanceState); final Activity activity = getActivity(); if (!Utils.isDeviceProvisioned(activity) && !canRunBeforeDeviceProvisioned()) { + Log.i(TAG, "Refusing to start because device is not provisioned"); activity.finish(); return; } String chooseLockAction = getActivity().getIntent().getAction(); mFingerprintManager = Utils.getFingerprintManagerOrNull(getActivity()); + mFaceManager = Utils.getFaceManagerOrNull(getActivity()); mDPM = (DevicePolicyManager) getSystemService(Context.DEVICE_POLICY_SERVICE); - mKeyStore = KeyStore.getInstance(); mChooseLockSettingsHelper = new ChooseLockSettingsHelper(this.getActivity()); mLockPatternUtils = new LockPatternUtils(getActivity()); mIsSetNewPassword = ACTION_SET_NEW_PARENT_PROFILE_PASSWORD.equals(chooseLockAction) @@ -184,10 +208,9 @@ public class ChooseLockGeneric extends SettingsActivity { .getBooleanExtra(CONFIRM_CREDENTIALS, true); if (getActivity() instanceof ChooseLockGeneric.InternalActivity) { mPasswordConfirmed = !confirmCredentials; - mUserPassword = getActivity().getIntent().getStringExtra( + mUserPassword = getActivity().getIntent().getByteArrayExtra( ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD); } - mHideDrawer = getActivity().getIntent().getBooleanExtra(EXTRA_HIDE_DRAWER, false); mHasChallenge = getActivity().getIntent().getBooleanExtra( ChooseLockSettingsHelper.EXTRA_KEY_HAS_CHALLENGE, false); @@ -195,6 +218,14 @@ public class ChooseLockGeneric extends SettingsActivity { ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE, 0); mForFingerprint = getActivity().getIntent().getBooleanExtra( ChooseLockSettingsHelper.EXTRA_KEY_FOR_FINGERPRINT, false); + mForFace = getActivity().getIntent().getBooleanExtra( + ChooseLockSettingsHelper.EXTRA_KEY_FOR_FACE, false); + mRequestedMinComplexity = getActivity().getIntent() + .getIntExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY, PASSWORD_COMPLEXITY_NONE); + mCallerAppName = + getActivity().getIntent().getStringExtra(EXTRA_KEY_CALLER_APP_NAME); + mIsCallingAppAdmin = getActivity().getIntent() + .getBooleanExtra(EXTRA_KEY_IS_CALLING_APP_ADMIN, /* defValue= */ false); mForChangeCredRequiredForBoot = getArguments() != null && getArguments().getBoolean( ChooseLockSettingsHelper.EXTRA_KEY_FOR_CHANGE_CRED_REQUIRED_FOR_BOOT); mUserManager = UserManager.get(getActivity()); @@ -202,11 +233,8 @@ public class ChooseLockGeneric extends SettingsActivity { if (savedInstanceState != null) { mPasswordConfirmed = savedInstanceState.getBoolean(PASSWORD_CONFIRMED); mWaitingForConfirmation = savedInstanceState.getBoolean(WAITING_FOR_CONFIRMATION); - mEncryptionRequestQuality = savedInstanceState.getInt(ENCRYPT_REQUESTED_QUALITY); - mEncryptionRequestDisabled = savedInstanceState.getBoolean( - ENCRYPT_REQUESTED_DISABLED); if (mUserPassword == null) { - mUserPassword = savedInstanceState.getString( + mUserPassword = savedInstanceState.getByteArray( ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD); } } @@ -220,7 +248,8 @@ public class ChooseLockGeneric extends SettingsActivity { UserManager.get(getActivity()), getArguments(), getActivity().getIntent().getExtras()).getIdentifier(); - mController = new ChooseLockGenericController(getContext(), mUserId); + mController = new ChooseLockGenericController( + getContext(), mUserId, mRequestedMinComplexity, mLockPatternUtils); if (ACTION_SET_NEW_PASSWORD.equals(chooseLockAction) && UserManager.get(getActivity()).isManagedProfile(mUserId) && mLockPatternUtils.isSeparateProfileChallengeEnabled(mUserId)) { @@ -258,6 +287,10 @@ public class ChooseLockGeneric extends SettingsActivity { return false; } + protected Class<? extends ChooseLockGeneric.InternalActivity> getInternalActivityClass() { + return ChooseLockGeneric.InternalActivity.class; + } + protected void addHeaderView() { if (mForFingerprint) { setHeaderView(R.layout.choose_lock_generic_fingerprint_header); @@ -265,6 +298,12 @@ public class ChooseLockGeneric extends SettingsActivity { ((TextView) getHeaderView().findViewById(R.id.fingerprint_header_description)) .setText(R.string.fingerprint_unlock_title); } + } else if (mForFace) { + setHeaderView(R.layout.choose_lock_generic_face_header); + if (mIsSetNewPassword) { + ((TextView) getHeaderView().findViewById(R.id.face_header_description)) + .setText(R.string.face_unlock_title); + } } } @@ -277,13 +316,16 @@ public class ChooseLockGeneric extends SettingsActivity { // unlock method to an insecure one showFactoryResetProtectionWarningDialog(key); return true; - } else if (KEY_SKIP_FINGERPRINT.equals(key)) { + } else if (KEY_SKIP_FINGERPRINT.equals(key) || KEY_SKIP_FACE.equals(key)) { Intent chooseLockGenericIntent = new Intent(getActivity(), - ChooseLockGeneric.InternalActivity.class); + getInternalActivityClass()); chooseLockGenericIntent.setAction(getIntent().getAction()); // Forward the target user id to ChooseLockGeneric. chooseLockGenericIntent.putExtra(Intent.EXTRA_USER_ID, mUserId); chooseLockGenericIntent.putExtra(CONFIRM_CREDENTIALS, !mPasswordConfirmed); + chooseLockGenericIntent.putExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY, + mRequestedMinComplexity); + chooseLockGenericIntent.putExtra(EXTRA_KEY_CALLER_APP_NAME, mCallerAppName); if (mUserPassword != null) { chooseLockGenericIntent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD, mUserPassword); @@ -311,8 +353,6 @@ public class ChooseLockGeneric extends SettingsActivity { && LockPatternUtils.isDeviceEncryptionEnabled() && !LockPatternUtils.isFileEncryptionEnabled() && !dpm.getDoNotAskCredentialsOnBoot()) { - mEncryptionRequestQuality = quality; - mEncryptionRequestDisabled = disabled; // Get the intent that the encryption interstitial should start for creating // the new unlock method. Intent unlockMethodIntent = getIntentForUnlockMethod(quality); @@ -329,11 +369,12 @@ public class ChooseLockGeneric extends SettingsActivity { unlockMethodIntent); intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FINGERPRINT, mForFingerprint); - intent.putExtra(EXTRA_HIDE_DRAWER, mHideDrawer); + intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FACE, + mForFace); startActivityForResult( intent, mIsSetNewPassword && mHasChallenge - ? CHOOSE_LOCK_BEFORE_FINGERPRINT_REQUEST + ? CHOOSE_LOCK_BEFORE_BIOMETRIC_REQUEST : ENABLE_ENCRYPTION_REQUEST); } else { if (mForChangeCredRequiredForBoot) { @@ -351,10 +392,12 @@ public class ChooseLockGeneric extends SettingsActivity { mWaitingForConfirmation = false; if (requestCode == CONFIRM_EXISTING_REQUEST && resultCode == Activity.RESULT_OK) { mPasswordConfirmed = true; - mUserPassword = data.getStringExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD); + mUserPassword = data != null + ? data.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD) + : null; updatePreferencesOrFinish(false /* isRecreatingActivity */); if (mForChangeCredRequiredForBoot) { - if (!TextUtils.isEmpty(mUserPassword)) { + if (!(mUserPassword == null || mUserPassword.length == 0)) { maybeEnableEncryption( mLockPatternUtils.getKeyguardStoredPasswordQuality(mUserId), false); } else { @@ -376,9 +419,9 @@ public class ChooseLockGeneric extends SettingsActivity { finish(); } } - } else if (requestCode == CHOOSE_LOCK_BEFORE_FINGERPRINT_REQUEST - && resultCode == FingerprintEnrollBase.RESULT_FINISHED) { - Intent intent = getFindSensorIntent(getActivity()); + } else if (requestCode == CHOOSE_LOCK_BEFORE_BIOMETRIC_REQUEST + && resultCode == BiometricEnrollBase.RESULT_FINISHED) { + Intent intent = getBiometricEnrollIntent(getActivity()); if (data != null) { intent.putExtras(data.getExtras()); } @@ -392,6 +435,8 @@ public class ChooseLockGeneric extends SettingsActivity { resultCode == RESULT_FINISHED ? RESULT_OK : resultCode, data); finish(); } + } else if (requestCode == SearchFeatureProvider.REQUEST_CODE) { + return; } else { getActivity().setResult(Activity.RESULT_CANCELED); finish(); @@ -401,8 +446,11 @@ public class ChooseLockGeneric extends SettingsActivity { } } - protected Intent getFindSensorIntent(Context context) { - return new Intent(context, FingerprintEnrollFindSensor.class); + protected Intent getBiometricEnrollIntent(Context context) { + final Intent intent = + new Intent(context, BiometricEnrollActivity.InternalActivity.class); + intent.putExtra(BiometricEnrollActivity.EXTRA_SKIP_INTRO, true); + return intent; } @Override @@ -411,16 +459,22 @@ public class ChooseLockGeneric extends SettingsActivity { // Saved so we don't force user to re-enter their password if configuration changes outState.putBoolean(PASSWORD_CONFIRMED, mPasswordConfirmed); outState.putBoolean(WAITING_FOR_CONFIRMATION, mWaitingForConfirmation); - outState.putInt(ENCRYPT_REQUESTED_QUALITY, mEncryptionRequestQuality); - outState.putBoolean(ENCRYPT_REQUESTED_DISABLED, mEncryptionRequestDisabled); if (mUserPassword != null) { - outState.putString(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD, mUserPassword); + outState.putByteArray(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD, mUserPassword); } } - private void updatePreferencesOrFinish(boolean isRecreatingActivity) { + @VisibleForTesting + void updatePreferencesOrFinish(boolean isRecreatingActivity) { Intent intent = getActivity().getIntent(); - int quality = intent.getIntExtra(LockPatternUtils.PASSWORD_TYPE_KEY, -1); + int quality = -1; + if (StorageManager.isFileEncryptedNativeOrEmulated()) { + quality = intent.getIntExtra(LockPatternUtils.PASSWORD_TYPE_KEY, -1); + } else { + // For non-file encrypted devices we need to show encryption interstitial, so always + // show the lock type picker and ignore PASSWORD_TYPE_KEY. + Log.i(TAG, "Ignoring PASSWORD_TYPE_KEY because device is not file encrypted"); + } if (quality == -1) { // If caller didn't specify password quality, show UI and allow the user to choose. quality = intent.getIntExtra(MINIMUM_QUALITY_KEY, -1); @@ -445,13 +499,42 @@ public class ChooseLockGeneric extends SettingsActivity { protected void addPreferences() { addPreferencesFromResource(R.xml.security_settings_picker); + if (!TextUtils.isEmpty(mCallerAppName) && !mIsCallingAppAdmin) { + FooterPreferenceMixinCompat footerMixin = + new FooterPreferenceMixinCompat(this, getSettingsLifecycle()); + FooterPreference footer = footerMixin.createFooterPreference(); + footer.setTitle(getFooterString()); + } + // Used for testing purposes findPreference(ScreenLockType.NONE.preferenceKey).setViewId(R.id.lock_none); findPreference(KEY_SKIP_FINGERPRINT).setViewId(R.id.lock_none); + findPreference(KEY_SKIP_FACE).setViewId(R.id.lock_none); findPreference(ScreenLockType.PIN.preferenceKey).setViewId(R.id.lock_pin); findPreference(ScreenLockType.PASSWORD.preferenceKey).setViewId(R.id.lock_password); } + private String getFooterString() { + @StringRes int stringId; + switch (mRequestedMinComplexity) { + case PASSWORD_COMPLEXITY_HIGH: + stringId = R.string.unlock_footer_high_complexity_requested; + break; + case PASSWORD_COMPLEXITY_MEDIUM: + stringId = R.string.unlock_footer_medium_complexity_requested; + break; + case PASSWORD_COMPLEXITY_LOW: + stringId = R.string.unlock_footer_low_complexity_requested; + break; + case PASSWORD_COMPLEXITY_NONE: + default: + stringId = R.string.unlock_footer_none_complexity_requested; + break; + } + + return getResources().getString(stringId, mCallerAppName); + } + private void updatePreferenceText() { if (mForFingerprint) { setPreferenceTitle(ScreenLockType.PATTERN, @@ -459,6 +542,12 @@ public class ChooseLockGeneric extends SettingsActivity { setPreferenceTitle(ScreenLockType.PIN, R.string.fingerprint_unlock_set_unlock_pin); setPreferenceTitle(ScreenLockType.PASSWORD, R.string.fingerprint_unlock_set_unlock_password); + } else if (mForFace) { + setPreferenceTitle(ScreenLockType.PATTERN, + R.string.face_unlock_set_unlock_pattern); + setPreferenceTitle(ScreenLockType.PIN, R.string.face_unlock_set_unlock_pin); + setPreferenceTitle(ScreenLockType.PASSWORD, + R.string.face_unlock_set_unlock_password); } if (mManagedPasswordProvider.isSettingManagedPasswordSupported()) { @@ -471,6 +560,9 @@ public class ChooseLockGeneric extends SettingsActivity { if (!(mForFingerprint && mIsSetNewPassword)) { removePreference(KEY_SKIP_FINGERPRINT); } + if (!(mForFace && mIsSetNewPassword)) { + removePreference(KEY_SKIP_FACE); + } } private void setPreferenceTitle(ScreenLockType lock, @StringRes int title) { @@ -537,7 +629,7 @@ public class ChooseLockGeneric extends SettingsActivity { final PreferenceScreen entries = getPreferenceScreen(); int adminEnforcedQuality = mDPM.getPasswordQuality(null, mUserId); - EnforcedAdmin enforcedAdmin = RestrictedLockUtils.checkIfPasswordQualityIsSet( + EnforcedAdmin enforcedAdmin = RestrictedLockUtilsInternal.checkIfPasswordQualityIsSet( getActivity(), mUserId); for (ScreenLockType lock : ScreenLockType.values()) { @@ -590,16 +682,17 @@ public class ChooseLockGeneric extends SettingsActivity { setPreferenceSummary(ScreenLockType.MANAGED, R.string.secure_lock_encryption_warning); } - protected Intent getLockManagedPasswordIntent(String password) { + protected Intent getLockManagedPasswordIntent(byte[] password) { return mManagedPasswordProvider.createIntent(false, password); } - protected Intent getLockPasswordIntent(int quality, int minLength, int maxLength) { + protected Intent getLockPasswordIntent(int quality) { ChooseLockPassword.IntentBuilder builder = new ChooseLockPassword.IntentBuilder(getContext()) .setPasswordQuality(quality) - .setPasswordLengthRange(minLength, maxLength) + .setRequestedMinComplexity(mRequestedMinComplexity) .setForFingerprint(mForFingerprint) + .setForFace(mForFace) .setUserId(mUserId); if (mHasChallenge) { builder.setChallenge(mChallenge); @@ -614,6 +707,7 @@ public class ChooseLockGeneric extends SettingsActivity { ChooseLockPattern.IntentBuilder builder = new ChooseLockPattern.IntentBuilder(getContext()) .setForFingerprint(mForFingerprint) + .setForFace(mForFace) .setUserId(mUserId); if (mHasChallenge) { builder.setChallenge(mChallenge); @@ -631,6 +725,30 @@ public class ChooseLockGeneric extends SettingsActivity { } /** + * Keeps track of the biometric removal status. When all biometrics (including managed + * profiles) are removed, finishes the activity. Otherwise, it's possible the UI still + * shows enrolled biometrics due to the async remove. + */ + private class RemovalTracker { + boolean mFingerprintDone; + boolean mFaceDone; + + void onFingerprintDone() { + mFingerprintDone = true; + if (mFingerprintDone && mFaceDone) { + finish(); + } + } + + void onFaceDone() { + mFaceDone = true; + if (mFingerprintDone && mFaceDone) { + finish(); + } + } + } + + /** * Invokes an activity to change the user's pattern, password or PIN based on given quality * and minimum quality specified by DevicePolicyManager. If quality is * {@link DevicePolicyManager#PASSWORD_QUALITY_UNSPECIFIED}, password is cleared. @@ -657,7 +775,7 @@ public class ChooseLockGeneric extends SettingsActivity { intent.putExtra(EXTRA_CHOOSE_LOCK_GENERIC_EXTRAS, getIntent().getExtras()); startActivityForResult(intent, mIsSetNewPassword && mHasChallenge - ? CHOOSE_LOCK_BEFORE_FINGERPRINT_REQUEST + ? CHOOSE_LOCK_BEFORE_BIOMETRIC_REQUEST : CHOOSE_LOCK_REQUEST); return; } @@ -666,33 +784,32 @@ public class ChooseLockGeneric extends SettingsActivity { mChooseLockSettingsHelper.utils().clearLock(mUserPassword, mUserId); mChooseLockSettingsHelper.utils().setLockScreenDisabled(disabled, mUserId); getActivity().setResult(Activity.RESULT_OK); - removeAllFingerprintForUserAndFinish(mUserId); + removeAllBiometricsForUserAndFinish(mUserId); } else { - removeAllFingerprintForUserAndFinish(mUserId); + removeAllBiometricsForUserAndFinish(mUserId); } } + private void removeAllBiometricsForUserAndFinish(final int userId) { + final RemovalTracker tracker = new RemovalTracker(); + removeAllFingerprintForUserAndFinish(userId, tracker); + removeAllFaceForUserAndFinish(userId, tracker); + } + private Intent getIntentForUnlockMethod(int quality) { Intent intent = null; if (quality >= DevicePolicyManager.PASSWORD_QUALITY_MANAGED) { intent = getLockManagedPasswordIntent(mUserPassword); } else if (quality >= DevicePolicyManager.PASSWORD_QUALITY_NUMERIC) { - int minLength = mDPM.getPasswordMinimumLength(null, mUserId); - if (minLength < MIN_PASSWORD_LENGTH) { - minLength = MIN_PASSWORD_LENGTH; - } - final int maxLength = mDPM.getPasswordMaximumLength(quality); - intent = getLockPasswordIntent(quality, minLength, maxLength); + intent = getLockPasswordIntent(quality); } else if (quality == DevicePolicyManager.PASSWORD_QUALITY_SOMETHING) { intent = getLockPatternIntent(); } - if (intent != null) { - intent.putExtra(EXTRA_HIDE_DRAWER, mHideDrawer); - } return intent; } - private void removeAllFingerprintForUserAndFinish(final int userId) { + private void removeAllFingerprintForUserAndFinish(final int userId, + RemovalTracker tracker) { if (mFingerprintManager != null && mFingerprintManager.isHardwareDetected()) { if (mFingerprintManager.hasEnrolledFingerprints(userId)) { mFingerprintManager.setActiveUser(userId); @@ -706,7 +823,7 @@ public class ChooseLockGeneric extends SettingsActivity { CharSequence errString) { Log.e(TAG, String.format( "Can't remove fingerprint %d in group %d. Reason: %s", - fp.getFingerId(), fp.getGroupId(), errString)); + fp.getBiometricId(), fp.getGroupId(), errString)); // TODO: need to proceed with the removal of managed profile // fingerprints and finish() gracefully. } @@ -714,25 +831,27 @@ public class ChooseLockGeneric extends SettingsActivity { @Override public void onRemovalSucceeded(Fingerprint fp, int remaining) { if (remaining == 0) { - removeManagedProfileFingerprintsAndFinishIfNecessary(userId); + removeManagedProfileFingerprintsAndFinishIfNecessary(userId, + tracker); } } }); } else { // No fingerprints in this user, we may also want to delete managed profile // fingerprints - removeManagedProfileFingerprintsAndFinishIfNecessary(userId); + removeManagedProfileFingerprintsAndFinishIfNecessary(userId, tracker); } } else { // The removal callback will call finish, once all fingerprints are removed. // We need to wait for that to occur, otherwise, the UI will still show that // fingerprints exist even though they are (about to) be removed depending on // the race condition. - finish(); + tracker.onFingerprintDone(); } } - private void removeManagedProfileFingerprintsAndFinishIfNecessary(final int parentUserId) { + private void removeManagedProfileFingerprintsAndFinishIfNecessary(final int parentUserId, + RemovalTracker tracker) { if (mFingerprintManager != null && mFingerprintManager.isHardwareDetected()) { mFingerprintManager.setActiveUser(UserHandle.myUserId()); } @@ -745,14 +864,69 @@ public class ChooseLockGeneric extends SettingsActivity { final UserInfo userInfo = profiles.get(i); if (userInfo.isManagedProfile() && !mLockPatternUtils .isSeparateProfileChallengeEnabled(userInfo.id)) { - removeAllFingerprintForUserAndFinish(userInfo.id); + removeAllFingerprintForUserAndFinish(userInfo.id, tracker); hasChildProfile = true; break; } } } if (!hasChildProfile) { - finish(); + tracker.onFingerprintDone(); + } + } + + // TODO: figure out how to eliminate duplicated code. It's a bit hard due to the async-ness + private void removeAllFaceForUserAndFinish(final int userId, RemovalTracker tracker) { + if (mFaceManager != null && mFaceManager.isHardwareDetected()) { + if (mFaceManager.hasEnrolledTemplates(userId)) { + mFaceManager.setActiveUser(userId); + Face face = new Face(null, 0, 0); + mFaceManager.remove(face, userId, + new FaceManager.RemovalCallback() { + @Override + public void onRemovalError(Face face, int errMsgId, CharSequence err) { + Log.e(TAG, String.format("Can't remove face %d. Reason: %s", + face.getBiometricId(), err)); + } + @Override + public void onRemovalSucceeded(Face face, int remaining) { + if (remaining == 0) { + removeManagedProfileFacesAndFinishIfNecessary(userId, tracker); + } + } + }); + } else { + // No faces in this user, we may also want to delete managed profile faces + removeManagedProfileFacesAndFinishIfNecessary(userId, tracker); + } + } else { + tracker.onFaceDone(); + } + } + + // TODO: figure out how to eliminate duplicated code. It's a bit hard due to the async-ness + private void removeManagedProfileFacesAndFinishIfNecessary(final int parentUserId, + RemovalTracker tracker) { + if (mFaceManager != null && mFaceManager.isHardwareDetected()) { + mFaceManager.setActiveUser(UserHandle.myUserId()); + } + boolean hasChildProfile = false; + if (!mUserManager.getUserInfo(parentUserId).isManagedProfile()) { + // Current user is primary profile, remove work profile faces if necessary + final List<UserInfo> profiles = mUserManager.getProfiles(parentUserId); + final int profilesSize = profiles.size(); + for (int i = 0; i < profilesSize; i++) { + final UserInfo userInfo = profiles.get(i); + if (userInfo.isManagedProfile() && !mLockPatternUtils + .isSeparateProfileChallengeEnabled(userInfo.id)) { + removeAllFaceForUserAndFinish(userInfo.id, tracker); + hasChildProfile = true; + break; + } + } + } + if (!hasChildProfile) { + tracker.onFaceDone(); } } @@ -915,7 +1089,7 @@ public class ChooseLockGeneric extends SettingsActivity { @Override public int getMetricsCategory() { - return MetricsEvent.DIALOG_FRP; + return SettingsEnums.DIALOG_FRP; } } } diff --git a/src/com/android/settings/password/ChooseLockGenericController.java b/src/com/android/settings/password/ChooseLockGenericController.java index 2550510a3e..62a0063580 100644 --- a/src/com/android/settings/password/ChooseLockGenericController.java +++ b/src/com/android/settings/password/ChooseLockGenericController.java @@ -16,12 +16,18 @@ package com.android.settings.password; +import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_NONE; + import android.app.admin.DevicePolicyManager; +import android.app.admin.DevicePolicyManager.PasswordComplexity; +import android.app.admin.PasswordMetrics; import android.content.Context; import android.os.UserHandle; + import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; +import com.android.internal.widget.LockPatternUtils; import com.android.settings.R; import java.util.ArrayList; @@ -35,51 +41,82 @@ public class ChooseLockGenericController { private final Context mContext; private final int mUserId; + @PasswordComplexity private final int mRequestedMinComplexity; private ManagedLockPasswordProvider mManagedPasswordProvider; private DevicePolicyManager mDpm; + private final LockPatternUtils mLockPatternUtils; public ChooseLockGenericController(Context context, int userId) { this( context, userId, + PASSWORD_COMPLEXITY_NONE, + new LockPatternUtils(context)); + } + + /** + * @param requestedMinComplexity specifies the min password complexity to be taken into account + * when determining the available screen lock types + */ + public ChooseLockGenericController(Context context, int userId, + @PasswordComplexity int requestedMinComplexity, LockPatternUtils lockPatternUtils) { + this( + context, + userId, + requestedMinComplexity, context.getSystemService(DevicePolicyManager.class), - ManagedLockPasswordProvider.get(context, userId)); + ManagedLockPasswordProvider.get(context, userId), + lockPatternUtils); } @VisibleForTesting ChooseLockGenericController( Context context, int userId, + @PasswordComplexity int requestedMinComplexity, DevicePolicyManager dpm, - ManagedLockPasswordProvider managedLockPasswordProvider) { + ManagedLockPasswordProvider managedLockPasswordProvider, + LockPatternUtils lockPatternUtils) { mContext = context; mUserId = userId; + mRequestedMinComplexity = requestedMinComplexity; mManagedPasswordProvider = managedLockPasswordProvider; mDpm = dpm; + mLockPatternUtils = lockPatternUtils; } /** - * @return The higher quality of either the specified {@code quality} or the quality required - * by {@link DevicePolicyManager#getPasswordQuality}. + * Returns the highest quality among the specified {@code quality}, the quality required by + * {@link DevicePolicyManager#getPasswordQuality}, and the quality required by min password + * complexity. */ public int upgradeQuality(int quality) { - // Compare min allowed password quality - return Math.max(quality, mDpm.getPasswordQuality(null, mUserId)); + // Compare specified quality and dpm quality + int dpmUpgradedQuality = Math.max(quality, mDpm.getPasswordQuality(null, mUserId)); + return Math.max(dpmUpgradedQuality, + PasswordMetrics.complexityLevelToMinQuality(mRequestedMinComplexity)); } /** * Whether the given screen lock type should be visible in the given context. */ public boolean isScreenLockVisible(ScreenLockType type) { + final boolean managedProfile = mUserId != UserHandle.myUserId(); switch (type) { case NONE: - return !mContext.getResources().getBoolean(R.bool.config_hide_none_security_option); + return !mContext.getResources().getBoolean(R.bool.config_hide_none_security_option) + && !managedProfile; // Profiles should use unified challenge instead. case SWIPE: return !mContext.getResources().getBoolean(R.bool.config_hide_swipe_security_option) - // Swipe doesn't make sense for profiles. - && mUserId == UserHandle.myUserId(); + && !managedProfile; // Swipe doesn't make sense for profiles. case MANAGED: return mManagedPasswordProvider.isManagedPasswordChoosable(); + case PIN: + case PATTERN: + case PASSWORD: + // Hide the secure lock screen options if the device doesn't support the secure lock + // screen feature. + return mLockPatternUtils.hasSecureLockScreen(); } return true; } diff --git a/src/com/android/settings/password/ChooseLockPassword.java b/src/com/android/settings/password/ChooseLockPassword.java index 1913ec6872..639dd6b63b 100644 --- a/src/com/android/settings/password/ChooseLockPassword.java +++ b/src/com/android/settings/password/ChooseLockPassword.java @@ -16,16 +16,20 @@ package com.android.settings.password; +import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_NONE; import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC; import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC; import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_COMPLEX; import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC; import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX; +import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_REQUESTED_MIN_COMPLEXITY; + import android.app.Activity; -import android.app.Fragment; import android.app.admin.DevicePolicyManager; +import android.app.admin.DevicePolicyManager.PasswordComplexity; import android.app.admin.PasswordMetrics; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; import android.content.res.Resources.Theme; @@ -34,9 +38,6 @@ import android.graphics.Typeface; import android.os.Bundle; import android.os.Handler; import android.os.Message; -import androidx.annotation.StringRes; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; import android.text.Editable; import android.text.InputType; import android.text.Selection; @@ -44,18 +45,22 @@ import android.text.Spannable; import android.text.TextUtils; import android.text.TextWatcher; import android.util.Log; +import android.util.Pair; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; -import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; -import android.widget.Button; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.TextView.OnEditorActionListener; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.annotation.StringRes; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.widget.LockPatternUtils; import com.android.internal.widget.LockPatternUtils.RequestThrottledException; import com.android.internal.widget.TextViewInputDisabler; @@ -67,21 +72,16 @@ import com.android.settings.Utils; import com.android.settings.core.InstrumentedFragment; import com.android.settings.notification.RedactionInterstitial; import com.android.settings.widget.ImeAwareEditText; -import com.android.setupwizardlib.GlifLayout; + +import com.google.android.setupcompat.template.FooterBarMixin; +import com.google.android.setupcompat.template.FooterButton; +import com.google.android.setupdesign.GlifLayout; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; public class ChooseLockPassword extends SettingsActivity { - public static final String PASSWORD_MIN_KEY = "lockscreen.password_min"; - public static final String PASSWORD_MAX_KEY = "lockscreen.password_max"; - public static final String PASSWORD_MIN_LETTERS_KEY = "lockscreen.password_min_letters"; - public static final String PASSWORD_MIN_LOWERCASE_KEY = "lockscreen.password_min_lowercase"; - public static final String PASSWORD_MIN_UPPERCASE_KEY = "lockscreen.password_min_uppercase"; - public static final String PASSWORD_MIN_NUMERIC_KEY = "lockscreen.password_min_numeric"; - public static final String PASSWORD_MIN_SYMBOLS_KEY = "lockscreen.password_min_symbols"; - public static final String PASSWORD_MIN_NONLETTER_KEY = "lockscreen.password_min_nonletter"; - private static final String TAG = "ChooseLockPassword"; @Override @@ -113,12 +113,6 @@ public class ChooseLockPassword extends SettingsActivity { return this; } - public IntentBuilder setPasswordLengthRange(int min, int max) { - mIntent.putExtra(PASSWORD_MIN_KEY, min); - mIntent.putExtra(PASSWORD_MAX_KEY, max); - return this; - } - public IntentBuilder setUserId(int userId) { mIntent.putExtra(Intent.EXTRA_USER_ID, userId); return this; @@ -130,7 +124,7 @@ public class ChooseLockPassword extends SettingsActivity { return this; } - public IntentBuilder setPassword(String password) { + public IntentBuilder setPassword(byte[] password) { mIntent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD, password); return this; } @@ -140,6 +134,16 @@ public class ChooseLockPassword extends SettingsActivity { return this; } + public IntentBuilder setForFace(boolean forFace) { + mIntent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FACE, forFace); + return this; + } + + public IntentBuilder setRequestedMinComplexity(@PasswordComplexity int level) { + mIntent.putExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY, level); + return this; + } + public Intent build() { return mIntent; } @@ -158,26 +162,31 @@ public class ChooseLockPassword extends SettingsActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - boolean forFingerprint = getIntent() + final boolean forFingerprint = getIntent() .getBooleanExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FINGERPRINT, false); - CharSequence msg = getText(forFingerprint - ? R.string.lockpassword_choose_your_password_header_for_fingerprint - : R.string.lockpassword_choose_your_screen_lock_header); + final boolean forFace = getIntent() + .getBooleanExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FACE, false); + + CharSequence msg = getText(R.string.lockpassword_choose_your_screen_lock_header); + if (forFingerprint) { + msg = getText(R.string.lockpassword_choose_your_password_header_for_fingerprint); + } else if (forFace) { + msg = getText(R.string.lockpassword_choose_your_password_header_for_face); + } + setTitle(msg); - LinearLayout layout = (LinearLayout) findViewById(R.id.content_parent); - layout.setFitsSystemWindows(false); + findViewById(R.id.content_parent).setFitsSystemWindows(false); } public static class ChooseLockPasswordFragment extends InstrumentedFragment - implements OnClickListener, OnEditorActionListener, TextWatcher, - SaveAndFinishWorker.Listener { + implements 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 byte[] mCurrentPassword; + private byte[] mChosenPassword; private boolean mHasChallenge; private long mChallenge; private ImeAwareEditText mPasswordEntry; @@ -191,13 +200,10 @@ public class ChooseLockPassword extends SettingsActivity { private int mPasswordMinNumeric = 0; private int mPasswordMinNonLetter = 0; private int mPasswordMinLengthToFulfillAllPolicies = 0; + private boolean mPasswordNumSequenceAllowed = true; + @PasswordComplexity private int mRequestedMinComplexity = PASSWORD_COMPLEXITY_NONE; protected int mUserId; - private boolean mHideDrawer = false; private byte[] mPasswordHistoryHashFactor; - /** - * Password requirements that we need to verify. - */ - private int[] mPasswordRequirements; private LockPatternUtils mLockPatternUtils; private SaveAndFinishWorker mSaveAndFinishWorker; @@ -207,13 +213,13 @@ public class ChooseLockPassword extends SettingsActivity { private PasswordRequirementAdapter mPasswordRequirementAdapter; private GlifLayout mLayout; protected boolean mForFingerprint; + protected boolean mForFace; - private String mFirstPin; + private byte[] mFirstPin; private RecyclerView mPasswordRestrictionView; protected boolean mIsAlphaMode; - protected Button mSkipButton; - private Button mClearButton; - private Button mNextButton; + protected FooterButton mSkipOrClearButton; + private FooterButton mNextButton; private TextView mMessage; private TextChangedHandler mTextChangedHandler; @@ -228,7 +234,7 @@ public class ChooseLockPassword extends SettingsActivity { private static final int MIN_NUMBER_IN_PASSWORD = 4; private static final int MIN_NON_LETTER_IN_PASSWORD = 5; - // Error code returned from {@link #validatePassword(String)}. + // Error code returned from {@link #validatePassword(byte[])}. static final int NO_ERROR = 0; static final int CONTAIN_INVALID_CHARACTERS = 1 << 0; static final int TOO_SHORT = 1 << 1; @@ -249,19 +255,23 @@ public class ChooseLockPassword extends SettingsActivity { protected enum Stage { Introduction( - R.string.lockpassword_choose_your_screen_lock_header, + R.string.lockpassword_choose_your_screen_lock_header, // password R.string.lockpassword_choose_your_password_header_for_fingerprint, - R.string.lockpassword_choose_your_screen_lock_header, + R.string.lockpassword_choose_your_password_header_for_face, + R.string.lockpassword_choose_your_screen_lock_header, // pin R.string.lockpassword_choose_your_pin_header_for_fingerprint, - R.string.lockpassword_choose_your_password_message, - R.string.lock_settings_picker_fingerprint_added_security_message, + R.string.lockpassword_choose_your_pin_header_for_face, + R.string.lockpassword_choose_your_password_message, // added security message + R.string.lock_settings_picker_biometrics_added_security_message, R.string.lockpassword_choose_your_pin_message, - R.string.lock_settings_picker_fingerprint_added_security_message, + R.string.lock_settings_picker_biometrics_added_security_message, R.string.next_label), NeedToConfirm( R.string.lockpassword_confirm_your_password_header, R.string.lockpassword_confirm_your_password_header, + R.string.lockpassword_confirm_your_password_header, + R.string.lockpassword_confirm_your_pin_header, R.string.lockpassword_confirm_your_pin_header, R.string.lockpassword_confirm_your_pin_header, 0, @@ -273,6 +283,8 @@ public class ChooseLockPassword extends SettingsActivity { ConfirmWrong( R.string.lockpassword_confirm_passwords_dont_match, R.string.lockpassword_confirm_passwords_dont_match, + R.string.lockpassword_confirm_passwords_dont_match, + R.string.lockpassword_confirm_pins_dont_match, R.string.lockpassword_confirm_pins_dont_match, R.string.lockpassword_confirm_pins_dont_match, 0, @@ -281,45 +293,71 @@ public class ChooseLockPassword extends SettingsActivity { 0, R.string.lockpassword_confirm_label); - Stage(int hintInAlpha, int hintInAlphaForFingerprint, - int hintInNumeric, int hintInNumericForFingerprint, - int messageInAlpha, int messageInAlphaForFingerprint, - int messageInNumeric, int messageInNumericForFingerprint, + Stage(int hintInAlpha, int hintInAlphaForFingerprint, int hintInAlphaForFace, + int hintInNumeric, int hintInNumericForFingerprint, int hintInNumericForFace, + int messageInAlpha, int messageInAlphaForBiometrics, + int messageInNumeric, int messageInNumericForBiometrics, int nextButtonText) { this.alphaHint = hintInAlpha; this.alphaHintForFingerprint = hintInAlphaForFingerprint; + this.alphaHintForFace = hintInAlphaForFace; + this.numericHint = hintInNumeric; this.numericHintForFingerprint = hintInNumericForFingerprint; + this.numericHintForFace = hintInNumericForFace; + this.alphaMessage = messageInAlpha; - this.alphaMessageForFingerprint = messageInAlphaForFingerprint; + this.alphaMessageForBiometrics = messageInAlphaForBiometrics; this.numericMessage = messageInNumeric; - this.numericMessageForFingerprint = messageInNumericForFingerprint; + this.numericMessageForBiometrics = messageInNumericForBiometrics; this.buttonText = nextButtonText; } + public static final int TYPE_NONE = 0; + public static final int TYPE_FINGERPRINT = 1; + public static final int TYPE_FACE = 2; + + // Password public final int alphaHint; public final int alphaHintForFingerprint; + public final int alphaHintForFace; + + // PIN public final int numericHint; public final int numericHintForFingerprint; + public final int numericHintForFace; + public final int alphaMessage; - public final int alphaMessageForFingerprint; + public final int alphaMessageForBiometrics; public final int numericMessage; - public final int numericMessageForFingerprint; + public final int numericMessageForBiometrics; public final int buttonText; - public @StringRes int getHint(boolean isAlpha, boolean isFingerprint) { + public @StringRes int getHint(boolean isAlpha, int type) { if (isAlpha) { - return isFingerprint ? alphaHintForFingerprint : alphaHint; + if (type == TYPE_FINGERPRINT) { + return alphaHintForFingerprint; + } else if (type == TYPE_FACE) { + return alphaHintForFace; + } else { + return alphaHint; + } } else { - return isFingerprint ? numericHintForFingerprint : numericHint; + if (type == TYPE_FINGERPRINT) { + return numericHintForFingerprint; + } else if (type == TYPE_FACE) { + return numericHintForFace; + } else { + return numericHint; + } } } - public @StringRes int getMessage(boolean isAlpha, boolean isFingerprint) { + public @StringRes int getMessage(boolean isAlpha, int type) { if (isAlpha) { - return isFingerprint ? alphaMessageForFingerprint : alphaMessage; + return type != TYPE_NONE ? alphaMessageForBiometrics : alphaMessage; } else { - return isFingerprint ? numericMessageForFingerprint : numericMessage; + return type != TYPE_NONE ? numericMessageForBiometrics : numericMessage; } } } @@ -341,21 +379,28 @@ public class ChooseLockPassword extends SettingsActivity { mUserId = Utils.getUserIdFromBundle(getActivity(), intent.getExtras()); mForFingerprint = intent.getBooleanExtra( ChooseLockSettingsHelper.EXTRA_KEY_FOR_FINGERPRINT, false); - processPasswordRequirements(intent); + mForFace = intent.getBooleanExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FACE, false); + mRequestedMinComplexity = intent.getIntExtra( + EXTRA_KEY_REQUESTED_MIN_COMPLEXITY, PASSWORD_COMPLEXITY_NONE); + mRequestedQuality = Math.max( + intent.getIntExtra(LockPatternUtils.PASSWORD_TYPE_KEY, mRequestedQuality), + mLockPatternUtils.getRequestedPasswordQuality(mUserId)); + + loadDpmPasswordRequirements(); mChooseLockSettingsHelper = new ChooseLockSettingsHelper(getActivity()); - mHideDrawer = getActivity().getIntent().getBooleanExtra(EXTRA_HIDE_DRAWER, false); if (intent.getBooleanExtra( ChooseLockSettingsHelper.EXTRA_KEY_FOR_CHANGE_CRED_REQUIRED_FOR_BOOT, false)) { SaveAndFinishWorker w = new SaveAndFinishWorker(); final boolean required = getActivity().getIntent().getBooleanExtra( EncryptionInterstitial.EXTRA_REQUIRE_PASSWORD, true); - String current = intent.getStringExtra( + byte[] currentBytes = intent.getByteArrayExtra( ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD); + w.setBlocking(true); w.setListener(this); - w.start(mChooseLockSettingsHelper.utils(), required, - false, 0, current, current, mRequestedQuality, mUserId); + w.start(mChooseLockSettingsHelper.utils(), required, false, 0, + currentBytes, currentBytes, mRequestedQuality, mUserId); } mTextChangedHandler = new TextChangedHandler(); } @@ -377,17 +422,31 @@ public class ChooseLockPassword extends SettingsActivity { ViewGroup container = view.findViewById(R.id.password_container); container.setOpticalInsets(Insets.NONE); - mSkipButton = (Button) view.findViewById(R.id.skip_button); - mSkipButton.setOnClickListener(this); - mNextButton = (Button) view.findViewById(R.id.next_button); - mNextButton.setOnClickListener(this); - mClearButton = view.findViewById(R.id.clear_button); - mClearButton.setOnClickListener(this); - - - mMessage = view.findViewById(R.id.message); + final FooterBarMixin mixin = mLayout.getMixin(FooterBarMixin.class); + mixin.setSecondaryButton( + new FooterButton.Builder(getActivity()) + .setText(R.string.lockpassword_clear_label) + .setListener(this::onSkipOrClearButtonClick) + .setButtonType(FooterButton.ButtonType.SKIP) + .setTheme(R.style.SudGlifButton_Secondary) + .build() + ); + mixin.setPrimaryButton( + new FooterButton.Builder(getActivity()) + .setText(R.string.next_label) + .setListener(this::onNextButtonClick) + .setButtonType(FooterButton.ButtonType.NEXT) + .setTheme(R.style.SudGlifButton_Primary) + .build() + ); + mSkipOrClearButton = mixin.getSecondaryButton(); + mNextButton = mixin.getPrimaryButton(); + + mMessage = view.findViewById(R.id.sud_layout_description); if (mForFingerprint) { mLayout.setIcon(getActivity().getDrawable(R.drawable.ic_fingerprint_header)); + } else if (mForFace) { + mLayout.setIcon(getActivity().getDrawable(R.drawable.ic_face_header)); } mIsAlphaMode = DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC == mRequestedQuality @@ -416,7 +475,8 @@ public class ChooseLockPassword extends SettingsActivity { Intent intent = getActivity().getIntent(); final boolean confirmCredentials = intent.getBooleanExtra( ChooseLockGeneric.CONFIRM_CREDENTIALS, true); - mCurrentPassword = intent.getStringExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD); + mCurrentPassword = intent.getByteArrayExtra( + ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD); mHasChallenge = intent.getBooleanExtra( ChooseLockSettingsHelper.EXTRA_KEY_HAS_CHALLENGE, false); mChallenge = intent.getLongExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE, 0); @@ -428,8 +488,9 @@ public class ChooseLockPassword extends SettingsActivity { mUserId); } } else { + // restore from previous state - mFirstPin = savedInstanceState.getString(KEY_FIRST_PIN); + mFirstPin = savedInstanceState.getByteArray(KEY_FIRST_PIN); final String state = savedInstanceState.getString(KEY_UI_STAGE); if (state != null) { mUiStage = Stage.valueOf(state); @@ -437,7 +498,7 @@ public class ChooseLockPassword extends SettingsActivity { } if (mCurrentPassword == null) { - mCurrentPassword = savedInstanceState.getString(KEY_CURRENT_PASSWORD); + mCurrentPassword = savedInstanceState.getByteArray(KEY_CURRENT_PASSWORD); } // Re-attach to the exiting worker if there is one. @@ -447,61 +508,20 @@ public class ChooseLockPassword extends SettingsActivity { if (activity instanceof SettingsActivity) { final SettingsActivity sa = (SettingsActivity) activity; - int title = Stage.Introduction.getHint(mIsAlphaMode, mForFingerprint); + int title = Stage.Introduction.getHint(mIsAlphaMode, getStageType()); sa.setTitle(title); mLayout.setHeaderText(title); } } - private void setupPasswordRequirementsView(View view) { - // Construct passwordRequirements and requirementDescriptions. - List<Integer> passwordRequirements = new ArrayList<>(); - List<String> requirementDescriptions = new ArrayList<>(); - if (mPasswordMinUpperCase > 0) { - passwordRequirements.add(MIN_UPPER_LETTERS_IN_PASSWORD); - requirementDescriptions.add(getResources().getQuantityString( - R.plurals.lockpassword_password_requires_uppercase, mPasswordMinUpperCase, - mPasswordMinUpperCase)); - } - if (mPasswordMinLowerCase > 0) { - passwordRequirements.add(MIN_LOWER_LETTERS_IN_PASSWORD); - requirementDescriptions.add(getResources().getQuantityString( - R.plurals.lockpassword_password_requires_lowercase, mPasswordMinLowerCase, - mPasswordMinLowerCase)); - } - if (mPasswordMinLetters > 0) { - if (mPasswordMinLetters > mPasswordMinUpperCase + mPasswordMinLowerCase) { - passwordRequirements.add(MIN_LETTER_IN_PASSWORD); - requirementDescriptions.add(getResources().getQuantityString( - R.plurals.lockpassword_password_requires_letters, mPasswordMinLetters, - mPasswordMinLetters)); - } - } - if (mPasswordMinNumeric > 0) { - passwordRequirements.add(MIN_NUMBER_IN_PASSWORD); - requirementDescriptions.add(getResources().getQuantityString( - R.plurals.lockpassword_password_requires_numeric, mPasswordMinNumeric, - mPasswordMinNumeric)); - } - if (mPasswordMinSymbols > 0) { - passwordRequirements.add(MIN_SYMBOLS_IN_PASSWORD); - requirementDescriptions.add(getResources().getQuantityString( - R.plurals.lockpassword_password_requires_symbols, mPasswordMinSymbols, - mPasswordMinSymbols)); - } - if (mPasswordMinNonLetter > 0) { - if (mPasswordMinNonLetter > mPasswordMinNumeric + mPasswordMinSymbols) { - passwordRequirements.add(MIN_NON_LETTER_IN_PASSWORD); - requirementDescriptions.add(getResources().getQuantityString( - R.plurals.lockpassword_password_requires_nonletter, mPasswordMinNonLetter, + protected int getStageType() { + return mForFingerprint ? Stage.TYPE_FINGERPRINT : + mForFace ? Stage.TYPE_FACE : + Stage.TYPE_NONE; + } - mPasswordMinNonLetter)); - } - } - // Convert list to array. - mPasswordRequirements = passwordRequirements.stream().mapToInt(i -> i).toArray(); - mPasswordRestrictionView = - (RecyclerView) view.findViewById(R.id.password_requirements_view); + private void setupPasswordRequirementsView(View view) { + mPasswordRestrictionView = view.findViewById(R.id.password_requirements_view); mPasswordRestrictionView.setLayoutManager(new LinearLayoutManager(getActivity())); mPasswordRequirementAdapter = new PasswordRequirementAdapter(); mPasswordRestrictionView.setAdapter(mPasswordRequirementAdapter); @@ -509,7 +529,7 @@ public class ChooseLockPassword extends SettingsActivity { @Override public int getMetricsCategory() { - return MetricsEvent.CHOOSE_LOCK_PASSWORD; + return SettingsEnums.CHOOSE_LOCK_PASSWORD; } @Override @@ -536,8 +556,8 @@ public class ChooseLockPassword extends SettingsActivity { public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putString(KEY_UI_STAGE, mUiStage.name()); - outState.putString(KEY_FIRST_PIN, mFirstPin); - outState.putString(KEY_CURRENT_PASSWORD, mCurrentPassword); + outState.putByteArray(KEY_FIRST_PIN, mFirstPin); + outState.putByteArray(KEY_CURRENT_PASSWORD, mCurrentPassword); } @Override @@ -550,7 +570,7 @@ public class ChooseLockPassword extends SettingsActivity { getActivity().setResult(RESULT_FINISHED); getActivity().finish(); } else { - mCurrentPassword = data.getStringExtra( + mCurrentPassword = data.getByteArrayExtra( ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD); } break; @@ -575,38 +595,23 @@ public class ChooseLockPassword extends SettingsActivity { /** * Read the requirements from {@link DevicePolicyManager} and intent and aggregate them. - * - * @param intent the incoming intent */ - private void processPasswordRequirements(Intent intent) { + private void loadDpmPasswordRequirements() { final int dpmPasswordQuality = mLockPatternUtils.getRequestedPasswordQuality(mUserId); - mRequestedQuality = Math.max(intent.getIntExtra(LockPatternUtils.PASSWORD_TYPE_KEY, - mRequestedQuality), dpmPasswordQuality); - mPasswordMinLength = Math.max(Math.max( - LockPatternUtils.MIN_LOCK_PASSWORD_SIZE, - intent.getIntExtra(PASSWORD_MIN_KEY, mPasswordMinLength)), + if (dpmPasswordQuality == PASSWORD_QUALITY_NUMERIC_COMPLEX) { + mPasswordNumSequenceAllowed = false; + } + mPasswordMinLength = Math.max(LockPatternUtils.MIN_LOCK_PASSWORD_SIZE, mLockPatternUtils.getRequestedMinimumPasswordLength(mUserId)); - mPasswordMaxLength = intent.getIntExtra(PASSWORD_MAX_KEY, mPasswordMaxLength); - mPasswordMinLetters = Math.max(intent.getIntExtra(PASSWORD_MIN_LETTERS_KEY, - mPasswordMinLetters), mLockPatternUtils.getRequestedPasswordMinimumLetters( - mUserId)); - mPasswordMinUpperCase = Math.max(intent.getIntExtra(PASSWORD_MIN_UPPERCASE_KEY, - mPasswordMinUpperCase), mLockPatternUtils.getRequestedPasswordMinimumUpperCase( - mUserId)); - mPasswordMinLowerCase = Math.max(intent.getIntExtra(PASSWORD_MIN_LOWERCASE_KEY, - mPasswordMinLowerCase), mLockPatternUtils.getRequestedPasswordMinimumLowerCase( - mUserId)); - mPasswordMinNumeric = Math.max(intent.getIntExtra(PASSWORD_MIN_NUMERIC_KEY, - mPasswordMinNumeric), mLockPatternUtils.getRequestedPasswordMinimumNumeric( - mUserId)); - mPasswordMinSymbols = Math.max(intent.getIntExtra(PASSWORD_MIN_SYMBOLS_KEY, - mPasswordMinSymbols), mLockPatternUtils.getRequestedPasswordMinimumSymbols( - mUserId)); - mPasswordMinNonLetter = Math.max(intent.getIntExtra(PASSWORD_MIN_NONLETTER_KEY, - mPasswordMinNonLetter), mLockPatternUtils.getRequestedPasswordMinimumNonLetter( - mUserId)); - - // Modify the value based on dpm policy. + mPasswordMaxLength = mLockPatternUtils.getMaximumPasswordLength(mRequestedQuality); + mPasswordMinLetters = mLockPatternUtils.getRequestedPasswordMinimumLetters(mUserId); + mPasswordMinUpperCase = mLockPatternUtils.getRequestedPasswordMinimumUpperCase(mUserId); + mPasswordMinLowerCase = mLockPatternUtils.getRequestedPasswordMinimumLowerCase(mUserId); + mPasswordMinNumeric = mLockPatternUtils.getRequestedPasswordMinimumNumeric(mUserId); + mPasswordMinSymbols = mLockPatternUtils.getRequestedPasswordMinimumSymbols(mUserId); + mPasswordMinNonLetter = mLockPatternUtils.getRequestedPasswordMinimumNonLetter(mUserId); + + // Modify the value based on dpm policy switch (dpmPasswordQuality) { case PASSWORD_QUALITY_ALPHABETIC: if (mPasswordMinLetters == 0) { @@ -632,36 +637,116 @@ public class ChooseLockPassword extends SettingsActivity { mPasswordMinSymbols = 0; mPasswordMinNonLetter = 0; } + mPasswordMinLengthToFulfillAllPolicies = getMinLengthToFulfillAllPolicies(); } /** + * Merges the dpm requirements and the min complexity requirements. + * + * <p>Since there are more than one set of metrics to meet the min complexity requirement, + * and we are not hard-coding any one of them to be the requirements the user must fulfil, + * we are taking what the user has already entered into account when compiling the list of + * requirements from min complexity. Then we merge this list with the DPM requirements, and + * present the merged set as validation results to the user on the UI. + * + * <p>For example, suppose min complexity requires either ALPHABETIC(8+), or + * ALPHANUMERIC(6+). If the user has entered "a", the length requirement displayed on the UI + * would be 8. Then the user appends "1" to make it "a1". We now know the user is entering + * an alphanumeric password so we would update the min complexity required min length to 6. + * This might result in a little confusion for the user but the UI does not support showing + * multiple sets of requirements / validation results as options to users, this is the best + * we can do now. + */ + private void mergeMinComplexityAndDpmRequirements(int userEnteredPasswordQuality) { + if (mRequestedMinComplexity == PASSWORD_COMPLEXITY_NONE) { + // dpm requirements are dominant if min complexity is none + return; + } + + // reset dpm requirements + loadDpmPasswordRequirements(); + + PasswordMetrics minMetrics = PasswordMetrics.getMinimumMetrics( + mRequestedMinComplexity, userEnteredPasswordQuality, mRequestedQuality, + requiresNumeric(), requiresLettersOrSymbols()); + mPasswordNumSequenceAllowed = mPasswordNumSequenceAllowed + && minMetrics.quality != PASSWORD_QUALITY_NUMERIC_COMPLEX; + mPasswordMinLength = Math.max(mPasswordMinLength, minMetrics.length); + mPasswordMinLetters = Math.max(mPasswordMinLetters, minMetrics.letters); + mPasswordMinUpperCase = Math.max(mPasswordMinUpperCase, minMetrics.upperCase); + mPasswordMinLowerCase = Math.max(mPasswordMinLowerCase, minMetrics.lowerCase); + mPasswordMinNumeric = Math.max(mPasswordMinNumeric, minMetrics.numeric); + mPasswordMinSymbols = Math.max(mPasswordMinSymbols, minMetrics.symbols); + mPasswordMinNonLetter = Math.max(mPasswordMinNonLetter, minMetrics.nonLetter); + + if (minMetrics.quality == PASSWORD_QUALITY_ALPHABETIC) { + if (!requiresLettersOrSymbols()) { + mPasswordMinLetters = 1; + } + } + if (minMetrics.quality == PASSWORD_QUALITY_ALPHANUMERIC) { + if (!requiresLettersOrSymbols()) { + mPasswordMinLetters = 1; + } + if (!requiresNumeric()) { + mPasswordMinNumeric = 1; + } + } + + mPasswordMinLengthToFulfillAllPolicies = getMinLengthToFulfillAllPolicies(); + } + + private boolean requiresLettersOrSymbols() { + // This is the condition for the password to be considered ALPHABETIC according to + // PasswordMetrics.computeForPassword() + return mPasswordMinLetters + mPasswordMinUpperCase + + mPasswordMinLowerCase + mPasswordMinSymbols + mPasswordMinNonLetter > 0; + } + + private boolean requiresNumeric() { + return mPasswordMinNumeric > 0; + } + + /** * Validates PIN/Password and returns the validation result. * * @param password the raw password the user typed in * @return the validation result. */ - private int validatePassword(String password) { + @VisibleForTesting + int validatePassword(byte[] password) { int errorCode = NO_ERROR; final PasswordMetrics metrics = PasswordMetrics.computeForPassword(password); + mergeMinComplexityAndDpmRequirements(metrics.quality); - - if (password.length() < mPasswordMinLength) { + if (password == null || password.length < mPasswordMinLength) { if (mPasswordMinLength > mPasswordMinLengthToFulfillAllPolicies) { errorCode |= TOO_SHORT; } - } else if (password.length() > mPasswordMaxLength) { + } else if (password.length > mPasswordMaxLength) { errorCode |= TOO_LONG; } else { // The length requirements are fulfilled. - final int dpmQuality = mLockPatternUtils.getRequestedPasswordQuality(mUserId); - if (dpmQuality == PASSWORD_QUALITY_NUMERIC_COMPLEX && - metrics.numeric == password.length()) { + if (!mPasswordNumSequenceAllowed + && !requiresLettersOrSymbols() + && metrics.numeric == password.length) { // Check for repeated characters or sequences (e.g. '1234', '0000', '2468') - // if DevicePolicyManager requires a complex numeric password. There can be - // two cases in the UI: 1. User chooses to enroll a PIN, 2. User chooses to - // enroll a password but enters a numeric-only pin. We should carry out the - // sequence check in both cases. + // if DevicePolicyManager or min password complexity requires a complex numeric + // password. There can be two cases in the UI: 1. User chooses to enroll a + // PIN, 2. User chooses to enroll a password but enters a numeric-only pin. We + // should carry out the sequence check in both cases. + // + // Conditions for the !requiresLettersOrSymbols() to be necessary: + // - DPM requires NUMERIC_COMPLEX + // - min complexity not NONE, user picks PASSWORD type so ALPHABETIC or + // ALPHANUMERIC is required + // Imagine user has entered "12345678", if we don't skip the sequence check, the + // validation result would show both "requires a letter" and "sequence not + // allowed", while the only requirement the user needs to know is "requires a + // letter" because once the user has fulfilled the alphabetic requirement, the + // password would not be containing only digits so this check would not be + // performed anyway. final int sequence = PasswordMetrics.maxLengthSequence(password); if (sequence > PasswordMetrics.MAX_ALLOWED_SEQUENCE) { errorCode |= CONTAIN_SEQUENTIAL_DIGITS; @@ -675,8 +760,8 @@ public class ChooseLockPassword extends SettingsActivity { } // Allow non-control Latin-1 characters only. - for (int i = 0; i < password.length(); i++) { - char c = password.charAt(i); + for (int i = 0; i < password.length; i++) { + char c = (char) password[i]; if (c < 32 || c > 127) { errorCode |= CONTAIN_INVALID_CHARACTERS; break; @@ -692,43 +777,24 @@ public class ChooseLockPassword extends SettingsActivity { } } - // Check the requirements one by one. - for (int i = 0; i < mPasswordRequirements.length; i++) { - int passwordRestriction = mPasswordRequirements[i]; - switch (passwordRestriction) { - case MIN_LETTER_IN_PASSWORD: - if (metrics.letters < mPasswordMinLetters) { - errorCode |= NOT_ENOUGH_LETTER; - } - break; - case MIN_UPPER_LETTERS_IN_PASSWORD: - if (metrics.upperCase < mPasswordMinUpperCase) { - errorCode |= NOT_ENOUGH_UPPER_CASE; - } - break; - case MIN_LOWER_LETTERS_IN_PASSWORD: - if (metrics.lowerCase < mPasswordMinLowerCase) { - errorCode |= NOT_ENOUGH_LOWER_CASE; - } - break; - case MIN_SYMBOLS_IN_PASSWORD: - if (metrics.symbols < mPasswordMinSymbols) { - errorCode |= NOT_ENOUGH_SYMBOLS; - } - break; - case MIN_NUMBER_IN_PASSWORD: - if (metrics.numeric < mPasswordMinNumeric) { - errorCode |= NOT_ENOUGH_DIGITS; - } - break; - case MIN_NON_LETTER_IN_PASSWORD: - if (metrics.nonLetter < mPasswordMinNonLetter) { - errorCode |= NOT_ENOUGH_NON_LETTER; - } - break; - } + if (metrics.letters < mPasswordMinLetters) { + errorCode |= NOT_ENOUGH_LETTER; + } + if (metrics.upperCase < mPasswordMinUpperCase) { + errorCode |= NOT_ENOUGH_UPPER_CASE; + } + if (metrics.lowerCase < mPasswordMinLowerCase) { + errorCode |= NOT_ENOUGH_LOWER_CASE; + } + if (metrics.symbols < mPasswordMinSymbols) { + errorCode |= NOT_ENOUGH_SYMBOLS; + } + if (metrics.numeric < mPasswordMinNumeric) { + errorCode |= NOT_ENOUGH_DIGITS; + } + if (metrics.nonLetter < mPasswordMinNonLetter) { + errorCode |= NOT_ENOUGH_NON_LETTER; } - return errorCode; } @@ -746,8 +812,9 @@ public class ChooseLockPassword extends SettingsActivity { public void handleNext() { if (mSaveAndFinishWorker != null) return; - mChosenPassword = mPasswordEntry.getText().toString(); - if (TextUtils.isEmpty(mChosenPassword)) { + // TODO(b/120484642): This is a point of entry for passwords from the UI + mChosenPassword = LockPatternUtils.charSequenceToByteArray(mPasswordEntry.getText()); + if (mChosenPassword == null || mChosenPassword.length == 0) { return; } if (mUiStage == Stage.Introduction) { @@ -755,9 +822,11 @@ public class ChooseLockPassword extends SettingsActivity { mFirstPin = mChosenPassword; mPasswordEntry.setText(""); updateStage(Stage.NeedToConfirm); + } else { + Arrays.fill(mChosenPassword, (byte) 0); } } else if (mUiStage == Stage.NeedToConfirm) { - if (mFirstPin.equals(mChosenPassword)) { + if (Arrays.equals(mFirstPin, mChosenPassword)) { startSaveAndFinish(); } else { CharSequence tmp = mPasswordEntry.getText(); @@ -765,6 +834,7 @@ public class ChooseLockPassword extends SettingsActivity { Selection.setSelection((Spannable) tmp, 0, tmp.length()); } updateStage(Stage.ConfirmWrong); + Arrays.fill(mChosenPassword, (byte) 0); } } } @@ -774,19 +844,15 @@ public class ChooseLockPassword extends SettingsActivity { } protected void setNextText(int text) { - mNextButton.setText(text); + mNextButton.setText(getActivity(), text); } - public void onClick(View v) { - switch (v.getId()) { - case R.id.next_button: - handleNext(); - break; + protected void onSkipOrClearButtonClick(View view) { + mPasswordEntry.setText(""); + } - case R.id.clear_button: - mPasswordEntry.setText(""); - break; - } + protected void onNextButtonClick(View view) { + handleNext(); } public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { @@ -843,14 +909,20 @@ public class ChooseLockPassword extends SettingsActivity { mPasswordMinNonLetter)); } if ((errorCode & TOO_SHORT) > 0) { - messages.add(getString(mIsAlphaMode ? - R.string.lockpassword_password_too_short - : R.string.lockpassword_pin_too_short, mPasswordMinLength)); + messages.add(getResources().getQuantityString( + mIsAlphaMode + ? R.plurals.lockpassword_password_too_short + : R.plurals.lockpassword_pin_too_short, + mPasswordMinLength, + mPasswordMinLength)); } if ((errorCode & TOO_LONG) > 0) { - messages.add(getString(mIsAlphaMode ? - R.string.lockpassword_password_too_long - : R.string.lockpassword_pin_too_long, mPasswordMaxLength + 1)); + messages.add(getResources().getQuantityString( + mIsAlphaMode + ? R.plurals.lockpassword_password_too_long + : R.plurals.lockpassword_pin_too_long, + mPasswordMaxLength + 1, + mPasswordMaxLength + 1)); } if ((errorCode & CONTAIN_SEQUENTIAL_DIGITS) > 0) { messages.add(getString(R.string.lockpassword_pin_no_sequential_digits)); @@ -875,8 +947,8 @@ public class ChooseLockPassword extends SettingsActivity { */ protected void updateUi() { final boolean canInput = mSaveAndFinishWorker == null; - String password = mPasswordEntry.getText().toString(); - final int length = password.length(); + byte[] password = LockPatternUtils.charSequenceToByteArray(mPasswordEntry.getText()); + final int length = password.length; if (mUiStage == Stage.Introduction) { mPasswordRestrictionView.setVisibility(View.VISIBLE); final int errorCode = validatePassword(password); @@ -888,11 +960,11 @@ public class ChooseLockPassword extends SettingsActivity { } else { // Hide password requirement view when we are just asking user to confirm the pw. mPasswordRestrictionView.setVisibility(View.GONE); - setHeaderText(getString(mUiStage.getHint(mIsAlphaMode, mForFingerprint))); + setHeaderText(getString(mUiStage.getHint(mIsAlphaMode, getStageType()))); setNextEnabled(canInput && length >= mPasswordMinLength); - mClearButton.setEnabled(canInput && length > 0); + mSkipOrClearButton.setVisibility(toVisibility(canInput && length > 0)); } - int message = mUiStage.getMessage(mIsAlphaMode, mForFingerprint); + int message = mUiStage.getMessage(mIsAlphaMode, getStageType()); if (message != 0) { mMessage.setVisibility(View.VISIBLE); mMessage.setText(message); @@ -900,13 +972,12 @@ public class ChooseLockPassword extends SettingsActivity { mMessage.setVisibility(View.INVISIBLE); } - mClearButton.setVisibility(toVisibility(mUiStage != Stage.Introduction)); - setNextText(mUiStage.buttonText); mPasswordEntryInputDisabler.setInputEnabled(canInput); + Arrays.fill(password, (byte) 0); } - private int toVisibility(boolean visibleOrGone) { + protected int toVisibility(boolean visibleOrGone) { return visibleOrGone ? View.VISIBLE : View.GONE; } @@ -962,10 +1033,21 @@ public class ChooseLockPassword extends SettingsActivity { public void onChosenLockSaveFinished(boolean wasSecureBefore, Intent resultData) { getActivity().setResult(RESULT_FINISHED, resultData); + if (mChosenPassword != null) { + Arrays.fill(mChosenPassword, (byte) 0); + } + if (mCurrentPassword != null) { + Arrays.fill(mCurrentPassword, (byte) 0); + } + if (mFirstPin != null) { + Arrays.fill(mFirstPin, (byte) 0); + } + + mPasswordEntry.setText(""); + if (!wasSecureBefore) { Intent intent = getRedactionInterstitialIntent(getActivity()); if (intent != null) { - intent.putExtra(EXTRA_HIDE_DRAWER, mHideDrawer); startActivity(intent); } } @@ -999,13 +1081,13 @@ public class ChooseLockPassword extends SettingsActivity { public static class SaveAndFinishWorker extends SaveChosenLockWorkerBase { - private String mChosenPassword; - private String mCurrentPassword; + private byte[] mChosenPassword; + private byte[] mCurrentPassword; private int mRequestedQuality; public void start(LockPatternUtils utils, boolean required, boolean hasChallenge, long challenge, - String chosenPassword, String currentPassword, int requestedQuality, int userId) { + byte[] chosenPassword, byte[] currentPassword, int requestedQuality, int userId) { prepare(utils, required, hasChallenge, challenge, userId); mChosenPassword = chosenPassword; @@ -1017,12 +1099,11 @@ public class ChooseLockPassword extends SettingsActivity { } @Override - protected Intent saveAndVerifyInBackground() { + protected Pair<Boolean, Intent> saveAndVerifyInBackground() { + final boolean success = mUtils.saveLockPassword( + mChosenPassword, mCurrentPassword, mRequestedQuality, mUserId); Intent result = null; - mUtils.saveLockPassword(mChosenPassword, mCurrentPassword, mRequestedQuality, - mUserId); - - if (mHasChallenge) { + if (success && mHasChallenge) { byte[] token; try { token = mUtils.verifyPassword(mChosenPassword, mChallenge, mUserId); @@ -1037,8 +1118,7 @@ public class ChooseLockPassword extends SettingsActivity { result = new Intent(); result.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token); } - - return result; + return Pair.create(success, result); } } } diff --git a/src/com/android/settings/password/ChooseLockPattern.java b/src/com/android/settings/password/ChooseLockPattern.java index 95759f3132..5de4e3ad14 100644 --- a/src/com/android/settings/password/ChooseLockPattern.java +++ b/src/com/android/settings/password/ChooseLockPattern.java @@ -17,13 +17,14 @@ package com.android.settings.password; import android.app.Activity; -import android.app.Fragment; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; import android.content.res.ColorStateList; import android.content.res.Resources.Theme; import android.os.Bundle; import android.util.Log; +import android.util.Pair; import android.util.TypedValue; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -33,7 +34,8 @@ import android.widget.LinearLayout; import android.widget.ScrollView; import android.widget.TextView; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.fragment.app.Fragment; + import com.android.internal.widget.LinearLayoutWithDefaultTouchRecepient; import com.android.internal.widget.LockPatternUtils; import com.android.internal.widget.LockPatternUtils.RequestThrottledException; @@ -47,11 +49,14 @@ import com.android.settings.SetupWizardUtils; import com.android.settings.Utils; import com.android.settings.core.InstrumentedFragment; import com.android.settings.notification.RedactionInterstitial; -import com.android.setupwizardlib.GlifLayout; import com.google.android.collect.Lists; +import com.google.android.setupcompat.template.FooterBarMixin; +import com.google.android.setupcompat.template.FooterButton; +import com.google.android.setupdesign.GlifLayout; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -111,7 +116,7 @@ public class ChooseLockPattern extends SettingsActivity { return this; } - public IntentBuilder setPattern(String pattern) { + public IntentBuilder setPattern(byte[] pattern) { mIntent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD, pattern); return this; } @@ -121,6 +126,11 @@ public class ChooseLockPattern extends SettingsActivity { return this; } + public IntentBuilder setForFace(boolean forFace) { + mIntent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FACE, forFace); + return this; + } + public Intent build() { return mIntent; } @@ -140,12 +150,20 @@ public class ChooseLockPattern extends SettingsActivity { protected void onCreate(Bundle savedInstanceState) { // requestWindowFeature(Window.FEATURE_NO_TITLE); super.onCreate(savedInstanceState); - boolean forFingerprint = getIntent() + final boolean forFingerprint = getIntent() .getBooleanExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FINGERPRINT, false); - setTitle(forFingerprint ? R.string.lockpassword_choose_your_pattern_header_for_fingerprint - : R.string.lockpassword_choose_your_screen_lock_header); - LinearLayout layout = (LinearLayout) findViewById(R.id.content_parent); - layout.setFitsSystemWindows(false); + final boolean forFace = getIntent() + .getBooleanExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FACE, false); + + int msg = R.string.lockpassword_choose_your_screen_lock_header; + if (forFingerprint) { + msg = R.string.lockpassword_choose_your_pattern_header_for_fingerprint; + } else if (forFace) { + msg = R.string.lockpassword_choose_your_pattern_header_for_face; + } + + setTitle(msg); + findViewById(R.id.content_parent).setFitsSystemWindows(false); } @Override @@ -156,7 +174,7 @@ public class ChooseLockPattern extends SettingsActivity { } public static class ChooseLockPatternFragment extends InstrumentedFragment - implements View.OnClickListener, SaveAndFinishWorker.Listener { + implements SaveAndFinishWorker.Listener { public static final int CONFIRM_EXISTING_REQUEST = 55; @@ -166,11 +184,11 @@ public class ChooseLockPattern extends SettingsActivity { // how long we wait to clear a wrong pattern private static final int WRONG_PATTERN_CLEAR_TIMEOUT_MS = 2000; - private static final int ID_EMPTY_MESSAGE = -1; + protected static final int ID_EMPTY_MESSAGE = -1; private static final String FRAGMENT_TAG_SAVE_AND_FINISH = "save_and_finish_worker"; - private String mCurrentPattern; + private byte[] mCurrentPattern; private boolean mHasChallenge; private long mChallenge; protected TextView mTitleText; @@ -178,10 +196,9 @@ public class ChooseLockPattern extends SettingsActivity { protected TextView mMessageText; protected LockPatternView mLockPatternView; protected TextView mFooterText; - private TextView mFooterLeftButton; - private TextView mFooterRightButton; + protected FooterButton mSkipOrClearButton; + private FooterButton mNextButton; protected List<LockPatternView.Cell> mChosenPattern = null; - private boolean mHideDrawer = false; private ColorStateList mDefaultHeaderColorList; // ScrollView that contains title and header, only exist in land mode @@ -208,7 +225,7 @@ public class ChooseLockPattern extends SettingsActivity { getActivity().setResult(RESULT_FINISHED); getActivity().finish(); } else { - mCurrentPattern = data.getStringExtra( + mCurrentPattern = data.getByteArrayExtra( ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD); } @@ -218,11 +235,11 @@ public class ChooseLockPattern extends SettingsActivity { } protected void setRightButtonEnabled(boolean enabled) { - mFooterRightButton.setEnabled(enabled); + mNextButton.setEnabled(enabled); } protected void setRightButtonText(int text) { - mFooterRightButton.setText(text); + mNextButton.setText(getActivity(), text); } /** @@ -273,8 +290,7 @@ public class ChooseLockPattern extends SettingsActivity { mHeaderText.setTextColor(mDefaultHeaderColorList); } mFooterText.setText(""); - mFooterLeftButton.setEnabled(false); - mFooterRightButton.setEnabled(false); + mNextButton.setEnabled(false); if (mTitleHeaderScrollView != null) { mTitleHeaderScrollView.post(new Runnable() { @@ -289,7 +305,7 @@ public class ChooseLockPattern extends SettingsActivity { @Override public int getMetricsCategory() { - return MetricsEvent.CHOOSE_LOCK_PATTERN; + return SettingsEnums.CHOOSE_LOCK_PATTERN; } @@ -344,7 +360,7 @@ public class ChooseLockPattern extends SettingsActivity { protected enum Stage { Introduction( - R.string.lock_settings_picker_fingerprint_added_security_message, + R.string.lock_settings_picker_biometrics_added_security_message, R.string.lockpassword_choose_your_pattern_message, R.string.lockpattern_recording_intro_header, LeftButtonMode.Gone, RightButtonMode.ContinueDisabled, @@ -353,13 +369,13 @@ public class ChooseLockPattern extends SettingsActivity { ID_EMPTY_MESSAGE, ID_EMPTY_MESSAGE, R.string.lockpattern_settings_help_how_to_record, LeftButtonMode.Gone, RightButtonMode.Ok, ID_EMPTY_MESSAGE, false), ChoiceTooShort( - R.string.lock_settings_picker_fingerprint_added_security_message, + R.string.lock_settings_picker_biometrics_added_security_message, R.string.lockpassword_choose_your_pattern_message, R.string.lockpattern_recording_incorrect_too_short, LeftButtonMode.Retry, RightButtonMode.ContinueDisabled, ID_EMPTY_MESSAGE, true), FirstChoiceValid( - R.string.lock_settings_picker_fingerprint_added_security_message, + R.string.lock_settings_picker_biometrics_added_security_message, R.string.lockpassword_choose_your_pattern_message, R.string.lockpattern_pattern_entered_header, LeftButtonMode.Retry, RightButtonMode.Continue, ID_EMPTY_MESSAGE, false), @@ -377,7 +393,7 @@ public class ChooseLockPattern extends SettingsActivity { /** - * @param messageForFingerprint The message displayed at the top, above header for + * @param messageForBiometrics The message displayed at the top, above header for * fingerprint flow. * @param message The message displayed at the top. * @param headerMessage The message displayed at the top. @@ -386,12 +402,12 @@ public class ChooseLockPattern extends SettingsActivity { * @param footerMessage The footer message. * @param patternEnabled Whether the pattern widget is enabled. */ - Stage(int messageForFingerprint, int message, int headerMessage, + Stage(int messageForBiometrics, int message, int headerMessage, LeftButtonMode leftMode, RightButtonMode rightMode, int footerMessage, boolean patternEnabled) { this.headerMessage = headerMessage; - this.messageForFingerprint = messageForFingerprint; + this.messageForBiometrics = messageForBiometrics; this.message = message; this.leftMode = leftMode; this.rightMode = rightMode; @@ -400,7 +416,7 @@ public class ChooseLockPattern extends SettingsActivity { } final int headerMessage; - final int messageForFingerprint; + final int messageForBiometrics; final int message; final LeftButtonMode leftMode; final RightButtonMode rightMode; @@ -420,6 +436,7 @@ public class ChooseLockPattern extends SettingsActivity { private SaveAndFinishWorker mSaveAndFinishWorker; protected int mUserId; protected boolean mForFingerprint; + protected boolean mForFace; private static final String KEY_UI_STAGE = "uiStage"; private static final String KEY_PATTERN_CHOICE = "chosenPattern"; @@ -441,16 +458,17 @@ public class ChooseLockPattern extends SettingsActivity { SaveAndFinishWorker w = new SaveAndFinishWorker(); final boolean required = getActivity().getIntent().getBooleanExtra( EncryptionInterstitial.EXTRA_REQUIRE_PASSWORD, true); - String current = intent.getStringExtra( + byte[] current = intent.getByteArrayExtra( ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD); w.setBlocking(true); w.setListener(this); w.start(mChooseLockSettingsHelper.utils(), required, - false, 0, LockPatternUtils.stringToPattern(current), current, mUserId); + false, 0, LockPatternUtils.byteArrayToPattern(current), current, mUserId); } - mHideDrawer = getActivity().getIntent().getBooleanExtra(EXTRA_HIDE_DRAWER, false); mForFingerprint = intent.getBooleanExtra( ChooseLockSettingsHelper.EXTRA_KEY_FOR_FINGERPRINT, false); + mForFace = intent.getBooleanExtra( + ChooseLockSettingsHelper.EXTRA_KEY_FOR_FACE, false); } @Override @@ -460,26 +478,48 @@ public class ChooseLockPattern extends SettingsActivity { R.layout.choose_lock_pattern, container, false); layout.setHeaderText(getActivity().getTitle()); if (getResources().getBoolean(R.bool.config_lock_pattern_minimal_ui)) { - View iconView = layout.findViewById(R.id.suw_layout_icon); + View iconView = layout.findViewById(R.id.sud_layout_icon); if (iconView != null) { iconView.setVisibility(View.GONE); } } else { if (mForFingerprint) { layout.setIcon(getActivity().getDrawable(R.drawable.ic_fingerprint_header)); + } else if (mForFace) { + layout.setIcon(getActivity().getDrawable(R.drawable.ic_face_header)); } } + + final FooterBarMixin mixin = layout.getMixin(FooterBarMixin.class); + mixin.setSecondaryButton( + new FooterButton.Builder(getActivity()) + .setText(R.string.lockpattern_tutorial_cancel_label) + .setListener(this::onSkipOrClearButtonClick) + .setButtonType(FooterButton.ButtonType.OTHER) + .setTheme(R.style.SudGlifButton_Secondary) + .build() + ); + mixin.setPrimaryButton( + new FooterButton.Builder(getActivity()) + .setText(R.string.lockpattern_tutorial_continue_label) + .setListener(this::onNextButtonClick) + .setButtonType(FooterButton.ButtonType.NEXT) + .setTheme(R.style.SudGlifButton_Primary) + .build() + ); + mSkipOrClearButton = mixin.getSecondaryButton(); + mNextButton = mixin.getPrimaryButton(); + return layout; } - @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - mTitleText = view.findViewById(R.id.suw_layout_title); + mTitleText = view.findViewById(R.id.suc_layout_title); mHeaderText = (TextView) view.findViewById(R.id.headerText); mDefaultHeaderColorList = mHeaderText.getTextColors(); - mMessageText = view.findViewById(R.id.message); + mMessageText = view.findViewById(R.id.sud_layout_description); mLockPatternView = (LockPatternView) view.findViewById(R.id.lockPattern); mLockPatternView.setOnPatternListener(mChooseNewLockPatternListener); mLockPatternView.setTactileFeedbackEnabled( @@ -488,15 +528,9 @@ public class ChooseLockPattern extends SettingsActivity { mFooterText = (TextView) view.findViewById(R.id.footerText); - mFooterLeftButton = (TextView) view.findViewById(R.id.footerLeftButton); - mFooterRightButton = (TextView) view.findViewById(R.id.footerRightButton); - mTitleHeaderScrollView = (ScrollView) view.findViewById(R.id .scroll_layout_title_header); - mFooterLeftButton.setOnClickListener(this); - mFooterRightButton.setOnClickListener(this); - // make it so unhandled touch events within the unlock screen go to the // lock pattern view. final LinearLayoutWithDefaultTouchRecepient topLayout @@ -507,7 +541,8 @@ public class ChooseLockPattern extends SettingsActivity { final boolean confirmCredentials = getActivity().getIntent() .getBooleanExtra(ChooseLockGeneric.CONFIRM_CREDENTIALS, true); Intent intent = getActivity().getIntent(); - mCurrentPattern = intent.getStringExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD); + mCurrentPattern = + intent.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD); mHasChallenge = intent.getBooleanExtra( ChooseLockSettingsHelper.EXTRA_KEY_HAS_CHALLENGE, false); mChallenge = intent.getLongExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE, 0); @@ -530,13 +565,13 @@ public class ChooseLockPattern extends SettingsActivity { } } else { // restore from previous state - final String patternString = savedInstanceState.getString(KEY_PATTERN_CHOICE); - if (patternString != null) { - mChosenPattern = LockPatternUtils.stringToPattern(patternString); + final byte[] pattern = savedInstanceState.getByteArray(KEY_PATTERN_CHOICE); + if (pattern != null) { + mChosenPattern = LockPatternUtils.byteArrayToPattern(pattern); } if (mCurrentPattern == null) { - mCurrentPattern = savedInstanceState.getString(KEY_CURRENT_PATTERN); + mCurrentPattern = savedInstanceState.getByteArray(KEY_CURRENT_PATTERN); } updateStage(Stage.values()[savedInstanceState.getInt(KEY_UI_STAGE)]); @@ -605,12 +640,12 @@ public class ChooseLockPattern extends SettingsActivity { } } - public void onClick(View v) { - if (v == mFooterLeftButton) { - handleLeftButton(); - } else if (v == mFooterRightButton) { - handleRightButton(); - } + protected void onSkipOrClearButtonClick(View view) { + handleLeftButton(); + } + + protected void onNextButtonClick(View view) { + handleRightButton(); } public boolean onKeyDown(int keyCode, KeyEvent event) { @@ -632,13 +667,12 @@ public class ChooseLockPattern extends SettingsActivity { outState.putInt(KEY_UI_STAGE, mUiStage.ordinal()); if (mChosenPattern != null) { - outState.putString(KEY_PATTERN_CHOICE, - LockPatternUtils.patternToString(mChosenPattern)); + outState.putByteArray(KEY_PATTERN_CHOICE, + LockPatternUtils.patternToByteArray(mChosenPattern)); } if (mCurrentPattern != null) { - outState.putString(KEY_CURRENT_PATTERN, - mCurrentPattern); + outState.putByteArray(KEY_CURRENT_PATTERN, mCurrentPattern); } } @@ -663,7 +697,8 @@ public class ChooseLockPattern extends SettingsActivity { } else { mHeaderText.setText(stage.headerMessage); } - int message = mForFingerprint ? stage.messageForFingerprint : stage.message; + final boolean forBiometrics = mForFingerprint || mForFace; + int message = forBiometrics ? stage.messageForBiometrics : stage.message; if (message == ID_EMPTY_MESSAGE) { mMessageText.setText(""); } else { @@ -686,13 +721,13 @@ public class ChooseLockPattern extends SettingsActivity { mHeaderText.setTextColor(mDefaultHeaderColorList); } - if (stage == Stage.NeedToConfirm && mForFingerprint) { + if (stage == Stage.NeedToConfirm && forBiometrics) { mHeaderText.setText(""); mTitleText.setText(R.string.lockpassword_draw_your_pattern_again_header); } } - updateFooterLeftButton(stage, mFooterLeftButton); + updateFooterLeftButton(stage); setRightButtonText(stage.rightMode.text); setRightButtonEnabled(stage.rightMode.enabled); @@ -742,13 +777,13 @@ public class ChooseLockPattern extends SettingsActivity { } } - protected void updateFooterLeftButton(Stage stage, TextView footerLeftButton) { + protected void updateFooterLeftButton(Stage stage) { if (stage.leftMode == LeftButtonMode.Gone) { - footerLeftButton.setVisibility(View.GONE); + mSkipOrClearButton.setVisibility(View.GONE); } else { - footerLeftButton.setVisibility(View.VISIBLE); - footerLeftButton.setText(stage.leftMode.text); - footerLeftButton.setEnabled(stage.leftMode.enabled); + mSkipOrClearButton.setVisibility(View.VISIBLE); + mSkipOrClearButton.setText(getActivity(), stage.leftMode.text); + mSkipOrClearButton.setEnabled(stage.leftMode.enabled); } } @@ -784,10 +819,13 @@ public class ChooseLockPattern extends SettingsActivity { public void onChosenLockSaveFinished(boolean wasSecureBefore, Intent resultData) { getActivity().setResult(RESULT_FINISHED, resultData); + if (mCurrentPattern != null) { + Arrays.fill(mCurrentPattern, (byte) 0); + } + if (!wasSecureBefore) { Intent intent = getRedactionInterstitialIntent(getActivity()); if (intent != null) { - intent.putExtra(EXTRA_HIDE_DRAWER, mHideDrawer); startActivity(intent); } } @@ -798,12 +836,12 @@ public class ChooseLockPattern extends SettingsActivity { public static class SaveAndFinishWorker extends SaveChosenLockWorkerBase { private List<LockPatternView.Cell> mChosenPattern; - private String mCurrentPattern; + private byte[] mCurrentPattern; private boolean mLockVirgin; public void start(LockPatternUtils utils, boolean credentialRequired, boolean hasChallenge, long challenge, - List<LockPatternView.Cell> chosenPattern, String currentPattern, int userId) { + List<LockPatternView.Cell> chosenPattern, byte[] currentPattern, int userId) { prepare(utils, credentialRequired, hasChallenge, challenge, userId); mCurrentPattern = currentPattern; @@ -816,12 +854,11 @@ public class ChooseLockPattern extends SettingsActivity { } @Override - protected Intent saveAndVerifyInBackground() { - Intent result = null; + protected Pair<Boolean, Intent> saveAndVerifyInBackground() { final int userId = mUserId; - mUtils.saveLockPattern(mChosenPattern, mCurrentPattern, userId); - - if (mHasChallenge) { + final boolean success = mUtils.saveLockPattern(mChosenPattern, mCurrentPattern, userId); + Intent result = null; + if (success && mHasChallenge) { byte[] token; try { token = mUtils.verifyPattern(mChosenPattern, mChallenge, userId); @@ -836,8 +873,7 @@ public class ChooseLockPattern extends SettingsActivity { result = new Intent(); result.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token); } - - return result; + return Pair.create(success, result); } @Override diff --git a/src/com/android/settings/password/ChooseLockSettingsHelper.java b/src/com/android/settings/password/ChooseLockSettingsHelper.java index 4dafda6b05..28ded2d8bf 100644 --- a/src/com/android/settings/password/ChooseLockSettingsHelper.java +++ b/src/com/android/settings/password/ChooseLockSettingsHelper.java @@ -16,9 +16,10 @@ package com.android.settings.password; +import static com.android.settings.Utils.SETTINGS_PACKAGE_NAME; + import android.annotation.Nullable; import android.app.Activity; -import android.app.Fragment; import android.app.KeyguardManager; import android.app.admin.DevicePolicyManager; import android.content.Intent; @@ -26,11 +27,14 @@ import android.content.IntentSender; import android.os.Bundle; import android.os.UserManager; -import com.android.internal.annotations.VisibleForTesting; +import androidx.annotation.VisibleForTesting; +import androidx.fragment.app.Fragment; + import com.android.internal.widget.LockPatternUtils; -import com.android.settings.SettingsActivity; +import com.android.settings.SetupWizardUtils; import com.android.settings.Utils; -import com.android.setupwizardlib.util.WizardManagerHelper; + +import com.google.android.setupcompat.util.WizardManagerHelper; public final class ChooseLockSettingsHelper { @@ -41,9 +45,28 @@ public final class ChooseLockSettingsHelper { public static final String EXTRA_KEY_CHALLENGE = "challenge"; public static final String EXTRA_KEY_CHALLENGE_TOKEN = "hw_auth_token"; public static final String EXTRA_KEY_FOR_FINGERPRINT = "for_fingerprint"; + public static final String EXTRA_KEY_FOR_FACE = "for_face"; public static final String EXTRA_KEY_FOR_CHANGE_CRED_REQUIRED_FOR_BOOT = "for_cred_req_boot"; /** + * Intent extra for passing the requested min password complexity to later steps in the set new + * screen lock flow. + */ + public static final String EXTRA_KEY_REQUESTED_MIN_COMPLEXITY = "requested_min_complexity"; + + /** + * Intent extra for passing the label of the calling app to later steps in the set new screen + * lock flow. + */ + public static final String EXTRA_KEY_CALLER_APP_NAME = "caller_app_name"; + + /** + * Intent extra indicating that the calling app is an admin, such as a Device Adimn, Device + * Owner, or Profile Owner. + */ + public static final String EXTRA_KEY_IS_CALLING_APP_ADMIN = "is_calling_app_admin"; + + /** * When invoked via {@link ConfirmLockPassword.InternalActivity}, this flag * controls if we relax the enforcement of * {@link Utils#enforceSameOwner(android.content.Context, int)}. @@ -76,7 +99,13 @@ public final class ChooseLockSettingsHelper { * @see Activity#onActivityResult(int, int, android.content.Intent) */ public boolean launchConfirmationActivity(int request, CharSequence title) { - return launchConfirmationActivity(request, title, null, null, false, false); + return launchConfirmationActivity( + request /* request */, + title /* title */, + null /* header */, + null /* description */, + false /* returnCredentials */, + false /* external */); } /** @@ -89,7 +118,13 @@ public final class ChooseLockSettingsHelper { * @see Activity#onActivityResult(int, int, android.content.Intent) */ public boolean launchConfirmationActivity(int request, CharSequence title, boolean returnCredentials) { - return launchConfirmationActivity(request, title, null, null, returnCredentials, false); + return launchConfirmationActivity( + request /* request */, + title /* title */, + null /* header */, + null /* description */, + returnCredentials /* returnCredentials */, + false /* external */); } /** @@ -104,8 +139,16 @@ public final class ChooseLockSettingsHelper { */ public boolean launchConfirmationActivity(int request, CharSequence title, boolean returnCredentials, int userId) { - return launchConfirmationActivity(request, title, null, null, - returnCredentials, false, false, 0, Utils.enforceSameOwner(mActivity, userId)); + return launchConfirmationActivity( + request /* request */, + title /* title */, + null /* header */, + null /* description */, + returnCredentials /* returnCredentials */, + false /* external */, + false /* hasChallenge */, + 0 /* challenge */, + Utils.enforceSameOwner(mActivity, userId) /* userId */); } /** @@ -125,8 +168,16 @@ public final class ChooseLockSettingsHelper { boolean launchConfirmationActivity(int request, @Nullable CharSequence title, @Nullable CharSequence header, @Nullable CharSequence description, boolean returnCredentials, boolean external) { - return launchConfirmationActivity(request, title, header, description, - returnCredentials, external, false, 0, Utils.getCredentialOwnerUserId(mActivity)); + return launchConfirmationActivity( + request /* request */, + title /* title */, + header /* header */, + description /* description */, + returnCredentials /* returnCredentials */, + external /* external */, + false /* hasChallenge */, + 0 /* challenge */, + Utils.getCredentialOwnerUserId(mActivity) /* userId */); } /** @@ -147,8 +198,16 @@ public final class ChooseLockSettingsHelper { boolean launchConfirmationActivity(int request, @Nullable CharSequence title, @Nullable CharSequence header, @Nullable CharSequence description, boolean returnCredentials, boolean external, int userId) { - return launchConfirmationActivity(request, title, header, description, - returnCredentials, external, false, 0, Utils.enforceSameOwner(mActivity, userId)); + return launchConfirmationActivity( + request /* request */, + title /* title */, + header /* header */, + description /* description */, + returnCredentials /* returnCredentials */, + external /* external */, + false /* hasChallenge */, + 0 /* challenge */, + Utils.enforceSameOwner(mActivity, userId) /* userId */); } /** @@ -164,8 +223,16 @@ public final class ChooseLockSettingsHelper { public boolean launchConfirmationActivity(int request, @Nullable CharSequence title, @Nullable CharSequence header, @Nullable CharSequence description, long challenge) { - return launchConfirmationActivity(request, title, header, description, - true, false, true, challenge, Utils.getCredentialOwnerUserId(mActivity)); + return launchConfirmationActivity( + request /* request */, + title /* title */, + header /* header */, + description /* description */, + true /* returnCredentials */, + false /* external */, + true /* hasChallenge */, + challenge /* challenge */, + Utils.getCredentialOwnerUserId(mActivity) /* userId */); } /** @@ -182,8 +249,16 @@ public final class ChooseLockSettingsHelper { public boolean launchConfirmationActivity(int request, @Nullable CharSequence title, @Nullable CharSequence header, @Nullable CharSequence description, long challenge, int userId) { - return launchConfirmationActivity(request, title, header, description, - true, false, true, challenge, Utils.enforceSameOwner(mActivity, userId)); + return launchConfirmationActivity( + request /* request */, + title /* title */, + header /* header */, + description /* description */, + true /* returnCredentials */, + false /* external */, + true /* hasChallenge */, + challenge /* challenge */, + Utils.enforceSameOwner(mActivity, userId) /* userId */); } /** @@ -203,8 +278,16 @@ public final class ChooseLockSettingsHelper { public boolean launchConfirmationActivityWithExternalAndChallenge(int request, @Nullable CharSequence title, @Nullable CharSequence header, @Nullable CharSequence description, boolean external, long challenge, int userId) { - return launchConfirmationActivity(request, title, header, description, false, - external, true, challenge, Utils.enforceSameOwner(mActivity, userId)); + return launchConfirmationActivity( + request /* request */, + title /* title */, + header /* header */, + description /* description */, + false /* returnCredentials */, + external /* external */, + true /* hasChallenge */, + challenge /* challenge */, + Utils.enforceSameOwner(mActivity, userId) /* userId */); } /** @@ -217,31 +300,69 @@ public final class ChooseLockSettingsHelper { @Nullable CharSequence description, int userId) { final Bundle extras = new Bundle(); extras.putBoolean(EXTRA_ALLOW_ANY_USER, true); - return launchConfirmationActivity(request, title, header, description, false, - false, true, 0, userId, extras); + return launchConfirmationActivity( + request /* request */, + title /* title */, + header /* header */, + description /* description */, + false /* returnCredentials */, + false /* external */, + true /* hasChallenge */, + 0 /* challenge */, + userId /* userId */, + extras /* extras */); } private boolean launchConfirmationActivity(int request, @Nullable CharSequence title, @Nullable CharSequence header, @Nullable CharSequence description, boolean returnCredentials, boolean external, boolean hasChallenge, long challenge, int userId) { - return launchConfirmationActivity(request, title, header, description, returnCredentials, - external, hasChallenge, challenge, userId, null /* alternateButton */, null); + return launchConfirmationActivity( + request /* request */, + title /* title */, + header /* header */, + description /* description */, + returnCredentials /* returnCredentials */, + external /* external */, + hasChallenge /* hasChallenge */, + challenge /* challenge */, + userId /* userId */, + null /* alternateButton */, + null /* extras */); } private boolean launchConfirmationActivity(int request, @Nullable CharSequence title, @Nullable CharSequence header, @Nullable CharSequence description, boolean returnCredentials, boolean external, boolean hasChallenge, long challenge, int userId, Bundle extras) { - return launchConfirmationActivity(request, title, header, description, returnCredentials, - external, hasChallenge, challenge, userId, null /* alternateButton */, extras); + return launchConfirmationActivity( + request /* request */, + title /* title */, + header /* header */, + description /* description */, + returnCredentials /* returnCredentials */, + external /* external */, + hasChallenge /* hasChallenge */, + challenge /* challenge */, + userId /* userId */, + null /* alternateButton */, + extras /* extras */); } public boolean launchFrpConfirmationActivity(int request, @Nullable CharSequence header, @Nullable CharSequence description, @Nullable CharSequence alternateButton) { - return launchConfirmationActivity(request, null /* title */, header, description, - false /* returnCredentials */, true /* external */, false /* hasChallenge */, - 0 /* challenge */, LockPatternUtils.USER_FRP, alternateButton, null); + return launchConfirmationActivity( + request /* request */, + null /* title */, + header /* header */, + description /* description */, + false /* returnCredentials */, + true /* external */, + false /* hasChallenge */, + 0 /* challenge */, + LockPatternUtils.USER_FRP /* userId */, + alternateButton /* alternateButton */, + null /* extras */); } private boolean launchConfirmationActivity(int request, @Nullable CharSequence title, @@ -283,22 +404,20 @@ public final class ChooseLockSettingsHelper { intent.putExtra(ConfirmDeviceCredentialBaseFragment.TITLE_TEXT, title); intent.putExtra(ConfirmDeviceCredentialBaseFragment.HEADER_TEXT, header); intent.putExtra(ConfirmDeviceCredentialBaseFragment.DETAILS_TEXT, message); - intent.putExtra(ConfirmDeviceCredentialBaseFragment.ALLOW_FP_AUTHENTICATION, external); // TODO: Remove dark theme and show_cancel_button options since they are no longer used intent.putExtra(ConfirmDeviceCredentialBaseFragment.DARK_THEME, false); intent.putExtra(ConfirmDeviceCredentialBaseFragment.SHOW_CANCEL_BUTTON, false); intent.putExtra(ConfirmDeviceCredentialBaseFragment.SHOW_WHEN_LOCKED, external); + intent.putExtra(ConfirmDeviceCredentialBaseFragment.USE_FADE_ANIMATION, external); intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_RETURN_CREDENTIALS, returnCredentials); intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_HAS_CHALLENGE, hasChallenge); intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE, challenge); - // we should never have a drawer when confirming device credentials. - intent.putExtra(SettingsActivity.EXTRA_HIDE_DRAWER, true); intent.putExtra(Intent.EXTRA_USER_ID, userId); intent.putExtra(KeyguardManager.EXTRA_ALTERNATE_BUTTON_LABEL, alternateButton); if (extras != null) { intent.putExtras(extras); } - intent.setClassName(ConfirmDeviceCredentialBaseFragment.PACKAGE, activityClass.getName()); + intent.setClassName(SETTINGS_PACKAGE_NAME, activityClass.getName()); if (external) { intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); if (mFragment != null) { @@ -340,6 +459,7 @@ public final class ChooseLockSettingsHelper { } private void copyInternalExtras(Intent inIntent, Intent outIntent) { + SetupWizardUtils.copySetupExtras(inIntent, outIntent); String theme = inIntent.getStringExtra(WizardManagerHelper.EXTRA_THEME); if (theme != null) { outIntent.putExtra(WizardManagerHelper.EXTRA_THEME, theme); diff --git a/src/com/android/settings/password/ChooseLockTypeDialogFragment.java b/src/com/android/settings/password/ChooseLockTypeDialogFragment.java index 1050a549d5..8793471e20 100644 --- a/src/com/android/settings/password/ChooseLockTypeDialogFragment.java +++ b/src/com/android/settings/password/ChooseLockTypeDialogFragment.java @@ -17,11 +17,9 @@ package com.android.settings.password; import android.app.Activity; -import android.app.AlertDialog; -import android.app.AlertDialog.Builder; import android.app.Dialog; -import android.app.Fragment; import android.app.admin.DevicePolicyManager; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; @@ -34,12 +32,15 @@ import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.TextView; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.appcompat.app.AlertDialog.Builder; +import androidx.fragment.app.Fragment; + import com.android.internal.widget.LockPatternUtils; import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; import com.android.settings.password.ChooseLockGeneric.ChooseLockGenericFragment; -import com.android.setupwizardlib.util.WizardManagerHelper; + +import com.google.android.setupcompat.util.WizardManagerHelper; import java.util.List; @@ -132,13 +133,13 @@ public class ChooseLockTypeDialogFragment extends InstrumentedDialogFragment mAdapter = new ScreenLockAdapter(context, locks, mController); builder.setAdapter(mAdapter, this); builder.setTitle(R.string.setup_lock_settings_options_dialog_title); - AlertDialog alertDialog = builder.create(); + Dialog alertDialog = builder.create(); return alertDialog; } @Override public int getMetricsCategory() { - return MetricsEvent.SETTINGS_CHOOSE_LOCK_DIALOG; + return SettingsEnums.SETTINGS_CHOOSE_LOCK_DIALOG; } private static class ScreenLockAdapter extends ArrayAdapter<ScreenLockType> { diff --git a/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java b/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java index 65d72f11bb..8476f92429 100644 --- a/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java +++ b/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java @@ -17,31 +17,54 @@ package com.android.settings.password; +import static com.android.settings.Utils.SETTINGS_PACKAGE_NAME; + import android.app.Activity; import android.app.KeyguardManager; import android.app.admin.DevicePolicyManager; +import android.app.trust.TrustManager; import android.content.Context; import android.content.Intent; +import android.hardware.biometrics.BiometricConstants; +import android.hardware.biometrics.BiometricManager; +import android.hardware.biometrics.BiometricPrompt; +import android.hardware.biometrics.BiometricPrompt.AuthenticationCallback; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.UserHandle; import android.os.UserManager; import android.util.Log; +import androidx.annotation.NonNull; +import androidx.fragment.app.FragmentActivity; + import com.android.internal.widget.LockPatternUtils; +import com.android.settings.R; import com.android.settings.Utils; +import java.util.concurrent.Executor; + /** * Launch this when you want to confirm the user is present by asking them to enter their * PIN/password/pattern. */ -public class ConfirmDeviceCredentialActivity extends Activity { +public class ConfirmDeviceCredentialActivity extends FragmentActivity { public static final String TAG = ConfirmDeviceCredentialActivity.class.getSimpleName(); + // The normal flow that apps go through + private static final int CREDENTIAL_NORMAL = 1; + // Unlocks the managed profile when the primary profile is unlocked + private static final int CREDENTIAL_MANAGED = 2; + + private static final String TAG_BIOMETRIC_FRAGMENT = "fragment"; + public static class InternalActivity extends ConfirmDeviceCredentialActivity { } public static Intent createIntent(CharSequence title, CharSequence details) { Intent intent = new Intent(); - intent.setClassName("com.android.settings", + intent.setClassName(SETTINGS_PACKAGE_NAME, ConfirmDeviceCredentialActivity.class.getName()); intent.putExtra(KeyguardManager.EXTRA_TITLE, title); intent.putExtra(KeyguardManager.EXTRA_DESCRIPTION, details); @@ -50,7 +73,7 @@ public class ConfirmDeviceCredentialActivity extends Activity { public static Intent createIntent(CharSequence title, CharSequence details, long challenge) { Intent intent = new Intent(); - intent.setClassName("com.android.settings", + intent.setClassName(SETTINGS_PACKAGE_NAME, ConfirmDeviceCredentialActivity.class.getName()); intent.putExtra(KeyguardManager.EXTRA_TITLE, title); intent.putExtra(KeyguardManager.EXTRA_DESCRIPTION, details); @@ -59,57 +82,277 @@ public class ConfirmDeviceCredentialActivity extends Activity { return intent; } + private BiometricManager mBiometricManager; + private BiometricFragment mBiometricFragment; + private DevicePolicyManager mDevicePolicyManager; + private LockPatternUtils mLockPatternUtils; + private UserManager mUserManager; + private TrustManager mTrustManager; + private ChooseLockSettingsHelper mChooseLockSettingsHelper; + private Handler mHandler = new Handler(Looper.getMainLooper()); + private boolean mIsFallback; // BiometricPrompt fallback + private boolean mCCLaunched; + + private String mTitle; + private String mDetails; + private int mUserId; + private int mCredentialMode; + private boolean mGoingToBackground; + + private Executor mExecutor = (runnable -> { + mHandler.post(runnable); + }); + + private AuthenticationCallback mAuthenticationCallback = new AuthenticationCallback() { + public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) { + if (!mGoingToBackground) { + if (errorCode == BiometricPrompt.BIOMETRIC_ERROR_USER_CANCELED + || errorCode == BiometricPrompt.BIOMETRIC_ERROR_CANCELED) { + if (mIsFallback) { + mBiometricManager.onConfirmDeviceCredentialError( + errorCode, getStringForError(errorCode)); + } + finish(); + } else { + // All other errors go to some version of CC + showConfirmCredentials(); + } + } + } + + public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) { + mTrustManager.setDeviceLockedForUser(mUserId, false); + + ConfirmDeviceCredentialUtils.reportSuccessfulAttempt(mLockPatternUtils, mUserManager, + mUserId); + ConfirmDeviceCredentialUtils.checkForPendingIntent( + ConfirmDeviceCredentialActivity.this); + + if (mIsFallback) { + mBiometricManager.onConfirmDeviceCredentialSuccess(); + } + + setResult(Activity.RESULT_OK); + finish(); + } + }; + + private String getStringForError(int errorCode) { + switch (errorCode) { + case BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED: + return getString(com.android.internal.R.string.biometric_error_user_canceled); + case BiometricConstants.BIOMETRIC_ERROR_CANCELED: + return getString(com.android.internal.R.string.biometric_error_canceled); + default: + return null; + } + } + @Override - public void onCreate(Bundle savedInstanceState) { + protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + mBiometricManager = getSystemService(BiometricManager.class); + mDevicePolicyManager = getSystemService(DevicePolicyManager.class); + mUserManager = UserManager.get(this); + mTrustManager = getSystemService(TrustManager.class); + mLockPatternUtils = new LockPatternUtils(this); + Intent intent = getIntent(); - String title = intent.getStringExtra(KeyguardManager.EXTRA_TITLE); - String details = intent.getStringExtra(KeyguardManager.EXTRA_DESCRIPTION); + mTitle = intent.getStringExtra(KeyguardManager.EXTRA_TITLE); + mDetails = intent.getStringExtra(KeyguardManager.EXTRA_DESCRIPTION); String alternateButton = intent.getStringExtra( KeyguardManager.EXTRA_ALTERNATE_BUTTON_LABEL); boolean frp = KeyguardManager.ACTION_CONFIRM_FRP_CREDENTIAL.equals(intent.getAction()); - int userId = Utils.getCredentialOwnerUserId(this); + mUserId = UserHandle.myUserId(); if (isInternalActivity()) { try { - userId = Utils.getUserIdFromBundle(this, intent.getExtras()); + mUserId = Utils.getUserIdFromBundle(this, intent.getExtras()); } catch (SecurityException se) { Log.e(TAG, "Invalid intent extra", se); } } - final boolean isManagedProfile = UserManager.get(this).isManagedProfile(userId); + final int effectiveUserId = mUserManager.getCredentialOwnerProfile(mUserId); + final boolean isManagedProfile = UserManager.get(this).isManagedProfile(mUserId); // if the client app did not hand in a title and we are about to show the work challenge, // check whether there is a policy setting the organization name and use that as title - if ((title == null) && isManagedProfile) { - title = getTitleFromOrganizationName(userId); + if ((mTitle == null) && isManagedProfile) { + mTitle = getTitleFromOrganizationName(mUserId); } - ChooseLockSettingsHelper helper = new ChooseLockSettingsHelper(this); + mChooseLockSettingsHelper = new ChooseLockSettingsHelper(this); final LockPatternUtils lockPatternUtils = new LockPatternUtils(this); - boolean launched; + + Bundle bpBundle = + intent.getBundleExtra(KeyguardManager.EXTRA_BIOMETRIC_PROMPT_BUNDLE); + if (bpBundle != null) { + mIsFallback = true; + mTitle = bpBundle.getString(BiometricPrompt.KEY_TITLE); + mDetails = bpBundle.getString(BiometricPrompt.KEY_SUBTITLE); + } else { + bpBundle = new Bundle(); + bpBundle.putString(BiometricPrompt.KEY_TITLE, mTitle); + bpBundle.putString(BiometricPrompt.KEY_DESCRIPTION, mDetails); + } + + boolean launchedBiometric = false; + boolean launchedCDC = false; // If the target is a managed user and user key not unlocked yet, we will force unlock // tied profile so it will enable work mode and unlock managed profile, when personal // challenge is unlocked. if (frp) { - launched = helper.launchFrpConfirmationActivity(0, title, details, alternateButton); + launchedCDC = mChooseLockSettingsHelper.launchFrpConfirmationActivity( + 0, mTitle, mDetails, alternateButton); } else if (isManagedProfile && isInternalActivity() - && !lockPatternUtils.isSeparateProfileChallengeEnabled(userId)) { - // We set the challenge as 0L, so it will force to unlock managed profile when it - // unlocks primary profile screen lock, by calling verifyTiedProfileChallenge() - launched = helper.launchConfirmationActivityWithExternalAndChallenge( - 0 /* request code */, null /* title */, title, details, true /* isExternal */, - 0L /* challenge */, userId); + && !lockPatternUtils.isSeparateProfileChallengeEnabled(mUserId)) { + mCredentialMode = CREDENTIAL_MANAGED; + if (isBiometricAllowed(effectiveUserId, mUserId)) { + showBiometricPrompt(bpBundle); + launchedBiometric = true; + } else { + showConfirmCredentials(); + launchedCDC = true; + } } else { - launched = helper.launchConfirmationActivity(0 /* request code */, null /* title */, - title, details, false /* returnCredentials */, true /* isExternal */, userId); + mCredentialMode = CREDENTIAL_NORMAL; + if (isBiometricAllowed(effectiveUserId, mUserId)) { + // Don't need to check if biometrics / pin/pattern/pass are enrolled. It will go to + // onAuthenticationError and do the right thing automatically. + showBiometricPrompt(bpBundle); + launchedBiometric = true; + } else { + showConfirmCredentials(); + launchedCDC = true; + } } - if (!launched) { + + if (launchedCDC) { + finish(); + } else if (launchedBiometric) { + // Keep this activity alive until BiometricPrompt goes away + } else { Log.d(TAG, "No pattern, password or PIN set."); setResult(Activity.RESULT_OK); + finish(); + } + } + + @Override + protected void onStart() { + super.onStart(); + // Translucent activity that is "visible", so it doesn't complain about finish() + // not being called before onResume(). + setVisible(true); + } + + @Override + public void onPause() { + super.onPause(); + if (!isChangingConfigurations()) { + mGoingToBackground = true; + if (mBiometricFragment != null) { + Log.d(TAG, "Authenticating: " + mBiometricFragment.isAuthenticating()); + if (mBiometricFragment.isAuthenticating()) { + mBiometricFragment.cancel(); + } + } + + if (mIsFallback && !mCCLaunched) { + mBiometricManager.onConfirmDeviceCredentialError( + BiometricConstants.BIOMETRIC_ERROR_CANCELED, + getString(com.android.internal.R.string.biometric_error_user_canceled)); + } + + finish(); + } else { + mGoingToBackground = false; + } + } + + // User could be locked while Effective user is unlocked even though the effective owns the + // credential. Otherwise, biometric can't unlock fbe/keystore through + // verifyTiedProfileChallenge. In such case, we also wanna show the user message that + // biometric is disabled due to device restart. + private boolean isStrongAuthRequired(int effectiveUserId) { + return !mLockPatternUtils.isBiometricAllowedForUser(effectiveUserId) + || !mUserManager.isUserUnlocked(mUserId); + } + + private boolean isBiometricDisabledByAdmin(int effectiveUserId) { + final int disabledFeatures = + mDevicePolicyManager.getKeyguardDisabledFeatures(null, effectiveUserId); + return (disabledFeatures & DevicePolicyManager.KEYGUARD_DISABLE_BIOMETRICS) != 0; + } + + private boolean isBiometricAllowed(int effectiveUserId, int realUserId) { + return !isStrongAuthRequired(effectiveUserId) + && !isBiometricDisabledByAdmin(effectiveUserId) + && !mLockPatternUtils.hasPendingEscrowToken(realUserId); + } + + private void showBiometricPrompt(Bundle bundle) { + mBiometricManager.setActiveUser(mUserId); + + mBiometricFragment = (BiometricFragment) getSupportFragmentManager() + .findFragmentByTag(TAG_BIOMETRIC_FRAGMENT); + boolean newFragment = false; + + if (mBiometricFragment == null) { + mBiometricFragment = BiometricFragment.newInstance(bundle); + newFragment = true; + } + mBiometricFragment.setCallbacks(mExecutor, mAuthenticationCallback); + mBiometricFragment.setUser(mUserId); + + if (newFragment) { + getSupportFragmentManager().beginTransaction() + .add(mBiometricFragment, TAG_BIOMETRIC_FRAGMENT).commit(); + } + } + + /** + * Shows ConfirmDeviceCredentials for normal apps. + */ + private void showConfirmCredentials() { + mCCLaunched = true; + boolean launched = false; + // The only difference between CREDENTIAL_MANAGED and CREDENTIAL_NORMAL is that for + // CREDENTIAL_MANAGED, we launch the real confirm credential activity with an explicit + // but dummy challenge value (0L). This will result in ConfirmLockPassword calling + // verifyTiedProfileChallenge() (if it's a profile with unified challenge), due to the + // difference between ConfirmLockPassword.startVerifyPassword() and + // ConfirmLockPassword.startCheckPassword(). Calling verifyTiedProfileChallenge() here is + // necessary when this is part of the turning on work profile flow, because it forces + // unlocking the work profile even before the profile is running. + // TODO: Remove the duplication of checkPassword and verifyPassword in ConfirmLockPassword, + // LockPatternChecker and LockPatternUtils. verifyPassword should be the only API to use, + // which optionally accepts a challenge. + if (mCredentialMode == CREDENTIAL_MANAGED) { + launched = mChooseLockSettingsHelper + .launchConfirmationActivityWithExternalAndChallenge( + 0 /* request code */, null /* title */, mTitle, mDetails, + true /* isExternal */, 0L /* challenge */, mUserId); + } else if (mCredentialMode == CREDENTIAL_NORMAL){ + launched = mChooseLockSettingsHelper.launchConfirmationActivity( + 0 /* request code */, null /* title */, + mTitle, mDetails, false /* returnCredentials */, true /* isExternal */, + mUserId); + } + if (!launched) { + Log.d(TAG, "No pin/pattern/pass set"); + setResult(Activity.RESULT_OK); } finish(); } + @Override + public void finish() { + super.finish(); + // Finish without animation since the activity is just there so we can launch + // BiometricPrompt. + overridePendingTransition(R.anim.confirm_credential_biometric_transition_enter, 0); + } + private boolean isInternalActivity() { return this instanceof ConfirmDeviceCredentialActivity.InternalActivity; } diff --git a/src/com/android/settings/password/ConfirmDeviceCredentialBaseActivity.java b/src/com/android/settings/password/ConfirmDeviceCredentialBaseActivity.java index 1775394a94..998b3fcdd1 100644 --- a/src/com/android/settings/password/ConfirmDeviceCredentialBaseActivity.java +++ b/src/com/android/settings/password/ConfirmDeviceCredentialBaseActivity.java @@ -16,14 +16,19 @@ package com.android.settings.password; -import android.app.Fragment; import android.app.KeyguardManager; +import android.hardware.biometrics.BiometricConstants; +import android.hardware.biometrics.BiometricManager; +import android.hardware.biometrics.IBiometricConfirmDeviceCredentialCallback; import android.os.Bundle; import android.os.UserManager; +import android.util.Log; import android.view.MenuItem; import android.view.WindowManager; import android.widget.LinearLayout; +import androidx.fragment.app.Fragment; + import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.SetupWizardUtils; @@ -32,6 +37,7 @@ import com.android.settings.Utils; public abstract class ConfirmDeviceCredentialBaseActivity extends SettingsActivity { private static final String STATE_IS_KEYGUARD_LOCKED = "STATE_IS_KEYGUARD_LOCKED"; + private static final String TAG = "ConfirmDeviceCredentialBaseActivity"; enum ConfirmCredentialTheme { NORMAL, @@ -44,6 +50,16 @@ public abstract class ConfirmDeviceCredentialBaseActivity extends SettingsActivi private boolean mFirstTimeVisible = true; private boolean mIsKeyguardLocked = false; private ConfirmCredentialTheme mConfirmCredentialTheme; + private BiometricManager mBiometricManager; + + // TODO(b/123378871): Remove when moved. + private final IBiometricConfirmDeviceCredentialCallback mCancelCallback + = new IBiometricConfirmDeviceCredentialCallback.Stub() { + @Override + public void cancel() { + finish(); + } + }; private boolean isInternalActivity() { return (this instanceof ConfirmLockPassword.InternalActivity) @@ -52,8 +68,15 @@ public abstract class ConfirmDeviceCredentialBaseActivity extends SettingsActivi @Override protected void onCreate(Bundle savedState) { - int credentialOwnerUserId = Utils.getCredentialOwnerUserId(this, - Utils.getUserIdFromBundle(this, getIntent().getExtras(), isInternalActivity())); + final int credentialOwnerUserId; + try { + credentialOwnerUserId = Utils.getCredentialOwnerUserId(this, + Utils.getUserIdFromBundle(this, getIntent().getExtras(), isInternalActivity())); + } catch (SecurityException e) { + Log.e(TAG, "Invalid user Id supplied", e); + finish(); + return; + } if (UserManager.get(this).isManagedProfile(credentialOwnerUserId)) { setTheme(R.style.Theme_ConfirmDeviceCredentialsWork); mConfirmCredentialTheme = ConfirmCredentialTheme.WORK; @@ -67,11 +90,13 @@ public abstract class ConfirmDeviceCredentialBaseActivity extends SettingsActivi } super.onCreate(savedState); + mBiometricManager = getSystemService(BiometricManager.class); + mBiometricManager.registerCancellationCallback(mCancelCallback); + if (mConfirmCredentialTheme == ConfirmCredentialTheme.NORMAL) { // Prevent the content parent from consuming the window insets because GlifLayout uses // it to show the status bar background. - LinearLayout layout = (LinearLayout) findViewById(R.id.content_parent); - layout.setFitsSystemWindows(false); + findViewById(R.id.content_parent).setFitsSystemWindows(false); } getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE); mIsKeyguardLocked = savedState == null @@ -123,7 +148,7 @@ public abstract class ConfirmDeviceCredentialBaseActivity extends SettingsActivi } private ConfirmDeviceCredentialBaseFragment getFragment() { - Fragment fragment = getFragmentManager().findFragmentById(R.id.main_content); + Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.main_content); if (fragment != null && fragment instanceof ConfirmDeviceCredentialBaseFragment) { return (ConfirmDeviceCredentialBaseFragment) fragment; } @@ -139,6 +164,26 @@ public abstract class ConfirmDeviceCredentialBaseActivity extends SettingsActivi } } + @Override + public void onStop() { + super.onStop(); + // TODO(b/123378871): Remove when moved. + if (!isChangingConfigurations()) { + mBiometricManager.onConfirmDeviceCredentialError( + BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED, + getString(com.android.internal.R.string.biometric_error_user_canceled)); + } + } + + @Override + public void finish() { + super.finish(); + if (getIntent().getBooleanExtra( + ConfirmDeviceCredentialBaseFragment.USE_FADE_ANIMATION, false)) { + overridePendingTransition(0, R.anim.confirm_credential_biometric_transition_exit); + } + } + public void prepareEnterAnimation() { getFragment().prepareEnterAnimation(); } diff --git a/src/com/android/settings/password/ConfirmDeviceCredentialBaseFragment.java b/src/com/android/settings/password/ConfirmDeviceCredentialBaseFragment.java index 3f2675029d..bb953a133a 100644 --- a/src/com/android/settings/password/ConfirmDeviceCredentialBaseFragment.java +++ b/src/com/android/settings/password/ConfirmDeviceCredentialBaseFragment.java @@ -17,29 +17,23 @@ // TODO (b/35202196): move this class out of the root of the package. package com.android.settings.password; +import static com.android.settings.Utils.SETTINGS_PACKAGE_NAME; + import android.annotation.Nullable; -import android.app.ActivityManager; -import android.app.ActivityOptions; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.DialogFragment; -import android.app.FragmentManager; -import android.app.IActivityManager; import android.app.KeyguardManager; import android.app.admin.DevicePolicyManager; -import android.app.trust.TrustManager; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; -import android.content.IntentSender; import android.content.pm.UserInfo; import android.graphics.Point; import android.graphics.PorterDuff; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; +import android.hardware.biometrics.BiometricManager; import android.os.Bundle; import android.os.Handler; -import android.os.RemoteException; import android.os.UserManager; import android.text.TextUtils; import android.view.View; @@ -49,29 +43,30 @@ import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.FragmentManager; + import com.android.internal.widget.LockPatternUtils; import com.android.settings.R; import com.android.settings.Utils; import com.android.settings.core.InstrumentedFragment; -import com.android.settings.fingerprint.FingerprintUiHelper; /** * Base fragment to be shared for PIN/Pattern/Password confirmation fragments. */ -public abstract class ConfirmDeviceCredentialBaseFragment extends InstrumentedFragment - implements FingerprintUiHelper.Callback { - - public static final String PACKAGE = "com.android.settings"; - public static final String TITLE_TEXT = PACKAGE + ".ConfirmCredentials.title"; - public static final String HEADER_TEXT = PACKAGE + ".ConfirmCredentials.header"; - public static final String DETAILS_TEXT = PACKAGE + ".ConfirmCredentials.details"; - public static final String ALLOW_FP_AUTHENTICATION = - PACKAGE + ".ConfirmCredentials.allowFpAuthentication"; - public static final String DARK_THEME = PACKAGE + ".ConfirmCredentials.darkTheme"; +public abstract class ConfirmDeviceCredentialBaseFragment extends InstrumentedFragment { + + public static final String TITLE_TEXT = SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.title"; + public static final String HEADER_TEXT = SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.header"; + public static final String DETAILS_TEXT = SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.details"; + public static final String DARK_THEME = SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.darkTheme"; public static final String SHOW_CANCEL_BUTTON = - PACKAGE + ".ConfirmCredentials.showCancelButton"; + SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.showCancelButton"; public static final String SHOW_WHEN_LOCKED = - PACKAGE + ".ConfirmCredentials.showWhenLocked"; + SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.showWhenLocked"; + public static final String USE_FADE_ANIMATION = + SETTINGS_PACKAGE_NAME + ".ConfirmCredentials.useFadeAnimation"; protected static final int USER_TYPE_PRIMARY = 1; protected static final int USER_TYPE_MANAGED_PROFILE = 2; @@ -80,10 +75,8 @@ public abstract class ConfirmDeviceCredentialBaseFragment extends InstrumentedFr /** Time we wait before clearing a wrong input attempt (e.g. pattern) and the error message. */ protected static final long CLEAR_WRONG_ATTEMPT_TIMEOUT_MS = 3000; - private FingerprintUiHelper mFingerprintHelper; protected boolean mReturnCredentials = false; protected Button mCancelButton; - protected ImageView mFingerprintIcon; protected int mEffectiveUserId; protected int mUserId; protected UserManager mUserManager; @@ -93,6 +86,7 @@ public abstract class ConfirmDeviceCredentialBaseFragment extends InstrumentedFr protected final Handler mHandler = new Handler(); protected boolean mFrp; private CharSequence mFrpAlternateButtonText; + protected BiometricManager mBiometricManager; private boolean isInternalActivity() { return (getActivity() instanceof ConfirmLockPassword.InternalActivity) @@ -116,16 +110,14 @@ public abstract class ConfirmDeviceCredentialBaseFragment extends InstrumentedFr mLockPatternUtils = new LockPatternUtils(getActivity()); mDevicePolicyManager = (DevicePolicyManager) getActivity().getSystemService( Context.DEVICE_POLICY_SERVICE); + mBiometricManager = getActivity().getSystemService(BiometricManager.class); } @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); mCancelButton = (Button) view.findViewById(R.id.cancelButton); - mFingerprintIcon = (ImageView) view.findViewById(R.id.fingerprintIcon); - mFingerprintHelper = new FingerprintUiHelper( - mFingerprintIcon, - (TextView) view.findViewById(R.id.errorText), this, mEffectiveUserId); + boolean showCancelButton = getActivity().getIntent().getBooleanExtra( SHOW_CANCEL_BUTTON, false); boolean hasAlternateButton = mFrp && !TextUtils.isEmpty(mFrpAlternateButtonText); @@ -153,29 +145,16 @@ public abstract class ConfirmDeviceCredentialBaseFragment extends InstrumentedFr } } - private boolean isFingerprintDisabledByAdmin() { - final int disabledFeatures = - mDevicePolicyManager.getKeyguardDisabledFeatures(null, mEffectiveUserId); - return (disabledFeatures & DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT) != 0; - } - // User could be locked while Effective user is unlocked even though the effective owns the // credential. Otherwise, fingerprint can't unlock fbe/keystore through // verifyTiedProfileChallenge. In such case, we also wanna show the user message that // fingerprint is disabled due to device restart. protected boolean isStrongAuthRequired() { return mFrp - || !mLockPatternUtils.isFingerprintAllowedForUser(mEffectiveUserId) + || !mLockPatternUtils.isBiometricAllowedForUser(mEffectiveUserId) || !mUserManager.isUserUnlocked(mUserId); } - private boolean isFingerprintAllowed() { - return !mReturnCredentials - && getActivity().getIntent().getBooleanExtra(ALLOW_FP_AUTHENTICATION, false) - && !isStrongAuthRequired() - && !isFingerprintDisabledByAdmin(); - } - @Override public void onResume() { super.onResume(); @@ -183,13 +162,6 @@ public abstract class ConfirmDeviceCredentialBaseFragment extends InstrumentedFr } protected void refreshLockScreen() { - if (isFingerprintAllowed()) { - mFingerprintHelper.startListening(); - } else { - if (mFingerprintHelper.isListening()) { - mFingerprintHelper.stopListening(); - } - } updateErrorMessage(mLockPatternUtils.getCurrentFailedPasswordAttempts(mEffectiveUserId)); } @@ -214,28 +186,10 @@ public abstract class ConfirmDeviceCredentialBaseFragment extends InstrumentedFr @Override public void onPause() { super.onPause(); - if (mFingerprintHelper.isListening()) { - mFingerprintHelper.stopListening(); - } - } - - @Override - public void onAuthenticated() { - // Check whether we are still active. - if (getActivity() != null && getActivity().isResumed()) { - TrustManager trustManager = - (TrustManager) getActivity().getSystemService(Context.TRUST_SERVICE); - trustManager.setDeviceLockedForUser(mEffectiveUserId, false); - authenticationSucceeded(); - checkForPendingIntent(); - } } protected abstract void authenticationSucceeded(); - @Override - public void onFingerprintIconVisibilityChanged(boolean visible) { - } public void prepareEnterAnimation() { } @@ -243,29 +197,6 @@ public abstract class ConfirmDeviceCredentialBaseFragment extends InstrumentedFr public void startEnterAnimation() { } - protected void checkForPendingIntent() { - int taskId = getActivity().getIntent().getIntExtra(Intent.EXTRA_TASK_ID, -1); - if (taskId != -1) { - try { - IActivityManager activityManager = ActivityManager.getService(); - final ActivityOptions options = ActivityOptions.makeBasic(); - activityManager.startActivityFromRecents(taskId, options.toBundle()); - return; - } catch (RemoteException e) { - // Do nothing. - } - } - IntentSender intentSender = getActivity().getIntent() - .getParcelableExtra(Intent.EXTRA_INTENT); - if (intentSender != null) { - try { - getActivity().startIntentSenderForResult(intentSender, -1, null, 0, 0, 0); - } catch (IntentSender.SendIntentException e) { - /* ignore */ - } - } - } - private void setWorkChallengeBackground(View baseView, int userId) { View mainContent = getActivity().findViewById(com.android.settings.R.id.main_content); if (mainContent != null) { @@ -290,15 +221,6 @@ public abstract class ConfirmDeviceCredentialBaseFragment extends InstrumentedFr } } - protected void reportSuccessfulAttempt() { - mLockPatternUtils.reportSuccessfulPasswordAttempt(mEffectiveUserId); - if (mUserManager.isManagedProfile(mEffectiveUserId)) { - // Keyguard is responsible to disable StrongAuth for primary user. Disable StrongAuth - // for work challenge only here. - mLockPatternUtils.userPresent(mEffectiveUserId); - } - } - protected void reportFailedAttempt() { updateErrorMessage( mLockPatternUtils.getCurrentFailedPasswordAttempts(mEffectiveUserId) + 1); diff --git a/src/com/android/settings/password/ConfirmDeviceCredentialUtils.java b/src/com/android/settings/password/ConfirmDeviceCredentialUtils.java new file mode 100644 index 0000000000..11d6924620 --- /dev/null +++ b/src/com/android/settings/password/ConfirmDeviceCredentialUtils.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2018 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.password; + +import android.app.Activity; +import android.app.ActivityManager; +import android.app.ActivityOptions; +import android.app.IActivityManager; +import android.content.Intent; +import android.content.IntentSender; +import android.os.RemoteException; +import android.os.UserManager; + +import com.android.internal.widget.LockPatternUtils; + +/** Class containing methods shared between CDCA and CDCBA */ +public class ConfirmDeviceCredentialUtils { + + public static void checkForPendingIntent(Activity activity) { + // See Change-Id I52c203735fa9b53fd2f7df971824747eeb930f36 for context + int taskId = activity.getIntent().getIntExtra(Intent.EXTRA_TASK_ID, -1); + if (taskId != -1) { + try { + IActivityManager activityManager = ActivityManager.getService(); + final ActivityOptions options = ActivityOptions.makeBasic(); + activityManager.startActivityFromRecents(taskId, options.toBundle()); + return; + } catch (RemoteException e) { + // Do nothing. + } + } + IntentSender intentSender = activity.getIntent().getParcelableExtra(Intent.EXTRA_INTENT); + if (intentSender != null) { + try { + activity.startIntentSenderForResult(intentSender, -1, null, 0, 0, 0); + } catch (IntentSender.SendIntentException e) { + /* ignore */ + } + } + } + + public static void reportSuccessfulAttempt(LockPatternUtils utils, UserManager userManager, + int userId) { + utils.reportSuccessfulPasswordAttempt(userId); + if (userManager.isManagedProfile(userId)) { + // Keyguard is responsible to disable StrongAuth for primary user. Disable StrongAuth + // for work challenge only here. + utils.userPresent(userId); + } + } +} diff --git a/src/com/android/settings/password/ConfirmLockPassword.java b/src/com/android/settings/password/ConfirmLockPassword.java index a7059da9ea..2992ebeb8f 100644 --- a/src/com/android/settings/password/ConfirmLockPassword.java +++ b/src/com/android/settings/password/ConfirmLockPassword.java @@ -16,8 +16,8 @@ package com.android.settings.password; -import android.app.Fragment; import android.app.admin.DevicePolicyManager; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; import android.graphics.Typeface; @@ -40,7 +40,8 @@ import android.view.inputmethod.InputMethodManager; import android.widget.TextView; import android.widget.TextView.OnEditorActionListener; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.fragment.app.Fragment; + import com.android.internal.widget.LockPatternChecker; import com.android.internal.widget.LockPatternUtils; import com.android.internal.widget.TextViewInputDisabler; @@ -84,7 +85,7 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); - Fragment fragment = getFragmentManager().findFragmentById(R.id.main_content); + Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.main_content); if (fragment != null && fragment instanceof ConfirmLockPasswordFragment) { ((ConfirmLockPasswordFragment)fragment).onWindowFocusChanged(hasFocus); } @@ -104,7 +105,6 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { private CountDownTimer mCountdownTimer; private boolean mIsAlpha; private InputMethodManager mImm; - private boolean mUsingFingerprint = false; private AppearAnimationUtils mAppearAnimationUtils; private DisappearAnimationUtils mDisappearAnimationUtils; @@ -135,9 +135,9 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { mHeaderTextView = (TextView) view.findViewById(R.id.headerText); if (mHeaderTextView == null) { - mHeaderTextView = view.findViewById(R.id.suw_layout_title); + mHeaderTextView = view.findViewById(R.id.suc_layout_title); } - mDetailsTextView = (TextView) view.findViewById(R.id.detailsText); + mDetailsTextView = (TextView) view.findViewById(R.id.sud_layout_description); mErrorTextView = (TextView) view.findViewById(R.id.errorText); mIsAlpha = DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC == storedQuality || DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC == storedQuality @@ -242,7 +242,6 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { mCancelButton.setAlpha(0f); mPasswordEntry.setAlpha(0f); mErrorTextView.setAlpha(0f); - mFingerprintIcon.setAlpha(0f); } private View[] getActiveViews() { @@ -254,9 +253,6 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { } result.add(mPasswordEntry); result.add(mErrorTextView); - if (mFingerprintIcon.getVisibility() == View.VISIBLE) { - result.add(mFingerprintIcon); - } return result.toArray(new View[] {}); } @@ -278,7 +274,7 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { @Override public int getMetricsCategory() { - return MetricsEvent.CONFIRM_LOCK_PASSWORD; + return SettingsEnums.CONFIRM_LOCK_PASSWORD; } @Override @@ -302,17 +298,12 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { mCredentialCheckResultTracker.setResult(true, new Intent(), 0, mEffectiveUserId); } - @Override - public void onFingerprintIconVisibilityChanged(boolean visible) { - mUsingFingerprint = visible; - } - private void updatePasswordEntry() { final boolean isLockedOut = mLockPatternUtils.getLockoutAttemptDeadline(mEffectiveUserId) != 0; mPasswordEntry.setEnabled(!isLockedOut); mPasswordEntryInputDisabler.setInputEnabled(!isLockedOut); - if (isLockedOut || mUsingFingerprint) { + if (isLockedOut) { mImm.hideSoftInputFromWindow(mPasswordEntry.getWindowToken(), 0 /*flags*/); } else { mPasswordEntry.scheduleShowSoftInput(); @@ -332,8 +323,9 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { return; } - final String pin = mPasswordEntry.getText().toString(); - if (TextUtils.isEmpty(pin)) { + // TODO(b/120484642): This is a point of entry for passwords from the UI + final byte[] pin = LockPatternUtils.charSequenceToByteArray(mPasswordEntry.getText()); + if (pin == null || pin.length == 0) { return; } @@ -359,7 +351,7 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { return getActivity() instanceof ConfirmLockPassword.InternalActivity; } - private void startVerifyPassword(final String pin, final Intent intent) { + private void startVerifyPassword(final byte[] pin, final Intent intent) { long challenge = getActivity().getIntent().getLongExtra( ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE, 0); final int localEffectiveUserId = mEffectiveUserId; @@ -390,7 +382,7 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { onVerifyCallback); } - private void startCheckPassword(final String pin, final Intent intent) { + private void startCheckPassword(final byte[] pin, final Intent intent) { final int localEffectiveUserId = mEffectiveUserId; mPendingLockCheck = LockPatternChecker.checkPassword( mLockPatternUtils, @@ -443,10 +435,12 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { mPasswordEntryInputDisabler.setInputEnabled(true); if (matched) { if (newResult) { - reportSuccessfulAttempt(); + ConfirmDeviceCredentialUtils.reportSuccessfulAttempt(mLockPatternUtils, + mUserManager, mEffectiveUserId); } + mBiometricManager.onConfirmDeviceCredentialSuccess(); startDisappearAnimation(intent); - checkForPendingIntent(); + ConfirmDeviceCredentialUtils.checkForPendingIntent(getActivity()); } else { if (timeoutMs > 0) { refreshLockScreen(); @@ -498,15 +492,11 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { } public void onClick(View v) { - switch (v.getId()) { - case R.id.next_button: - handleNext(); - break; - - case R.id.cancel_button: - getActivity().setResult(RESULT_CANCELED); - getActivity().finish(); - break; + if (v.getId() == R.id.next_button) { + handleNext(); + } else if (v.getId() == R.id.cancel_button) { + getActivity().setResult(RESULT_CANCELED); + getActivity().finish(); } } diff --git a/src/com/android/settings/password/ConfirmLockPattern.java b/src/com/android/settings/password/ConfirmLockPattern.java index 84db5408f8..f43ee7f465 100644 --- a/src/com/android/settings/password/ConfirmLockPattern.java +++ b/src/com/android/settings/password/ConfirmLockPattern.java @@ -17,6 +17,7 @@ package com.android.settings.password; import android.app.Activity; +import android.app.settings.SettingsEnums; import android.content.Intent; import android.os.AsyncTask; import android.os.Bundle; @@ -31,7 +32,6 @@ import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import android.widget.TextView; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.widget.LinearLayoutWithDefaultTouchRecepient; import com.android.internal.widget.LockPatternChecker; import com.android.internal.widget.LockPatternUtils; @@ -116,7 +116,7 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { false); mHeaderTextView = (TextView) view.findViewById(R.id.headerText); mLockPatternView = (LockPatternView) view.findViewById(R.id.lockPattern); - mDetailsTextView = (TextView) view.findViewById(R.id.detailsText); + mDetailsTextView = (TextView) view.findViewById(R.id.sud_layout_description); mErrorTextView = (TextView) view.findViewById(R.id.errorText); mLeftSpacerLandscape = view.findViewById(R.id.leftSpacer); mRightSpacerLandscape = view.findViewById(R.id.rightSpacer); @@ -200,7 +200,7 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { @Override public int getMetricsCategory() { - return MetricsEvent.CONFIRM_LOCK_PATTERN; + return SettingsEnums.CONFIRM_LOCK_PATTERN; } @Override @@ -231,7 +231,6 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { mCancelButton.setAlpha(0f); mLockPatternView.setAlpha(0f); mDetailsTextView.setAlpha(0f); - mFingerprintIcon.setAlpha(0f); } private int getDefaultDetails() { @@ -265,9 +264,6 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { } result.add(row); } - if (mFingerprintIcon.getVisibility() == View.VISIBLE) { - result.add(new ArrayList<Object>(Collections.singletonList(mFingerprintIcon))); - } Object[][] resultArr = new Object[result.size()][cellStates[0].length]; for (int i = 0; i < result.size(); i++) { ArrayList<Object> row = result.get(i); @@ -377,16 +373,6 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { } } - @Override - public void onFingerprintIconVisibilityChanged(boolean visible) { - if (mLeftSpacerLandscape != null && mRightSpacerLandscape != null) { - - // In landscape, adjust spacing depending on fingerprint icon visibility. - mLeftSpacerLandscape.setVisibility(visible ? View.GONE : View.VISIBLE); - mRightSpacerLandscape.setVisibility(visible ? View.GONE : View.VISIBLE); - } - } - /** * The pattern listener that responds according to a user confirming * an existing lock pattern. @@ -462,7 +448,7 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { mLockPatternUtils, pattern, challenge, localUserId, onVerifyCallback) : LockPatternChecker.verifyTiedProfileChallenge( - mLockPatternUtils, LockPatternUtils.patternToString(pattern), + mLockPatternUtils, LockPatternUtils.patternToByteArray(pattern), true, challenge, localUserId, onVerifyCallback); } @@ -487,7 +473,7 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_TYPE, StorageManager.CRYPT_TYPE_PATTERN); intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD, - LockPatternUtils.patternToString(pattern)); + LockPatternUtils.patternToByteArray(pattern)); } mCredentialCheckResultTracker.setResult(matched, intent, timeoutMs, localEffectiveUserId); @@ -501,10 +487,12 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { mLockPatternView.setEnabled(true); if (matched) { if (newResult) { - reportSuccessfulAttempt(); + ConfirmDeviceCredentialUtils.reportSuccessfulAttempt(mLockPatternUtils, + mUserManager, mEffectiveUserId); } + mBiometricManager.onConfirmDeviceCredentialSuccess(); startDisappearAnimation(intent); - checkForPendingIntent(); + ConfirmDeviceCredentialUtils.checkForPendingIntent(getActivity()); } else { if (timeoutMs > 0) { refreshLockScreen(); diff --git a/src/com/android/settings/password/CredentialCheckResultTracker.java b/src/com/android/settings/password/CredentialCheckResultTracker.java index a17939c446..1993ec6b64 100644 --- a/src/com/android/settings/password/CredentialCheckResultTracker.java +++ b/src/com/android/settings/password/CredentialCheckResultTracker.java @@ -16,10 +16,11 @@ package com.android.settings.password; -import android.app.Fragment; import android.content.Intent; import android.os.Bundle; +import androidx.fragment.app.Fragment; + /** * An invisible retained fragment to track lock check result. */ diff --git a/src/com/android/settings/password/ManagedLockPasswordProvider.java b/src/com/android/settings/password/ManagedLockPasswordProvider.java index 5786a5afa0..5006926feb 100644 --- a/src/com/android/settings/password/ManagedLockPasswordProvider.java +++ b/src/com/android/settings/password/ManagedLockPasswordProvider.java @@ -19,8 +19,6 @@ package com.android.settings.password; import android.content.Context; import android.content.Intent; -import com.android.settings.R; - /** * Helper for handling managed passwords in security settings UI. * It provides resources that should be shown in settings UI when lock password quality is set to @@ -61,7 +59,7 @@ public class ManagedLockPasswordProvider { * @param password Current lock password. * @return Intent that should update lock password to a managed password. */ - Intent createIntent(boolean requirePasswordToDecrypt, String password) { + Intent createIntent(boolean requirePasswordToDecrypt, byte[] password) { return null; } } diff --git a/src/com/android/settings/password/PasswordRequirementAdapter.java b/src/com/android/settings/password/PasswordRequirementAdapter.java index 0148ca5568..0e194afca1 100644 --- a/src/com/android/settings/password/PasswordRequirementAdapter.java +++ b/src/com/android/settings/password/PasswordRequirementAdapter.java @@ -16,12 +16,13 @@ package com.android.settings.password; -import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; +import androidx.recyclerview.widget.RecyclerView; + import com.android.settings.R; import com.android.settings.password.PasswordRequirementAdapter.PasswordRequirementViewHolder; diff --git a/src/com/android/settings/password/PasswordUtils.java b/src/com/android/settings/password/PasswordUtils.java new file mode 100644 index 0000000000..1ead492788 --- /dev/null +++ b/src/com/android/settings/password/PasswordUtils.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2019 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.password; + +import static com.android.settings.Utils.SETTINGS_PACKAGE_NAME; + +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.app.IActivityManager; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.UserHandle; +import android.util.Log; + +import com.android.settings.Utils; + +public final class PasswordUtils extends com.android.settingslib.Utils { + + private static final String TAG = "Settings"; + + /** + * Returns whether the uid which the activity with {@code activityToken} is launched from has + * been granted the {@code permission}. + */ + public static boolean isCallingAppPermitted(Context context, IBinder activityToken, + String permission) { + try { + return context.checkPermission(permission, /* pid= */ -1, + ActivityManager.getService().getLaunchedFromUid(activityToken)) + == PackageManager.PERMISSION_GRANTED; + } catch (RemoteException e) { + Log.v(TAG, "Could not talk to activity manager.", e); + return false; + } + } + + /** + * Returns the label of the package which the activity with {@code activityToken} is launched + * from or {@code null} if it is launched from the settings app itself. + */ + @Nullable + public static CharSequence getCallingAppLabel(Context context, IBinder activityToken) { + String pkg = getCallingAppPackageName(activityToken); + if (pkg == null || pkg.equals(SETTINGS_PACKAGE_NAME)) { + return null; + } + + return Utils.getApplicationLabel(context, pkg); + } + + /** + * Returns the package name which the activity with {@code activityToken} is launched from. + */ + @Nullable + public static String getCallingAppPackageName(IBinder activityToken) { + String pkg = null; + try { + pkg = ActivityManager.getService().getLaunchedFromPackage(activityToken); + } catch (RemoteException e) { + Log.v(TAG, "Could not talk to activity manager.", e); + } + return pkg; + } + + /** Crashes the calling application and provides it with {@code message}. */ + public static void crashCallingApplication(IBinder activityToken, String message) { + IActivityManager am = ActivityManager.getService(); + try { + int uid = am.getLaunchedFromUid(activityToken); + int userId = UserHandle.getUserId(uid); + am.crashApplication( + uid, + /* initialPid= */ -1, + getCallingAppPackageName(activityToken), + userId, + message); + } catch (RemoteException e) { + Log.v(TAG, "Could not talk to activity manager.", e); + } + } +} diff --git a/src/com/android/settings/password/SaveChosenLockWorkerBase.java b/src/com/android/settings/password/SaveChosenLockWorkerBase.java index 95980e93e8..2798b3d7f4 100644 --- a/src/com/android/settings/password/SaveChosenLockWorkerBase.java +++ b/src/com/android/settings/password/SaveChosenLockWorkerBase.java @@ -16,14 +16,18 @@ package com.android.settings.password; -import android.app.Fragment; import android.content.Context; import android.content.Intent; import android.os.AsyncTask; import android.os.Bundle; import android.os.UserManager; +import android.util.Pair; +import android.widget.Toast; + +import androidx.fragment.app.Fragment; import com.android.internal.widget.LockPatternUtils; +import com.android.settings.R; /** * An invisible retained worker fragment to track the AsyncWork that saves (and optionally @@ -83,7 +87,7 @@ abstract class SaveChosenLockWorkerBase extends Fragment { protected void start() { if (mBlocking) { - finish(saveAndVerifyInBackground()); + finish(saveAndVerifyInBackground().second); } else { new Task().execute(); } @@ -91,9 +95,10 @@ abstract class SaveChosenLockWorkerBase extends Fragment { /** * Executes the save and verify work in background. - * @return Intent with challenge token or null. + * @return pair where the first is a boolean confirming whether the change was successful or not + * and second is the Intent which has the challenge token or is null. */ - protected abstract Intent saveAndVerifyInBackground(); + protected abstract Pair<Boolean, Intent> saveAndVerifyInBackground(); protected void finish(Intent resultData) { mFinished = true; @@ -107,19 +112,24 @@ abstract class SaveChosenLockWorkerBase extends Fragment { mBlocking = blocking; } - private class Task extends AsyncTask<Void, Void, Intent> { + private class Task extends AsyncTask<Void, Void, Pair<Boolean, Intent>> { + @Override - protected Intent doInBackground(Void... params){ + protected Pair<Boolean, Intent> doInBackground(Void... params){ return saveAndVerifyInBackground(); } @Override - protected void onPostExecute(Intent resultData) { - finish(resultData); + protected void onPostExecute(Pair<Boolean, Intent> resultData) { + if (!resultData.first) { + Toast.makeText(getContext(), R.string.lockpassword_credential_changed, + Toast.LENGTH_LONG).show(); + } + finish(resultData.second); } } interface Listener { - public void onChosenLockSaveFinished(boolean wasSecureBefore, Intent resultData); + void onChosenLockSaveFinished(boolean wasSecureBefore, Intent resultData); } } diff --git a/src/com/android/settings/password/ScreenLockType.java b/src/com/android/settings/password/ScreenLockType.java index 608c8f6576..8253065378 100644 --- a/src/com/android/settings/password/ScreenLockType.java +++ b/src/com/android/settings/password/ScreenLockType.java @@ -45,9 +45,6 @@ public enum ScreenLockType { DevicePolicyManager.PASSWORD_QUALITY_MANAGED, "unlock_set_managed"); - private static final ScreenLockType MIN_QUALITY = ScreenLockType.NONE; - private static final ScreenLockType MAX_QUALITY = ScreenLockType.MANAGED; - /** * The default quality of the type of lock used. For example, in the case of PIN, the default * quality if PASSWORD_QUALITY_NUMERIC, while the highest quality is diff --git a/src/com/android/settings/password/SetNewPasswordActivity.java b/src/com/android/settings/password/SetNewPasswordActivity.java index 99f67cb13d..055e5bedf8 100644 --- a/src/com/android/settings/password/SetNewPasswordActivity.java +++ b/src/com/android/settings/password/SetNewPasswordActivity.java @@ -16,16 +16,33 @@ package com.android.settings.password; +import static android.Manifest.permission.REQUEST_PASSWORD_COMPLEXITY; import static android.app.admin.DevicePolicyManager.ACTION_SET_NEW_PARENT_PROFILE_PASSWORD; import static android.app.admin.DevicePolicyManager.ACTION_SET_NEW_PASSWORD; +import static android.app.admin.DevicePolicyManager.EXTRA_PASSWORD_COMPLEXITY; +import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_NONE; + +import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_CALLER_APP_NAME; +import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_IS_CALLING_APP_ADMIN; +import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_REQUESTED_MIN_COMPLEXITY; import android.app.Activity; import android.app.admin.DevicePolicyManager; +import android.app.admin.DevicePolicyManager.PasswordComplexity; +import android.app.admin.PasswordMetrics; +import android.app.settings.SettingsEnums; +import android.content.ComponentName; +import android.content.Context; import android.content.Intent; import android.os.Bundle; +import android.os.IBinder; import android.util.Log; import com.android.settings.Utils; +import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; + +import java.util.List; /** * Trampolines {@link DevicePolicyManager#ACTION_SET_NEW_PASSWORD} and @@ -37,6 +54,21 @@ public class SetNewPasswordActivity extends Activity implements SetNewPasswordCo private String mNewPasswordAction; private SetNewPasswordController mSetNewPasswordController; + /** + * From intent extra {@link DevicePolicyManager#EXTRA_PASSWORD_COMPLEXITY}. + * + * <p>This is used only if caller has the required permission and activity is launched by + * {@link DevicePolicyManager#ACTION_SET_NEW_PASSWORD}. + */ + private @PasswordComplexity int mRequestedMinComplexity = PASSWORD_COMPLEXITY_NONE; + + /** + * Label of the app which launches this activity. + * + * <p>Value would be {@code null} if launched from settings app. + */ + private String mCallerAppName = null; + @Override protected void onCreate(Bundle savedState) { super.onCreate(savedState); @@ -48,6 +80,29 @@ public class SetNewPasswordActivity extends Activity implements SetNewPasswordCo finish(); return; } + + logSetNewPasswordIntent(); + + final IBinder activityToken = getActivityToken(); + mCallerAppName = (String) PasswordUtils.getCallingAppLabel(this, activityToken); + if (ACTION_SET_NEW_PASSWORD.equals(mNewPasswordAction) + && getIntent().hasExtra(EXTRA_PASSWORD_COMPLEXITY)) { + final boolean hasPermission = PasswordUtils.isCallingAppPermitted( + this, activityToken, REQUEST_PASSWORD_COMPLEXITY); + if (hasPermission) { + mRequestedMinComplexity = + PasswordMetrics.sanitizeComplexityLevel(getIntent() + .getIntExtra(EXTRA_PASSWORD_COMPLEXITY, PASSWORD_COMPLEXITY_NONE)); + } else { + PasswordUtils.crashCallingApplication(activityToken, + "Must have permission " + + REQUEST_PASSWORD_COMPLEXITY + " to use extra " + + EXTRA_PASSWORD_COMPLEXITY); + finish(); + return; + } + } + mSetNewPasswordController = SetNewPasswordController.create( this, this, getIntent(), getActivityToken()); mSetNewPasswordController.dispatchSetNewPasswordIntent(); @@ -60,7 +115,56 @@ public class SetNewPasswordActivity extends Activity implements SetNewPasswordCo : new Intent(this, ChooseLockGeneric.class); intent.setAction(mNewPasswordAction); intent.putExtras(chooseLockFingerprintExtras); + if (mCallerAppName != null) { + intent.putExtra(EXTRA_KEY_CALLER_APP_NAME, mCallerAppName); + } + if (mRequestedMinComplexity != PASSWORD_COMPLEXITY_NONE) { + intent.putExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY, mRequestedMinComplexity); + } + if (isCallingAppAdmin()) { + intent.putExtra(EXTRA_KEY_IS_CALLING_APP_ADMIN, true); + } startActivity(intent); finish(); } + + private boolean isCallingAppAdmin() { + DevicePolicyManager devicePolicyManager = getSystemService(DevicePolicyManager.class); + String callingAppPackageName = PasswordUtils.getCallingAppPackageName(getActivityToken()); + List<ComponentName> admins = devicePolicyManager.getActiveAdmins(); + if (admins == null) { + return false; + } + for (ComponentName componentName : admins) { + if (componentName.getPackageName().equals(callingAppPackageName)) { + return true; + } + } + return false; + } + + private void logSetNewPasswordIntent() { + final String callingAppPackageName = + PasswordUtils.getCallingAppPackageName(getActivityToken()); + + // use int min value to denote absence of EXTRA_PASSWORD_COMPLEXITY + final int extraPasswordComplexity = getIntent().hasExtra(EXTRA_PASSWORD_COMPLEXITY) + ? getIntent().getIntExtra(EXTRA_PASSWORD_COMPLEXITY, PASSWORD_COMPLEXITY_NONE) + : Integer.MIN_VALUE; + + // this activity is launched by either ACTION_SET_NEW_PASSWORD or + // ACTION_SET_NEW_PARENT_PROFILE_PASSWORD + final int action = ACTION_SET_NEW_PASSWORD.equals(mNewPasswordAction) + ? SettingsEnums.ACTION_SET_NEW_PASSWORD + : SettingsEnums.ACTION_SET_NEW_PARENT_PROFILE_PASSWORD; + + final MetricsFeatureProvider metricsProvider = + FeatureFactory.getFactory(this).getMetricsFeatureProvider(); + metricsProvider.action( + metricsProvider.getAttribution(this), + action, + SettingsEnums.SET_NEW_PASSWORD_ACTIVITY, + callingAppPackageName, + extraPasswordComplexity); + } } diff --git a/src/com/android/settings/password/SetNewPasswordController.java b/src/com/android/settings/password/SetNewPasswordController.java index a8d51eddd4..bf552718b1 100644 --- a/src/com/android/settings/password/SetNewPasswordController.java +++ b/src/com/android/settings/password/SetNewPasswordController.java @@ -17,8 +17,10 @@ package com.android.settings.password; import static android.app.admin.DevicePolicyManager.ACTION_SET_NEW_PASSWORD; +import static android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_FACE; import static android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT; import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_SOMETHING; + import static com.android.internal.util.Preconditions.checkNotNull; import android.annotation.Nullable; @@ -27,12 +29,14 @@ import android.app.admin.DevicePolicyManager; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; +import android.hardware.face.FaceManager; import android.hardware.fingerprint.FingerprintManager; import android.os.Bundle; import android.os.IBinder; import android.os.UserManager; -import com.android.internal.annotations.VisibleForTesting; +import androidx.annotation.VisibleForTesting; + import com.android.internal.widget.LockPatternUtils; import com.android.settings.Utils; @@ -57,6 +61,8 @@ final class SetNewPasswordController { private final PackageManager mPackageManager; @Nullable private final FingerprintManager mFingerprintManager; + @Nullable + private final FaceManager mFaceManager; private final DevicePolicyManager mDevicePolicyManager; private final Ui mUi; @@ -77,9 +83,10 @@ final class SetNewPasswordController { } // Create a wrapper of FingerprintManager for testing, see IFingerPrintManager for details. final FingerprintManager fingerprintManager = Utils.getFingerprintManagerOrNull(context); + final FaceManager faceManager = Utils.getFaceManagerOrNull(context); return new SetNewPasswordController(userId, context.getPackageManager(), - fingerprintManager, + fingerprintManager, faceManager, (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE), ui); } @@ -88,11 +95,13 @@ final class SetNewPasswordController { int targetUserId, PackageManager packageManager, FingerprintManager fingerprintManager, + FaceManager faceManager, DevicePolicyManager devicePolicyManager, Ui ui) { mTargetUserId = targetUserId; mPackageManager = checkNotNull(packageManager); mFingerprintManager = fingerprintManager; + mFaceManager = faceManager; mDevicePolicyManager = checkNotNull(devicePolicyManager); mUi = checkNotNull(ui); } @@ -102,7 +111,14 @@ final class SetNewPasswordController { */ public void dispatchSetNewPasswordIntent() { final Bundle extras; - if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT) + // TODO: handle the case with multiple biometrics, perhaps take an arg for biometric type? + if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE) + && mFaceManager != null + && mFaceManager.isHardwareDetected() + && !mFaceManager.hasEnrolledTemplates(mTargetUserId) + && !isFaceDisabledByAdmin()) { + extras = getFaceChooseLockExtras(); + } else if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT) && mFingerprintManager != null && mFingerprintManager.isHardwareDetected() && !mFingerprintManager.hasEnrolledFingerprints(mTargetUserId) @@ -130,9 +146,28 @@ final class SetNewPasswordController { return chooseLockExtras; } + private Bundle getFaceChooseLockExtras() { + Bundle chooseLockExtras = new Bundle(); + long challenge = mFaceManager.generateChallenge(); + chooseLockExtras.putInt(ChooseLockGeneric.ChooseLockGenericFragment.MINIMUM_QUALITY_KEY, + PASSWORD_QUALITY_SOMETHING); + chooseLockExtras.putBoolean( + ChooseLockGeneric.ChooseLockGenericFragment.HIDE_DISABLED_PREFS, true); + chooseLockExtras.putBoolean(ChooseLockSettingsHelper.EXTRA_KEY_HAS_CHALLENGE, true); + chooseLockExtras.putLong(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE, challenge); + chooseLockExtras.putBoolean(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FACE, true); + return chooseLockExtras; + } + private boolean isFingerprintDisabledByAdmin() { int disabledFeatures = mDevicePolicyManager.getKeyguardDisabledFeatures(null, mTargetUserId); return (disabledFeatures & KEYGUARD_DISABLE_FINGERPRINT) != 0; } + + private boolean isFaceDisabledByAdmin() { + int disabledFeatures = + mDevicePolicyManager.getKeyguardDisabledFeatures(null, mTargetUserId); + return (disabledFeatures & KEYGUARD_DISABLE_FACE) != 0; + } } diff --git a/src/com/android/settings/password/SetupChooseLockGeneric.java b/src/com/android/settings/password/SetupChooseLockGeneric.java index b96ebea67c..9a16529794 100644 --- a/src/com/android/settings/password/SetupChooseLockGeneric.java +++ b/src/com/android/settings/password/SetupChooseLockGeneric.java @@ -16,27 +16,37 @@ package com.android.settings.password; +import static android.Manifest.permission.REQUEST_PASSWORD_COMPLEXITY; +import static android.app.admin.DevicePolicyManager.EXTRA_PASSWORD_COMPLEXITY; + +import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_REQUESTED_MIN_COMPLEXITY; + import android.app.admin.DevicePolicyManager; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.os.Bundle; +import android.os.IBinder; import android.os.UserHandle; -import androidx.preference.PreferenceFragment; -import androidx.preference.Preference; -import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; +import androidx.fragment.app.Fragment; +import androidx.preference.Preference; +import androidx.preference.PreferenceFragmentCompat; +import androidx.recyclerview.widget.RecyclerView; + import com.android.internal.widget.LockPatternUtils; import com.android.settings.R; import com.android.settings.SetupEncryptionInterstitial; import com.android.settings.SetupWizardUtils; -import com.android.settings.fingerprint.SetupFingerprintEnrollFindSensor; +import com.android.settings.biometrics.BiometricEnrollActivity; +import com.android.settings.biometrics.fingerprint.SetupFingerprintEnrollFindSensor; import com.android.settings.utils.SettingsDividerItemDecoration; -import com.android.setupwizardlib.GlifPreferenceLayout; + +import com.google.android.setupdesign.GlifPreferenceLayout; /** * Setup Wizard's version of ChooseLockGeneric screen. It inherits the logic and basic structure @@ -45,6 +55,7 @@ import com.android.setupwizardlib.GlifPreferenceLayout; * Other changes should be done to ChooseLockGeneric class instead and let this class inherit * those changes. */ +// TODO(b/123225425): Restrict SetupChooseLockGeneric to be accessible by SUW only public class SetupChooseLockGeneric extends ChooseLockGeneric { private static final String KEY_UNLOCK_SET_DO_LATER = "unlock_set_do_later"; @@ -55,7 +66,7 @@ public class SetupChooseLockGeneric extends ChooseLockGeneric { } @Override - /* package */ Class<? extends PreferenceFragment> getFragmentClass() { + /* package */ Class<? extends PreferenceFragmentCompat> getFragmentClass() { return SetupChooseLockGenericFragment.class; } @@ -68,8 +79,21 @@ public class SetupChooseLockGeneric extends ChooseLockGeneric { @Override protected void onCreate(Bundle savedInstance) { super.onCreate(savedInstance); - LinearLayout layout = (LinearLayout) findViewById(R.id.content_parent); - layout.setFitsSystemWindows(false); + + if(getIntent().hasExtra(EXTRA_KEY_REQUESTED_MIN_COMPLEXITY)) { + IBinder activityToken = getActivityToken(); + boolean hasPermission = PasswordUtils.isCallingAppPermitted( + this, activityToken, REQUEST_PASSWORD_COMPLEXITY); + if (!hasPermission) { + PasswordUtils.crashCallingApplication(activityToken, + "Must have permission " + REQUEST_PASSWORD_COMPLEXITY + + " to use extra " + EXTRA_PASSWORD_COMPLEXITY); + finish(); + return; + } + } + + findViewById(R.id.content_parent).setFitsSystemWindows(false); } public static class SetupChooseLockGenericFragment extends ChooseLockGenericFragment { @@ -83,7 +107,7 @@ public class SetupChooseLockGeneric extends ChooseLockGeneric { GlifPreferenceLayout layout = (GlifPreferenceLayout) view; layout.setDividerItemDecoration(new SettingsDividerItemDecoration(getContext())); layout.setDividerInset(getContext().getResources().getDimensionPixelSize( - R.dimen.suw_items_glif_text_divider_inset)); + R.dimen.sud_items_glif_text_divider_inset)); layout.setIcon(getContext().getDrawable(R.drawable.ic_lock)); @@ -101,8 +125,8 @@ public class SetupChooseLockGeneric extends ChooseLockGeneric { @Override protected void addHeaderView() { - if (mForFingerprint) { - setHeaderView(R.layout.setup_choose_lock_generic_fingerprint_header); + if (mForFingerprint || mForFace) { + setHeaderView(R.layout.setup_choose_lock_generic_biometrics_header); } else { setHeaderView(R.layout.setup_choose_lock_generic_header); } @@ -134,6 +158,11 @@ public class SetupChooseLockGeneric extends ChooseLockGeneric { return true; } + @Override + protected Class<? extends ChooseLockGeneric.InternalActivity> getInternalActivityClass() { + return SetupChooseLockGeneric.InternalActivity.class; + } + /*** * Disables preferences that are less secure than required quality and shows only secure * screen lock options here. @@ -166,8 +195,14 @@ public class SetupChooseLockGeneric extends ChooseLockGeneric { final String key = preference.getKey(); if (KEY_UNLOCK_SET_DO_LATER.equals(key)) { // show warning. - SetupSkipDialog dialog = SetupSkipDialog.newInstance(getActivity().getIntent() - .getBooleanExtra(SetupSkipDialog.EXTRA_FRP_SUPPORTED, false)); + SetupSkipDialog dialog = SetupSkipDialog.newInstance( + getActivity().getIntent() + .getBooleanExtra(SetupSkipDialog.EXTRA_FRP_SUPPORTED, false), + /* isPatternMode= */ false, + /* isAlphaMode= */ false, + /* isFingerprintSupported= */ false, + /* isFaceSupported= */ false + ); dialog.show(getFragmentManager()); return true; } @@ -175,9 +210,9 @@ public class SetupChooseLockGeneric extends ChooseLockGeneric { } @Override - protected Intent getLockPasswordIntent(int quality, int minLength, int maxLength) { + protected Intent getLockPasswordIntent(int quality) { final Intent intent = SetupChooseLockPassword.modifyIntentForSetup( - getContext(), super.getLockPasswordIntent(quality, minLength, maxLength)); + getContext(), super.getLockPasswordIntent(quality)); SetupWizardUtils.copySetupExtras(getActivity().getIntent(), intent); return intent; } @@ -200,10 +235,31 @@ public class SetupChooseLockGeneric extends ChooseLockGeneric { } @Override - protected Intent getFindSensorIntent(Context context) { - final Intent intent = new Intent(context, SetupFingerprintEnrollFindSensor.class); + protected Intent getBiometricEnrollIntent(Context context) { + final Intent intent = super.getBiometricEnrollIntent(context); SetupWizardUtils.copySetupExtras(getActivity().getIntent(), intent); return intent; } } + + public static class InternalActivity extends ChooseLockGeneric.InternalActivity { + @Override + protected boolean isValidFragment(String fragmentName) { + return InternalSetupChooseLockGenericFragment.class.getName().equals(fragmentName); + } + + @Override + /* package */ Class<? extends Fragment> getFragmentClass() { + return InternalSetupChooseLockGenericFragment.class; + } + + public static class InternalSetupChooseLockGenericFragment + extends ChooseLockGenericFragment { + @Override + protected boolean canRunBeforeDeviceProvisioned() { + return true; + } + } + } + } diff --git a/src/com/android/settings/password/SetupChooseLockPassword.java b/src/com/android/settings/password/SetupChooseLockPassword.java index 56b66a324a..60bb0cdc30 100644 --- a/src/com/android/settings/password/SetupChooseLockPassword.java +++ b/src/com/android/settings/password/SetupChooseLockPassword.java @@ -17,17 +17,18 @@ package com.android.settings.password; import android.app.Activity; -import android.app.Fragment; import android.app.admin.DevicePolicyManager; import android.content.Context; import android.content.Intent; import android.os.Bundle; -import androidx.annotation.Nullable; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.LinearLayout; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + import com.android.settings.R; import com.android.settings.SetupRedactionInterstitial; import com.android.settings.password.ChooseLockTypeDialogFragment.OnLockTypeSelectedListener; @@ -71,8 +72,11 @@ public class SetupChooseLockPassword extends ChooseLockPassword { public static class SetupChooseLockPasswordFragment extends ChooseLockPasswordFragment implements OnLockTypeSelectedListener { + private static final String TAG_SKIP_SCREEN_LOCK_DIALOG = "skip_screen_lock_dialog"; + @Nullable private Button mOptionsButton; + private boolean mLeftButtonIsSkip; @Override public void onViewCreated(View view, Bundle savedInstanceState) { @@ -91,26 +95,31 @@ public class SetupChooseLockPassword extends ChooseLockPassword { if (showOptionsButton && anyOptionsShown) { mOptionsButton = view.findViewById(R.id.screen_lock_options); mOptionsButton.setVisibility(View.VISIBLE); - mOptionsButton.setOnClickListener(this); + mOptionsButton.setOnClickListener((btn) -> + ChooseLockTypeDialogFragment.newInstance(mUserId) + .show(getChildFragmentManager(), TAG_SKIP_SCREEN_LOCK_DIALOG)); } } @Override - public void onClick(View v) { - switch (v.getId()) { - case R.id.screen_lock_options: - ChooseLockTypeDialogFragment.newInstance(mUserId) - .show(getChildFragmentManager(), null); - break; - case R.id.skip_button: - SetupSkipDialog dialog = SetupSkipDialog.newInstance( - getActivity().getIntent() - .getBooleanExtra(SetupSkipDialog.EXTRA_FRP_SUPPORTED, false)); - dialog.show(getFragmentManager()); - break; - default: - super.onClick(v); + protected void onSkipOrClearButtonClick(View view) { + if (mLeftButtonIsSkip) { + SetupSkipDialog dialog = SetupSkipDialog.newInstance( + getActivity().getIntent() + .getBooleanExtra(SetupSkipDialog.EXTRA_FRP_SUPPORTED, false), + /* isPatternMode= */ false, + mIsAlphaMode, + getActivity().getIntent() + .getBooleanExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FINGERPRINT, + false), + getActivity().getIntent() + .getBooleanExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FACE, false) + + ); + dialog.show(getFragmentManager()); + return; } + super.onSkipOrClearButtonClick(view); } @Override @@ -132,9 +141,23 @@ public class SetupChooseLockPassword extends ChooseLockPassword { } @Override + protected int getStageType() { + // Return TYPE_NONE to make generic lock screen launch in Setup wizard flow before + // fingerprint and face setup. + return Stage.TYPE_NONE; + } + + @Override protected void updateUi() { super.updateUi(); - mSkipButton.setVisibility(mForFingerprint ? View.GONE : View.VISIBLE); + // Show the skip button during SUW but not during Settings > Biometric Enrollment + if (mUiStage == Stage.Introduction) { + mSkipOrClearButton.setText(getActivity(), R.string.skip_label); + mLeftButtonIsSkip = true; + } else { + mSkipOrClearButton.setText(getActivity(), R.string.lockpassword_clear_label); + mLeftButtonIsSkip = false; + } if (mOptionsButton != null) { mOptionsButton.setVisibility( diff --git a/src/com/android/settings/password/SetupChooseLockPattern.java b/src/com/android/settings/password/SetupChooseLockPattern.java index 0b272f5b93..8e59d8c582 100644 --- a/src/com/android/settings/password/SetupChooseLockPattern.java +++ b/src/com/android/settings/password/SetupChooseLockPattern.java @@ -16,16 +16,17 @@ package com.android.settings.password; -import android.app.Fragment; import android.content.Context; import android.content.Intent; import android.os.Bundle; -import androidx.annotation.Nullable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + import com.android.settings.R; import com.android.settings.SetupRedactionInterstitial; @@ -53,11 +54,23 @@ public class SetupChooseLockPattern extends ChooseLockPattern { return SetupChooseLockPatternFragment.class; } + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Show generic pattern title when pattern lock screen launch in Setup wizard flow before + // fingerprint and face setup. + setTitle(R.string.lockpassword_choose_your_screen_lock_header); + } + public static class SetupChooseLockPatternFragment extends ChooseLockPatternFragment implements ChooseLockTypeDialogFragment.OnLockTypeSelectedListener { + private static final String TAG_SKIP_SCREEN_LOCK_DIALOG = "skip_screen_lock_dialog"; + @Nullable private Button mOptionsButton; + private boolean mLeftButtonIsSkip; @Override public View onCreateView( @@ -67,23 +80,35 @@ public class SetupChooseLockPattern extends ChooseLockPattern { mOptionsButton = view.findViewById(R.id.screen_lock_options); mOptionsButton.setOnClickListener((btn) -> ChooseLockTypeDialogFragment.newInstance(mUserId) - .show(getChildFragmentManager(), null)); - } - // enable skip button only during setup wizard and not with fingerprint flow. - if (!mForFingerprint) { - Button skipButton = view.findViewById(R.id.skip_button); - skipButton.setVisibility(View.VISIBLE); - skipButton.setOnClickListener(v -> { - SetupSkipDialog dialog = SetupSkipDialog.newInstance( - getActivity().getIntent() - .getBooleanExtra(SetupSkipDialog.EXTRA_FRP_SUPPORTED, false)); - dialog.show(getFragmentManager()); - }); + .show(getChildFragmentManager(), TAG_SKIP_SCREEN_LOCK_DIALOG)); } + // Show the skip button during SUW but not during Settings > Biometric Enrollment + mSkipOrClearButton.setOnClickListener(this::onSkipOrClearButtonClick); return view; } @Override + protected void onSkipOrClearButtonClick(View view) { + if (mLeftButtonIsSkip) { + SetupSkipDialog dialog = SetupSkipDialog.newInstance( + getActivity().getIntent() + .getBooleanExtra(SetupSkipDialog.EXTRA_FRP_SUPPORTED, false), + /* isPatternMode= */ true, + /* isAlphaMode= */ false, + getActivity().getIntent() + .getBooleanExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FINGERPRINT, + false), + getActivity().getIntent() + .getBooleanExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FACE, false) + + ); + dialog.show(getFragmentManager()); + return; + } + super.onSkipOrClearButtonClick(view); + } + + @Override public void onLockTypeSelected(ScreenLockType lock) { if (ScreenLockType.PATTERN == lock) { return; @@ -97,7 +122,25 @@ public class SetupChooseLockPattern extends ChooseLockPattern { if (!getResources().getBoolean(R.bool.config_lock_pattern_minimal_ui) && mOptionsButton != null) { mOptionsButton.setVisibility( - stage == Stage.Introduction ? View.VISIBLE : View.INVISIBLE); + (stage == Stage.Introduction || stage == Stage.HelpScreen || + stage == Stage.ChoiceTooShort || stage == Stage.FirstChoiceValid) + ? View.VISIBLE : View.INVISIBLE); + } + + if (stage.leftMode == LeftButtonMode.Gone && stage == Stage.Introduction) { + mSkipOrClearButton.setVisibility(View.VISIBLE); + mSkipOrClearButton.setText(getActivity(), R.string.skip_label); + mLeftButtonIsSkip = true; + } else { + mLeftButtonIsSkip = false; + } + + // Show generic pattern message when pattern lock screen launch in Setup wizard flow + // before fingerprint and face setup. + if (stage.message == ID_EMPTY_MESSAGE) { + mMessageText.setText(""); + } else { + mMessageText.setText(stage.message); } } diff --git a/src/com/android/settings/password/SetupSkipDialog.java b/src/com/android/settings/password/SetupSkipDialog.java index f5396c4efd..68f8dd4888 100644 --- a/src/com/android/settings/password/SetupSkipDialog.java +++ b/src/com/android/settings/password/SetupSkipDialog.java @@ -17,14 +17,15 @@ package com.android.settings.password; import android.app.Activity; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.FragmentManager; +import android.app.settings.SettingsEnums; import android.content.DialogInterface; import android.os.Bundle; + import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.FragmentManager; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; @@ -34,20 +35,29 @@ public class SetupSkipDialog extends InstrumentedDialogFragment public static final String EXTRA_FRP_SUPPORTED = ":settings:frp_supported"; private static final String ARG_FRP_SUPPORTED = "frp_supported"; + // The key indicates type of lock screen is pattern setup. + private static final String ARG_LOCK_TYPE_PATTERN = "lock_type_pattern"; + // The key indicates type of lock screen setup is alphanumeric for password setup. + private static final String ARG_LOCK_TYPE_ALPHANUMERIC = "lock_type_alphanumeric"; private static final String TAG_SKIP_DIALOG = "skip_dialog"; public static final int RESULT_SKIP = Activity.RESULT_FIRST_USER + 10; - public static SetupSkipDialog newInstance(boolean isFrpSupported) { + public static SetupSkipDialog newInstance(boolean isFrpSupported, boolean isPatternMode, + boolean isAlphanumericMode, boolean isFingerprintSupported, boolean isFaceSupported) { SetupSkipDialog dialog = new SetupSkipDialog(); Bundle args = new Bundle(); args.putBoolean(ARG_FRP_SUPPORTED, isFrpSupported); + args.putBoolean(ARG_LOCK_TYPE_PATTERN, isPatternMode); + args.putBoolean(ARG_LOCK_TYPE_ALPHANUMERIC, isAlphanumericMode); + args.putBoolean(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FINGERPRINT, isFingerprintSupported); + args.putBoolean(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FACE, isFaceSupported); dialog.setArguments(args); return dialog; } @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DIALOG_FINGERPRINT_SKIP_SETUP; + return SettingsEnums.DIALOG_FINGERPRINT_SKIP_SETUP; } @Override @@ -58,13 +68,36 @@ public class SetupSkipDialog extends InstrumentedDialogFragment @NonNull public AlertDialog.Builder onCreateDialogBuilder() { Bundle args = getArguments(); - return new AlertDialog.Builder(getContext()) - .setPositiveButton(R.string.skip_anyway_button_label, this) - .setNegativeButton(R.string.go_back_button_label, this) - .setTitle(R.string.lock_screen_intro_skip_title) - .setMessage(args.getBoolean(ARG_FRP_SUPPORTED) ? - R.string.lock_screen_intro_skip_dialog_text_frp : - R.string.lock_screen_intro_skip_dialog_text); + final boolean isFaceSupported = + args.getBoolean(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FACE); + final boolean isFingerprintSupported = + args.getBoolean(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FINGERPRINT); + if (isFaceSupported || isFingerprintSupported) { + final int titleId; + + if (args.getBoolean(ARG_LOCK_TYPE_PATTERN)) { + titleId = R.string.lock_screen_pattern_skip_title; + } else { + titleId = args.getBoolean(ARG_LOCK_TYPE_ALPHANUMERIC) ? + R.string.lock_screen_password_skip_title : R.string.lock_screen_pin_skip_title; + } + + return new AlertDialog.Builder(getContext()) + .setPositiveButton(R.string.skip_lock_screen_dialog_button_label, this) + .setNegativeButton(R.string.cancel_lock_screen_dialog_button_label, this) + .setTitle(titleId) + .setMessage(isFaceSupported ? + R.string.face_lock_screen_setup_skip_dialog_text : + R.string.fingerprint_lock_screen_setup_skip_dialog_text); + } else { + return new AlertDialog.Builder(getContext()) + .setPositiveButton(R.string.skip_anyway_button_label, this) + .setNegativeButton(R.string.go_back_button_label, this) + .setTitle(R.string.lock_screen_intro_skip_title) + .setMessage(args.getBoolean(ARG_FRP_SUPPORTED) ? + R.string.lock_screen_intro_skip_dialog_text_frp : + R.string.lock_screen_intro_skip_dialog_text); + } } @Override diff --git a/src/com/android/settings/print/PrintJobMessagePreferenceController.java b/src/com/android/settings/print/PrintJobMessagePreferenceController.java new file mode 100644 index 0000000000..9573e5d600 --- /dev/null +++ b/src/com/android/settings/print/PrintJobMessagePreferenceController.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2018 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.print; + +import android.content.Context; +import android.print.PrintJob; +import android.print.PrintJobInfo; +import android.text.TextUtils; + +public class PrintJobMessagePreferenceController extends PrintJobPreferenceControllerBase { + + public PrintJobMessagePreferenceController(Context context, String key) { + super(context, key); + } + + @Override + protected void updateUi() { + final PrintJob printJob = getPrintJob(); + + if (printJob == null) { + mFragment.finish(); + return; + } + + if (printJob.isCancelled() || printJob.isCompleted()) { + mFragment.finish(); + return; + } + + final PrintJobInfo info = printJob.getInfo(); + final CharSequence status = info.getStatus(mContext.getPackageManager()); + mPreference.setVisible(!TextUtils.isEmpty(status)); + mPreference.setSummary(status); + } +} diff --git a/src/com/android/settings/print/PrintJobPreferenceController.java b/src/com/android/settings/print/PrintJobPreferenceController.java new file mode 100644 index 0000000000..0eff0d6d61 --- /dev/null +++ b/src/com/android/settings/print/PrintJobPreferenceController.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2018 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.print; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.print.PrintJob; +import android.print.PrintJobInfo; +import android.text.format.DateUtils; + +import com.android.settings.R; + +import java.text.DateFormat; + +public class PrintJobPreferenceController extends PrintJobPreferenceControllerBase { + + public PrintJobPreferenceController(Context context, String key) { + super(context, key); + } + + @Override + protected void updateUi() { + final PrintJob printJob = getPrintJob(); + + if (printJob == null) { + mFragment.finish(); + return; + } + + if (printJob.isCancelled() || printJob.isCompleted()) { + mFragment.finish(); + return; + } + + PrintJobInfo info = printJob.getInfo(); + + switch (info.getState()) { + case PrintJobInfo.STATE_CREATED: { + mPreference.setTitle(mContext.getString( + R.string.print_configuring_state_title_template, info.getLabel())); + } + break; + case PrintJobInfo.STATE_QUEUED: + case PrintJobInfo.STATE_STARTED: { + if (!printJob.getInfo().isCancelling()) { + mPreference.setTitle(mContext.getString( + R.string.print_printing_state_title_template, info.getLabel())); + } else { + mPreference.setTitle(mContext.getString( + R.string.print_cancelling_state_title_template, info.getLabel())); + } + } + break; + + case PrintJobInfo.STATE_FAILED: { + mPreference.setTitle(mContext.getString( + R.string.print_failed_state_title_template, info.getLabel())); + } + break; + + case PrintJobInfo.STATE_BLOCKED: { + if (!printJob.getInfo().isCancelling()) { + mPreference.setTitle(mContext.getString( + R.string.print_blocked_state_title_template, info.getLabel())); + } else { + mPreference.setTitle(mContext.getString( + R.string.print_cancelling_state_title_template, info.getLabel())); + } + } + break; + } + + mPreference.setSummary(mContext.getString(R.string.print_job_summary, + info.getPrinterName(), DateUtils.formatSameDayTime( + info.getCreationTime(), info.getCreationTime(), DateFormat.SHORT, + DateFormat.SHORT))); + + TypedArray a = mContext.obtainStyledAttributes(new int[]{ + android.R.attr.colorControlNormal}); + int tintColor = a.getColor(0, 0); + a.recycle(); + + switch (info.getState()) { + case PrintJobInfo.STATE_QUEUED: + case PrintJobInfo.STATE_STARTED: { + Drawable icon = mContext.getDrawable(com.android.internal.R.drawable.ic_print); + icon.setTint(tintColor); + mPreference.setIcon(icon); + break; + } + + case PrintJobInfo.STATE_FAILED: + case PrintJobInfo.STATE_BLOCKED: { + Drawable icon = mContext.getDrawable( + com.android.internal.R.drawable.ic_print_error); + icon.setTint(tintColor); + mPreference.setIcon(icon); + break; + } + } + } +} diff --git a/src/com/android/settings/print/PrintJobPreferenceControllerBase.java b/src/com/android/settings/print/PrintJobPreferenceControllerBase.java new file mode 100644 index 0000000000..0726a19f4d --- /dev/null +++ b/src/com/android/settings/print/PrintJobPreferenceControllerBase.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2018 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.print; + +import android.content.Context; +import android.print.PrintJob; +import android.print.PrintJobId; +import android.print.PrintManager; +import android.util.Log; + +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.core.BasePreferenceController; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.core.lifecycle.events.OnStop; + +public abstract class PrintJobPreferenceControllerBase extends BasePreferenceController implements + LifecycleObserver, OnStart, OnStop, PrintManager.PrintJobStateChangeListener { + private static final String TAG = "PrintJobPrefCtrlBase"; + + private static final String EXTRA_PRINT_JOB_ID = "EXTRA_PRINT_JOB_ID"; + + private final PrintManager mPrintManager; + protected Preference mPreference; + protected PrintJobSettingsFragment mFragment; + protected PrintJobId mPrintJobId; + + public PrintJobPreferenceControllerBase(Context context, String key) { + super(context, key); + mPrintManager = ((PrintManager) mContext.getSystemService( + Context.PRINT_SERVICE)).getGlobalPrintManagerForUser(mContext.getUserId()); + } + + @Override + public void onStart() { + mPrintManager.addPrintJobStateChangeListener(this); + updateUi(); + } + + @Override + public void onStop() { + mPrintManager.removePrintJobStateChangeListener(this); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } + + @Override + public void onPrintJobStateChanged(PrintJobId printJobId) { + updateUi(); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + } + + public void init(PrintJobSettingsFragment fragment) { + mFragment = fragment; + processArguments(); + } + + protected PrintJob getPrintJob() { + return mPrintManager.getPrintJob(mPrintJobId); + } + + protected abstract void updateUi(); + + private void processArguments() { + String printJobId = mFragment.getArguments().getString(EXTRA_PRINT_JOB_ID); + if (printJobId == null) { + printJobId = mFragment.getActivity().getIntent().getStringExtra(EXTRA_PRINT_JOB_ID); + + if (printJobId == null) { + Log.w(TAG, EXTRA_PRINT_JOB_ID + " not set"); + mFragment.finish(); + return; + } + } + mPrintJobId = PrintJobId.unflattenFromString(printJobId); + } +} diff --git a/src/com/android/settings/print/PrintJobSettingsFragment.java b/src/com/android/settings/print/PrintJobSettingsFragment.java index bb50d0af25..eeb8ed45f3 100644 --- a/src/com/android/settings/print/PrintJobSettingsFragment.java +++ b/src/com/android/settings/print/PrintJobSettingsFragment.java @@ -16,114 +16,60 @@ package com.android.settings.print; +import android.app.settings.SettingsEnums; import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.drawable.Drawable; import android.os.Bundle; import android.print.PrintJob; -import android.print.PrintJobId; -import android.print.PrintJobInfo; -import android.print.PrintManager; -import android.print.PrintManager.PrintJobStateChangeListener; -import androidx.preference.Preference; -import android.text.TextUtils; -import android.text.format.DateUtils; -import android.util.Log; -import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; -import android.view.ViewGroup; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; -import com.android.settings.SettingsPreferenceFragment; - -import java.text.DateFormat; +import com.android.settings.dashboard.DashboardFragment; /** * Fragment for management of a print job. */ -public class PrintJobSettingsFragment extends SettingsPreferenceFragment { - private static final String LOG_TAG = PrintJobSettingsFragment.class.getSimpleName(); +public class PrintJobSettingsFragment extends DashboardFragment { + private static final String TAG = "PrintJobSettingsFragment"; private static final int MENU_ITEM_ID_CANCEL = 1; private static final int MENU_ITEM_ID_RESTART = 2; - private static final String EXTRA_PRINT_JOB_ID = "EXTRA_PRINT_JOB_ID"; - - private static final String PRINT_JOB_PREFERENCE = "print_job_preference"; - private static final String PRINT_JOB_MESSAGE_PREFERENCE = "print_job_message_preference"; - - private final PrintJobStateChangeListener mPrintJobStateChangeListener = - new PrintJobStateChangeListener() { - @Override - public void onPrintJobStateChanged(PrintJobId printJobId) { - updateUi(); - } - }; - - private PrintManager mPrintManager; - - private Preference mPrintJobPreference; - private Preference mMessagePreference; - - private PrintJobId mPrintJobId; - @Override - public int getMetricsCategory() { - return MetricsEvent.PRINT_JOB_SETTINGS; + protected int getPreferenceScreenResId() { + return R.xml.print_job_settings; } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - View view = super.onCreateView(inflater, container, savedInstanceState); - - addPreferencesFromResource(R.xml.print_job_settings); - mPrintJobPreference = findPreference(PRINT_JOB_PREFERENCE); - mMessagePreference = findPreference(PRINT_JOB_MESSAGE_PREFERENCE); - - mPrintManager = ((PrintManager) getActivity().getSystemService( - Context.PRINT_SERVICE)).getGlobalPrintManagerForUser( - getActivity().getUserId()); - - getActivity().getActionBar().setTitle(R.string.print_print_job); - - processArguments(); - - setHasOptionsMenu(true); - - return view; + protected String getLogTag() { + return TAG; } @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - getListView().setEnabled(false); + public void onAttach(Context context) { + super.onAttach(context); + use(PrintJobPreferenceController.class).init(this); + use(PrintJobMessagePreferenceController.class).init(this); } @Override - public void onStart() { - super.onStart(); - mPrintManager.addPrintJobStateChangeListener( - mPrintJobStateChangeListener); - updateUi(); + public int getMetricsCategory() { + return SettingsEnums.PRINT_JOB_SETTINGS; } @Override - public void onStop() { - super.onStop(); - mPrintManager.removePrintJobStateChangeListener( - mPrintJobStateChangeListener); + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + getListView().setEnabled(false); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); - PrintJob printJob = getPrintJob(); + final PrintJob printJob = use(PrintJobPreferenceController.class).getPrintJob(); if (printJob == null) { return; } @@ -143,7 +89,7 @@ public class PrintJobSettingsFragment extends SettingsPreferenceFragment { @Override public boolean onOptionsItemSelected(MenuItem item) { - PrintJob printJob = getPrintJob(); + final PrintJob printJob = use(PrintJobPreferenceController.class).getPrintJob(); if (printJob != null) { switch (item.getItemId()) { @@ -163,113 +109,4 @@ public class PrintJobSettingsFragment extends SettingsPreferenceFragment { return super.onOptionsItemSelected(item); } - - private void processArguments() { - String printJobId = getArguments().getString(EXTRA_PRINT_JOB_ID); - if (printJobId == null) { - printJobId = getIntent().getStringExtra(EXTRA_PRINT_JOB_ID); - - if (printJobId == null) { - Log.w(LOG_TAG, EXTRA_PRINT_JOB_ID + " not set"); - finish(); - return; - } - } - - - mPrintJobId = PrintJobId.unflattenFromString(printJobId); - } - - private PrintJob getPrintJob() { - return mPrintManager.getPrintJob(mPrintJobId); - } - - private void updateUi() { - PrintJob printJob = getPrintJob(); - - if (printJob == null) { - finish(); - return; - } - - if (printJob.isCancelled() || printJob.isCompleted()) { - finish(); - return; - } - - PrintJobInfo info = printJob.getInfo(); - - switch (info.getState()) { - case PrintJobInfo.STATE_CREATED: { - mPrintJobPreference.setTitle(getString( - R.string.print_configuring_state_title_template, info.getLabel())); - } break; - case PrintJobInfo.STATE_QUEUED: - case PrintJobInfo.STATE_STARTED: { - if (!printJob.getInfo().isCancelling()) { - mPrintJobPreference.setTitle(getString( - R.string.print_printing_state_title_template, info.getLabel())); - } else { - mPrintJobPreference.setTitle(getString( - R.string.print_cancelling_state_title_template, info.getLabel())); - } - } break; - - case PrintJobInfo.STATE_FAILED: { - mPrintJobPreference.setTitle(getString( - R.string.print_failed_state_title_template, info.getLabel())); - } break; - - case PrintJobInfo.STATE_BLOCKED: { - if (!printJob.getInfo().isCancelling()) { - mPrintJobPreference.setTitle(getString( - R.string.print_blocked_state_title_template, info.getLabel())); - } else { - mPrintJobPreference.setTitle(getString( - R.string.print_cancelling_state_title_template, info.getLabel())); - } - } break; - } - - mPrintJobPreference.setSummary(getString(R.string.print_job_summary, - info.getPrinterName(), DateUtils.formatSameDayTime( - info.getCreationTime(), info.getCreationTime(), DateFormat.SHORT, - DateFormat.SHORT))); - - TypedArray a = getActivity().obtainStyledAttributes(new int[]{ - android.R.attr.colorControlNormal}); - int tintColor = a.getColor(0, 0); - a.recycle(); - - switch (info.getState()) { - case PrintJobInfo.STATE_QUEUED: - case PrintJobInfo.STATE_STARTED: { - Drawable icon = getActivity().getDrawable(com.android.internal.R.drawable.ic_print); - icon.setTint(tintColor); - mPrintJobPreference.setIcon(icon); - break; - } - - case PrintJobInfo.STATE_FAILED: - case PrintJobInfo.STATE_BLOCKED: { - Drawable icon = getActivity().getDrawable( - com.android.internal.R.drawable.ic_print_error); - icon.setTint(tintColor); - mPrintJobPreference.setIcon(icon); - break; - } - } - - CharSequence status = info.getStatus(getPackageManager()); - if (!TextUtils.isEmpty(status)) { - if (getPreferenceScreen().findPreference(PRINT_JOB_MESSAGE_PREFERENCE) == null) { - getPreferenceScreen().addPreference(mMessagePreference); - } - mMessagePreference.setSummary(status); - } else { - getPreferenceScreen().removePreference(mMessagePreference); - } - - getActivity().invalidateOptionsMenu(); - } } diff --git a/src/com/android/settings/print/PrintServiceSettingsFragment.java b/src/com/android/settings/print/PrintServiceSettingsFragment.java index 3e3582397b..8d4017580b 100644 --- a/src/com/android/settings/print/PrintServiceSettingsFragment.java +++ b/src/com/android/settings/print/PrintServiceSettingsFragment.java @@ -17,20 +17,15 @@ package com.android.settings.print; import android.app.Activity; -import android.app.LoaderManager; +import android.app.settings.SettingsEnums; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentSender.SendIntentException; -import android.content.Loader; import android.content.pm.ResolveInfo; -import android.database.DataSetObserver; -import android.graphics.Color; -import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.print.PrintManager; -import android.print.PrintServicesLoader; import android.print.PrinterDiscoverySession; import android.print.PrinterDiscoverySession.OnPrintersChangeListener; import android.print.PrinterId; @@ -44,21 +39,23 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; -import android.view.ViewGroup; import android.view.View.OnClickListener; +import android.view.ViewGroup; import android.view.accessibility.AccessibilityManager; -import android.widget.AdapterView; -import android.widget.BaseAdapter; import android.widget.Filter; import android.widget.Filterable; import android.widget.ImageView; import android.widget.LinearLayout; -import android.widget.ListView; import android.widget.SearchView; import android.widget.Switch; import android.widget.TextView; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.annotation.NonNull; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.Loader; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver; + import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.SettingsPreferenceFragment; @@ -77,23 +74,18 @@ public class PrintServiceSettingsFragment extends SettingsPreferenceFragment implements SwitchBar.OnSwitchChangeListener, LoaderManager.LoaderCallbacks<List<PrintServiceInfo>> { - private static final String LOG_TAG = "PrintServiceSettingsFragment"; + private static final String LOG_TAG = "PrintServiceSettings"; private static final int LOADER_ID_PRINTERS_LOADER = 1; private static final int LOADER_ID_PRINT_SERVICE_LOADER = 2; - private final DataSetObserver mDataObserver = new DataSetObserver() { + private final AdapterDataObserver mDataObserver = new AdapterDataObserver() { @Override public void onChanged() { invalidateOptionsMenuIfNeeded(); updateEmptyView(); } - @Override - public void onInvalidated() { - invalidateOptionsMenuIfNeeded(); - } - private void invalidateOptionsMenuIfNeeded() { final int unfilteredItemCount = mPrintersAdapter.getUnfilteredCount(); if ((mLastUnfilteredItemCount <= 0 && unfilteredItemCount > 0) @@ -125,7 +117,7 @@ public class PrintServiceSettingsFragment extends SettingsPreferenceFragment @Override public int getMetricsCategory() { - return MetricsEvent.PRINT_SERVICE_SETTINGS; + return SettingsEnums.PRINT_SERVICE_SETTINGS; } @Override @@ -151,6 +143,8 @@ public class PrintServiceSettingsFragment extends SettingsPreferenceFragment @Override public void onStart() { super.onStart(); + initComponents(); + updateUiForArguments(); updateEmptyView(); updateUiForServiceState(); } @@ -166,22 +160,9 @@ public class PrintServiceSettingsFragment extends SettingsPreferenceFragment @Override public void onStop() { super.onStop(); - } - - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - initComponents(); - updateUiForArguments(); - getListView().setVisibility(View.GONE); - getBackupListView().setVisibility(View.VISIBLE); - } - - @Override - public void onDestroyView() { - super.onDestroyView(); mSwitchBar.removeOnSwitchChangeListener(this); mSwitchBar.hide(); + mPrintersAdapter.unregisterAdapterDataObserver(mDataObserver); } private void onPreferenceToggled(String preferenceKey, boolean enabled) { @@ -189,31 +170,24 @@ public class PrintServiceSettingsFragment extends SettingsPreferenceFragment .setPrintServiceEnabled(mComponentName, enabled); } - private ListView getBackupListView() { - return (ListView) getView().findViewById(R.id.backup_list); - } - private void updateEmptyView() { ViewGroup contentRoot = (ViewGroup) getListView().getParent(); - View emptyView = getBackupListView().getEmptyView(); + View emptyView = getEmptyView(); if (!mToggleSwitch.isChecked()) { - if (emptyView != null && emptyView.getId() != R.id.empty_print_state) { + if (emptyView != null) { contentRoot.removeView(emptyView); emptyView = null; } if (emptyView == null) { emptyView = getActivity().getLayoutInflater().inflate( R.layout.empty_print_state, contentRoot, false); - ImageView iconView = (ImageView) emptyView.findViewById(R.id.icon); - iconView.setContentDescription(getString(R.string.print_service_disabled)); TextView textView = (TextView) emptyView.findViewById(R.id.message); textView.setText(R.string.print_service_disabled); contentRoot.addView(emptyView); - getBackupListView().setEmptyView(emptyView); + setEmptyView(emptyView); } } else if (mPrintersAdapter.getUnfilteredCount() <= 0) { - if (emptyView != null - && emptyView.getId() != R.id.empty_printers_list_service_enabled) { + if (emptyView != null) { contentRoot.removeView(emptyView); emptyView = null; } @@ -221,22 +195,24 @@ public class PrintServiceSettingsFragment extends SettingsPreferenceFragment emptyView = getActivity().getLayoutInflater().inflate( R.layout.empty_printers_list_service_enabled, contentRoot, false); contentRoot.addView(emptyView); - getBackupListView().setEmptyView(emptyView); + setEmptyView(emptyView); } - } else if (mPrintersAdapter.getCount() <= 0) { - if (emptyView != null && emptyView.getId() != R.id.empty_print_state) { + } else if (mPrintersAdapter.getItemCount() <= 0) { + if (emptyView != null) { contentRoot.removeView(emptyView); emptyView = null; } if (emptyView == null) { emptyView = getActivity().getLayoutInflater().inflate( R.layout.empty_print_state, contentRoot, false); - ImageView iconView = (ImageView) emptyView.findViewById(R.id.icon); - iconView.setContentDescription(getString(R.string.print_no_printers_found)); TextView textView = (TextView) emptyView.findViewById(R.id.message); textView.setText(R.string.print_no_printers_found); contentRoot.addView(emptyView); - getBackupListView().setEmptyView(emptyView); + setEmptyView(emptyView); + } + } else if (mPrintersAdapter.getItemCount() > 0) { + if (emptyView != null) { + contentRoot.removeView(emptyView); } } } @@ -254,7 +230,7 @@ public class PrintServiceSettingsFragment extends SettingsPreferenceFragment private void initComponents() { mPrintersAdapter = new PrintersAdapter(); - mPrintersAdapter.registerDataSetObserver(mDataObserver); + mPrintersAdapter.registerAdapterDataObserver(mDataObserver); final SettingsActivity activity = (SettingsActivity) getActivity(); @@ -263,31 +239,12 @@ public class PrintServiceSettingsFragment extends SettingsPreferenceFragment mSwitchBar.show(); mToggleSwitch = mSwitchBar.getSwitch(); - mToggleSwitch.setOnBeforeCheckedChangeListener(new ToggleSwitch.OnBeforeCheckedChangeListener() { - @Override - public boolean onBeforeCheckedChanged(ToggleSwitch toggleSwitch, boolean checked) { - onPreferenceToggled(mPreferenceKey, checked); - return false; - } + mToggleSwitch.setOnBeforeCheckedChangeListener((toggleSwitch, checked) -> { + onPreferenceToggled(mPreferenceKey, checked); + return false; }); - getBackupListView().setSelector(new ColorDrawable(Color.TRANSPARENT)); - getBackupListView().setAdapter(mPrintersAdapter); - getBackupListView().setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView<?> parent, View view, int position, long id) { - PrinterInfo printer = (PrinterInfo) mPrintersAdapter.getItem(position); - - if (printer.getInfoIntent() != null) { - try { - getActivity().startIntentSender(printer.getInfoIntent().getIntentSender(), - null, 0, 0, 0); - } catch (SendIntentException e) { - Log.e(LOG_TAG, "Could not execute info intent: %s", e); - } - } - } - }); + getListView().setAdapter(mPrintersAdapter); } @@ -316,7 +273,7 @@ public class PrintServiceSettingsFragment extends SettingsPreferenceFragment @Override public Loader<List<PrintServiceInfo>> onCreateLoader(int id, Bundle args) { - return new PrintServicesLoader( + return new SettingsPrintServicesLoader( (PrintManager) getContext().getSystemService(Context.PRINT_SERVICE), getContext(), PrintManager.ALL_SERVICES); } @@ -446,8 +403,17 @@ public class PrintServiceSettingsFragment extends SettingsPreferenceFragment } } - private final class PrintersAdapter extends BaseAdapter + public static class ViewHolder extends RecyclerView.ViewHolder { + + public ViewHolder(@NonNull View itemView) { + super(itemView); + } + } + + + private final class PrintersAdapter extends RecyclerView.Adapter<ViewHolder> implements LoaderManager.LoaderCallbacks<List<PrinterInfo>>, Filterable { + private final Object mLock = new Object(); private final List<PrinterInfo> mPrinters = new ArrayList<PrinterInfo>(); @@ -509,19 +475,19 @@ public class PrintServiceSettingsFragment extends SettingsPreferenceFragment } } notifyDataSetChanged(); + } }; } @Override - public int getCount() { + public int getItemCount() { synchronized (mLock) { return mFilteredPrinters.size(); } } - @Override - public Object getItem(int position) { + private Object getItem(int position) { synchronized (mLock) { return mFilteredPrinters.get(position); } @@ -543,24 +509,27 @@ public class PrintServiceSettingsFragment extends SettingsPreferenceFragment return printer.getStatus() != PrinterInfo.STATUS_UNAVAILABLE; } + @NonNull @Override - public View getView(int position, View convertView, ViewGroup parent) { - if (convertView == null) { - convertView = getActivity().getLayoutInflater().inflate( - R.layout.printer_dropdown_item, parent, false); - } + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + final View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.printer_dropdown_item, parent, false); + return new ViewHolder(view); + } - convertView.setEnabled(isActionable(position)); + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + holder.itemView.setEnabled(isActionable(position)); final PrinterInfo printer = (PrinterInfo) getItem(position); CharSequence title = printer.getName(); CharSequence subtitle = printer.getDescription(); Drawable icon = printer.loadIcon(getActivity()); - TextView titleView = (TextView) convertView.findViewById(R.id.title); + TextView titleView = holder.itemView.findViewById(R.id.title); titleView.setText(title); - TextView subtitleView = (TextView) convertView.findViewById(R.id.subtitle); + TextView subtitleView = holder.itemView.findViewById(R.id.subtitle); if (!TextUtils.isEmpty(subtitle)) { subtitleView.setText(subtitle); subtitleView.setVisibility(View.VISIBLE); @@ -569,7 +538,7 @@ public class PrintServiceSettingsFragment extends SettingsPreferenceFragment subtitleView.setVisibility(View.GONE); } - LinearLayout moreInfoView = (LinearLayout) convertView.findViewById(R.id.more_info); + LinearLayout moreInfoView = holder.itemView.findViewById(R.id.more_info); if (printer.getInfoIntent() != null) { moreInfoView.setVisibility(View.VISIBLE); moreInfoView.setOnClickListener(new OnClickListener() { @@ -587,7 +556,7 @@ public class PrintServiceSettingsFragment extends SettingsPreferenceFragment moreInfoView.setVisibility(View.GONE); } - ImageView iconView = (ImageView) convertView.findViewById(R.id.icon); + ImageView iconView = holder.itemView.findViewById(R.id.icon); if (icon != null) { iconView.setVisibility(View.VISIBLE); if (!isActionable(position)) { @@ -603,7 +572,18 @@ public class PrintServiceSettingsFragment extends SettingsPreferenceFragment iconView.setVisibility(View.GONE); } - return convertView; + holder.itemView.setOnClickListener(v -> { + PrinterInfo pi = (PrinterInfo) getItem(position); + + if (pi.getInfoIntent() != null) { + try { + getActivity().startIntentSender(pi.getInfoIntent().getIntentSender(), + null, 0, 0, 0); + } catch (SendIntentException e) { + Log.e(LOG_TAG, "Could not execute info intent: %s", e); + } + } + }); } @Override @@ -642,7 +622,7 @@ public class PrintServiceSettingsFragment extends SettingsPreferenceFragment mFilteredPrinters.clear(); mLastSearchString = null; } - notifyDataSetInvalidated(); + notifyDataSetChanged(); } } diff --git a/src/com/android/settings/print/PrintSettingPreferenceController.java b/src/com/android/settings/print/PrintSettingPreferenceController.java index f40846a121..16c432032f 100644 --- a/src/com/android/settings/print/PrintSettingPreferenceController.java +++ b/src/com/android/settings/print/PrintSettingPreferenceController.java @@ -24,6 +24,7 @@ import android.print.PrintJobId; import android.print.PrintJobInfo; import android.print.PrintManager; import android.printservice.PrintServiceInfo; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; diff --git a/src/com/android/settings/print/PrintSettingsFragment.java b/src/com/android/settings/print/PrintSettingsFragment.java index b42be8f29e..09a0a279e4 100644 --- a/src/com/android/settings/print/PrintSettingsFragment.java +++ b/src/com/android/settings/print/PrintSettingsFragment.java @@ -18,13 +18,11 @@ package com.android.settings.print; import static com.android.settings.print.PrintSettingPreferenceController.shouldShowToUser; -import android.app.LoaderManager.LoaderCallbacks; +import android.app.settings.SettingsEnums; import android.content.ActivityNotFoundException; -import android.content.AsyncTaskLoader; import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.Loader; import android.content.pm.PackageManager; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; @@ -35,12 +33,9 @@ import android.print.PrintJobId; import android.print.PrintJobInfo; import android.print.PrintManager; import android.print.PrintManager.PrintJobStateChangeListener; -import android.print.PrintServicesLoader; import android.printservice.PrintServiceInfo; import android.provider.SearchIndexableResource; import android.provider.Settings; -import androidx.preference.Preference; -import androidx.preference.PreferenceCategory; import android.text.TextUtils; import android.text.format.DateUtils; import android.util.Log; @@ -51,11 +46,17 @@ import android.view.ViewGroup; import android.widget.Button; import android.widget.TextView; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.loader.app.LoaderManager.LoaderCallbacks; +import androidx.loader.content.AsyncTaskLoader; +import androidx.loader.content.Loader; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; + import com.android.settings.R; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; -import com.android.settings.utils.ProfileSettingsPreferenceFragment; +import com.android.settingslib.search.SearchIndexable; +import com.android.settingslib.widget.apppreference.AppPreference; import java.text.DateFormat; import java.util.ArrayList; @@ -64,6 +65,7 @@ import java.util.List; /** * Fragment with the top level print settings. */ +@SearchIndexable public class PrintSettingsFragment extends ProfileSettingsPreferenceFragment implements Indexable, OnClickListener { public static final String TAG = "PrintSettingsFragment"; @@ -94,7 +96,7 @@ public class PrintSettingsFragment extends ProfileSettingsPreferenceFragment @Override public int getMetricsCategory() { - return MetricsEvent.PRINT_SETTINGS; + return SettingsEnums.PRINT_SETTINGS; } @Override @@ -165,7 +167,7 @@ public class PrintSettingsFragment extends ProfileSettingsPreferenceFragment PrintManager printManager = (PrintManager) getContext().getSystemService(Context.PRINT_SERVICE); if (printManager != null) { - return new PrintServicesLoader(printManager, getContext(), + return new SettingsPrintServicesLoader(printManager, getContext(), PrintManager.ALL_SERVICES); } else { return null; @@ -191,7 +193,7 @@ public class PrintSettingsFragment extends ProfileSettingsPreferenceFragment } for (PrintServiceInfo service : services) { - Preference preference = new Preference(context); + AppPreference preference = new AppPreference(context); String title = service.getResolveInfo().loadLabel(pm).toString(); preference.setTitle(title); @@ -240,7 +242,7 @@ public class PrintSettingsFragment extends ProfileSettingsPreferenceFragment } Preference preference = new Preference(getPrefContext()); preference.setTitle(R.string.print_menu_item_add_service); - preference.setIcon(R.drawable.ic_menu_add); + preference.setIcon(R.drawable.ic_add_24dp); preference.setOrder(ORDER_LAST); preference.setIntent(addNewServiceIntent); preference.setPersistent(false); @@ -354,7 +356,7 @@ public class PrintSettingsFragment extends ProfileSettingsPreferenceFragment printJob.getCreationTime(), printJob.getCreationTime(), DateFormat.SHORT, DateFormat.SHORT))); - TypedArray a = getActivity().obtainStyledAttributes(new int[] { + TypedArray a = getActivity().obtainStyledAttributes(new int[]{ android.R.attr.colorControlNormal}); int tintColor = a.getColor(0, 0); a.recycle(); diff --git a/src/com/android/settings/utils/ProfileSettingsPreferenceFragment.java b/src/com/android/settings/print/ProfileSettingsPreferenceFragment.java index e1c4d285db..b616ccc432 100644 --- a/src/com/android/settings/utils/ProfileSettingsPreferenceFragment.java +++ b/src/com/android/settings/print/ProfileSettingsPreferenceFragment.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.settings.utils; +package com.android.settings.print; import android.content.Context; import android.content.Intent; @@ -27,7 +27,7 @@ import android.widget.Spinner; import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; -import com.android.settingslib.drawer.UserAdapter; +import com.android.settings.dashboard.profileselector.UserAdapter; /** * Base fragment class for per profile settings. @@ -46,8 +46,8 @@ public abstract class ProfileSettingsPreferenceFragment extends SettingsPreferen spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> parent, View view, int position, - long id) { - UserHandle selectedUser = profileSpinnerAdapter.getUserHandle(position); + long id) { + final UserHandle selectedUser = profileSpinnerAdapter.getUserHandle(position); if (selectedUser.getIdentifier() != UserHandle.myUserId()) { Intent intent = new Intent(getIntentActionString()); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); diff --git a/src/com/android/settings/print/SettingsPrintServicesLoader.java b/src/com/android/settings/print/SettingsPrintServicesLoader.java new file mode 100644 index 0000000000..e94c589d4b --- /dev/null +++ b/src/com/android/settings/print/SettingsPrintServicesLoader.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2018 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.print; + +import android.annotation.NonNull; +import android.content.Context; +import android.print.PrintManager; +import android.print.PrintServicesLoader; +import android.printservice.PrintServiceInfo; + +import androidx.loader.content.Loader; + +import com.android.internal.util.Preconditions; + +import java.util.List; + +/** + * Loader for the list of print services. Can be parametrized to select a subset. + */ +public class SettingsPrintServicesLoader extends Loader<List<PrintServiceInfo>> { + + private PrintServicesLoader mLoader; + + public SettingsPrintServicesLoader(@NonNull PrintManager printManager, @NonNull Context context, + int selectionFlags) { + super(Preconditions.checkNotNull(context)); + + mLoader = new PrintServicesLoader(printManager, context, selectionFlags) { + @Override + public void deliverResult(List<PrintServiceInfo> data) { + super.deliverResult(data); + + // deliver the result to outer Loader class + SettingsPrintServicesLoader.this.deliverResult(data); + } + }; + } + + @Override + protected void onForceLoad() { + mLoader.forceLoad(); + } + + @Override + protected void onStartLoading() { + mLoader.startLoading(); + } + + @Override + protected void onStopLoading() { + mLoader.stopLoading(); + } + + @Override + protected boolean onCancelLoad() { + return mLoader.cancelLoad(); + } + + @Override + protected void onAbandon() { + mLoader.abandon(); + } + + @Override + protected void onReset() { + mLoader.reset(); + } +} diff --git a/src/com/android/settings/privacy/AccessibilityUsagePreferenceController.java b/src/com/android/settings/privacy/AccessibilityUsagePreferenceController.java new file mode 100644 index 0000000000..8aff2238a4 --- /dev/null +++ b/src/com/android/settings/privacy/AccessibilityUsagePreferenceController.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2019 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.privacy; + +import android.accessibilityservice.AccessibilityServiceInfo; +import android.content.Context; +import android.view.accessibility.AccessibilityManager; + +import androidx.annotation.NonNull; +import androidx.preference.Preference; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; + +import java.util.List; + + +public class AccessibilityUsagePreferenceController extends BasePreferenceController { + + private final @NonNull List<AccessibilityServiceInfo> mEnabledServiceInfos; + + public AccessibilityUsagePreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + + final AccessibilityManager accessibilityManager = context.getSystemService( + AccessibilityManager.class); + mEnabledServiceInfos = accessibilityManager.getEnabledAccessibilityServiceList( + AccessibilityServiceInfo.FEEDBACK_ALL_MASK); + } + + @Override + public int getAvailabilityStatus() { + return mEnabledServiceInfos.isEmpty() ? UNSUPPORTED_ON_DEVICE : AVAILABLE; + } + + @Override + public CharSequence getSummary() { + return mContext.getResources().getQuantityString(R.plurals.accessibility_usage_summary, + mEnabledServiceInfos.size(), mEnabledServiceInfos.size()); + } +} diff --git a/src/com/android/settings/privacy/EnableContentCapturePreferenceController.java b/src/com/android/settings/privacy/EnableContentCapturePreferenceController.java new file mode 100644 index 0000000000..47610aa18b --- /dev/null +++ b/src/com/android/settings/privacy/EnableContentCapturePreferenceController.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2019 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.privacy; + +import android.annotation.NonNull; +import android.content.Context; +import android.provider.Settings; + +import com.android.settings.core.TogglePreferenceController; +import com.android.settings.utils.ContentCaptureUtils; + +public final class EnableContentCapturePreferenceController extends TogglePreferenceController { + + public EnableContentCapturePreferenceController(@NonNull Context context, @NonNull String key) { + super(context, key); + } + + @Override + public boolean isChecked() { + return ContentCaptureUtils.isEnabledForUser(mContext); + } + + @Override + public boolean setChecked(boolean isChecked) { + ContentCaptureUtils.setEnabledForUser(mContext, isChecked); + return true; + } + + @Override + public int getAvailabilityStatus() { + boolean available = ContentCaptureUtils.isFeatureAvailable() + && ContentCaptureUtils.getServiceSettingsComponentName() == null; + return available ? AVAILABLE : UNSUPPORTED_ON_DEVICE; + } +} diff --git a/src/com/android/settings/privacy/EnableContentCaptureWithServiceSettingsPreferenceController.java b/src/com/android/settings/privacy/EnableContentCaptureWithServiceSettingsPreferenceController.java new file mode 100644 index 0000000000..809bfbdc21 --- /dev/null +++ b/src/com/android/settings/privacy/EnableContentCaptureWithServiceSettingsPreferenceController.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2019 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.privacy; + +import android.annotation.NonNull; +import android.content.ComponentName; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.UserInfo; +import android.os.UserHandle; +import android.os.UserManager; +import android.util.Log; + +import androidx.appcompat.app.AlertDialog; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.core.TogglePreferenceController; +import com.android.settings.dashboard.profileselector.UserAdapter; +import com.android.settings.utils.ContentCaptureUtils; + +import java.util.ArrayList; +import java.util.List; + +public final class EnableContentCaptureWithServiceSettingsPreferenceController + extends TogglePreferenceController { + + private static final String TAG = "ContentCaptureController"; + + private final UserManager mUserManager; + + public EnableContentCaptureWithServiceSettingsPreferenceController(@NonNull Context context, + @NonNull String key) { + super(context, key); + + mUserManager = UserManager.get(context); + } + + @Override + public boolean isChecked() { + return ContentCaptureUtils.isEnabledForUser(mContext); + } + + @Override + public boolean setChecked(boolean isChecked) { + ContentCaptureUtils.setEnabledForUser(mContext, isChecked); + return true; + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + + ComponentName componentName = ContentCaptureUtils.getServiceSettingsComponentName(); + if (componentName != null) { + preference.setIntent(new Intent(Intent.ACTION_MAIN).setComponent(componentName)); + } else { + // Should not happen - preference should be disabled by controller + Log.w(TAG, "No component name for custom service settings"); + preference.setSelectable(false); + } + + preference.setOnPreferenceClickListener((pref) -> { + ProfileSelectDialog.show(mContext, pref); + return true; + }); + } + + @Override + public int getAvailabilityStatus() { + boolean available = ContentCaptureUtils.isFeatureAvailable() + && ContentCaptureUtils.getServiceSettingsComponentName() != null; + return available ? AVAILABLE : UNSUPPORTED_ON_DEVICE; + } + + private static final class ProfileSelectDialog { + public static void show(Context context, Preference pref) { + final UserManager userManager = UserManager.get(context); + final List<UserInfo> userInfos = userManager.getUsers(); + final ArrayList<UserHandle> userHandles = new ArrayList<>(userInfos.size()); + for (UserInfo info: userInfos) { + userHandles.add(info.getUserHandle()); + } + + AlertDialog.Builder builder = new AlertDialog.Builder(context); + UserAdapter adapter = UserAdapter.createUserAdapter(userManager, context, userHandles); + builder.setTitle(com.android.settingslib.R.string.choose_profile) + .setAdapter(adapter, (DialogInterface dialog, int which) -> { + final UserHandle user = userHandles.get(which); + // Show menu on top level items. + final Intent intent = pref.getIntent(); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + context.startActivityAsUser(intent, user); + }) + .show(); + } + } + +} diff --git a/src/com/android/settings/privacy/PermissionBarChartPreferenceController.java b/src/com/android/settings/privacy/PermissionBarChartPreferenceController.java new file mode 100644 index 0000000000..28533df03a --- /dev/null +++ b/src/com/android/settings/privacy/PermissionBarChartPreferenceController.java @@ -0,0 +1,243 @@ +/* + * Copyright (C) 2019 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.privacy; + +import static android.Manifest.permission_group.CAMERA; +import static android.Manifest.permission_group.LOCATION; +import static android.Manifest.permission_group.MICROPHONE; + +import static java.util.concurrent.TimeUnit.DAYS; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.permission.PermissionControllerManager; +import android.permission.RuntimePermissionUsageInfo; +import android.provider.DeviceConfig; +import android.util.Log; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; +import com.android.settingslib.Utils; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnCreate; +import com.android.settingslib.core.lifecycle.events.OnSaveInstanceState; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.widget.BarChartInfo; +import com.android.settingslib.widget.BarChartPreference; +import com.android.settingslib.widget.BarViewInfo; + +import java.util.ArrayList; +import java.util.List; + + +public class PermissionBarChartPreferenceController extends BasePreferenceController implements + PermissionControllerManager.OnPermissionUsageResultCallback, LifecycleObserver, OnCreate, + OnStart, OnSaveInstanceState { + + private static final String TAG = "BarChartPreferenceCtl"; + private static final String KEY_PERMISSION_USAGE = "usage_infos"; + + @VisibleForTesting + List<RuntimePermissionUsageInfo> mOldUsageInfos; + private PackageManager mPackageManager; + private PrivacyDashboardFragment mParent; + private BarChartPreference mBarChartPreference; + + public PermissionBarChartPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + mOldUsageInfos = new ArrayList<>(); + mPackageManager = context.getPackageManager(); + } + + public void setFragment(PrivacyDashboardFragment fragment) { + mParent = fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + if (savedInstanceState != null) { + mOldUsageInfos = savedInstanceState.getParcelableArrayList(KEY_PERMISSION_USAGE); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + outState.putParcelableList(KEY_PERMISSION_USAGE, mOldUsageInfos); + } + + @Override + public int getAvailabilityStatus() { + return UNSUPPORTED_ON_DEVICE; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mBarChartPreference = screen.findPreference(getPreferenceKey()); + + final BarChartInfo info = new BarChartInfo.Builder() + .setTitle(R.string.permission_bar_chart_title) + .setDetails(R.string.permission_bar_chart_details) + .setEmptyText(R.string.permission_bar_chart_empty_text) + .setDetailsOnClickListener((View v) -> { + final Intent intent = new Intent(Intent.ACTION_REVIEW_PERMISSION_USAGE); + intent.putExtra(Intent.EXTRA_DURATION_MILLIS, DAYS.toMillis(1)); + mContext.startActivity(intent); + }) + .build(); + + mBarChartPreference.initializeBarChart(info); + if (!mOldUsageInfos.isEmpty()) { + mBarChartPreference.setBarViewInfos(createBarViews(mOldUsageInfos)); + } + } + + @Override + public void onStart() { + if (!isAvailable()) { + return; + } + + // We don't hide chart when we have existing data. + mBarChartPreference.updateLoadingState(mOldUsageInfos.isEmpty() /* isLoading */); + // But we still need to hint user with progress bar that we are updating new usage data. + mParent.setLoadingEnabled(true /* enabled */); + retrievePermissionUsageData(); + } + + @Override + public void onPermissionUsageResult(@NonNull List<RuntimePermissionUsageInfo> usageInfos) { + usageInfos.sort((x, y) -> { + int usageDiff = y.getAppAccessCount() - x.getAppAccessCount(); + if (usageDiff != 0) { + return usageDiff; + } + String xName = x.getName(); + String yName = y.getName(); + if (xName.equals(LOCATION)) { + return -1; + } else if (yName.equals(LOCATION)) { + return 1; + } else if (xName.equals(MICROPHONE)) { + return -1; + } else if (yName.equals(MICROPHONE)) { + return 1; + } else if (xName.equals(CAMERA)) { + return -1; + } else if (yName.equals(CAMERA)) { + return 1; + } + return x.getName().compareTo(y.getName()); + }); + + // If the result is different, we need to update bar views. + if (!areSamePermissionGroups(usageInfos)) { + mBarChartPreference.setBarViewInfos(createBarViews(usageInfos)); + mOldUsageInfos = usageInfos; + } + + mBarChartPreference.updateLoadingState(false /* isLoading */); + mParent.setLoadingEnabled(false /* enabled */); + } + + private void retrievePermissionUsageData() { + mContext.getSystemService(PermissionControllerManager.class).getPermissionUsages( + false /* countSystem */, (int) DAYS.toMillis(1), + mContext.getMainExecutor() /* executor */, this /* callback */); + } + + private BarViewInfo[] createBarViews(List<RuntimePermissionUsageInfo> usageInfos) { + if (usageInfos.isEmpty()) { + return null; + } + + final BarViewInfo[] barViewInfos = new BarViewInfo[ + Math.min(BarChartPreference.MAXIMUM_BAR_VIEWS, usageInfos.size())]; + + for (int index = 0; index < barViewInfos.length; index++) { + final RuntimePermissionUsageInfo permissionGroupInfo = usageInfos.get(index); + final int count = permissionGroupInfo.getAppAccessCount(); + final CharSequence permLabel = getPermissionGroupLabel(permissionGroupInfo.getName()); + + barViewInfos[index] = new BarViewInfo( + getPermissionGroupIcon(permissionGroupInfo.getName()), count, permLabel, + mContext.getResources().getQuantityString(R.plurals.permission_bar_chart_label, + count, count), permLabel); + + // Set the click listener for each bar view. + // The listener will navigate user to permission usage app. + barViewInfos[index].setClickListener((View v) -> { + final Intent intent = new Intent(Intent.ACTION_REVIEW_PERMISSION_USAGE); + intent.putExtra(Intent.EXTRA_PERMISSION_GROUP_NAME, permissionGroupInfo.getName()); + intent.putExtra(Intent.EXTRA_DURATION_MILLIS, DAYS.toMillis(1)); + mContext.startActivity(intent); + }); + } + + return barViewInfos; + } + + private Drawable getPermissionGroupIcon(String permissionGroup) { + Drawable icon = null; + try { + icon = mPackageManager.getPermissionGroupInfo(permissionGroup, 0) + .loadIcon(mPackageManager); + icon.setTintList(Utils.getColorAttr(mContext, android.R.attr.textColorSecondary)); + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "Cannot find group icon for " + permissionGroup, e); + } + + return icon; + } + + private CharSequence getPermissionGroupLabel(String permissionGroup) { + CharSequence label = null; + try { + label = mPackageManager.getPermissionGroupInfo(permissionGroup, 0) + .loadLabel(mPackageManager); + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "Cannot find group label for " + permissionGroup, e); + } + + return label; + } + + private boolean areSamePermissionGroups(List<RuntimePermissionUsageInfo> newUsageInfos) { + if (newUsageInfos.size() != mOldUsageInfos.size()) { + return false; + } + + for (int index = 0; index < newUsageInfos.size(); index++) { + final RuntimePermissionUsageInfo newInfo = newUsageInfos.get(index); + final RuntimePermissionUsageInfo oldInfo = mOldUsageInfos.get(index); + + if (!newInfo.getName().equals(oldInfo.getName()) || + newInfo.getAppAccessCount() != oldInfo.getAppAccessCount()) { + return false; + } + } + return true; + } +} diff --git a/src/com/android/settings/privacy/PrivacyDashboardFragment.java b/src/com/android/settings/privacy/PrivacyDashboardFragment.java new file mode 100644 index 0000000000..fa21f9dd44 --- /dev/null +++ b/src/com/android/settings/privacy/PrivacyDashboardFragment.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2018 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.privacy; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.os.Bundle; +import android.provider.SearchIndexableResource; +import android.view.View; + +import androidx.annotation.VisibleForTesting; + +import com.android.settings.R; +import com.android.settings.Utils; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.notification.LockScreenNotificationPreferenceController; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.search.SearchIndexable; + +import java.util.ArrayList; +import java.util.List; + +@SearchIndexable +public class PrivacyDashboardFragment extends DashboardFragment { + private static final String TAG = "PrivacyDashboardFrag"; + private static final String KEY_LOCK_SCREEN_NOTIFICATIONS = "privacy_lock_screen_notifications"; + private static final String KEY_WORK_PROFILE_CATEGORY = + "privacy_work_profile_notifications_category"; + private static final String KEY_NOTIFICATION_WORK_PROFILE_NOTIFICATIONS = + "privacy_lock_screen_work_profile_notifications"; + + @VisibleForTesting + View mProgressHeader; + @VisibleForTesting + View mProgressAnimation; + + @Override + public int getMetricsCategory() { + return SettingsEnums.TOP_LEVEL_PRIVACY; + } + + @Override + protected String getLogTag() { + return TAG; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.privacy_dashboard_settings; + } + + @Override + public int getHelpResource() { + return R.string.help_url_privacy_dashboard; + } + + @Override + protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { + return buildPreferenceControllers(context, getSettingsLifecycle()); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + use(PermissionBarChartPreferenceController.class).setFragment(this /* fragment */); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + Utils.setActionBarShadowAnimation(getActivity(), getSettingsLifecycle(), getListView()); + initLoadingBar(); + } + + @VisibleForTesting + void initLoadingBar() { + mProgressHeader = setPinnedHeaderView(R.layout.progress_header); + mProgressAnimation = mProgressHeader.findViewById(R.id.progress_bar_animation); + setLoadingEnabled(false); + } + + @VisibleForTesting + void setLoadingEnabled(boolean enabled) { + if (mProgressHeader != null && mProgressAnimation != null) { + mProgressHeader.setVisibility(enabled ? View.VISIBLE : View.INVISIBLE); + mProgressAnimation.setVisibility(enabled ? View.VISIBLE : View.INVISIBLE); + } + } + + private static List<AbstractPreferenceController> buildPreferenceControllers( + Context context, Lifecycle lifecycle) { + final List<AbstractPreferenceController> controllers = new ArrayList<>(); + final LockScreenNotificationPreferenceController notificationController = + new LockScreenNotificationPreferenceController(context, + KEY_LOCK_SCREEN_NOTIFICATIONS, + KEY_WORK_PROFILE_CATEGORY, + KEY_NOTIFICATION_WORK_PROFILE_NOTIFICATIONS); + if (lifecycle != null) { + lifecycle.addObserver(notificationController); + } + controllers.add(notificationController); + + return controllers; + + } + + public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, + boolean enabled) { + final ArrayList<SearchIndexableResource> result = new ArrayList<>(); + + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.privacy_dashboard_settings; + result.add(sir); + return result; + } + + @Override + public List<AbstractPreferenceController> createPreferenceControllers( + Context context) { + return buildPreferenceControllers(context, null); + } + }; +} diff --git a/src/com/android/settings/privacy/WorkPolicyInfoPreferenceController.java b/src/com/android/settings/privacy/WorkPolicyInfoPreferenceController.java new file mode 100644 index 0000000000..45c2c21d57 --- /dev/null +++ b/src/com/android/settings/privacy/WorkPolicyInfoPreferenceController.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2019 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.privacy; + +import android.content.Context; +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.preference.Preference; + +import com.android.settings.core.BasePreferenceController; +import com.android.settings.enterprise.EnterprisePrivacyFeatureProvider; +import com.android.settings.overlay.FeatureFactory; + +public class WorkPolicyInfoPreferenceController extends BasePreferenceController { + + private final @NonNull EnterprisePrivacyFeatureProvider mEnterpriseProvider; + + public WorkPolicyInfoPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + mEnterpriseProvider = + FeatureFactory.getFactory(context).getEnterprisePrivacyFeatureProvider(context); + } + + @Override + public int getAvailabilityStatus() { + return mEnterpriseProvider.hasWorkPolicyInfo() + ? AVAILABLE_UNSEARCHABLE + : UNSUPPORTED_ON_DEVICE; + } + + @Override + public boolean handlePreferenceTreeClick(Preference preference) { + if (TextUtils.equals(getPreferenceKey(), preference.getKey())) { + mEnterpriseProvider.showWorkPolicyInfo(); + return true; + } + return false; + } +} diff --git a/src/com/android/settings/search/BaseSearchIndexProvider.java b/src/com/android/settings/search/BaseSearchIndexProvider.java index 3864750a67..80775bf82e 100644 --- a/src/com/android/settings/search/BaseSearchIndexProvider.java +++ b/src/com/android/settings/search/BaseSearchIndexProvider.java @@ -16,16 +16,20 @@ package com.android.settings.search; +import static com.android.settings.core.PreferenceXmlParserUtils.METADATA_KEY; +import static com.android.settings.core.PreferenceXmlParserUtils.METADATA_SEARCHABLE; +import static com.android.settings.core.PreferenceXmlParserUtils.MetadataFlag.FLAG_INCLUDE_PREF_SCREEN; +import static com.android.settings.core.PreferenceXmlParserUtils.MetadataFlag.FLAG_NEED_KEY; +import static com.android.settings.core.PreferenceXmlParserUtils.MetadataFlag.FLAG_NEED_SEARCHABLE; + import android.annotation.XmlRes; import android.content.Context; -import android.content.res.XmlResourceParser; +import android.os.Bundle; import android.provider.SearchIndexableResource; +import android.util.Log; + import androidx.annotation.CallSuper; import androidx.annotation.VisibleForTesting; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.util.Log; -import android.util.Xml; import com.android.settings.core.BasePreferenceController; import com.android.settings.core.PreferenceControllerListHelper; @@ -33,7 +37,6 @@ import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.core.PreferenceXmlParserUtils; import com.android.settingslib.core.AbstractPreferenceController; -import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; @@ -65,11 +68,12 @@ public class BaseSearchIndexProvider implements Indexable.SearchIndexProvider { public List<String> getNonIndexableKeys(Context context) { if (!isPageSearchEnabled(context)) { // Entire page should be suppressed, mark all keys from this page as non-indexable. - return getNonIndexableKeysFromXml(context); + return getNonIndexableKeysFromXml(context, true /* suppressAllPage */); } + final List<String> nonIndexableKeys = new ArrayList<>(); + nonIndexableKeys.addAll(getNonIndexableKeysFromXml(context, false /* suppressAllPage */)); final List<AbstractPreferenceController> controllers = getPreferenceControllers(context); if (controllers != null && !controllers.isEmpty()) { - final List<String> nonIndexableKeys = new ArrayList<>(); for (AbstractPreferenceController controller : controllers) { if (controller instanceof PreferenceControllerMixin) { ((PreferenceControllerMixin) controller) @@ -84,10 +88,8 @@ public class BaseSearchIndexProvider implements Indexable.SearchIndexProvider { nonIndexableKeys.add(controller.getPreferenceKey()); } } - return nonIndexableKeys; - } else { - return new ArrayList<>(); } + return nonIndexableKeys; } @Override @@ -130,7 +132,11 @@ public class BaseSearchIndexProvider implements Indexable.SearchIndexProvider { return true; } - private List<String> getNonIndexableKeysFromXml(Context context) { + /** + * Get all non-indexable keys from xml. If {@param suppressAllPage} is set, all keys are + * considered non-indexable. Otherwise, only keys with searchable="false" are included. + */ + private List<String> getNonIndexableKeysFromXml(Context context, boolean suppressAllPage) { final List<SearchIndexableResource> resources = getXmlResourcesToIndex( context, true /* not used*/); if (resources == null || resources.isEmpty()) { @@ -138,27 +144,32 @@ public class BaseSearchIndexProvider implements Indexable.SearchIndexProvider { } final List<String> nonIndexableKeys = new ArrayList<>(); for (SearchIndexableResource res : resources) { - nonIndexableKeys.addAll(getNonIndexableKeysFromXml(context, res.xmlResId)); + nonIndexableKeys.addAll( + getNonIndexableKeysFromXml(context, res.xmlResId, suppressAllPage)); } return nonIndexableKeys; } @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) - public List<String> getNonIndexableKeysFromXml(Context context, @XmlRes int xmlResId) { - final List<String> nonIndexableKeys = new ArrayList<>(); - final XmlResourceParser parser = context.getResources().getXml(xmlResId); - final AttributeSet attrs = Xml.asAttributeSet(parser); + public List<String> getNonIndexableKeysFromXml(Context context, @XmlRes int xmlResId, + boolean suppressAllPage) { + return getKeysFromXml(context, xmlResId, suppressAllPage); + } + + private List<String> getKeysFromXml(Context context, @XmlRes int xmlResId, + boolean suppressAllPage) { + final List<String> keys = new ArrayList<>(); try { - while (parser.next() != XmlPullParser.END_DOCUMENT) { - final String key = PreferenceXmlParserUtils.getDataKey(context, attrs); - if (!TextUtils.isEmpty(key)) { - nonIndexableKeys.add(key); + final List<Bundle> metadata = PreferenceXmlParserUtils.extractMetadata(context, + xmlResId, FLAG_NEED_KEY | FLAG_INCLUDE_PREF_SCREEN | FLAG_NEED_SEARCHABLE); + for (Bundle bundle : metadata) { + if (suppressAllPage || !bundle.getBoolean(METADATA_SEARCHABLE, true)) { + keys.add(bundle.getString(METADATA_KEY)); } } } catch (IOException | XmlPullParserException e) { Log.w(TAG, "Error parsing non-indexable from xml " + xmlResId); } - return nonIndexableKeys; + return keys; } - } diff --git a/src/com/android/settings/search/DatabaseIndexingManager.java b/src/com/android/settings/search/DatabaseIndexingManager.java deleted file mode 100644 index e793cac547..0000000000 --- a/src/com/android/settings/search/DatabaseIndexingManager.java +++ /dev/null @@ -1,348 +0,0 @@ -/* - * Copyright (C) 2017 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.search; - - -import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_ID; -import static com.android.settings.search.DatabaseResultLoader - .COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE; -import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_KEY; -import static com.android.settings.search.DatabaseResultLoader.SELECT_COLUMNS; -import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.CLASS_NAME; -import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_ENTRIES; -import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_KEYWORDS; -import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_KEY_REF; -import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_ON; -import static com.android.settings.search.IndexDatabaseHelper.IndexColumns - .DATA_SUMMARY_ON_NORMALIZED; -import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_TITLE; -import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_TITLE_NORMALIZED; -import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DOCID; -import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.ENABLED; -import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.ICON; -import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.INTENT_ACTION; -import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.INTENT_TARGET_CLASS; -import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.INTENT_TARGET_PACKAGE; -import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.LOCALE; -import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.PAYLOAD; -import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.PAYLOAD_TYPE; -import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.SCREEN_TITLE; -import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.USER_ID; -import static com.android.settings.search.IndexDatabaseHelper.Tables.TABLE_PREFS_INDEX; - -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ResolveInfo; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteException; -import android.os.Build; -import android.provider.SearchIndexablesContract; -import android.provider.SearchIndexablesContract.SiteMapColumns; -import androidx.annotation.VisibleForTesting; -import android.text.TextUtils; -import android.util.Log; - -import com.android.settings.overlay.FeatureFactory; -import com.android.settings.search.indexing.IndexData; -import com.android.settings.search.indexing.IndexDataConverter; -import com.android.settings.search.indexing.PreIndexData; -import com.android.settings.search.indexing.PreIndexDataCollector; - -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; - -/** - * Consumes the SearchIndexableProvider content providers. - * Updates the Resource, Raw Data and non-indexable data for Search. - * - * TODO(b/33577327) this class needs to be refactored by moving most of its methods into controllers - */ -public class DatabaseIndexingManager { - - private static final String LOG_TAG = "DatabaseIndexingManager"; - - private PreIndexDataCollector mCollector; - private IndexDataConverter mConverter; - - private Context mContext; - - public DatabaseIndexingManager(Context context) { - mContext = context; - } - - /** - * Accumulate all data and non-indexable keys from each of the content-providers. - * Only the first indexing for the default language gets static search results - subsequent - * calls will only gather non-indexable keys. - */ - public void performIndexing() { - final long startTime = System.currentTimeMillis(); - final Intent intent = new Intent(SearchIndexablesContract.PROVIDER_INTERFACE); - final List<ResolveInfo> providers = - mContext.getPackageManager().queryIntentContentProviders(intent, 0); - - final String localeStr = Locale.getDefault().toString(); - final String fingerprint = Build.FINGERPRINT; - final String providerVersionedNames = - IndexDatabaseHelper.buildProviderVersionedNames(providers); - - final boolean isFullIndex = isFullIndex(mContext, localeStr, fingerprint, - providerVersionedNames); - - if (isFullIndex) { - rebuildDatabase(); - } - - PreIndexData indexData = getIndexDataFromProviders(providers, isFullIndex); - - final long updateDatabaseStartTime = System.currentTimeMillis(); - updateDatabase(indexData, isFullIndex); - if (SettingsSearchIndexablesProvider.DEBUG) { - final long updateDatabaseTime = System.currentTimeMillis() - updateDatabaseStartTime; - Log.d(LOG_TAG, "performIndexing updateDatabase took time: " + updateDatabaseTime); - } - - //TODO(63922686): Setting indexed should be a single method, not 3 separate setters. - IndexDatabaseHelper.setLocaleIndexed(mContext, localeStr); - IndexDatabaseHelper.setBuildIndexed(mContext, fingerprint); - IndexDatabaseHelper.setProvidersIndexed(mContext, providerVersionedNames); - - if (SettingsSearchIndexablesProvider.DEBUG) { - final long indexingTime = System.currentTimeMillis() - startTime; - Log.d(LOG_TAG, "performIndexing took time: " + indexingTime - + "ms. Full index? " + isFullIndex); - } - } - - @VisibleForTesting - PreIndexData getIndexDataFromProviders(List<ResolveInfo> providers, boolean isFullIndex) { - if (mCollector == null) { - mCollector = new PreIndexDataCollector(mContext); - } - return mCollector.collectIndexableData(providers, isFullIndex); - } - - /** - * Checks if the indexed data is obsolete, when either: - * - Device language has changed - * - Device has taken an OTA. - * In both cases, the device requires a full index. - * - * @param locale is the default for the device - * @param fingerprint id for the current build. - * @return true if a full index should be preformed. - */ - @VisibleForTesting - boolean isFullIndex(Context context, String locale, String fingerprint, - String providerVersionedNames) { - final boolean isLocaleIndexed = IndexDatabaseHelper.isLocaleAlreadyIndexed(context, locale); - final boolean isBuildIndexed = IndexDatabaseHelper.isBuildIndexed(context, fingerprint); - final boolean areProvidersIndexed = IndexDatabaseHelper - .areProvidersIndexed(context, providerVersionedNames); - - return !(isLocaleIndexed && isBuildIndexed && areProvidersIndexed); - } - - /** - * Drop the currently stored database, and clear the flags which mark the database as indexed. - */ - private void rebuildDatabase() { - // Drop the database when the locale or build has changed. This eliminates rows which are - // dynamically inserted in the old language, or deprecated settings. - final SQLiteDatabase db = getWritableDatabase(); - IndexDatabaseHelper.getInstance(mContext).reconstruct(db); - } - - /** - * Adds new data to the database and verifies the correctness of the ENABLED column. - * First, the data to be updated and all non-indexable keys are copied locally. - * Then all new data to be added is inserted. - * Then search results are verified to have the correct value of enabled. - * Finally, we record that the locale has been indexed. - * - * @param needsReindexing true the database needs to be rebuilt. - */ - @VisibleForTesting - void updateDatabase(PreIndexData preIndexData, boolean needsReindexing) { - final Map<String, Set<String>> nonIndexableKeys = preIndexData.nonIndexableKeys; - - final SQLiteDatabase database = getWritableDatabase(); - if (database == null) { - Log.w(LOG_TAG, "Cannot indexDatabase Index as I cannot get a writable database"); - return; - } - - try { - database.beginTransaction(); - - // Convert all Pre-index data to Index data. - List<IndexData> indexData = getIndexData(preIndexData); - insertIndexData(database, indexData); - - // Only check for non-indexable key updates after initial index. - // Enabled state with non-indexable keys is checked when items are first inserted. - if (!needsReindexing) { - updateDataInDatabase(database, nonIndexableKeys); - } - - database.setTransactionSuccessful(); - } finally { - database.endTransaction(); - } - } - - @VisibleForTesting - List<IndexData> getIndexData(PreIndexData data) { - if (mConverter == null) { - mConverter = new IndexDataConverter(mContext); - } - return mConverter.convertPreIndexDataToIndexData(data); - } - - /** - * Inserts all of the entries in {@param indexData} into the {@param database} - * as Search Data and as part of the Information Hierarchy. - */ - @VisibleForTesting - void insertIndexData(SQLiteDatabase database, List<IndexData> indexData) { - ContentValues values; - - for (IndexData dataRow : indexData) { - if (TextUtils.isEmpty(dataRow.normalizedTitle)) { - continue; - } - - values = new ContentValues(); - values.put(IndexDatabaseHelper.IndexColumns.DOCID, dataRow.getDocId()); - values.put(LOCALE, dataRow.locale); - values.put(DATA_TITLE, dataRow.updatedTitle); - values.put(DATA_TITLE_NORMALIZED, dataRow.normalizedTitle); - values.put(DATA_SUMMARY_ON, dataRow.updatedSummaryOn); - values.put(DATA_SUMMARY_ON_NORMALIZED, dataRow.normalizedSummaryOn); - values.put(DATA_ENTRIES, dataRow.entries); - values.put(DATA_KEYWORDS, dataRow.spaceDelimitedKeywords); - values.put(CLASS_NAME, dataRow.className); - values.put(SCREEN_TITLE, dataRow.screenTitle); - values.put(INTENT_ACTION, dataRow.intentAction); - values.put(INTENT_TARGET_PACKAGE, dataRow.intentTargetPackage); - values.put(INTENT_TARGET_CLASS, dataRow.intentTargetClass); - values.put(ICON, dataRow.iconResId); - values.put(ENABLED, dataRow.enabled); - values.put(DATA_KEY_REF, dataRow.key); - values.put(USER_ID, dataRow.userId); - values.put(PAYLOAD_TYPE, dataRow.payloadType); - values.put(PAYLOAD, dataRow.payload); - - database.replaceOrThrow(TABLE_PREFS_INDEX, null, values); - - if (!TextUtils.isEmpty(dataRow.className) - && !TextUtils.isEmpty(dataRow.childClassName)) { - final ContentValues siteMapPair = new ContentValues(); - siteMapPair.put(SiteMapColumns.PARENT_CLASS, dataRow.className); - siteMapPair.put(SiteMapColumns.PARENT_TITLE, dataRow.screenTitle); - siteMapPair.put(SiteMapColumns.CHILD_CLASS, dataRow.childClassName); - siteMapPair.put(SiteMapColumns.CHILD_TITLE, dataRow.updatedTitle); - - database.replaceOrThrow(IndexDatabaseHelper.Tables.TABLE_SITE_MAP, - null /* nullColumnHack */, siteMapPair); - } - } - } - - /** - * Upholds the validity of enabled data for the user. - * All rows which are enabled but are now flagged with non-indexable keys will become disabled. - * All rows which are disabled but no longer a non-indexable key will become enabled. - * - * @param database The database to validate. - * @param nonIndexableKeys A map between package name and the set of non-indexable keys for it. - */ - @VisibleForTesting - void updateDataInDatabase(SQLiteDatabase database, - Map<String, Set<String>> nonIndexableKeys) { - final String whereEnabled = ENABLED + " = 1"; - final String whereDisabled = ENABLED + " = 0"; - - final Cursor enabledResults = database.query(TABLE_PREFS_INDEX, SELECT_COLUMNS, - whereEnabled, null, null, null, null); - - final ContentValues enabledToDisabledValue = new ContentValues(); - enabledToDisabledValue.put(ENABLED, 0); - - String packageName; - // TODO Refactor: Move these two loops into one method. - while (enabledResults.moveToNext()) { - // Package name is the key for remote providers. - // If package name is null, the provider is Settings. - packageName = enabledResults.getString(COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE); - if (packageName == null) { - packageName = mContext.getPackageName(); - } - - final String key = enabledResults.getString(COLUMN_INDEX_KEY); - final Set<String> packageKeys = nonIndexableKeys.get(packageName); - - // The indexed item is set to Enabled but is now non-indexable - if (packageKeys != null && packageKeys.contains(key)) { - final String whereClause = DOCID + " = " + enabledResults.getInt(COLUMN_INDEX_ID); - database.update(TABLE_PREFS_INDEX, enabledToDisabledValue, whereClause, null); - } - } - enabledResults.close(); - - final Cursor disabledResults = database.query(TABLE_PREFS_INDEX, SELECT_COLUMNS, - whereDisabled, null, null, null, null); - - final ContentValues disabledToEnabledValue = new ContentValues(); - disabledToEnabledValue.put(ENABLED, 1); - - while (disabledResults.moveToNext()) { - // Package name is the key for remote providers. - // If package name is null, the provider is Settings. - packageName = disabledResults.getString(COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE); - if (packageName == null) { - packageName = mContext.getPackageName(); - } - - final String key = disabledResults.getString(COLUMN_INDEX_KEY); - final Set<String> packageKeys = nonIndexableKeys.get(packageName); - - // The indexed item is set to Disabled but is no longer non-indexable. - // We do not enable keys when packageKeys is null because it means the keys came - // from an unrecognized package and therefore should not be surfaced as results. - if (packageKeys != null && !packageKeys.contains(key)) { - String whereClause = DOCID + " = " + disabledResults.getInt(COLUMN_INDEX_ID); - database.update(TABLE_PREFS_INDEX, disabledToEnabledValue, whereClause, null); - } - } - disabledResults.close(); - } - - private SQLiteDatabase getWritableDatabase() { - try { - return IndexDatabaseHelper.getInstance(mContext).getWritableDatabase(); - } catch (SQLiteException e) { - Log.e(LOG_TAG, "Cannot open writable database", e); - return null; - } - } -}
\ No newline at end of file diff --git a/src/com/android/settings/search/DatabaseIndexingUtils.java b/src/com/android/settings/search/DatabaseIndexingUtils.java index eaf69b7623..e71db9dbb2 100644 --- a/src/com/android/settings/search/DatabaseIndexingUtils.java +++ b/src/com/android/settings/search/DatabaseIndexingUtils.java @@ -17,23 +17,9 @@ package com.android.settings.search; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.util.ArrayMap; import android.util.Log; -import com.android.internal.logging.nano.MetricsProto; -import com.android.settings.SettingsActivity; -import com.android.settings.core.BasePreferenceController; -import com.android.settings.core.PreferenceControllerMixin; -import com.android.settings.core.SubSettingLauncher; -import com.android.settingslib.core.AbstractPreferenceController; - import java.lang.reflect.Field; -import java.util.List; -import java.util.Map; /** * Utility class for {@like DatabaseIndexingManager} to handle the mapping between Payloads @@ -46,96 +32,6 @@ public class DatabaseIndexingUtils { public static final String FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER = "SEARCH_INDEX_DATA_PROVIDER"; - /** - * Builds intent that launches the search destination as a sub-setting. - */ - public static Intent buildSearchResultPageIntent(Context context, String className, String key, - String screenTitle) { - return buildSearchResultPageIntent(context, className, key, screenTitle, - MetricsProto.MetricsEvent.DASHBOARD_SEARCH_RESULTS); - } - - public static Intent buildSearchResultPageIntent(Context context, String className, String key, - String screenTitle, int sourceMetricsCategory) { - final Bundle args = new Bundle(); - args.putString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, key); - final Intent searchDestination = new SubSettingLauncher(context) - .setDestination(className) - .setArguments(args) - .setTitle(screenTitle) - .setSourceMetricsCategory(sourceMetricsCategory) - .toIntent(); - searchDestination.putExtra(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, key) - .setAction("com.android.settings.SEARCH_RESULT_TRAMPOLINE") - .setComponent(null); - return searchDestination; - } - - /** - * @param className which wil provide the map between from {@link Uri}s to - * {@link PreferenceControllerMixin} - * @return A map between {@link Uri}s and {@link PreferenceControllerMixin}s to get the payload - * types for Settings. - */ - public static Map<String, ResultPayload> getPayloadKeyMap(String className, Context context) { - ArrayMap<String, ResultPayload> map = new ArrayMap<>(); - if (context == null) { - return map; - } - - final Class<?> clazz = getIndexableClass(className); - - if (clazz == null) { - Log.d(TAG, "SearchIndexableResource '" + className + - "' should implement the " + Indexable.class.getName() + " interface!"); - return map; - } - - // Will be non null only for a Local provider implementing a - // SEARCH_INDEX_DATA_PROVIDER field - final Indexable.SearchIndexProvider provider = getSearchIndexProvider(clazz); - - final List<AbstractPreferenceController> controllers = - provider.getPreferenceControllers(context); - - if (controllers == null) { - return map; - } - - for (AbstractPreferenceController controller : controllers) { - ResultPayload payload; - if (controller instanceof PreferenceControllerMixin) { - payload = ((PreferenceControllerMixin) controller).getResultPayload(); - - } else if (controller instanceof BasePreferenceController) { - payload = ((BasePreferenceController) controller).getResultPayload(); - } else { - throw new IllegalStateException(controller.getClass().getName() - + " must implement " + PreferenceControllerMixin.class.getName()); - } - if (payload != null) { - map.put(controller.getPreferenceKey(), payload); - } - } - - return map; - } - - public static Class<?> getIndexableClass(String className) { - final Class<?> clazz; - try { - clazz = Class.forName(className); - } catch (ClassNotFoundException e) { - Log.d(TAG, "Cannot find class: " + className); - return null; - } - return isIndexableClass(clazz) ? clazz : null; - } - - public static boolean isIndexableClass(final Class<?> clazz) { - return (clazz != null) && Indexable.class.isAssignableFrom(clazz); - } - public static Indexable.SearchIndexProvider getSearchIndexProvider(final Class<?> clazz) { try { final Field f = clazz.getField(FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER); diff --git a/src/com/android/settings/search/DatabaseResultLoader.java b/src/com/android/settings/search/DatabaseResultLoader.java deleted file mode 100644 index 7902eef239..0000000000 --- a/src/com/android/settings/search/DatabaseResultLoader.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2017 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.search; - -import static com.android.settings.search.IndexDatabaseHelper.IndexColumns; - -/** - * AsyncTask to retrieve Settings, first party app and any intent based results. - */ -public class DatabaseResultLoader { - - private static final String TAG = "DatabaseResultLoader"; - - public static final String[] SELECT_COLUMNS = { - IndexColumns.DOCID, - IndexColumns.DATA_TITLE, - IndexColumns.DATA_SUMMARY_ON, - IndexColumns.DATA_SUMMARY_OFF, - IndexColumns.CLASS_NAME, - IndexColumns.SCREEN_TITLE, - IndexColumns.ICON, - IndexColumns.INTENT_ACTION, - IndexColumns.INTENT_TARGET_PACKAGE, - IndexColumns.INTENT_TARGET_CLASS, - IndexColumns.DATA_KEY_REF, - IndexColumns.PAYLOAD_TYPE, - IndexColumns.PAYLOAD - }; - - /** - * These indices are used to match the columns of the this loader's SELECT statement. - * These are not necessarily the same order nor similar coverage as the schema defined in - * IndexDatabaseHelper - */ - public static final int COLUMN_INDEX_ID = 0; - public static final int COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE = 8; - public static final int COLUMN_INDEX_KEY = 10; -}
\ No newline at end of file diff --git a/src/com/android/settings/search/DeviceIndexFeatureProvider.java b/src/com/android/settings/search/DeviceIndexFeatureProvider.java deleted file mode 100644 index e6b3e937b8..0000000000 --- a/src/com/android/settings/search/DeviceIndexFeatureProvider.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright (C) 2018 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.search; - -import static com.android.settings.slices.SliceDeepLinkSpringBoard.INTENT; -import static com.android.settings.slices.SliceDeepLinkSpringBoard.SETTINGS; - -import android.app.job.JobInfo; -import android.app.job.JobScheduler; -import android.content.ComponentName; -import android.content.Context; -import android.content.pm.PackageManager; -import android.content.pm.ServiceInfo; -import android.net.Uri; -import android.os.Binder; -import android.os.Build; -import android.provider.Settings; -import android.text.TextUtils; -import android.util.Log; - -import com.android.settings.R; -import com.android.settings.Utils; -import com.android.settings.slices.SettingsSliceProvider; - -import java.util.List; -import java.util.Locale; - -public interface DeviceIndexFeatureProvider { - - String TAG = "DeviceIndex"; - - String INDEX_VERSION = "settings:index_version"; - String INDEX_LANGUAGE = "settings:language"; - - // Increment when new items are added to ensure they get pushed to the device index. - String VERSION = Build.FINGERPRINT; - - // When the device language changes, re-index so Slices trigger in device language. - Locale LANGUAGE = Locale.getDefault(); - - boolean isIndexingEnabled(); - - void index(Context context, CharSequence title, Uri sliceUri, Uri launchUri, - List<String> keywords); - - void clearIndex(Context context); - - default void updateIndex(Context context, boolean force) { - if (!isIndexingEnabled()) { - Log.i(TAG, "Skipping: device index is not enabled"); - return; - } - - if (!Utils.isDeviceProvisioned(context)) { - Log.w(TAG, "Skipping: device is not provisioned"); - return; - } - - final ComponentName jobComponent = new ComponentName(context.getPackageName(), - DeviceIndexUpdateJobService.class.getName()); - - try { - final int callerUid = Binder.getCallingUid(); - final ServiceInfo si = context.getPackageManager().getServiceInfo(jobComponent, - PackageManager.MATCH_DIRECT_BOOT_AWARE - | PackageManager.MATCH_DIRECT_BOOT_UNAWARE); - if (si == null) { - Log.w(TAG, "Skipping: No such service " + jobComponent); - return; - } - if (si.applicationInfo.uid != callerUid) { - Log.w(TAG, "Skipping: Uid cannot schedule DeviceIndexUpdate: " + callerUid); - return; - } - } catch (PackageManager.NameNotFoundException e) { - Log.w(TAG, "Skipping: error finding DeviceIndexUpdateJobService from packageManager"); - return; - } - - if (!force && skipIndex(context)) { - Log.i(TAG, "Skipping: already indexed."); - // No need to update. - return; - } - - // Prevent scheduling multiple jobs - setIndexState(context); - - final int jobId = context.getResources().getInteger(R.integer.device_index_update); - // Schedule a job so that we know it'll be able to complete, but try to run as - // soon as possible. - context.getSystemService(JobScheduler.class).schedule( - new JobInfo.Builder(jobId, jobComponent) - .setPersisted(true) - .setMinimumLatency(1000) - .setOverrideDeadline(1) - .build()); - - } - - static Uri createDeepLink(String s) { - return new Uri.Builder().scheme(SETTINGS) - .authority(SettingsSliceProvider.SLICE_AUTHORITY) - .appendQueryParameter(INTENT, s) - .build(); - } - - static boolean skipIndex(Context context) { - final boolean isSameVersion = TextUtils.equals( - Settings.Secure.getString(context.getContentResolver(), INDEX_VERSION), VERSION); - final boolean isSameLanguage = TextUtils.equals( - Settings.Secure.getString(context.getContentResolver(), INDEX_LANGUAGE), - LANGUAGE.toString()); - return isSameLanguage && isSameVersion; - } - - static void setIndexState(Context context) { - Settings.Secure.putString(context.getContentResolver(), INDEX_VERSION, VERSION); - Settings.Secure.putString(context.getContentResolver(), INDEX_LANGUAGE, - LANGUAGE.toString()); - } -} diff --git a/src/com/android/settings/search/DeviceIndexUpdateJobService.java b/src/com/android/settings/search/DeviceIndexUpdateJobService.java deleted file mode 100644 index 3eb904119b..0000000000 --- a/src/com/android/settings/search/DeviceIndexUpdateJobService.java +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright (C) 2018 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.search; - -import static android.app.slice.Slice.HINT_LARGE; -import static android.app.slice.Slice.HINT_TITLE; -import static android.app.slice.SliceItem.FORMAT_TEXT; -import static com.android.settings.search.DeviceIndexFeatureProvider.createDeepLink; - -import android.app.job.JobParameters; -import android.app.job.JobService; -import android.content.ContentResolver; -import android.content.Intent; -import android.net.Uri; -import android.net.Uri.Builder; -import android.provider.SettingsSlicesContract; -import android.util.Log; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.settings.overlay.FeatureFactory; -import com.android.settings.slices.SettingsSliceProvider; -import com.android.settings.slices.SliceDeepLinkSpringBoard; - -import java.util.Collection; -import java.util.concurrent.CountDownLatch; - -import androidx.slice.Slice; -import androidx.slice.SliceItem; -import androidx.slice.SliceViewManager; -import androidx.slice.SliceViewManager.SliceCallback; -import androidx.slice.SliceMetadata; -import androidx.slice.core.SliceQuery; -import androidx.slice.widget.ListContent; - -public class DeviceIndexUpdateJobService extends JobService { - - private static final String TAG = "DeviceIndexUpdate"; - private static final boolean DEBUG = false; - @VisibleForTesting - protected boolean mRunningJob; - - @Override - public boolean onStartJob(JobParameters params) { - if (DEBUG) Log.d(TAG, "onStartJob"); - if (!mRunningJob) { - mRunningJob = true; - Thread thread = new Thread(() -> updateIndex(params)); - thread.setPriority(Thread.MIN_PRIORITY); - thread.start(); - } - return true; - } - - @Override - public boolean onStopJob(JobParameters params) { - if (DEBUG) Log.d(TAG, "onStopJob " + mRunningJob); - if (mRunningJob) { - mRunningJob = false; - return true; - } - return false; - } - - @VisibleForTesting - protected void updateIndex(JobParameters params) { - if (DEBUG) { - Log.d(TAG, "Starting index"); - } - final DeviceIndexFeatureProvider indexProvider = FeatureFactory.getFactory(this) - .getDeviceIndexFeatureProvider(); - final SliceViewManager manager = getSliceViewManager(); - final Uri baseUri = new Builder() - .scheme(ContentResolver.SCHEME_CONTENT) - .authority(SettingsSliceProvider.SLICE_AUTHORITY) - .build(); - final Uri platformBaseUri = new Builder() - .scheme(ContentResolver.SCHEME_CONTENT) - .authority(SettingsSlicesContract.AUTHORITY) - .build(); - final Collection<Uri> slices = manager.getSliceDescendants(baseUri); - slices.addAll(manager.getSliceDescendants(platformBaseUri)); - - if (DEBUG) { - Log.d(TAG, "Indexing " + slices.size() + " slices"); - } - - indexProvider.clearIndex(this /* context */); - - for (Uri slice : slices) { - if (!mRunningJob) { - return; - } - Slice loadedSlice = bindSliceSynchronous(manager, slice); - // TODO: Get Title APIs on SliceMetadata and use that. - SliceMetadata metaData = getMetadata(loadedSlice); - CharSequence title = findTitle(loadedSlice, metaData); - if (title != null) { - if (DEBUG) { - Log.d(TAG, "Indexing: " + slice + " " + title + " " + loadedSlice); - } - indexProvider.index(this, title, slice, createDeepLink( - new Intent(SliceDeepLinkSpringBoard.ACTION_VIEW_SLICE) - .setPackage(getPackageName()) - .putExtra(SliceDeepLinkSpringBoard.EXTRA_SLICE, slice.toString()) - .toUri(Intent.URI_ANDROID_APP_SCHEME)), - metaData.getSliceKeywords()); - } - } - if (DEBUG) { - Log.d(TAG, "Done indexing"); - } - jobFinished(params, false); - } - - protected SliceViewManager getSliceViewManager() { - return SliceViewManager.getInstance(this); - } - - protected SliceMetadata getMetadata(Slice loadedSlice) { - return SliceMetadata.from(this, loadedSlice); - } - - protected CharSequence findTitle(Slice loadedSlice, SliceMetadata metaData) { - ListContent content = new ListContent(null, loadedSlice); - SliceItem headerItem = content.getHeaderItem(); - if (headerItem == null) { - if (content.getRowItems().size() != 0) { - headerItem = content.getRowItems().get(0); - } else { - return null; - } - } - // Look for a title, then large text, then any text at all. - SliceItem title = SliceQuery.find(headerItem, FORMAT_TEXT, HINT_TITLE, null); - if (title != null) { - return title.getText(); - } - title = SliceQuery.find(headerItem, FORMAT_TEXT, HINT_LARGE, null); - if (title != null) { - return title.getText(); - } - title = SliceQuery.find(headerItem, FORMAT_TEXT); - if (title != null) { - return title.getText(); - } - return null; - } - - protected Slice bindSliceSynchronous(SliceViewManager manager, Uri slice) { - final Slice[] returnSlice = new Slice[1]; - CountDownLatch latch = new CountDownLatch(1); - SliceCallback callback = new SliceCallback() { - @Override - public void onSliceUpdated(Slice s) { - try { - SliceMetadata m = SliceMetadata.from(DeviceIndexUpdateJobService.this, s); - if (m.getLoadingState() == SliceMetadata.LOADED_ALL) { - returnSlice[0] = s; - latch.countDown(); - manager.unregisterSliceCallback(slice, this); - } - } catch (Exception e) { - Log.w(TAG, slice + " cannot be indexed", e); - returnSlice[0] = s; - } - } - }; - // Register a callback until we get a loaded slice. - manager.registerSliceCallback(slice, callback); - // Trigger the first bind in case no loading is needed. - callback.onSliceUpdated(manager.bindSlice(slice)); - try { - latch.await(); - } catch (InterruptedException e) { - } - return returnSlice[0]; - } -} diff --git a/src/com/android/settings/search/IndexDatabaseHelper.java b/src/com/android/settings/search/IndexDatabaseHelper.java deleted file mode 100644 index 7c87bf74f6..0000000000 --- a/src/com/android/settings/search/IndexDatabaseHelper.java +++ /dev/null @@ -1,306 +0,0 @@ -/* - * 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.search; - -import android.content.Context; -import android.content.pm.ResolveInfo; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; -import android.os.Build; -import android.provider.SearchIndexablesContract.SiteMapColumns; -import androidx.annotation.VisibleForTesting; -import android.text.TextUtils; -import android.util.Log; - -import java.util.List; - -public class IndexDatabaseHelper extends SQLiteOpenHelper { - - private static final String TAG = "IndexDatabaseHelper"; - - private static final String DATABASE_NAME = "search_index.db"; - private static final int DATABASE_VERSION = 118; - - private static final String SHARED_PREFS_TAG = "indexing_manager"; - - private static final String PREF_KEY_INDEXED_PROVIDERS = "indexed_providers"; - - public interface Tables { - String TABLE_PREFS_INDEX = "prefs_index"; - String TABLE_SITE_MAP = "site_map"; - String TABLE_META_INDEX = "meta_index"; - String TABLE_SAVED_QUERIES = "saved_queries"; - } - - public interface IndexColumns { - String DOCID = "docid"; - String LOCALE = "locale"; - String DATA_RANK = "data_rank"; - String DATA_TITLE = "data_title"; - String DATA_TITLE_NORMALIZED = "data_title_normalized"; - String DATA_SUMMARY_ON = "data_summary_on"; - String DATA_SUMMARY_ON_NORMALIZED = "data_summary_on_normalized"; - String DATA_SUMMARY_OFF = "data_summary_off"; - String DATA_SUMMARY_OFF_NORMALIZED = "data_summary_off_normalized"; - String DATA_ENTRIES = "data_entries"; - String DATA_KEYWORDS = "data_keywords"; - String CLASS_NAME = "class_name"; - String SCREEN_TITLE = "screen_title"; - String INTENT_ACTION = "intent_action"; - String INTENT_TARGET_PACKAGE = "intent_target_package"; - String INTENT_TARGET_CLASS = "intent_target_class"; - String ICON = "icon"; - String ENABLED = "enabled"; - String DATA_KEY_REF = "data_key_reference"; - String USER_ID = "user_id"; - String PAYLOAD_TYPE = "payload_type"; - String PAYLOAD = "payload"; - } - - public interface MetaColumns { - String BUILD = "build"; - } - - public interface SavedQueriesColumns { - String QUERY = "query"; - String TIME_STAMP = "timestamp"; - } - - private static final String CREATE_INDEX_TABLE = - "CREATE VIRTUAL TABLE " + Tables.TABLE_PREFS_INDEX + " USING fts4" + - "(" + - IndexColumns.LOCALE + - ", " + - IndexColumns.DATA_RANK + - ", " + - IndexColumns.DATA_TITLE + - ", " + - IndexColumns.DATA_TITLE_NORMALIZED + - ", " + - IndexColumns.DATA_SUMMARY_ON + - ", " + - IndexColumns.DATA_SUMMARY_ON_NORMALIZED + - ", " + - IndexColumns.DATA_SUMMARY_OFF + - ", " + - IndexColumns.DATA_SUMMARY_OFF_NORMALIZED + - ", " + - IndexColumns.DATA_ENTRIES + - ", " + - IndexColumns.DATA_KEYWORDS + - ", " + - IndexColumns.SCREEN_TITLE + - ", " + - IndexColumns.CLASS_NAME + - ", " + - IndexColumns.ICON + - ", " + - IndexColumns.INTENT_ACTION + - ", " + - IndexColumns.INTENT_TARGET_PACKAGE + - ", " + - IndexColumns.INTENT_TARGET_CLASS + - ", " + - IndexColumns.ENABLED + - ", " + - IndexColumns.DATA_KEY_REF + - ", " + - IndexColumns.USER_ID + - ", " + - IndexColumns.PAYLOAD_TYPE + - ", " + - IndexColumns.PAYLOAD + - ");"; - - private static final String CREATE_META_TABLE = - "CREATE TABLE " + Tables.TABLE_META_INDEX + - "(" + - MetaColumns.BUILD + " VARCHAR(32) NOT NULL" + - ")"; - - private static final String CREATE_SAVED_QUERIES_TABLE = - "CREATE TABLE " + Tables.TABLE_SAVED_QUERIES + - "(" + - SavedQueriesColumns.QUERY + " VARCHAR(64) NOT NULL" + - ", " + - SavedQueriesColumns.TIME_STAMP + " INTEGER" + - ")"; - - private static final String CREATE_SITE_MAP_TABLE = - "CREATE VIRTUAL TABLE " + Tables.TABLE_SITE_MAP + " USING fts4" + - "(" + - SiteMapColumns.PARENT_CLASS + - ", " + - SiteMapColumns.CHILD_CLASS + - ", " + - SiteMapColumns.PARENT_TITLE + - ", " + - SiteMapColumns.CHILD_TITLE + - ")"; - private static final String INSERT_BUILD_VERSION = - "INSERT INTO " + Tables.TABLE_META_INDEX + - " VALUES ('" + Build.VERSION.INCREMENTAL + "');"; - - private static final String SELECT_BUILD_VERSION = - "SELECT " + MetaColumns.BUILD + " FROM " + Tables.TABLE_META_INDEX + " LIMIT 1;"; - - private static IndexDatabaseHelper sSingleton; - - private final Context mContext; - - public static synchronized IndexDatabaseHelper getInstance(Context context) { - if (sSingleton == null) { - sSingleton = new IndexDatabaseHelper(context); - } - return sSingleton; - } - - public IndexDatabaseHelper(Context context) { - super(context, DATABASE_NAME, null, DATABASE_VERSION); - mContext = context.getApplicationContext(); - } - - @Override - public void onCreate(SQLiteDatabase db) { - bootstrapDB(db); - } - - private void bootstrapDB(SQLiteDatabase db) { - db.execSQL(CREATE_INDEX_TABLE); - db.execSQL(CREATE_META_TABLE); - db.execSQL(CREATE_SAVED_QUERIES_TABLE); - db.execSQL(CREATE_SITE_MAP_TABLE); - db.execSQL(INSERT_BUILD_VERSION); - Log.i(TAG, "Bootstrapped database"); - } - - @Override - public void onOpen(SQLiteDatabase db) { - super.onOpen(db); - - Log.i(TAG, "Using schema version: " + db.getVersion()); - - if (!Build.VERSION.INCREMENTAL.equals(getBuildVersion(db))) { - Log.w(TAG, "Index needs to be rebuilt as build-version is not the same"); - // We need to drop the tables and recreate them - reconstruct(db); - } else { - Log.i(TAG, "Index is fine"); - } - } - - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - if (oldVersion < DATABASE_VERSION) { - Log.w(TAG, "Detected schema version '" + oldVersion + "'. " + - "Index needs to be rebuilt for schema version '" + newVersion + "'."); - // We need to drop the tables and recreate them - reconstruct(db); - } - } - - @Override - public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { - Log.w(TAG, "Detected schema version '" + oldVersion + "'. " + - "Index needs to be rebuilt for schema version '" + newVersion + "'."); - // We need to drop the tables and recreate them - reconstruct(db); - } - - public void reconstruct(SQLiteDatabase db) { - mContext.getSharedPreferences(SHARED_PREFS_TAG, Context.MODE_PRIVATE) - .edit() - .clear() - .commit(); - dropTables(db); - bootstrapDB(db); - } - - private String getBuildVersion(SQLiteDatabase db) { - String version = null; - Cursor cursor = null; - try { - cursor = db.rawQuery(SELECT_BUILD_VERSION, null); - if (cursor.moveToFirst()) { - version = cursor.getString(0); - } - } catch (Exception e) { - Log.e(TAG, "Cannot get build version from Index metadata"); - } finally { - if (cursor != null) { - cursor.close(); - } - } - return version; - } - - @VisibleForTesting - static String buildProviderVersionedNames(List<ResolveInfo> providers) { - StringBuilder sb = new StringBuilder(); - for (ResolveInfo info : providers) { - sb.append(info.providerInfo.packageName) - .append(':') - .append(info.providerInfo.applicationInfo.longVersionCode) - .append(','); - } - return sb.toString(); - } - - static void setLocaleIndexed(Context context, String locale) { - context.getSharedPreferences(SHARED_PREFS_TAG, Context.MODE_PRIVATE) - .edit() - .putBoolean(locale, true) - .apply(); - } - - static void setProvidersIndexed(Context context, String providerVersionedNames) { - context.getSharedPreferences(SHARED_PREFS_TAG, Context.MODE_PRIVATE) - .edit() - .putString(PREF_KEY_INDEXED_PROVIDERS, providerVersionedNames) - .apply(); - } - - static boolean isLocaleAlreadyIndexed(Context context, String locale) { - return context.getSharedPreferences(SHARED_PREFS_TAG, Context.MODE_PRIVATE) - .getBoolean(locale, false); - } - - static boolean areProvidersIndexed(Context context, String providerVersionedNames) { - final String indexedProviders = - context.getSharedPreferences(SHARED_PREFS_TAG, Context.MODE_PRIVATE) - .getString(PREF_KEY_INDEXED_PROVIDERS, null); - return TextUtils.equals(indexedProviders, providerVersionedNames); - } - - static boolean isBuildIndexed(Context context, String buildNo) { - return context.getSharedPreferences(SHARED_PREFS_TAG, - Context.MODE_PRIVATE).getBoolean(buildNo, false); - } - - static void setBuildIndexed(Context context, String buildNo) { - // Use #apply() instead of #commit() since #commit() Robolectric loop indefinitely in sdk 26 - context.getSharedPreferences(SHARED_PREFS_TAG, 0).edit().putBoolean(buildNo, true).apply(); - } - - private void dropTables(SQLiteDatabase db) { - db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_META_INDEX); - db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_PREFS_INDEX); - db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_SAVED_QUERIES); - db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_SITE_MAP); - } -} diff --git a/src/com/android/settings/search/Indexable.java b/src/com/android/settings/search/Indexable.java index e157fac74a..eef7184762 100644 --- a/src/com/android/settings/search/Indexable.java +++ b/src/com/android/settings/search/Indexable.java @@ -19,6 +19,8 @@ package com.android.settings.search; import android.content.Context; import android.provider.SearchIndexableResource; +import androidx.annotation.Keep; + import com.android.settingslib.core.AbstractPreferenceController; import java.util.List; @@ -46,6 +48,7 @@ public interface Indexable { * @return a list of {@link android.provider.SearchIndexableResource} references. * Can be null. */ + @Keep List<SearchIndexableResource> getXmlResourcesToIndex(Context context, boolean enabled); /** @@ -56,6 +59,7 @@ public interface Indexable { * or not. * @return a list of {@link SearchIndexableRaw} references. Can be null. */ + @Keep List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled); /** @@ -64,12 +68,14 @@ public interface Indexable { * @param context the context. * @return a list of {@link SearchIndexableRaw} references. Can be null. */ + @Keep List<String> getNonIndexableKeys(Context context); /** * @return a list of {@link AbstractPreferenceController} for ResultPayload data during * Indexing. */ + @Keep List<AbstractPreferenceController> getPreferenceControllers(Context context); } } diff --git a/src/com/android/settings/search/InlineListPayload.java b/src/com/android/settings/search/InlineListPayload.java deleted file mode 100644 index c11c4a388f..0000000000 --- a/src/com/android/settings/search/InlineListPayload.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.android.settings.search; - -import android.content.Intent; -import android.os.Parcel; -import android.os.Parcelable; - -/** - * Payload for settings which are selected from multiple values. For example, Location can be - * set to multiple degrees of accuracy. - */ -public class InlineListPayload extends InlinePayload { - - /** - * Number of selections in the list. - */ - private int mNumOptions; - - public InlineListPayload(String key, @PayloadType int payloadType, Intent intent, - boolean isDeviceSupported, int numOptions, int defaultValue) { - super(key, payloadType, intent, isDeviceSupported, defaultValue); - mNumOptions = numOptions; - } - - private InlineListPayload(Parcel in) { - super(in); - mNumOptions = in.readInt(); - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - super.writeToParcel(dest, flags); - dest.writeInt(mNumOptions); - } - - @Override - protected int standardizeInput(int input) throws IllegalArgumentException { - if (input < 0 || input >= mNumOptions) { - throw new IllegalArgumentException( - "Invalid argument for ListSelect. Expected between 0 and " - + mNumOptions + " but found: " + input); - } - return input; - } - - @Override - @PayloadType public int getType() { - return PayloadType.INLINE_LIST; - } - - public static final Parcelable.Creator<InlineListPayload> CREATOR = - new Parcelable.Creator<InlineListPayload>() { - @Override - public InlineListPayload createFromParcel(Parcel in) { - return new InlineListPayload(in); - } - - @Override - public InlineListPayload[] newArray(int size) { - return new InlineListPayload[size]; - } - }; -} diff --git a/src/com/android/settings/search/InlinePayload.java b/src/com/android/settings/search/InlinePayload.java deleted file mode 100644 index 1cb694a7cb..0000000000 --- a/src/com/android/settings/search/InlinePayload.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright (C) 2017 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.search; - -import android.content.Intent; - -import android.content.Context; -import android.os.Parcel; -import android.provider.Settings; -import com.android.internal.annotations.VisibleForTesting; - -/** - * Abstract Payload for inline settings results. - */ -public abstract class InlinePayload extends ResultPayload { - - public static final int FALSE = 0; - public static final int TRUE = 1; - - /** - * Defines the key to access and store the Setting the inline result represents. - */ - private final String mSettingKey; - - /** - * Defines where the Setting is stored. - */ - @SettingsSource final int mSettingSource; - - /** - * True when the setting is available for the device. - */ - final boolean mIsDeviceSupported; - - /** - * The default value for the setting. - */ - final int mDefaultvalue; - - /** - * @param key uniquely identifies the stored setting. - * @param source of the setting. Used to determine where to get and set the setting. - * @param intent to the setting page. - * @param isDeviceSupported is true when the setting is valid for the given device. - */ - public InlinePayload(String key, @SettingsSource int source, Intent intent, - boolean isDeviceSupported, int defaultValue) { - super(intent); - mSettingKey = key; - mSettingSource = source; - mIsDeviceSupported = isDeviceSupported; - mDefaultvalue = defaultValue; - } - - InlinePayload(Parcel parcel) { - super(parcel.readParcelable(Intent.class.getClassLoader())); - mSettingKey = parcel.readString(); - mSettingSource = parcel.readInt(); - mIsDeviceSupported = parcel.readInt() == TRUE; - mDefaultvalue = parcel.readInt(); - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - super.writeToParcel(dest, flags); - dest.writeString(mSettingKey); - dest.writeInt(mSettingSource); - dest.writeInt(mIsDeviceSupported ? TRUE : FALSE); - dest.writeInt(mDefaultvalue); - } - - @Override - @PayloadType public abstract int getType(); - - /** - * @returns the status of the underlying setting. See {@link ResultPayload.Availability} for - * possible values. - */ - @Availability public int getAvailability() { - if (mIsDeviceSupported) { - return Availability.AVAILABLE; - } - return Availability.DISABLED_UNSUPPORTED; - } - - /** - * Checks if the input is valid for the given setting. - * - * @param input The number to be get or set for the setting. - * @return {@param input} mapped to the public-facing API for settings. - * @throws IllegalArgumentException when the input is not valid for the given inline type. - */ - protected abstract int standardizeInput(int input) throws IllegalArgumentException; - - /** - * @returns the current value of the setting. - */ - public int getValue(Context context) { - int settingsValue = -1; - switch(mSettingSource) { - case SettingsSource.SECURE: - settingsValue = Settings.Secure.getInt(context.getContentResolver(), - mSettingKey, mDefaultvalue); - break; - case SettingsSource.SYSTEM: - settingsValue = Settings.System.getInt(context.getContentResolver(), - mSettingKey, mDefaultvalue); - break; - - case SettingsSource.GLOBAL: - settingsValue = Settings.Global.getInt(context.getContentResolver(), - mSettingKey, mDefaultvalue); - break; - } - - return standardizeInput(settingsValue); - } - - /** - * Attempts to set the setting value. - * - * @param newValue is the requested value for the setting. - * @returns true when the setting was changed, and false otherwise. - */ - public boolean setValue(Context context, int newValue) { - newValue = standardizeInput(newValue); - - switch(mSettingSource) { - case SettingsSource.GLOBAL: - return Settings.Global.putInt(context.getContentResolver(), mSettingKey, newValue); - case SettingsSource.SECURE: - return Settings.Secure.putInt(context.getContentResolver(), mSettingKey, newValue); - case SettingsSource.SYSTEM: - return Settings.System.putInt(context.getContentResolver(), mSettingKey, newValue); - case SettingsSource.UNKNOWN: - return false; - } - - return false; - } - - public String getKey() { - return mSettingKey; - } -}
\ No newline at end of file diff --git a/src/com/android/settings/search/InlineSwitchPayload.java b/src/com/android/settings/search/InlineSwitchPayload.java deleted file mode 100644 index bac0313171..0000000000 --- a/src/com/android/settings/search/InlineSwitchPayload.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (C) 2017 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.search; - -import android.content.Intent; -import android.os.Parcel; -import android.os.Parcelable; - -/** - * Payload for inline Switch results. Mappings from integer to boolean. - */ -public class InlineSwitchPayload extends InlinePayload { - - private static final int ON = 1; - private static final int OFF = 0; - - /** - * Provides a mapping for how switches are stored. - * If mIsStandard is true, then (0 == false) and (1 == true) - * If mIsStandard is false, then (1 == false) and (0 == true) - */ - private boolean mIsStandard; - - /** - * - * @param key uniquely identifies the stored setting. - * @param source of the setting. Used to determine where to get and set the setting. - * @param onValue is the value stored as on for the switch. Should be 0 or 1. - * @param intent to the setting page. - * @param isDeviceSupported is true when the setting is valid for the given device. - */ - public InlineSwitchPayload(String key, @SettingsSource int source, - int onValue, Intent intent, boolean isDeviceSupported, int defaultValue) { - super(key, source, intent, isDeviceSupported, defaultValue); - // If on is stored as TRUE then the switch is standard. - mIsStandard = onValue == TRUE; - } - - private InlineSwitchPayload(Parcel in) { - super(in); - mIsStandard = in.readInt() == TRUE; - } - - @Override - @PayloadType public int getType() { - return PayloadType.INLINE_SWITCH; - } - - @Override - protected int standardizeInput(int value) { - if (value != OFF && value != ON) { - throw new IllegalArgumentException("Invalid input for InlineSwitch. Expected: " - + ON + " or " + OFF - + " but found: " + value); - } - return mIsStandard - ? value - : 1 - value; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - super.writeToParcel(dest, flags); - dest.writeInt(mIsStandard ? TRUE : FALSE); - } - - public static final Parcelable.Creator<InlineSwitchPayload> CREATOR = - new Parcelable.Creator<InlineSwitchPayload>() { - @Override - public InlineSwitchPayload createFromParcel(Parcel in) { - return new InlineSwitchPayload(in); - } - - @Override - public InlineSwitchPayload[] newArray(int size) { - return new InlineSwitchPayload[size]; - } - }; - - public boolean isStandard() { - return mIsStandard; - } -} diff --git a/src/com/android/settings/search/ResultPayload.java b/src/com/android/settings/search/ResultPayload.java deleted file mode 100644 index 6108569aee..0000000000 --- a/src/com/android/settings/search/ResultPayload.java +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Copyright (C) 2017 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.search; - -import android.annotation.IntDef; -import android.content.Intent; -import android.os.Parcel; -import android.os.Parcelable; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -/** - * A interface for search results types. Examples include Inline results, third party apps - * or any future possibilities. - */ -public class ResultPayload implements Parcelable { - protected final Intent mIntent; - - @IntDef({PayloadType.INTENT, PayloadType.INLINE_SLIDER, PayloadType.INLINE_SWITCH, - PayloadType.INLINE_LIST, PayloadType.SAVED_QUERY}) - @Retention(RetentionPolicy.SOURCE) - public @interface PayloadType { - /** - * Resulting page will be started using an mIntent - */ - int INTENT = 0; - - /** - * Result is a inline widget, using a slider widget as UI. - */ - int INLINE_SLIDER = 1; - - /** - * Result is a inline widget, using a toggle widget as UI. - */ - int INLINE_SWITCH = 2; - - /** - * Result is an inline list-select, with an undefined UI. - */ - int INLINE_LIST = 3; - - /** - * Result is a recently saved query. - */ - int SAVED_QUERY = 4; - } - - /** - * Enumerates the possible values for the Availability of a setting. - */ - @IntDef({Availability.AVAILABLE, - Availability.DISABLED_DEPENDENT_SETTING, - Availability.DISABLED_DEPENDENT_APP, - Availability.DISABLED_UNSUPPORTED, - Availability.RESOURCE_CONTENTION, - Availability.INTENT_ONLY, - Availability.DISABLED_FOR_USER,}) - @Retention(RetentionPolicy.SOURCE) - public @interface Availability { - /** - * The setting is available. - */ - int AVAILABLE = 0; - - /** - * The setting has a dependency in settings app which is currently disabled, blocking - * access. - */ - int DISABLED_DEPENDENT_SETTING = 1; - - /** - * The setting is not supported by the device. - */ - int DISABLED_UNSUPPORTED = 2; - - /** - * The setting you are trying to change is being used by another application and cannot - * be changed until it is released by said application. - */ - int RESOURCE_CONTENTION = 3; - - /** - * The setting is disabled because corresponding app is disabled. - */ - int DISABLED_DEPENDENT_APP = 4; - - /** - * This setting is supported on the device but cannot be changed inline. - */ - int INTENT_ONLY = 5; - - /** - * The setting cannot be changed by the current user. - * ex: MobileNetworkTakeMeThereSetting should not be available to a secondary user. - */ - int DISABLED_FOR_USER = 6; - } - - @IntDef({SettingsSource.UNKNOWN, SettingsSource.SYSTEM, SettingsSource.SECURE, - SettingsSource.GLOBAL}) - @Retention(RetentionPolicy.SOURCE) - public @interface SettingsSource { - int UNKNOWN = 0; - int SYSTEM = 1; - int SECURE = 2; - int GLOBAL = 3; - } - - - private ResultPayload(Parcel in) { - mIntent = in.readParcelable(ResultPayload.class.getClassLoader()); - } - - public ResultPayload(Intent intent) { - mIntent = intent; - } - - @ResultPayload.PayloadType - public int getType() { - return PayloadType.INTENT; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeParcelable(mIntent, flags); - } - - public static final Creator<ResultPayload> CREATOR = new Creator<ResultPayload>() { - @Override - public ResultPayload createFromParcel(Parcel in) { - return new ResultPayload(in); - } - - @Override - public ResultPayload[] newArray(int size) { - return new ResultPayload[size]; - } - }; - - public Intent getIntent() { - return mIntent; - } -} diff --git a/src/com/android/settings/search/ResultPayloadUtils.java b/src/com/android/settings/search/ResultPayloadUtils.java deleted file mode 100644 index 2c908c194d..0000000000 --- a/src/com/android/settings/search/ResultPayloadUtils.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (C) 2017 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.search; - -import android.os.Parcel; -import android.os.Parcelable; - -/** - * Utility class to Marshall and Unmarshall the payloads stored in the SQLite Database - */ -public class ResultPayloadUtils { - - private static final String TAG = "PayloadUtil"; - - public static byte[] marshall(ResultPayload payload) { - Parcel parcel = Parcel.obtain(); - payload.writeToParcel(parcel, 0); - byte[] bytes = parcel.marshall(); - parcel.recycle(); - return bytes; - } - - public static <T> T unmarshall(byte[] bytes, Parcelable.Creator<T> creator) { - T result; - Parcel parcel = unmarshall(bytes); - result = creator.createFromParcel(parcel); - parcel.recycle(); - return result; - } - - private static Parcel unmarshall(byte[] bytes) { - Parcel parcel = Parcel.obtain(); - parcel.unmarshall(bytes, 0, bytes.length); - parcel.setDataPosition(0); - return parcel; - } -} diff --git a/src/com/android/settings/search/SearchFeatureProvider.java b/src/com/android/settings/search/SearchFeatureProvider.java index cbe49f8c79..896f6e5c78 100644 --- a/src/com/android/settings/search/SearchFeatureProvider.java +++ b/src/com/android/settings/search/SearchFeatureProvider.java @@ -16,21 +16,30 @@ */ package com.android.settings.search; +import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO; + import android.annotation.NonNull; import android.app.Activity; +import android.app.settings.SettingsEnums; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; +import android.view.View; +import android.view.ViewGroup; import android.widget.Toolbar; +import com.android.settings.R; +import com.android.settings.Utils; import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.search.SearchIndexableResources; /** * FeatureProvider for Settings Search */ public interface SearchFeatureProvider { - Intent SEARCH_UI_INTENT = new Intent("com.android.settings.action.SETTINGS_SEARCH"); + int REQUEST_CODE = 501; /** * Ensures the caller has necessary privilege to launch search result page. @@ -42,36 +51,55 @@ public interface SearchFeatureProvider { throws SecurityException, IllegalArgumentException; /** - * Synchronously updates the Settings database. - */ - void updateIndex(Context context); - - DatabaseIndexingManager getIndexingManager(Context context); - - /** * @return a {@link SearchIndexableResources} to be used for indexing search results. */ SearchIndexableResources getSearchIndexableResources(); - default String getSettingsIntelligencePkgName() { - return "com.android.settings.intelligence"; + default String getSettingsIntelligencePkgName(Context context) { + return context.getString(R.string.config_settingsintelligence_package_name); } /** * Initializes the search toolbar. */ - default void initSearchToolbar(Activity activity, Toolbar toolbar) { + default void initSearchToolbar(Activity activity, Toolbar toolbar, int pageId) { if (activity == null || toolbar == null) { return; } + + if (!Utils.isDeviceProvisioned(activity) || + !Utils.isPackageEnabled(activity, getSettingsIntelligencePkgName(activity))) { + final ViewGroup parent = (ViewGroup) toolbar.getParent(); + if (parent != null) { + parent.setVisibility(View.GONE); + } + return; + } + // Please forgive me for what I am about to do. + // + // Need to make the navigation icon non-clickable so that the entire card is clickable + // and goes to the search UI. Also set the background to null so there's no ripple. + final View navView = toolbar.getNavigationView(); + navView.setClickable(false); + navView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); + navView.setBackground(null); + toolbar.setOnClickListener(tb -> { - final Intent intent = SEARCH_UI_INTENT; - intent.setPackage(getSettingsIntelligencePkgName()); + final Context context = activity.getApplicationContext(); + final Intent intent = buildSearchIntent(context, pageId); - FeatureFactory.getFactory( - activity.getApplicationContext()).getSlicesFeatureProvider() - .indexSliceDataAsync(activity.getApplicationContext()); - activity.startActivityForResult(intent, 0 /* requestCode */); + if (activity.getPackageManager().queryIntentActivities(intent, + PackageManager.MATCH_DEFAULT_ONLY).isEmpty()) { + return; + } + + FeatureFactory.getFactory(context).getSlicesFeatureProvider() + .indexSliceDataAsync(context); + FeatureFactory.getFactory(context).getMetricsFeatureProvider() + .action(context, SettingsEnums.ACTION_SEARCH_RESULTS); + activity.startActivityForResult(intent, REQUEST_CODE); }); } + + Intent buildSearchIntent(Context context, int pageId); } diff --git a/src/com/android/settings/search/SearchFeatureProviderImpl.java b/src/com/android/settings/search/SearchFeatureProviderImpl.java index 78c47edf04..63bf420c02 100644 --- a/src/com/android/settings/search/SearchFeatureProviderImpl.java +++ b/src/com/android/settings/search/SearchFeatureProviderImpl.java @@ -19,13 +19,13 @@ package com.android.settings.search; import android.content.ComponentName; import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.provider.Settings; import android.text.TextUtils; -import com.android.internal.annotations.VisibleForTesting; -import com.android.settings.overlay.FeatureFactory; -import com.android.settings.search.indexing.IndexData; - -import java.util.Locale; +import com.android.settingslib.search.SearchIndexableResources; +import com.android.settingslib.search.SearchIndexableResourcesMobile; /** * FeatureProvider for the refactored search code. @@ -34,8 +34,6 @@ public class SearchFeatureProviderImpl implements SearchFeatureProvider { private static final String TAG = "SearchFeatureProvider"; - private static final String METRICS_ACTION_SETTINGS_INDEX = "search_synchronous_indexing"; - private DatabaseIndexingManager mDatabaseIndexingManager; private SearchIndexableResources mSearchIndexableResources; @Override @@ -46,7 +44,7 @@ public class SearchFeatureProviderImpl implements SearchFeatureProvider { } final String packageName = caller.getPackageName(); final boolean isSettingsPackage = TextUtils.equals(packageName, context.getPackageName()) - || TextUtils.equals(getSettingsIntelligencePkgName(), packageName); + || TextUtils.equals(getSettingsIntelligencePkgName(context), packageName); final boolean isWhitelistedPackage = isSignatureWhitelisted(context, caller.getPackageName()); if (isSettingsPackage || isWhitelistedPackage) { @@ -57,47 +55,29 @@ public class SearchFeatureProviderImpl implements SearchFeatureProvider { } @Override - public DatabaseIndexingManager getIndexingManager(Context context) { - if (mDatabaseIndexingManager == null) { - mDatabaseIndexingManager = new DatabaseIndexingManager(context.getApplicationContext()); - } - return mDatabaseIndexingManager; - } - - @Override - public void updateIndex(Context context) { - long indexStartTime = System.currentTimeMillis(); - getIndexingManager(context).performIndexing(); - int indexingTime = (int) (System.currentTimeMillis() - indexStartTime); - FeatureFactory.getFactory(context).getMetricsFeatureProvider() - .histogram(context, METRICS_ACTION_SETTINGS_INDEX, indexingTime); - } - - @Override public SearchIndexableResources getSearchIndexableResources() { if (mSearchIndexableResources == null) { - mSearchIndexableResources = new SearchIndexableResourcesImpl(); + mSearchIndexableResources = new SearchIndexableResourcesMobile(); } return mSearchIndexableResources; } + @Override + public Intent buildSearchIntent(Context context, int pageId) { + return new Intent(Settings.ACTION_APP_SEARCH_SETTINGS) + .setPackage(getSettingsIntelligencePkgName(context)) + .putExtra(Intent.EXTRA_REFERRER, buildReferrer(context, pageId)); + } + protected boolean isSignatureWhitelisted(Context context, String callerPackage) { return false; } - /** - * A generic method to make the query suitable for searching the database. - * - * @return the cleaned query string - */ - @VisibleForTesting - String cleanQuery(String query) { - if (TextUtils.isEmpty(query)) { - return null; - } - if (Locale.getDefault().equals(Locale.JAPAN)) { - query = IndexData.normalizeJapaneseString(query); - } - return query.trim(); + private static Uri buildReferrer(Context context, int pageId) { + return new Uri.Builder() + .scheme("android-app") + .authority(context.getPackageName()) + .path(String.valueOf(pageId)) + .build(); } } diff --git a/src/com/android/settings/search/SearchIndexableResourcesImpl.java b/src/com/android/settings/search/SearchIndexableResourcesImpl.java deleted file mode 100644 index 7d7606de00..0000000000 --- a/src/com/android/settings/search/SearchIndexableResourcesImpl.java +++ /dev/null @@ -1,202 +0,0 @@ -/* - * 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.search; - -import androidx.annotation.VisibleForTesting; - -import com.android.settings.DateTimeSettings; -import com.android.settings.DisplaySettings; -import com.android.settings.LegalSettings; -import com.android.settings.connecteddevice.AdvancedConnectedDeviceDashboardFragment; -import com.android.settings.connecteddevice.PreviouslyConnectedDeviceDashboardFragment; -import com.android.settings.connecteddevice.BluetoothDashboardFragment; -import com.android.settings.datausage.DataUsageSummaryLegacy; -import com.android.settings.deviceinfo.aboutphone.MyDeviceInfoFragment; -import com.android.settings.accessibility.AccessibilitySettings; -import com.android.settings.accessibility.AccessibilityShortcutPreferenceFragment; -import com.android.settings.accessibility.MagnificationPreferenceFragment; -import com.android.settings.accessibility.VibrationSettings; -import com.android.settings.accounts.AccountDashboardFragment; -import com.android.settings.applications.AppAndNotificationDashboardFragment; -import com.android.settings.applications.DefaultAppSettings; -import com.android.settings.applications.SpecialAccessSettings; -import com.android.settings.applications.assist.ManageAssist; -import com.android.settings.backup.BackupSettingsActivity; -import com.android.settings.backup.BackupSettingsFragment; -import com.android.settings.connecteddevice.ConnectedDeviceDashboardFragment; -import com.android.settings.connecteddevice.usb.UsbDetailsFragment; -import com.android.settings.datausage.DataUsageSummary; -import com.android.settings.deletionhelper.AutomaticStorageManagerSettings; -import com.android.settings.development.DevelopmentSettingsDashboardFragment; -import com.android.settings.deviceinfo.DeviceInfoSettings; -import com.android.settings.deviceinfo.StorageDashboardFragment; -import com.android.settings.deviceinfo.StorageSettings; -import com.android.settings.display.AmbientDisplaySettings; -import com.android.settings.display.AutoBrightnessSettings; -import com.android.settings.display.NightDisplaySettings; -import com.android.settings.display.ScreenZoomSettings; -import com.android.settings.dream.DreamSettings; -import com.android.settings.enterprise.EnterprisePrivacySettings; -import com.android.settings.fuelgauge.PowerUsageAdvanced; -import com.android.settings.fuelgauge.batterysaver.BatterySaverSettings; -import com.android.settings.fuelgauge.PowerUsageSummary; -import com.android.settings.fuelgauge.SmartBatterySettings; -import com.android.settings.gestures.AssistGestureSettings; -import com.android.settings.gestures.DoubleTapPowerSettings; -import com.android.settings.gestures.DoubleTapScreenSettings; -import com.android.settings.gestures.DoubleTwistGestureSettings; -import com.android.settings.gestures.SwipeUpGestureSettings; -import com.android.settings.gestures.GestureSettings; -import com.android.settings.gestures.PickupGestureSettings; -import com.android.settings.gestures.SwipeToNotificationSettings; -import com.android.settings.inputmethod.AvailableVirtualKeyboardFragment; -import com.android.settings.inputmethod.PhysicalKeyboardFragment; -import com.android.settings.inputmethod.VirtualKeyboardFragment; -import com.android.settings.language.LanguageAndInputSettings; -import com.android.settings.location.LocationSettings; -import com.android.settings.location.RecentLocationRequestSeeAllFragment; -import com.android.settings.location.ScanningSettings; -import com.android.settings.network.NetworkDashboardFragment; -import com.android.settings.nfc.PaymentSettings; -import com.android.settings.notification.ConfigureNotificationSettings; -import com.android.settings.notification.SoundSettings; -import com.android.settings.notification.ZenModeAutomationSettings; -import com.android.settings.notification.ZenModeCallsSettings; -import com.android.settings.notification.ZenModeMsgEventReminderSettings; -import com.android.settings.notification.ZenModeBlockedEffectsSettings; -import com.android.settings.notification.ZenModeRestrictNotificationsSettings; -import com.android.settings.notification.ZenModeSettings; -import com.android.settings.notification.ZenModeSoundVibrationSettings; -import com.android.settings.print.PrintSettingsFragment; -import com.android.settings.security.EncryptionAndCredential; -import com.android.settings.security.LockscreenDashboardFragment; -import com.android.settings.security.ScreenPinningSettings; -import com.android.settings.security.SecuritySettings; -import com.android.settings.security.screenlock.ScreenLockSettings; -import com.android.settings.sim.SimSettings; -import com.android.settings.support.SupportDashboardActivity; -import com.android.settings.system.ResetDashboardFragment; -import com.android.settings.system.SystemDashboardFragment; -import com.android.settings.tts.TextToSpeechSettings; -import com.android.settings.tts.TtsEnginePreferenceFragment; -import com.android.settings.users.UserSettings; -import com.android.settings.wallpaper.WallpaperTypeSettings; -import com.android.settings.wfd.WifiDisplaySettings; -import com.android.settings.wifi.ConfigureWifiSettings; -import com.android.settings.wifi.WifiSettings; - -import java.util.Collection; -import java.util.HashSet; -import java.util.Set; - -public class SearchIndexableResourcesImpl implements SearchIndexableResources { - - private final Set<Class> sProviders = new HashSet<>(); - - @VisibleForTesting - void addIndex(Class indexClass) { - sProviders.add(indexClass); - } - - public SearchIndexableResourcesImpl() { - addIndex(WifiSettings.class); - addIndex(NetworkDashboardFragment.class); - addIndex(ConfigureWifiSettings.class); - addIndex(SimSettings.class); - addIndex(DataUsageSummary.class); - addIndex(DataUsageSummaryLegacy.class); - addIndex(ScreenZoomSettings.class); - addIndex(DisplaySettings.class); - addIndex(AutoBrightnessSettings.class); - addIndex(AmbientDisplaySettings.class); - addIndex(WallpaperTypeSettings.class); - addIndex(AppAndNotificationDashboardFragment.class); - addIndex(SoundSettings.class); - addIndex(ZenModeSettings.class); - addIndex(StorageSettings.class); - addIndex(PowerUsageAdvanced.class); - addIndex(DefaultAppSettings.class); - addIndex(ManageAssist.class); - addIndex(SpecialAccessSettings.class); - addIndex(UserSettings.class); - addIndex(AssistGestureSettings.class); - addIndex(PickupGestureSettings.class); - addIndex(DoubleTapScreenSettings.class); - addIndex(DoubleTapPowerSettings.class); - addIndex(DoubleTwistGestureSettings.class); - addIndex(SwipeUpGestureSettings.class); - addIndex(SwipeToNotificationSettings.class); - addIndex(GestureSettings.class); - addIndex(LanguageAndInputSettings.class); - addIndex(LocationSettings.class); - addIndex(ScanningSettings.class); - addIndex(SecuritySettings.class); - addIndex(ScreenLockSettings.class); - addIndex(EncryptionAndCredential.class); - addIndex(ScreenPinningSettings.class); - addIndex(AccountDashboardFragment.class); - addIndex(VirtualKeyboardFragment.class); - addIndex(AvailableVirtualKeyboardFragment.class); - addIndex(PhysicalKeyboardFragment.class); - addIndex(BackupSettingsActivity.class); - addIndex(BackupSettingsFragment.class); - addIndex(DateTimeSettings.class); - addIndex(AccessibilitySettings.class); - addIndex(PrintSettingsFragment.class); - addIndex(DevelopmentSettingsDashboardFragment.class); - addIndex(DeviceInfoSettings.class); - addIndex(LegalSettings.class); - addIndex(SystemDashboardFragment.class); - addIndex(ResetDashboardFragment.class); - addIndex(StorageDashboardFragment.class); - addIndex(ConnectedDeviceDashboardFragment.class); - addIndex(AdvancedConnectedDeviceDashboardFragment.class); - addIndex(EnterprisePrivacySettings.class); - addIndex(PaymentSettings.class); - addIndex(TextToSpeechSettings.class); - addIndex(TtsEnginePreferenceFragment.class); - addIndex(MagnificationPreferenceFragment.class); - addIndex(AccessibilityShortcutPreferenceFragment.class); - addIndex(DreamSettings.class); - addIndex(SupportDashboardActivity.class); - addIndex(AutomaticStorageManagerSettings.class); - addIndex(ConfigureNotificationSettings.class); - addIndex(PowerUsageSummary.class); - addIndex(BatterySaverSettings.class); - addIndex(LockscreenDashboardFragment.class); - addIndex(UsbDetailsFragment.class); - addIndex(WifiDisplaySettings.class); - addIndex(ZenModeMsgEventReminderSettings.class); - addIndex(ZenModeCallsSettings.class); - addIndex(ZenModeSoundVibrationSettings.class); - addIndex(ZenModeBlockedEffectsSettings.class); - addIndex(ZenModeAutomationSettings.class); - addIndex(ZenModeRestrictNotificationsSettings.class); - addIndex(NightDisplaySettings.class); - addIndex(SmartBatterySettings.class); - addIndex(MyDeviceInfoFragment.class); - addIndex(VibrationSettings.class); - addIndex(RecentLocationRequestSeeAllFragment.class); - addIndex(PreviouslyConnectedDeviceDashboardFragment.class); - addIndex(BluetoothDashboardFragment.class); - } - - @Override - public Collection<Class> getProviderValues() { - return sProviders; - } -} diff --git a/src/com/android/settings/search/SettingsSearchIndexablesProvider.java b/src/com/android/settings/search/SettingsSearchIndexablesProvider.java index b9134e5c98..b5982243e2 100644 --- a/src/com/android/settings/search/SettingsSearchIndexablesProvider.java +++ b/src/com/android/settings/search/SettingsSearchIndexablesProvider.java @@ -41,21 +41,28 @@ import static android.provider.SearchIndexablesContract.INDEXABLES_RAW_COLUMNS; import static android.provider.SearchIndexablesContract.INDEXABLES_XML_RES_COLUMNS; import static android.provider.SearchIndexablesContract.NON_INDEXABLES_KEYS_COLUMNS; import static android.provider.SearchIndexablesContract.SITE_MAP_COLUMNS; +import static android.provider.SearchIndexablesContract.SLICE_URI_PAIRS_COLUMNS; import static com.android.settings.dashboard.DashboardFragmentRegistry.CATEGORY_KEY_TO_PARENT_MAP; +import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.database.MatrixCursor; +import android.net.Uri; import android.provider.SearchIndexableResource; import android.provider.SearchIndexablesContract; import android.provider.SearchIndexablesProvider; +import android.provider.SettingsSlicesContract; import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; +import androidx.slice.SliceViewManager; + import com.android.settings.SettingsActivity; import com.android.settings.overlay.FeatureFactory; +import com.android.settings.slices.SettingsSliceProvider; import com.android.settingslib.drawer.DashboardCategory; import com.android.settingslib.drawer.Tile; @@ -168,8 +175,8 @@ public class SettingsSearchIndexablesProvider extends SearchIndexablesProvider { // Build parent-child class pairs for all children listed under this key. for (Tile tile : category.getTiles()) { String childClass = null; - if (tile.metaData != null) { - childClass = tile.metaData.getString( + if (tile.getMetaData() != null) { + childClass = tile.getMetaData().getString( SettingsActivity.META_DATA_KEY_FRAGMENT_CLASS); } if (childClass == null) { @@ -184,6 +191,33 @@ public class SettingsSearchIndexablesProvider extends SearchIndexablesProvider { return cursor; } + @Override + public Cursor querySliceUriPairs() { + final SliceViewManager manager = SliceViewManager.getInstance(getContext()); + final MatrixCursor cursor = new MatrixCursor(SLICE_URI_PAIRS_COLUMNS); + final Uri baseUri = + new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .build(); + final Uri platformBaseUri = + new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSlicesContract.AUTHORITY) + .build(); + + final Collection<Uri> sliceUris = manager.getSliceDescendants(baseUri); + sliceUris.addAll(manager.getSliceDescendants(platformBaseUri)); + + for (Uri uri : sliceUris) { + cursor.newRow() + .add(SearchIndexablesContract.SliceUriPairColumns.KEY, uri.getLastPathSegment()) + .add(SearchIndexablesContract.SliceUriPairColumns.SLICE_URI, uri); + } + + return cursor; + } + private List<String> getNonIndexableKeysFromProvider(Context context) { final Collection<Class> values = FeatureFactory.getFactory(context) .getSearchFeatureProvider().getSearchIndexableResources().getProviderValues(); @@ -207,7 +241,7 @@ public class SettingsSearchIndexablesProvider extends SearchIndexablesProvider { if (System.getProperty(SYSPROP_CRASH_ON_ERROR) != null) { throw new RuntimeException(e); } - Log.e(TAG, "Error trying to get non-indexable keys from: " + clazz.getName() , e); + Log.e(TAG, "Error trying to get non-indexable keys from: " + clazz.getName(), e); continue; } diff --git a/src/com/android/settings/search/actionbar/SearchMenuController.java b/src/com/android/settings/search/actionbar/SearchMenuController.java index 131f7884fc..25d0d3027d 100644 --- a/src/com/android/settings/search/actionbar/SearchMenuController.java +++ b/src/com/android/settings/search/actionbar/SearchMenuController.java @@ -17,20 +17,24 @@ package com.android.settings.search.actionbar; import android.annotation.NonNull; -import android.app.Fragment; +import android.app.settings.SettingsEnums; +import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; import android.os.Bundle; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import androidx.fragment.app.Fragment; + import com.android.settings.R; import com.android.settings.Utils; +import com.android.settings.core.InstrumentedFragment; +import com.android.settings.core.InstrumentedPreferenceFragment; import com.android.settings.overlay.FeatureFactory; import com.android.settings.search.SearchFeatureProvider; import com.android.settingslib.core.lifecycle.LifecycleObserver; -import com.android.settingslib.core.lifecycle.ObservableFragment; -import com.android.settingslib.core.lifecycle.ObservablePreferenceFragment; import com.android.settingslib.core.lifecycle.events.OnCreateOptionsMenu; public class SearchMenuController implements LifecycleObserver, OnCreateOptionsMenu { @@ -38,24 +42,34 @@ public class SearchMenuController implements LifecycleObserver, OnCreateOptionsM public static final String NEED_SEARCH_ICON_IN_ACTION_BAR = "need_search_icon_in_action_bar"; private final Fragment mHost; + private final int mPageId; - public static void init(@NonNull ObservablePreferenceFragment host) { - host.getLifecycle().addObserver(new SearchMenuController(host)); + public static void init(@NonNull InstrumentedPreferenceFragment host) { + host.getSettingsLifecycle().addObserver( + new SearchMenuController(host, host.getMetricsCategory())); } - public static void init(@NonNull ObservableFragment host) { - host.getLifecycle().addObserver(new SearchMenuController(host)); + public static void init(@NonNull InstrumentedFragment host) { + host.getSettingsLifecycle().addObserver( + new SearchMenuController(host, host.getMetricsCategory())); } - private SearchMenuController(@NonNull Fragment host) { + private SearchMenuController(@NonNull Fragment host, int pageId) { mHost = host; + mPageId = pageId; } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + final Context context = mHost.getContext(); + final String SettingsIntelligencePkgName = context.getString( + R.string.config_settingsintelligence_package_name); if (!Utils.isDeviceProvisioned(mHost.getContext())) { return; } + if (!Utils.isPackageEnabled(mHost.getContext(), SettingsIntelligencePkgName)) { + return; + } if (menu == null) { return; } @@ -69,11 +83,18 @@ public class SearchMenuController implements LifecycleObserver, OnCreateOptionsM searchItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); searchItem.setOnMenuItemClickListener(target -> { - final Intent intent = SearchFeatureProvider.SEARCH_UI_INTENT; - intent.setPackage(FeatureFactory.getFactory(mHost.getContext()) - .getSearchFeatureProvider().getSettingsIntelligencePkgName()); + final Intent intent = FeatureFactory.getFactory(context) + .getSearchFeatureProvider() + .buildSearchIntent(context, mPageId); + + if (context.getPackageManager().queryIntentActivities(intent, + PackageManager.MATCH_DEFAULT_ONLY).isEmpty()) { + return true; + } - mHost.startActivityForResult(intent, 0 /* requestCode */); + FeatureFactory.getFactory(context).getMetricsFeatureProvider() + .action(context, SettingsEnums.ACTION_SEARCH_RESULTS); + mHost.startActivityForResult(intent, SearchFeatureProvider.REQUEST_CODE); return true; }); } diff --git a/src/com/android/settings/search/indexing/IndexData.java b/src/com/android/settings/search/indexing/IndexData.java deleted file mode 100644 index eac752855b..0000000000 --- a/src/com/android/settings/search/indexing/IndexData.java +++ /dev/null @@ -1,313 +0,0 @@ -/* - * Copyright (C) 2017 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.search.indexing; - -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.text.TextUtils; - -import com.android.settings.SettingsActivity; -import com.android.settings.search.DatabaseIndexingUtils; -import com.android.settings.search.ResultPayload; -import com.android.settings.search.ResultPayloadUtils; - -import java.text.Normalizer; -import java.util.Locale; -import java.util.Objects; -import java.util.regex.Pattern; - -/** - * Data class representing a single row in the Setting Search results database. - */ -public class IndexData { - public final String locale; - public final String updatedTitle; - public final String normalizedTitle; - public final String updatedSummaryOn; - public final String normalizedSummaryOn; - public final String entries; - public final String className; - public final String childClassName; - public final String screenTitle; - public final int iconResId; - public final String spaceDelimitedKeywords; - public final String intentAction; - public final String intentTargetPackage; - public final String intentTargetClass; - public final boolean enabled; - public final String key; - public final int userId; - public final int payloadType; - public final byte[] payload; - - private static final String NON_BREAKING_HYPHEN = "\u2011"; - private static final String EMPTY = ""; - private static final String HYPHEN = "-"; - private static final String SPACE = " "; - // Regex matching a comma, and any number of subsequent white spaces. - private static final String LIST_DELIMITERS = "[,]\\s*"; - - private static final Pattern REMOVE_DIACRITICALS_PATTERN - = Pattern.compile("\\p{InCombiningDiacriticalMarks}+"); - - private IndexData(Builder builder) { - locale = Locale.getDefault().toString(); - updatedTitle = normalizeHyphen(builder.mTitle); - updatedSummaryOn = normalizeHyphen(builder.mSummaryOn); - if (Locale.JAPAN.toString().equalsIgnoreCase(locale)) { - // Special case for JP. Convert charset to the same type for indexing purpose. - normalizedTitle = normalizeJapaneseString(builder.mTitle); - normalizedSummaryOn = normalizeJapaneseString(builder.mSummaryOn); - } else { - normalizedTitle = normalizeString(builder.mTitle); - normalizedSummaryOn = normalizeString(builder.mSummaryOn); - } - entries = builder.mEntries; - className = builder.mClassName; - childClassName = builder.mChildClassName; - screenTitle = builder.mScreenTitle; - iconResId = builder.mIconResId; - spaceDelimitedKeywords = normalizeKeywords(builder.mKeywords); - intentAction = builder.mIntentAction; - intentTargetPackage = builder.mIntentTargetPackage; - intentTargetClass = builder.mIntentTargetClass; - enabled = builder.mEnabled; - key = builder.mKey; - userId = builder.mUserId; - payloadType = builder.mPayloadType; - payload = builder.mPayload != null ? ResultPayloadUtils.marshall(builder.mPayload) - : null; - } - - /** - * Returns the doc id for this row. - */ - public int getDocId() { - // Eventually we want all DocIds to be the data_reference key. For settings values, - // this will be preference keys, and for non-settings they should be unique. - return TextUtils.isEmpty(key) - ? Objects.hash(updatedTitle, className, screenTitle, intentTargetClass) - : key.hashCode(); - } - - @Override - public String toString() { - return new StringBuilder(updatedTitle) - .append(": ") - .append(updatedSummaryOn) - .toString(); - } - - /** - * In the list of keywords, replace the comma and all subsequent whitespace with a single space. - */ - public static String normalizeKeywords(String input) { - return (input != null) ? input.replaceAll(LIST_DELIMITERS, SPACE) : EMPTY; - } - - /** - * @return {@param input} where all non-standard hyphens are replaced by normal hyphens. - */ - public static String normalizeHyphen(String input) { - return (input != null) ? input.replaceAll(NON_BREAKING_HYPHEN, HYPHEN) : EMPTY; - } - - /** - * @return {@param input} with all hyphens removed, and all letters lower case. - */ - public static String normalizeString(String input) { - final String normalizedHypen = normalizeHyphen(input); - final String nohyphen = (input != null) ? normalizedHypen.replaceAll(HYPHEN, EMPTY) : EMPTY; - final String normalized = Normalizer.normalize(nohyphen, Normalizer.Form.NFD); - - return REMOVE_DIACRITICALS_PATTERN.matcher(normalized).replaceAll("").toLowerCase(); - } - - public static String normalizeJapaneseString(String input) { - final String nohyphen = (input != null) ? input.replaceAll(HYPHEN, EMPTY) : EMPTY; - final String normalized = Normalizer.normalize(nohyphen, Normalizer.Form.NFKD); - final StringBuffer sb = new StringBuffer(); - final int length = normalized.length(); - for (int i = 0; i < length; i++) { - char c = normalized.charAt(i); - // Convert Hiragana to full-width Katakana - if (c >= '\u3041' && c <= '\u3096') { - sb.append((char) (c - '\u3041' + '\u30A1')); - } else { - sb.append(c); - } - } - - return REMOVE_DIACRITICALS_PATTERN.matcher(sb.toString()).replaceAll("").toLowerCase(); - } - - public static class Builder { - private String mTitle; - private String mSummaryOn; - private String mEntries; - private String mClassName; - private String mChildClassName; - private String mScreenTitle; - private int mIconResId; - private String mKeywords; - private String mIntentAction; - private String mIntentTargetPackage; - private String mIntentTargetClass; - private boolean mEnabled; - private String mKey; - private int mUserId; - @ResultPayload.PayloadType - private int mPayloadType; - private ResultPayload mPayload; - - public Builder setTitle(String title) { - mTitle = title; - return this; - } - - public Builder setSummaryOn(String summaryOn) { - mSummaryOn = summaryOn; - return this; - } - - public Builder setEntries(String entries) { - mEntries = entries; - return this; - } - - public Builder setClassName(String className) { - mClassName = className; - return this; - } - - public Builder setChildClassName(String childClassName) { - mChildClassName = childClassName; - return this; - } - - public Builder setScreenTitle(String screenTitle) { - mScreenTitle = screenTitle; - return this; - } - - public Builder setIconResId(int iconResId) { - mIconResId = iconResId; - return this; - } - - public Builder setKeywords(String keywords) { - mKeywords = keywords; - return this; - } - - public Builder setIntentAction(String intentAction) { - mIntentAction = intentAction; - return this; - } - - public Builder setIntentTargetPackage(String intentTargetPackage) { - mIntentTargetPackage = intentTargetPackage; - return this; - } - - public Builder setIntentTargetClass(String intentTargetClass) { - mIntentTargetClass = intentTargetClass; - return this; - } - - public Builder setEnabled(boolean enabled) { - mEnabled = enabled; - return this; - } - - public Builder setKey(String key) { - mKey = key; - return this; - } - - public Builder setUserId(int userId) { - mUserId = userId; - return this; - } - - public Builder setPayload(ResultPayload payload) { - mPayload = payload; - - if (mPayload != null) { - setPayloadType(mPayload.getType()); - } - return this; - } - - /** - * Payload type is added when a Payload is added to the Builder in {setPayload} - * - * @param payloadType PayloadType - * @return The Builder - */ - private Builder setPayloadType(@ResultPayload.PayloadType int payloadType) { - mPayloadType = payloadType; - return this; - } - - /** - * Adds intent to inline payloads, or creates an Intent Payload as a fallback if the - * payload is null. - */ - private void setIntent(Context context) { - if (mPayload != null) { - return; - } - final Intent intent = buildIntent(context); - mPayload = new ResultPayload(intent); - mPayloadType = ResultPayload.PayloadType.INTENT; - } - - /** - * Adds Intent payload to builder. - */ - private Intent buildIntent(Context context) { - final Intent intent; - - boolean isEmptyIntentAction = TextUtils.isEmpty(mIntentAction); - // No intent action is set, or the intent action is for a subsetting. - if (isEmptyIntentAction) { - // Action is null, we will launch it as a sub-setting - intent = DatabaseIndexingUtils.buildSearchResultPageIntent(context, mClassName, - mKey, mScreenTitle); - } else { - intent = new Intent(mIntentAction); - final String targetClass = mIntentTargetClass; - if (!TextUtils.isEmpty(mIntentTargetPackage) - && !TextUtils.isEmpty(targetClass)) { - final ComponentName component = new ComponentName(mIntentTargetPackage, - targetClass); - intent.setComponent(component); - } - intent.putExtra(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, mKey); - } - return intent; - } - - public IndexData build(Context context) { - setIntent(context); - return new IndexData(this); - } - } -}
\ No newline at end of file diff --git a/src/com/android/settings/search/indexing/IndexDataConverter.java b/src/com/android/settings/search/indexing/IndexDataConverter.java deleted file mode 100644 index 8aa84fa515..0000000000 --- a/src/com/android/settings/search/indexing/IndexDataConverter.java +++ /dev/null @@ -1,300 +0,0 @@ -/* - * Copyright (C) 2017 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.search.indexing; - -import android.annotation.Nullable; -import android.content.Context; -import android.content.res.Resources; -import android.content.res.XmlResourceParser; -import android.provider.SearchIndexableData; -import android.provider.SearchIndexableResource; -import androidx.annotation.DrawableRes; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.util.Log; -import android.util.Xml; - -import com.android.settings.search.DatabaseIndexingUtils; -import com.android.settings.core.PreferenceXmlParserUtils; -import com.android.settings.search.ResultPayload; -import com.android.settings.search.SearchIndexableRaw; - -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * Helper class to convert {@link PreIndexData} to {@link IndexData}. - */ -public class IndexDataConverter { - - private static final String LOG_TAG = "IndexDataConverter"; - - private static final String NODE_NAME_PREFERENCE_SCREEN = "PreferenceScreen"; - private static final String NODE_NAME_CHECK_BOX_PREFERENCE = "CheckBoxPreference"; - private static final String NODE_NAME_LIST_PREFERENCE = "ListPreference"; - - private final Context mContext; - - public IndexDataConverter(Context context) { - mContext = context; - } - - /** - * Return the collection of {@param preIndexData} converted into {@link IndexData}. - * - * @param preIndexData a collection of {@link SearchIndexableResource}, - * {@link SearchIndexableRaw} and non-indexable keys. - */ - public List<IndexData> convertPreIndexDataToIndexData(PreIndexData preIndexData) { - final long current = System.currentTimeMillis(); - final List<SearchIndexableData> indexableData = preIndexData.dataToUpdate; - final Map<String, Set<String>> nonIndexableKeys = preIndexData.nonIndexableKeys; - final List<IndexData> indexData = new ArrayList<>(); - - for (SearchIndexableData data : indexableData) { - if (data instanceof SearchIndexableRaw) { - final SearchIndexableRaw rawData = (SearchIndexableRaw) data; - final Set<String> rawNonIndexableKeys = nonIndexableKeys.get( - rawData.intentTargetPackage); - final IndexData.Builder builder = convertRaw(rawData, rawNonIndexableKeys); - - if (builder != null) { - indexData.add(builder.build(mContext)); - } - } else if (data instanceof SearchIndexableResource) { - final SearchIndexableResource sir = (SearchIndexableResource) data; - final Set<String> resourceNonIndexableKeys = - getNonIndexableKeysForResource(nonIndexableKeys, sir.packageName); - final List<IndexData> resourceData = convertResource(sir, resourceNonIndexableKeys); - indexData.addAll(resourceData); - } - } - - final long endConversion = System.currentTimeMillis(); - Log.d(LOG_TAG, "Converting pre-index data to index data took: " - + (endConversion - current)); - - return indexData; - } - - /** - * Return the conversion of {@link SearchIndexableRaw} to {@link IndexData}. - * The fields of {@link SearchIndexableRaw} are a subset of {@link IndexData}, - * and there is some data sanitization in the conversion. - */ - @Nullable - private IndexData.Builder convertRaw(SearchIndexableRaw raw, Set<String> nonIndexableKeys) { - // A row is enabled if it does not show up as an nonIndexableKey - boolean enabled = !(nonIndexableKeys != null && nonIndexableKeys.contains(raw.key)); - - IndexData.Builder builder = new IndexData.Builder(); - builder.setTitle(raw.title) - .setSummaryOn(raw.summaryOn) - .setEntries(raw.entries) - .setKeywords(raw.keywords) - .setClassName(raw.className) - .setScreenTitle(raw.screenTitle) - .setIconResId(raw.iconResId) - .setIntentAction(raw.intentAction) - .setIntentTargetPackage(raw.intentTargetPackage) - .setIntentTargetClass(raw.intentTargetClass) - .setEnabled(enabled) - .setKey(raw.key) - .setUserId(raw.userId); - - return builder; - } - - /** - * Return the conversion of the {@link SearchIndexableResource} to {@link IndexData}. - * Each of the elements in the xml layout attribute of {@param sir} is a candidate to be - * converted (including the header element). - * - * TODO (b/33577327) simplify this method. - */ - private List<IndexData> convertResource(SearchIndexableResource sir, - Set<String> nonIndexableKeys) { - final Context context = sir.context; - XmlResourceParser parser = null; - - List<IndexData> resourceIndexData = new ArrayList<>(); - try { - parser = context.getResources().getXml(sir.xmlResId); - - int type; - while ((type = parser.next()) != XmlPullParser.END_DOCUMENT - && type != XmlPullParser.START_TAG) { - // Parse next until start tag is found - } - - String nodeName = parser.getName(); - if (!NODE_NAME_PREFERENCE_SCREEN.equals(nodeName)) { - throw new RuntimeException( - "XML document must start with <PreferenceScreen> tag; found" - + nodeName + " at " + parser.getPositionDescription()); - } - - final int outerDepth = parser.getDepth(); - final AttributeSet attrs = Xml.asAttributeSet(parser); - - final String screenTitle = PreferenceXmlParserUtils.getDataTitle(context, attrs); - String key = PreferenceXmlParserUtils.getDataKey(context, attrs); - - String title; - String headerTitle; - String summary; - String headerSummary; - String keywords; - String headerKeywords; - String childFragment; - @DrawableRes int iconResId; - ResultPayload payload; - boolean enabled; - final String fragmentName = sir.className; - final String intentAction = sir.intentAction; - final String intentTargetPackage = sir.intentTargetPackage; - final String intentTargetClass = sir.intentTargetClass; - - Map<String, ResultPayload> controllerUriMap = new HashMap<>(); - - if (fragmentName != null) { - controllerUriMap = DatabaseIndexingUtils - .getPayloadKeyMap(fragmentName, context); - } - - headerTitle = PreferenceXmlParserUtils.getDataTitle(context, attrs); - headerSummary = PreferenceXmlParserUtils.getDataSummary(context, attrs); - headerKeywords = PreferenceXmlParserUtils.getDataKeywords(context, attrs); - enabled = !nonIndexableKeys.contains(key); - - // TODO: Set payload type for header results - IndexData.Builder headerBuilder = new IndexData.Builder(); - headerBuilder.setTitle(headerTitle) - .setSummaryOn(headerSummary) - .setKeywords(headerKeywords) - .setClassName(fragmentName) - .setScreenTitle(screenTitle) - .setIntentAction(intentAction) - .setIntentTargetPackage(intentTargetPackage) - .setIntentTargetClass(intentTargetClass) - .setEnabled(enabled) - .setKey(key) - .setUserId(-1 /* default user id */); - - // Flag for XML headers which a child element's title. - boolean isHeaderUnique = true; - IndexData.Builder builder; - - while ((type = parser.next()) != XmlPullParser.END_DOCUMENT - && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { - if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { - continue; - } - - nodeName = parser.getName(); - - title = PreferenceXmlParserUtils.getDataTitle(context, attrs); - key = PreferenceXmlParserUtils.getDataKey(context, attrs); - enabled = !nonIndexableKeys.contains(key); - keywords = PreferenceXmlParserUtils.getDataKeywords(context, attrs); - iconResId = PreferenceXmlParserUtils.getDataIcon(context, attrs); - - if (isHeaderUnique && TextUtils.equals(headerTitle, title)) { - isHeaderUnique = false; - } - - builder = new IndexData.Builder(); - builder.setTitle(title) - .setKeywords(keywords) - .setClassName(fragmentName) - .setScreenTitle(screenTitle) - .setIconResId(iconResId) - .setIntentAction(intentAction) - .setIntentTargetPackage(intentTargetPackage) - .setIntentTargetClass(intentTargetClass) - .setEnabled(enabled) - .setKey(key) - .setUserId(-1 /* default user id */); - - if (!nodeName.equals(NODE_NAME_CHECK_BOX_PREFERENCE)) { - summary = PreferenceXmlParserUtils.getDataSummary(context, attrs); - - String entries = null; - - if (nodeName.endsWith(NODE_NAME_LIST_PREFERENCE)) { - entries = PreferenceXmlParserUtils.getDataEntries(context, attrs); - } - - // TODO (b/62254931) index primitives instead of payload - payload = controllerUriMap.get(key); - childFragment = PreferenceXmlParserUtils.getDataChildFragment(context, attrs); - - builder.setSummaryOn(summary) - .setEntries(entries) - .setChildClassName(childFragment) - .setPayload(payload); - - resourceIndexData.add(builder.build(mContext)); - } else { - // TODO (b/33577327) We removed summary off here. We should check if we can - // merge this 'else' section with the one above. Put a break point to - // investigate. - String summaryOn = PreferenceXmlParserUtils.getDataSummaryOn(context, attrs); - String summaryOff = PreferenceXmlParserUtils.getDataSummaryOff(context, attrs); - - if (TextUtils.isEmpty(summaryOn) && TextUtils.isEmpty(summaryOff)) { - summaryOn = PreferenceXmlParserUtils.getDataSummary(context, attrs); - } - - builder.setSummaryOn(summaryOn); - - resourceIndexData.add(builder.build(mContext)); - } - } - - // The xml header's title does not match the title of one of the child settings. - if (isHeaderUnique) { - resourceIndexData.add(headerBuilder.build(mContext)); - } - } catch (XmlPullParserException e) { - Log.w(LOG_TAG, "XML Error parsing PreferenceScreen: ", e); - } catch (IOException e) { - Log.w(LOG_TAG, "IO Error parsing PreferenceScreen: ", e); - } catch (Resources.NotFoundException e) { - Log.w(LOG_TAG, "Resoucre not found error parsing PreferenceScreen: ", e); - } finally { - if (parser != null) parser.close(); - } - return resourceIndexData; - } - - private Set<String> getNonIndexableKeysForResource(Map<String, Set<String>> nonIndexableKeys, - String packageName) { - return nonIndexableKeys.containsKey(packageName) - ? nonIndexableKeys.get(packageName) - : new HashSet<>(); - } -} diff --git a/src/com/android/settings/search/indexing/PreIndexData.java b/src/com/android/settings/search/indexing/PreIndexData.java deleted file mode 100644 index de3cf7ca27..0000000000 --- a/src/com/android/settings/search/indexing/PreIndexData.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2017 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.search.indexing; - -import android.provider.SearchIndexableData; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - - -/** - * Holds Data sources for indexable data. - * TODO (b/33577327) add getters and setters for data. - */ -public class PreIndexData { - public List<SearchIndexableData> dataToUpdate; - public Map<String, Set<String>> nonIndexableKeys; - - public PreIndexData() { - dataToUpdate = new ArrayList<>(); - nonIndexableKeys = new HashMap<>(); - } - - public PreIndexData(PreIndexData other) { - dataToUpdate = new ArrayList<>(other.dataToUpdate); - nonIndexableKeys = new HashMap<>(other.nonIndexableKeys); - } - - public PreIndexData copy() { - return new PreIndexData(this); - } - - public void clear() { - dataToUpdate.clear(); - nonIndexableKeys.clear(); - } -} diff --git a/src/com/android/settings/search/indexing/PreIndexDataCollector.java b/src/com/android/settings/search/indexing/PreIndexDataCollector.java deleted file mode 100644 index 63000b4874..0000000000 --- a/src/com/android/settings/search/indexing/PreIndexDataCollector.java +++ /dev/null @@ -1,368 +0,0 @@ -/* - * Copyright (C) 2017 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.search.indexing; - -import android.Manifest; -import android.content.ContentResolver; -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.database.Cursor; -import android.net.Uri; -import android.provider.SearchIndexableResource; -import android.provider.SearchIndexablesContract; -import android.text.TextUtils; -import android.util.ArraySet; -import android.util.Log; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.settings.search.SearchIndexableRaw; -import com.android.settings.search.SettingsSearchIndexablesProvider; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_CLASS_NAME; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_ICON_RESID; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_RESID; - -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_CLASS_NAME; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_ENTRIES; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_ICON_RESID; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_ACTION; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_TARGET_CLASS; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_KEY; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_KEYWORDS; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_RANK; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SCREEN_TITLE; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SUMMARY_OFF; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SUMMARY_ON; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_TITLE; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_USER_ID; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_ACTION; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE; - -/** - * Collects all data from {@link android.provider.SearchIndexablesProvider} to be indexed. - */ -public class PreIndexDataCollector { - - private static final String TAG = "IndexableDataCollector"; - - // TODO (b/64938328) update to new search package. - private final String BASE_AUTHORITY = "com.android.settings"; - - private static final List<String> EMPTY_LIST = Collections.emptyList(); - - private Context mContext; - - private PreIndexData mIndexData; - - public PreIndexDataCollector(Context context) { - mContext = context; - } - - public PreIndexData collectIndexableData(List<ResolveInfo> providers, boolean isFullIndex) { - mIndexData = new PreIndexData(); - - for (final ResolveInfo info : providers) { - if (!isWellKnownProvider(info)) { - continue; - } - final String authority = info.providerInfo.authority; - final String packageName = info.providerInfo.packageName; - - if (isFullIndex) { - addIndexablesFromRemoteProvider(packageName, authority); - } - - final long nonIndexableStartTime = System.currentTimeMillis(); - addNonIndexablesKeysFromRemoteProvider(packageName, authority); - if (SettingsSearchIndexablesProvider.DEBUG) { - final long nonIndexableTime = System.currentTimeMillis() - nonIndexableStartTime; - Log.d(TAG, "performIndexing update non-indexable for package " + packageName - + " took time: " + nonIndexableTime); - } - } - - return mIndexData; - } - - private boolean addIndexablesFromRemoteProvider(String packageName, String authority) { - try { - final Context context = BASE_AUTHORITY.equals(authority) ? - mContext : mContext.createPackageContext(packageName, 0); - - final Uri uriForResources = buildUriForXmlResources(authority); - mIndexData.dataToUpdate.addAll(getIndexablesForXmlResourceUri(context, packageName, - uriForResources, SearchIndexablesContract.INDEXABLES_XML_RES_COLUMNS)); - - final Uri uriForRawData = buildUriForRawData(authority); - mIndexData.dataToUpdate.addAll(getIndexablesForRawDataUri(context, packageName, - uriForRawData, SearchIndexablesContract.INDEXABLES_RAW_COLUMNS)); - return true; - } catch (PackageManager.NameNotFoundException e) { - Log.w(TAG, "Could not create context for " + packageName + ": " - + Log.getStackTraceString(e)); - return false; - } - } - - @VisibleForTesting - List<SearchIndexableResource> getIndexablesForXmlResourceUri(Context packageContext, - String packageName, Uri uri, String[] projection) { - - final ContentResolver resolver = packageContext.getContentResolver(); - final Cursor cursor = resolver.query(uri, projection, null, null, null); - List<SearchIndexableResource> resources = new ArrayList<>(); - - if (cursor == null) { - Log.w(TAG, "Cannot add index data for Uri: " + uri.toString()); - return resources; - } - - try { - final int count = cursor.getCount(); - if (count > 0) { - while (cursor.moveToNext()) { - final int xmlResId = cursor.getInt(COLUMN_INDEX_XML_RES_RESID); - - final String className = cursor.getString(COLUMN_INDEX_XML_RES_CLASS_NAME); - final int iconResId = cursor.getInt(COLUMN_INDEX_XML_RES_ICON_RESID); - - final String action = cursor.getString(COLUMN_INDEX_XML_RES_INTENT_ACTION); - final String targetPackage = cursor.getString( - COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE); - final String targetClass = cursor.getString( - COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS); - - SearchIndexableResource sir = new SearchIndexableResource(packageContext); - sir.xmlResId = xmlResId; - sir.className = className; - sir.packageName = packageName; - sir.iconResId = iconResId; - sir.intentAction = action; - sir.intentTargetPackage = targetPackage; - sir.intentTargetClass = targetClass; - - resources.add(sir); - } - } - } finally { - cursor.close(); - } - return resources; - } - - private void addNonIndexablesKeysFromRemoteProvider(String packageName, - String authority) { - final List<String> keys = - getNonIndexablesKeysFromRemoteProvider(packageName, authority); - - if (keys != null && !keys.isEmpty()) { - mIndexData.nonIndexableKeys.put(authority, new ArraySet<>(keys)); - } - } - - @VisibleForTesting - List<String> getNonIndexablesKeysFromRemoteProvider(String packageName, - String authority) { - try { - final Context packageContext = mContext.createPackageContext(packageName, 0); - - final Uri uriForNonIndexableKeys = buildUriForNonIndexableKeys(authority); - return getNonIndexablesKeys(packageContext, uriForNonIndexableKeys, - SearchIndexablesContract.NON_INDEXABLES_KEYS_COLUMNS); - } catch (PackageManager.NameNotFoundException e) { - Log.w(TAG, "Could not create context for " + packageName + ": " - + Log.getStackTraceString(e)); - return EMPTY_LIST; - } - } - - @VisibleForTesting - Uri buildUriForXmlResources(String authority) { - return Uri.parse("content://" + authority + "/" + - SearchIndexablesContract.INDEXABLES_XML_RES_PATH); - } - - @VisibleForTesting - Uri buildUriForRawData(String authority) { - return Uri.parse("content://" + authority + "/" + - SearchIndexablesContract.INDEXABLES_RAW_PATH); - } - - @VisibleForTesting - Uri buildUriForNonIndexableKeys(String authority) { - return Uri.parse("content://" + authority + "/" + - SearchIndexablesContract.NON_INDEXABLES_KEYS_PATH); - } - - @VisibleForTesting - List<SearchIndexableRaw> getIndexablesForRawDataUri(Context packageContext, String packageName, - Uri uri, String[] projection) { - final ContentResolver resolver = packageContext.getContentResolver(); - final Cursor cursor = resolver.query(uri, projection, null, null, null); - List<SearchIndexableRaw> rawData = new ArrayList<>(); - - if (cursor == null) { - Log.w(TAG, "Cannot add index data for Uri: " + uri.toString()); - return rawData; - } - - try { - final int count = cursor.getCount(); - if (count > 0) { - while (cursor.moveToNext()) { - final int providerRank = cursor.getInt(COLUMN_INDEX_RAW_RANK); - // TODO Remove rank - final String title = cursor.getString(COLUMN_INDEX_RAW_TITLE); - final String summaryOn = cursor.getString(COLUMN_INDEX_RAW_SUMMARY_ON); - final String summaryOff = cursor.getString(COLUMN_INDEX_RAW_SUMMARY_OFF); - final String entries = cursor.getString(COLUMN_INDEX_RAW_ENTRIES); - final String keywords = cursor.getString(COLUMN_INDEX_RAW_KEYWORDS); - - final String screenTitle = cursor.getString(COLUMN_INDEX_RAW_SCREEN_TITLE); - - final String className = cursor.getString(COLUMN_INDEX_RAW_CLASS_NAME); - final int iconResId = cursor.getInt(COLUMN_INDEX_RAW_ICON_RESID); - - final String action = cursor.getString(COLUMN_INDEX_RAW_INTENT_ACTION); - final String targetPackage = cursor.getString( - COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE); - final String targetClass = cursor.getString( - COLUMN_INDEX_RAW_INTENT_TARGET_CLASS); - - final String key = cursor.getString(COLUMN_INDEX_RAW_KEY); - final int userId = cursor.getInt(COLUMN_INDEX_RAW_USER_ID); - - SearchIndexableRaw data = new SearchIndexableRaw(packageContext); - data.title = title; - data.summaryOn = summaryOn; - data.summaryOff = summaryOff; - data.entries = entries; - data.keywords = keywords; - data.screenTitle = screenTitle; - data.className = className; - data.packageName = packageName; - data.iconResId = iconResId; - data.intentAction = action; - data.intentTargetPackage = targetPackage; - data.intentTargetClass = targetClass; - data.key = key; - data.userId = userId; - - rawData.add(data); - } - } - } finally { - cursor.close(); - } - - return rawData; - } - - private List<String> getNonIndexablesKeys(Context packageContext, Uri uri, - String[] projection) { - - final ContentResolver resolver = packageContext.getContentResolver(); - final List<String> result = new ArrayList<>(); - Cursor cursor; - try { - cursor = resolver.query(uri, projection, null, null, null); - } catch (NullPointerException e) { - Log.e(TAG, "Exception querying the keys!", e); - return result; - } - - if (cursor == null) { - Log.w(TAG, "Cannot add index data for Uri: " + uri.toString()); - return result; - } - - try { - final int count = cursor.getCount(); - if (count > 0) { - while (cursor.moveToNext()) { - final String key = cursor.getString(COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE); - - if (TextUtils.isEmpty(key) && Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "Empty non-indexable key from: " - + packageContext.getPackageName()); - continue; - } - - result.add(key); - } - } - return result; - } finally { - cursor.close(); - } - } - - /** - * Only allow a "well known" SearchIndexablesProvider. The provider should: - * - * - have read/write {@link Manifest.permission#READ_SEARCH_INDEXABLES} - * - be from a privileged package - */ - @VisibleForTesting - boolean isWellKnownProvider(ResolveInfo info) { - final String authority = info.providerInfo.authority; - final String packageName = info.providerInfo.applicationInfo.packageName; - - if (TextUtils.isEmpty(authority) || TextUtils.isEmpty(packageName)) { - return false; - } - - final String readPermission = info.providerInfo.readPermission; - final String writePermission = info.providerInfo.writePermission; - - if (TextUtils.isEmpty(readPermission) || TextUtils.isEmpty(writePermission)) { - return false; - } - - if (!android.Manifest.permission.READ_SEARCH_INDEXABLES.equals(readPermission) || - !android.Manifest.permission.READ_SEARCH_INDEXABLES.equals(writePermission)) { - return false; - } - - return isPrivilegedPackage(packageName, mContext); - } - - /** - * @return true if the {@param packageName} is privileged. - */ - private boolean isPrivilegedPackage(String packageName, Context context) { - final PackageManager pm = context.getPackageManager(); - try { - PackageInfo packInfo = pm.getPackageInfo(packageName, 0); - return ((packInfo.applicationInfo.privateFlags - & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0); - } catch (PackageManager.NameNotFoundException e) { - return false; - } - } -} diff --git a/src/com/android/settings/security/ChangeProfileScreenLockPreferenceController.java b/src/com/android/settings/security/ChangeProfileScreenLockPreferenceController.java index c244e47b56..26370d9f52 100644 --- a/src/com/android/settings/security/ChangeProfileScreenLockPreferenceController.java +++ b/src/com/android/settings/security/ChangeProfileScreenLockPreferenceController.java @@ -21,9 +21,10 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.UserHandle; -import androidx.preference.Preference; import android.text.TextUtils; +import androidx.preference.Preference; + import com.android.settings.R; import com.android.settings.Utils; import com.android.settings.core.SubSettingLauncher; @@ -77,7 +78,7 @@ public class ChangeProfileScreenLockPreferenceController extends extras.putInt(Intent.EXTRA_USER_ID, mProfileChallengeUserId); new SubSettingLauncher(mContext) .setDestination(ChooseLockGeneric.ChooseLockGenericFragment.class.getName()) - .setTitle(R.string.lock_settings_picker_title_profile) + .setTitleRes(R.string.lock_settings_picker_title_profile) .setSourceMetricsCategory(mHost.getMetricsCategory()) .setArguments(extras) .launch(); diff --git a/src/com/android/settings/security/ChangeScreenLockPreferenceController.java b/src/com/android/settings/security/ChangeScreenLockPreferenceController.java index 5188d1ce91..650b1e39e2 100644 --- a/src/com/android/settings/security/ChangeScreenLockPreferenceController.java +++ b/src/com/android/settings/security/ChangeScreenLockPreferenceController.java @@ -21,9 +21,10 @@ import android.content.Context; import android.os.UserHandle; import android.os.UserManager; import android.os.storage.StorageManager; +import android.text.TextUtils; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.text.TextUtils; import com.android.internal.widget.LockPatternUtils; import com.android.settings.R; @@ -35,6 +36,7 @@ import com.android.settings.password.ChooseLockGeneric; import com.android.settings.security.screenlock.ScreenLockSettings; import com.android.settings.widget.GearPreference; import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.RestrictedPreference; import com.android.settingslib.core.AbstractPreferenceController; @@ -77,14 +79,13 @@ public class ChangeScreenLockPreferenceController extends AbstractPreferenceCont @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - mPreference = (RestrictedPreference) screen.findPreference(getPreferenceKey()); + mPreference = screen.findPreference(getPreferenceKey()); } @Override public void updateState(Preference preference) { if (mPreference != null && mPreference instanceof GearPreference) { - if (mLockPatternUtils.isSecure(mUserId) - || !mLockPatternUtils.isLockScreenDisabled(mUserId)) { + if (mLockPatternUtils.isSecure(mUserId)) { ((GearPreference) mPreference).setOnGearClickListener(this); } else { ((GearPreference) mPreference).setOnGearClickListener(null); @@ -129,7 +130,7 @@ public class ChangeScreenLockPreferenceController extends AbstractPreferenceCont new SubSettingLauncher(mContext) .setDestination(ChooseLockGeneric.ChooseLockGenericFragment.class.getName()) - .setTitle(R.string.lock_settings_picker_title) + .setTitleRes(R.string.lock_settings_picker_title) .setSourceMetricsCategory(mHost.getMetricsCategory()) .launch(); return true; @@ -170,7 +171,7 @@ public class ChangeScreenLockPreferenceController extends AbstractPreferenceCont * DO or PO installed in the user may disallow to change password. */ void disableIfPasswordQualityManaged(int userId) { - final RestrictedLockUtils.EnforcedAdmin admin = RestrictedLockUtils + final RestrictedLockUtils.EnforcedAdmin admin = RestrictedLockUtilsInternal .checkIfPasswordQualityIsSet(mContext, userId); final DevicePolicyManager dpm = (DevicePolicyManager) mContext .getSystemService(Context.DEVICE_POLICY_SERVICE); diff --git a/src/com/android/settings/security/ConfigureKeyGuardDialog.java b/src/com/android/settings/security/ConfigureKeyGuardDialog.java deleted file mode 100644 index f5d9068729..0000000000 --- a/src/com/android/settings/security/ConfigureKeyGuardDialog.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (C) 2017 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.security; - -import android.app.Activity; -import android.app.AlertDialog; -import android.app.Dialog; -import android.app.admin.DevicePolicyManager; -import android.content.DialogInterface; -import android.content.Intent; -import android.os.Bundle; -import androidx.annotation.VisibleForTesting; - -import com.android.internal.logging.nano.MetricsProto; -import com.android.settings.CredentialStorage; -import com.android.settings.R; -import com.android.settings.core.instrumentation.InstrumentedDialogFragment; -import com.android.settings.password.ChooseLockGeneric; - -/** - * Prompt for key guard configuration confirmation. - */ -public class ConfigureKeyGuardDialog extends InstrumentedDialogFragment - implements DialogInterface.OnClickListener, DialogInterface.OnDismissListener { - - public static final String TAG = "ConfigureKeyGuardDialog"; - - private boolean mConfigureConfirmed; - - @Override - public int getMetricsCategory() { - return MetricsProto.MetricsEvent.CONFIGURE_KEYGUARD_DIALOG; - } - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - return new AlertDialog.Builder(getActivity()) - .setTitle(android.R.string.dialog_alert_title) - .setMessage(R.string.credentials_configure_lock_screen_hint) - .setPositiveButton(R.string.credentials_configure_lock_screen_button, this) - .setNegativeButton(android.R.string.cancel, this) - .create(); - } - - @Override - public void onClick(DialogInterface dialog, int button) { - mConfigureConfirmed = (button == DialogInterface.BUTTON_POSITIVE); - } - - @Override - public void onDismiss(DialogInterface dialog) { - if (mConfigureConfirmed) { - mConfigureConfirmed = false; - startPasswordSetup(); - return; - } else { - final Activity activity = getActivity(); - if (activity != null) { - activity.finish(); - } - } - } - - @VisibleForTesting - void startPasswordSetup() { - Intent intent = new Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD); - intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment.MINIMUM_QUALITY_KEY, - CredentialStorage.MIN_PASSWORD_QUALITY); - startActivity(intent); - } -} diff --git a/src/com/android/settings/security/CredentialStorage.java b/src/com/android/settings/security/CredentialStorage.java new file mode 100644 index 0000000000..0ea37b5bc7 --- /dev/null +++ b/src/com/android/settings/security/CredentialStorage.java @@ -0,0 +1,401 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.security; + +import android.app.Activity; +import android.app.admin.DevicePolicyManager; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.UserInfo; +import android.content.res.Resources; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Process; +import android.os.RemoteException; +import android.os.UserHandle; +import android.os.UserManager; +import android.security.Credentials; +import android.security.KeyChain; +import android.security.KeyChain.KeyChainConnection; +import android.security.KeyStore; +import android.text.TextUtils; +import android.util.Log; +import android.widget.Toast; + +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.FragmentActivity; + +import com.android.internal.widget.LockPatternUtils; +import com.android.org.bouncycastle.asn1.ASN1InputStream; +import com.android.org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import com.android.settings.R; +import com.android.settings.password.ChooseLockSettingsHelper; +import com.android.settings.vpn2.VpnUtils; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import sun.security.util.ObjectIdentifier; +import sun.security.x509.AlgorithmId; + +/** + * CredentialStorage handles resetting and installing keys into KeyStore. + */ +public final class CredentialStorage extends FragmentActivity { + + private static final String TAG = "CredentialStorage"; + + public static final String ACTION_INSTALL = "com.android.credentials.INSTALL"; + public static final String ACTION_RESET = "com.android.credentials.RESET"; + + // This is the minimum acceptable password quality. If the current password quality is + // lower than this, keystore should not be activated. + public static final int MIN_PASSWORD_QUALITY = DevicePolicyManager.PASSWORD_QUALITY_SOMETHING; + + private static final int CONFIRM_CLEAR_SYSTEM_CREDENTIAL_REQUEST = 1; + + private final KeyStore mKeyStore = KeyStore.getInstance(); + private LockPatternUtils mUtils; + + /** + * When non-null, the bundle containing credentials to install. + */ + private Bundle mInstallBundle; + + @Override + protected void onCreate(Bundle savedState) { + super.onCreate(savedState); + mUtils = new LockPatternUtils(this); + } + + @Override + protected void onResume() { + super.onResume(); + + final Intent intent = getIntent(); + final String action = intent.getAction(); + final UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE); + if (!userManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_CREDENTIALS)) { + if (ACTION_RESET.equals(action)) { + new ResetDialog(); + } else { + if (ACTION_INSTALL.equals(action) && checkCallerIsCertInstallerOrSelfInProfile()) { + mInstallBundle = intent.getExtras(); + } + handleInstall(); + } + } else { + finish(); + } + } + + /** + * Install credentials from mInstallBundle into Keystore. + */ + private void handleInstall() { + // something already decided we are done, do not proceed + if (isFinishing()) { + return; + } + if (installIfAvailable()) { + finish(); + } + } + + private boolean isHardwareBackedKey(byte[] keyData) { + try { + final ASN1InputStream bIn = new ASN1InputStream(new ByteArrayInputStream(keyData)); + final PrivateKeyInfo pki = PrivateKeyInfo.getInstance(bIn.readObject()); + final String algOid = pki.getPrivateKeyAlgorithm().getAlgorithm().getId(); + final String algName = new AlgorithmId(new ObjectIdentifier(algOid)).getName(); + + return KeyChain.isBoundKeyAlgorithm(algName); + } catch (IOException e) { + Log.e(TAG, "Failed to parse key data"); + return false; + } + } + + /** + * Install credentials if available, otherwise do nothing. + * + * @return true if the installation is done and the activity should be finished, false if + * an asynchronous task is pending and will finish the activity when it's done. + */ + private boolean installIfAvailable() { + if (mInstallBundle == null || mInstallBundle.isEmpty()) { + return true; + } + + final Bundle bundle = mInstallBundle; + mInstallBundle = null; + + final int uid = bundle.getInt(Credentials.EXTRA_INSTALL_AS_UID, KeyStore.UID_SELF); + + if (uid != KeyStore.UID_SELF && !UserHandle.isSameUser(uid, Process.myUid())) { + final int dstUserId = UserHandle.getUserId(uid); + + // Restrict install target to the wifi uid. + if (uid != Process.WIFI_UID) { + Log.e(TAG, "Failed to install credentials as uid " + uid + ": cross-user installs" + + " may only target wifi uids"); + return true; + } + + final Intent installIntent = new Intent(ACTION_INSTALL) + .setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT) + .putExtras(bundle); + startActivityAsUser(installIntent, new UserHandle(dstUserId)); + return true; + } + + boolean shouldFinish = true; + if (bundle.containsKey(Credentials.EXTRA_USER_PRIVATE_KEY_NAME)) { + final String key = bundle.getString(Credentials.EXTRA_USER_PRIVATE_KEY_NAME); + final byte[] value = bundle.getByteArray(Credentials.EXTRA_USER_PRIVATE_KEY_DATA); + + if (!mKeyStore.importKey(key, value, uid, KeyStore.FLAG_NONE)) { + Log.e(TAG, "Failed to install " + key + " as uid " + uid); + return true; + } + // The key was prepended USER_PRIVATE_KEY by the CredentialHelper. However, + // KeyChain internally uses the raw alias name and only prepends USER_PRIVATE_KEY + // to the key name when interfacing with KeyStore. + // This is generally a symptom of CredentialStorage and CredentialHelper relying + // on internal implementation details of KeyChain and imitating its functionality + // rather than delegating to KeyChain for the certificate installation. + if (uid == Process.SYSTEM_UID || uid == KeyStore.UID_SELF) { + new MarkKeyAsUserSelectable( + key.replaceFirst("^" + Credentials.USER_PRIVATE_KEY, "")).execute(); + shouldFinish = false; + } + } + + final int flags = KeyStore.FLAG_NONE; + + if (bundle.containsKey(Credentials.EXTRA_USER_CERTIFICATE_NAME)) { + final String certName = bundle.getString(Credentials.EXTRA_USER_CERTIFICATE_NAME); + final byte[] certData = bundle.getByteArray(Credentials.EXTRA_USER_CERTIFICATE_DATA); + + if (!mKeyStore.put(certName, certData, uid, flags)) { + Log.e(TAG, "Failed to install " + certName + " as uid " + uid); + return shouldFinish; + } + } + + if (bundle.containsKey(Credentials.EXTRA_CA_CERTIFICATES_NAME)) { + final String caListName = bundle.getString(Credentials.EXTRA_CA_CERTIFICATES_NAME); + final byte[] caListData = bundle.getByteArray(Credentials.EXTRA_CA_CERTIFICATES_DATA); + + if (!mKeyStore.put(caListName, caListData, uid, flags)) { + Log.e(TAG, "Failed to install " + caListName + " as uid " + uid); + return shouldFinish; + } + } + + // Send the broadcast. + final Intent broadcast = new Intent(KeyChain.ACTION_KEYCHAIN_CHANGED); + sendBroadcast(broadcast); + + setResult(RESULT_OK); + return shouldFinish; + } + + /** + * Prompt for reset confirmation, resetting on confirmation, finishing otherwise. + */ + private class ResetDialog + implements DialogInterface.OnClickListener, DialogInterface.OnDismissListener { + private boolean mResetConfirmed; + + private ResetDialog() { + final AlertDialog dialog = new AlertDialog.Builder(CredentialStorage.this) + .setTitle(android.R.string.dialog_alert_title) + .setMessage(R.string.credentials_reset_hint) + .setPositiveButton(android.R.string.ok, this) + .setNegativeButton(android.R.string.cancel, this) + .create(); + dialog.setOnDismissListener(this); + dialog.show(); + } + + @Override + public void onClick(DialogInterface dialog, int button) { + mResetConfirmed = (button == DialogInterface.BUTTON_POSITIVE); + } + + @Override + public void onDismiss(DialogInterface dialog) { + if (!mResetConfirmed) { + finish(); + return; + } + + mResetConfirmed = false; + if (!mUtils.isSecure(UserHandle.myUserId())) { + // This task will call finish() in the end. + new ResetKeyStoreAndKeyChain().execute(); + } else if (!confirmKeyGuard(CONFIRM_CLEAR_SYSTEM_CREDENTIAL_REQUEST)) { + Log.w(TAG, "Failed to launch credential confirmation for a secure user."); + finish(); + } + // Confirmation result will be handled in onActivityResult if needed. + } + } + + /** + * Background task to handle reset of both keystore and user installed CAs. + */ + private class ResetKeyStoreAndKeyChain extends AsyncTask<Void, Void, Boolean> { + + @Override + protected Boolean doInBackground(Void... unused) { + + // Clear all the users credentials could have been installed in for this user. + mUtils.resetKeyStore(UserHandle.myUserId()); + + try { + final KeyChainConnection keyChainConnection = KeyChain.bind(CredentialStorage.this); + try { + return keyChainConnection.getService().reset(); + } catch (RemoteException e) { + return false; + } finally { + keyChainConnection.close(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + } + + @Override + protected void onPostExecute(Boolean success) { + if (success) { + Toast.makeText(CredentialStorage.this, + R.string.credentials_erased, Toast.LENGTH_SHORT).show(); + clearLegacyVpnIfEstablished(); + } else { + Toast.makeText(CredentialStorage.this, + R.string.credentials_not_erased, Toast.LENGTH_SHORT).show(); + } + finish(); + } + } + + private void clearLegacyVpnIfEstablished() { + final boolean isDone = VpnUtils.disconnectLegacyVpn(getApplicationContext()); + if (isDone) { + Toast.makeText(CredentialStorage.this, R.string.vpn_disconnected, + Toast.LENGTH_SHORT).show(); + } + } + + /** + * Background task to mark a given key alias as user-selectable, so that + * it can be selected by users from the Certificate Selection prompt. + */ + private class MarkKeyAsUserSelectable extends AsyncTask<Void, Void, Boolean> { + final String mAlias; + + MarkKeyAsUserSelectable(String alias) { + mAlias = alias; + } + + @Override + protected Boolean doInBackground(Void... unused) { + try (KeyChainConnection keyChainConnection = KeyChain.bind(CredentialStorage.this)) { + keyChainConnection.getService().setUserSelectable(mAlias, true); + return true; + } catch (RemoteException e) { + Log.w(TAG, "Failed to mark key " + mAlias + " as user-selectable."); + return false; + } catch (InterruptedException e) { + Log.w(TAG, "Failed to mark key " + mAlias + " as user-selectable."); + Thread.currentThread().interrupt(); + return false; + } + } + + @Override + protected void onPostExecute(Boolean result) { + Log.i(TAG, String.format("Marked alias %s as selectable, success? %s", + mAlias, result)); + CredentialStorage.this.finish(); + } + } + + /** + * Check that the caller is either certinstaller or Settings running in a profile of this user. + */ + private boolean checkCallerIsCertInstallerOrSelfInProfile() { + if (TextUtils.equals("com.android.certinstaller", getCallingPackage())) { + // CertInstaller is allowed to install credentials if it has the same signature as + // Settings package. + return getPackageManager().checkSignatures( + getCallingPackage(), getPackageName()) == PackageManager.SIGNATURE_MATCH; + } + + final int launchedFromUserId; + try { + final int launchedFromUid = android.app.ActivityManager.getService() + .getLaunchedFromUid(getActivityToken()); + if (launchedFromUid == -1) { + Log.e(TAG, ACTION_INSTALL + " must be started with startActivityForResult"); + return false; + } + if (!UserHandle.isSameApp(launchedFromUid, Process.myUid())) { + // Not the same app + return false; + } + launchedFromUserId = UserHandle.getUserId(launchedFromUid); + } catch (RemoteException re) { + // Error talking to ActivityManager, just give up + return false; + } + + final UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE); + final UserInfo parentInfo = userManager.getProfileParent(launchedFromUserId); + // Caller is running in a profile of this user + return ((parentInfo != null) && (parentInfo.id == UserHandle.myUserId())); + } + + /** + * Confirm existing key guard, returning password via onActivityResult. + */ + private boolean confirmKeyGuard(int requestCode) { + final Resources res = getResources(); + return new ChooseLockSettingsHelper(this) + .launchConfirmationActivity(requestCode, + res.getText(R.string.credentials_title), true); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == CONFIRM_CLEAR_SYSTEM_CREDENTIAL_REQUEST) { + if (resultCode == Activity.RESULT_OK) { + new ResetKeyStoreAndKeyChain().execute(); + return; + } + // failed confirmation, bail + finish(); + } + } +} diff --git a/src/com/android/settings/security/CredentialStoragePreferenceController.java b/src/com/android/settings/security/CredentialStoragePreferenceController.java index 3bf61f63c1..060d9642e1 100644 --- a/src/com/android/settings/security/CredentialStoragePreferenceController.java +++ b/src/com/android/settings/security/CredentialStoragePreferenceController.java @@ -19,6 +19,7 @@ package com.android.settings.security; import android.content.Context; import android.os.UserManager; import android.security.KeyStore; + import androidx.preference.Preference; import com.android.settings.R; diff --git a/src/com/android/settings/security/CryptKeeperSettings.java b/src/com/android/settings/security/CryptKeeperSettings.java index 72ae0a7974..6555f56894 100644 --- a/src/com/android/settings/security/CryptKeeperSettings.java +++ b/src/com/android/settings/security/CryptKeeperSettings.java @@ -17,8 +17,8 @@ package com.android.settings.security; import android.app.Activity; -import android.app.AlertDialog; import android.app.admin.DevicePolicyManager; +import android.app.settings.SettingsEnums; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -28,14 +28,15 @@ import android.os.BatteryManager; import android.os.Bundle; import android.os.UserHandle; import android.os.storage.StorageManager; -import androidx.preference.Preference; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.appcompat.app.AlertDialog; +import androidx.preference.Preference; + import com.android.settings.CryptKeeperConfirm; import com.android.settings.R; import com.android.settings.SettingsActivity; @@ -122,7 +123,7 @@ public class CryptKeeperSettings extends InstrumentedPreferenceFragment { @Override public int getMetricsCategory() { - return MetricsEvent.CRYPT_KEEPER; + return SettingsEnums.CRYPT_KEEPER; } @Override @@ -172,7 +173,7 @@ public class CryptKeeperSettings extends InstrumentedPreferenceFragment { if (helper.utils().getKeyguardStoredPasswordQuality(UserHandle.myUserId()) == DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED) { - showFinalConfirmation(StorageManager.CRYPT_TYPE_DEFAULT, ""); + showFinalConfirmation(StorageManager.CRYPT_TYPE_DEFAULT, "".getBytes()); return true; } @@ -192,14 +193,14 @@ public class CryptKeeperSettings extends InstrumentedPreferenceFragment { // confirmation prompt; otherwise, go back to the initial state. if (resultCode == Activity.RESULT_OK && data != null) { int type = data.getIntExtra(ChooseLockSettingsHelper.EXTRA_KEY_TYPE, -1); - String password = data.getStringExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD); - if (!TextUtils.isEmpty(password)) { + byte[] password = data.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD); + if (!(password == null || password.length == 0)) { showFinalConfirmation(type, password); } } } - private void showFinalConfirmation(int type, String password) { + private void showFinalConfirmation(int type, byte[] password) { Preference preference = new Preference(getPreferenceManager().getContext()); preference.setFragment(CryptKeeperConfirm.class.getName()); preference.setTitle(R.string.crypt_keeper_confirm_title); @@ -207,16 +208,16 @@ public class CryptKeeperSettings extends InstrumentedPreferenceFragment { ((SettingsActivity) getActivity()).onPreferenceStartFragment(null, preference); } - private void addEncryptionInfoToPreference(Preference preference, int type, String password) { + private void addEncryptionInfoToPreference(Preference preference, int type, byte[] password) { Activity activity = getActivity(); DevicePolicyManager dpm = (DevicePolicyManager) activity.getSystemService(Context.DEVICE_POLICY_SERVICE); if (dpm.getDoNotAskCredentialsOnBoot()) { preference.getExtras().putInt(TYPE, StorageManager.CRYPT_TYPE_DEFAULT); - preference.getExtras().putString(PASSWORD, ""); + preference.getExtras().putByteArray(PASSWORD, "".getBytes()); } else { preference.getExtras().putInt(TYPE, type); - preference.getExtras().putString(PASSWORD, password); + preference.getExtras().putByteArray(PASSWORD, password); } } } diff --git a/src/com/android/settings/security/EncryptionAndCredential.java b/src/com/android/settings/security/EncryptionAndCredential.java index 66ae20b08c..a6d2a0ab83 100644 --- a/src/com/android/settings/security/EncryptionAndCredential.java +++ b/src/com/android/settings/security/EncryptionAndCredential.java @@ -16,20 +16,20 @@ package com.android.settings.security; -import static com.android.settings.security.EncryptionStatusPreferenceController - .PREF_KEY_ENCRYPTION_DETAIL_PAGE; +import static com.android.settings.security.EncryptionStatusPreferenceController.PREF_KEY_ENCRYPTION_DETAIL_PAGE; +import android.app.settings.SettingsEnums; import android.content.Context; import android.os.UserManager; import android.provider.SearchIndexableResource; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.widget.PreferenceCategoryController; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.Arrays; @@ -38,13 +38,14 @@ import java.util.List; /** * Encryption and Credential settings. */ +@SearchIndexable public class EncryptionAndCredential extends DashboardFragment { private static final String TAG = "EncryptionAndCredential"; @Override public int getMetricsCategory() { - return MetricsEvent.ENCRYPTION_AND_CREDENTIAL; + return SettingsEnums.ENCRYPTION_AND_CREDENTIAL; } @Override @@ -54,7 +55,7 @@ public class EncryptionAndCredential extends DashboardFragment { @Override protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { - return buildPreferenceControllers(context, getLifecycle()); + return buildPreferenceControllers(context, getSettingsLifecycle()); } @Override diff --git a/src/com/android/settings/security/EncryptionStatusPreferenceController.java b/src/com/android/settings/security/EncryptionStatusPreferenceController.java index 27e896aff3..ea38068648 100644 --- a/src/com/android/settings/security/EncryptionStatusPreferenceController.java +++ b/src/com/android/settings/security/EncryptionStatusPreferenceController.java @@ -18,9 +18,10 @@ package com.android.settings.security; import android.content.Context; import android.os.UserManager; -import androidx.preference.Preference; import android.text.TextUtils; +import androidx.preference.Preference; + import com.android.internal.widget.LockPatternUtils; import com.android.settings.R; import com.android.settings.core.BasePreferenceController; diff --git a/src/com/android/settings/security/LockUnificationPreferenceController.java b/src/com/android/settings/security/LockUnificationPreferenceController.java index ca6dc2d4ff..bf374de81c 100644 --- a/src/com/android/settings/security/LockUnificationPreferenceController.java +++ b/src/com/android/settings/security/LockUnificationPreferenceController.java @@ -27,6 +27,7 @@ import android.content.Intent; import android.os.Bundle; import android.os.UserHandle; import android.os.UserManager; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; @@ -38,10 +39,21 @@ import com.android.settings.core.SubSettingLauncher; import com.android.settings.overlay.FeatureFactory; import com.android.settings.password.ChooseLockGeneric; import com.android.settings.password.ChooseLockSettingsHelper; -import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.RestrictedSwitchPreference; import com.android.settingslib.core.AbstractPreferenceController; +/** + * Controller for password unification/un-unification flows. + * + * When password is being unified, there may be two cases: + * 1. If work password is not empty and satisfies device-wide policies (if any), it will be made + * into device-wide password. To do that we need both current device and profile passwords + * because both of them will be changed as a result. + * 2. Otherwise device-wide password is preserved. In this case we only need current profile + * password, but after unifying the passwords we proceed to ask the user for a new device + * password. + */ public class LockUnificationPreferenceController extends AbstractPreferenceController implements PreferenceControllerMixin, Preference.OnPreferenceChangeListener { @@ -50,39 +62,39 @@ public class LockUnificationPreferenceController extends AbstractPreferenceContr private static final int MY_USER_ID = UserHandle.myUserId(); private final UserManager mUm; + private final DevicePolicyManager mDpm; private final LockPatternUtils mLockPatternUtils; - private final int mProfileChallengeUserId; + private final int mProfileUserId; private final SecuritySettings mHost; private RestrictedSwitchPreference mUnifyProfile; - private String mCurrentDevicePassword; - private String mCurrentProfilePassword; + private byte[] mCurrentDevicePassword; + private byte[] mCurrentProfilePassword; + private boolean mKeepDeviceLock; @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - mUnifyProfile = (RestrictedSwitchPreference) screen.findPreference(KEY_UNIFICATION); + mUnifyProfile = screen.findPreference(KEY_UNIFICATION); } public LockUnificationPreferenceController(Context context, SecuritySettings host) { super(context); mHost = host; - mUm = (UserManager) context.getSystemService(Context.USER_SERVICE); + mUm = context.getSystemService(UserManager.class); + mDpm = context.getSystemService(DevicePolicyManager.class); mLockPatternUtils = FeatureFactory.getFactory(context) .getSecurityFeatureProvider() .getLockPatternUtils(context); - mProfileChallengeUserId = Utils.getManagedProfileId(mUm, MY_USER_ID); + mProfileUserId = Utils.getManagedProfileId(mUm, MY_USER_ID); } @Override public boolean isAvailable() { - final boolean allowSeparateProfileChallenge = - mProfileChallengeUserId != UserHandle.USER_NULL - && mLockPatternUtils.isSeparateProfileChallengeAllowed( - mProfileChallengeUserId); - return allowSeparateProfileChallenge; + return mProfileUserId != UserHandle.USER_NULL + && mLockPatternUtils.isSeparateProfileChallengeAllowed(mProfileUserId); } @Override @@ -92,18 +104,18 @@ public class LockUnificationPreferenceController extends AbstractPreferenceContr @Override public boolean onPreferenceChange(Preference preference, Object value) { - if (Utils.startQuietModeDialogIfNecessary(mContext, mUm, mProfileChallengeUserId)) { + if (Utils.startQuietModeDialogIfNecessary(mContext, mUm, mProfileUserId)) { return false; } - if ((Boolean) value) { - final boolean compliantForDevice = - (mLockPatternUtils.getKeyguardStoredPasswordQuality(mProfileChallengeUserId) - >= DevicePolicyManager.PASSWORD_QUALITY_SOMETHING - && mLockPatternUtils.isSeparateProfileChallengeAllowedToUnify( - mProfileChallengeUserId)); - UnificationConfirmationDialog dialog = - UnificationConfirmationDialog.newInstance(compliantForDevice); - dialog.show(mHost); + final boolean useOneLock = (Boolean) value; + if (useOneLock) { + // Keep current device (personal) lock if the profile lock is empty or is not compliant + // with the policy on personal side. + mKeepDeviceLock = + mLockPatternUtils.getKeyguardStoredPasswordQuality(mProfileUserId) + < DevicePolicyManager.PASSWORD_QUALITY_SOMETHING + || !mDpm.isProfileActivePasswordSufficientForParent(mProfileUserId); + UnificationConfirmationDialog.newInstance(!mKeepDeviceLock).show(mHost); } else { final String title = mContext.getString(R.string.unlock_set_unlock_launch_picker_title); final ChooseLockSettingsHelper helper = @@ -121,12 +133,12 @@ public class LockUnificationPreferenceController extends AbstractPreferenceContr public void updateState(Preference preference) { if (mUnifyProfile != null) { final boolean separate = - mLockPatternUtils.isSeparateProfileChallengeEnabled(mProfileChallengeUserId); + mLockPatternUtils.isSeparateProfileChallengeEnabled(mProfileUserId); mUnifyProfile.setChecked(!separate); if (separate) { - mUnifyProfile.setDisabledByAdmin(RestrictedLockUtils.checkIfRestrictionEnforced( - mContext, UserManager.DISALLOW_UNIFIED_PASSWORD, - mProfileChallengeUserId)); + mUnifyProfile.setDisabledByAdmin(RestrictedLockUtilsInternal + .checkIfRestrictionEnforced(mContext, UserManager.DISALLOW_UNIFIED_PASSWORD, + mProfileUserId)); } } } @@ -139,13 +151,13 @@ public class LockUnificationPreferenceController extends AbstractPreferenceContr } else if (requestCode == UNIFY_LOCK_CONFIRM_DEVICE_REQUEST && resultCode == Activity.RESULT_OK) { mCurrentDevicePassword = - data.getStringExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD); - launchConfirmProfileLockForUnification(); + data.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD); + launchConfirmProfileLock(); return true; } else if (requestCode == UNIFY_LOCK_CONFIRM_PROFILE_REQUEST && resultCode == Activity.RESULT_OK) { mCurrentProfilePassword = - data.getStringExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD); + data.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD); unifyLocks(); return true; } @@ -154,66 +166,88 @@ public class LockUnificationPreferenceController extends AbstractPreferenceContr private void ununifyLocks() { final Bundle extras = new Bundle(); - extras.putInt(Intent.EXTRA_USER_ID, mProfileChallengeUserId); + extras.putInt(Intent.EXTRA_USER_ID, mProfileUserId); new SubSettingLauncher(mContext) .setDestination(ChooseLockGeneric.ChooseLockGenericFragment.class.getName()) - .setTitle(R.string.lock_settings_picker_title_profile) + .setTitleRes(R.string.lock_settings_picker_title_profile) .setSourceMetricsCategory(mHost.getMetricsCategory()) .setArguments(extras) .launch(); } - void launchConfirmDeviceLockForUnification() { + /** Asks the user to confirm device lock (if there is one) and proceeds to ask profile lock. */ + private void launchConfirmDeviceAndProfileLock() { final String title = mContext.getString( R.string.unlock_set_unlock_launch_picker_title); final ChooseLockSettingsHelper helper = new ChooseLockSettingsHelper(mHost.getActivity(), mHost); if (!helper.launchConfirmationActivity( UNIFY_LOCK_CONFIRM_DEVICE_REQUEST, title, true, MY_USER_ID)) { - launchConfirmProfileLockForUnification(); + launchConfirmProfileLock(); } } - private void launchConfirmProfileLockForUnification() { + private void launchConfirmProfileLock() { final String title = mContext.getString( R.string.unlock_set_unlock_launch_picker_title_profile); final ChooseLockSettingsHelper helper = new ChooseLockSettingsHelper(mHost.getActivity(), mHost); if (!helper.launchConfirmationActivity( - UNIFY_LOCK_CONFIRM_PROFILE_REQUEST, title, true, mProfileChallengeUserId)) { + UNIFY_LOCK_CONFIRM_PROFILE_REQUEST, title, true, mProfileUserId)) { unifyLocks(); // TODO: update relevant prefs. // createPreferenceHierarchy(); } } + void startUnification() { + // If the device lock stays the same, only confirm profile lock. Otherwise confirm both. + if (mKeepDeviceLock) { + launchConfirmProfileLock(); + } else { + launchConfirmDeviceAndProfileLock(); + } + } + private void unifyLocks() { - int profileQuality = - mLockPatternUtils.getKeyguardStoredPasswordQuality(mProfileChallengeUserId); + if (mKeepDeviceLock) { + unifyKeepingDeviceLock(); + promptForNewDeviceLock(); + } else { + unifyKeepingWorkLock(); + } + mCurrentDevicePassword = null; + mCurrentProfilePassword = null; + } + + private void unifyKeepingWorkLock() { + final int profileQuality = + mLockPatternUtils.getKeyguardStoredPasswordQuality(mProfileUserId); + // PASSWORD_QUALITY_SOMETHING means pattern, everything above means PIN/password. if (profileQuality == DevicePolicyManager.PASSWORD_QUALITY_SOMETHING) { mLockPatternUtils.saveLockPattern( - LockPatternUtils.stringToPattern(mCurrentProfilePassword), + LockPatternUtils.byteArrayToPattern(mCurrentProfilePassword), mCurrentDevicePassword, MY_USER_ID); } else { mLockPatternUtils.saveLockPassword( - mCurrentProfilePassword, mCurrentDevicePassword, - profileQuality, MY_USER_ID); + mCurrentProfilePassword, mCurrentDevicePassword, profileQuality, MY_USER_ID); } - mLockPatternUtils.setSeparateProfileChallengeEnabled(mProfileChallengeUserId, false, + mLockPatternUtils.setSeparateProfileChallengeEnabled(mProfileUserId, false, mCurrentProfilePassword); final boolean profilePatternVisibility = - mLockPatternUtils.isVisiblePatternEnabled(mProfileChallengeUserId); + mLockPatternUtils.isVisiblePatternEnabled(mProfileUserId); mLockPatternUtils.setVisiblePatternEnabled(profilePatternVisibility, MY_USER_ID); - mCurrentDevicePassword = null; - mCurrentProfilePassword = null; } - void unifyUncompliantLocks() { - mLockPatternUtils.setSeparateProfileChallengeEnabled(mProfileChallengeUserId, false, + private void unifyKeepingDeviceLock() { + mLockPatternUtils.setSeparateProfileChallengeEnabled(mProfileUserId, false, mCurrentProfilePassword); + } + + private void promptForNewDeviceLock() { new SubSettingLauncher(mContext) .setDestination(ChooseLockGeneric.ChooseLockGenericFragment.class.getName()) - .setTitle(R.string.lock_settings_picker_title) + .setTitleRes(R.string.lock_settings_picker_title) .setSourceMetricsCategory(mHost.getMetricsCategory()) .launch(); } diff --git a/src/com/android/settings/security/LockdownButtonPreferenceController.java b/src/com/android/settings/security/LockdownButtonPreferenceController.java index d0a49d7bf6..5b29868f06 100644 --- a/src/com/android/settings/security/LockdownButtonPreferenceController.java +++ b/src/com/android/settings/security/LockdownButtonPreferenceController.java @@ -19,30 +19,25 @@ package com.android.settings.security; import android.content.Context; import android.os.UserHandle; import android.provider.Settings; -import androidx.preference.Preference; -import androidx.preference.TwoStatePreference; import com.android.internal.widget.LockPatternUtils; -import com.android.settings.core.BasePreferenceController; import com.android.settings.core.TogglePreferenceController; public class LockdownButtonPreferenceController extends TogglePreferenceController { - private static final String KEY_LOCKDOWN_ENALBED = "security_setting_lockdown_enabled"; - private final LockPatternUtils mLockPatternUtils; - public LockdownButtonPreferenceController(Context context) { - super(context, KEY_LOCKDOWN_ENALBED); + public LockdownButtonPreferenceController(Context context, String key) { + super(context, key); mLockPatternUtils = new LockPatternUtils(context); } @Override public int getAvailabilityStatus() { if (mLockPatternUtils.isSecure(UserHandle.myUserId())) { - return BasePreferenceController.AVAILABLE; + return AVAILABLE; } else { - return BasePreferenceController.DISABLED_FOR_USER; + return DISABLED_FOR_USER; } } diff --git a/src/com/android/settings/security/LockscreenDashboardFragment.java b/src/com/android/settings/security/LockscreenDashboardFragment.java index c928b8ecce..3472d4802a 100644 --- a/src/com/android/settings/security/LockscreenDashboardFragment.java +++ b/src/com/android/settings/security/LockscreenDashboardFragment.java @@ -16,18 +16,25 @@ package com.android.settings.security; +import android.app.settings.SettingsEnums; import android.content.Context; +import android.hardware.display.AmbientDisplayConfiguration; import android.provider.SearchIndexableResource; + import androidx.annotation.VisibleForTesting; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.display.AmbientDisplayAlwaysOnPreferenceController; +import com.android.settings.display.AmbientDisplayNotificationsPreferenceController; +import com.android.settings.gestures.DoubleTapScreenPreferenceController; +import com.android.settings.gestures.PickupGesturePreferenceController; import com.android.settings.notification.LockScreenNotificationPreferenceController; import com.android.settings.search.BaseSearchIndexProvider; -import com.android.settings.users.AddUserWhenLockedPreferenceController; +import com.android.settings.security.screenlock.LockScreenPreferenceController; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.Arrays; @@ -36,9 +43,12 @@ import java.util.List; /** * Settings screen for lock screen preference */ +@SearchIndexable public class LockscreenDashboardFragment extends DashboardFragment implements OwnerInfoPreferenceController.OwnerInfoCallback { + public static final String KEY_AMBIENT_DISPLAY_ALWAYS_ON = "ambient_display_always_on"; + private static final String TAG = "LockscreenDashboardFragment"; @VisibleForTesting @@ -53,11 +63,13 @@ public class LockscreenDashboardFragment extends DashboardFragment static final String KEY_ADD_USER_FROM_LOCK_SCREEN = "security_lockscreen_add_users_when_locked"; + + private AmbientDisplayConfiguration mConfig; private OwnerInfoPreferenceController mOwnerInfoPreferenceController; @Override public int getMetricsCategory() { - return MetricsEvent.SETTINGS_LOCK_SCREEN_PREFERENCES; + return SettingsEnums.SETTINGS_LOCK_SCREEN_PREFERENCES; } @Override @@ -76,9 +88,20 @@ public class LockscreenDashboardFragment extends DashboardFragment } @Override + public void onAttach(Context context) { + super.onAttach(context); + use(AmbientDisplayAlwaysOnPreferenceController.class) + .setConfig(getConfig(context)) + .setCallback(this::updatePreferenceStates); + use(AmbientDisplayNotificationsPreferenceController.class).setConfig(getConfig(context)); + use(DoubleTapScreenPreferenceController.class).setConfig(getConfig(context)); + use(PickupGesturePreferenceController.class).setConfig(getConfig(context)); + } + + @Override protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { final List<AbstractPreferenceController> controllers = new ArrayList<>(); - final Lifecycle lifecycle = getLifecycle(); + final Lifecycle lifecycle = getSettingsLifecycle(); final LockScreenNotificationPreferenceController notificationController = new LockScreenNotificationPreferenceController(context, KEY_LOCK_SCREEN_NOTIFICATON, @@ -86,12 +109,9 @@ public class LockscreenDashboardFragment extends DashboardFragment KEY_LOCK_SCREEN_NOTIFICATON_WORK_PROFILE); lifecycle.addObserver(notificationController); controllers.add(notificationController); - controllers.add(new AddUserWhenLockedPreferenceController( - context, KEY_ADD_USER_FROM_LOCK_SCREEN, lifecycle)); mOwnerInfoPreferenceController = new OwnerInfoPreferenceController(context, this, lifecycle); controllers.add(mOwnerInfoPreferenceController); - controllers.add(new LockdownButtonPreferenceController(context)); return controllers; } @@ -103,6 +123,13 @@ public class LockscreenDashboardFragment extends DashboardFragment } } + private AmbientDisplayConfiguration getConfig(Context context) { + if (mConfig == null) { + mConfig = new AmbientDisplayConfiguration(context); + } + return mConfig; + } + public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = new BaseSearchIndexProvider() { @Override @@ -118,11 +145,8 @@ public class LockscreenDashboardFragment extends DashboardFragment Context context) { final List<AbstractPreferenceController> controllers = new ArrayList<>(); controllers.add(new LockScreenNotificationPreferenceController(context)); - controllers.add(new AddUserWhenLockedPreferenceController(context, - KEY_ADD_USER_FROM_LOCK_SCREEN, null /* lifecycle */)); controllers.add(new OwnerInfoPreferenceController( context, null /* fragment */, null /* lifecycle */)); - controllers.add(new LockdownButtonPreferenceController(context)); return controllers; } @@ -130,8 +154,13 @@ public class LockscreenDashboardFragment extends DashboardFragment public List<String> getNonIndexableKeys(Context context) { final List<String> niks = super.getNonIndexableKeys(context); niks.add(KEY_ADD_USER_FROM_LOCK_SCREEN); - niks.add(KEY_LOCK_SCREEN_NOTIFICATON_WORK_PROFILE); return niks; } + + @Override + protected boolean isPageSearchEnabled(Context context) { + return new LockScreenPreferenceController(context, "anykey") + .isAvailable(); + } }; } diff --git a/src/com/android/settings/security/OwnerInfoPreferenceController.java b/src/com/android/settings/security/OwnerInfoPreferenceController.java index 01a02900e5..5276722f22 100644 --- a/src/com/android/settings/security/OwnerInfoPreferenceController.java +++ b/src/com/android/settings/security/OwnerInfoPreferenceController.java @@ -15,10 +15,11 @@ */ package com.android.settings.security; -import android.app.Fragment; import android.content.Context; import android.os.UserHandle; + import androidx.annotation.VisibleForTesting; +import androidx.fragment.app.Fragment; import androidx.preference.Preference; import androidx.preference.Preference.OnPreferenceClickListener; import androidx.preference.PreferenceScreen; @@ -26,8 +27,8 @@ import androidx.preference.PreferenceScreen; import com.android.internal.widget.LockPatternUtils; import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.users.OwnerInfoSettings; -import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; +import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.RestrictedPreference; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; @@ -60,7 +61,7 @@ public class OwnerInfoPreferenceController extends AbstractPreferenceController @Override public void displayPreference(PreferenceScreen screen) { - mOwnerInfoPref = (RestrictedPreference) screen.findPreference(KEY_OWNER_INFO); + mOwnerInfoPref = screen.findPreference(KEY_OWNER_INFO); } @Override @@ -139,6 +140,6 @@ public class OwnerInfoPreferenceController extends AbstractPreferenceController @VisibleForTesting EnforcedAdmin getDeviceOwner() { - return RestrictedLockUtils.getDeviceOwner(mContext); + return RestrictedLockUtilsInternal.getDeviceOwner(mContext); } } diff --git a/src/com/android/settings/security/ResetCredentialsPreferenceController.java b/src/com/android/settings/security/ResetCredentialsPreferenceController.java index d77fa62860..0700b462c8 100644 --- a/src/com/android/settings/security/ResetCredentialsPreferenceController.java +++ b/src/com/android/settings/security/ResetCredentialsPreferenceController.java @@ -19,6 +19,7 @@ package com.android.settings.security; import android.content.Context; import android.os.UserManager; import android.security.KeyStore; + import androidx.preference.PreferenceScreen; import com.android.settingslib.RestrictedPreference; @@ -51,7 +52,7 @@ public class ResetCredentialsPreferenceController extends RestrictedEncryptionPr @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - mPreference = (RestrictedPreference) screen.findPreference(getPreferenceKey()); + mPreference = screen.findPreference(getPreferenceKey()); } @Override diff --git a/src/com/android/settings/security/ScreenPinningSettings.java b/src/com/android/settings/security/ScreenPinningSettings.java index 0fb426b830..c60fd472a9 100644 --- a/src/com/android/settings/security/ScreenPinningSettings.java +++ b/src/com/android/settings/security/ScreenPinningSettings.java @@ -16,30 +16,31 @@ package com.android.settings.security; import android.app.admin.DevicePolicyManager; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.UserHandle; import android.provider.SearchIndexableResource; import android.provider.Settings; -import androidx.preference.SwitchPreference; -import androidx.preference.Preference; -import androidx.preference.Preference.OnPreferenceChangeListener; -import androidx.preference.PreferenceScreen; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Switch; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.preference.Preference; +import androidx.preference.Preference.OnPreferenceChangeListener; +import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreference; + import com.android.internal.widget.LockPatternUtils; import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.SettingsPreferenceFragment; import com.android.settings.password.ChooseLockGeneric; import com.android.settings.search.BaseSearchIndexProvider; -import com.android.settings.search.Indexable; import com.android.settings.widget.SwitchBar; +import com.android.settingslib.search.SearchIndexable; import java.util.Arrays; import java.util.List; @@ -47,8 +48,9 @@ import java.util.List; /** * Screen pinning settings. */ +@SearchIndexable public class ScreenPinningSettings extends SettingsPreferenceFragment - implements SwitchBar.OnSwitchChangeListener, Indexable { + implements SwitchBar.OnSwitchChangeListener { private static final CharSequence KEY_USE_SCREEN_LOCK = "use_screen_lock"; private static final int CHANGE_LOCK_METHOD_REQUEST = 43; @@ -59,7 +61,7 @@ public class ScreenPinningSettings extends SettingsPreferenceFragment @Override public int getMetricsCategory() { - return MetricsEvent.SCREEN_PINNING; + return SettingsEnums.SCREEN_PINNING; } @Override diff --git a/src/com/android/settings/security/SecuritySettings.java b/src/com/android/settings/security/SecuritySettings.java index 0839450a93..7c3391c0b5 100644 --- a/src/com/android/settings/security/SecuritySettings.java +++ b/src/com/android/settings/security/SecuritySettings.java @@ -15,36 +15,32 @@ */ package com.android.settings.security; -import static com.android.settings.security.EncryptionStatusPreferenceController - .PREF_KEY_ENCRYPTION_SECURITY_PAGE; +import static com.android.settings.security.EncryptionStatusPreferenceController.PREF_KEY_ENCRYPTION_SECURITY_PAGE; -import android.app.Activity; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; -import android.hardware.fingerprint.FingerprintManager; import android.provider.SearchIndexableResource; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; -import com.android.settings.Utils; +import com.android.settings.biometrics.face.FaceProfileStatusPreferenceController; +import com.android.settings.biometrics.face.FaceStatusPreferenceController; +import com.android.settings.biometrics.fingerprint.FingerprintProfileStatusPreferenceController; +import com.android.settings.biometrics.fingerprint.FingerprintStatusPreferenceController; import com.android.settings.dashboard.DashboardFragment; -import com.android.settings.dashboard.SummaryLoader; import com.android.settings.enterprise.EnterprisePrivacyPreferenceController; -import com.android.settings.enterprise.ManageDeviceAdminPreferenceController; -import com.android.settings.fingerprint.FingerprintProfileStatusPreferenceController; -import com.android.settings.fingerprint.FingerprintStatusPreferenceController; -import com.android.settings.location.LocationPreferenceController; import com.android.settings.search.BaseSearchIndexProvider; -import com.android.settings.security.screenlock.LockScreenPreferenceController; import com.android.settings.security.trustagent.ManageTrustAgentsPreferenceController; import com.android.settings.security.trustagent.TrustAgentListPreferenceController; import com.android.settings.widget.PreferenceCategoryController; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.List; +@SearchIndexable public class SecuritySettings extends DashboardFragment { private static final String TAG = "SecuritySettings"; @@ -58,7 +54,7 @@ public class SecuritySettings extends DashboardFragment { @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.SECURITY; + return SettingsEnums.SECURITY; } @Override @@ -78,7 +74,7 @@ public class SecuritySettings extends DashboardFragment { @Override protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { - return buildPreferenceControllers(context, getLifecycle(), this /* host*/); + return buildPreferenceControllers(context, getSettingsLifecycle(), this /* host*/); } /** @@ -97,13 +93,8 @@ public class SecuritySettings extends DashboardFragment { super.onActivityResult(requestCode, resultCode, data); } - void launchConfirmDeviceLockForUnification() { - use(LockUnificationPreferenceController.class) - .launchConfirmDeviceLockForUnification(); - } - - void unifyUncompliantLocks() { - use(LockUnificationPreferenceController.class).unifyUncompliantLocks(); + void startUnification() { + use(LockUnificationPreferenceController.class).startUnification(); } void updateUnificationPreference() { @@ -113,20 +104,17 @@ public class SecuritySettings extends DashboardFragment { private static List<AbstractPreferenceController> buildPreferenceControllers(Context context, Lifecycle lifecycle, SecuritySettings host) { final List<AbstractPreferenceController> controllers = new ArrayList<>(); - controllers.add(new LocationPreferenceController(context, lifecycle)); - controllers.add(new ManageDeviceAdminPreferenceController(context)); controllers.add(new EnterprisePrivacyPreferenceController(context)); controllers.add(new ManageTrustAgentsPreferenceController(context)); controllers.add(new ScreenPinningPreferenceController(context)); controllers.add(new SimLockPreferenceController(context)); - controllers.add(new ShowPasswordPreferenceController(context)); controllers.add(new EncryptionStatusPreferenceController(context, PREF_KEY_ENCRYPTION_SECURITY_PAGE)); controllers.add(new TrustAgentListPreferenceController(context, host, lifecycle)); final List<AbstractPreferenceController> securityPreferenceControllers = new ArrayList<>(); + securityPreferenceControllers.add(new FaceStatusPreferenceController(context)); securityPreferenceControllers.add(new FingerprintStatusPreferenceController(context)); - securityPreferenceControllers.add(new LockScreenPreferenceController(context, lifecycle)); securityPreferenceControllers.add(new ChangeScreenLockPreferenceController(context, host)); controllers.add(new PreferenceCategoryController(context, SECURITY_CATEGORY) .setChildren(securityPreferenceControllers)); @@ -138,6 +126,7 @@ public class SecuritySettings extends DashboardFragment { profileSecurityControllers.add(new LockUnificationPreferenceController(context, host)); profileSecurityControllers.add(new VisiblePatternProfilePreferenceController( context, lifecycle)); + profileSecurityControllers.add(new FaceProfileStatusPreferenceController(context)); profileSecurityControllers.add(new FingerprintProfileStatusPreferenceController(context)); controllers.add(new PreferenceCategoryController(context, WORK_PROFILE_SECURITY_CATEGORY) .setChildren(profileSecurityControllers)); @@ -170,39 +159,4 @@ public class SecuritySettings extends DashboardFragment { null /* host*/); } }; - - static class SummaryProvider implements SummaryLoader.SummaryProvider { - - private final Context mContext; - private final SummaryLoader mSummaryLoader; - - public SummaryProvider(Context context, SummaryLoader summaryLoader) { - mContext = context; - mSummaryLoader = summaryLoader; - } - - @Override - public void setListening(boolean listening) { - if (listening) { - final FingerprintManager fpm = - Utils.getFingerprintManagerOrNull(mContext); - if (fpm != null && fpm.isHardwareDetected()) { - mSummaryLoader.setSummary(this, - mContext.getString(R.string.security_dashboard_summary)); - } else { - mSummaryLoader.setSummary(this, mContext.getString( - R.string.security_dashboard_summary_no_fingerprint)); - } - } - } - } - - public static final SummaryLoader.SummaryProviderFactory SUMMARY_PROVIDER_FACTORY = - new SummaryLoader.SummaryProviderFactory() { - @Override - public SummaryLoader.SummaryProvider createSummaryProvider(Activity activity, - SummaryLoader summaryLoader) { - return new SummaryProvider(activity, summaryLoader); - } - }; } diff --git a/src/com/android/settings/security/ShowPasswordPreferenceController.java b/src/com/android/settings/security/ShowPasswordPreferenceController.java index 8672974fd2..472101bfc5 100644 --- a/src/com/android/settings/security/ShowPasswordPreferenceController.java +++ b/src/com/android/settings/security/ShowPasswordPreferenceController.java @@ -21,9 +21,9 @@ import android.os.UserHandle; import android.provider.Settings; import com.android.internal.widget.LockPatternUtils; +import com.android.settings.R; import com.android.settings.core.TogglePreferenceController; import com.android.settings.overlay.FeatureFactory; -import com.android.settings.R; public class ShowPasswordPreferenceController extends TogglePreferenceController { diff --git a/src/com/android/settings/security/SimLockPreferenceController.java b/src/com/android/settings/security/SimLockPreferenceController.java index 683b99a3bf..d4def6bedb 100644 --- a/src/com/android/settings/security/SimLockPreferenceController.java +++ b/src/com/android/settings/security/SimLockPreferenceController.java @@ -19,13 +19,14 @@ package com.android.settings.security; import android.content.Context; import android.os.PersistableBundle; import android.os.UserManager; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; import android.telephony.CarrierConfigManager; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + import com.android.settings.core.BasePreferenceController; import java.util.List; @@ -76,7 +77,7 @@ public class SimLockPreferenceController extends BasePreferenceController { */ private boolean isSimReady() { final List<SubscriptionInfo> subInfoList = - mSubscriptionManager.getActiveSubscriptionInfoList(); + mSubscriptionManager.getActiveSubscriptionInfoList(true); if (subInfoList != null) { for (SubscriptionInfo subInfo : subInfoList) { final int simState = mTelephonyManager.getSimState(subInfo.getSimSlotIndex()); @@ -94,7 +95,7 @@ public class SimLockPreferenceController extends BasePreferenceController { */ private boolean isSimIccReady() { final List<SubscriptionInfo> subInfoList = - mSubscriptionManager.getActiveSubscriptionInfoList(); + mSubscriptionManager.getActiveSubscriptionInfoList(true); if (subInfoList != null) { for (SubscriptionInfo subInfo : subInfoList) { diff --git a/src/com/android/settings/security/TopLevelSecurityEntryPreferenceController.java b/src/com/android/settings/security/TopLevelSecurityEntryPreferenceController.java new file mode 100644 index 0000000000..4b00424068 --- /dev/null +++ b/src/com/android/settings/security/TopLevelSecurityEntryPreferenceController.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2018 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.security; + +import android.content.Context; +import android.hardware.face.FaceManager; +import android.hardware.fingerprint.FingerprintManager; + +import com.android.settings.R; +import com.android.settings.Utils; +import com.android.settings.core.BasePreferenceController; + +public class TopLevelSecurityEntryPreferenceController extends BasePreferenceController { + + public TopLevelSecurityEntryPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE_UNSEARCHABLE; + } + + @Override + public CharSequence getSummary() { + final FingerprintManager fpm = + Utils.getFingerprintManagerOrNull(mContext); + final FaceManager faceManager = + Utils.getFaceManagerOrNull(mContext); + if (faceManager != null && faceManager.isHardwareDetected()) { + return mContext.getText(R.string.security_dashboard_summary_face); + } else if (fpm != null && fpm.isHardwareDetected()) { + return mContext.getText(R.string.security_dashboard_summary); + } else { + return mContext.getText(R.string.security_dashboard_summary_no_fingerprint); + } + } +} diff --git a/src/com/android/settings/security/UnificationConfirmationDialog.java b/src/com/android/settings/security/UnificationConfirmationDialog.java index 029e64f3fe..d8638bc5df 100644 --- a/src/com/android/settings/security/UnificationConfirmationDialog.java +++ b/src/com/android/settings/security/UnificationConfirmationDialog.java @@ -16,13 +16,14 @@ package com.android.settings.security; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.FragmentManager; +import android.app.settings.SettingsEnums; import android.content.DialogInterface; import android.os.Bundle; -import com.android.internal.logging.nano.MetricsProto; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.FragmentManager; + import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; @@ -59,15 +60,8 @@ public class UnificationConfirmationDialog extends InstrumentedDialogFragment { .setPositiveButton( compliant ? R.string.lock_settings_profile_unification_dialog_confirm : R.string - .lock_settings_profile_unification_dialog_uncompliant_confirm, - (dialog, whichButton) -> { - if (compliant) { - parentFragment.launchConfirmDeviceLockForUnification(); - } else { - parentFragment.unifyUncompliantLocks(); - } - } - ) + .lock_settings_profile_unification_dialog_uncompliant_confirm, + (dialog, whichButton) -> parentFragment.startUnification()) .setNegativeButton(R.string.cancel, null) .create(); } @@ -80,6 +74,6 @@ public class UnificationConfirmationDialog extends InstrumentedDialogFragment { @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DIALOG_UNIFICATION_CONFIRMATION; + return SettingsEnums.DIALOG_UNIFICATION_CONFIRMATION; } } diff --git a/src/com/android/settings/security/VisiblePatternProfilePreferenceController.java b/src/com/android/settings/security/VisiblePatternProfilePreferenceController.java index 1c71975bf8..2db8c24119 100644 --- a/src/com/android/settings/security/VisiblePatternProfilePreferenceController.java +++ b/src/com/android/settings/security/VisiblePatternProfilePreferenceController.java @@ -16,14 +16,16 @@ package com.android.settings.security; + import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_SOMETHING; import android.content.Context; import android.os.UserHandle; import android.os.UserManager; +import android.util.Log; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.util.Log; import com.android.internal.widget.LockPatternUtils; import com.android.settings.Utils; diff --git a/src/com/android/settings/security/screenlock/LockAfterTimeoutPreferenceController.java b/src/com/android/settings/security/screenlock/LockAfterTimeoutPreferenceController.java index c2bd38bcef..999c945e87 100644 --- a/src/com/android/settings/security/screenlock/LockAfterTimeoutPreferenceController.java +++ b/src/com/android/settings/security/screenlock/LockAfterTimeoutPreferenceController.java @@ -22,17 +22,19 @@ import android.app.admin.DevicePolicyManager; import android.content.Context; import android.os.UserHandle; import android.provider.Settings; -import androidx.preference.Preference; import android.text.TextUtils; import android.util.Log; +import androidx.preference.Preference; + import com.android.internal.widget.LockPatternUtils; import com.android.settings.R; -import com.android.settings.TimeoutListPreference; import com.android.settings.core.PreferenceControllerMixin; +import com.android.settings.display.TimeoutListPreference; import com.android.settings.overlay.FeatureFactory; import com.android.settings.security.trustagent.TrustAgentManager; import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.core.AbstractPreferenceController; public class LockAfterTimeoutPreferenceController extends AbstractPreferenceController @@ -105,7 +107,7 @@ public class LockAfterTimeoutPreferenceController extends AbstractPreferenceCont preference.setValue(String.valueOf(currentTimeout)); if (mDPM != null) { final RestrictedLockUtils.EnforcedAdmin admin = - RestrictedLockUtils.checkIfMaximumTimeToLockIsSet(mContext); + RestrictedLockUtilsInternal.checkIfMaximumTimeToLockIsSet(mContext); final long adminTimeout = mDPM.getMaximumTimeToLock(null /* admin */, UserHandle.myUserId()); final long displayTimeout = Math.max(0, diff --git a/src/com/android/settings/security/screenlock/LockScreenPreferenceController.java b/src/com/android/settings/security/screenlock/LockScreenPreferenceController.java index 1624f12cbb..7d83f4011e 100644 --- a/src/com/android/settings/security/screenlock/LockScreenPreferenceController.java +++ b/src/com/android/settings/security/screenlock/LockScreenPreferenceController.java @@ -20,6 +20,7 @@ import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED import android.content.Context; import android.os.UserHandle; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; @@ -27,26 +28,20 @@ import com.android.internal.widget.LockPatternUtils; import com.android.settings.core.BasePreferenceController; import com.android.settings.notification.LockScreenNotificationPreferenceController; import com.android.settings.overlay.FeatureFactory; -import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnResume; public class LockScreenPreferenceController extends BasePreferenceController implements LifecycleObserver, OnResume { - static final String KEY_LOCKSCREEN_PREFERENCES = "lockscreen_preferences"; - private static final int MY_USER_ID = UserHandle.myUserId(); private final LockPatternUtils mLockPatternUtils; private Preference mPreference; - public LockScreenPreferenceController(Context context, Lifecycle lifecycle) { - super(context, KEY_LOCKSCREEN_PREFERENCES); + public LockScreenPreferenceController(Context context, String key) { + super(context, key); mLockPatternUtils = FeatureFactory.getFactory(context) .getSecurityFeatureProvider().getLockPatternUtils(context); - if (lifecycle != null) { - lifecycle.addObserver(this); - } } @Override @@ -59,11 +54,11 @@ public class LockScreenPreferenceController extends BasePreferenceController imp public int getAvailabilityStatus() { if (!mLockPatternUtils.isSecure(MY_USER_ID)) { return mLockPatternUtils.isLockScreenDisabled(MY_USER_ID) - ? DISABLED_FOR_USER : AVAILABLE; + ? DISABLED_FOR_USER : AVAILABLE_UNSEARCHABLE; } else { return mLockPatternUtils.getKeyguardStoredPasswordQuality(MY_USER_ID) == PASSWORD_QUALITY_UNSPECIFIED - ? DISABLED_FOR_USER : AVAILABLE; + ? DISABLED_FOR_USER : AVAILABLE_UNSEARCHABLE; } } diff --git a/src/com/android/settings/security/screenlock/PatternVisiblePreferenceController.java b/src/com/android/settings/security/screenlock/PatternVisiblePreferenceController.java index 5f436803c8..133078c9ee 100644 --- a/src/com/android/settings/security/screenlock/PatternVisiblePreferenceController.java +++ b/src/com/android/settings/security/screenlock/PatternVisiblePreferenceController.java @@ -18,6 +18,7 @@ package com.android.settings.security.screenlock; import android.app.admin.DevicePolicyManager; import android.content.Context; + import androidx.preference.Preference; import androidx.preference.TwoStatePreference; diff --git a/src/com/android/settings/security/screenlock/PowerButtonInstantLockPreferenceController.java b/src/com/android/settings/security/screenlock/PowerButtonInstantLockPreferenceController.java index 6421bd9fe9..ffd01830da 100644 --- a/src/com/android/settings/security/screenlock/PowerButtonInstantLockPreferenceController.java +++ b/src/com/android/settings/security/screenlock/PowerButtonInstantLockPreferenceController.java @@ -18,9 +18,10 @@ package com.android.settings.security.screenlock; import android.app.admin.DevicePolicyManager; import android.content.Context; +import android.text.TextUtils; + import androidx.preference.Preference; import androidx.preference.TwoStatePreference; -import android.text.TextUtils; import com.android.internal.widget.LockPatternUtils; import com.android.settings.R; diff --git a/src/com/android/settings/security/screenlock/ScreenLockSettings.java b/src/com/android/settings/security/screenlock/ScreenLockSettings.java index 2fa4124f95..3fa9dd86c1 100644 --- a/src/com/android/settings/security/screenlock/ScreenLockSettings.java +++ b/src/com/android/settings/security/screenlock/ScreenLockSettings.java @@ -16,12 +16,13 @@ package com.android.settings.security.screenlock; -import android.app.Fragment; +import android.app.settings.SettingsEnums; import android.content.Context; import android.os.UserHandle; import android.provider.SearchIndexableResource; -import com.android.internal.logging.nano.MetricsProto; +import androidx.fragment.app.Fragment; + import com.android.internal.widget.LockPatternUtils; import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; @@ -30,23 +31,23 @@ import com.android.settings.search.Indexable; import com.android.settings.security.OwnerInfoPreferenceController; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.List; +@SearchIndexable public class ScreenLockSettings extends DashboardFragment implements OwnerInfoPreferenceController.OwnerInfoCallback { private static final String TAG = "ScreenLockSettings"; - private static final String KEY_LOCK_SCREEN_TITLE = "security_settings_password_sub_screen"; - private static final int MY_USER_ID = UserHandle.myUserId(); private LockPatternUtils mLockPatternUtils; @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.SCREEN_LOCK_SETTINGS; + return SettingsEnums.SCREEN_LOCK_SETTINGS; } @Override @@ -62,7 +63,7 @@ public class ScreenLockSettings extends DashboardFragment @Override protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { mLockPatternUtils = new LockPatternUtils(context); - return buildPreferenceControllers(context, this /* parent */, getLifecycle(), + return buildPreferenceControllers(context, this /* parent */, getSettingsLifecycle(), mLockPatternUtils); } @@ -104,12 +105,5 @@ public class ScreenLockSettings extends DashboardFragment return buildPreferenceControllers(context, null /* parent */, null /* lifecycle */, new LockPatternUtils(context)); } - - @Override - public List<String> getNonIndexableKeys(Context context) { - final List<String> keys = super.getNonIndexableKeys(context); - keys.add(KEY_LOCK_SCREEN_TITLE); - return keys; - } }; } diff --git a/src/com/android/settings/security/trustagent/ManageTrustAgentsPreferenceController.java b/src/com/android/settings/security/trustagent/ManageTrustAgentsPreferenceController.java index 3d92bdea47..056c1f293c 100644 --- a/src/com/android/settings/security/trustagent/ManageTrustAgentsPreferenceController.java +++ b/src/com/android/settings/security/trustagent/ManageTrustAgentsPreferenceController.java @@ -18,6 +18,7 @@ package com.android.settings.security.trustagent; import android.content.Context; import android.os.UserHandle; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; diff --git a/src/com/android/settings/security/trustagent/TrustAgentInfo.java b/src/com/android/settings/security/trustagent/TrustAgentInfo.java new file mode 100644 index 0000000000..f819802127 --- /dev/null +++ b/src/com/android/settings/security/trustagent/TrustAgentInfo.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2018 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.security.trustagent; + +import android.content.ComponentName; +import android.graphics.drawable.Drawable; + +public class TrustAgentInfo implements Comparable<TrustAgentInfo> { + private final CharSequence mLabel; + private final ComponentName mComponentName; + private final Drawable mIcon; + + public TrustAgentInfo(CharSequence label, ComponentName componentName, Drawable icon) { + mLabel = label; + mComponentName = componentName; + mIcon = icon; + } + + public CharSequence getLabel() { + return mLabel; + } + + public ComponentName getComponentName() { + return mComponentName; + } + + public Drawable getIcon() { + return mIcon; + } + + @Override + public boolean equals(Object other) { + if (other instanceof TrustAgentInfo) { + return mComponentName.equals(((TrustAgentInfo) other).getComponentName()); + } + return false; + } + + @Override + public int compareTo(TrustAgentInfo other) { + return mComponentName.compareTo(other.getComponentName()); + } +}
\ No newline at end of file diff --git a/src/com/android/settings/security/trustagent/TrustAgentListPreferenceController.java b/src/com/android/settings/security/trustagent/TrustAgentListPreferenceController.java index 89d8de7b04..eb50b7c0b3 100644 --- a/src/com/android/settings/security/trustagent/TrustAgentListPreferenceController.java +++ b/src/com/android/settings/security/trustagent/TrustAgentListPreferenceController.java @@ -23,11 +23,12 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.UserHandle; +import android.text.TextUtils; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceScreen; -import android.text.TextUtils; import com.android.internal.widget.LockPatternUtils; import com.android.settings.R; @@ -90,7 +91,7 @@ public class TrustAgentListPreferenceController extends AbstractPreferenceContro @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - mSecurityCategory = (PreferenceCategory) screen.findPreference(PREF_KEY_SECURITY_CATEGORY); + mSecurityCategory = screen.findPreference(PREF_KEY_SECURITY_CATEGORY); updateTrustAgents(); } diff --git a/src/com/android/settings/security/trustagent/TrustAgentManager.java b/src/com/android/settings/security/trustagent/TrustAgentManager.java index 7dcb832b9b..f5c693aba2 100644 --- a/src/com/android/settings/security/trustagent/TrustAgentManager.java +++ b/src/com/android/settings/security/trustagent/TrustAgentManager.java @@ -29,15 +29,17 @@ import android.content.res.TypedArray; import android.content.res.XmlResourceParser; import android.os.UserHandle; import android.service.trust.TrustAgentService; -import androidx.annotation.VisibleForTesting; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; import android.util.Slog; import android.util.Xml; +import androidx.annotation.VisibleForTesting; + import com.android.internal.widget.LockPatternUtils; import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -51,7 +53,7 @@ import java.util.List; public class TrustAgentManager { // Only allow one trust agent on the platform. - private static final boolean ONLY_ONE_TRUST_AGENT = true; + private static final boolean ONLY_ONE_TRUST_AGENT = false; public static class TrustAgentComponentInfo { public ComponentName componentName; @@ -112,7 +114,7 @@ public class TrustAgentManager { final List<ResolveInfo> resolveInfos = pm.queryIntentServices(TRUST_AGENT_INTENT, PackageManager.GET_META_DATA); final List<ComponentName> enabledTrustAgents = utils.getEnabledTrustAgents(myUserId); - final RestrictedLockUtils.EnforcedAdmin admin = RestrictedLockUtils + final RestrictedLockUtils.EnforcedAdmin admin = RestrictedLockUtilsInternal .checkIfKeyguardFeaturesDisabled( context, DevicePolicyManager.KEYGUARD_DISABLE_TRUST_AGENTS, myUserId); diff --git a/src/com/android/settings/security/trustagent/TrustAgentSettings.java b/src/com/android/settings/security/trustagent/TrustAgentSettings.java index 02d354a78f..36a3867e42 100644 --- a/src/com/android/settings/security/trustagent/TrustAgentSettings.java +++ b/src/com/android/settings/security/trustagent/TrustAgentSettings.java @@ -16,66 +16,26 @@ package com.android.settings.security.trustagent; -import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; - -import android.app.admin.DevicePolicyManager; -import android.content.ComponentName; +import android.app.settings.SettingsEnums; import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.os.UserHandle; -import android.service.trust.TrustAgentService; -import androidx.preference.SwitchPreference; -import androidx.preference.Preference; -import androidx.preference.PreferenceGroup; -import android.util.ArrayMap; -import android.util.ArraySet; +import android.provider.SearchIndexableResource; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.internal.widget.LockPatternUtils; import com.android.settings.R; -import com.android.settings.SettingsPreferenceFragment; -import com.android.settings.overlay.FeatureFactory; -import com.android.settingslib.RestrictedLockUtils; -import com.android.settingslib.RestrictedSwitchPreference; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; +import com.android.settingslib.search.SearchIndexable; +import java.util.ArrayList; import java.util.List; -public class TrustAgentSettings extends SettingsPreferenceFragment implements - Preference.OnPreferenceChangeListener { - private static final String SERVICE_INTERFACE = TrustAgentService.SERVICE_INTERFACE; - - private ArrayMap<ComponentName, AgentInfo> mAvailableAgents; - private final ArraySet<ComponentName> mActiveAgents = new ArraySet<ComponentName>(); - private LockPatternUtils mLockPatternUtils; - private DevicePolicyManager mDpm; - private TrustAgentManager mTrustAgentManager; - - public static final class AgentInfo { - CharSequence label; - ComponentName component; // service that implements ITrustAgent - SwitchPreference preference; - public Drawable icon; - - @Override - public boolean equals(Object other) { - if (other instanceof AgentInfo) { - return component.equals(((AgentInfo)other).component); - } - return true; - } - - public int compareTo(AgentInfo other) { - return component.compareTo(other.component); - } - } +@SearchIndexable +public class TrustAgentSettings extends DashboardFragment { + private static final String TAG = "TrustAgentSettings"; @Override public int getMetricsCategory() { - return MetricsEvent.TRUST_AGENT; + return SettingsEnums.TRUST_AGENT; } @Override @@ -84,122 +44,25 @@ public class TrustAgentSettings extends SettingsPreferenceFragment implements } @Override - public void onCreate(Bundle icicle) { - super.onCreate(icicle); - mDpm = getActivity().getSystemService(DevicePolicyManager.class); - mTrustAgentManager = - FeatureFactory.getFactory(getActivity()).getSecurityFeatureProvider() - .getTrustAgentManager(); - - addPreferencesFromResource(R.xml.trust_agent_settings); - } - - public void onResume() { - super.onResume(); - removePreference("dummy_preference"); - updateAgents(); - } - - private void updateAgents() { - final Context context = getActivity(); - if (mAvailableAgents == null) { - mAvailableAgents = findAvailableTrustAgents(); - } - if (mLockPatternUtils == null) { - mLockPatternUtils = new LockPatternUtils(getActivity()); - } - loadActiveAgents(); - PreferenceGroup category = - (PreferenceGroup) getPreferenceScreen().findPreference("trust_agents"); - category.removeAll(); - - final EnforcedAdmin admin = RestrictedLockUtils.checkIfKeyguardFeaturesDisabled(context, - DevicePolicyManager.KEYGUARD_DISABLE_TRUST_AGENTS, UserHandle.myUserId()); - - final int count = mAvailableAgents.size(); - for (int i = 0; i < count; i++) { - AgentInfo agent = mAvailableAgents.valueAt(i); - final RestrictedSwitchPreference preference = - new RestrictedSwitchPreference(getPrefContext()); - preference.useAdminDisabledSummary(true); - agent.preference = preference; - preference.setPersistent(false); - preference.setTitle(agent.label); - preference.setIcon(agent.icon); - preference.setPersistent(false); - preference.setOnPreferenceChangeListener(this); - preference.setChecked(mActiveAgents.contains(agent.component)); - - if (admin != null - && mDpm.getTrustAgentConfiguration(null, agent.component) == null) { - preference.setChecked(false); - preference.setDisabledByAdmin(admin); - } - - category.addPreference(agent.preference); - } - } - - private void loadActiveAgents() { - List<ComponentName> activeTrustAgents = mLockPatternUtils.getEnabledTrustAgents( - UserHandle.myUserId()); - if (activeTrustAgents != null) { - mActiveAgents.addAll(activeTrustAgents); - } - } - - private void saveActiveAgents() { - mLockPatternUtils.setEnabledTrustAgents(mActiveAgents, - UserHandle.myUserId()); - } - - private ArrayMap<ComponentName, AgentInfo> findAvailableTrustAgents() { - PackageManager pm = getActivity().getPackageManager(); - Intent trustAgentIntent = new Intent(SERVICE_INTERFACE); - List<ResolveInfo> resolveInfos = pm.queryIntentServices(trustAgentIntent, - PackageManager.GET_META_DATA); - - ArrayMap<ComponentName, AgentInfo> agents = new ArrayMap<ComponentName, AgentInfo>(); - final int count = resolveInfos.size(); - agents.ensureCapacity(count); - for (int i = 0; i < count; i++ ) { - ResolveInfo resolveInfo = resolveInfos.get(i); - if (resolveInfo.serviceInfo == null) { - continue; - } - if (!mTrustAgentManager.shouldProvideTrust(resolveInfo, pm)) { - continue; - } - ComponentName name = mTrustAgentManager.getComponentName(resolveInfo); - AgentInfo agentInfo = new AgentInfo(); - agentInfo.label = resolveInfo.loadLabel(pm); - agentInfo.icon = resolveInfo.loadIcon(pm); - agentInfo.component = name; - agents.put(name, agentInfo); - } - return agents; + protected String getLogTag() { + return TAG; } @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - if (preference instanceof SwitchPreference) { - final int count = mAvailableAgents.size(); - for (int i = 0; i < count; i++) { - AgentInfo agent = mAvailableAgents.valueAt(i); - if (agent.preference == preference) { - if ((Boolean) newValue) { - if (!mActiveAgents.contains(agent.component)) { - mActiveAgents.add(agent.component); - } - } else { - mActiveAgents.remove(agent.component); - } - saveActiveAgents(); - return true; - } - } - } - return false; + protected int getPreferenceScreenResId() { + return R.xml.trust_agent_settings; } + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex( + Context context, boolean enabled) { + final List<SearchIndexableResource> result = new ArrayList<>(); + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.trust_agent_settings; + result.add(sir); + return result; + } + }; } diff --git a/src/com/android/settings/security/trustagent/TrustAgentsPreferenceController.java b/src/com/android/settings/security/trustagent/TrustAgentsPreferenceController.java new file mode 100644 index 0000000000..42688397f2 --- /dev/null +++ b/src/com/android/settings/security/trustagent/TrustAgentsPreferenceController.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2018 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.security.trustagent; + +import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; + +import android.app.admin.DevicePolicyManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.graphics.drawable.Drawable; +import android.os.UserHandle; +import android.service.trust.TrustAgentService; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.IconDrawableFactory; + +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreference; + +import com.android.internal.widget.LockPatternUtils; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.overlay.FeatureFactory; +import com.android.settings.security.SecurityFeatureProvider; +import com.android.settingslib.RestrictedLockUtilsInternal; +import com.android.settingslib.RestrictedSwitchPreference; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnStart; + +import java.util.List; + +public class TrustAgentsPreferenceController extends BasePreferenceController + implements Preference.OnPreferenceChangeListener, LifecycleObserver, OnStart { + + private static final Intent TRUST_AGENT_INTENT = + new Intent(TrustAgentService.SERVICE_INTERFACE); + + private final ArrayMap<ComponentName, TrustAgentInfo> mAvailableAgents; + private final ArraySet<ComponentName> mActiveAgents; + private final DevicePolicyManager mDevicePolicyManager; + private final IconDrawableFactory mIconDrawableFactory; + private final LockPatternUtils mLockPatternUtils; + private final PackageManager mPackageManager; + private final TrustAgentManager mTrustAgentManager; + + private PreferenceScreen mScreen; + + public TrustAgentsPreferenceController(Context context, String key) { + super(context, key); + mAvailableAgents = new ArrayMap<>(); + mActiveAgents = new ArraySet<>(); + mDevicePolicyManager = context.getSystemService(DevicePolicyManager.class); + mIconDrawableFactory = IconDrawableFactory.newInstance(context); + final SecurityFeatureProvider securityFeatureProvider = + FeatureFactory.getFactory(context).getSecurityFeatureProvider(); + mTrustAgentManager = securityFeatureProvider.getTrustAgentManager(); + mLockPatternUtils = securityFeatureProvider.getLockPatternUtils(context); + mPackageManager = context.getPackageManager(); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mScreen = screen; + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } + + @Override + public void onStart() { + updateAgents(); + } + + private void updateAgents() { + findAvailableTrustAgents(); + loadActiveAgents(); + removeUselessExistingPreferences(); + + final EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfKeyguardFeaturesDisabled( + mContext, DevicePolicyManager.KEYGUARD_DISABLE_TRUST_AGENTS, UserHandle.myUserId()); + + for (TrustAgentInfo agent : mAvailableAgents.values()) { + final ComponentName componentName = agent.getComponentName(); + RestrictedSwitchPreference preference = (RestrictedSwitchPreference) + mScreen.findPreference(componentName.flattenToString()); + if (preference == null) { + preference = new RestrictedSwitchPreference(mScreen.getContext()); + } + preference.setKey(componentName.flattenToString()); + preference.useAdminDisabledSummary(true); + preference.setTitle(agent.getLabel()); + preference.setIcon(agent.getIcon()); + preference.setOnPreferenceChangeListener(this); + preference.setChecked(mActiveAgents.contains(componentName)); + if (admin != null && mDevicePolicyManager.getTrustAgentConfiguration(null /* admin */, + componentName) == null) { + preference.setChecked(false); + preference.setDisabledByAdmin(admin); + } + mScreen.addPreference(preference); + } + } + + private void loadActiveAgents() { + final List<ComponentName> activeTrustAgents = mLockPatternUtils.getEnabledTrustAgents( + UserHandle.myUserId()); + if (activeTrustAgents != null) { + mActiveAgents.addAll(activeTrustAgents); + } + } + + private void saveActiveAgents() { + mLockPatternUtils.setEnabledTrustAgents(mActiveAgents, UserHandle.myUserId()); + } + + private void findAvailableTrustAgents() { + final List<ResolveInfo> resolveInfos = mPackageManager.queryIntentServices( + TRUST_AGENT_INTENT, PackageManager.GET_META_DATA); + mAvailableAgents.clear(); + for (ResolveInfo resolveInfo : resolveInfos) { + if (resolveInfo.serviceInfo == null) { + continue; + } + if (!mTrustAgentManager.shouldProvideTrust(resolveInfo, mPackageManager)) { + continue; + } + final CharSequence label = resolveInfo.loadLabel(mPackageManager); + final ComponentName componentName = mTrustAgentManager.getComponentName(resolveInfo); + final Drawable icon = mIconDrawableFactory.getBadgedIcon( + resolveInfo.getComponentInfo().applicationInfo); + final TrustAgentInfo agentInfo = new TrustAgentInfo(label, componentName, icon); + mAvailableAgents.put(componentName, agentInfo); + } + } + + private void removeUselessExistingPreferences() { + final int count = mScreen.getPreferenceCount(); + if (count <= 0) { + return; + } + for (int i = count - 1; i >= 0; i--) { + final Preference pref = mScreen.getPreference(i); + final String[] names = TextUtils.split(pref.getKey(), "/"); + final ComponentName componentName = new ComponentName(names[0], names[1]); + if (!mAvailableAgents.containsKey(componentName)) { + mScreen.removePreference(pref); + mActiveAgents.remove(componentName); + } + } + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + if (!(preference instanceof SwitchPreference)) { + return false; + } + for (TrustAgentInfo agent : mAvailableAgents.values()) { + final ComponentName componentName = agent.getComponentName(); + if (!TextUtils.equals(preference.getKey(), componentName.flattenToString())) { + continue; + } + if ((Boolean) newValue && !mActiveAgents.contains(componentName)) { + mActiveAgents.add(componentName); + } else { + mActiveAgents.remove(componentName); + } + saveActiveAgents(); + return true; + } + return false; + } +} diff --git a/src/com/android/settings/shortcut/CreateShortcut.java b/src/com/android/settings/shortcut/CreateShortcut.java index 78969fed3e..392685f58c 100644 --- a/src/com/android/settings/shortcut/CreateShortcut.java +++ b/src/com/android/settings/shortcut/CreateShortcut.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010 The Android Open Source Project + * Copyright (C) 2018 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. @@ -16,189 +16,56 @@ package com.android.settings.shortcut; -import android.app.LauncherActivity; -import android.content.ComponentName; +import static com.android.settings.search.actionbar.SearchMenuController.NEED_SEARCH_ICON_IN_ACTION_BAR; + +import android.app.settings.SettingsEnums; import android.content.Context; -import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.content.pm.ShortcutInfo; -import android.content.pm.ShortcutManager; -import android.graphics.Bitmap; -import android.graphics.Bitmap.Config; -import android.graphics.Canvas; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; -import android.graphics.drawable.LayerDrawable; -import android.net.ConnectivityManager; -import android.os.AsyncTask; -import androidx.annotation.VisibleForTesting; -import android.view.ContextThemeWrapper; -import android.view.LayoutInflater; -import android.view.View; -import android.view.View.MeasureSpec; -import android.widget.ImageView; -import android.widget.ListView; +import android.os.Bundle; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; -import com.android.settings.Settings.TetherSettingsActivity; -import com.android.settings.overlay.FeatureFactory; - -import java.util.ArrayList; -import java.util.List; +import com.android.settings.dashboard.DashboardFragment; -public class CreateShortcut extends LauncherActivity { +/** + * UI for create widget/shortcut screen. + */ +public class CreateShortcut extends DashboardFragment { - @VisibleForTesting - static final String SHORTCUT_ID_PREFIX = "component-shortcut-"; + private static final String TAG = "CreateShortcut"; @Override - protected Intent getTargetIntent() { - return getBaseIntent().addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - } - - @Override - protected void onListItemClick(ListView l, View v, int position, long id) { - final ListItem item = itemForPosition(position); - logCreateShortcut(item.resolveInfo); - setResult(RESULT_OK, createResultIntent(intentForPosition(position), - item.resolveInfo, item.label)); - finish(); - } - - @VisibleForTesting - Intent createResultIntent(Intent shortcutIntent, ResolveInfo resolveInfo, - CharSequence label) { - shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); - ShortcutManager sm = getSystemService(ShortcutManager.class); - ActivityInfo activityInfo = resolveInfo.activityInfo; - - Icon maskableIcon = activityInfo.icon != 0 ? Icon.createWithAdaptiveBitmap( - createIcon(activityInfo.icon, - R.layout.shortcut_badge_maskable, - getResources().getDimensionPixelSize(R.dimen.shortcut_size_maskable))) : - Icon.createWithResource(this, R.drawable.ic_launcher_settings); - String shortcutId = SHORTCUT_ID_PREFIX + - shortcutIntent.getComponent().flattenToShortString(); - ShortcutInfo info = new ShortcutInfo.Builder(this, shortcutId) - .setShortLabel(label) - .setIntent(shortcutIntent) - .setIcon(maskableIcon) - .build(); - Intent intent = sm.createShortcutResultIntent(info); - if (intent == null) { - intent = new Intent(); - } - intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, - Intent.ShortcutIconResource.fromContext(this, R.mipmap.ic_launcher_settings)); - intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); - intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, label); - - if (activityInfo.icon != 0) { - intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, createIcon(activityInfo.icon, - R.layout.shortcut_badge, - getResources().getDimensionPixelSize(R.dimen.shortcut_size))); - } - return intent; - } - - private void logCreateShortcut(ResolveInfo info) { - if (info == null || info.activityInfo == null) { - return; + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + Bundle args = getArguments(); + if (args == null) { + args = new Bundle(); + setArguments(args); } - FeatureFactory.getFactory(this).getMetricsFeatureProvider().action( - this, MetricsProto.MetricsEvent.ACTION_SETTINGS_CREATE_SHORTCUT, - info.activityInfo.name); - } - - private Bitmap createIcon(int resource, int layoutRes, int size) { - Context context = new ContextThemeWrapper(this, android.R.style.Theme_Material); - View view = LayoutInflater.from(context).inflate(layoutRes, null); - Drawable iconDrawable = getDrawable(resource); - if (iconDrawable instanceof LayerDrawable) { - iconDrawable = ((LayerDrawable) iconDrawable).getDrawable(1); - } - ((ImageView) view.findViewById(android.R.id.icon)).setImageDrawable(iconDrawable); - - int spec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY); - view.measure(spec, spec); - Bitmap bitmap = Bitmap.createBitmap(view.getMeasuredWidth(), view.getMeasuredHeight(), - Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight()); - view.draw(canvas); - return bitmap; + args.putBoolean(NEED_SEARCH_ICON_IN_ACTION_BAR, false); } @Override - protected boolean onEvaluateShowIcons() { - return false; + public void onAttach(Context context) { + super.onAttach(context); + use(CreateShortcutPreferenceController.class).setActivity(getActivity()); } @Override - protected void onSetContentView() { - setContentView(R.layout.activity_list); + protected int getPreferenceScreenResId() { + return R.xml.create_shortcut; } - /** - * Perform query on package manager for list items. The default - * implementation queries for activities. - */ - protected List<ResolveInfo> onQueryPackageManager(Intent queryIntent) { - List<ResolveInfo> activities = getPackageManager().queryIntentActivities(queryIntent, - PackageManager.GET_META_DATA); - final ConnectivityManager cm = - (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); - if (activities == null) return null; - for (int i = activities.size() - 1; i >= 0; i--) { - ResolveInfo info = activities.get(i); - if (info.activityInfo.name.endsWith(TetherSettingsActivity.class.getSimpleName())) { - if (!cm.isTetheringSupported()) { - activities.remove(i); - } - } - } - return activities; + @Override + protected String getLogTag() { + return TAG; } - @VisibleForTesting - static Intent getBaseIntent() { - return new Intent(Intent.ACTION_MAIN).addCategory("com.android.settings.SHORTCUT"); + @Override + public int getMetricsCategory() { + return SettingsEnums.SETTINGS_CREATE_SHORTCUT; } - public static class ShortcutsUpdateTask extends AsyncTask<Void, Void, Void> { - - private final Context mContext; - - public ShortcutsUpdateTask(Context context) { - mContext = context; - } - - @Override - public Void doInBackground(Void... params) { - ShortcutManager sm = mContext.getSystemService(ShortcutManager.class); - PackageManager pm = mContext.getPackageManager(); - - List<ShortcutInfo> updates = new ArrayList<>(); - for (ShortcutInfo info : sm.getPinnedShortcuts()) { - if (!info.getId().startsWith(SHORTCUT_ID_PREFIX)) { - continue; - } - ComponentName cn = ComponentName.unflattenFromString( - info.getId().substring(SHORTCUT_ID_PREFIX.length())); - ResolveInfo ri = pm.resolveActivity(getBaseIntent().setComponent(cn), 0); - if (ri == null) { - continue; - } - updates.add(new ShortcutInfo.Builder(mContext, info.getId()) - .setShortLabel(ri.loadLabel(pm)).build()); - } - if (!updates.isEmpty()) { - sm.updateShortcuts(updates); - } - return null; - } + @Override + public int getHelpResource() { + return 0; } } diff --git a/src/com/android/settings/shortcut/CreateShortcutPreferenceController.java b/src/com/android/settings/shortcut/CreateShortcutPreferenceController.java new file mode 100644 index 0000000000..6b95b92f1b --- /dev/null +++ b/src/com/android/settings/shortcut/CreateShortcutPreferenceController.java @@ -0,0 +1,284 @@ +/* + * Copyright (C) 2018 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.shortcut; + +import android.app.Activity; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ShortcutInfo; +import android.content.pm.ShortcutManager; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.graphics.drawable.LayerDrawable; +import android.net.ConnectivityManager; +import android.util.Log; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageView; + +import com.android.settings.R; +import com.android.settings.Settings.TetherSettingsActivity; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceGroup; + +/** + * {@link BasePreferenceController} that populates a list of widgets that Settings app support. + */ +public class CreateShortcutPreferenceController extends BasePreferenceController { + + private static final String TAG = "CreateShortcutPrefCtrl"; + + static final String SHORTCUT_ID_PREFIX = "component-shortcut-"; + static final Intent SHORTCUT_PROBE = new Intent(Intent.ACTION_MAIN) + .addCategory("com.android.settings.SHORTCUT") + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + private final ShortcutManager mShortcutManager; + private final PackageManager mPackageManager; + private final ConnectivityManager mConnectivityManager; + private final MetricsFeatureProvider mMetricsFeatureProvider; + private Activity mHost; + + public CreateShortcutPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + mConnectivityManager = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + mShortcutManager = context.getSystemService(ShortcutManager.class); + mPackageManager = context.getPackageManager(); + mMetricsFeatureProvider = FeatureFactory.getFactory(context) + .getMetricsFeatureProvider(); + } + + public void setActivity(Activity host) { + mHost = host; + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE_UNSEARCHABLE; + } + + @Override + public void updateState(Preference preference) { + if (!(preference instanceof PreferenceGroup)) { + return; + } + final PreferenceGroup group = (PreferenceGroup) preference; + group.removeAll(); + final List<ResolveInfo> shortcuts = queryShortcuts(); + final Context uiContext = preference.getContext(); + if (shortcuts.isEmpty()) { + return; + } + PreferenceCategory category = new PreferenceCategory(uiContext); + group.addPreference(category); + int bucket = 0; + for (ResolveInfo info : shortcuts) { + // Priority is not consecutive (aka, jumped), add a divider between prefs. + final int currentBucket = info.priority / 10; + boolean needDivider = currentBucket != bucket; + bucket = currentBucket; + if (needDivider) { + // add a new Category + category = new PreferenceCategory(uiContext); + group.addPreference(category); + } + + final Preference pref = new Preference(uiContext); + pref.setTitle(info.loadLabel(mPackageManager)); + pref.setKey(info.activityInfo.getComponentName().flattenToString()); + pref.setOnPreferenceClickListener(clickTarget -> { + if (mHost == null) { + return false; + } + final Intent shortcutIntent = createResultIntent( + buildShortcutIntent(info), + info, clickTarget.getTitle()); + mHost.setResult(Activity.RESULT_OK, shortcutIntent); + logCreateShortcut(info); + mHost.finish(); + return true; + }); + category.addPreference(pref); + } + } + + /** + * Create {@link Intent} that will be consumed by ShortcutManager, which later generates a + * launcher widget using this intent. + */ + @VisibleForTesting + Intent createResultIntent(Intent shortcutIntent, ResolveInfo resolveInfo, + CharSequence label) { + ShortcutInfo info = createShortcutInfo(mContext, shortcutIntent, resolveInfo, label); + Intent intent = mShortcutManager.createShortcutResultIntent(info); + if (intent == null) { + intent = new Intent(); + } + intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, + Intent.ShortcutIconResource.fromContext(mContext, R.mipmap.ic_launcher_settings)) + .putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent) + .putExtra(Intent.EXTRA_SHORTCUT_NAME, label); + + final ActivityInfo activityInfo = resolveInfo.activityInfo; + if (activityInfo.icon != 0) { + intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, createIcon( + mContext, + activityInfo.applicationInfo, + activityInfo.icon, + R.layout.shortcut_badge, + mContext.getResources().getDimensionPixelSize(R.dimen.shortcut_size))); + } + return intent; + } + + /** + * Finds all shortcut supported by Settings. + */ + @VisibleForTesting + List<ResolveInfo> queryShortcuts() { + final List<ResolveInfo> shortcuts = new ArrayList<>(); + final List<ResolveInfo> activities = mPackageManager.queryIntentActivities(SHORTCUT_PROBE, + PackageManager.GET_META_DATA); + + if (activities == null) { + return null; + } + for (ResolveInfo info : activities) { + if (info.activityInfo.name.endsWith(TetherSettingsActivity.class.getSimpleName())) { + if (!mConnectivityManager.isTetheringSupported()) { + continue; + } + } + if (!info.activityInfo.applicationInfo.isSystemApp()) { + Log.d(TAG, "Skipping non-system app: " + info.activityInfo); + continue; + } + shortcuts.add(info); + } + Collections.sort(shortcuts, SHORTCUT_COMPARATOR); + return shortcuts; + } + + private void logCreateShortcut(ResolveInfo info) { + if (info == null || info.activityInfo == null) { + return; + } + mMetricsFeatureProvider.action( + mContext, SettingsEnums.ACTION_SETTINGS_CREATE_SHORTCUT, + info.activityInfo.name); + } + + private static Intent buildShortcutIntent(ResolveInfo info) { + return new Intent(SHORTCUT_PROBE) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP) + .setClassName(info.activityInfo.packageName, info.activityInfo.name); + } + + private static ShortcutInfo createShortcutInfo(Context context, Intent shortcutIntent, + ResolveInfo resolveInfo, CharSequence label) { + final ActivityInfo activityInfo = resolveInfo.activityInfo; + + final Icon maskableIcon; + if (activityInfo.icon != 0 && activityInfo.applicationInfo != null) { + maskableIcon = Icon.createWithAdaptiveBitmap(createIcon( + context, + activityInfo.applicationInfo, activityInfo.icon, + R.layout.shortcut_badge_maskable, + context.getResources().getDimensionPixelSize(R.dimen.shortcut_size_maskable))); + } else { + maskableIcon = Icon.createWithResource(context, R.drawable.ic_launcher_settings); + } + final String shortcutId = SHORTCUT_ID_PREFIX + + shortcutIntent.getComponent().flattenToShortString(); + return new ShortcutInfo.Builder(context, shortcutId) + .setShortLabel(label) + .setIntent(shortcutIntent) + .setIcon(maskableIcon) + .build(); + } + + private static Bitmap createIcon(Context context, ApplicationInfo app, int resource, + int layoutRes, int size) { + final Context themedContext = new ContextThemeWrapper(context, + android.R.style.Theme_Material); + final View view = LayoutInflater.from(themedContext).inflate(layoutRes, null); + final int spec = View.MeasureSpec.makeMeasureSpec(size, View.MeasureSpec.EXACTLY); + view.measure(spec, spec); + final Bitmap bitmap = Bitmap.createBitmap(view.getMeasuredWidth(), view.getMeasuredHeight(), + Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(bitmap); + + Drawable iconDrawable; + try { + iconDrawable = context.getPackageManager().getResourcesForApplication(app) + .getDrawable(resource); + if (iconDrawable instanceof LayerDrawable) { + iconDrawable = ((LayerDrawable) iconDrawable).getDrawable(1); + } + ((ImageView) view.findViewById(android.R.id.icon)).setImageDrawable(iconDrawable); + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "Cannot load icon from app " + app + ", returning a default icon"); + Icon icon = Icon.createWithResource(context, R.drawable.ic_launcher_settings); + ((ImageView) view.findViewById(android.R.id.icon)).setImageIcon(icon); + } + + view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight()); + view.draw(canvas); + return bitmap; + } + + public static void updateRestoredShortcuts(Context context) { + ShortcutManager sm = context.getSystemService(ShortcutManager.class); + List<ShortcutInfo> updatedShortcuts = new ArrayList<>(); + for (ShortcutInfo si : sm.getPinnedShortcuts()) { + if (si.getId().startsWith(SHORTCUT_ID_PREFIX)) { + ResolveInfo ri = context.getPackageManager().resolveActivity(si.getIntent(), 0); + + if (ri != null) { + updatedShortcuts.add(createShortcutInfo(context, buildShortcutIntent(ri), ri, + si.getShortLabel())); + } + } + } + if (!updatedShortcuts.isEmpty()) { + sm.updateShortcuts(updatedShortcuts); + } + } + + private static final Comparator<ResolveInfo> SHORTCUT_COMPARATOR = + (i1, i2) -> i1.priority - i2.priority; +} diff --git a/src/com/android/settings/shortcut/ShortcutsUpdateTask.java b/src/com/android/settings/shortcut/ShortcutsUpdateTask.java new file mode 100644 index 0000000000..54f7d1ceaf --- /dev/null +++ b/src/com/android/settings/shortcut/ShortcutsUpdateTask.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2018 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.shortcut; + +import static com.android.settings.shortcut.CreateShortcutPreferenceController.SHORTCUT_ID_PREFIX; +import static com.android.settings.shortcut.CreateShortcutPreferenceController.SHORTCUT_PROBE; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ShortcutInfo; +import android.content.pm.ShortcutManager; +import android.os.AsyncTask; + +import java.util.ArrayList; +import java.util.List; + +public class ShortcutsUpdateTask extends AsyncTask<Void, Void, Void> { + + private final Context mContext; + + public ShortcutsUpdateTask(Context context) { + mContext = context; + } + + @Override + public Void doInBackground(Void... params) { + ShortcutManager sm = mContext.getSystemService(ShortcutManager.class); + PackageManager pm = mContext.getPackageManager(); + + List<ShortcutInfo> updates = new ArrayList<>(); + for (ShortcutInfo info : sm.getPinnedShortcuts()) { + if (!info.getId().startsWith(SHORTCUT_ID_PREFIX)) { + continue; + } + ComponentName cn = ComponentName.unflattenFromString( + info.getId().substring(SHORTCUT_ID_PREFIX.length())); + ResolveInfo ri = pm.resolveActivity(new Intent(SHORTCUT_PROBE).setComponent(cn), 0); + if (ri == null) { + continue; + } + updates.add(new ShortcutInfo.Builder(mContext, info.getId()) + .setShortLabel(ri.loadLabel(pm)).build()); + } + if (!updates.isEmpty()) { + sm.updateShortcuts(updates); + } + return null; + } +} diff --git a/src/com/android/settings/sim/CallsSimListDialogFragment.java b/src/com/android/settings/sim/CallsSimListDialogFragment.java new file mode 100644 index 0000000000..7d3de44382 --- /dev/null +++ b/src/com/android/settings/sim/CallsSimListDialogFragment.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2019 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.sim; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.telecom.PhoneAccount; +import android.telecom.PhoneAccountHandle; +import android.telecom.TelecomManager; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; + +import java.util.ArrayList; +import java.util.List; + +/** + * Specialized version of SimListDialogFragment that fetches a list of SIMs which support calls. + */ +public class CallsSimListDialogFragment extends SimListDialogFragment { + @Override + protected List<SubscriptionInfo> getCurrentSubscriptions() { + final Context context = getContext(); + final SubscriptionManager subscriptionManager = context.getSystemService( + SubscriptionManager.class); + final TelecomManager telecomManager = context.getSystemService(TelecomManager.class); + final TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class); + final List<PhoneAccountHandle> phoneAccounts = + telecomManager.getCallCapablePhoneAccounts(); + final List<SubscriptionInfo> result = new ArrayList<>(); + + if (phoneAccounts == null) { + return result; + } + for (PhoneAccountHandle handle : phoneAccounts) { + final PhoneAccount phoneAccount = telecomManager.getPhoneAccount(handle); + final int subId = telephonyManager.getSubIdForPhoneAccount(phoneAccount); + + if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + continue; + } + result.add(subscriptionManager.getActiveSubscriptionInfo(subId)); + } + return result; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.DIALOG_CALL_SIM_LIST; + } +} diff --git a/src/com/android/settings/sim/PreferredSimDialogFragment.java b/src/com/android/settings/sim/PreferredSimDialogFragment.java new file mode 100644 index 0000000000..29f4c65534 --- /dev/null +++ b/src/com/android/settings/sim/PreferredSimDialogFragment.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2019 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.sim; + +import android.app.Activity; +import android.app.Dialog; +import android.app.settings.SettingsEnums; +import android.content.DialogInterface; +import android.os.Bundle; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; + +import com.android.settings.R; + +/** + * Presents a dialog asking the user if they want to update all services to use a given "preferred" + * SIM. Typically this would be used in a case where a device goes from having multiple SIMs down to + * only one. + */ +public class PreferredSimDialogFragment extends SimDialogFragment implements + DialogInterface.OnClickListener { + private static final String TAG = "PreferredSimDialogFrag"; + + public static PreferredSimDialogFragment newInstance() { + final PreferredSimDialogFragment fragment = new PreferredSimDialogFragment(); + final Bundle args = initArguments(SimDialogActivity.PREFERRED_PICK, + R.string.sim_preferred_title); + fragment.setArguments(args); + return fragment; + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + final AlertDialog dialog = new AlertDialog.Builder(getContext()) + .setTitle(getTitleResId()) + .setPositiveButton(R.string.yes, this) + .setNegativeButton(R.string.no, null) + .create(); + updateDialog(dialog); + return dialog; + } + + @Override + public void onClick(DialogInterface dialog, int buttonClicked) { + if (buttonClicked != DialogInterface.BUTTON_POSITIVE) { + return; + } + final SimDialogActivity activity = (SimDialogActivity) getActivity(); + final SubscriptionInfo info = getPreferredSubscription(); + if (info != null) { + activity.onSubscriptionSelected(getDialogType(), info.getSubscriptionId()); + } + } + + public SubscriptionInfo getPreferredSubscription() { + final Activity activity = getActivity(); + final int slotId = activity.getIntent().getIntExtra(SimDialogActivity.PREFERRED_SIM, + SubscriptionManager.INVALID_SUBSCRIPTION_ID); + return getSubscriptionManager().getActiveSubscriptionInfoForSimSlotIndex(slotId); + } + + private void updateDialog(AlertDialog dialog) { + final SubscriptionInfo info = getPreferredSubscription(); + if (info == null) { + dismiss(); + return; + } + final String message = + getContext().getString(R.string.sim_preferred_message, info.getDisplayName()); + dialog.setMessage(message); + } + + @Override + public void updateDialog() { + updateDialog((AlertDialog) getDialog()); + } + + @VisibleForTesting + protected SubscriptionManager getSubscriptionManager() { + return getContext().getSystemService(SubscriptionManager.class); + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.DIALOG_PREFERRED_SIM_PICKER; + } +} diff --git a/src/com/android/settings/sim/SimDialogActivity.java b/src/com/android/settings/sim/SimDialogActivity.java index 5a45fb40ea..d24dbf56f2 100644 --- a/src/com/android/settings/sim/SimDialogActivity.java +++ b/src/com/android/settings/sim/SimDialogActivity.java @@ -17,36 +17,30 @@ package com.android.settings.sim; import android.app.Activity; -import android.app.AlertDialog; -import android.app.Dialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.res.Resources; import android.content.Intent; import android.os.Bundle; import android.telecom.PhoneAccount; import android.telecom.PhoneAccountHandle; import android.telecom.TelecomManager; -import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.ImageView; -import android.widget.ListAdapter; -import android.widget.TextView; +import android.util.Log; import android.widget.Toast; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; + import com.android.settings.R; -import java.util.ArrayList; -import java.util.Iterator; import java.util.List; -public class SimDialogActivity extends Activity { +/** + * This activity provides singleton semantics per dialog type for showing various kinds of + * dialogs asking the user to make choices about which SIM to use for various services + * (calls, SMS, and data). + */ +public class SimDialogActivity extends FragmentActivity { private static String TAG = "SimDialogActivity"; public static String PREFERRED_SIM = "preferred_sim"; @@ -64,285 +58,129 @@ public class SimDialogActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + showOrUpdateDialog(); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + setIntent(intent); + showOrUpdateDialog(); + } + + private void showOrUpdateDialog() { final int dialogType = getIntent().getIntExtra(DIALOG_TYPE_KEY, INVALID_PICK); + final String tag = Integer.toString(dialogType); + final FragmentManager fragmentManager = getSupportFragmentManager(); + SimDialogFragment fragment = (SimDialogFragment) fragmentManager.findFragmentByTag(tag); + + if (fragment == null) { + fragment = createFragment(dialogType); + fragment.show(fragmentManager, tag); + } else { + fragment.updateDialog(); + } + } + private SimDialogFragment createFragment(int dialogType) { switch (dialogType) { case DATA_PICK: + return SimListDialogFragment.newInstance(dialogType, R.string.select_sim_for_data, + false /* includeAskEveryTime */); case CALLS_PICK: + return CallsSimListDialogFragment.newInstance(dialogType, + R.string.select_sim_for_calls, + true /* includeAskEveryTime */); case SMS_PICK: - case SMS_PICK_FOR_MESSAGE: - createDialog(this, dialogType).show(); - break; + return SimListDialogFragment.newInstance(dialogType, R.string.select_sim_for_sms, + true /* includeAskEveryTime */); case PREFERRED_PICK: - displayPreferredDialog(getIntent().getIntExtra(PREFERRED_SIM, 0)); - break; + if (!getIntent().hasExtra(PREFERRED_SIM)) { + throw new IllegalArgumentException("Missing required extra " + PREFERRED_SIM); + } + return PreferredSimDialogFragment.newInstance(); + case SMS_PICK_FOR_MESSAGE: + return SimListDialogFragment.newInstance(dialogType, R.string.select_sim_for_sms, + false /* includeAskEveryTime */); default: throw new IllegalArgumentException("Invalid dialog type " + dialogType + " sent."); } - } - private void displayPreferredDialog(final int slotId) { - final Resources res = getResources(); - final Context context = getApplicationContext(); - final SubscriptionInfo sir = SubscriptionManager.from(context) - .getActiveSubscriptionInfoForSimSlotIndex(slotId); - - if (sir != null) { - AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(this); - alertDialogBuilder.setTitle(R.string.sim_preferred_title); - alertDialogBuilder.setMessage(res.getString( - R.string.sim_preferred_message, sir.getDisplayName())); - - alertDialogBuilder.setPositiveButton(R.string.yes, new - DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int id) { - final int subId = sir.getSubscriptionId(); - PhoneAccountHandle phoneAccountHandle = - subscriptionIdToPhoneAccountHandle(subId); - setDefaultDataSubId(context, subId); - setDefaultSmsSubId(context, subId); - setUserSelectedOutgoingPhoneAccount(phoneAccountHandle); - finish(); - } - }); - alertDialogBuilder.setNegativeButton(R.string.no, new - DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog,int id) { - finish(); - } - }); - - alertDialogBuilder.create().show(); - } else { - finish(); + public void onSubscriptionSelected(int dialogType, int subId) { + if (getSupportFragmentManager().findFragmentByTag(Integer.toString(dialogType)) == null) { + Log.w(TAG, "onSubscriptionSelected ignored because stored fragment was null"); + return; } - } - - private static void setDefaultDataSubId(final Context context, final int subId) { - final SubscriptionManager subscriptionManager = SubscriptionManager.from(context); - subscriptionManager.setDefaultDataSubId(subId); - Toast.makeText(context, R.string.data_switch_started, Toast.LENGTH_LONG).show(); - } - - private static void setDefaultSmsSubId(final Context context, final int subId) { - final SubscriptionManager subscriptionManager = SubscriptionManager.from(context); - subscriptionManager.setDefaultSmsSubId(subId); - } - - private void setUserSelectedOutgoingPhoneAccount(PhoneAccountHandle phoneAccount) { - final TelecomManager telecomManager = TelecomManager.from(this); - telecomManager.setUserSelectedOutgoingPhoneAccount(phoneAccount); - } - - private PhoneAccountHandle subscriptionIdToPhoneAccountHandle(final int subId) { - final TelecomManager telecomManager = TelecomManager.from(this); - final TelephonyManager telephonyManager = TelephonyManager.from(this); - final Iterator<PhoneAccountHandle> phoneAccounts = - telecomManager.getCallCapablePhoneAccounts().listIterator(); - - while (phoneAccounts.hasNext()) { - final PhoneAccountHandle phoneAccountHandle = phoneAccounts.next(); - final PhoneAccount phoneAccount = telecomManager.getPhoneAccount(phoneAccountHandle); - if (subId == telephonyManager.getSubIdForPhoneAccount(phoneAccount)) { - return phoneAccountHandle; - } - } - - return null; - } - - public Dialog createDialog(final Context context, final int id) { - final ArrayList<String> list = new ArrayList<String>(); - final SubscriptionManager subscriptionManager = SubscriptionManager.from(context); - final List<SubscriptionInfo> subInfoList = - subscriptionManager.getActiveSubscriptionInfoList(); - final int selectableSubInfoLength = subInfoList == null ? 0 : subInfoList.size(); - - final DialogInterface.OnClickListener selectionListener = - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int value) { - - final SubscriptionInfo sir; - - switch (id) { - case DATA_PICK: - sir = subInfoList.get(value); - setDefaultDataSubId(context, sir.getSubscriptionId()); - break; - case CALLS_PICK: - final TelecomManager telecomManager = - TelecomManager.from(context); - final List<PhoneAccountHandle> phoneAccountsList = - telecomManager.getCallCapablePhoneAccounts(); - setUserSelectedOutgoingPhoneAccount( - value < 1 ? null : phoneAccountsList.get(value - 1)); - break; - case SMS_PICK: - sir = subInfoList.get(value); - setDefaultSmsSubId(context, sir.getSubscriptionId()); - break; - case SMS_PICK_FOR_MESSAGE: - sir = subInfoList.get(value); - // Don't set a default here. - // The caller has created this dialog waiting for a result. - Intent intent = new Intent(); - intent.putExtra(RESULT_SUB_ID, sir.getSubscriptionId()); - setResult(Activity.RESULT_OK, intent); - break; - default: - throw new IllegalArgumentException("Invalid dialog type " - + id + " in SIM dialog."); - } - - finish(); - } - }; - - Dialog.OnKeyListener keyListener = new Dialog.OnKeyListener() { - @Override - public boolean onKey(DialogInterface arg0, int keyCode, - KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_BACK) { - finish(); - } - return true; - } - }; - - ArrayList<SubscriptionInfo> callsSubInfoList = new ArrayList<SubscriptionInfo>(); - if (id == CALLS_PICK) { - final TelecomManager telecomManager = TelecomManager.from(context); - final TelephonyManager telephonyManager = TelephonyManager.from(context); - final Iterator<PhoneAccountHandle> phoneAccounts = - telecomManager.getCallCapablePhoneAccounts().listIterator(); - - list.add(getResources().getString(R.string.sim_calls_ask_first_prefs_title)); - callsSubInfoList.add(null); - while (phoneAccounts.hasNext()) { - final PhoneAccount phoneAccount = - telecomManager.getPhoneAccount(phoneAccounts.next()); - list.add((String)phoneAccount.getLabel()); - int subId = telephonyManager.getSubIdForPhoneAccount(phoneAccount); - if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { - final SubscriptionInfo sir = SubscriptionManager.from(context) - .getActiveSubscriptionInfo(subId); - callsSubInfoList.add(sir); - } else { - callsSubInfoList.add(null); - } - } - } else { - for (int i = 0; i < selectableSubInfoLength; ++i) { - final SubscriptionInfo sir = subInfoList.get(i); - CharSequence displayName = sir.getDisplayName(); - if (displayName == null) { - displayName = ""; - } - list.add(displayName.toString()); - } - } - - String[] arr = list.toArray(new String[0]); - - AlertDialog.Builder builder = new AlertDialog.Builder(context); - - ListAdapter adapter = new SelectAccountListAdapter( - id == CALLS_PICK ? callsSubInfoList : subInfoList, - builder.getContext(), - R.layout.select_account_list_item, - arr, id); - - switch (id) { + switch (dialogType) { case DATA_PICK: - builder.setTitle(R.string.select_sim_for_data); + setDefaultDataSubId(subId); break; case CALLS_PICK: - builder.setTitle(R.string.select_sim_for_calls); + setDefaultCallsSubId(subId); break; case SMS_PICK: - // intentional fallthrough + setDefaultSmsSubId(subId); + break; + case PREFERRED_PICK: + setPreferredSim(subId); + break; case SMS_PICK_FOR_MESSAGE: - builder.setTitle(R.string.sim_card_select_title); + // Don't set a default here. + // The caller has created this dialog waiting for a result. + Intent intent = new Intent(); + intent.putExtra(RESULT_SUB_ID, subId); + setResult(Activity.RESULT_OK, intent); break; - default: - throw new IllegalArgumentException("Invalid dialog type " - + id + " in SIM dialog."); + throw new IllegalArgumentException( + "Invalid dialog type " + dialogType + " sent."); } + } - Dialog dialog = builder.setAdapter(adapter, selectionListener).create(); - dialog.setOnKeyListener(keyListener); - - dialog.setOnCancelListener(new DialogInterface.OnCancelListener() { - @Override - public void onCancel(DialogInterface dialogInterface) { - finish(); - } - }); - - return dialog; + public void onFragmentDismissed(SimDialogFragment simDialogFragment) { + final List<Fragment> fragments = getSupportFragmentManager().getFragments(); + if (fragments.size() == 1 && fragments.get(0) == simDialogFragment) { + finishAndRemoveTask(); + } + } + private void setDefaultDataSubId(final int subId) { + final SubscriptionManager subscriptionManager = getSystemService(SubscriptionManager.class); + final TelephonyManager telephonyManager = getSystemService( + TelephonyManager.class).createForSubscriptionId(subId); + subscriptionManager.setDefaultDataSubId(subId); + telephonyManager.setDataEnabled(true); + Toast.makeText(this, R.string.data_switch_started, Toast.LENGTH_LONG).show(); } - private class SelectAccountListAdapter extends ArrayAdapter<String> { - private Context mContext; - private int mResId; - private int mDialogId; - private final float OPACITY = 0.54f; - private List<SubscriptionInfo> mSubInfoList; + private void setDefaultCallsSubId(final int subId) { + final PhoneAccountHandle phoneAccount = subscriptionIdToPhoneAccountHandle(subId); + final TelecomManager telecomManager = getSystemService(TelecomManager.class); + telecomManager.setUserSelectedOutgoingPhoneAccount(phoneAccount); + } - public SelectAccountListAdapter(List<SubscriptionInfo> subInfoList, - Context context, int resource, String[] arr, int dialogId) { - super(context, resource, arr); - mContext = context; - mResId = resource; - mDialogId = dialogId; - mSubInfoList = subInfoList; - } + private void setDefaultSmsSubId(final int subId) { + final SubscriptionManager subscriptionManager = getSystemService(SubscriptionManager.class); + subscriptionManager.setDefaultSmsSubId(subId); + } - @Override - public View getView(int position, View convertView, ViewGroup parent) { - LayoutInflater inflater = (LayoutInflater) - mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - View rowView; - final ViewHolder holder; + private void setPreferredSim(final int subId) { + setDefaultDataSubId(subId); + setDefaultSmsSubId(subId); + setDefaultCallsSubId(subId); + } - if (convertView == null) { - // Cache views for faster scrolling - rowView = inflater.inflate(mResId, null); - holder = new ViewHolder(); - holder.title = (TextView) rowView.findViewById(R.id.title); - holder.summary = (TextView) rowView.findViewById(R.id.summary); - holder.icon = (ImageView) rowView.findViewById(R.id.icon); - rowView.setTag(holder); - } else { - rowView = convertView; - holder = (ViewHolder) rowView.getTag(); - } + private PhoneAccountHandle subscriptionIdToPhoneAccountHandle(final int subId) { + final TelecomManager telecomManager = getSystemService(TelecomManager.class); + final TelephonyManager telephonyManager = getSystemService(TelephonyManager.class); - final SubscriptionInfo sir = mSubInfoList.get(position); - if (sir == null) { - holder.title.setText(getItem(position)); - holder.summary.setText(""); - holder.icon.setImageDrawable(getResources() - .getDrawable(R.drawable.ic_live_help)); - holder.icon.setAlpha(OPACITY); - } else { - holder.title.setText(sir.getDisplayName()); - holder.summary.setText(sir.getNumber()); - holder.icon.setImageBitmap(sir.createIconBitmap(mContext)); + for (PhoneAccountHandle handle : telecomManager.getCallCapablePhoneAccounts()) { + final PhoneAccount phoneAccount = telecomManager.getPhoneAccount(handle); + if (subId == telephonyManager.getSubIdForPhoneAccount(phoneAccount)) { + return handle; } - return rowView; - } - - private class ViewHolder { - TextView title; - TextView summary; - ImageView icon; } + return null; } } diff --git a/src/com/android/settings/sim/SimDialogFragment.java b/src/com/android/settings/sim/SimDialogFragment.java new file mode 100644 index 0000000000..362fccc75a --- /dev/null +++ b/src/com/android/settings/sim/SimDialogFragment.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2019 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.sim; + +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; + +import androidx.annotation.NonNull; + +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; +import com.android.settings.network.SubscriptionsChangeListener; + +/** Common functionality for showing a dialog in SimDialogActivity. */ +public abstract class SimDialogFragment extends InstrumentedDialogFragment implements + SubscriptionsChangeListener.SubscriptionsChangeListenerClient { + private static final String TAG = "SimDialogFragment"; + + private static final String KEY_TITLE_ID = "title_id"; + private static final String KEY_DIALOG_TYPE = "dialog_type"; + + private SubscriptionsChangeListener mChangeListener; + + protected static Bundle initArguments(int dialogType, int titleResId) { + final Bundle args = new Bundle(); + args.putInt(KEY_DIALOG_TYPE, dialogType); + args.putInt(KEY_TITLE_ID, titleResId); + return args; + } + + public int getDialogType() { + return getArguments().getInt(KEY_DIALOG_TYPE); + } + + public int getTitleResId() { + return getArguments().getInt(KEY_TITLE_ID); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + mChangeListener = new SubscriptionsChangeListener(context, this); + } + + @Override + public void onPause() { + super.onPause(); + mChangeListener.stop(); + } + + @Override + public void onResume() { + super.onResume(); + mChangeListener.start(); + } + + @Override + public void onDismiss(@NonNull DialogInterface dialog) { + super.onDismiss(dialog); + final SimDialogActivity activity = (SimDialogActivity) getActivity(); + if (activity != null && !activity.isFinishing()) { + activity.onFragmentDismissed(this); + } + } + + public abstract void updateDialog(); + + @Override + public void onAirplaneModeChanged(boolean airplaneModeEnabled) { + } + + @Override + public void onSubscriptionsChanged() { + updateDialog(); + } +} diff --git a/src/com/android/settings/sim/SimListDialogFragment.java b/src/com/android/settings/sim/SimListDialogFragment.java new file mode 100644 index 0000000000..3b78927de5 --- /dev/null +++ b/src/com/android/settings/sim/SimListDialogFragment.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2019 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.sim; + +import android.app.Dialog; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; + +import com.android.settings.R; +import com.android.settings.Utils; + +import java.util.ArrayList; +import java.util.List; + +/** + * Shows a dialog consisting of a list of SIMs (aka subscriptions), possibly including an additional + * entry indicating "ask me every time". + */ +public class SimListDialogFragment extends SimDialogFragment implements + DialogInterface.OnClickListener { + protected static final String KEY_INCLUDE_ASK_EVERY_TIME = "include_ask_every_time"; + + protected SelectSubscriptionAdapter mAdapter; + @VisibleForTesting + List<SubscriptionInfo> mSubscriptions; + + public static SimListDialogFragment newInstance(int dialogType, int titleResId, + boolean includeAskEveryTime) { + final SimListDialogFragment fragment = new SimListDialogFragment(); + final Bundle args = initArguments(dialogType, titleResId); + args.putBoolean(KEY_INCLUDE_ASK_EVERY_TIME, includeAskEveryTime); + fragment.setArguments(args); + return fragment; + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + mSubscriptions = new ArrayList<>(); + + final AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); + builder.setTitle(getTitleResId()); + + mAdapter = new SelectSubscriptionAdapter(builder.getContext(), mSubscriptions); + + setAdapter(builder); + final Dialog dialog = builder.create(); + updateDialog(); + return dialog; + } + + @Override + public void onClick(DialogInterface dialog, int selectionIndex) { + if (selectionIndex >= 0 && selectionIndex < mSubscriptions.size()) { + int subId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; + final SubscriptionInfo subscription = mSubscriptions.get(selectionIndex); + if (subscription != null) { + subId = subscription.getSubscriptionId(); + } + final SimDialogActivity activity = (SimDialogActivity) getActivity(); + activity.onSubscriptionSelected(getDialogType(), subId); + } + } + + protected List<SubscriptionInfo> getCurrentSubscriptions() { + final SubscriptionManager manager = getContext().getSystemService( + SubscriptionManager.class); + return manager.getActiveSubscriptionInfoList(true); + } + + @Override + public void updateDialog() { + List<SubscriptionInfo> currentSubscriptions = getCurrentSubscriptions(); + if (currentSubscriptions == null) { + dismiss(); + return; + } + if (getArguments().getBoolean(KEY_INCLUDE_ASK_EVERY_TIME)) { + final List<SubscriptionInfo> tmp = new ArrayList<>(currentSubscriptions.size() + 1); + tmp.add(null); + tmp.addAll(currentSubscriptions); + currentSubscriptions = tmp; + } + if (currentSubscriptions.equals(mSubscriptions)) { + return; + } + mSubscriptions.clear(); + mSubscriptions.addAll(currentSubscriptions); + mAdapter.notifyDataSetChanged(); + } + + @VisibleForTesting + void setAdapter(AlertDialog.Builder builder) { + builder.setAdapter(mAdapter, this); + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.DIALOG_SIM_LIST; + } + + private static class SelectSubscriptionAdapter extends BaseAdapter { + private Context mContext; + private LayoutInflater mInflater; + List<SubscriptionInfo> mSubscriptions; + + public SelectSubscriptionAdapter(Context context, List<SubscriptionInfo> subscriptions) { + mSubscriptions = subscriptions; + mContext = context; + } + + @Override + public int getCount() { + return mSubscriptions.size(); + } + + @Override + public SubscriptionInfo getItem(int position) { + return mSubscriptions.get(position); + } + + @Override + public long getItemId(int position) { + final SubscriptionInfo info = mSubscriptions.get(position); + if (info == null) { + return SubscriptionManager.INVALID_SUBSCRIPTION_ID; + } + return info.getSubscriptionId(); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + if (mInflater == null) { + mInflater = LayoutInflater.from(parent.getContext()); + } + convertView = mInflater.inflate(R.layout.select_account_list_item, parent, false); + } + final SubscriptionInfo sub = getItem(position); + + final TextView title = convertView.findViewById(R.id.title); + final TextView summary = convertView.findViewById(R.id.summary); + final ImageView icon = convertView.findViewById(R.id.icon); + + if (sub == null) { + title.setText(R.string.sim_calls_ask_first_prefs_title); + summary.setText(""); + icon.setImageDrawable(mContext.getDrawable(R.drawable.ic_feedback_24dp)); + icon.setImageTintList( + Utils.getColorAttr(mContext, android.R.attr.textColorSecondary)); + } else { + title.setText(sub.getDisplayName()); + summary.setText(sub.getNumber()); + icon.setImageBitmap(sub.createIconBitmap(mContext)); + + } + return convertView; + } + } +} diff --git a/src/com/android/settings/sim/SimPreferenceDialog.java b/src/com/android/settings/sim/SimPreferenceDialog.java index 2b6ebe3b2e..0f22d2bfe2 100644 --- a/src/com/android/settings/sim/SimPreferenceDialog.java +++ b/src/com/android/settings/sim/SimPreferenceDialog.java @@ -16,7 +16,6 @@ package com.android.settings.sim; import android.app.Activity; -import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.res.Resources; @@ -39,6 +38,8 @@ import android.widget.ImageView; import android.widget.Spinner; import android.widget.TextView; +import androidx.appcompat.app.AlertDialog; + import com.android.settings.R; import com.android.settings.Utils; diff --git a/src/com/android/settings/sim/SimSelectNotification.java b/src/com/android/settings/sim/SimSelectNotification.java index a1e942a8ab..fb83a4d67c 100644 --- a/src/com/android/settings/sim/SimSelectNotification.java +++ b/src/com/android/settings/sim/SimSelectNotification.java @@ -16,12 +16,22 @@ package com.android.settings.sim; +import static android.provider.Settings.ENABLE_MMS_DATA_REQUEST_REASON_INCOMING_MMS; +import static android.provider.Settings.ENABLE_MMS_DATA_REQUEST_REASON_OUTGOING_MMS; +import static android.provider.Settings.EXTRA_ENABLE_MMS_DATA_REQUEST_REASON; +import static android.provider.Settings.EXTRA_SUB_ID; import static android.telephony.TelephonyManager.EXTRA_DEFAULT_SUBSCRIPTION_SELECT_TYPE; import static android.telephony.TelephonyManager.EXTRA_DEFAULT_SUBSCRIPTION_SELECT_TYPE_ALL; import static android.telephony.TelephonyManager.EXTRA_DEFAULT_SUBSCRIPTION_SELECT_TYPE_DATA; import static android.telephony.TelephonyManager.EXTRA_DEFAULT_SUBSCRIPTION_SELECT_TYPE_NONE; +import static android.telephony.TelephonyManager.EXTRA_SIM_COMBINATION_NAMES; +import static android.telephony.TelephonyManager.EXTRA_SIM_COMBINATION_WARNING_TYPE; +import static android.telephony.TelephonyManager.EXTRA_SIM_COMBINATION_WARNING_TYPE_DUAL_CDMA; +import static android.telephony.TelephonyManager.EXTRA_SIM_COMBINATION_WARNING_TYPE_NONE; import static android.telephony.TelephonyManager.EXTRA_SUBSCRIPTION_ID; +import static android.telephony.data.ApnSetting.TYPE_MMS; +import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; @@ -29,33 +39,127 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.res.Resources; +import android.provider.Settings; +import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; +import android.util.Log; +import com.android.internal.annotations.VisibleForTesting; import com.android.settings.R; -import com.android.settings.Settings.SimSettingsActivity; - -import androidx.core.app.NotificationCompat; +import com.android.settings.network.SubscriptionUtil; +import com.android.settings.network.telephony.MobileNetworkActivity; +import com.android.settingslib.HelpUtils; public class SimSelectNotification extends BroadcastReceiver { private static final String TAG = "SimSelectNotification"; - private static final int NOTIFICATION_ID = 1; + @VisibleForTesting + public static final int SIM_SELECT_NOTIFICATION_ID = 1; + @VisibleForTesting + public static final int ENABLE_MMS_NOTIFICATION_ID = 2; + @VisibleForTesting + public static final int SIM_WARNING_NOTIFICATION_ID = 3; - private static final String SIM_SELECT_NOTIFICATION_CHANNEL = + @VisibleForTesting + public static final String SIM_SELECT_NOTIFICATION_CHANNEL = "sim_select_notification_channel"; + @VisibleForTesting + public static final String ENABLE_MMS_NOTIFICATION_CHANNEL = + "enable_mms_notification_channel"; + + @VisibleForTesting + public static final String SIM_WARNING_NOTIFICATION_CHANNEL = + "sim_warning_notification_channel"; + @Override public void onReceive(Context context, Intent intent) { - if (!TelephonyManager.ACTION_PRIMARY_SUBSCRIPTION_LIST_CHANGED.equals(intent.getAction())) { + String action = intent.getAction(); + + if (action == null) { + Log.w(TAG, "Received unexpected intent with null action."); return; } - // Cancel any previous notifications - cancelNotification(context); - // Create a notification to tell the user that some defaults are missing - createNotification(context); + switch (action) { + case TelephonyManager.ACTION_PRIMARY_SUBSCRIPTION_LIST_CHANGED: + onPrimarySubscriptionListChanged(context, intent); + break; + case Settings.ACTION_ENABLE_MMS_DATA_REQUEST: + onEnableMmsDataRequest(context, intent); + break; + default: + Log.w(TAG, "Received unexpected intent " + intent.getAction()); + } + } + + private void onEnableMmsDataRequest(Context context, Intent intent) { + // Getting subId from extra. + int subId = intent.getIntExtra(EXTRA_SUB_ID, SubscriptionManager.INVALID_SUBSCRIPTION_ID); + if (subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) { + subId = SubscriptionManager.getDefaultSmsSubscriptionId(); + } + + SubscriptionManager subscriptionManager = ((SubscriptionManager) context.getSystemService( + Context.TELEPHONY_SUBSCRIPTION_SERVICE)); + if (!subscriptionManager.isActiveSubId(subId)) { + Log.w(TAG, "onEnableMmsDataRequest invalid sub ID " + subId); + return; + } + final SubscriptionInfo info = subscriptionManager.getActiveSubscriptionInfo(subId); + if (info == null) { + Log.w(TAG, "onEnableMmsDataRequest null SubscriptionInfo for sub ID " + subId); + return; + } + + // Getting request reason from extra, which will determine the notification title. + CharSequence notificationTitle = null; + int requestReason = intent.getIntExtra(EXTRA_ENABLE_MMS_DATA_REQUEST_REASON, -1); + if (requestReason == ENABLE_MMS_DATA_REQUEST_REASON_INCOMING_MMS) { + notificationTitle = context.getResources().getText( + R.string.enable_receiving_mms_notification_title); + } else if (requestReason == ENABLE_MMS_DATA_REQUEST_REASON_OUTGOING_MMS) { + notificationTitle = context.getResources().getText( + R.string.enable_sending_mms_notification_title); + } else { + Log.w(TAG, "onEnableMmsDataRequest invalid request reason " + requestReason); + return; + } + + TelephonyManager tm = ((TelephonyManager) context.getSystemService( + Context.TELEPHONY_SERVICE)).createForSubscriptionId(subId); + + if (tm.isDataEnabledForApn(TYPE_MMS)) { + Log.w(TAG, "onEnableMmsDataRequest MMS data already enabled on sub ID " + subId); + return; + } + + CharSequence notificationSummary = context.getResources().getString( + R.string.enable_mms_notification_summary, SubscriptionUtil.getDisplayName(info)); + + cancelEnableMmsNotification(context); + + createEnableMmsNotification(context, notificationTitle, notificationSummary, subId); + } + + private void onPrimarySubscriptionListChanged(Context context, Intent intent) { + startSimSelectDialogIfNeeded(context, intent); + sendSimCombinationWarningIfNeeded(context, intent); + } + + private void startSimSelectDialogIfNeeded(Context context, Intent intent) { int dialogType = intent.getIntExtra(EXTRA_DEFAULT_SUBSCRIPTION_SELECT_TYPE, EXTRA_DEFAULT_SUBSCRIPTION_SELECT_TYPE_NONE); + + if (dialogType == EXTRA_DEFAULT_SUBSCRIPTION_SELECT_TYPE_NONE) { + return; + } + + // Cancel any previous notifications + cancelSimSelectNotification(context); + // Create a notification to tell the user that some defaults are missing + createSimSelectNotification(context); + if (dialogType == EXTRA_DEFAULT_SUBSCRIPTION_SELECT_TYPE_ALL) { int subId = intent.getIntExtra(EXTRA_SUBSCRIPTION_ID, SubscriptionManager.DEFAULT_SUBSCRIPTION_ID); @@ -63,11 +167,12 @@ public class SimSelectNotification extends BroadcastReceiver { // If there is only one subscription, ask if user wants to use if for everything Intent newIntent = new Intent(context, SimDialogActivity.class); newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - newIntent.putExtra(SimDialogActivity.DIALOG_TYPE_KEY, SimDialogActivity.PREFERRED_PICK); + newIntent.putExtra(SimDialogActivity.DIALOG_TYPE_KEY, + SimDialogActivity.PREFERRED_PICK); newIntent.putExtra(SimDialogActivity.PREFERRED_SIM, slotIndex); context.startActivity(newIntent); } else if (dialogType == EXTRA_DEFAULT_SUBSCRIPTION_SELECT_TYPE_DATA) { - // If there are mulitple, ensure they pick default data + // If there are multiple, ensure they pick default data Intent newIntent = new Intent(context, SimDialogActivity.class); newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); newIntent.putExtra(SimDialogActivity.DIALOG_TYPE_KEY, SimDialogActivity.DATA_PICK); @@ -75,21 +180,34 @@ public class SimSelectNotification extends BroadcastReceiver { } } - private void createNotification(Context context){ + private void sendSimCombinationWarningIfNeeded(Context context, Intent intent) { + final int warningType = intent.getIntExtra(EXTRA_SIM_COMBINATION_WARNING_TYPE, + EXTRA_SIM_COMBINATION_WARNING_TYPE_NONE); + + if (warningType == EXTRA_SIM_COMBINATION_WARNING_TYPE_DUAL_CDMA) { + // Cancel any previous notifications + cancelSimCombinationWarningNotification(context); + // Create a notification to tell the user that some defaults are missing + createSimCombinationWarningNotification(context, intent); + } + } + + private void createSimSelectNotification(Context context){ final Resources resources = context.getResources(); NotificationChannel notificationChannel = new NotificationChannel( SIM_SELECT_NOTIFICATION_CHANNEL, - resources.getString(R.string.sim_selection_channel_title), + resources.getText(R.string.sim_selection_channel_title), NotificationManager.IMPORTANCE_LOW); - NotificationCompat.Builder builder = - new NotificationCompat.Builder(context, SIM_SELECT_NOTIFICATION_CHANNEL) + Notification.Builder builder = + new Notification.Builder(context, SIM_SELECT_NOTIFICATION_CHANNEL) .setSmallIcon(R.drawable.ic_sim_card_alert_white_48dp) .setColor(context.getColor(R.color.sim_noitification)) - .setContentTitle(resources.getString(R.string.sim_notification_title)) - .setContentText(resources.getString(R.string.sim_notification_summary)); - Intent resultIntent = new Intent(context, SimSettingsActivity.class); + .setContentTitle(resources.getText(R.string.sim_notification_title)) + .setContentText(resources.getText(R.string.sim_notification_summary)) + .setAutoCancel(true); + Intent resultIntent = new Intent(Settings.ACTION_WIRELESS_SETTINGS); resultIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); PendingIntent resultPendingIntent = PendingIntent.getActivity(context, 0, resultIntent, PendingIntent.FLAG_CANCEL_CURRENT); @@ -97,12 +215,100 @@ public class SimSelectNotification extends BroadcastReceiver { NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.createNotificationChannel(notificationChannel); - notificationManager.notify(NOTIFICATION_ID, builder.build()); + notificationManager.notify(SIM_SELECT_NOTIFICATION_ID, builder.build()); } - public static void cancelNotification(Context context) { + public static void cancelSimSelectNotification(Context context) { NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.cancel(NOTIFICATION_ID); + notificationManager.cancel(SIM_SELECT_NOTIFICATION_ID); + } + + private void createEnableMmsNotification(Context context, CharSequence titleString, + CharSequence notificationSummary, int subId) { + final Resources resources = context.getResources(); + + NotificationChannel notificationChannel = new NotificationChannel( + ENABLE_MMS_NOTIFICATION_CHANNEL, + resources.getText(R.string.enable_mms_notification_channel_title), + NotificationManager.IMPORTANCE_HIGH); + + Notification.Builder builder = + new Notification.Builder(context, ENABLE_MMS_NOTIFICATION_CHANNEL) + .setSmallIcon(R.drawable.ic_settings_24dp) + .setColor(context.getColor(R.color.sim_noitification)) + .setContentTitle(titleString) + .setContentText(notificationSummary) + .setStyle(new Notification.BigTextStyle().bigText(notificationSummary)) + .setAutoCancel(true); + + // Create the pending intent that will lead to the subscription setting page. + Intent resultIntent = new Intent(Settings.ACTION_MMS_MESSAGE_SETTING); + resultIntent.setClass(context, MobileNetworkActivity.class); + resultIntent.putExtra(Settings.EXTRA_SUB_ID, subId); + resultIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + PendingIntent resultPendingIntent = PendingIntent.getActivity(context, 0, resultIntent, + PendingIntent.FLAG_CANCEL_CURRENT); + builder.setContentIntent(resultPendingIntent); + + // Notify the notification. + NotificationManager notificationManager = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.createNotificationChannel(notificationChannel); + notificationManager.notify(ENABLE_MMS_NOTIFICATION_ID, builder.build()); + } + + private void cancelEnableMmsNotification(Context context) { + NotificationManager notificationManager = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(ENABLE_MMS_NOTIFICATION_ID); + } + + private void createSimCombinationWarningNotification(Context context, Intent intent){ + final Resources resources = context.getResources(); + final String simNames = intent.getStringExtra(EXTRA_SIM_COMBINATION_NAMES); + + if (simNames == null) { + return; + } + + CharSequence dualCdmaSimWarningSummary = resources.getString( + R.string.dual_cdma_sim_warning_notification_summary, simNames); + + NotificationChannel notificationChannel = new NotificationChannel( + SIM_WARNING_NOTIFICATION_CHANNEL, + resources.getText(R.string.dual_cdma_sim_warning_notification_channel_title), + NotificationManager.IMPORTANCE_HIGH); + + Notification.Builder builder = + new Notification.Builder(context, SIM_WARNING_NOTIFICATION_CHANNEL) + .setSmallIcon(R.drawable.ic_sim_card_alert_white_48dp) + .setColor(context.getColor(R.color.sim_noitification)) + .setContentTitle(resources.getText( + R.string.sim_combination_warning_notification_title)) + .setContentText(dualCdmaSimWarningSummary) + .setStyle(new Notification.BigTextStyle().bigText( + dualCdmaSimWarningSummary)) + .setAutoCancel(true); + + // Create the pending intent that will lead to the helper page. + Intent resultIntent = HelpUtils.getHelpIntent( + context, + context.getString(R.string.help_uri_sim_combination_warning), + context.getClass().getName()); + PendingIntent resultPendingIntent = PendingIntent.getActivity(context, 0, resultIntent, + PendingIntent.FLAG_CANCEL_CURRENT); + builder.setContentIntent(resultPendingIntent); + + NotificationManager notificationManager = + context.getSystemService(NotificationManager.class); + notificationManager.createNotificationChannel(notificationChannel); + notificationManager.notify(SIM_WARNING_NOTIFICATION_ID, builder.build()); + } + + public static void cancelSimCombinationWarningNotification(Context context) { + NotificationManager notificationManager = + context.getSystemService(NotificationManager.class); + notificationManager.cancel(SIM_WARNING_NOTIFICATION_ID); } } diff --git a/src/com/android/settings/sim/SimSettings.java b/src/com/android/settings/sim/SimSettings.java index b0cf194e04..1222913d8c 100644 --- a/src/com/android/settings/sim/SimSettings.java +++ b/src/com/android/settings/sim/SimSettings.java @@ -16,6 +16,7 @@ package com.android.settings.sim; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; import android.content.res.Resources; @@ -23,8 +24,6 @@ import android.graphics.drawable.BitmapDrawable; import android.os.Bundle; import android.os.SystemProperties; import android.provider.SearchIndexableResource; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; import android.telecom.PhoneAccountHandle; import android.telecom.TelecomManager; import android.telephony.PhoneNumberUtils; @@ -35,17 +34,21 @@ import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.Log; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + import com.android.internal.telephony.TelephonyProperties; import com.android.settings.R; import com.android.settings.RestrictedSettingsFragment; import com.android.settings.Utils; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.List; +@SearchIndexable public class SimSettings extends RestrictedSettingsFragment implements Indexable { private static final String TAG = "SimSettings"; private static final boolean DBG = false; @@ -81,7 +84,7 @@ public class SimSettings extends RestrictedSettingsFragment implements Indexable @Override public int getMetricsCategory() { - return MetricsEvent.SIM; + return SettingsEnums.SIM; } @Override @@ -98,7 +101,7 @@ public class SimSettings extends RestrictedSettingsFragment implements Indexable mSimCards = (PreferenceScreen)findPreference(SIM_CARD_CATEGORY); mAvailableSubInfos = new ArrayList<SubscriptionInfo>(mNumSlots); mSelectableSubInfos = new ArrayList<SubscriptionInfo>(); - SimSelectNotification.cancelNotification(getActivity()); + SimSelectNotification.cancelSimSelectNotification(getActivity()); } private final SubscriptionManager.OnSubscriptionsChangedListener mOnSubscriptionsChangeListener @@ -111,7 +114,7 @@ public class SimSettings extends RestrictedSettingsFragment implements Indexable }; private void updateSubscriptions() { - mSubInfoList = mSubscriptionManager.getActiveSubscriptionInfoList(); + mSubInfoList = mSubscriptionManager.getActiveSubscriptionInfoList(true); for (int i = 0; i < mNumSlots; ++i) { Preference pref = mSimCards.findPreference("sim" + i); if (pref instanceof SimPreference) { @@ -217,7 +220,7 @@ public class SimSettings extends RestrictedSettingsFragment implements Indexable Log.d(TAG, "Register for call state change"); for (int i = 0; i < mPhoneCount; i++) { int subId = mSelectableSubInfos.get(i).getSubscriptionId(); - tm.listen(getPhoneStateListener(i, subId), + tm.createForSubscriptionId(subId).listen(getPhoneStateListener(i), PhoneStateListener.LISTEN_CALL_STATE); } } @@ -236,13 +239,13 @@ public class SimSettings extends RestrictedSettingsFragment implements Indexable } } - private PhoneStateListener getPhoneStateListener(int phoneId, int subId) { + private PhoneStateListener getPhoneStateListener(int phoneId) { // Disable Sim selection for Data when voice call is going on as changing the default data // sim causes a modem reset currently and call gets disconnected // ToDo : Add subtext on disabled preference to let user know that default data sim cannot // be changed while call is going on final int i = phoneId; - mPhoneStateListener[phoneId] = new PhoneStateListener(subId) { + mPhoneStateListener[phoneId] = new PhoneStateListener() { @Override public void onCallStateChanged(int state, String incomingNumber) { if (DBG) log("PhoneStateListener.onCallStateChanged: state=" + state); diff --git a/src/com/android/settings/slices/BlockingSlicePrefController.java b/src/com/android/settings/slices/BlockingSlicePrefController.java new file mode 100644 index 0000000000..94810c565a --- /dev/null +++ b/src/com/android/settings/slices/BlockingSlicePrefController.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2018 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.slices; + +import android.content.Context; + +import androidx.slice.Slice; + +import com.android.settings.core.BasePreferenceController; + +/** + * The blocking slice preference controller. It will make whole page invisible for a certain time + * until {@link Slice} is fully loaded. + */ +public class BlockingSlicePrefController extends SlicePreferenceController implements + BasePreferenceController.UiBlocker { + + public BlockingSlicePrefController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public void onChanged(Slice slice) { + super.onChanged(slice); + if (mUiBlockListener != null) { + mUiBlockListener.onBlockerWorkFinished(this); + } + } +} diff --git a/src/com/android/settings/slices/CustomSliceRegistry.java b/src/com/android/settings/slices/CustomSliceRegistry.java new file mode 100644 index 0000000000..dc3324b3d9 --- /dev/null +++ b/src/com/android/settings/slices/CustomSliceRegistry.java @@ -0,0 +1,371 @@ +/* + * Copyright (C) 2018 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.slices; + +import static android.provider.SettingsSlicesContract.KEY_LOCATION; +import static android.provider.SettingsSlicesContract.KEY_WIFI; + +import android.content.ContentResolver; +import android.net.Uri; +import android.provider.SettingsSlicesContract; +import android.util.ArrayMap; + +import androidx.annotation.VisibleForTesting; + +import com.android.settings.flashlight.FlashlightSlice; +import com.android.settings.fuelgauge.batterytip.BatteryTipPreferenceController; +import com.android.settings.homepage.contextualcards.deviceinfo.DataUsageSlice; +import com.android.settings.homepage.contextualcards.deviceinfo.DeviceInfoSlice; +import com.android.settings.homepage.contextualcards.deviceinfo.EmergencyInfoSlice; +import com.android.settings.homepage.contextualcards.deviceinfo.StorageSlice; +import com.android.settings.homepage.contextualcards.slices.BatteryFixSlice; +import com.android.settings.homepage.contextualcards.slices.BluetoothDevicesSlice; +import com.android.settings.homepage.contextualcards.slices.ContextualNotificationChannelSlice; +import com.android.settings.homepage.contextualcards.slices.LowStorageSlice; +import com.android.settings.homepage.contextualcards.slices.NotificationChannelSlice; +import com.android.settings.location.LocationSlice; +import com.android.settings.media.MediaOutputIndicatorSlice; +import com.android.settings.media.MediaOutputSlice; +import com.android.settings.network.telephony.MobileDataSlice; +import com.android.settings.notification.ZenModeButtonPreferenceController; +import com.android.settings.wifi.calling.WifiCallingSliceHelper; +import com.android.settings.wifi.slice.ContextualWifiSlice; +import com.android.settings.wifi.slice.WifiSlice; +import com.android.settingslib.media.MediaOutputSliceConstants; + +import java.util.Map; + +/** + * A registry of custom slice Uris. + */ +public class CustomSliceRegistry { + + /** + * Uri for Airplane mode Slice. + */ + public static final Uri AIRPLANE_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSlicesContract.AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath(SettingsSlicesContract.KEY_AIRPLANE_MODE) + .build(); + + /** + * Uri for Battery Fix Slice. + */ + public static final Uri BATTERY_FIX_SLICE_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendEncodedPath(SettingsSlicesContract.PATH_SETTING_INTENT) + .appendPath(BatteryTipPreferenceController.PREF_NAME) + .build(); + + /** + * Backing Uri for the Bluetooth Slice. + */ + public static final Uri BLUETOOTH_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSlicesContract.AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath(SettingsSlicesContract.KEY_BLUETOOTH) + .build(); + + /** + * Backing Uri for Bluetooth devices Slice. + */ + public static final Uri BLUETOOTH_DEVICES_SLICE_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath("bluetooth_devices") + .build(); + + /** + * Backing Uri for Contextual Notification channel Slice. + */ + public static final Uri CONTEXTUAL_NOTIFICATION_CHANNEL_SLICE_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath("contextual_notification_channel") + .build(); + + /** + * Backing Uri for the Wifi Slice. + */ + public static final Uri CONTEXTUAL_WIFI_SLICE_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSlicesContract.AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath("contextual_wifi") + .build(); + + /** + * Backing Uri for the Data usage Slice. + */ + public static final Uri DATA_USAGE_SLICE_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath("data_usage_card") + .build(); + /** + * Backing Uri for the Device info Slice. + */ + public static final Uri DEVICE_INFO_SLICE_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_INTENT) + .appendPath("device_info_card") + .build(); + /** + * Backing Uri for the Emergency Info Slice. + */ + public static final Uri EMERGENCY_INFO_SLICE_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_INTENT) + .appendPath("emergency_info_card") + .build(); + /** + * Slice Uri for Enhanced 4G slice + */ + public static final Uri ENHANCED_4G_SLICE_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath("enhanced_4g_lte") + .build(); + /** + * Backing Uri for the Flashlight Slice. + */ + public static final Uri FLASHLIGHT_SLICE_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath("flashlight") + .build(); + /** + * Backing Uri for the Location Slice. + */ + public static final Uri LOCATION_SLICE_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSlicesContract.AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath(KEY_LOCATION) + .build(); + /** + * Backing Uri for Low storage Slice. + */ + public static final Uri LOW_STORAGE_SLICE_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendEncodedPath(SettingsSlicesContract.PATH_SETTING_INTENT) + .appendPath("low_storage") + .build(); + /** + * Backing Uri for NFC Slice + */ + public static final Uri NFC_SLICE_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath("toggle_nfc") + .build(); + + /** + * Backing Uri for Mobile Data Slice. + */ + public static final Uri MOBILE_DATA_SLICE_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendEncodedPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath("mobile_data") + .build(); + /** + * Backing Uri for Notification channel Slice. + */ + public static final Uri NOTIFICATION_CHANNEL_SLICE_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath("notification_channel") + .build(); + /** + * Backing Uri for the storage slice. + */ + public static final Uri STORAGE_SLICE_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_INTENT) + .appendPath("storage_card") + .build(); + /** + * Full {@link Uri} for the Alarm volume Slice. + */ + public static final Uri VOLUME_ALARM_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath("alarm_volume") + .build(); + /** + * Full {@link Uri} for the Call Volume Slice. + */ + public static final Uri VOLUME_CALL_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath("call_volume") + .build(); + /** + * Full {@link Uri} for the Media Volume Slice. + */ + public static final Uri VOLUME_MEDIA_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath("media_volume") + .build(); + + /** + * Full {@link Uri} for the Remote Media Volume Slice. + */ + public static final Uri VOLUME_REMOTE_MEDIA_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath("remote_volume") + .build(); + + /** + * Full {@link Uri} for the Ringer volume Slice. + */ + public static final Uri VOLUME_RINGER_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath("ring_volume") + .build(); + + /** + * Full {@link Uri} for the Wifi Calling Slice. + */ + public static final Uri WIFI_CALLING_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_INTENT) + .appendPath(WifiCallingSliceHelper.PATH_WIFI_CALLING) + .build(); + /** + * Full {@link Uri} for the Wifi Calling Preference Slice. + */ + public static final Uri WIFI_CALLING_PREFERENCE_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath(WifiCallingSliceHelper.PATH_WIFI_CALLING_PREFERENCE) + .build(); + /** + * Backing Uri for the Wifi Slice. + */ + public static final Uri WIFI_SLICE_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSlicesContract.AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath(KEY_WIFI) + .build(); + + /** + * Backing Uri for the Zen Mode Slice. + */ + public static final Uri ZEN_MODE_SLICE_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath(ZenModeButtonPreferenceController.KEY) + .build(); + + /** + * Backing Uri for the Media output Slice. + */ + public static Uri MEDIA_OUTPUT_SLICE_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath(MediaOutputSliceConstants.KEY_MEDIA_OUTPUT) + .build(); + + /** + * Backing Uri for the Media output indicator Slice. + */ + public static Uri MEDIA_OUTPUT_INDICATOR_SLICE_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_INTENT) + .appendPath("media_output_indicator") + .build(); + + @VisibleForTesting + static final Map<Uri, Class<? extends CustomSliceable>> sUriToSlice; + + static { + sUriToSlice = new ArrayMap<>(); + + sUriToSlice.put(BATTERY_FIX_SLICE_URI, BatteryFixSlice.class); + sUriToSlice.put(BLUETOOTH_DEVICES_SLICE_URI, BluetoothDevicesSlice.class); + sUriToSlice.put(CONTEXTUAL_NOTIFICATION_CHANNEL_SLICE_URI, + ContextualNotificationChannelSlice.class); + sUriToSlice.put(CONTEXTUAL_WIFI_SLICE_URI, ContextualWifiSlice.class); + sUriToSlice.put(DATA_USAGE_SLICE_URI, DataUsageSlice.class); + sUriToSlice.put(DEVICE_INFO_SLICE_URI, DeviceInfoSlice.class); + sUriToSlice.put(EMERGENCY_INFO_SLICE_URI, EmergencyInfoSlice.class); + sUriToSlice.put(FLASHLIGHT_SLICE_URI, FlashlightSlice.class); + sUriToSlice.put(LOCATION_SLICE_URI, LocationSlice.class); + sUriToSlice.put(LOW_STORAGE_SLICE_URI, LowStorageSlice.class); + sUriToSlice.put(MOBILE_DATA_SLICE_URI, MobileDataSlice.class); + sUriToSlice.put(NOTIFICATION_CHANNEL_SLICE_URI, NotificationChannelSlice.class); + sUriToSlice.put(STORAGE_SLICE_URI, StorageSlice.class); + sUriToSlice.put(WIFI_SLICE_URI, WifiSlice.class); + sUriToSlice.put(MEDIA_OUTPUT_SLICE_URI, MediaOutputSlice.class); + sUriToSlice.put(MEDIA_OUTPUT_INDICATOR_SLICE_URI, MediaOutputIndicatorSlice.class); + } + + public static Class<? extends CustomSliceable> getSliceClassByUri(Uri uri) { + return sUriToSlice.get(removeParameterFromUri(uri)); + } + + public static Uri removeParameterFromUri(Uri uri) { + return uri != null ? uri.buildUpon().clearQuery().build() : null; + } + + /** + * Returns {@code true} if {@param uri} is a valid Slice Uri handled by + * {@link CustomSliceRegistry}. + */ + public static boolean isValidUri(Uri uri) { + return sUriToSlice.containsKey(removeParameterFromUri(uri)); + } + + /** + * Returns {@code true} if {@param action} is a valid intent action handled by + * {@link CustomSliceRegistry}. + */ + public static boolean isValidAction(String action) { + return isValidUri(Uri.parse(action)); + } +} diff --git a/src/com/android/settings/slices/CustomSliceable.java b/src/com/android/settings/slices/CustomSliceable.java new file mode 100644 index 0000000000..9566be1285 --- /dev/null +++ b/src/com/android/settings/slices/CustomSliceable.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2018 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.slices; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; + +import androidx.slice.Slice; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + + +/** + * Common functions for custom Slices. + * <p> + * A template for all Settings slices which are not represented by a Preference. By + * standardizing the methods used by the Slice helpers, we can use generically take actions + * rather than maintaining a list of all of the custom slices every time we reference Slices in + * Settings. + * <p> + * By default, all Slices in Settings should be built through Preference Controllers extending + * {@link com.android.settings.core.BasePreferenceController}, which are automatically piped + * into Settings-Slices infrastructure. Cases where you should implement this interface are: + * <ul> + * <li>Multi-line slices</li> + * <li>Slices that don't exist in the UI</li> + * <li>Preferences that use a supported component, like a Switch Bar</li> + * </ul> + * <p> + * Note that if your UI is supported because the Preference is not backed by a + * {@link com.android.settings.dashboard.DashboardFragment}, then you should first convert the + * existing fragment into a dashboard fragment, and then extend + * {@link com.android.settings.core.BasePreferenceController}. + * <p> + * If you implement this interface, you should add your Slice to {@link CustomSliceManager}. + */ +public interface CustomSliceable extends Sliceable { + + /** + * The color representing not to be tinted for the slice. + */ + int COLOR_NOT_TINTED = -1; + + /** + * @return an complete instance of the {@link Slice}. + */ + Slice getSlice(); + + /** + * @return a {@link android.content.ContentResolver#SCHEME_CONTENT content} {@link Uri} which + * backs the {@link Slice} returned by {@link #getSlice()}. + */ + Uri getUri(); + + /** + * Handles the actions sent by the {@link Intent intents} bound to the {@link Slice} returned by + * {@link #getSlice()}. + * + * @param intent which has the action taken on a {@link Slice}. + */ + default void onNotifyChange(Intent intent) {} + + /** + * @return an {@link Intent} to the source of the Slice data. + */ + Intent getIntent(); + + /** + * Standardize the intents returned to indicate actions by the Slice. + * <p> + * The {@link PendingIntent} is linked to {@link SliceBroadcastReceiver} where the Intent + * Action is found by {@code getUri().toString()}. + * + * @return a {@link PendingIntent} linked to {@link SliceBroadcastReceiver}. + */ + default PendingIntent getBroadcastIntent(Context context) { + final Intent intent = new Intent(getUri().toString()) + .setData(getUri()) + .setClass(context, SliceBroadcastReceiver.class); + return PendingIntent.getBroadcast(context, 0 /* requestCode */, intent, + PendingIntent.FLAG_CANCEL_CURRENT); + } + + @Override + default boolean isSliceable() { + return true; + } + + /** + * Build an instance of a {@link CustomSliceable} which has a {@link Context}-only constructor. + */ + static CustomSliceable createInstance(Context context, + Class<? extends CustomSliceable> sliceable) { + try { + final Constructor<? extends CustomSliceable> constructor = + sliceable.getConstructor(Context.class); + final Object[] params = new Object[]{context.getApplicationContext()}; + return constructor.newInstance(params); + } catch (NoSuchMethodException | InstantiationException | + IllegalArgumentException | InvocationTargetException | IllegalAccessException e) { + throw new IllegalStateException( + "Invalid sliceable class: " + sliceable, e); + } + } +}
\ No newline at end of file diff --git a/src/com/android/settings/slices/SettingsSliceProvider.java b/src/com/android/settings/slices/SettingsSliceProvider.java index b327f02971..9b5fbd8625 100644 --- a/src/com/android/settings/slices/SettingsSliceProvider.java +++ b/src/com/android/settings/slices/SettingsSliceProvider.java @@ -18,32 +18,37 @@ package com.android.settings.slices; import static android.Manifest.permission.READ_SEARCH_INDEXABLES; +import android.app.PendingIntent; import android.app.slice.SliceManager; import android.content.ContentResolver; +import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.Uri; import android.os.StrictMode; import android.provider.Settings; import android.provider.SettingsSlicesContract; -import androidx.annotation.VisibleForTesting; import android.text.TextUtils; -import android.util.ArraySet; +import android.util.ArrayMap; import android.util.KeyValueListParser; import android.util.Log; import android.util.Pair; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.collection.ArraySet; +import androidx.slice.Slice; +import androidx.slice.SliceProvider; + +import com.android.settings.R; import com.android.settings.bluetooth.BluetoothSliceBuilder; import com.android.settings.core.BasePreferenceController; -import com.android.settings.location.LocationSliceBuilder; import com.android.settings.notification.ZenModeSliceBuilder; import com.android.settings.overlay.FeatureFactory; -import com.android.settings.wifi.WifiSliceBuilder; -import com.android.settings.wifi.calling.WifiCallingSliceHelper; import com.android.settingslib.SliceBroadcastRelay; import com.android.settingslib.utils.ThreadUtils; -import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -52,10 +57,6 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; -import java.util.concurrent.ConcurrentHashMap; - -import androidx.slice.Slice; -import androidx.slice.SliceProvider; /** * A {@link SliceProvider} for Settings to enabled inline results in system apps. @@ -101,6 +102,12 @@ public class SettingsSliceProvider extends SliceProvider { "com.android.settings.slice.action.SLIDER_CHANGED"; /** + * Action passed for copy data for the Copyable Slices. + */ + public static final String ACTION_COPY = + "com.android.settings.slice.action.COPY"; + + /** * Intent Extra passed for the key identifying the Setting Slice. */ public static final String EXTRA_SLICE_KEY = "com.android.settings.slice.extra.key"; @@ -111,51 +118,45 @@ public class SettingsSliceProvider extends SliceProvider { public static final String EXTRA_SLICE_PLATFORM_DEFINED = "com.android.settings.slice.extra.platform"; + private static final KeyValueListParser KEY_VALUE_LIST_PARSER = new KeyValueListParser(','); + @VisibleForTesting SlicesDatabaseAccessor mSlicesDatabaseAccessor; @VisibleForTesting Map<Uri, SliceData> mSliceWeakDataCache; - @VisibleForTesting - Map<Uri, SliceData> mSliceDataCache; - - private final KeyValueListParser mParser; - final Set<Uri> mRegisteredUris = new ArraySet<>(); + final Map<Uri, SliceBackgroundWorker> mPinnedWorkers = new ArrayMap<>(); public SettingsSliceProvider() { super(READ_SEARCH_INDEXABLES); - mParser = new KeyValueListParser(','); } @Override public boolean onCreateSliceProvider() { mSlicesDatabaseAccessor = new SlicesDatabaseAccessor(getContext()); - mSliceDataCache = new ConcurrentHashMap<>(); mSliceWeakDataCache = new WeakHashMap<>(); return true; } @Override - public Uri onMapIntentToUri(Intent intent) { - try { - return getContext().getSystemService(SliceManager.class).mapIntentToUri( - SliceDeepLinkSpringBoard.parse( - intent.getData(), getContext().getPackageName())); - } catch (URISyntaxException e) { - return null; - } - } - - @Override public void onSlicePinned(Uri sliceUri) { - if (WifiSliceBuilder.WIFI_URI.equals(sliceUri)) { - registerIntentToUri(WifiSliceBuilder.INTENT_FILTER, sliceUri); + if (CustomSliceRegistry.isValidUri(sliceUri)) { + final Context context = getContext(); + final CustomSliceable sliceable = FeatureFactory.getFactory(context) + .getSlicesFeatureProvider().getSliceableFromUri(context, sliceUri); + final IntentFilter filter = sliceable.getIntentFilter(); + if (filter != null) { + registerIntentToUri(filter, sliceUri); + } + ThreadUtils.postOnMainThread(() -> startBackgroundWorker(sliceable, sliceUri)); return; - } else if (ZenModeSliceBuilder.ZEN_MODE_URI.equals(sliceUri)) { + } + + if (CustomSliceRegistry.ZEN_MODE_SLICE_URI.equals(sliceUri)) { registerIntentToUri(ZenModeSliceBuilder.INTENT_FILTER, sliceUri); return; - } else if (BluetoothSliceBuilder.BLUETOOTH_URI.equals(sliceUri)) { + } else if (CustomSliceRegistry.BLUETOOTH_URI.equals(sliceUri)) { registerIntentToUri(BluetoothSliceBuilder.INTENT_FILTER, sliceUri); return; } @@ -166,12 +167,8 @@ public class SettingsSliceProvider extends SliceProvider { @Override public void onSliceUnpinned(Uri sliceUri) { - if (mRegisteredUris.contains(sliceUri)) { - Log.d(TAG, "Unregistering uri broadcast relay: " + sliceUri); - SliceBroadcastRelay.unregisterReceivers(getContext(), sliceUri); - mRegisteredUris.remove(sliceUri); - } - mSliceDataCache.remove(sliceUri); + SliceBroadcastRelay.unregisterReceivers(getContext(), sliceUri); + ThreadUtils.postOnMainThread(() -> stopBackgroundWorker(sliceUri)); } @Override @@ -190,21 +187,34 @@ public class SettingsSliceProvider extends SliceProvider { return null; } - // If adding a new Slice, do not directly match Slice URIs. - // Use {@link SlicesDatabaseAccessor}. - if (WifiCallingSliceHelper.WIFI_CALLING_URI.equals(sliceUri)) { + // Before adding a slice to {@link CustomSliceManager}, please get approval + // from the Settings team. + if (CustomSliceRegistry.isValidUri(sliceUri)) { + final Context context = getContext(); + return FeatureFactory.getFactory(context) + .getSlicesFeatureProvider().getSliceableFromUri(context, sliceUri) + .getSlice(); + } + + if (CustomSliceRegistry.WIFI_CALLING_URI.equals(sliceUri)) { return FeatureFactory.getFactory(getContext()) .getSlicesFeatureProvider() .getNewWifiCallingSliceHelper(getContext()) .createWifiCallingSlice(sliceUri); - } else if (WifiSliceBuilder.WIFI_URI.equals(sliceUri)) { - return WifiSliceBuilder.getSlice(getContext()); - } else if (ZenModeSliceBuilder.ZEN_MODE_URI.equals(sliceUri)) { + } else if (CustomSliceRegistry.ZEN_MODE_SLICE_URI.equals(sliceUri)) { return ZenModeSliceBuilder.getSlice(getContext()); - } else if (BluetoothSliceBuilder.BLUETOOTH_URI.equals(sliceUri)) { + } else if (CustomSliceRegistry.BLUETOOTH_URI.equals(sliceUri)) { return BluetoothSliceBuilder.getSlice(getContext()); - } else if (LocationSliceBuilder.LOCATION_URI.equals(sliceUri)) { - return LocationSliceBuilder.getSlice(getContext()); + } else if (CustomSliceRegistry.ENHANCED_4G_SLICE_URI.equals(sliceUri)) { + return FeatureFactory.getFactory(getContext()) + .getSlicesFeatureProvider() + .getNewEnhanced4gLteSliceHelper(getContext()) + .createEnhanced4gLteSlice(sliceUri); + } else if (CustomSliceRegistry.WIFI_CALLING_PREFERENCE_URI.equals(sliceUri)) { + return FeatureFactory.getFactory(getContext()) + .getSlicesFeatureProvider() + .getNewWifiCallingSliceHelper(getContext()) + .createWifiCallingPreferenceSlice(sliceUri); } SliceData cachedSliceData = mSliceWeakDataCache.get(sliceUri); @@ -214,7 +224,7 @@ public class SettingsSliceProvider extends SliceProvider { } // Remove the SliceData from the cache after it has been used to prevent a memory-leak. - if (!mSliceDataCache.containsKey(sliceUri)) { + if (!getPinnedSlices().contains(sliceUri)) { mSliceWeakDataCache.remove(sliceUri); } return SliceBuilderUtils.buildSlice(getContext(), cachedSliceData); @@ -285,9 +295,76 @@ public class SettingsSliceProvider extends SliceProvider { final List<String> keys = mSlicesDatabaseAccessor.getSliceKeys(isPlatformUri); descendants.addAll(buildUrisFromKeys(keys, authority)); descendants.addAll(getSpecialCaseUris(isPlatformUri)); + grantWhitelistedPackagePermissions(getContext(), descendants); return descendants; } + @Nullable + @Override + public PendingIntent onCreatePermissionRequest(@NonNull Uri sliceUri, + @NonNull String callingPackage) { + final Intent settingsIntent = new Intent(Settings.ACTION_SETTINGS); + final PendingIntent noOpIntent = PendingIntent.getActivity(getContext(), + 0 /* requestCode */, settingsIntent, 0 /* flags */); + return noOpIntent; + } + + @VisibleForTesting + static void grantWhitelistedPackagePermissions(Context context, List<Uri> descendants) { + if (descendants == null) { + Log.d(TAG, "No descendants to grant permission with, skipping."); + } + final String[] whitelistPackages = + context.getResources().getStringArray(R.array.slice_whitelist_package_names); + if (whitelistPackages == null || whitelistPackages.length == 0) { + Log.d(TAG, "No packages to whitelist, skipping."); + return; + } else { + Log.d(TAG, String.format( + "Whitelisting %d uris to %d pkgs.", + descendants.size(), whitelistPackages.length)); + } + final SliceManager sliceManager = context.getSystemService(SliceManager.class); + for (Uri descendant : descendants) { + for (String toPackage : whitelistPackages) { + sliceManager.grantSlicePermission(toPackage, descendant); + } + } + } + + private void startBackgroundWorker(Sliceable sliceable, Uri uri) { + final Class workerClass = sliceable.getBackgroundWorkerClass(); + if (workerClass == null) { + return; + } + + if (mPinnedWorkers.containsKey(uri)) { + return; + } + + Log.d(TAG, "Starting background worker for: " + uri); + final SliceBackgroundWorker worker = SliceBackgroundWorker.getInstance( + getContext(), sliceable, uri); + mPinnedWorkers.put(uri, worker); + worker.onSlicePinned(); + } + + private void stopBackgroundWorker(Uri uri) { + final SliceBackgroundWorker worker = mPinnedWorkers.get(uri); + if (worker != null) { + Log.d(TAG, "Stopping background worker for: " + uri); + worker.onSliceUnpinned(); + mPinnedWorkers.remove(uri); + } + } + + @Override + public void shutdown() { + ThreadUtils.postOnMainThread(() -> { + SliceBackgroundWorker.shutdown(); + }); + } + private List<Uri> buildUrisFromKeys(List<String> keys, String authority) { final List<Uri> descendants = new ArrayList<>(); @@ -313,7 +390,7 @@ public class SettingsSliceProvider extends SliceProvider { try { sliceData = mSlicesDatabaseAccessor.getSliceDataFromUri(uri); } catch (IllegalStateException e) { - Log.e(TAG, "Could not get slice data for uri: " + uri, e); + Log.d(TAG, "Could not create slicedata for uri: " + uri, e); return; } @@ -325,11 +402,8 @@ public class SettingsSliceProvider extends SliceProvider { registerIntentToUri(filter, uri); } - final List<Uri> pinnedSlices = getContext().getSystemService( - SliceManager.class).getPinnedSlices(); - if (pinnedSlices.contains(uri)) { - mSliceDataCache.put(uri, sliceData); - } + ThreadUtils.postOnMainThread(() -> startBackgroundWorker(controller, uri)); + mSliceWeakDataCache.put(uri, sliceData); getContext().getContentResolver().notifyChange(uri, null /* content observer */); @@ -339,9 +413,7 @@ public class SettingsSliceProvider extends SliceProvider { @VisibleForTesting void loadSliceInBackground(Uri uri) { - ThreadUtils.postOnBackgroundThread(() -> { - loadSlice(uri); - }); + ThreadUtils.postOnBackgroundThread(() -> loadSlice(uri)); } /** @@ -362,15 +434,17 @@ public class SettingsSliceProvider extends SliceProvider { private List<Uri> getSpecialCasePlatformUris() { return Arrays.asList( - WifiSliceBuilder.WIFI_URI, - BluetoothSliceBuilder.BLUETOOTH_URI, - LocationSliceBuilder.LOCATION_URI + CustomSliceRegistry.WIFI_SLICE_URI, + CustomSliceRegistry.BLUETOOTH_URI, + CustomSliceRegistry.LOCATION_SLICE_URI ); } private List<Uri> getSpecialCaseOemUris() { return Arrays.asList( - ZenModeSliceBuilder.ZEN_MODE_URI + CustomSliceRegistry.FLASHLIGHT_SLICE_URI, + CustomSliceRegistry.MOBILE_DATA_SLICE_URI, + CustomSliceRegistry.ZEN_MODE_SLICE_URI ); } @@ -380,8 +454,6 @@ public class SettingsSliceProvider extends SliceProvider { * {@param intentFilter} happen. */ void registerIntentToUri(IntentFilter intentFilter, Uri sliceUri) { - Log.d(TAG, "Registering Uri for broadcast relay: " + sliceUri); - mRegisteredUris.add(sliceUri); SliceBroadcastRelay.registerReceiver(getContext(), sliceUri, SliceRelayReceiver.class, intentFilter); } @@ -393,7 +465,7 @@ public class SettingsSliceProvider extends SliceProvider { final Set<String> set = new ArraySet<>(); try { - mParser.setString(value); + KEY_VALUE_LIST_PARSER.setString(value); } catch (IllegalArgumentException e) { Log.e(TAG, "Bad Settings Slices Whitelist flags", e); return set; diff --git a/src/com/android/settings/slices/SliceBackgroundWorker.java b/src/com/android/settings/slices/SliceBackgroundWorker.java new file mode 100644 index 0000000000..6bafc00f9f --- /dev/null +++ b/src/com/android/settings/slices/SliceBackgroundWorker.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2018 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.slices; + +import android.annotation.MainThread; +import android.annotation.Nullable; +import android.content.Context; +import android.net.Uri; +import android.util.ArrayMap; +import android.util.Log; + +import java.io.Closeable; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * The Slice background worker is used to make Settings Slices be able to work with data that is + * changing continuously, e.g. available Wi-Fi networks. + * + * The background worker will be started at {@link SettingsSliceProvider#onSlicePinned(Uri)}, be + * stopped at {@link SettingsSliceProvider#onSliceUnpinned(Uri)}, and be closed at {@link + * SettingsSliceProvider#shutdown()}. + * + * {@link SliceBackgroundWorker} caches the results, uses the cache to compare if there is any data + * changed, and then notifies the Slice {@link Uri} to update. + * + * It also stores all instances of all workers to ensure each worker is a Singleton. + */ +public abstract class SliceBackgroundWorker<E> implements Closeable { + + private static final String TAG = "SliceBackgroundWorker"; + + private static final Map<Uri, SliceBackgroundWorker> LIVE_WORKERS = new ArrayMap<>(); + + private final Context mContext; + private final Uri mUri; + + private List<E> mCachedResults; + + protected SliceBackgroundWorker(Context context, Uri uri) { + mContext = context; + mUri = uri; + } + + protected Uri getUri() { + return mUri; + } + + protected Context getContext() { + return mContext; + } + + /** + * Returns the singleton instance of {@link SliceBackgroundWorker} for specified {@link Uri} if + * exists + */ + @Nullable + @SuppressWarnings("TypeParameterUnusedInFormals") + public static <T extends SliceBackgroundWorker> T getInstance(Uri uri) { + return (T) LIVE_WORKERS.get(uri); + } + + /** + * Returns the singleton instance of {@link SliceBackgroundWorker} for specified {@link + * CustomSliceable} + */ + static SliceBackgroundWorker getInstance(Context context, Sliceable sliceable, Uri uri) { + SliceBackgroundWorker worker = getInstance(uri); + if (worker == null) { + final Class<? extends SliceBackgroundWorker> workerClass = + sliceable.getBackgroundWorkerClass(); + worker = createInstance(context.getApplicationContext(), uri, workerClass); + LIVE_WORKERS.put(uri, worker); + } + return worker; + } + + private static SliceBackgroundWorker createInstance(Context context, Uri uri, + Class<? extends SliceBackgroundWorker> clazz) { + Log.d(TAG, "create instance: " + clazz); + try { + return clazz.getConstructor(Context.class, Uri.class).newInstance(context, uri); + } catch (NoSuchMethodException | IllegalAccessException | InstantiationException | + InvocationTargetException e) { + throw new IllegalStateException( + "Invalid slice background worker: " + clazz, e); + } + } + + static void shutdown() { + for (SliceBackgroundWorker worker : LIVE_WORKERS.values()) { + try { + worker.close(); + } catch (IOException e) { + Log.w(TAG, "Shutting down worker failed", e); + } + } + LIVE_WORKERS.clear(); + } + + /** + * Called when the Slice is pinned. This is the place to register callbacks or initialize scan + * tasks. + */ + @MainThread + protected abstract void onSlicePinned(); + + /** + * Called when the Slice is unpinned. This is the place to unregister callbacks or perform any + * final cleanup. + */ + @MainThread + protected abstract void onSliceUnpinned(); + + /** + * @return a {@link List} of cached results + */ + public final List<E> getResults() { + return mCachedResults == null ? null : new ArrayList<>(mCachedResults); + } + + /** + * Update the results when data changes + */ + protected final void updateResults(List<E> results) { + boolean needNotify = false; + + if (results == null) { + if (mCachedResults != null) { + needNotify = true; + } + } else { + needNotify = !areListsTheSame(results, mCachedResults); + } + + if (needNotify) { + mCachedResults = results; + notifySliceChange(); + } + } + + protected boolean areListsTheSame(List<E> a, List<E> b) { + return a.equals(b); + } + + /** + * Notify that data was updated and attempt to sync changes to the Slice. + */ + protected final void notifySliceChange() { + mContext.getContentResolver().notifyChange(mUri, null); + } +} diff --git a/src/com/android/settings/slices/SliceBroadcastReceiver.java b/src/com/android/settings/slices/SliceBroadcastReceiver.java index 53fa4be8e3..fc3d0cc696 100644 --- a/src/com/android/settings/slices/SliceBroadcastReceiver.java +++ b/src/com/android/settings/slices/SliceBroadcastReceiver.java @@ -17,14 +17,19 @@ package com.android.settings.slices; import static com.android.settings.bluetooth.BluetoothSliceBuilder.ACTION_BLUETOOTH_SLICE_CHANGED; +import static com.android.settings.network.telephony.Enhanced4gLteSliceHelper.ACTION_ENHANCED_4G_LTE_CHANGED; import static com.android.settings.notification.ZenModeSliceBuilder.ACTION_ZEN_MODE_SLICE_CHANGED; +import static com.android.settings.slices.SettingsSliceProvider.ACTION_COPY; import static com.android.settings.slices.SettingsSliceProvider.ACTION_SLIDER_CHANGED; import static com.android.settings.slices.SettingsSliceProvider.ACTION_TOGGLE_CHANGED; import static com.android.settings.slices.SettingsSliceProvider.EXTRA_SLICE_KEY; import static com.android.settings.slices.SettingsSliceProvider.EXTRA_SLICE_PLATFORM_DEFINED; import static com.android.settings.wifi.calling.WifiCallingSliceHelper.ACTION_WIFI_CALLING_CHANGED; -import static com.android.settings.wifi.WifiSliceBuilder.ACTION_WIFI_SLICE_CHANGED; +import static com.android.settings.wifi.calling.WifiCallingSliceHelper.ACTION_WIFI_CALLING_PREFERENCE_CELLULAR_PREFERRED; +import static com.android.settings.wifi.calling.WifiCallingSliceHelper.ACTION_WIFI_CALLING_PREFERENCE_WIFI_ONLY; +import static com.android.settings.wifi.calling.WifiCallingSliceHelper.ACTION_WIFI_CALLING_PREFERENCE_WIFI_PREFERRED; +import android.app.settings.SettingsEnums; import android.app.slice.Slice; import android.content.BroadcastReceiver; import android.content.ContentResolver; @@ -34,16 +39,13 @@ import android.net.Uri; import android.provider.SettingsSlicesContract; import android.text.TextUtils; import android.util.Log; -import android.util.Pair; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.bluetooth.BluetoothSliceBuilder; import com.android.settings.core.BasePreferenceController; import com.android.settings.core.SliderPreferenceController; import com.android.settings.core.TogglePreferenceController; import com.android.settings.notification.ZenModeSliceBuilder; import com.android.settings.overlay.FeatureFactory; -import com.android.settings.wifi.WifiSliceBuilder; /** * Responds to actions performed on slices and notifies slices of updates in state changes. @@ -59,6 +61,14 @@ public class SliceBroadcastReceiver extends BroadcastReceiver { final boolean isPlatformSlice = intent.getBooleanExtra(EXTRA_SLICE_PLATFORM_DEFINED, false /* default */); + if (CustomSliceRegistry.isValidAction(action)) { + final CustomSliceable sliceable = + CustomSliceable.createInstance(context, + CustomSliceRegistry.getSliceClassByUri(Uri.parse(action))); + sliceable.onNotifyChange(intent); + return; + } + switch (action) { case ACTION_TOGGLE_CHANGED: final boolean isChecked = intent.getBooleanExtra(Slice.EXTRA_TOGGLE_STATE, false); @@ -71,9 +81,6 @@ public class SliceBroadcastReceiver extends BroadcastReceiver { case ACTION_BLUETOOTH_SLICE_CHANGED: BluetoothSliceBuilder.handleUriChange(context, intent); break; - case ACTION_WIFI_SLICE_CHANGED: - WifiSliceBuilder.handleUriChange(context, intent); - break; case ACTION_WIFI_CALLING_CHANGED: FeatureFactory.getFactory(context) .getSlicesFeatureProvider() @@ -83,6 +90,23 @@ public class SliceBroadcastReceiver extends BroadcastReceiver { case ACTION_ZEN_MODE_SLICE_CHANGED: ZenModeSliceBuilder.handleUriChange(context, intent); break; + case ACTION_ENHANCED_4G_LTE_CHANGED: + FeatureFactory.getFactory(context) + .getSlicesFeatureProvider() + .getNewEnhanced4gLteSliceHelper(context) + .handleEnhanced4gLteChanged(intent); + break; + case ACTION_WIFI_CALLING_PREFERENCE_WIFI_ONLY: + case ACTION_WIFI_CALLING_PREFERENCE_WIFI_PREFERRED: + case ACTION_WIFI_CALLING_PREFERENCE_CELLULAR_PREFERRED: + FeatureFactory.getFactory(context) + .getSlicesFeatureProvider() + .getNewWifiCallingSliceHelper(context) + .handleWifiCallingPreferenceChanged(intent); + break; + case ACTION_COPY: + handleCopyAction(context, key, isPlatformSlice); + break; } } @@ -140,11 +164,12 @@ public class SliceBroadcastReceiver extends BroadcastReceiver { } final SliderPreferenceController sliderController = (SliderPreferenceController) controller; - final int maxSteps = sliderController.getMaxSteps(); - if (newPosition < 0 || newPosition > maxSteps) { + final int minValue = sliderController.getMin(); + final int maxValue = sliderController.getMax(); + if (newPosition < minValue || newPosition > maxValue) { throw new IllegalArgumentException( - "Invalid position passed to Slider controller. Expected between 0 and " - + maxSteps + " but found " + newPosition); + "Invalid position passed to Slider controller. Expected between " + minValue + + " and " + maxValue + " but found " + newPosition); } sliderController.setSliderPosition(newPosition); @@ -152,17 +177,39 @@ public class SliceBroadcastReceiver extends BroadcastReceiver { updateUri(context, key, isPlatformSlice); } + private void handleCopyAction(Context context, String key, boolean isPlatformSlice) { + if (TextUtils.isEmpty(key)) { + throw new IllegalArgumentException("No key passed to Intent for controller"); + } + + final BasePreferenceController controller = getPreferenceController(context, key); + + if (!(controller instanceof Sliceable)) { + throw new IllegalArgumentException( + "Copyable action passed for a non-copyable key:" + key); + } + + if (!controller.isAvailable()) { + Log.w(TAG, "Can't update " + key + " since the setting is unavailable"); + if (!controller.hasAsyncUpdate()) { + updateUri(context, key, isPlatformSlice); + } + return; + } + + controller.copy(); + } + /** * Log Slice value update events into MetricsFeatureProvider. The logging schema generally * follows the pattern in SharedPreferenceLogger. */ private void logSliceValueChange(Context context, String sliceKey, int newValue) { - final Pair<Integer, Object> namePair = Pair.create( - MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_NAME, sliceKey); - final Pair<Integer, Object> valuePair = Pair.create( - MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_INT_VALUE, newValue); FeatureFactory.getFactory(context).getMetricsFeatureProvider() - .action(context, MetricsEvent.ACTION_SETTINGS_SLICE_CHANGED, namePair, valuePair); + .action(SettingsEnums.PAGE_UNKNOWN, + SettingsEnums.ACTION_SETTINGS_SLICE_CHANGED, + SettingsEnums.PAGE_UNKNOWN, + sliceKey, newValue); } private BasePreferenceController getPreferenceController(Context context, String key) { diff --git a/src/com/android/settings/slices/SliceBuilderUtils.java b/src/com/android/settings/slices/SliceBuilderUtils.java index baac3b620f..0e510325f8 100644 --- a/src/com/android/settings/slices/SliceBuilderUtils.java +++ b/src/com/android/settings/slices/SliceBuilderUtils.java @@ -16,50 +16,48 @@ package com.android.settings.slices; -import static com.android.settings.core.BasePreferenceController.AVAILABLE; -import static com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE; import static com.android.settings.core.BasePreferenceController.DISABLED_DEPENDENT_SETTING; -import static com.android.settings.core.BasePreferenceController.DISABLED_FOR_USER; -import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE; import static com.android.settings.slices.SettingsSliceProvider.EXTRA_SLICE_KEY; import static com.android.settings.slices.SettingsSliceProvider.EXTRA_SLICE_PLATFORM_DEFINED; import android.annotation.ColorInt; import android.app.PendingIntent; +import android.app.settings.SettingsEnums; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.net.Uri; -import android.provider.Settings; +import android.os.Bundle; import android.provider.SettingsSlicesContract; import android.text.TextUtils; +import android.util.ArraySet; import android.util.Log; import android.util.Pair; -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.annotation.VisibleForTesting; +import androidx.core.graphics.drawable.IconCompat; +import androidx.slice.Slice; +import androidx.slice.builders.ListBuilder; +import androidx.slice.builders.ListBuilder.InputRangeBuilder; +import androidx.slice.builders.ListBuilder.RowBuilder; +import androidx.slice.builders.SliceAction; + import com.android.settings.R; +import com.android.settings.SettingsActivity; import com.android.settings.SubSettings; import com.android.settings.Utils; import com.android.settings.core.BasePreferenceController; import com.android.settings.core.SliderPreferenceController; +import com.android.settings.core.SubSettingLauncher; import com.android.settings.core.TogglePreferenceController; import com.android.settings.overlay.FeatureFactory; -import com.android.settings.search.DatabaseIndexingUtils; -import com.android.settingslib.SliceBroadcastRelay; import com.android.settingslib.core.AbstractPreferenceController; -import androidx.core.graphics.drawable.IconCompat; - -import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; -import androidx.slice.Slice; -import androidx.slice.builders.ListBuilder; -import androidx.slice.builders.SliceAction; - /** * Utility class to build Slices objects and Preference Controllers based on the Database managed @@ -79,12 +77,12 @@ public class SliceBuilderUtils { public static Slice buildSlice(Context context, SliceData sliceData) { Log.d(TAG, "Creating slice for: " + sliceData.getPreferenceController()); final BasePreferenceController controller = getPreferenceController(context, sliceData); - final Pair<Integer, Object> sliceNamePair = - Pair.create(MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_NAME, sliceData.getKey()); - // Log Slice requests using the same schema as SharedPreferenceLogger (but with a different - // action name). FeatureFactory.getFactory(context).getMetricsFeatureProvider() - .action(context, MetricsEvent.ACTION_SETTINGS_SLICE_REQUESTED, sliceNamePair); + .action(SettingsEnums.PAGE_UNKNOWN, + SettingsEnums.ACTION_SETTINGS_SLICE_REQUESTED, + SettingsEnums.PAGE_UNKNOWN, + sliceData.getKey(), + 0); if (!controller.isAvailable()) { // Cannot guarantee setting page is accessible, let the presenter handle error case. @@ -95,6 +93,10 @@ public class SliceBuilderUtils { return buildUnavailableSlice(context, sliceData); } + if (controller.isCopyableSlice()) { + return buildCopyableSlice(context, sliceData, controller); + } + switch (sliceData.getSliceType()) { case SliceData.SliceType.INTENT: return buildIntentSlice(context, sliceData, controller); @@ -165,10 +167,11 @@ public class SliceBuilderUtils { * @return {@link PendingIntent} for a non-primary {@link SliceAction}. */ public static PendingIntent getActionIntent(Context context, String action, SliceData data) { - final Intent intent = new Intent(action); - intent.setClass(context, SliceBroadcastReceiver.class); - intent.putExtra(EXTRA_SLICE_KEY, data.getKey()); - intent.putExtra(EXTRA_SLICE_PLATFORM_DEFINED, data.isPlatformDefined()); + final Intent intent = new Intent(action) + .setData(data.getUri()) + .setClass(context, SliceBroadcastReceiver.class) + .putExtra(EXTRA_SLICE_KEY, data.getKey()) + .putExtra(EXTRA_SLICE_PLATFORM_DEFINED, data.isPlatformDefined()); return PendingIntent.getBroadcast(context, 0 /* requestCode */, intent, PendingIntent.FLAG_CANCEL_CURRENT); } @@ -185,26 +188,29 @@ public class SliceBuilderUtils { * @return the summary text for a {@link Slice} built for {@param sliceData}. */ public static CharSequence getSubtitleText(Context context, - AbstractPreferenceController controller, SliceData sliceData) { - CharSequence summaryText = sliceData.getScreenTitle(); - if (isValidSummary(context, summaryText) && !TextUtils.equals(summaryText, - sliceData.getTitle())) { - return summaryText; - } - - if (controller != null) { - summaryText = controller.getSummary(); + BasePreferenceController controller, SliceData sliceData) { - if (isValidSummary(context, summaryText)) { - return summaryText; - } + // Priority 1 : User prefers showing the dynamic summary in slice view rather than static + // summary. Note it doesn't require a valid summary - so we can force some slices to have + // empty summaries (ex: volume). + if (controller.useDynamicSliceSummary()) { + return controller.getSummary(); } - summaryText = sliceData.getSummary(); + // Priority 2: Show summary from slice data. + CharSequence summaryText = sliceData.getSummary(); if (isValidSummary(context, summaryText)) { return summaryText; } + // Priority 3: Show screen title. + summaryText = sliceData.getScreenTitle(); + if (isValidSummary(context, summaryText) && !TextUtils.equals(summaryText, + sliceData.getTitle())) { + return summaryText; + } + + // Priority 4: Show empty text. return ""; } @@ -219,10 +225,25 @@ public class SliceBuilderUtils { .build(); } - @VisibleForTesting - static Intent getContentIntent(Context context, SliceData sliceData) { + public static Intent buildSearchResultPageIntent(Context context, String className, String key, + String screenTitle, int sourceMetricsCategory) { + final Bundle args = new Bundle(); + args.putString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, key); + final Intent searchDestination = new SubSettingLauncher(context) + .setDestination(className) + .setArguments(args) + .setTitleText(screenTitle) + .setSourceMetricsCategory(sourceMetricsCategory) + .toIntent(); + searchDestination.putExtra(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, key) + .setAction("com.android.settings.SEARCH_RESULT_TRAMPOLINE") + .setComponent(null); + return searchDestination; + } + + public static Intent getContentIntent(Context context, SliceData sliceData) { final Uri contentUri = new Uri.Builder().appendPath(sliceData.getKey()).build(); - final Intent intent = DatabaseIndexingUtils.buildSearchResultPageIntent(context, + final Intent intent = buildSearchResultPageIntent(context, sliceData.getFragmentClassName(), sliceData.getKey(), sliceData.getScreenTitle().toString(), 0 /* TODO */); intent.setClassName(context.getPackageName(), SubSettings.class.getName()); @@ -233,22 +254,23 @@ public class SliceBuilderUtils { private static Slice buildToggleSlice(Context context, SliceData sliceData, BasePreferenceController controller) { final PendingIntent contentIntent = getContentPendingIntent(context, sliceData); - final IconCompat icon = IconCompat.createWithResource(context, sliceData.getIconResource()); + final IconCompat icon = getSafeIcon(context, sliceData); final CharSequence subtitleText = getSubtitleText(context, controller, sliceData); - @ColorInt final int color = Utils.getColorAccent(context); + @ColorInt final int color = Utils.getColorAccentDefaultColor(context); final TogglePreferenceController toggleController = (TogglePreferenceController) controller; final SliceAction sliceAction = getToggleAction(context, sliceData, toggleController.isChecked()); - final List<String> keywords = buildSliceKeywords(sliceData); + final Set<String> keywords = buildSliceKeywords(sliceData); return new ListBuilder(context, sliceData.getUri(), ListBuilder.INFINITY) .setAccentColor(color) - .addRow(rowBuilder -> rowBuilder + .addRow(new RowBuilder() .setTitle(sliceData.getTitle()) .setSubtitle(subtitleText) .setPrimaryAction( - new SliceAction(contentIntent, icon, sliceData.getTitle())) + SliceAction.createDeeplink(contentIntent, icon, + ListBuilder.ICON_IMAGE, sliceData.getTitle())) .addEndItem(sliceAction)) .setKeywords(keywords) .build(); @@ -257,18 +279,20 @@ public class SliceBuilderUtils { private static Slice buildIntentSlice(Context context, SliceData sliceData, BasePreferenceController controller) { final PendingIntent contentIntent = getContentPendingIntent(context, sliceData); - final IconCompat icon = IconCompat.createWithResource(context, sliceData.getIconResource()); + final IconCompat icon = getSafeIcon(context, sliceData); final CharSequence subtitleText = getSubtitleText(context, controller, sliceData); - @ColorInt final int color = Utils.getColorAccent(context); - final List<String> keywords = buildSliceKeywords(sliceData); + @ColorInt final int color = Utils.getColorAccentDefaultColor(context); + final Set<String> keywords = buildSliceKeywords(sliceData); return new ListBuilder(context, sliceData.getUri(), ListBuilder.INFINITY) .setAccentColor(color) - .addRow(rowBuilder -> rowBuilder + .addRow(new RowBuilder() .setTitle(sliceData.getTitle()) .setSubtitle(subtitleText) .setPrimaryAction( - new SliceAction(contentIntent, icon, sliceData.getTitle()))) + SliceAction.createDeeplink(contentIntent, icon, + ListBuilder.ICON_IMAGE, + sliceData.getTitle()))) .setKeywords(keywords) .build(); } @@ -276,28 +300,56 @@ public class SliceBuilderUtils { private static Slice buildSliderSlice(Context context, SliceData sliceData, BasePreferenceController controller) { final SliderPreferenceController sliderController = (SliderPreferenceController) controller; + if (sliderController.getMax() <= sliderController.getMin()) { + Log.e(TAG, "Invalid sliderController: " + sliderController.getPreferenceKey()); + return null; + } final PendingIntent actionIntent = getSliderAction(context, sliceData); final PendingIntent contentIntent = getContentPendingIntent(context, sliceData); - final IconCompat icon = IconCompat.createWithResource(context, sliceData.getIconResource()); + final IconCompat icon = getSafeIcon(context, sliceData); + @ColorInt final int color = Utils.getColorAccentDefaultColor(context); final CharSequence subtitleText = getSubtitleText(context, controller, sliceData); - @ColorInt final int color = Utils.getColorAccent(context); - final SliceAction primaryAction = new SliceAction(contentIntent, icon, - sliceData.getTitle()); - final List<String> keywords = buildSliceKeywords(sliceData); + final SliceAction primaryAction = SliceAction.createDeeplink(contentIntent, icon, + ListBuilder.ICON_IMAGE, sliceData.getTitle()); + final Set<String> keywords = buildSliceKeywords(sliceData); return new ListBuilder(context, sliceData.getUri(), ListBuilder.INFINITY) .setAccentColor(color) - .addInputRange(builder -> builder + .addInputRange(new InputRangeBuilder() .setTitle(sliceData.getTitle()) .setSubtitle(subtitleText) .setPrimaryAction(primaryAction) - .setMax(sliderController.getMaxSteps()) + .setMax(sliderController.getMax()) + .setMin(sliderController.getMin()) .setValue(sliderController.getSliderPosition()) .setInputAction(actionIntent)) .setKeywords(keywords) .build(); } + private static Slice buildCopyableSlice(Context context, SliceData sliceData, + BasePreferenceController controller) { + final SliceAction copyableAction = getCopyableAction(context, sliceData); + final PendingIntent contentIntent = getContentPendingIntent(context, sliceData); + final IconCompat icon = getSafeIcon(context, sliceData); + final SliceAction primaryAction = SliceAction.createDeeplink(contentIntent, icon, + ListBuilder.ICON_IMAGE, + sliceData.getTitle()); + final CharSequence subtitleText = getSubtitleText(context, controller, sliceData); + @ColorInt final int color = Utils.getColorAccentDefaultColor(context); + final Set<String> keywords = buildSliceKeywords(sliceData); + + return new ListBuilder(context, sliceData.getUri(), ListBuilder.INFINITY) + .setAccentColor(color) + .addRow(new RowBuilder() + .setTitle(sliceData.getTitle()) + .setSubtitle(subtitleText) + .setPrimaryAction(primaryAction) + .addEndItem(copyableAction)) + .setKeywords(keywords) + .build(); + } + private static BasePreferenceController getPreferenceController(Context context, String controllerClassName, String controllerKey) { try { @@ -313,13 +365,21 @@ public class SliceBuilderUtils { boolean isChecked) { PendingIntent actionIntent = getActionIntent(context, SettingsSliceProvider.ACTION_TOGGLE_CHANGED, sliceData); - return new SliceAction(actionIntent, null, isChecked); + return SliceAction.createToggle(actionIntent, null, isChecked); } private static PendingIntent getSliderAction(Context context, SliceData sliceData) { return getActionIntent(context, SettingsSliceProvider.ACTION_SLIDER_CHANGED, sliceData); } + private static SliceAction getCopyableAction(Context context, SliceData sliceData) { + final PendingIntent intent = getActionIntent(context, + SettingsSliceProvider.ACTION_COPY, sliceData); + final IconCompat icon = IconCompat.createWithResource(context, + R.drawable.ic_content_copy_grey600_24dp); + return SliceAction.create(intent, icon, ListBuilder.ICON_IMAGE, sliceData.getTitle()); + } + private static boolean isValidSummary(Context context, CharSequence summary) { if (summary == null || TextUtils.isEmpty(summary.toString().trim())) { return false; @@ -333,8 +393,8 @@ public class SliceBuilderUtils { || TextUtils.equals(summary, doublePlaceHolder)); } - private static List<String> buildSliceKeywords(SliceData data) { - final List<String> keywords = new ArrayList<>(); + private static Set<String> buildSliceKeywords(SliceData data) { + final Set<String> keywords = new ArraySet<>(); keywords.add(data.getTitle()); @@ -356,21 +416,41 @@ public class SliceBuilderUtils { private static Slice buildUnavailableSlice(Context context, SliceData data) { final String title = data.getTitle(); - final List<String> keywords = buildSliceKeywords(data); - @ColorInt final int color = Utils.getColorAccent(context); - final CharSequence summary = context.getText(R.string.disabled_dependent_setting_summary); - final IconCompat icon = IconCompat.createWithResource(context, data.getIconResource()); - final SliceAction primaryAction = new SliceAction(getContentPendingIntent(context, data), - icon, title); + final Set<String> keywords = buildSliceKeywords(data); + @ColorInt final int color = Utils.getColorAccentDefaultColor(context); + + final String customSubtitle = data.getUnavailableSliceSubtitle(); + final CharSequence subtitle = !TextUtils.isEmpty(customSubtitle) ? customSubtitle + : context.getText(R.string.disabled_dependent_setting_summary); + final IconCompat icon = getSafeIcon(context, data); + final SliceAction primaryAction = SliceAction.createDeeplink( + getContentPendingIntent(context, data), + icon, ListBuilder.ICON_IMAGE, title); return new ListBuilder(context, data.getUri(), ListBuilder.INFINITY) .setAccentColor(color) - .addRow(builder -> builder + .addRow(new RowBuilder() .setTitle(title) - .setTitleItem(icon) - .setSubtitle(summary) + .setTitleItem(icon, ListBuilder.ICON_IMAGE) + .setSubtitle(subtitle) .setPrimaryAction(primaryAction)) .setKeywords(keywords) .build(); } + + @VisibleForTesting + static IconCompat getSafeIcon(Context context, SliceData data) { + int iconResource = data.getIconResource(); + + if (iconResource == 0) { + iconResource = R.drawable.ic_settings_accent; + } + try { + return IconCompat.createWithResource(context, iconResource); + } catch (Exception e) { + Log.w(TAG, "Falling back to settings icon because there is an error getting slice icon " + + data.getUri(), e); + return IconCompat.createWithResource(context, R.drawable.ic_settings_accent); + } + } } diff --git a/src/com/android/settings/slices/SliceData.java b/src/com/android/settings/slices/SliceData.java index 689108a007..f8185390af 100644 --- a/src/com/android/settings/slices/SliceData.java +++ b/src/com/android/settings/slices/SliceData.java @@ -22,14 +22,12 @@ import android.text.TextUtils; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.util.List; /** * Data class representing a slice stored by {@link SlicesIndexer}. * Note that {@link #mKey} is treated as a primary key for this class and determines equality. */ public class SliceData { - /** * Flags indicating the UI type of the Slice. */ @@ -75,6 +73,8 @@ public class SliceData { private final boolean mIsPlatformDefined; + private final String mUnavailableSliceSubtitle; + public String getKey() { return mKey; } @@ -119,6 +119,10 @@ public class SliceData { return mIsPlatformDefined; } + public String getUnavailableSliceSubtitle() { + return mUnavailableSliceSubtitle; + } + private SliceData(Builder builder) { mKey = builder.mKey; mTitle = builder.mTitle; @@ -131,6 +135,7 @@ public class SliceData { mPreferenceController = builder.mPrefControllerClassName; mSliceType = builder.mSliceType; mIsPlatformDefined = builder.mIsPlatformDefined; + mUnavailableSliceSubtitle = builder.mUnavailableSliceSubtitle; } @Override @@ -170,6 +175,8 @@ public class SliceData { private boolean mIsPlatformDefined; + private String mUnavailableSliceSubtitle; + public Builder setKey(String key) { mKey = key; return this; @@ -225,6 +232,12 @@ public class SliceData { return this; } + public Builder setUnavailableSliceSubtitle( + String unavailableSliceSubtitle) { + mUnavailableSliceSubtitle = unavailableSliceSubtitle; + return this; + } + public SliceData build() { if (TextUtils.isEmpty(mKey)) { throw new InvalidSliceDataException("Key cannot be empty"); diff --git a/src/com/android/settings/slices/SliceDataConverter.java b/src/com/android/settings/slices/SliceDataConverter.java index bfe090e846..7a8ab838f1 100644 --- a/src/com/android/settings/slices/SliceDataConverter.java +++ b/src/com/android/settings/slices/SliceDataConverter.java @@ -22,8 +22,10 @@ import static com.android.settings.core.PreferenceXmlParserUtils.METADATA_KEY; import static com.android.settings.core.PreferenceXmlParserUtils.METADATA_PLATFORM_SLICE_FLAG; import static com.android.settings.core.PreferenceXmlParserUtils.METADATA_SUMMARY; import static com.android.settings.core.PreferenceXmlParserUtils.METADATA_TITLE; +import static com.android.settings.core.PreferenceXmlParserUtils.METADATA_UNAVAILABLE_SLICE_SUBTITLE; import android.accessibilityservice.AccessibilityServiceInfo; +import android.app.settings.SettingsEnums; import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageManager; @@ -39,7 +41,8 @@ import android.util.Log; import android.util.Xml; import android.view.accessibility.AccessibilityManager; -import com.android.internal.annotations.VisibleForTesting; +import androidx.annotation.VisibleForTesting; + import com.android.settings.R; import com.android.settings.accessibility.AccessibilitySettings; import com.android.settings.accessibility.AccessibilitySlicePreferenceController; @@ -50,6 +53,7 @@ import com.android.settings.dashboard.DashboardFragment; import com.android.settings.overlay.FeatureFactory; import com.android.settings.search.DatabaseIndexingUtils; import com.android.settings.search.Indexable.SearchIndexProvider; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -74,13 +78,12 @@ class SliceDataConverter { private static final String NODE_NAME_PREFERENCE_SCREEN = "PreferenceScreen"; + private final MetricsFeatureProvider mMetricsFeatureProvider; private Context mContext; - private List<SliceData> mSliceData; - public SliceDataConverter(Context context) { mContext = context; - mSliceData = new ArrayList<>(); + mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider(); } /** @@ -94,9 +97,7 @@ class SliceDataConverter { * {@link com.android.settings.core.BasePreferenceController}. */ public List<SliceData> getSliceData() { - if (!mSliceData.isEmpty()) { - return mSliceData; - } + List<SliceData> sliceData = new ArrayList<>(); final Collection<Class> indexableClasses = FeatureFactory.getFactory(mContext) .getSearchFeatureProvider().getSearchIndexableResources().getProviderValues(); @@ -115,12 +116,12 @@ class SliceDataConverter { final List<SliceData> providerSliceData = getSliceDataFromProvider(provider, fragmentName); - mSliceData.addAll(providerSliceData); + sliceData.addAll(providerSliceData); } final List<SliceData> a11ySliceData = getAccessibilitySliceData(); - mSliceData.addAll(a11ySliceData); - return mSliceData; + sliceData.addAll(a11ySliceData); + return sliceData; } private List<SliceData> getSliceDataFromProvider(SearchIndexProvider provider, @@ -154,6 +155,7 @@ class SliceDataConverter { XmlResourceParser parser = null; final List<SliceData> xmlSliceData = new ArrayList<>(); + String controllerClassName = ""; try { parser = mContext.getResources().getXml(xmlResId); @@ -185,12 +187,13 @@ class SliceDataConverter { | MetadataFlag.FLAG_NEED_PREF_TITLE | MetadataFlag.FLAG_NEED_PREF_ICON | MetadataFlag.FLAG_NEED_PREF_SUMMARY - | MetadataFlag.FLAG_NEED_PLATFORM_SLICE_FLAG); + | MetadataFlag.FLAG_NEED_PLATFORM_SLICE_FLAG + | MetadataFlag.FLAG_UNAVAILABLE_SLICE_SUBTITLE); for (Bundle bundle : metadata) { // TODO (b/67996923) Non-controller Slices should become intent-only slices. // Note that without a controller, dynamic summaries are impossible. - final String controllerClassName = bundle.getString(METADATA_CONTROLLER); + controllerClassName = bundle.getString(METADATA_CONTROLLER); if (TextUtils.isEmpty(controllerClassName)) { continue; } @@ -202,6 +205,8 @@ class SliceDataConverter { final int sliceType = SliceBuilderUtils.getSliceType(mContext, controllerClassName, key); final boolean isPlatformSlice = bundle.getBoolean(METADATA_PLATFORM_SLICE_FLAG); + final String unavailableSliceSubtitle = bundle.getString( + METADATA_UNAVAILABLE_SLICE_SUBTITLE); final SliceData xmlSlice = new SliceData.Builder() .setKey(key) @@ -213,24 +218,38 @@ class SliceDataConverter { .setFragmentName(fragmentName) .setSliceType(sliceType) .setPlatformDefined(isPlatformSlice) + .setUnavailableSliceSubtitle(unavailableSliceSubtitle) .build(); final BasePreferenceController controller = SliceBuilderUtils.getPreferenceController(mContext, xmlSlice); // Only add pre-approved Slices available on the device. - if (controller.isAvailable() && controller.isSliceable()) { + if (controller.isSliceable() && controller.isAvailable()) { xmlSliceData.add(xmlSlice); } } } catch (SliceData.InvalidSliceDataException e) { Log.w(TAG, "Invalid data when building SliceData for " + fragmentName, e); - } catch (XmlPullParserException e) { - Log.w(TAG, "XML Error parsing PreferenceScreen: ", e); - } catch (IOException e) { - Log.w(TAG, "IO Error parsing PreferenceScreen: ", e); - } catch (Resources.NotFoundException e) { - Log.w(TAG, "Resource not found error parsing PreferenceScreen: ", e); + mMetricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, + SettingsEnums.ACTION_VERIFY_SLICE_ERROR_INVALID_DATA, + SettingsEnums.PAGE_UNKNOWN, + controllerClassName, + 1); + } catch (XmlPullParserException | IOException | Resources.NotFoundException e) { + Log.w(TAG, "Error parsing PreferenceScreen: ", e); + mMetricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, + SettingsEnums.ACTION_VERIFY_SLICE_PARSING_ERROR, + SettingsEnums.PAGE_UNKNOWN, + fragmentName, + 1); + } catch (Exception e) { + Log.w(TAG, "Get slice data from XML failed ", e); + mMetricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, + SettingsEnums.ACTION_VERIFY_SLICE_OTHER_EXCEPTION, + SettingsEnums.PAGE_UNKNOWN, + fragmentName + "_" + controllerClassName, + 1); } finally { if (parser != null) parser.close(); } @@ -270,7 +289,7 @@ class SliceDataConverter { final String title = resolveInfo.loadLabel(packageManager).toString(); int iconResource = resolveInfo.getIconResource(); if (iconResource == 0) { - iconResource = R.mipmap.ic_accessibility_generic; + iconResource = R.drawable.ic_accessibility_generic; } sliceDataBuilder.setKey(flattenedName) diff --git a/src/com/android/settings/slices/SliceDeepLinkSpringBoard.java b/src/com/android/settings/slices/SliceDeepLinkSpringBoard.java index 4f8ed96b9a..2ff071eebc 100644 --- a/src/com/android/settings/slices/SliceDeepLinkSpringBoard.java +++ b/src/com/android/settings/slices/SliceDeepLinkSpringBoard.java @@ -22,83 +22,55 @@ import android.provider.Settings; import android.util.Log; import com.android.settings.bluetooth.BluetoothSliceBuilder; -import com.android.settings.location.LocationSliceBuilder; import com.android.settings.notification.ZenModeSliceBuilder; -import com.android.settings.overlay.FeatureFactory; -import com.android.settings.wifi.WifiSliceBuilder; -import com.android.settings.wifi.calling.WifiCallingSliceHelper; - -import java.net.URISyntaxException; public class SliceDeepLinkSpringBoard extends Activity { private static final String TAG = "DeeplinkSpringboard"; - public static final String INTENT = "intent"; - public static final String SETTINGS = "settings"; - public static final String ACTION_VIEW_SLICE = "com.android.settings.action.VIEW_SLICE"; public static final String EXTRA_SLICE = "slice"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - Uri uri = getIntent().getData(); - if (uri == null) { + final Uri sliceUri = parse(getIntent().getData()); + if (sliceUri == null) { Log.e(TAG, "No data found"); finish(); return; } try { - Intent intent = parse(uri, getPackageName()); - if (ACTION_VIEW_SLICE.equals(intent.getAction())) { - // This shouldn't matter since the slice is shown instead of the device - // index caring about the launch uri. - final Uri slice = Uri.parse(intent.getStringExtra(EXTRA_SLICE)); - final Intent launchIntent; - - // TODO (b/80263568) Avoid duplicating this list of Slice Uris. - if (WifiSliceBuilder.WIFI_URI.equals(slice)) { - launchIntent = WifiSliceBuilder.getIntent(this /* context */); - } else if (ZenModeSliceBuilder.ZEN_MODE_URI.equals(slice)) { - launchIntent = ZenModeSliceBuilder.getIntent(this /* context */); - } else if (BluetoothSliceBuilder.BLUETOOTH_URI.equals(slice)) { - launchIntent = BluetoothSliceBuilder.getIntent(this /* context */); - } else if (LocationSliceBuilder.LOCATION_URI.equals(slice)) { - launchIntent = LocationSliceBuilder.getIntent(this /* context */); - } else { - final SlicesDatabaseAccessor slicesDatabaseAccessor = - new SlicesDatabaseAccessor(this /* context */); - // Sadly have to block here because we don't know where to go. - final SliceData sliceData = slicesDatabaseAccessor.getSliceDataFromUri(slice); - launchIntent = SliceBuilderUtils.getContentIntent(this, sliceData); - } + // This shouldn't matter since the slice is shown instead of the device + // index caring about the launch uri. + Intent launchIntent; - startActivity(launchIntent); + // TODO (b/80263568) Avoid duplicating this list of Slice Uris. + if (CustomSliceRegistry.isValidUri(sliceUri)) { + final CustomSliceable sliceable = + CustomSliceable.createInstance(getApplicationContext(), + CustomSliceRegistry.getSliceClassByUri(sliceUri)); + launchIntent = sliceable.getIntent(); + } else if (CustomSliceRegistry.ZEN_MODE_SLICE_URI.equals(sliceUri)) { + launchIntent = ZenModeSliceBuilder.getIntent(this /* context */); + } else if (CustomSliceRegistry.BLUETOOTH_URI.equals(sliceUri)) { + launchIntent = BluetoothSliceBuilder.getIntent(this /* context */); } else { - startActivity(intent); + final SlicesDatabaseAccessor slicesDatabaseAccessor = + new SlicesDatabaseAccessor(this /* context */); + // Sadly have to block here because we don't know where to go. + final SliceData sliceData = + slicesDatabaseAccessor.getSliceDataFromUri(sliceUri); + launchIntent = SliceBuilderUtils.getContentIntent(this, sliceData); } + startActivity(launchIntent); finish(); - } catch (URISyntaxException e) { - Log.e(TAG, "Error decoding uri", e); - finish(); - } catch (IllegalStateException e) { + } catch (Exception e) { Log.w(TAG, "Couldn't launch Slice intent", e); startActivity(new Intent(Settings.ACTION_SETTINGS)); finish(); } } - public static Intent parse(Uri uri, String pkg) throws URISyntaxException { - Intent intent = Intent.parseUri(uri.getQueryParameter(INTENT), - Intent.URI_ANDROID_APP_SCHEME); - // Start with some really strict constraints and loosen them if we need to. - // Don't allow components. - intent.setComponent(null); - // Clear out the extras. - if (intent.getExtras() != null) { - intent.getExtras().clear(); - } - // Make sure this points at Settings. - intent.setPackage(pkg); - return intent; + private static Uri parse(Uri uri) { + return Uri.parse(uri.getQueryParameter(EXTRA_SLICE)); } } diff --git a/src/com/android/settings/slices/SlicePreference.java b/src/com/android/settings/slices/SlicePreference.java new file mode 100644 index 0000000000..a88ae768db --- /dev/null +++ b/src/com/android/settings/slices/SlicePreference.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2018 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.slices; + +import android.content.Context; +import android.util.AttributeSet; + +import androidx.slice.Slice; +import androidx.slice.widget.SliceView; + +import com.android.settings.R; +import com.android.settingslib.widget.LayoutPreference; + +/** + * Preference for {@link SliceView} + */ +public class SlicePreference extends LayoutPreference { + private SliceView mSliceView; + + public SlicePreference(Context context, AttributeSet attrs) { + super(context, attrs, R.attr.slicePreferenceStyle); + init(); + } + + public SlicePreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + mSliceView = findViewById(R.id.slice_view); + mSliceView.showTitleItems(true); + mSliceView.setScrollable(false); + } + + public void onSliceUpdated(Slice slice) { + mSliceView.onChanged(slice); + notifyChanged(); + } +} diff --git a/src/com/android/settings/slices/SlicePreferenceController.java b/src/com/android/settings/slices/SlicePreferenceController.java new file mode 100644 index 0000000000..2432c992b3 --- /dev/null +++ b/src/com/android/settings/slices/SlicePreferenceController.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2018 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.slices; + +import android.content.Context; +import android.net.Uri; + +import androidx.annotation.VisibleForTesting; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.Observer; +import androidx.preference.PreferenceScreen; +import androidx.slice.Slice; +import androidx.slice.widget.SliceLiveData; +import androidx.slice.widget.SliceView; + +import com.android.settings.core.BasePreferenceController; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.core.lifecycle.events.OnStop; + +/** + * Default {@link BasePreferenceController} for {@link SliceView}. It will take {@link Uri} for + * Slice and display what's inside this {@link Uri} + */ +public class SlicePreferenceController extends BasePreferenceController implements + LifecycleObserver, OnStart, OnStop, Observer<Slice> { + @VisibleForTesting + LiveData<Slice> mLiveData; + @VisibleForTesting + SlicePreference mSlicePreference; + private Uri mUri; + + public SlicePreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mSlicePreference = screen.findPreference(getPreferenceKey()); + } + + @Override + public int getAvailabilityStatus() { + return mUri != null ? AVAILABLE : UNSUPPORTED_ON_DEVICE; + } + + public void setSliceUri(Uri uri) { + mUri = uri; + mLiveData = SliceLiveData.fromUri(mContext, mUri); + + //TODO(b/120803703): figure out why we need to remove observer first + mLiveData.removeObserver(this); + } + + @Override + public void onStart() { + if (mLiveData != null) { + mLiveData.observeForever(this); + } + } + + @Override + public void onStop() { + if (mLiveData != null) { + mLiveData.removeObserver(this); + } + } + + @Override + public void onChanged(Slice slice) { + mSlicePreference.onSliceUpdated(slice); + } +} diff --git a/src/com/android/settings/slices/SliceRelayReceiver.java b/src/com/android/settings/slices/SliceRelayReceiver.java index c8ec12eae8..b4d868dbf2 100644 --- a/src/com/android/settings/slices/SliceRelayReceiver.java +++ b/src/com/android/settings/slices/SliceRelayReceiver.java @@ -21,6 +21,7 @@ import android.content.Context; import android.content.Intent; import android.net.Uri; import android.text.TextUtils; + import com.android.settingslib.SliceBroadcastRelay; /** diff --git a/src/com/android/settings/slices/Sliceable.java b/src/com/android/settings/slices/Sliceable.java new file mode 100644 index 0000000000..04a3b50363 --- /dev/null +++ b/src/com/android/settings/slices/Sliceable.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2019 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.slices; + +import static android.content.Context.CLIPBOARD_SERVICE; + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.IntentFilter; +import android.widget.Toast; + +import com.android.settings.R; + +/** + * A collection of API making a PreferenceController "sliceable" + */ +public interface Sliceable { + /** + * @return an {@link IntentFilter} that includes all broadcasts which can affect the state of + * this Setting. + */ + default IntentFilter getIntentFilter() { + return null; + } + + /** + * Determines if the controller should be used as a Slice. + * <p> + * Important criteria for a Slice are: + * - Must be secure + * - Must not be a privacy leak + * - Must be understandable as a stand-alone Setting. + * <p> + * This does not guarantee the setting is available. + * + * @return {@code true} if the controller should be used externally as a Slice. + */ + default boolean isSliceable() { + return false; + } + + /** + * @return {@code true} if the setting update asynchronously. + * <p> + * For example, a Wifi controller would return true, because it needs to update the radio + * and wait for it to turn on. + */ + default boolean hasAsyncUpdate() { + return false; + } + + /** + * Copy the key slice information to the clipboard. + * It is highly recommended to show the toast to notify users when implemented this function. + */ + default void copy() { + } + + /** + * Whether or not it's a copyable slice. + */ + default boolean isCopyableSlice() { + return false; + } + + /** + * Whether or not summary comes from something dynamic (ie, not hardcoded in xml) + */ + default boolean useDynamicSliceSummary() { + return false; + } + + /** + * Set the copy content to the clipboard and show the toast. + */ + static void setCopyContent(Context context, CharSequence copyContent, + CharSequence messageTitle) { + final ClipboardManager clipboard = (ClipboardManager) context.getSystemService( + CLIPBOARD_SERVICE); + final ClipData clip = ClipData.newPlainText("text", copyContent); + clipboard.setPrimaryClip(clip); + + final String toast = context.getString(R.string.copyable_slice_toast, messageTitle); + Toast.makeText(context, toast, Toast.LENGTH_SHORT).show(); + } + + /** + * Settings Slices which require background work, such as updating lists should implement a + * {@link SliceBackgroundWorker} and return it here. An example of background work is updating + * a list of Wifi networks available in the area. + * + * @return a {@link Class<? extends SliceBackgroundWorker>} to perform background work for the + * slice. + */ + default Class<? extends SliceBackgroundWorker> getBackgroundWorkerClass() { + return null; + } +} diff --git a/src/com/android/settings/slices/SlicesDatabaseAccessor.java b/src/com/android/settings/slices/SlicesDatabaseAccessor.java index b14a716aca..2553a21771 100644 --- a/src/com/android/settings/slices/SlicesDatabaseAccessor.java +++ b/src/com/android/settings/slices/SlicesDatabaseAccessor.java @@ -18,22 +18,21 @@ package com.android.settings.slices; import static com.android.settings.slices.SlicesDatabaseHelper.Tables.TABLE_SLICES_INDEX; +import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; - -import android.content.Context; import android.os.Binder; import android.util.Pair; +import androidx.slice.Slice; + import com.android.settings.overlay.FeatureFactory; import com.android.settings.slices.SlicesDatabaseHelper.IndexColumns; import java.util.ArrayList; import java.util.List; -import androidx.slice.Slice; - /** * Class used to map a {@link Uri} from {@link SettingsSliceProvider} to a Slice. */ @@ -50,6 +49,7 @@ public class SlicesDatabaseAccessor { IndexColumns.CONTROLLER, IndexColumns.PLATFORM_SLICE, IndexColumns.SLICE_TYPE, + IndexColumns.UNAVAILABLE_SLICE_SUBTITLE, }; // Cursor value for boolean true @@ -71,8 +71,12 @@ public class SlicesDatabaseAccessor { */ public SliceData getSliceDataFromUri(Uri uri) { Pair<Boolean, String> pathData = SliceBuilderUtils.getPathData(uri); - Cursor cursor = getIndexedSliceData(pathData.second /* key */); - return buildSliceData(cursor, uri, pathData.first /* isIntentOnly */); + if (pathData == null) { + throw new IllegalStateException("Invalid Slices uri: " + uri); + } + try (Cursor cursor = getIndexedSliceData(pathData.second /* key */)) { + return buildSliceData(cursor, uri, pathData.first /* isIntentOnly */); + } } /** @@ -81,8 +85,9 @@ public class SlicesDatabaseAccessor { * Used when handling the action of the {@link Slice}. */ public SliceData getSliceDataFromKey(String key) { - Cursor cursor = getIndexedSliceData(key); - return buildSliceData(cursor, null /* uri */, false /* isIntentOnly */); + try (Cursor cursor = getIndexedSliceData(key)) { + return buildSliceData(cursor, null /* uri */, false /* isIntentOnly */); + } } /** @@ -162,6 +167,8 @@ public class SlicesDatabaseAccessor { cursor.getColumnIndex(IndexColumns.PLATFORM_SLICE)) == TRUE; int sliceType = cursor.getInt( cursor.getColumnIndex(IndexColumns.SLICE_TYPE)); + final String unavailableSliceSubtitle = cursor.getString( + cursor.getColumnIndex(IndexColumns.UNAVAILABLE_SLICE_SUBTITLE)); if (isIntentOnly) { sliceType = SliceData.SliceType.INTENT; @@ -179,6 +186,7 @@ public class SlicesDatabaseAccessor { .setUri(uri) .setPlatformDefined(isPlatformDefined) .setSliceType(sliceType) + .setUnavailableSliceSubtitle(unavailableSliceSubtitle) .build(); } diff --git a/src/com/android/settings/slices/SlicesDatabaseHelper.java b/src/com/android/settings/slices/SlicesDatabaseHelper.java index d4b71cfbbd..1f9b05e933 100644 --- a/src/com/android/settings/slices/SlicesDatabaseHelper.java +++ b/src/com/android/settings/slices/SlicesDatabaseHelper.java @@ -17,13 +17,13 @@ package com.android.settings.slices; import android.content.Context; - import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.os.Build; -import androidx.annotation.VisibleForTesting; import android.util.Log; +import androidx.annotation.VisibleForTesting; + import java.util.Locale; /** @@ -36,7 +36,7 @@ public class SlicesDatabaseHelper extends SQLiteOpenHelper { private static final String DATABASE_NAME = "slices_index.db"; private static final String SHARED_PREFS_TAG = "slices_shared_prefs"; - private static final int DATABASE_VERSION = 2; + private static final int DATABASE_VERSION = 5; public interface Tables { String TABLE_SLICES_INDEX = "slices_index"; @@ -93,6 +93,11 @@ public class SlicesDatabaseHelper extends SQLiteOpenHelper { * {@link SliceData.SliceType} representing the inline type of the result. */ String SLICE_TYPE = "slice_type"; + + /** + * Customized subtitle if it's a unavailable slice + */ + String UNAVAILABLE_SLICE_SUBTITLE = "unavailable_slice_subtitle"; } private static final String CREATE_SLICES_TABLE = @@ -117,6 +122,8 @@ public class SlicesDatabaseHelper extends SQLiteOpenHelper { IndexColumns.PLATFORM_SLICE + ", " + IndexColumns.SLICE_TYPE + + ", " + + IndexColumns.UNAVAILABLE_SLICE_SUBTITLE + ");"; private final Context mContext; @@ -143,7 +150,7 @@ public class SlicesDatabaseHelper extends SQLiteOpenHelper { @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion < DATABASE_VERSION) { - Log.d(TAG, "Reconstructing DB from " + oldVersion + "to " + newVersion); + Log.d(TAG, "Reconstructing DB from " + oldVersion + " to " + newVersion); reconstruct(db); } } diff --git a/src/com/android/settings/slices/SlicesFeatureProvider.java b/src/com/android/settings/slices/SlicesFeatureProvider.java index 8dd6547b39..b649eb2204 100644 --- a/src/com/android/settings/slices/SlicesFeatureProvider.java +++ b/src/com/android/settings/slices/SlicesFeatureProvider.java @@ -1,7 +1,9 @@ package com.android.settings.slices; import android.content.Context; +import android.net.Uri; +import com.android.settings.network.telephony.Enhanced4gLteSliceHelper; import com.android.settings.wifi.calling.WifiCallingSliceHelper; /** @@ -11,11 +13,23 @@ public interface SlicesFeatureProvider { boolean DEBUG = false; - SlicesIndexer getSliceIndexer(Context context); - SliceDataConverter getSliceDataConverter(Context context); /** + * Starts a new UI session for the purpose of using Slices. + * + * A UI session is defined as a duration of time when user stays in a UI screen. Screen rotation + * does not break the continuation of session, going to a sub-page and coming out does not break + * the continuation either. Leaving the page and coming back breaks it. + */ + void newUiSession(); + + /** + * Returns the token created in {@link #newUiSession}. + */ + long getUiSessionToken(); + + /** * Asynchronous call to index the data used to build Slices. * If the data is already indexed, the data will not change. */ @@ -27,8 +41,23 @@ public interface SlicesFeatureProvider { */ void indexSliceData(Context context); + + /** + * Return a {@link CustomSliceable} associated to the Uri. + * <p> + * Do not change this method signature to accommodate for a special-case sliceable - a context + * is the only thing that should be needed to create the object. + */ + CustomSliceable getSliceableFromUri(Context context, Uri uri); + /** * Gets new WifiCallingSliceHelper object */ WifiCallingSliceHelper getNewWifiCallingSliceHelper(Context context); + + /** + * Gets new Enhanced4gLteSliceHelper object + */ + Enhanced4gLteSliceHelper getNewEnhanced4gLteSliceHelper(Context context); } + diff --git a/src/com/android/settings/slices/SlicesFeatureProviderImpl.java b/src/com/android/settings/slices/SlicesFeatureProviderImpl.java index 16684bfb02..87d7401c60 100644 --- a/src/com/android/settings/slices/SlicesFeatureProviderImpl.java +++ b/src/com/android/settings/slices/SlicesFeatureProviderImpl.java @@ -1,7 +1,26 @@ +/* + * Copyright (C) 2018 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.slices; import android.content.Context; +import android.net.Uri; +import android.os.SystemClock; +import com.android.settings.network.telephony.Enhanced4gLteSliceHelper; import com.android.settings.wifi.calling.WifiCallingSliceHelper; import com.android.settingslib.utils.ThreadUtils; @@ -10,26 +29,29 @@ import com.android.settingslib.utils.ThreadUtils; */ public class SlicesFeatureProviderImpl implements SlicesFeatureProvider { + private long mUiSessionToken; private SlicesIndexer mSlicesIndexer; private SliceDataConverter mSliceDataConverter; @Override - public SlicesIndexer getSliceIndexer(Context context) { - if (mSlicesIndexer == null) { - mSlicesIndexer = new SlicesIndexer(context); - } - return mSlicesIndexer; - } - - @Override public SliceDataConverter getSliceDataConverter(Context context) { - if(mSliceDataConverter == null) { + if (mSliceDataConverter == null) { mSliceDataConverter = new SliceDataConverter(context.getApplicationContext()); } return mSliceDataConverter; } @Override + public void newUiSession() { + mUiSessionToken = SystemClock.elapsedRealtime(); + } + + @Override + public long getUiSessionToken() { + return mUiSessionToken; + } + + @Override public void indexSliceDataAsync(Context context) { SlicesIndexer indexer = getSliceIndexer(context); ThreadUtils.postOnBackgroundThread(indexer); @@ -45,4 +67,28 @@ public class SlicesFeatureProviderImpl implements SlicesFeatureProvider { public WifiCallingSliceHelper getNewWifiCallingSliceHelper(Context context) { return new WifiCallingSliceHelper(context); } + + @Override + public Enhanced4gLteSliceHelper getNewEnhanced4gLteSliceHelper(Context context) { + return new Enhanced4gLteSliceHelper(context); + } + + @Override + public CustomSliceable getSliceableFromUri(Context context, Uri uri) { + final Uri newUri = CustomSliceRegistry.removeParameterFromUri(uri); + final Class clazz = CustomSliceRegistry.getSliceClassByUri(newUri); + if (clazz == null) { + throw new IllegalArgumentException("No Slice found for uri: " + uri); + } + + final CustomSliceable sliceable = CustomSliceable.createInstance(context, clazz); + return sliceable; + } + + private SlicesIndexer getSliceIndexer(Context context) { + if (mSlicesIndexer == null) { + mSlicesIndexer = new SlicesIndexer(context.getApplicationContext()); + } + return mSlicesIndexer; + } } diff --git a/src/com/android/settings/slices/SlicesIndexer.java b/src/com/android/settings/slices/SlicesIndexer.java index 704d8d6b07..495eb0000b 100644 --- a/src/com/android/settings/slices/SlicesIndexer.java +++ b/src/com/android/settings/slices/SlicesIndexer.java @@ -19,12 +19,12 @@ package com.android.settings.slices; import android.content.ContentValues; import android.content.Context; import android.database.sqlite.SQLiteDatabase; -import androidx.annotation.VisibleForTesting; import android.util.Log; -import com.android.settings.dashboard.DashboardFragment; +import androidx.annotation.VisibleForTesting; import com.android.settings.core.BasePreferenceController; +import com.android.settings.dashboard.DashboardFragment; import com.android.settings.overlay.FeatureFactory; import com.android.settings.slices.SlicesDatabaseHelper.IndexColumns; import com.android.settings.slices.SlicesDatabaseHelper.Tables; @@ -66,13 +66,12 @@ class SlicesIndexer implements Runnable { return; } - SQLiteDatabase database = mHelper.getWritableDatabase(); + final SQLiteDatabase database = mHelper.getWritableDatabase(); + long startTime = System.currentTimeMillis(); + database.beginTransaction(); try { - long startTime = System.currentTimeMillis(); - database.beginTransaction(); - - mHelper.reconstruct(mHelper.getWritableDatabase()); + mHelper.reconstruct(database); List<SliceData> indexData = getSliceData(); insertSliceData(database, indexData); @@ -111,6 +110,8 @@ class SlicesIndexer implements Runnable { values.put(IndexColumns.CONTROLLER, dataRow.getPreferenceController()); values.put(IndexColumns.PLATFORM_SLICE, dataRow.isPlatformDefined()); values.put(IndexColumns.SLICE_TYPE, dataRow.getSliceType()); + values.put(IndexColumns.UNAVAILABLE_SLICE_SUBTITLE, + dataRow.getUnavailableSliceSubtitle()); database.replaceOrThrow(Tables.TABLE_SLICES_INDEX, null /* nullColumnHack */, values); diff --git a/src/com/android/settings/sound/AudioSwitchPreferenceController.java b/src/com/android/settings/sound/AudioSwitchPreferenceController.java index 5cdc993cd6..5b70d16a7a 100644 --- a/src/com/android/settings/sound/AudioSwitchPreferenceController.java +++ b/src/com/android/settings/sound/AudioSwitchPreferenceController.java @@ -17,12 +17,6 @@ package com.android.settings.sound; import static android.media.AudioManager.STREAM_DEVICES_CHANGED_ACTION; -import static android.media.AudioManager.STREAM_MUSIC; -import static android.media.AudioManager.STREAM_VOICE_CALL; -import static android.media.AudioSystem.DEVICE_OUT_ALL_A2DP; -import static android.media.AudioSystem.DEVICE_OUT_ALL_SCO; -import static android.media.AudioSystem.DEVICE_OUT_HEARING_AID; -import static android.media.MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY; import android.bluetooth.BluetoothDevice; import android.content.BroadcastReceiver; @@ -34,17 +28,15 @@ import android.media.AudioDeviceCallback; import android.media.AudioDeviceInfo; import android.media.AudioManager; import android.media.MediaRouter; -import android.media.MediaRouter.Callback; import android.os.Handler; import android.os.Looper; +import android.util.FeatureFlagUtils; +import android.util.Log; + import androidx.preference.ListPreference; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.text.TextUtils; -import android.util.FeatureFlagUtils; -import android.util.Log; -import com.android.settings.R; import com.android.settings.bluetooth.Utils; import com.android.settings.core.BasePreferenceController; import com.android.settings.core.FeatureFlags; @@ -67,15 +59,11 @@ import java.util.concurrent.FutureTask; /** * Abstract class for audio switcher controller to notify subclass * updating the current status of switcher entry. Subclasses must overwrite - * {@link #setActiveBluetoothDevice(BluetoothDevice)} to set the - * active device for corresponding profile. */ public abstract class AudioSwitchPreferenceController extends BasePreferenceController - implements Preference.OnPreferenceChangeListener, BluetoothCallback, - LifecycleObserver, OnStart, OnStop { + implements BluetoothCallback, LifecycleObserver, OnStart, OnStop { - private static final String TAG = "AudioSwitchPreferenceController"; - private static final int INVALID_INDEX = -1; + private static final String TAG = "AudioSwitchPrefCtrl"; protected final List<BluetoothDevice> mConnectedDevices; protected final AudioManager mAudioManager; @@ -86,7 +74,6 @@ public abstract class AudioSwitchPreferenceController extends BasePreferenceCont protected AudioSwitchCallback mAudioSwitchPreferenceCallback; private final AudioManagerAudioDeviceCallback mAudioManagerAudioDeviceCallback; - private final MediaRouterCallback mMediaRouterCallback; private final WiredHeadsetBroadcastReceiver mReceiver; private final Handler mHandler; private LocalBluetoothManager mLocalBluetoothManager; @@ -102,7 +89,6 @@ public abstract class AudioSwitchPreferenceController extends BasePreferenceCont mHandler = new Handler(Looper.getMainLooper()); mAudioManagerAudioDeviceCallback = new AudioManagerAudioDeviceCallback(); mReceiver = new WiredHeadsetBroadcastReceiver(); - mMediaRouterCallback = new MediaRouterCallback(); mConnectedDevices = new ArrayList<>(); final FutureTask<LocalBluetoothManager> localBtManagerFutureTask = new FutureTask<>( // Avoid StrictMode ThreadPolicy violation @@ -133,35 +119,6 @@ public abstract class AudioSwitchPreferenceController extends BasePreferenceCont } @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - final String address = (String) newValue; - if (!(preference instanceof ListPreference)) { - return false; - } - - final ListPreference listPreference = (ListPreference) preference; - if (TextUtils.equals(address, mContext.getText(R.string.media_output_default_summary))) { - // Switch to default device which address is device name - mSelectedIndex = getDefaultDeviceIndex(); - setActiveBluetoothDevice(null); - listPreference.setSummary(mContext.getText(R.string.media_output_default_summary)); - } else { - // Switch to BT device which address is hardware address - final int connectedDeviceIndex = getConnectedDeviceIndex(address); - if (connectedDeviceIndex == INVALID_INDEX) { - return false; - } - final BluetoothDevice btDevice = mConnectedDevices.get(connectedDeviceIndex); - mSelectedIndex = connectedDeviceIndex; - setActiveBluetoothDevice(btDevice); - listPreference.setSummary(btDevice.getAliasName()); - } - return true; - } - - public abstract void setActiveBluetoothDevice(BluetoothDevice device); - - @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); mPreference = screen.findPreference(mPreferenceKey); @@ -188,12 +145,10 @@ public abstract class AudioSwitchPreferenceController extends BasePreferenceCont unregister(); } - /** - * Only concerned about whether the local adapter is connected to any profile of any device and - * are not really concerned about which profile. - */ @Override - public void onConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) { + public void onBluetoothStateChanged(int bluetoothState) { + // To handle the case that Bluetooth on and no connected devices + updateState(mPreference); } @Override @@ -212,17 +167,6 @@ public abstract class AudioSwitchPreferenceController extends BasePreferenceCont updateState(mPreference); } - @Override - public void onBluetoothStateChanged(int bluetoothState) { - } - - /** - * The local Bluetooth adapter has started the remote device discovery process. - */ - @Override - public void onScanningStateChanged(boolean started) { - } - /** * Indicates a change in the bond state of a remote * device. For example, if a device is bonded (paired). @@ -232,14 +176,6 @@ public abstract class AudioSwitchPreferenceController extends BasePreferenceCont updateState(mPreference); } - @Override - public void onDeviceDeleted(CachedBluetoothDevice cachedDevice) { - } - - @Override - public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) { - } - public void setCallback(AudioSwitchCallback callback) { mAudioSwitchPreferenceCallback = callback; } @@ -267,21 +203,15 @@ public abstract class AudioSwitchPreferenceController extends BasePreferenceCont } /** - * get A2dp connected device + * get A2dp devices on all states + * (STATE_DISCONNECTED, STATE_CONNECTING, STATE_CONNECTED, STATE_DISCONNECTING) */ protected List<BluetoothDevice> getConnectedA2dpDevices() { - final List<BluetoothDevice> connectedDevices = new ArrayList<>(); final A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile(); if (a2dpProfile == null) { - return connectedDevices; - } - final List<BluetoothDevice> devices = a2dpProfile.getConnectedDevices(); - for (BluetoothDevice device : devices) { - if (device.isConnected()) { - connectedDevices.add(device); - } + return new ArrayList<>(); } - return connectedDevices; + return a2dpProfile.getConnectedDevices(); } /** @@ -308,28 +238,16 @@ public abstract class AudioSwitchPreferenceController extends BasePreferenceCont } /** - * According to different stream and output device, find the active device from - * the corresponding profile. Hearing aid device could stream both STREAM_MUSIC - * and STREAM_VOICE_CALL. - * - * @param streamType the type of audio streams. - * @return the active device. Return null if the active device is current device - * or streamType is not STREAM_MUSIC or STREAM_VOICE_CALL. + * Find active hearing aid device */ - protected BluetoothDevice findActiveDevice(int streamType) { - if (streamType != STREAM_MUSIC && streamType != STREAM_VOICE_CALL) { - return null; - } - if (isStreamFromOutputDevice(STREAM_MUSIC, DEVICE_OUT_ALL_A2DP)) { - return mProfileManager.getA2dpProfile().getActiveDevice(); - } else if (isStreamFromOutputDevice(STREAM_VOICE_CALL, DEVICE_OUT_ALL_SCO)) { - return mProfileManager.getHeadsetProfile().getActiveDevice(); - } else if (isStreamFromOutputDevice(streamType, DEVICE_OUT_HEARING_AID)) { + protected BluetoothDevice findActiveHearingAidDevice() { + final HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile(); + + if (hearingAidProfile != null) { // The first element is the left active device; the second element is // the right active device. And they will have same hiSyncId. If either // or both side is not active, it will be null on that position. - List<BluetoothDevice> activeDevices = - mProfileManager.getHearingAidProfile().getActiveDevices(); + List<BluetoothDevice> activeDevices = hearingAidProfile.getActiveDevices(); for (BluetoothDevice btDevice : activeDevices) { if (btDevice != null && mConnectedDevices.contains(btDevice)) { // also need to check mConnectedDevices, because one of @@ -341,56 +259,17 @@ public abstract class AudioSwitchPreferenceController extends BasePreferenceCont return null; } - int getDefaultDeviceIndex() { - // Default device is after all connected devices. - return mConnectedDevices.size(); - } - - void setupPreferenceEntries(CharSequence[] mediaOutputs, CharSequence[] mediaValues, - BluetoothDevice activeDevice) { - // default to current device - mSelectedIndex = getDefaultDeviceIndex(); - // default device is after all connected devices. - mediaOutputs[mSelectedIndex] = mContext.getText(R.string.media_output_default_summary); - // use default device name as address - mediaValues[mSelectedIndex] = mContext.getText(R.string.media_output_default_summary); - for (int i = 0, size = mConnectedDevices.size(); i < size; i++) { - final BluetoothDevice btDevice = mConnectedDevices.get(i); - mediaOutputs[i] = btDevice.getAliasName(); - mediaValues[i] = btDevice.getAddress(); - if (btDevice.equals(activeDevice)) { - // select the active connected device. - mSelectedIndex = i; - } - } - } - - void setPreference(CharSequence[] mediaOutputs, CharSequence[] mediaValues, - Preference preference) { - final ListPreference listPreference = (ListPreference) preference; - listPreference.setEntries(mediaOutputs); - listPreference.setEntryValues(mediaValues); - listPreference.setValueIndex(mSelectedIndex); - listPreference.setSummary(mediaOutputs[mSelectedIndex]); - mAudioSwitchPreferenceCallback.onPreferenceDataChanged(listPreference); - } - - private int getConnectedDeviceIndex(String hardwareAddress) { - if (mConnectedDevices != null) { - for (int i = 0, size = mConnectedDevices.size(); i < size; i++) { - final BluetoothDevice btDevice = mConnectedDevices.get(i); - if (TextUtils.equals(btDevice.getAddress(), hardwareAddress)) { - return i; - } - } - } - return INVALID_INDEX; - } + /** + * Find the active device from the corresponding profile. + * + * @return the active device. Return null if the + * corresponding profile don't have active device. + */ + public abstract BluetoothDevice findActiveDevice(); private void register() { mLocalBluetoothManager.getEventManager().registerCallback(this); mAudioManager.registerAudioDeviceCallback(mAudioManagerAudioDeviceCallback, mHandler); - mMediaRouter.addCallback(ROUTE_TYPE_REMOTE_DISPLAY, mMediaRouterCallback); // Register for misc other intent broadcasts. IntentFilter intentFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG); @@ -401,7 +280,6 @@ public abstract class AudioSwitchPreferenceController extends BasePreferenceCont private void unregister() { mLocalBluetoothManager.getEventManager().unregisterCallback(this); mAudioManager.unregisterAudioDeviceCallback(mAudioManagerAudioDeviceCallback); - mMediaRouter.removeCallback(mMediaRouterCallback); mContext.unregisterReceiver(mReceiver); } @@ -429,49 +307,4 @@ public abstract class AudioSwitchPreferenceController extends BasePreferenceCont } } } - - /** Callback for cast device events. */ - private class MediaRouterCallback extends Callback { - @Override - public void onRouteSelected(MediaRouter router, int type, MediaRouter.RouteInfo info) { - } - - @Override - public void onRouteUnselected(MediaRouter router, int type, MediaRouter.RouteInfo info) { - } - - @Override - public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) { - if (info != null && !info.isDefault()) { - // cast mode - updateState(mPreference); - } - } - - @Override - public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) { - } - - @Override - public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo info) { - if (info != null && !info.isDefault()) { - // cast mode - updateState(mPreference); - } - } - - @Override - public void onRouteGrouped(MediaRouter router, MediaRouter.RouteInfo info, - MediaRouter.RouteGroup group, int index) { - } - - @Override - public void onRouteUngrouped(MediaRouter router, MediaRouter.RouteInfo info, - MediaRouter.RouteGroup group) { - } - - @Override - public void onRouteVolumeChanged(MediaRouter router, MediaRouter.RouteInfo info) { - } - } } diff --git a/src/com/android/settings/sound/HandsFreeProfileOutputPreferenceController.java b/src/com/android/settings/sound/HandsFreeProfileOutputPreferenceController.java index 0bf8786197..9157477796 100644 --- a/src/com/android/settings/sound/HandsFreeProfileOutputPreferenceController.java +++ b/src/com/android/settings/sound/HandsFreeProfileOutputPreferenceController.java @@ -17,16 +17,16 @@ package com.android.settings.sound; import static android.bluetooth.IBluetoothHearingAid.HI_SYNC_ID_INVALID; -import static android.media.AudioManager.STREAM_VOICE_CALL; -import static android.media.AudioSystem.DEVICE_OUT_USB_HEADSET; - -import com.android.settingslib.Utils; import android.bluetooth.BluetoothDevice; import android.content.Context; +import android.text.TextUtils; + +import androidx.preference.ListPreference; import androidx.preference.Preference; import com.android.settings.R; +import com.android.settingslib.Utils; import com.android.settingslib.bluetooth.HeadsetProfile; import com.android.settingslib.bluetooth.HearingAidProfile; @@ -34,14 +34,56 @@ import com.android.settingslib.bluetooth.HearingAidProfile; * This class allows switching between HFP-connected & HAP-connected BT devices * while in on-call state. */ -public class HandsFreeProfileOutputPreferenceController extends - AudioSwitchPreferenceController { +public class HandsFreeProfileOutputPreferenceController extends AudioSwitchPreferenceController + implements Preference.OnPreferenceChangeListener { + + private static final int INVALID_INDEX = -1; public HandsFreeProfileOutputPreferenceController(Context context, String key) { super(context, key); } @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + final String address = (String) newValue; + if (!(preference instanceof ListPreference)) { + return false; + } + + final CharSequence defaultSummary = mContext.getText(R.string.media_output_default_summary); + final ListPreference listPreference = (ListPreference) preference; + if (TextUtils.equals(address, defaultSummary)) { + // Switch to default device which address is device name + mSelectedIndex = getDefaultDeviceIndex(); + setActiveBluetoothDevice(null); + listPreference.setSummary(defaultSummary); + } else { + // Switch to BT device which address is hardware address + final int connectedDeviceIndex = getConnectedDeviceIndex(address); + if (connectedDeviceIndex == INVALID_INDEX) { + return false; + } + final BluetoothDevice btDevice = mConnectedDevices.get(connectedDeviceIndex); + mSelectedIndex = connectedDeviceIndex; + setActiveBluetoothDevice(btDevice); + listPreference.setSummary(btDevice.getAliasName()); + } + return true; + } + + private int getConnectedDeviceIndex(String hardwareAddress) { + if (mConnectedDevices != null) { + for (int i = 0, size = mConnectedDevices.size(); i < size; i++) { + final BluetoothDevice btDevice = mConnectedDevices.get(i); + if (TextUtils.equals(btDevice.getAddress(), hardwareAddress)) { + return i; + } + } + } + return INVALID_INDEX; + } + + @Override public void updateState(Preference preference) { if (preference == null) { // In case UI is not ready. @@ -79,18 +121,47 @@ public class HandsFreeProfileOutputPreferenceController extends CharSequence[] mediaValues = new CharSequence[numDevices + 1]; // Setup devices entries, select active connected device - setupPreferenceEntries(mediaOutputs, mediaValues, findActiveDevice(STREAM_VOICE_CALL)); - - if (isStreamFromOutputDevice(STREAM_VOICE_CALL, DEVICE_OUT_USB_HEADSET)) { - // If wired headset is plugged in and active, select to default device. - mSelectedIndex = getDefaultDeviceIndex(); - } + setupPreferenceEntries(mediaOutputs, mediaValues, findActiveDevice()); // Display connected devices, default device and show the active device setPreference(mediaOutputs, mediaValues, preference); } - @Override + int getDefaultDeviceIndex() { + // Default device is after all connected devices. + return mConnectedDevices.size(); + } + + void setupPreferenceEntries(CharSequence[] mediaOutputs, CharSequence[] mediaValues, + BluetoothDevice activeDevice) { + // default to current device + mSelectedIndex = getDefaultDeviceIndex(); + // default device is after all connected devices. + final CharSequence defaultSummary = mContext.getText(R.string.media_output_default_summary); + mediaOutputs[mSelectedIndex] = defaultSummary; + // use default device name as address + mediaValues[mSelectedIndex] = defaultSummary; + for (int i = 0, size = mConnectedDevices.size(); i < size; i++) { + final BluetoothDevice btDevice = mConnectedDevices.get(i); + mediaOutputs[i] = btDevice.getAliasName(); + mediaValues[i] = btDevice.getAddress(); + if (btDevice.equals(activeDevice)) { + // select the active connected device. + mSelectedIndex = i; + } + } + } + + void setPreference(CharSequence[] mediaOutputs, CharSequence[] mediaValues, + Preference preference) { + final ListPreference listPreference = (ListPreference) preference; + listPreference.setEntries(mediaOutputs); + listPreference.setEntryValues(mediaValues); + listPreference.setValueIndex(mSelectedIndex); + listPreference.setSummary(mediaOutputs[mSelectedIndex]); + mAudioSwitchPreferenceCallback.onPreferenceDataChanged(listPreference); + } + public void setActiveBluetoothDevice(BluetoothDevice device) { if (!Utils.isAudioModeOngoingCall(mContext)) { return; @@ -106,4 +177,15 @@ public class HandsFreeProfileOutputPreferenceController extends hfpProfile.setActiveDevice(device); } } + + @Override + public BluetoothDevice findActiveDevice() { + BluetoothDevice activeDevice = findActiveHearingAidDevice(); + final HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile(); + + if (activeDevice == null && headsetProfile != null) { + activeDevice = headsetProfile.getActiveDevice(); + } + return activeDevice; + } } diff --git a/src/com/android/settings/sound/MediaOutputPreferenceController.java b/src/com/android/settings/sound/MediaOutputPreferenceController.java index 5b1c217ff4..1831ad6d70 100644 --- a/src/com/android/settings/sound/MediaOutputPreferenceController.java +++ b/src/com/android/settings/sound/MediaOutputPreferenceController.java @@ -16,27 +16,28 @@ package com.android.settings.sound; -import static android.bluetooth.IBluetoothHearingAid.HI_SYNC_ID_INVALID; -import static android.media.AudioManager.STREAM_MUSIC; -import static android.media.AudioSystem.DEVICE_OUT_REMOTE_SUBMIX; -import static android.media.AudioSystem.DEVICE_OUT_USB_HEADSET; - -import com.android.settingslib.Utils; - import android.bluetooth.BluetoothDevice; import android.content.Context; +import android.content.Intent; import android.media.AudioManager; +import android.text.TextUtils; + import androidx.preference.Preference; import com.android.settings.R; +import com.android.settingslib.Utils; import com.android.settingslib.bluetooth.A2dpProfile; import com.android.settingslib.bluetooth.HearingAidProfile; +import com.android.settingslib.media.MediaOutputSliceConstants; + +import java.util.List; /** - * This class which allows switching between A2dp-connected & HAP-connected BT devices. - * A few conditions will disable this switcher: - * - No available BT device(s) - * - Media stream captured by cast device + * This class allows launching MediaOutputSlice to switch output device. + * Preference would hide only when + * - Bluetooth = OFF + * - Bluetooth = ON and Connected Devices = 0 and Previously Connected = 0 + * - Media stream captured by remote device * - During a call. */ public class MediaOutputPreferenceController extends AudioSwitchPreferenceController { @@ -52,13 +53,6 @@ public class MediaOutputPreferenceController extends AudioSwitchPreferenceContro return; } - if (isStreamFromOutputDevice(STREAM_MUSIC, DEVICE_OUT_REMOTE_SUBMIX)) { - // In cast mode, disable switch entry. - mPreference.setVisible(false); - preference.setSummary(mContext.getText(R.string.media_output_summary_unavailable)); - return; - } - if (Utils.isAudioModeOngoingCall(mContext)) { // Ongoing call status, switch entry for media will be disabled. mPreference.setVisible(false); @@ -67,55 +61,62 @@ public class MediaOutputPreferenceController extends AudioSwitchPreferenceContro return; } - mConnectedDevices.clear(); - // Otherwise, list all of the A2DP connected device and display the active device. - if (mAudioManager.getMode() == AudioManager.MODE_NORMAL) { - mConnectedDevices.addAll(getConnectedA2dpDevices()); - mConnectedDevices.addAll(getConnectedHearingAidDevices()); - } - - final int numDevices = mConnectedDevices.size(); - if (numDevices == 0) { - // Disable switch entry if there is no connected devices. - mPreference.setVisible(false); - final CharSequence summary = mContext.getText(R.string.media_output_default_summary); - final CharSequence[] defaultMediaOutput = new CharSequence[]{summary}; - mSelectedIndex = getDefaultDeviceIndex(); - preference.setSummary(summary); - setPreference(defaultMediaOutput, defaultMediaOutput, preference); - return; + boolean deviceConnected = false; + BluetoothDevice activeDevice = null; + // Show preference if there is connected or previously connected device + // Find active device and set its name as the preference's summary + List<BluetoothDevice> connectedA2dpDevices = getConnectedA2dpDevices(); + List<BluetoothDevice> connectedHADevices = getConnectedHearingAidDevices(); + if (mAudioManager.getMode() == AudioManager.MODE_NORMAL + && ((connectedA2dpDevices != null && !connectedA2dpDevices.isEmpty()) + || (connectedHADevices != null && !connectedHADevices.isEmpty()))) { + deviceConnected = true; + activeDevice = findActiveDevice(); } + mPreference.setVisible(deviceConnected); + mPreference.setSummary((activeDevice == null) ? + mContext.getText(R.string.media_output_default_summary) : + activeDevice.getAliasName()); + } - mPreference.setVisible(true); - CharSequence[] mediaOutputs = new CharSequence[numDevices + 1]; - CharSequence[] mediaValues = new CharSequence[numDevices + 1]; - - // Setup devices entries, select active connected device - setupPreferenceEntries(mediaOutputs, mediaValues, findActiveDevice(STREAM_MUSIC)); + @Override + public BluetoothDevice findActiveDevice() { + BluetoothDevice activeDevice = findActiveHearingAidDevice(); + final A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile(); - if (isStreamFromOutputDevice(STREAM_MUSIC, DEVICE_OUT_USB_HEADSET)) { - // If wired headset is plugged in and active, select to default device. - mSelectedIndex = getDefaultDeviceIndex(); + if (activeDevice == null && a2dpProfile != null) { + activeDevice = a2dpProfile.getActiveDevice(); } - - // Display connected devices, default device and show the active device - setPreference(mediaOutputs, mediaValues, preference); + return activeDevice; } + /** + * Find active hearing aid device + */ @Override - public void setActiveBluetoothDevice(BluetoothDevice device) { - if (mAudioManager.getMode() != AudioManager.MODE_NORMAL) { - return; + protected BluetoothDevice findActiveHearingAidDevice() { + final HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile(); + + if (hearingAidProfile != null) { + List<BluetoothDevice> activeDevices = hearingAidProfile.getActiveDevices(); + for (BluetoothDevice btDevice : activeDevices) { + if (btDevice != null) { + return btDevice; + } + } } - final HearingAidProfile hapProfile = mProfileManager.getHearingAidProfile(); - final A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile(); - if (hapProfile != null && a2dpProfile != null && device == null) { - hapProfile.setActiveDevice(null); - a2dpProfile.setActiveDevice(null); - } else if (hapProfile != null && hapProfile.getHiSyncId(device) != HI_SYNC_ID_INVALID) { - hapProfile.setActiveDevice(device); - } else if (a2dpProfile != null) { - a2dpProfile.setActiveDevice(device); + return null; + } + + @Override + public boolean handlePreferenceTreeClick(Preference preference) { + if (TextUtils.equals(preference.getKey(), getPreferenceKey())) { + final Intent intent = new Intent() + .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mContext.startActivity(intent); + return true; } + return false; } } diff --git a/src/com/android/settings/support/NewDeviceIntroSuggestionActivity.java b/src/com/android/settings/support/NewDeviceIntroSuggestionActivity.java deleted file mode 100644 index 1ec566e01b..0000000000 --- a/src/com/android/settings/support/NewDeviceIntroSuggestionActivity.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright (C) 2017 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.support; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.net.Uri; -import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; -import android.text.TextUtils; -import android.text.format.DateUtils; -import android.util.Log; - -import com.android.settings.R; -import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider; -import com.android.settings.overlay.FeatureFactory; -import com.android.settings.overlay.SupportFeatureProvider; - -import java.util.List; - -public class NewDeviceIntroSuggestionActivity extends Activity { - - private static final String TAG = "NewDeviceIntroSugg"; - @VisibleForTesting - static final String PREF_KEY_SUGGGESTION_FIRST_DISPLAY_TIME = - "pref_new_device_intro_suggestion_first_display_time_ms"; - @VisibleForTesting - static final String PREF_KEY_SUGGGESTION_COMPLETE = - "pref_new_device_intro_suggestion_complete"; - @VisibleForTesting - static final long PERMANENT_DISMISS_THRESHOLD = DateUtils.DAY_IN_MILLIS * 14; - - public static final String TIPS_PACKAGE_NAME = "com.google.android.apps.tips"; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - final Intent intent = getLaunchIntent(this); - if (intent != null) { - final SuggestionFeatureProvider featureProvider = FeatureFactory.getFactory(this) - .getSuggestionFeatureProvider(this); - final SharedPreferences prefs = featureProvider.getSharedPrefs(this); - prefs.edit().putBoolean(PREF_KEY_SUGGGESTION_COMPLETE, true).commit(); - startActivity(intent); - } - finish(); - } - - public static boolean isSuggestionComplete(Context context) { - // Always returns 'true' if Tips application exists. Check b/77652536 for more details. - return isTipsInstalledAsSystemApp(context) - || !isSupported(context) - || isExpired(context) - || hasLaunchedBefore(context) - || !canOpenUrlInBrowser(context); - } - - private static boolean isSupported(Context context) { - return context.getResources() - .getBoolean(R.bool.config_new_device_intro_suggestion_supported); - } - - private static boolean isExpired(Context context) { - final SuggestionFeatureProvider featureProvider = FeatureFactory.getFactory(context) - .getSuggestionFeatureProvider(context); - final SharedPreferences prefs = featureProvider.getSharedPrefs(context); - final long currentTimeMs = System.currentTimeMillis(); - final long firstDisplayTimeMs; - - if (!prefs.contains(PREF_KEY_SUGGGESTION_FIRST_DISPLAY_TIME)) { - firstDisplayTimeMs = currentTimeMs; - prefs.edit().putLong(PREF_KEY_SUGGGESTION_FIRST_DISPLAY_TIME, currentTimeMs).commit(); - } else { - firstDisplayTimeMs = prefs.getLong(PREF_KEY_SUGGGESTION_FIRST_DISPLAY_TIME, -1); - } - - final long dismissTimeMs = firstDisplayTimeMs + PERMANENT_DISMISS_THRESHOLD; - - final boolean expired = currentTimeMs > dismissTimeMs; - - Log.d(TAG, "is suggestion expired: " + expired); - return expired; - } - - private static boolean canOpenUrlInBrowser(Context context) { - final Intent intent = getLaunchIntent(context); - if (intent == null) { - // No url/intent to launch. - return false; - } - // Make sure we can handle the intent. - final List<ResolveInfo> resolveInfos = - context.getPackageManager().queryIntentActivities(intent, 0); - return resolveInfos != null && resolveInfos.size() != 0; - } - - private static boolean hasLaunchedBefore(Context context) { - final SuggestionFeatureProvider featureProvider = FeatureFactory.getFactory(context) - .getSuggestionFeatureProvider(context); - final SharedPreferences prefs = featureProvider.getSharedPrefs(context); - return prefs.getBoolean(PREF_KEY_SUGGGESTION_COMPLETE, false); - } - - @VisibleForTesting - static Intent getLaunchIntent(Context context) { - final SupportFeatureProvider supportProvider = FeatureFactory.getFactory(context) - .getSupportFeatureProvider(context); - if (supportProvider == null) { - return null; - } - final String url = supportProvider.getNewDeviceIntroUrl(context); - if (TextUtils.isEmpty(url)) { - return null; - } - return new Intent() - .setAction(Intent.ACTION_VIEW) - .addCategory(Intent.CATEGORY_BROWSABLE) - .setData(Uri.parse(url)); - } - - /** - * Check if the specified package exists and is marked with <i>FLAG_SYSTEM</i> - */ - private static boolean isTipsInstalledAsSystemApp(@NonNull Context context) { - try { - final PackageInfo info = context.getPackageManager().getPackageInfo(TIPS_PACKAGE_NAME, - PackageManager.MATCH_SYSTEM_ONLY); - return info != null; - } catch (PackageManager.NameNotFoundException e) { - Log.w(TAG, "Cannot find the package: " + TIPS_PACKAGE_NAME, e); - return false; - } - } -} diff --git a/src/com/android/settings/support/SupportDashboardActivity.java b/src/com/android/settings/support/SupportDashboardActivity.java index e829577cc3..8703ec7966 100644 --- a/src/com/android/settings/support/SupportDashboardActivity.java +++ b/src/com/android/settings/support/SupportDashboardActivity.java @@ -26,6 +26,7 @@ import com.android.settings.overlay.SupportFeatureProvider; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; import com.android.settings.search.SearchIndexableRaw; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.List; @@ -33,6 +34,7 @@ import java.util.List; /** * Trampoline activity that decides which version of support should be shown to the user. */ +@SearchIndexable public class SupportDashboardActivity extends Activity implements Indexable { @Override @@ -41,9 +43,9 @@ public class SupportDashboardActivity extends Activity implements Indexable { SupportFeatureProvider supportFeatureProvider = FeatureFactory.getFactory(this) .getSupportFeatureProvider(this); - // try to launch support v2 if we have the feature provider + // try to launch support if we have the feature provider if (supportFeatureProvider != null) { - supportFeatureProvider.startSupportV2(this); + supportFeatureProvider.startSupport(this); finish(); } } @@ -66,7 +68,6 @@ public class SupportDashboardActivity extends Activity implements Indexable { data.title = context.getString(R.string.page_tab_title_support); data.screenTitle = context.getString(R.string.settings_label); data.summaryOn = context.getString(R.string.support_summary); - data.iconResId = R.drawable.ic_homepage_support; data.intentTargetPackage = context.getPackageName(); data.intentTargetClass = SupportDashboardActivity.class.getName(); data.intentAction = Intent.ACTION_MAIN; diff --git a/src/com/android/settings/support/SupportPhone.java b/src/com/android/settings/support/SupportPhone.java deleted file mode 100644 index d27dca5a54..0000000000 --- a/src/com/android/settings/support/SupportPhone.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settings.support; - -import android.content.Intent; -import android.net.Uri; -import android.os.Parcel; -import android.os.Parcelable; -import android.text.TextUtils; - -import java.text.ParseException; - -/** - * Data model for a support phone number. - */ -public final class SupportPhone implements Parcelable { - - public final String language; - public final String number; - public final boolean isTollFree; - - public SupportPhone(String config) throws ParseException { - // Config follows this format: language:[tollfree|tolled]:number - final String[] tokens = config.split(":"); - if (tokens.length != 3) { - throw new ParseException("Phone config is invalid " + config, 0); - } - language = tokens[0]; - isTollFree = TextUtils.equals(tokens[1], "tollfree"); - number = tokens[2]; - } - - protected SupportPhone(Parcel in) { - language = in.readString(); - number = in.readString(); - isTollFree = in.readInt() != 0; - } - - public Intent getDialIntent() { - return new Intent(Intent.ACTION_DIAL) - .setData(new Uri.Builder() - .scheme("tel") - .appendPath(number) - .build()); - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeString(language); - dest.writeString(number); - dest.writeInt(isTollFree ? 1 : 0); - } - - public static final Creator<SupportPhone> CREATOR = new Creator<SupportPhone>() { - @Override - public SupportPhone createFromParcel(Parcel in) { - return new SupportPhone(in); - } - - @Override - public SupportPhone[] newArray(int size) { - return new SupportPhone[size]; - } - }; -} diff --git a/src/com/android/settings/support/SupportPreferenceController.java b/src/com/android/settings/support/SupportPreferenceController.java new file mode 100644 index 0000000000..793842f871 --- /dev/null +++ b/src/com/android/settings/support/SupportPreferenceController.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2018 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.support; + +import android.app.Activity; +import android.content.Context; +import android.text.TextUtils; + +import androidx.preference.Preference; + +import com.android.settings.core.BasePreferenceController; +import com.android.settings.overlay.FeatureFactory; +import com.android.settings.overlay.SupportFeatureProvider; + +public class SupportPreferenceController extends BasePreferenceController { + + private final SupportFeatureProvider mSupportFeatureProvider; + + private Activity mActivity; + + public SupportPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + mSupportFeatureProvider = FeatureFactory.getFactory(context) + .getSupportFeatureProvider(context); + } + + public void setActivity(Activity activity) { + mActivity = activity; + } + + @Override + public int getAvailabilityStatus() { + return mSupportFeatureProvider == null ? UNSUPPORTED_ON_DEVICE : AVAILABLE; + } + + @Override + public boolean handlePreferenceTreeClick(Preference preference) { + if (preference == null || mActivity == null || + !TextUtils.equals(preference.getKey(), getPreferenceKey())) { + return false; + } + mSupportFeatureProvider.startSupport(mActivity); + return true; + + } +} diff --git a/src/com/android/settings/support/actionbar/HelpMenuController.java b/src/com/android/settings/support/actionbar/HelpMenuController.java index 1188fc2bb1..498cc3ed52 100644 --- a/src/com/android/settings/support/actionbar/HelpMenuController.java +++ b/src/com/android/settings/support/actionbar/HelpMenuController.java @@ -20,11 +20,12 @@ import static com.android.settings.support.actionbar.HelpResourceProvider.HELP_U import android.annotation.NonNull; import android.app.Activity; -import android.app.Fragment; import android.os.Bundle; import android.view.Menu; import android.view.MenuInflater; +import androidx.fragment.app.Fragment; + import com.android.settingslib.HelpUtils; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.ObservableFragment; @@ -39,11 +40,11 @@ public class HelpMenuController implements LifecycleObserver, OnCreateOptionsMen private final Fragment mHost; public static void init(@NonNull ObservablePreferenceFragment host) { - host.getLifecycle().addObserver(new HelpMenuController(host)); + host.getSettingsLifecycle().addObserver(new HelpMenuController(host)); } public static void init(@NonNull ObservableFragment host) { - host.getLifecycle().addObserver(new HelpMenuController(host)); + host.getSettingsLifecycle().addObserver(new HelpMenuController(host)); } private HelpMenuController(@NonNull Fragment host) { diff --git a/src/com/android/settings/survey/SurveyMixin.java b/src/com/android/settings/survey/SurveyMixin.java index 9b7a9df0c7..5de265347e 100644 --- a/src/com/android/settings/survey/SurveyMixin.java +++ b/src/com/android/settings/survey/SurveyMixin.java @@ -16,8 +16,10 @@ package com.android.settings.survey; import android.app.Activity; -import android.app.Fragment; import android.content.BroadcastReceiver; + +import androidx.fragment.app.Fragment; + import com.android.settings.overlay.FeatureFactory; import com.android.settings.overlay.SurveyFeatureProvider; import com.android.settingslib.core.lifecycle.LifecycleObserver; diff --git a/src/com/android/settings/system/ResetDashboardFragment.java b/src/com/android/settings/system/ResetDashboardFragment.java index 03543cc498..5243d6a393 100644 --- a/src/com/android/settings/system/ResetDashboardFragment.java +++ b/src/com/android/settings/system/ResetDashboardFragment.java @@ -16,10 +16,10 @@ package com.android.settings.system; +import android.app.settings.SettingsEnums; import android.content.Context; import android.provider.SearchIndexableResource; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.applications.manageapplications.ResetAppPrefPreferenceController; import com.android.settings.dashboard.DashboardFragment; @@ -28,17 +28,19 @@ import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.List; +@SearchIndexable public class ResetDashboardFragment extends DashboardFragment { private static final String TAG = "ResetDashboardFragment"; @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.RESET_DASHBOARD; + return SettingsEnums.RESET_DASHBOARD; } @Override @@ -53,7 +55,7 @@ public class ResetDashboardFragment extends DashboardFragment { @Override protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { - return buildPreferenceControllers(context, getLifecycle()); + return buildPreferenceControllers(context, getSettingsLifecycle()); } private static List<AbstractPreferenceController> buildPreferenceControllers(Context context, diff --git a/src/com/android/settings/system/ResetPreferenceController.java b/src/com/android/settings/system/ResetPreferenceController.java index ec0c27b4ac..050efc4cd1 100644 --- a/src/com/android/settings/system/ResetPreferenceController.java +++ b/src/com/android/settings/system/ResetPreferenceController.java @@ -29,7 +29,7 @@ public class ResetPreferenceController extends BasePreferenceController { @Override public int getAvailabilityStatus() { return mContext.getResources().getBoolean(R.bool.config_show_reset_dashboard) - ? AVAILABLE + ? AVAILABLE_UNSEARCHABLE : UNSUPPORTED_ON_DEVICE; } } diff --git a/src/com/android/settings/system/SystemDashboardFragment.java b/src/com/android/settings/system/SystemDashboardFragment.java index 62be5ffd0d..3ab31e3a17 100644 --- a/src/com/android/settings/system/SystemDashboardFragment.java +++ b/src/com/android/settings/system/SystemDashboardFragment.java @@ -15,29 +15,35 @@ */ package com.android.settings.system; +import android.app.settings.SettingsEnums; import android.content.Context; import android.os.Bundle; import android.provider.SearchIndexableResource; + +import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceGroup; import androidx.preference.PreferenceScreen; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; -import com.android.settings.backup.BackupSettingsActivityPreferenceController; import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.overlay.FeatureFactory; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; +import com.android.settingslib.search.SearchIndexable; import java.util.Arrays; import java.util.List; +@SearchIndexable public class SystemDashboardFragment extends DashboardFragment { private static final String TAG = "SystemDashboardFrag"; private static final String KEY_RESET = "reset_dashboard"; + public static final String EXTRA_SHOW_AWARE_DISABLED = "show_aware_dialog_disabled"; + @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); @@ -47,11 +53,22 @@ public class SystemDashboardFragment extends DashboardFragment { if (getVisiblePreferenceCount(screen) == screen.getInitialExpandedChildrenCount() + 1) { screen.setInitialExpandedChildrenCount(Integer.MAX_VALUE); } + + showRestrictionDialog(); + } + + @VisibleForTesting + public void showRestrictionDialog() { + final Bundle args = getArguments(); + if (args != null && args.getBoolean(EXTRA_SHOW_AWARE_DISABLED, false)) { + FeatureFactory.getFactory(getContext()).getAwareFeatureProvider() + .showRestrictionDialog(this); + } } @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.SETTINGS_SYSTEM_CATEGORY; + return SettingsEnums.SETTINGS_SYSTEM_CATEGORY; } @Override @@ -94,14 +111,5 @@ public class SystemDashboardFragment extends DashboardFragment { sir.xmlResId = R.xml.system_dashboard_fragment; return Arrays.asList(sir); } - - @Override - public List<String> getNonIndexableKeys(Context context) { - List<String> keys = super.getNonIndexableKeys(context); - keys.add((new BackupSettingsActivityPreferenceController( - context).getPreferenceKey())); - keys.add(KEY_RESET); - return keys; - } }; }
\ No newline at end of file diff --git a/src/com/android/settings/system/SystemUpdatePreferenceController.java b/src/com/android/settings/system/SystemUpdatePreferenceController.java index 54ab924a57..38a88b9543 100644 --- a/src/com/android/settings/system/SystemUpdatePreferenceController.java +++ b/src/com/android/settings/system/SystemUpdatePreferenceController.java @@ -25,12 +25,13 @@ import android.os.Bundle; import android.os.PersistableBundle; import android.os.SystemUpdateManager; import android.os.UserManager; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; import android.telephony.CarrierConfigManager; import android.text.TextUtils; import android.util.Log; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + import com.android.settings.R; import com.android.settings.Utils; import com.android.settings.core.BasePreferenceController; @@ -138,7 +139,8 @@ public class SystemUpdatePreferenceController extends BasePreferenceController { } Log.d(TAG, "ciActionOnSysUpdate: broadcasting intent " + intentStr + " with extra " + extra + ", " + extraVal); + intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND); mContext.getApplicationContext().sendBroadcast(intent); } } -}
\ No newline at end of file +} diff --git a/src/com/android/settings/tts/TextToSpeechSettings.java b/src/com/android/settings/tts/TextToSpeechSettings.java index 95ac8f50fc..d4c7318002 100644 --- a/src/com/android/settings/tts/TextToSpeechSettings.java +++ b/src/com/android/settings/tts/TextToSpeechSettings.java @@ -20,7 +20,7 @@ import static android.provider.Settings.Secure.TTS_DEFAULT_PITCH; import static android.provider.Settings.Secure.TTS_DEFAULT_RATE; import static android.provider.Settings.Secure.TTS_DEFAULT_SYNTH; -import android.app.AlertDialog; +import android.app.settings.SettingsEnums; import android.content.ActivityNotFoundException; import android.content.ContentResolver; import android.content.Context; @@ -31,26 +31,27 @@ import android.speech.tts.TextToSpeech; import android.speech.tts.TextToSpeech.EngineInfo; import android.speech.tts.TtsEngines; import android.speech.tts.UtteranceProgressListener; -import androidx.preference.ListPreference; -import androidx.preference.Preference; import android.text.TextUtils; import android.util.Log; import android.util.Pair; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.appcompat.app.AlertDialog; +import androidx.preference.ListPreference; +import androidx.preference.Preference; + import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.SettingsPreferenceFragment; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; -import com.android.settings.widget.ActionButtonPreference; import com.android.settings.widget.GearPreference; import com.android.settings.widget.SeekBarPreference; +import com.android.settingslib.search.SearchIndexable; +import com.android.settingslib.widget.ActionButtonsPreference; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -58,9 +59,10 @@ import java.util.MissingResourceException; import java.util.Objects; import java.util.Set; +@SearchIndexable public class TextToSpeechSettings extends SettingsPreferenceFragment implements Preference.OnPreferenceChangeListener, - GearPreference.OnGearClickListener, Indexable { + GearPreference.OnGearClickListener { private static final String STATE_KEY_LOCALE_ENTRIES = "locale_entries"; private static final String STATE_KEY_LOCALE_ENTRY_VALUES = "locale_entry_values"; @@ -110,7 +112,7 @@ public class TextToSpeechSettings extends SettingsPreferenceFragment private SeekBarPreference mDefaultPitchPref; private SeekBarPreference mDefaultRatePref; - private ActionButtonPreference mActionButtons; + private ActionButtonsPreference mActionButtons; private int mDefaultPitch = TextToSpeech.Engine.DEFAULT_PITCH; private int mDefaultRate = TextToSpeech.Engine.DEFAULT_RATE; @@ -144,16 +146,11 @@ public class TextToSpeechSettings extends SettingsPreferenceFragment * screen for the first time (as opposed to when a user changes his choice * of engine). */ - private final TextToSpeech.OnInitListener mInitListener = new TextToSpeech.OnInitListener() { - @Override - public void onInit(int status) { - onInitEngine(status); - } - }; + private final TextToSpeech.OnInitListener mInitListener = this::onInitEngine; @Override public int getMetricsCategory() { - return MetricsEvent.TTS_TEXT_TO_SPEECH; + return SettingsEnums.TTS_TEXT_TO_SPEECH; } @Override @@ -171,13 +168,11 @@ public class TextToSpeechSettings extends SettingsPreferenceFragment mDefaultPitchPref = (SeekBarPreference) findPreference(KEY_DEFAULT_PITCH); mDefaultRatePref = (SeekBarPreference) findPreference(KEY_DEFAULT_RATE); - mActionButtons = ((ActionButtonPreference) findPreference(KEY_ACTION_BUTTONS)) + mActionButtons = ((ActionButtonsPreference) findPreference(KEY_ACTION_BUTTONS)) .setButton1Text(R.string.tts_play) - .setButton1Positive(true) .setButton1OnClickListener(v -> speakSampleText()) .setButton1Enabled(false) .setButton2Text(R.string.tts_reset) - .setButton2Positive(false) .setButton2OnClickListener(v -> resetTts()) .setButton1Enabled(true); @@ -212,6 +207,11 @@ public class TextToSpeechSettings extends SettingsPreferenceFragment @Override public void onResume() { super.onResume(); + // We tend to change the summary contents of our widgets, which at higher text sizes causes + // them to resize, which results in the recyclerview smoothly animating them at inopportune + // times. Disable the animation so widgets snap to their positions rather than sliding + // around while the user is interacting with it. + getListView().getItemAnimator().setMoveDuration(0); if (mTts == null || mCurrentDefaultLocale == null) { return; @@ -248,15 +248,20 @@ public class TextToSpeechSettings extends SettingsPreferenceFragment mTts.setOnUtteranceProgressListener(new UtteranceProgressListener() { @Override public void onStart(String utteranceId) { + updateWidgetState(false); } @Override public void onDone(String utteranceId) { + updateWidgetState(true); } @Override public void onError(String utteranceId) { Log.e(TAG, "Error while trying to synthesize sample text"); + // Re-enable just in case, although there isn't much hope that following synthesis + // requests are going to succeed. + updateWidgetState(true); } }); } @@ -320,7 +325,6 @@ public class TextToSpeechSettings extends SettingsPreferenceFragment if (mCurrentEngine != null) { EngineInfo info = mEnginesHelper.getEngineInfo(mCurrentEngine); - Preference mEnginePreference = findPreference(KEY_TTS_ENGINE_PREFERENCE); ((GearPreference) mEnginePreference).setOnGearClickListener(this); mEnginePreference.setSummary(info.label); @@ -362,14 +366,7 @@ public class TextToSpeechSettings extends SettingsPreferenceFragment if (status == TextToSpeech.SUCCESS) { if (DBG) Log.d(TAG, "TTS engine for settings screen initialized."); checkDefaultLocale(); - getActivity() - .runOnUiThread( - new Runnable() { - @Override - public void run() { - mLocalePreference.setEnabled(true); - } - }); + getActivity().runOnUiThread(() -> mLocalePreference.setEnabled(true)); } else { if (DBG) { Log.d(TAG, @@ -513,14 +510,7 @@ public class TextToSpeechSettings extends SettingsPreferenceFragment } // Sort it - Collections.sort( - entryPairs, - new Comparator<Pair<String, Locale>>() { - @Override - public int compare(Pair<String, Locale> lhs, Pair<String, Locale> rhs) { - return lhs.first.compareToIgnoreCase(rhs.first); - } - }); + Collections.sort(entryPairs, (lhs, rhs) -> lhs.first.compareToIgnoreCase(rhs.first)); // Get two arrays out of one of pairs mSelectedLocaleIndex = 0; // Will point to the R.string.tts_lang_use_system value @@ -708,9 +698,11 @@ public class TextToSpeechSettings extends SettingsPreferenceFragment } private void updateWidgetState(boolean enable) { - mActionButtons.setButton1Enabled(enable); - mDefaultRatePref.setEnabled(enable); - mDefaultPitchPref.setEnabled(enable); + getActivity().runOnUiThread(() -> { + mActionButtons.setButton1Enabled(enable); + mDefaultRatePref.setEnabled(enable); + mDefaultPitchPref.setEnabled(enable); + }); } private void displayNetworkAlert() { @@ -787,13 +779,6 @@ public class TextToSpeechSettings extends SettingsPreferenceFragment sir.xmlResId = R.xml.tts_settings; return Arrays.asList(sir); } - - @Override - public List<String> getNonIndexableKeys(Context context) { - final List<String> keys = super.getNonIndexableKeys(context); - keys.add("tts_engine_preference"); - return keys; - } }; } diff --git a/src/com/android/settings/tts/TtsEnginePreference.java b/src/com/android/settings/tts/TtsEnginePreference.java index a5cb1fd365..8f15db266e 100644 --- a/src/com/android/settings/tts/TtsEnginePreference.java +++ b/src/com/android/settings/tts/TtsEnginePreference.java @@ -16,20 +16,21 @@ package com.android.settings.tts; -import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; -import android.content.Intent; import android.speech.tts.TextToSpeech.EngineInfo; -import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; import android.util.Log; import android.widget.Checkable; import android.widget.CompoundButton; import android.widget.RadioButton; +import androidx.appcompat.app.AlertDialog; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + import com.android.settings.R; -import com.android.settings.SettingsActivity; + +import androidx.annotation.VisibleForTesting; public class TtsEnginePreference extends Preference { @@ -46,6 +47,7 @@ public class TtsEnginePreference extends Preference { * The shared radio button state, which button is checked etc. */ private final RadioButtonGroupState mSharedState; + private RadioButton mRadioButton; /** * When true, the change callbacks on the radio button will not @@ -53,21 +55,20 @@ public class TtsEnginePreference extends Preference { */ private volatile boolean mPreventRadioButtonCallbacks; - private RadioButton mRadioButton; - private Intent mVoiceCheckData; - private final CompoundButton.OnCheckedChangeListener mRadioChangeListener = - new CompoundButton.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - onRadioButtonClicked(buttonView, isChecked); - } - }; - - public TtsEnginePreference(Context context, EngineInfo info, RadioButtonGroupState state, - SettingsActivity prefActivity) { + new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + onRadioButtonClicked(buttonView, isChecked); + } + }; + + public TtsEnginePreference(Context context, EngineInfo info, RadioButtonGroupState state) { super(context); - setLayoutResource(R.layout.preference_tts_engine); + + setWidgetLayoutResource(R.layout.preference_widget_radiobutton); + setLayoutResource(R.layout.preference_radio); + setIconSpaceReserved(false); mSharedState = state; mEngineInfo = info; @@ -86,9 +87,8 @@ public class TtsEnginePreference extends Preference { "setSharedState()"); } - final RadioButton rb = (RadioButton) view.findViewById(R.id.tts_engine_radiobutton); + final RadioButton rb = view.itemView.findViewById(android.R.id.checkbox); rb.setOnCheckedChangeListener(mRadioChangeListener); - rb.setText(mEngineInfo.label); boolean isChecked = getKey().equals(mSharedState.getCurrentKey()); if (isChecked) { @@ -98,12 +98,12 @@ public class TtsEnginePreference extends Preference { mPreventRadioButtonCallbacks = true; rb.setChecked(isChecked); mPreventRadioButtonCallbacks = false; - mRadioButton = rb; } - public void setVoiceDataDetails(Intent data) { - mVoiceCheckData = data; + @Override + public void onClick() { + mRadioButton.setChecked(true); } private boolean shouldDisplayDataAlert() { @@ -144,7 +144,7 @@ public class TtsEnginePreference extends Preference { public void onClick(DialogInterface dialog, int which) { makeCurrentEngine(buttonView); } - },new DialogInterface.OnClickListener() { + }, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // Undo the click. @@ -175,9 +175,11 @@ public class TtsEnginePreference extends Preference { */ public interface RadioButtonGroupState { String getCurrentKey(); + Checkable getCurrentChecked(); void setCurrentKey(String key); + void setCurrentChecked(Checkable current); } diff --git a/src/com/android/settings/tts/TtsEnginePreferenceFragment.java b/src/com/android/settings/tts/TtsEnginePreferenceFragment.java index 2b35a59692..244e5b154c 100644 --- a/src/com/android/settings/tts/TtsEnginePreferenceFragment.java +++ b/src/com/android/settings/tts/TtsEnginePreferenceFragment.java @@ -1,28 +1,32 @@ package com.android.settings.tts; -import android.speech.tts.TextToSpeech; -import com.android.settings.R; +import static android.provider.Settings.Secure.TTS_DEFAULT_SYNTH; + +import android.app.settings.SettingsEnums; +import android.content.Context; import android.os.Bundle; -import com.android.settings.SettingsPreferenceFragment; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import androidx.preference.PreferenceCategory; -import android.speech.tts.TtsEngines; +import android.provider.SearchIndexableResource; +import android.speech.tts.TextToSpeech; import android.speech.tts.TextToSpeech.EngineInfo; -import com.android.settings.SettingsActivity; -import com.android.settings.tts.TtsEnginePreference.RadioButtonGroupState; -import android.widget.Checkable; +import android.speech.tts.TtsEngines; import android.util.Log; -import static android.provider.Settings.Secure.TTS_DEFAULT_SYNTH; -import com.android.settings.search.Indexable; +import android.widget.Checkable; + +import androidx.preference.PreferenceCategory; + +import com.android.settings.R; +import com.android.settings.SettingsPreferenceFragment; import com.android.settings.search.BaseSearchIndexProvider; -import android.content.Context; -import android.provider.SearchIndexableResource; +import com.android.settings.search.Indexable; +import com.android.settings.tts.TtsEnginePreference.RadioButtonGroupState; +import com.android.settingslib.search.SearchIndexable; -import java.util.List; import java.util.Arrays; +import java.util.List; -public class TtsEnginePreferenceFragment extends SettingsPreferenceFragment //implements - implements RadioButtonGroupState, Indexable { +@SearchIndexable +public class TtsEnginePreferenceFragment extends SettingsPreferenceFragment + implements RadioButtonGroupState { private static final String TAG = "TtsEnginePrefFragment"; private static final int VOICE_DATA_INTEGRITY_CHECK = 1977; @@ -63,7 +67,7 @@ public class TtsEnginePreferenceFragment extends SettingsPreferenceFragment //im @Override public int getMetricsCategory() { - return MetricsEvent.TTS_ENGINE_SETTINGS; + return SettingsEnums.TTS_ENGINE_SETTINGS; } @Override @@ -82,12 +86,10 @@ public class TtsEnginePreferenceFragment extends SettingsPreferenceFragment //im mEnginePreferenceCategory.removeAll(); - SettingsActivity activity = (SettingsActivity) getActivity(); - List<EngineInfo> engines = mEnginesHelper.getEngines(); for (EngineInfo engine : engines) { TtsEnginePreference enginePref = - new TtsEnginePreference(getPrefContext(), engine, this, activity); + new TtsEnginePreference(getPrefContext(), engine, this); mEnginePreferenceCategory.addPreference(enginePref); } } @@ -156,6 +158,7 @@ public class TtsEnginePreferenceFragment extends SettingsPreferenceFragment //im public void onUpdateEngine(int status) { if (status == TextToSpeech.SUCCESS) { Log.d( + TAG, "Updating engine: Successfully bound to the engine: " + mTts.getCurrentEngine()); diff --git a/src/com/android/settings/users/AddUserWhenLockedPreferenceController.java b/src/com/android/settings/users/AddUserWhenLockedPreferenceController.java index bebc2d73b0..f931fa4f66 100644 --- a/src/com/android/settings/users/AddUserWhenLockedPreferenceController.java +++ b/src/com/android/settings/users/AddUserWhenLockedPreferenceController.java @@ -16,72 +16,57 @@ package com.android.settings.users; import android.content.Context; -import android.provider.Settings.Global; +import android.provider.Settings; + import androidx.preference.Preference; -import com.android.settings.core.PreferenceControllerMixin; +import com.android.settings.core.TogglePreferenceController; import com.android.settingslib.RestrictedSwitchPreference; -import com.android.settingslib.core.AbstractPreferenceController; -import com.android.settingslib.core.lifecycle.Lifecycle; -import com.android.settingslib.core.lifecycle.LifecycleObserver; -import com.android.settingslib.core.lifecycle.events.OnPause; -import com.android.settingslib.core.lifecycle.events.OnResume; -public class AddUserWhenLockedPreferenceController extends AbstractPreferenceController - implements PreferenceControllerMixin, Preference.OnPreferenceChangeListener, - LifecycleObserver, OnPause, OnResume { +public class AddUserWhenLockedPreferenceController extends TogglePreferenceController { - private final String mPrefKey; private final UserCapabilities mUserCaps; - private boolean mShouldUpdateUserList; - public AddUserWhenLockedPreferenceController(Context context, String key, Lifecycle lifecycle) { - super(context); - mPrefKey = key; + public AddUserWhenLockedPreferenceController(Context context, String key) { + super(context, key); mUserCaps = UserCapabilities.create(context); - if (lifecycle != null) { - lifecycle.addObserver(this); - } } @Override public void updateState(Preference preference) { - RestrictedSwitchPreference restrictedSwitchPreference = + super.updateState(preference); + mUserCaps.updateAddUserCapabilities(mContext); + final RestrictedSwitchPreference restrictedSwitchPreference = (RestrictedSwitchPreference) preference; - int value = Global.getInt(mContext.getContentResolver(), Global.ADD_USERS_WHEN_LOCKED, 0); - restrictedSwitchPreference.setChecked(value == 1); - restrictedSwitchPreference.setDisabledByAdmin( - mUserCaps.disallowAddUser() ? mUserCaps.getEnforcedAdmin() : null); - } - - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - Boolean value = (Boolean) newValue; - Global.putInt(mContext.getContentResolver(), - Global.ADD_USERS_WHEN_LOCKED, value != null && value ? 1 : 0); - return true; - } - - @Override - public void onPause() { - mShouldUpdateUserList = true; + if (!isAvailable()) { + restrictedSwitchPreference.setVisible(false); + } else { + restrictedSwitchPreference.setDisabledByAdmin( + mUserCaps.disallowAddUser() ? mUserCaps.getEnforcedAdmin() : null); + restrictedSwitchPreference.setVisible(mUserCaps.mUserSwitcherEnabled); + } } @Override - public void onResume() { - if (mShouldUpdateUserList) { - mUserCaps.updateAddUserCapabilities(mContext); + public int getAvailabilityStatus() { + if (!mUserCaps.isAdmin()) { + return DISABLED_FOR_USER; + } else if (mUserCaps.disallowAddUser() || mUserCaps.disallowAddUserSetByAdmin()) { + return DISABLED_FOR_USER; + } else { + return mUserCaps.mUserSwitcherEnabled ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; } } @Override - public boolean isAvailable() { - return mUserCaps.isAdmin() && - (!mUserCaps.disallowAddUser() || mUserCaps.disallowAddUserSetByAdmin()); + public boolean isChecked() { + return Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.ADD_USERS_WHEN_LOCKED, 0) == 1; } @Override - public String getPreferenceKey() { - return mPrefKey; + public boolean setChecked(boolean isChecked) { + return Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.ADD_USERS_WHEN_LOCKED, isChecked ? 1 : 0); } } diff --git a/src/com/android/settings/users/AppRestrictionsFragment.java b/src/com/android/settings/users/AppRestrictionsFragment.java index db13242671..7b15e8a090 100644 --- a/src/com/android/settings/users/AppRestrictionsFragment.java +++ b/src/com/android/settings/users/AppRestrictionsFragment.java @@ -17,6 +17,7 @@ package com.android.settings.users; import android.app.Activity; +import android.app.settings.SettingsEnums; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -36,14 +37,6 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; import android.os.UserManager; -import androidx.preference.MultiSelectListPreference; -import androidx.preference.SwitchPreference; -import androidx.preference.ListPreference; -import androidx.preference.Preference; -import androidx.preference.Preference.OnPreferenceChangeListener; -import androidx.preference.Preference.OnPreferenceClickListener; -import androidx.preference.PreferenceGroup; -import androidx.preference.PreferenceViewHolder; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; @@ -52,7 +45,15 @@ import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.Switch; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.preference.ListPreference; +import androidx.preference.MultiSelectListPreference; +import androidx.preference.Preference; +import androidx.preference.Preference.OnPreferenceChangeListener; +import androidx.preference.Preference.OnPreferenceClickListener; +import androidx.preference.PreferenceGroup; +import androidx.preference.PreferenceViewHolder; +import androidx.preference.SwitchPreference; + import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; import com.android.settings.Utils; @@ -243,7 +244,7 @@ public class AppRestrictionsFragment extends SettingsPreferenceFragment implemen @Override public int getMetricsCategory() { - return MetricsEvent.USERS_APP_RESTRICTIONS; + return SettingsEnums.USERS_APP_RESTRICTIONS; } @Override @@ -449,7 +450,7 @@ public class AppRestrictionsFragment extends SettingsPreferenceFragment implemen private void addLocationAppRestrictionsPreference(AppRestrictionsHelper.SelectableAppInfo app, AppRestrictionsPreference p) { String packageName = app.packageName; - p.setIcon(R.drawable.ic_settings_location); + p.setIcon(R.drawable.ic_preference_location); p.setKey(getKeyForPackage(packageName)); ArrayList<RestrictionEntry> restrictions = RestrictionUtils.getRestrictions( getActivity(), mUser); diff --git a/src/com/android/settings/users/AutoSyncDataPreferenceController.java b/src/com/android/settings/users/AutoSyncDataPreferenceController.java index 42de892146..e7c5cb699a 100644 --- a/src/com/android/settings/users/AutoSyncDataPreferenceController.java +++ b/src/com/android/settings/users/AutoSyncDataPreferenceController.java @@ -16,9 +16,8 @@ package com.android.settings.users; import android.app.ActivityManager; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.Fragment; +import android.app.settings.SettingsEnums; import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; @@ -26,11 +25,13 @@ import android.os.Bundle; import android.os.Process; import android.os.UserHandle; import android.os.UserManager; -import androidx.preference.SwitchPreference; -import androidx.preference.Preference; import android.util.Log; -import com.android.internal.logging.nano.MetricsProto; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; +import androidx.preference.Preference; +import androidx.preference.SwitchPreference; + import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; @@ -146,7 +147,7 @@ public class AutoSyncDataPreferenceController extends AbstractPreferenceControll @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DIALOG_CONFIRM_AUTO_SYNC_CHANGE; + return SettingsEnums.DIALOG_CONFIRM_AUTO_SYNC_CHANGE; } @Override diff --git a/src/com/android/settings/users/AutoSyncPersonalDataPreferenceController.java b/src/com/android/settings/users/AutoSyncPersonalDataPreferenceController.java index 79099adc2b..2530977a3d 100644 --- a/src/com/android/settings/users/AutoSyncPersonalDataPreferenceController.java +++ b/src/com/android/settings/users/AutoSyncPersonalDataPreferenceController.java @@ -15,10 +15,11 @@ */ package com.android.settings.users; -import android.app.Fragment; import android.content.Context; import android.os.UserHandle; +import androidx.fragment.app.Fragment; + public class AutoSyncPersonalDataPreferenceController extends AutoSyncDataPreferenceController { private static final String TAG = "AutoSyncPersonalData"; diff --git a/src/com/android/settings/users/AutoSyncWorkDataPreferenceController.java b/src/com/android/settings/users/AutoSyncWorkDataPreferenceController.java index 5e8ad2cf9d..1e6284509a 100644 --- a/src/com/android/settings/users/AutoSyncWorkDataPreferenceController.java +++ b/src/com/android/settings/users/AutoSyncWorkDataPreferenceController.java @@ -15,10 +15,11 @@ */ package com.android.settings.users; -import android.app.Fragment; import android.content.Context; import android.os.UserHandle; +import androidx.fragment.app.Fragment; + import com.android.settings.Utils; public class AutoSyncWorkDataPreferenceController extends AutoSyncPersonalDataPreferenceController { diff --git a/src/com/android/settings/users/EditUserInfoController.java b/src/com/android/settings/users/EditUserInfoController.java index 315ebcb4ad..4d9244aeba 100644 --- a/src/com/android/settings/users/EditUserInfoController.java +++ b/src/com/android/settings/users/EditUserInfoController.java @@ -17,9 +17,7 @@ package com.android.settings.users; import android.app.Activity; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.Fragment; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.UserInfo; @@ -36,6 +34,10 @@ import android.view.WindowManager; import android.widget.EditText; import android.widget.ImageView; +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; + import com.android.settings.R; import com.android.settingslib.Utils; import com.android.settingslib.drawable.CircleFramedDrawable; @@ -102,9 +104,8 @@ public class EditUserInfoController { public void onActivityResult(int requestCode, int resultCode, Intent data) { mWaitingForActivityResult = false; - if (mEditUserInfoDialog != null && mEditUserInfoDialog.isShowing() - && mEditUserPhotoController.onActivityResult(requestCode, resultCode, data)) { - return; + if (mEditUserInfoDialog != null) { + mEditUserPhotoController.onActivityResult(requestCode, resultCode, data); } } @@ -114,7 +115,7 @@ public class EditUserInfoController { Activity activity = fragment.getActivity(); mUser = user; if (mUserManager == null) { - mUserManager = UserManager.get(activity); + mUserManager = activity.getSystemService(UserManager.class); } LayoutInflater inflater = activity.getLayoutInflater(); View content = inflater.inflate(R.layout.edit_user_info_dialog_content, null); @@ -135,8 +136,7 @@ public class EditUserInfoController { } } userPhotoView.setImageDrawable(drawable); - mEditUserPhotoController = new EditUserPhotoController(fragment, userPhotoView, - mSavedPhoto, drawable, mWaitingForActivityResult); + mEditUserPhotoController = createEditUserPhotoController(fragment, userPhotoView, drawable); mEditUserInfoDialog = new AlertDialog.Builder(activity) .setTitle(R.string.profile_info_settings_title) .setView(content) @@ -194,4 +194,11 @@ public class EditUserInfoController { return mEditUserInfoDialog; } + + @VisibleForTesting + EditUserPhotoController createEditUserPhotoController(Fragment fragment, + ImageView userPhotoView, Drawable drawable) { + return new EditUserPhotoController(fragment, userPhotoView, + mSavedPhoto, drawable, mWaitingForActivityResult); + } } diff --git a/src/com/android/settings/users/EditUserPhotoController.java b/src/com/android/settings/users/EditUserPhotoController.java index 9368b81353..3253f79d9d 100644 --- a/src/com/android/settings/users/EditUserPhotoController.java +++ b/src/com/android/settings/users/EditUserPhotoController.java @@ -17,7 +17,6 @@ package com.android.settings.users; import android.app.Activity; -import android.app.Fragment; import android.content.ClipData; import android.content.ContentResolver; import android.content.Context; @@ -38,7 +37,6 @@ import android.os.UserHandle; import android.os.UserManager; import android.provider.ContactsContract.DisplayPhoto; import android.provider.MediaStore; -import androidx.core.content.FileProvider; import android.util.Log; import android.view.Gravity; import android.view.View; @@ -50,8 +48,12 @@ import android.widget.ImageView; import android.widget.ListPopupWindow; import android.widget.TextView; +import androidx.core.content.FileProvider; +import androidx.fragment.app.Fragment; + import com.android.settings.R; import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.drawable.CircleFramedDrawable; import libcore.io.Streams; @@ -418,9 +420,9 @@ public class EditUserPhotoController { mAction = action; final int myUserId = UserHandle.myUserId(); - mAdmin = RestrictedLockUtils.checkIfRestrictionEnforced(context, + mAdmin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(context, restriction, myUserId); - mIsRestrictedByBase = RestrictedLockUtils.hasBaseUserRestriction(mContext, + mIsRestrictedByBase = RestrictedLockUtilsInternal.hasBaseUserRestriction(mContext, restriction, myUserId); } diff --git a/src/com/android/settings/users/MultiUserFooterPreferenceController.java b/src/com/android/settings/users/MultiUserFooterPreferenceController.java new file mode 100644 index 0000000000..1573bbd08d --- /dev/null +++ b/src/com/android/settings/users/MultiUserFooterPreferenceController.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2018 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.users; + +import android.content.Context; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; +import com.android.settingslib.widget.FooterPreference; +import com.android.settingslib.widget.FooterPreferenceMixinCompat; + +public class MultiUserFooterPreferenceController extends BasePreferenceController { + + @VisibleForTesting + final UserCapabilities mUserCaps; + + private FooterPreferenceMixinCompat mFooterMixin; + + public MultiUserFooterPreferenceController(Context context) { + super(context, "dummy_key"); + mUserCaps = UserCapabilities.create(context); + } + + public MultiUserFooterPreferenceController setFooterMixin( + FooterPreferenceMixinCompat footerMixin) { + mFooterMixin = footerMixin; + return this; + } + + @Override + public int getAvailabilityStatus() { + return (mUserCaps.mEnabled && !mUserCaps.mUserSwitcherEnabled) + ? AVAILABLE_UNSEARCHABLE + : DISABLED_FOR_USER; + } + + @Override + public void updateState(Preference preference) { + mUserCaps.updateAddUserCapabilities(mContext); + final FooterPreference pref = mFooterMixin.createFooterPreference(); + pref.setTitle(R.string.user_settings_footer_text); + pref.setVisible(isAvailable()); + } +} diff --git a/src/com/android/settings/users/MultiUserSwitchBarController.java b/src/com/android/settings/users/MultiUserSwitchBarController.java new file mode 100644 index 0000000000..a5fdf9b327 --- /dev/null +++ b/src/com/android/settings/users/MultiUserSwitchBarController.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2018 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.users; + +import android.content.Context; +import android.os.UserHandle; +import android.os.UserManager; +import android.provider.Settings; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; + +import com.android.settings.widget.SwitchWidgetController; +import com.android.settingslib.RestrictedLockUtilsInternal; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.core.lifecycle.events.OnStop; + +public class MultiUserSwitchBarController implements SwitchWidgetController.OnSwitchChangeListener, + LifecycleObserver, OnStart, OnStop { + private static final String TAG = "MultiUserSwitchBarCtrl"; + + interface OnMultiUserSwitchChangedListener { + void onMultiUserSwitchChanged(boolean newState); + } + @VisibleForTesting + final SwitchWidgetController mSwitchBar; + + private final Context mContext; + private final UserCapabilities mUserCapabilities; + private final OnMultiUserSwitchChangedListener mListener; + + + MultiUserSwitchBarController(Context context, SwitchWidgetController switchBar, + OnMultiUserSwitchChangedListener listener) { + mContext = context; + mSwitchBar = switchBar; + mListener = listener; + mUserCapabilities = UserCapabilities.create(context); + mSwitchBar.setChecked(mUserCapabilities.mUserSwitcherEnabled); + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.USER_SWITCHER_ENABLED, mSwitchBar.isChecked() ? 1 : 0); + + if (mUserCapabilities.mDisallowSwitchUser) { + mSwitchBar.setDisabledByAdmin(RestrictedLockUtilsInternal + .checkIfRestrictionEnforced(mContext, UserManager.DISALLOW_USER_SWITCH, + UserHandle.myUserId())); + } else { + mSwitchBar.setEnabled(!mUserCapabilities.mDisallowSwitchUser + && !mUserCapabilities.mIsGuest && mUserCapabilities.isAdmin()); + } + mSwitchBar.setListener(this); + } + + @Override + public void onStart() { + mSwitchBar.startListening(); + } + + @Override + public void onStop() { + mSwitchBar.stopListening(); + } + + @Override + public boolean onSwitchToggled(boolean isChecked) { + Log.d(TAG, "Toggling multi-user feature enabled state to: " + isChecked); + final boolean success = Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.USER_SWITCHER_ENABLED, isChecked ? 1 : 0); + if (success && mListener != null) { + mListener.onMultiUserSwitchChanged(isChecked); + } + return success; + } +} diff --git a/src/com/android/settings/users/OwnerInfoSettings.java b/src/com/android/settings/users/OwnerInfoSettings.java index 582431f343..d06ba64df8 100644 --- a/src/com/android/settings/users/OwnerInfoSettings.java +++ b/src/com/android/settings/users/OwnerInfoSettings.java @@ -16,9 +16,8 @@ package com.android.settings.users; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.Fragment; +import android.app.settings.SettingsEnums; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.os.Bundle; @@ -28,7 +27,9 @@ import android.view.LayoutInflater; import android.view.View; import android.widget.EditText; -import com.android.internal.logging.nano.MetricsProto; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; + import com.android.internal.widget.LockPatternUtils; import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; @@ -95,6 +96,6 @@ public class OwnerInfoSettings extends InstrumentedDialogFragment implements OnC @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DIALOG_OWNER_INFO_SETTINGS; + return SettingsEnums.DIALOG_OWNER_INFO_SETTINGS; } } diff --git a/src/com/android/settings/users/RestrictedProfileSettings.java b/src/com/android/settings/users/RestrictedProfileSettings.java index 99ae26df44..b00ee83f61 100644 --- a/src/com/android/settings/users/RestrictedProfileSettings.java +++ b/src/com/android/settings/users/RestrictedProfileSettings.java @@ -17,6 +17,7 @@ package com.android.settings.users; import android.app.Dialog; +import android.app.settings.SettingsEnums; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.UserInfo; @@ -26,7 +27,6 @@ import android.view.View; import android.widget.ImageView; import android.widget.TextView; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.Utils; @@ -138,9 +138,9 @@ public class RestrictedProfileSettings extends AppRestrictionsFragment public int getDialogMetricsCategory(int dialogId) { switch (dialogId) { case DIALOG_ID_EDIT_USER_INFO: - return MetricsProto.MetricsEvent.DIALOG_USER_EDIT; + return SettingsEnums.DIALOG_USER_EDIT; case DIALOG_CONFIRM_REMOVE: - return MetricsProto.MetricsEvent.DIALOG_USER_REMOVE; + return SettingsEnums.DIALOG_USER_REMOVE; default: return 0; } diff --git a/src/com/android/settings/users/RestrictionUtils.java b/src/com/android/settings/users/RestrictionUtils.java index a5cecde4b6..4d4a2dd5ae 100644 --- a/src/com/android/settings/users/RestrictionUtils.java +++ b/src/com/android/settings/users/RestrictionUtils.java @@ -22,7 +22,6 @@ import android.content.res.Resources; import android.os.Bundle; import android.os.UserHandle; import android.os.UserManager; -import android.provider.Settings.Secure; import com.android.settings.R; diff --git a/src/com/android/settings/users/UserCapabilities.java b/src/com/android/settings/users/UserCapabilities.java index f1bfae90c9..459a880964 100644 --- a/src/com/android/settings/users/UserCapabilities.java +++ b/src/com/android/settings/users/UserCapabilities.java @@ -22,8 +22,10 @@ import android.content.pm.UserInfo; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; + import com.android.settings.Utils; import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; public class UserCapabilities { boolean mEnabled = true; @@ -31,13 +33,15 @@ public class UserCapabilities { boolean mCanAddRestrictedProfile = true; boolean mIsAdmin; boolean mIsGuest; + boolean mUserSwitcherEnabled; boolean mCanAddGuest; boolean mDisallowAddUser; boolean mDisallowAddUserSetByAdmin; boolean mDisallowSwitchUser; RestrictedLockUtils.EnforcedAdmin mEnforcedAdmin; - private UserCapabilities() {} + private UserCapabilities() { + } public static UserCapabilities create(Context context) { UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE); @@ -62,14 +66,15 @@ public class UserCapabilities { } public void updateAddUserCapabilities(Context context) { - mEnforcedAdmin = RestrictedLockUtils.checkIfRestrictionEnforced(context, + final UserManager userManager = + (UserManager) context.getSystemService(Context.USER_SERVICE); + mEnforcedAdmin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(context, UserManager.DISALLOW_ADD_USER, UserHandle.myUserId()); - final boolean hasBaseUserRestriction = RestrictedLockUtils.hasBaseUserRestriction( + final boolean hasBaseUserRestriction = RestrictedLockUtilsInternal.hasBaseUserRestriction( context, UserManager.DISALLOW_ADD_USER, UserHandle.myUserId()); - mDisallowAddUserSetByAdmin = - mEnforcedAdmin != null && !hasBaseUserRestriction; - mDisallowAddUser = - (mEnforcedAdmin != null || hasBaseUserRestriction); + mDisallowAddUserSetByAdmin = mEnforcedAdmin != null && !hasBaseUserRestriction; + mDisallowAddUser = (mEnforcedAdmin != null || hasBaseUserRestriction); + mUserSwitcherEnabled = userManager.isUserSwitcherEnabled(); mCanAddUser = true; if (!mIsAdmin || UserManager.getMaxSupportedUsers() < 2 || !UserManager.supportsMultipleUsers() @@ -81,7 +86,6 @@ public class UserCapabilities { context.getContentResolver(), Settings.Global.ADD_USERS_WHEN_LOCKED, 0) == 1; mCanAddGuest = !mIsGuest && !mDisallowAddUser && canAddUsersWhenLocked; - UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE); mDisallowSwitchUser = userManager.hasUserRestriction(UserManager.DISALLOW_USER_SWITCH); } @@ -114,6 +118,8 @@ public class UserCapabilities { ", mDisallowAddUser=" + mDisallowAddUser + ", mEnforcedAdmin=" + mEnforcedAdmin + ", mDisallowSwitchUser=" + mDisallowSwitchUser + + ", mDisallowAddUserSetByAdmin=" + mDisallowAddUserSetByAdmin + + ", mUserSwitcherEnabled=" + mUserSwitcherEnabled + '}'; } } diff --git a/src/com/android/settings/users/UserDetailsSettings.java b/src/com/android/settings/users/UserDetailsSettings.java index d18e2ea5bf..371c152e15 100644 --- a/src/com/android/settings/users/UserDetailsSettings.java +++ b/src/com/android/settings/users/UserDetailsSettings.java @@ -17,20 +17,20 @@ package com.android.settings.users; import android.app.Dialog; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.DialogInterface; import android.content.pm.UserInfo; import android.os.Bundle; import android.os.UserHandle; import android.os.UserManager; -import androidx.preference.SwitchPreference; + import androidx.preference.Preference; +import androidx.preference.SwitchPreference; -import com.android.internal.logging.nano.MetricsProto; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; -import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; import java.util.List; @@ -69,7 +69,7 @@ public class UserDetailsSettings extends SettingsPreferenceFragment @Override public int getMetricsCategory() { - return MetricsEvent.USER_DETAILS; + return SettingsEnums.USER_DETAILS; } @Override @@ -104,7 +104,7 @@ public class UserDetailsSettings extends SettingsPreferenceFragment mPhonePref.setChecked( !mDefaultGuestRestrictions.getBoolean(UserManager.DISALLOW_OUTGOING_CALLS)); } - if (RestrictedLockUtils.hasBaseUserRestriction(context, + if (RestrictedLockUtilsInternal.hasBaseUserRestriction(context, UserManager.DISALLOW_REMOVE_USER, UserHandle.myUserId())) { removePreference(KEY_REMOVE_USER); } @@ -197,11 +197,11 @@ public class UserDetailsSettings extends SettingsPreferenceFragment public int getDialogMetricsCategory(int dialogId) { switch (dialogId) { case DIALOG_CONFIRM_REMOVE: - return MetricsProto.MetricsEvent.DIALOG_USER_REMOVE; + return SettingsEnums.DIALOG_USER_REMOVE; case DIALOG_CONFIRM_ENABLE_CALLING: - return MetricsProto.MetricsEvent.DIALOG_USER_ENABLE_CALLING; + return SettingsEnums.DIALOG_USER_ENABLE_CALLING; case DIALOG_CONFIRM_ENABLE_CALLING_AND_SMS: - return MetricsProto.MetricsEvent.DIALOG_USER_ENABLE_CALLING_AND_SMS; + return SettingsEnums.DIALOG_USER_ENABLE_CALLING_AND_SMS; default: return 0; } diff --git a/src/com/android/settings/users/UserDialogs.java b/src/com/android/settings/users/UserDialogs.java index 3ed383a625..2361e449a7 100644 --- a/src/com/android/settings/users/UserDialogs.java +++ b/src/com/android/settings/users/UserDialogs.java @@ -16,7 +16,6 @@ package com.android.settings.users; -import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; @@ -31,6 +30,8 @@ import android.view.View; import android.widget.ImageView; import android.widget.TextView; +import androidx.appcompat.app.AlertDialog; + import com.android.settings.R; import com.android.settings.Utils; diff --git a/src/com/android/settings/users/UserPreference.java b/src/com/android/settings/users/UserPreference.java index 391e13a706..3603d44ea0 100644 --- a/src/com/android/settings/users/UserPreference.java +++ b/src/com/android/settings/users/UserPreference.java @@ -20,14 +20,15 @@ import android.content.Context; import android.graphics.drawable.Drawable; import android.os.UserHandle; import android.os.UserManager; -import androidx.preference.PreferenceViewHolder; import android.util.AttributeSet; import android.view.View; import android.view.View.OnClickListener; import android.widget.ImageView; +import androidx.preference.PreferenceViewHolder; + import com.android.settings.R; -import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.RestrictedPreference; import java.util.Comparator; @@ -39,18 +40,22 @@ public class UserPreference extends RestrictedPreference { public static final int USERID_UNKNOWN = -10; public static final int USERID_GUEST_DEFAULTS = -11; public static final Comparator<UserPreference> SERIAL_NUMBER_COMPARATOR = - new Comparator<UserPreference>() { - @Override - public int compare(UserPreference p1, UserPreference p2) { - int sn1 = p1.getSerialNumber(); - int sn2 = p2.getSerialNumber(); - if (sn1 < sn2) { - return -1; - } else if (sn1 > sn2) { - return 1; - } - return 0; + (p1, p2) -> { + + if (p1 == null) { + return -1; + } + else if (p2 == null) { + return 1; + } + int sn1 = p1.getSerialNumber(); + int sn2 = p2.getSerialNumber(); + if (sn1 < sn2) { + return -1; + } else if (sn1 > sn2) { + return 1; } + return 0; }; private OnClickListener mDeleteClickListener; @@ -141,7 +146,7 @@ public class UserPreference extends RestrictedPreference { private boolean canDeleteUser() { return mDeleteClickListener != null - && !RestrictedLockUtils.hasBaseUserRestriction(getContext(), + && !RestrictedLockUtilsInternal.hasBaseUserRestriction(getContext(), UserManager.DISALLOW_REMOVE_USER, UserHandle.myUserId()); } diff --git a/src/com/android/settings/users/UserSettings.java b/src/com/android/settings/users/UserSettings.java index 5e88895d3e..7903b47290 100644 --- a/src/com/android/settings/users/UserSettings.java +++ b/src/com/android/settings/users/UserSettings.java @@ -18,9 +18,9 @@ package com.android.settings.users; import android.app.Activity; import android.app.ActivityManager; -import android.app.AlertDialog; import android.app.Dialog; import android.app.admin.DevicePolicyManager; +import android.app.settings.SettingsEnums; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; @@ -42,26 +42,25 @@ import android.os.UserHandle; import android.os.UserManager; import android.provider.ContactsContract; import android.provider.SearchIndexableResource; -import android.provider.Settings.Global; -import androidx.annotation.VisibleForTesting; -import androidx.annotation.WorkerThread; -import androidx.preference.Preference; -import androidx.preference.Preference.OnPreferenceClickListener; -import androidx.preference.PreferenceGroup; -import androidx.preference.PreferenceScreen; import android.util.Log; import android.util.SparseArray; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; -import android.view.View.OnClickListener; import android.widget.SimpleAdapter; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.annotation.VisibleForTesting; +import androidx.annotation.WorkerThread; +import androidx.appcompat.app.AlertDialog; +import androidx.preference.Preference; +import androidx.preference.PreferenceGroup; +import androidx.preference.PreferenceScreen; + import com.android.internal.util.UserIcons; import com.android.internal.widget.LockPatternUtils; import com.android.settings.R; +import com.android.settings.SettingsActivity; import com.android.settings.SettingsPreferenceFragment; import com.android.settings.Utils; import com.android.settings.core.SubSettingLauncher; @@ -69,10 +68,14 @@ import com.android.settings.dashboard.SummaryLoader; import com.android.settings.password.ChooseLockGeneric; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; +import com.android.settings.widget.SwitchBar; +import com.android.settings.widget.SwitchBarController; import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; +import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.RestrictedPreference; import com.android.settingslib.drawable.CircleFramedDrawable; +import com.android.settingslib.search.SearchIndexable; import java.io.IOException; import java.io.InputStream; @@ -89,9 +92,12 @@ import java.util.List; * The first one is always the current user. * Owner is the primary user. */ +@SearchIndexable public class UserSettings extends SettingsPreferenceFragment - implements OnPreferenceClickListener, OnClickListener, DialogInterface.OnDismissListener, - EditUserInfoController.OnContentChangedCallback, Indexable { + implements Preference.OnPreferenceClickListener, View.OnClickListener, + MultiUserSwitchBarController.OnMultiUserSwitchChangedListener, + DialogInterface.OnDismissListener, + EditUserInfoController.OnContentChangedCallback { private static final String TAG = "UserSettings"; @@ -102,11 +108,14 @@ public class UserSettings extends SettingsPreferenceFragment private static final String KEY_USER_LIST = "user_list"; private static final String KEY_USER_ME = "user_me"; + private static final String KEY_USER_GUEST = "user_guest"; private static final String KEY_ADD_USER = "user_add"; private static final String KEY_ADD_USER_WHEN_LOCKED = "user_settings_add_users_when_locked"; private static final int MENU_REMOVE_USER = Menu.FIRST; + private static final IntentFilter USER_REMOVED_INTENT_FILTER; + private static final int DIALOG_CONFIRM_REMOVE = 1; private static final int DIALOG_ADD_USER = 2; private static final int DIALOG_SETUP_USER = 3; @@ -132,9 +141,17 @@ public class UserSettings extends SettingsPreferenceFragment private static final String KEY_TITLE = "title"; private static final String KEY_SUMMARY = "summary"; - private PreferenceGroup mUserListCategory; - private UserPreference mMePreference; - private RestrictedPreference mAddUser; + static { + USER_REMOVED_INTENT_FILTER = new IntentFilter(Intent.ACTION_USER_REMOVED); + USER_REMOVED_INTENT_FILTER.addAction(Intent.ACTION_USER_INFO_CHANGED); + } + + @VisibleForTesting + PreferenceGroup mUserListCategory; + @VisibleForTesting + UserPreference mMePreference; + @VisibleForTesting + RestrictedPreference mAddUser; private int mRemovingUserId = -1; private int mAddedUserId = 0; private boolean mAddingUser; @@ -146,8 +163,10 @@ public class UserSettings extends SettingsPreferenceFragment private SparseArray<Bitmap> mUserIcons = new SparseArray<>(); private static SparseArray<Bitmap> sDarkDefaultUserBitmapCache = new SparseArray<>(); + private MultiUserSwitchBarController mSwitchBarController; private EditUserInfoController mEditUserInfoController = new EditUserInfoController(); private AddUserWhenLockedPreferenceController mAddUserWhenLockedPreferenceController; + private MultiUserFooterPreferenceController mMultiUserFooterPreferenceController; // A place to cache the generated default avatar private Drawable mDefaultIconDrawable; @@ -186,22 +205,40 @@ public class UserSettings extends SettingsPreferenceFragment @Override public int getMetricsCategory() { - return MetricsEvent.USER; + return SettingsEnums.USER; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + // Assume we are in a SettingsActivity. This is only safe because we currently use + // SettingsActivity as base for all preference fragments. + final SettingsActivity activity = (SettingsActivity) getActivity(); + final SwitchBar switchBar = activity.getSwitchBar(); + mSwitchBarController = new MultiUserSwitchBarController(activity, + new SwitchBarController(switchBar), this /* listener */); + getSettingsLifecycle().addObserver(mSwitchBarController); + switchBar.show(); } @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); addPreferencesFromResource(R.xml.user_settings); - if (Global.getInt(getContext().getContentResolver(), Global.DEVICE_PROVISIONED, 0) == 0) { - getActivity().finish(); + final Activity activity = getActivity(); + if (!Utils.isDeviceProvisioned(activity)) { + activity.finish(); return; } - final Context context = getActivity(); + mAddUserWhenLockedPreferenceController = new AddUserWhenLockedPreferenceController( - context, KEY_ADD_USER_WHEN_LOCKED, getLifecycle()); + activity, KEY_ADD_USER_WHEN_LOCKED); + mMultiUserFooterPreferenceController = new MultiUserFooterPreferenceController(activity) + .setFooterMixin(mFooterPreferenceMixin); + final PreferenceScreen screen = getPreferenceScreen(); mAddUserWhenLockedPreferenceController.displayPreference(screen); + mMultiUserFooterPreferenceController.displayPreference(screen); screen.findPreference(mAddUserWhenLockedPreferenceController.getPreferenceKey()) .setOnPreferenceChangeListener(mAddUserWhenLockedPreferenceController); @@ -216,8 +253,8 @@ public class UserSettings extends SettingsPreferenceFragment mEditUserInfoController.onRestoreInstanceState(icicle); } - mUserCaps = UserCapabilities.create(context); - mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE); + mUserCaps = UserCapabilities.create(activity); + mUserManager = (UserManager) activity.getSystemService(Context.USER_SERVICE); if (!mUserCaps.mEnabled) { return; } @@ -234,23 +271,16 @@ public class UserSettings extends SettingsPreferenceFragment mMePreference.setSummary(R.string.user_admin); } mAddUser = (RestrictedPreference) findPreference(KEY_ADD_USER); - mAddUser.useAdminDisabledSummary(false); - // Determine if add user/profile button should be visible - if (mUserCaps.mCanAddUser && Utils.isDeviceProvisioned(getActivity())) { - mAddUser.setVisible(true); - mAddUser.setOnPreferenceClickListener(this); - // change label to only mention user, if restricted profiles are not supported - if (!mUserCaps.mCanAddRestrictedProfile) { - mAddUser.setTitle(R.string.user_add_user_menu); - } - } else { - mAddUser.setVisible(false); + if (!mUserCaps.mCanAddRestrictedProfile) { + // Label should only mention adding a "user", not a "profile" + mAddUser.setTitle(R.string.user_add_user_menu); } - final IntentFilter filter = new IntentFilter(Intent.ACTION_USER_REMOVED); - filter.addAction(Intent.ACTION_USER_INFO_CHANGED); - context.registerReceiverAsUser(mUserChangeReceiver, UserHandle.ALL, filter, null, mHandler); - loadProfile(); - updateUserList(); + mAddUser.setOnPreferenceClickListener(this); + + activity.registerReceiverAsUser( + mUserChangeReceiver, UserHandle.ALL, USER_REMOVED_INTENT_FILTER, null, mHandler); + + updateUI(); mShouldUpdateUserList = false; } @@ -263,15 +293,11 @@ public class UserSettings extends SettingsPreferenceFragment } final PreferenceScreen screen = getPreferenceScreen(); - if (mAddUserWhenLockedPreferenceController.isAvailable()) { - mAddUserWhenLockedPreferenceController.updateState(screen.findPreference( - mAddUserWhenLockedPreferenceController.getPreferenceKey())); - } + mAddUserWhenLockedPreferenceController.updateState(screen.findPreference( + mAddUserWhenLockedPreferenceController.getPreferenceKey())); if (mShouldUpdateUserList) { - mUserCaps.updateAddUserCapabilities(getActivity()); - loadProfile(); - updateUserList(); + updateUI(); } } @@ -309,14 +335,18 @@ public class UserSettings extends SettingsPreferenceFragment @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { int pos = 0; - UserManager um = getContext().getSystemService(UserManager.class); - boolean allowRemoveUser = !um.hasUserRestriction(UserManager.DISALLOW_REMOVE_USER); - boolean canSwitchUsers = um.canSwitchUsers(); - if (!mUserCaps.mIsAdmin && allowRemoveUser && canSwitchUsers) { + final boolean canSwitchUsers = mUserManager.canSwitchUsers(); + if (!mUserCaps.mIsAdmin && canSwitchUsers) { String nickname = mUserManager.getUserName(); MenuItem removeThisUser = menu.add(0, MENU_REMOVE_USER, pos++, getResources().getString(R.string.user_remove_user_menu, nickname)); removeThisUser.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); + + final EnforcedAdmin disallowRemoveUserAdmin = + RestrictedLockUtilsInternal.checkIfRestrictionEnforced(getContext(), + UserManager.DISALLOW_REMOVE_USER, UserHandle.myUserId()); + RestrictedLockUtilsInternal.setMenuItemAsDisabledByAdmin(getContext(), removeThisUser, + disallowRemoveUserAdmin); } super.onCreateOptionsMenu(menu, inflater); } @@ -332,6 +362,17 @@ public class UserSettings extends SettingsPreferenceFragment } } + @Override + public void onMultiUserSwitchChanged(boolean newState) { + updateUI(); + } + + private void updateUI() { + mUserCaps.updateAddUserCapabilities(getActivity()); + loadProfile(); + updateUserList(); + } + /** * Loads profile information for the current user. */ @@ -449,7 +490,7 @@ public class UserSettings extends SettingsPreferenceFragment new SubSettingLauncher(getContext()) .setDestination(UserDetailsSettings.class.getName()) .setArguments(extras) - .setTitle(R.string.user_guest) + .setTitleRes(R.string.user_guest) .setSourceMetricsCategory(getMetricsCategory()) .launch(); return; @@ -462,7 +503,7 @@ public class UserSettings extends SettingsPreferenceFragment new SubSettingLauncher(getContext()) .setDestination(RestrictedProfileSettings.class.getName()) .setArguments(extras) - .setTitle(R.string.user_restrictions_title) + .setTitleRes(R.string.user_restrictions_title) .setSourceMetricsCategory(getMetricsCategory()) .launch(); } else if (info.id == UserHandle.myUserId()) { @@ -474,7 +515,7 @@ public class UserSettings extends SettingsPreferenceFragment new SubSettingLauncher(getContext()) .setDestination(UserDetailsSettings.class.getName()) .setArguments(extras) - .setTitle(info.name) + .setTitleText(info.name) .setSourceMetricsCategory(getMetricsCategory()) .launch(); } @@ -590,8 +631,8 @@ public class UserSettings extends SettingsPreferenceFragment AlertDialog.Builder builder = new AlertDialog.Builder(context); SimpleAdapter adapter = new SimpleAdapter(builder.getContext(), data, R.layout.two_line_list_item, - new String[] {KEY_TITLE, KEY_SUMMARY}, - new int[] {R.id.title, R.id.summary}); + new String[]{KEY_TITLE, KEY_SUMMARY}, + new int[]{R.id.title, R.id.summary}); builder.setTitle(R.string.user_add_user_type_title); builder.setAdapter(adapter, new DialogInterface.OnClickListener() { @@ -652,23 +693,23 @@ public class UserSettings extends SettingsPreferenceFragment public int getDialogMetricsCategory(int dialogId) { switch (dialogId) { case DIALOG_CONFIRM_REMOVE: - return MetricsEvent.DIALOG_USER_REMOVE; + return SettingsEnums.DIALOG_USER_REMOVE; case DIALOG_USER_CANNOT_MANAGE: - return MetricsEvent.DIALOG_USER_CANNOT_MANAGE; + return SettingsEnums.DIALOG_USER_CANNOT_MANAGE; case DIALOG_ADD_USER: - return MetricsEvent.DIALOG_USER_ADD; + return SettingsEnums.DIALOG_USER_ADD; case DIALOG_SETUP_USER: - return MetricsEvent.DIALOG_USER_SETUP; + return SettingsEnums.DIALOG_USER_SETUP; case DIALOG_SETUP_PROFILE: - return MetricsEvent.DIALOG_USER_SETUP_PROFILE; + return SettingsEnums.DIALOG_USER_SETUP_PROFILE; case DIALOG_CHOOSE_USER_TYPE: - return MetricsEvent.DIALOG_USER_CHOOSE_TYPE; + return SettingsEnums.DIALOG_USER_CHOOSE_TYPE; case DIALOG_NEED_LOCKSCREEN: - return MetricsEvent.DIALOG_USER_NEED_LOCKSCREEN; + return SettingsEnums.DIALOG_USER_NEED_LOCKSCREEN; case DIALOG_CONFIRM_EXIT_GUEST: - return MetricsEvent.DIALOG_USER_CONFIRM_EXIT_GUEST; + return SettingsEnums.DIALOG_USER_CONFIRM_EXIT_GUEST; case DIALOG_USER_PROFILE_EDITOR: - return MetricsEvent.DIALOG_USER_EDIT_PROFILE; + return SettingsEnums.DIALOG_USER_EDIT_PROFILE; default: return 0; } @@ -759,10 +800,13 @@ public class UserSettings extends SettingsPreferenceFragment removeThisUser(); } - private void updateUserList() { - if (getActivity() == null) return; - List<UserInfo> users = mUserManager.getUsers(true); + @VisibleForTesting + void updateUserList() { final Context context = getActivity(); + if (context == null) { + return; + } + final List<UserInfo> users = mUserManager.getUsers(true); final boolean voiceCapable = Utils.isVoiceCapable(context); final ArrayList<Integer> missingIcons = new ArrayList<>(); @@ -820,7 +864,7 @@ public class UserSettings extends SettingsPreferenceFragment // set. if (!mUserCaps.mDisallowSwitchUser) { pref.setOnPreferenceClickListener(this); - pref.setSelectable(true); + pref.setSelectable(mUserManager.canSwitchUsers()); } } else if (user.isRestricted()) { pref.setSummary(R.string.user_summary_restricted_profile); @@ -859,14 +903,18 @@ public class UserSettings extends SettingsPreferenceFragment null /* delete icon handler */); pref.setTitle(R.string.user_guest); pref.setIcon(getEncircledDefaultIcon()); + pref.setKey(KEY_USER_GUEST); userPreferences.add(pref); if (mUserCaps.mDisallowAddUser) { pref.setDisabledByAdmin(mUserCaps.mEnforcedAdmin); } else if (mUserCaps.mDisallowSwitchUser) { - pref.setDisabledByAdmin(RestrictedLockUtils.getDeviceOwner(context)); + pref.setDisabledByAdmin(RestrictedLockUtilsInternal.getDeviceOwner(context)); } else { pref.setDisabledByAdmin(null); } + if (!mUserManager.canSwitchUsers()) { + pref.setSelectable(false); + } int finalGuestId = guestId; pref.setOnPreferenceClickListener(preference -> { int id = finalGuestId; @@ -896,8 +944,6 @@ public class UserSettings extends SettingsPreferenceFragment loadIconsAsync(missingIcons); } - // Remove everything from mUserListCategory and add new users. - mUserListCategory.removeAll(); // If profiles are supported, mUserListCategory will have a special title if (mUserCaps.mCanAddRestrictedProfile) { mUserListCategory.setTitle(R.string.user_list_title); @@ -905,16 +951,35 @@ public class UserSettings extends SettingsPreferenceFragment mUserListCategory.setTitle(null); } + // Remove everything from mUserListCategory and add new users. + mUserListCategory.removeAll(); + + // If multi-user is disabled, just show footer and return. + final Preference addUserOnLockScreen = getPreferenceScreen().findPreference( + mAddUserWhenLockedPreferenceController.getPreferenceKey()); + mAddUserWhenLockedPreferenceController.updateState(addUserOnLockScreen); + mMultiUserFooterPreferenceController.updateState(null /* preference */); + mUserListCategory.setVisible(mUserCaps.mUserSwitcherEnabled); + + updateAddUser(context); + + if (!mUserCaps.mUserSwitcherEnabled) { + return; + } + for (UserPreference userPreference : userPreferences) { userPreference.setOrder(Preference.DEFAULT_ORDER); mUserListCategory.addPreference(userPreference); } - // Append Add user to the end of the list - if ((mUserCaps.mCanAddUser || mUserCaps.mDisallowAddUserSetByAdmin) && - Utils.isDeviceProvisioned(getActivity())) { - boolean moreUsers = mUserManager.canAddMoreUsers(); - mAddUser.setEnabled(moreUsers && !mAddingUser); + } + + private void updateAddUser(Context context) { + if ((mUserCaps.mCanAddUser || mUserCaps.mDisallowAddUserSetByAdmin) + && Utils.isDeviceProvisioned(context) && mUserCaps.mUserSwitcherEnabled) { + mAddUser.setVisible(true); + final boolean moreUsers = mUserManager.canAddMoreUsers(); + mAddUser.setEnabled(moreUsers && !mAddingUser && mUserManager.canSwitchUsers()); if (!moreUsers) { mAddUser.setSummary(getString(R.string.user_add_max_count, getMaxRealUsers())); } else { @@ -924,8 +989,9 @@ public class UserSettings extends SettingsPreferenceFragment mAddUser.setDisabledByAdmin( mUserCaps.mDisallowAddUser ? mUserCaps.mEnforcedAdmin : null); } + } else { + mAddUser.setVisible(false); } - } private int getMaxRealUsers() { @@ -1024,21 +1090,18 @@ public class UserSettings extends SettingsPreferenceFragment public void onClick(View v) { if (v.getTag() instanceof UserPreference) { int userId = ((UserPreference) v.getTag()).getUserId(); - switch (v.getId()) { - case UserPreference.DELETE_ID: - final EnforcedAdmin removeDisallowedAdmin = - RestrictedLockUtils.checkIfRestrictionEnforced(getContext(), - UserManager.DISALLOW_REMOVE_USER, UserHandle.myUserId()); - if (removeDisallowedAdmin != null) { - RestrictedLockUtils.sendShowAdminSupportDetailsIntent(getContext(), - removeDisallowedAdmin); - } else { - onRemoveUserClicked(userId); - } - break; - case UserPreference.SETTINGS_ID: - onManageUserClicked(userId, false); - break; + if (v.getId() == UserPreference.DELETE_ID) { + final EnforcedAdmin removeDisallowedAdmin = + RestrictedLockUtilsInternal.checkIfRestrictionEnforced(getContext(), + UserManager.DISALLOW_REMOVE_USER, UserHandle.myUserId()); + if (removeDisallowedAdmin != null) { + RestrictedLockUtils.sendShowAdminSupportDetailsIntent(getContext(), + removeDisallowedAdmin); + } else { + onRemoveUserClicked(userId); + } + } else if (v.getId() == UserPreference.SETTINGS_ID) { + onManageUserClicked(userId, false); } } } @@ -1175,10 +1238,11 @@ public class UserSettings extends SettingsPreferenceFragment } @Override - public List<String> getNonIndexableKeysFromXml(Context context, int xmlResId) { - final List<String> niks = super.getNonIndexableKeysFromXml(context, xmlResId); - new AddUserWhenLockedPreferenceController( - context, KEY_ADD_USER_WHEN_LOCKED, null /* lifecycle */) + public List<String> getNonIndexableKeysFromXml(Context context, int xmlResId, + boolean suppressAllPage) { + final List<String> niks = super.getNonIndexableKeysFromXml(context, xmlResId, + suppressAllPage); + new AddUserWhenLockedPreferenceController(context, KEY_ADD_USER_WHEN_LOCKED) .updateNonIndexableKeys(niks); new AutoSyncDataPreferenceController(context, null /* parent */) .updateNonIndexableKeys(niks); diff --git a/src/com/android/settings/utils/ContentCaptureUtils.java b/src/com/android/settings/utils/ContentCaptureUtils.java new file mode 100644 index 0000000000..a92651fbb4 --- /dev/null +++ b/src/com/android/settings/utils/ContentCaptureUtils.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2019 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.utils; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.ComponentName; +import android.content.Context; +import android.os.IBinder; +import android.os.ServiceManager; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.Log; +import android.view.contentcapture.ContentCaptureManager; + +public final class ContentCaptureUtils { + + private static final String TAG = ContentCaptureUtils.class.getSimpleName(); + private static final int MY_USER_ID = UserHandle.myUserId(); + + public static boolean isEnabledForUser(@NonNull Context context) { + boolean enabled = Settings.Secure.getIntForUser(context.getContentResolver(), + Settings.Secure.CONTENT_CAPTURE_ENABLED, 1, MY_USER_ID) == 1; + return enabled; + } + + public static void setEnabledForUser(@NonNull Context context, boolean enabled) { + Settings.Secure.putIntForUser(context.getContentResolver(), + Settings.Secure.CONTENT_CAPTURE_ENABLED, enabled ? 1 : 0, MY_USER_ID); + } + + public static boolean isFeatureAvailable() { + // We cannot look for ContentCaptureManager, because it's not available if the service + // didn't whitelist Settings + IBinder service = ServiceManager.checkService(Context.CONTENT_CAPTURE_MANAGER_SERVICE); + return service != null; + } + + @Nullable + public static ComponentName getServiceSettingsComponentName() { + try { + return ContentCaptureManager.getServiceSettingsComponentName(); + } catch (RuntimeException e) { + Log.w(TAG, "Could not get service settings: " + e); + return null; + } + } + + private ContentCaptureUtils() { + throw new UnsupportedOperationException("contains only static methods"); + } +} diff --git a/src/com/android/settings/utils/ManagedServiceSettings.java b/src/com/android/settings/utils/ManagedServiceSettings.java index 71f52f8353..3ec63c3f5b 100644 --- a/src/com/android/settings/utils/ManagedServiceSettings.java +++ b/src/com/android/settings/utils/ManagedServiceSettings.java @@ -18,10 +18,9 @@ package com.android.settings.utils; import android.annotation.Nullable; import android.app.ActivityManager; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.Fragment; import android.app.admin.DevicePolicyManager; +import android.app.settings.SettingsEnums; import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageItemInfo; @@ -30,18 +29,20 @@ import android.content.pm.ServiceInfo; import android.os.Bundle; import android.os.UserHandle; import android.os.UserManager; -import androidx.preference.SwitchPreference; -import androidx.preference.PreferenceScreen; import android.util.IconDrawableFactory; import android.util.Log; import android.view.View; -import com.android.internal.logging.nano.MetricsProto; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; +import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreference; + import com.android.settings.R; import com.android.settings.Utils; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; -import com.android.settings.notification.EmptyTextSettings; import com.android.settings.widget.AppSwitchPreference; +import com.android.settings.widget.EmptyTextSettings; import com.android.settingslib.applications.ServiceListing; import java.util.List; @@ -116,11 +117,12 @@ public abstract class ManagedServiceSettings extends EmptyTextSettings { CharSequence title = null; try { title = mPm.getApplicationInfoAsUser( - service.packageName, 0, getCurrentUser(managedProfileId)).loadLabel(mPm); + service.packageName, 0, UserHandle.myUserId()).loadLabel(mPm); } catch (PackageManager.NameNotFoundException e) { // unlikely, as we are iterating over live services. Log.e(TAG, "can't find package name", e); } + final CharSequence finalTitle = title; final String summary = service.loadLabel(mPm).toString(); final SwitchPreference pref = new AppSwitchPreference(getPrefContext()); pref.setPersistent(false); @@ -141,7 +143,11 @@ public abstract class ManagedServiceSettings extends EmptyTextSettings { } pref.setOnPreferenceChangeListener((preference, newValue) -> { final boolean enable = (boolean) newValue; - return setEnabled(cn, summary, enable); + if (finalTitle != null) { + return setEnabled(cn, finalTitle.toString(), enable); + } else { + return setEnabled(cn, null, enable); + } }); pref.setKey(cn.flattenToString()); screen.addPreference(pref); @@ -187,7 +193,7 @@ public abstract class ManagedServiceSettings extends EmptyTextSettings { @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DIALOG_SERVICE_ACCESS_WARNING; + return SettingsEnums.DIALOG_SERVICE_ACCESS_WARNING; } public ScaryWarningDialogFragment setServiceInfo(ComponentName cn, String label, @@ -234,9 +240,11 @@ public abstract class ManagedServiceSettings extends EmptyTextSettings { public final int warningDialogTitle; public final int warningDialogSummary; public final int emptyText; + public final String configIntentAction; - private Config(String tag, String setting, String intentAction, String permission, - String noun, int warningDialogTitle, int warningDialogSummary, int emptyText) { + private Config(String tag, String setting, String intentAction, String configIntentAction, + String permission, String noun, int warningDialogTitle, int warningDialogSummary, + int emptyText) { this.tag = tag; this.setting = setting; this.intentAction = intentAction; @@ -245,6 +253,7 @@ public abstract class ManagedServiceSettings extends EmptyTextSettings { this.warningDialogTitle = warningDialogTitle; this.warningDialogSummary = warningDialogSummary; this.emptyText = emptyText; + this.configIntentAction = configIntentAction; } public static class Builder{ @@ -256,6 +265,7 @@ public abstract class ManagedServiceSettings extends EmptyTextSettings { private int mWarningDialogTitle; private int mWarningDialogSummary; private int mEmptyText; + private String mConfigIntentAction; public Builder setTag(String tag) { mTag = tag; @@ -272,6 +282,11 @@ public abstract class ManagedServiceSettings extends EmptyTextSettings { return this; } + public Builder setConfigurationIntentAction(String action) { + mConfigIntentAction = action; + return this; + } + public Builder setPermission(String permission) { mPermission = permission; return this; @@ -298,8 +313,8 @@ public abstract class ManagedServiceSettings extends EmptyTextSettings { } public Config build() { - return new Config(mTag, mSetting, mIntentAction, mPermission, mNoun, - mWarningDialogTitle, mWarningDialogSummary, mEmptyText); + return new Config(mTag, mSetting, mIntentAction, mConfigIntentAction, mPermission, + mNoun, mWarningDialogTitle, mWarningDialogSummary, mEmptyText); } } } diff --git a/src/com/android/settings/utils/PreferenceGroupChildrenCache.java b/src/com/android/settings/utils/PreferenceGroupChildrenCache.java new file mode 100644 index 0000000000..dcbf4fdfe5 --- /dev/null +++ b/src/com/android/settings/utils/PreferenceGroupChildrenCache.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2018 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.utils; + +import android.text.TextUtils; +import android.util.ArrayMap; + +import androidx.preference.Preference; +import androidx.preference.PreferenceGroup; + +/** + * Class that helps track which {@link Preference}s in a {@link PreferenceGroup} are still being + * used, and remove unused ones. + */ +public class PreferenceGroupChildrenCache { + + private ArrayMap<String, Preference> mPreferenceCache; + + public void cacheRemoveAllPrefs(PreferenceGroup group) { + mPreferenceCache = new ArrayMap<>(); + final int N = group.getPreferenceCount(); + for (int i = 0; i < N; i++) { + Preference p = group.getPreference(i); + if (TextUtils.isEmpty(p.getKey())) { + continue; + } + mPreferenceCache.put(p.getKey(), p); + } + } + + public void removeCachedPrefs(PreferenceGroup group) { + for (Preference p : mPreferenceCache.values()) { + group.removePreference(p); + } + mPreferenceCache = null; + } + + public Preference getCachedPreference(String key) { + return mPreferenceCache != null ? mPreferenceCache.remove(key) : null; + } + + public int getCachedCount() { + return mPreferenceCache != null ? mPreferenceCache.size() : 0; + } +} diff --git a/src/com/android/settings/utils/SettingsDividerItemDecoration.java b/src/com/android/settings/utils/SettingsDividerItemDecoration.java index 32844d5ee7..3d9e901974 100644 --- a/src/com/android/settings/utils/SettingsDividerItemDecoration.java +++ b/src/com/android/settings/utils/SettingsDividerItemDecoration.java @@ -17,10 +17,11 @@ package com.android.settings.utils; import android.content.Context; + import androidx.preference.PreferenceViewHolder; import androidx.recyclerview.widget.RecyclerView; -import com.android.setupwizardlib.DividerItemDecoration; +import com.google.android.setupdesign.DividerItemDecoration; public class SettingsDividerItemDecoration extends DividerItemDecoration { diff --git a/src/com/android/settings/utils/ZenServiceListing.java b/src/com/android/settings/utils/ZenServiceListing.java index 40a4f34dfa..99f56f621c 100644 --- a/src/com/android/settings/utils/ZenServiceListing.java +++ b/src/com/android/settings/utils/ZenServiceListing.java @@ -18,19 +18,17 @@ package com.android.settings.utils; import android.app.ActivityManager; import android.app.NotificationManager; import android.content.ComponentName; -import android.content.ContentResolver; import android.content.Context; import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ComponentInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; -import android.provider.Settings; -import android.text.TextUtils; import android.util.ArraySet; import android.util.Slog; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Set; @@ -38,7 +36,7 @@ public class ZenServiceListing { private final Context mContext; private final ManagedServiceSettings.Config mConfig; - private final Set<ServiceInfo> mApprovedServices = new ArraySet<ServiceInfo>(); + private final Set<ComponentInfo> mApprovedComponents = new ArraySet<>(); private final List<Callback> mZenCallbacks = new ArrayList<>(); private final NotificationManager mNm; @@ -48,11 +46,14 @@ public class ZenServiceListing { mNm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); } - public ServiceInfo findService(final ComponentName cn) { - for (ServiceInfo service : mApprovedServices) { - final ComponentName serviceCN = new ComponentName(service.packageName, service.name); - if (serviceCN.equals(cn)) { - return service; + public ComponentInfo findService(final ComponentName cn) { + if (cn == null) { + return null; + } + for (ComponentInfo component : mApprovedComponents) { + final ComponentName ci = new ComponentName(component.packageName, component.name); + if (ci.equals(cn)) { + return component; } } return null; @@ -67,32 +68,29 @@ public class ZenServiceListing { } public void reloadApprovedServices() { - mApprovedServices.clear(); + mApprovedComponents.clear(); List<String> enabledNotificationListenerPkgs = mNm.getEnabledNotificationListenerPackages(); - List<ServiceInfo> services = new ArrayList<>(); - getServices(mConfig, services, mContext.getPackageManager()); - for (ServiceInfo service : services) { - final String servicePackage = service.getComponentName().getPackageName(); - if (mNm.isNotificationPolicyAccessGrantedForPackage(servicePackage) - || enabledNotificationListenerPkgs.contains(servicePackage)) { - mApprovedServices.add(service); + List<ComponentInfo> components = new ArrayList<>(); + getServices(mConfig, components, mContext.getPackageManager()); + getActivities(mConfig, components, mContext.getPackageManager()); + for (ComponentInfo componentInfo : components) { + final String pkg = componentInfo.getComponentName().getPackageName(); + if (mNm.isNotificationPolicyAccessGrantedForPackage(pkg) + || enabledNotificationListenerPkgs.contains(pkg)) { + mApprovedComponents.add(componentInfo); } } - if (!mApprovedServices.isEmpty()) { + if (!mApprovedComponents.isEmpty()) { for (Callback callback : mZenCallbacks) { - callback.onServicesReloaded(mApprovedServices); + callback.onComponentsReloaded(mApprovedComponents); } } } - private static int getServices(ManagedServiceSettings.Config c, List<ServiceInfo> list, + private static void getServices(ManagedServiceSettings.Config c, List<ComponentInfo> list, PackageManager pm) { - int services = 0; - if (list != null) { - list.clear(); - } final int user = ActivityManager.getCurrentUser(); List<ResolveInfo> installedServices = pm.queryIntentServicesAsUser( @@ -114,12 +112,28 @@ public class ZenServiceListing { if (list != null) { list.add(info); } - services++; } - return services; + } + + private static void getActivities(ManagedServiceSettings.Config c, List<ComponentInfo> list, + PackageManager pm) { + final int user = ActivityManager.getCurrentUser(); + + List<ResolveInfo> resolveInfos = pm.queryIntentActivitiesAsUser( + new Intent(c.configIntentAction), + PackageManager.GET_ACTIVITIES | PackageManager.GET_META_DATA, + user); + + for (int i = 0, count = resolveInfos.size(); i < count; i++) { + ResolveInfo resolveInfo = resolveInfos.get(i); + ActivityInfo info = resolveInfo.activityInfo; + if (list != null) { + list.add(info); + } + } } public interface Callback { - void onServicesReloaded(Set<ServiceInfo> services); + void onComponentsReloaded(Set<ComponentInfo> components); } } diff --git a/src/com/android/settings/vpn2/AppDialog.java b/src/com/android/settings/vpn2/AppDialog.java index e41ffefea5..815b28bf27 100644 --- a/src/com/android/settings/vpn2/AppDialog.java +++ b/src/com/android/settings/vpn2/AppDialog.java @@ -16,12 +16,13 @@ package com.android.settings.vpn2; -import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.pm.PackageInfo; import android.os.Bundle; +import androidx.appcompat.app.AlertDialog; + import com.android.settings.R; /** diff --git a/src/com/android/settings/vpn2/AppDialogFragment.java b/src/com/android/settings/vpn2/AppDialogFragment.java index 320dc20bda..2f9cd7a710 100644 --- a/src/com/android/settings/vpn2/AppDialogFragment.java +++ b/src/com/android/settings/vpn2/AppDialogFragment.java @@ -16,9 +16,8 @@ package com.android.settings.vpn2; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.Fragment; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.DialogInterface; import android.content.pm.PackageInfo; @@ -30,7 +29,9 @@ import android.os.UserHandle; import android.os.UserManager; import android.util.Log; -import com.android.internal.logging.nano.MetricsProto; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; + import com.android.internal.net.VpnConfig; import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; @@ -56,7 +57,7 @@ public class AppDialogFragment extends InstrumentedDialogFragment implements App @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DIALOG_VPN_APP_CONFIG; + return SettingsEnums.DIALOG_VPN_APP_CONFIG; } public interface Listener { diff --git a/src/com/android/settings/vpn2/AppManagementFragment.java b/src/com/android/settings/vpn2/AppManagementFragment.java index b61ae18472..5f4644614c 100644 --- a/src/com/android/settings/vpn2/AppManagementFragment.java +++ b/src/com/android/settings/vpn2/AppManagementFragment.java @@ -18,10 +18,9 @@ package com.android.settings.vpn2; import static android.app.AppOpsManager.OP_ACTIVATE_VPN; import android.annotation.NonNull; -import android.app.AlertDialog; import android.app.AppOpsManager; import android.app.Dialog; -import android.app.DialogFragment; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; @@ -34,12 +33,14 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; import android.os.UserManager; -import androidx.preference.Preference; import android.text.TextUtils; import android.util.Log; -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; +import androidx.preference.Preference; + import com.android.internal.net.VpnConfig; import com.android.internal.util.ArrayUtils; import com.android.settings.R; @@ -105,7 +106,7 @@ public class AppManagementFragment extends SettingsPreferenceFragment new SubSettingLauncher(context) .setDestination(AppManagementFragment.class.getName()) .setArguments(args) - .setTitle(pref.getLabel()) + .setTitleText(pref.getLabel()) .setSourceMetricsCategory(sourceMetricsCategory) .setUserHandle(new UserHandle(pref.getUserId())) .launch(); @@ -172,7 +173,7 @@ public class AppManagementFragment extends SettingsPreferenceFragment @Override public int getMetricsCategory() { - return MetricsEvent.VPN; + return SettingsEnums.VPN; } private boolean onForgetVpnClick() { @@ -334,7 +335,7 @@ public class AppManagementFragment extends SettingsPreferenceFragment @Override public int getMetricsCategory() { - return MetricsEvent.DIALOG_VPN_CANNOT_CONNECT; + return SettingsEnums.DIALOG_VPN_CANNOT_CONNECT; } public static void show(AppManagementFragment parent, String vpnLabel) { diff --git a/src/com/android/settings/vpn2/AppPreference.java b/src/com/android/settings/vpn2/AppPreference.java index fc06b1e112..6b64250df3 100644 --- a/src/com/android/settings/vpn2/AppPreference.java +++ b/src/com/android/settings/vpn2/AppPreference.java @@ -21,6 +21,7 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; import android.os.UserHandle; + import androidx.preference.Preference; import com.android.internal.net.LegacyVpnInfo; diff --git a/src/com/android/settings/vpn2/ConfigDialog.java b/src/com/android/settings/vpn2/ConfigDialog.java index 0c6d0611a5..9f2176c4f0 100644 --- a/src/com/android/settings/vpn2/ConfigDialog.java +++ b/src/com/android/settings/vpn2/ConfigDialog.java @@ -16,7 +16,6 @@ package com.android.settings.vpn2; -import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.net.Proxy; @@ -36,6 +35,8 @@ import android.widget.CompoundButton; import android.widget.Spinner; import android.widget.TextView; +import androidx.appcompat.app.AlertDialog; + import com.android.internal.net.VpnProfile; import com.android.settings.R; @@ -142,6 +143,7 @@ class ConfigDialog extends AlertDialog implements TextWatcher, } mMppe.setChecked(mProfile.mppe); mL2tpSecret.setText(mProfile.l2tpSecret); + mL2tpSecret.setTextAppearance(android.R.style.TextAppearance_DeviceDefault_Medium); mIpsecIdentifier.setText(mProfile.ipsecIdentifier); mIpsecSecret.setText(mProfile.ipsecSecret); loadCertificates(mIpsecUserCert, Credentials.USER_PRIVATE_KEY, 0, mProfile.ipsecUserCert); @@ -151,6 +153,7 @@ class ConfigDialog extends AlertDialog implements TextWatcher, R.string.vpn_no_server_cert, mProfile.ipsecServerCert); mSaveLogin.setChecked(mProfile.saveLogin); mAlwaysOnVpn.setChecked(mProfile.key.equals(VpnUtils.getLockdownVpn())); + mPassword.setTextAppearance(android.R.style.TextAppearance_DeviceDefault_Medium); // Hide lockdown VPN on devices that require IMS authentication if (SystemProperties.getBoolean("persist.radio.imsregrequired", false)) { diff --git a/src/com/android/settings/vpn2/ConfigDialogFragment.java b/src/com/android/settings/vpn2/ConfigDialogFragment.java index 0212a2050f..57ba8bfd79 100644 --- a/src/com/android/settings/vpn2/ConfigDialogFragment.java +++ b/src/com/android/settings/vpn2/ConfigDialogFragment.java @@ -16,9 +16,8 @@ package com.android.settings.vpn2; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.DialogFragment; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.DialogInterface; import android.net.ConnectivityManager; @@ -33,9 +32,9 @@ import android.util.Log; import android.view.View; import android.widget.Toast; -import com.android.internal.logging.nano.MetricsProto; +import androidx.appcompat.app.AlertDialog; + import com.android.internal.net.LegacyVpnInfo; -import com.android.internal.net.VpnConfig; import com.android.internal.net.VpnProfile; import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; @@ -57,12 +56,10 @@ public class ConfigDialogFragment extends InstrumentedDialogFragment implements ServiceManager.getService(Context.CONNECTIVITY_SERVICE)); private Context mContext; - private boolean mUnlocking = false; - @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DIALOG_LEGACY_VPN_CONFIG; + return SettingsEnums.DIALOG_LEGACY_VPN_CONFIG; } public static void show(VpnSettings parent, VpnProfile profile, boolean edit, boolean exists) { @@ -86,27 +83,6 @@ public class ConfigDialogFragment extends InstrumentedDialogFragment implements } @Override - public void onResume() { - super.onResume(); - - // Check KeyStore here, so others do not need to deal with it. - if (!KeyStore.getInstance().isUnlocked()) { - if (!mUnlocking) { - // Let us unlock KeyStore. See you later! - Credentials.getInstance().unlock(mContext); - } else { - // We already tried, but it is still not working! - dismiss(); - } - mUnlocking = !mUnlocking; - return; - } - - // Now KeyStore is always unlocked. Reset the flag. - mUnlocking = false; - } - - @Override public Dialog onCreateDialog(Bundle savedInstanceState) { Bundle args = getArguments(); VpnProfile profile = (VpnProfile) args.getParcelable(ARG_PROFILE); diff --git a/src/com/android/settings/vpn2/ConfirmLockdownFragment.java b/src/com/android/settings/vpn2/ConfirmLockdownFragment.java index 2e0914ed4e..1e8074c6a4 100644 --- a/src/com/android/settings/vpn2/ConfirmLockdownFragment.java +++ b/src/com/android/settings/vpn2/ConfirmLockdownFragment.java @@ -15,14 +15,14 @@ */ package com.android.settings.vpn2; -import android.app.Fragment; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.DialogFragment; +import android.app.settings.SettingsEnums; import android.content.DialogInterface; import android.os.Bundle; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; + import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; @@ -36,7 +36,7 @@ public class ConfirmLockdownFragment extends InstrumentedDialogFragment @Override public int getMetricsCategory() { - return MetricsEvent.DIALOG_VPN_REPLACE_EXISTING; + return SettingsEnums.DIALOG_VPN_REPLACE_EXISTING; } private static final String ARG_REPLACING = "replacing"; diff --git a/src/com/android/settings/vpn2/LegacyVpnPreference.java b/src/com/android/settings/vpn2/LegacyVpnPreference.java index 1f06a958ea..e44a0570fa 100644 --- a/src/com/android/settings/vpn2/LegacyVpnPreference.java +++ b/src/com/android/settings/vpn2/LegacyVpnPreference.java @@ -19,10 +19,11 @@ package com.android.settings.vpn2; import static com.android.internal.net.LegacyVpnInfo.STATE_CONNECTED; import android.content.Context; -import androidx.preference.Preference; import android.text.TextUtils; import android.view.View; +import androidx.preference.Preference; + import com.android.internal.net.VpnProfile; import com.android.settings.R; diff --git a/src/com/android/settings/vpn2/ManageablePreference.java b/src/com/android/settings/vpn2/ManageablePreference.java index 11da758541..68971f2f2f 100644 --- a/src/com/android/settings/vpn2/ManageablePreference.java +++ b/src/com/android/settings/vpn2/ManageablePreference.java @@ -23,8 +23,8 @@ import android.os.UserManager; import android.text.TextUtils; import android.util.AttributeSet; -import com.android.settings.widget.GearPreference; import com.android.settings.R; +import com.android.settings.widget.GearPreference; /** * This class sets appropriate enabled state and user admin message when userId is set diff --git a/src/com/android/settings/vpn2/OWNERS b/src/com/android/settings/vpn2/OWNERS new file mode 100644 index 0000000000..0419e2433e --- /dev/null +++ b/src/com/android/settings/vpn2/OWNERS @@ -0,0 +1,9 @@ +# People who can approve changes for submission. +jchalard@google.com +lorenzo@google.com +maze@google.com +reminv@google.com +xiaom@google.com + +# Emergency approvers in case the above are not available +satk@google.com diff --git a/src/com/android/settings/vpn2/VpnSettings.java b/src/com/android/settings/vpn2/VpnSettings.java index f6444bf643..83cdf71eec 100644 --- a/src/com/android/settings/vpn2/VpnSettings.java +++ b/src/com/android/settings/vpn2/VpnSettings.java @@ -16,10 +16,13 @@ package com.android.settings.vpn2; +import static android.app.AppOpsManager.OP_ACTIVATE_VPN; + import android.annotation.UiThread; import android.annotation.WorkerThread; import android.app.Activity; import android.app.AppOpsManager; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; @@ -40,8 +43,6 @@ import android.os.UserHandle; import android.os.UserManager; import android.security.Credentials; import android.security.KeyStore; -import androidx.preference.Preference; -import androidx.preference.PreferenceGroup; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; @@ -49,9 +50,11 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceGroup; + import com.android.internal.annotations.GuardedBy; -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.net.LegacyVpnInfo; import com.android.internal.net.VpnConfig; import com.android.internal.net.VpnProfile; @@ -60,7 +63,7 @@ import com.android.settings.R; import com.android.settings.RestrictedSettingsFragment; import com.android.settings.widget.GearPreference; import com.android.settings.widget.GearPreference.OnGearClickListener; -import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; import com.google.android.collect.Lists; @@ -71,8 +74,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import static android.app.AppOpsManager.OP_ACTIVATE_VPN; - /** * Settings screen listing VPNs. Configured VPNs and networks managed by apps * are shown in the same list. @@ -113,7 +114,7 @@ public class VpnSettings extends RestrictedSettingsFragment implements @Override public int getMetricsCategory() { - return MetricsEvent.VPN; + return SettingsEnums.VPN; } @Override @@ -142,7 +143,7 @@ public class VpnSettings extends RestrictedSettingsFragment implements // Disable all actions if VPN configuration has been disallowed for (int i = 0; i < menu.size(); i++) { if (isUiRestrictedByOnlyAdmin()) { - RestrictedLockUtils.setMenuItemAsDisabledByAdmin(getPrefContext(), + RestrictedLockUtilsInternal.setMenuItemAsDisabledByAdmin(getPrefContext(), menu.getItem(i), getRestrictionEnforcedAdmin()); } else { menu.getItem(i).setEnabled(!mUnavailable); @@ -152,17 +153,15 @@ public class VpnSettings extends RestrictedSettingsFragment implements @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.vpn_create: { - // Generate a new key. Here we just use the current time. - long millis = System.currentTimeMillis(); - while (mLegacyVpnPreferences.containsKey(Long.toHexString(millis))) { - ++millis; - } - VpnProfile profile = new VpnProfile(Long.toHexString(millis)); - ConfigDialogFragment.show(this, profile, true /* editing */, false /* exists */); - return true; + // Generate a new key. Here we just use the current time. + if (item.getItemId() == R.id.vpn_create) { + long millis = System.currentTimeMillis(); + while (mLegacyVpnPreferences.containsKey(Long.toHexString(millis))) { + ++millis; } + VpnProfile profile = new VpnProfile(Long.toHexString(millis)); + ConfigDialogFragment.show(this, profile, true /* editing */, false /* exists */); + return true; } return super.onOptionsItemSelected(item); } diff --git a/src/com/android/settings/vpn2/VpnUtils.java b/src/com/android/settings/vpn2/VpnUtils.java index 4528180b0e..38c56c527d 100644 --- a/src/com/android/settings/vpn2/VpnUtils.java +++ b/src/com/android/settings/vpn2/VpnUtils.java @@ -38,7 +38,8 @@ public class VpnUtils { private static final String TAG = "VpnUtils"; public static String getLockdownVpn() { - final byte[] value = KeyStore.getInstance().get(Credentials.LOCKDOWN_VPN); + final byte[] value = KeyStore.getInstance().get( + Credentials.LOCKDOWN_VPN, true /* suppressKeyNotFoundWarning */); return value == null ? null : new String(value); } diff --git a/src/com/android/settings/wallpaper/StyleSuggestionActivity.java b/src/com/android/settings/wallpaper/StyleSuggestionActivity.java new file mode 100644 index 0000000000..376724b0ca --- /dev/null +++ b/src/com/android/settings/wallpaper/StyleSuggestionActivity.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2019 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.wallpaper; + +import android.content.Context; +import android.provider.Settings; +import android.text.TextUtils; + +import com.android.internal.annotations.VisibleForTesting; + +public class StyleSuggestionActivity extends StyleSuggestionActivityBase { + + @VisibleForTesting + public static boolean isSuggestionComplete(Context context) { + if (!isWallpaperServiceEnabled(context)) { + return true; + } + + final String currentTheme = Settings.Secure.getStringForUser(context.getContentResolver(), + Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, context.getUserId()); + if (TextUtils.isEmpty(currentTheme)) { + // Empty value means the user has not visited the styles tab yet + return false; + } + return true; + } +} diff --git a/src/com/android/settings/wallpaper/StyleSuggestionActivityBase.java b/src/com/android/settings/wallpaper/StyleSuggestionActivityBase.java new file mode 100644 index 0000000000..abbf3dc814 --- /dev/null +++ b/src/com/android/settings/wallpaper/StyleSuggestionActivityBase.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2019 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.wallpaper; + +import android.app.Activity; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Bundle; + +import androidx.annotation.VisibleForTesting; + +import com.android.settings.R; +import com.android.settings.core.SubSettingLauncher; +import com.android.settings.display.WallpaperPreferenceController; + +import com.google.android.setupcompat.util.WizardManagerHelper; + +public abstract class StyleSuggestionActivityBase extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + final PackageManager pm = getPackageManager(); + final Intent intent = new Intent() + .setComponent(new WallpaperPreferenceController(this, "dummy key") + .getComponentName()) + .addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); + + // passing the necessary extra to next page + WizardManagerHelper.copyWizardManagerExtras(getIntent(), intent); + + addExtras(intent); + + if (pm.resolveActivity(intent, 0) != null) { + startActivity(intent); + } else { + startFallbackSuggestion(); + } + + finish(); + } + + /** + * Add any extras to the intent before launching the wallpaper activity + * @param intent + */ + protected void addExtras(Intent intent) { } + + @VisibleForTesting + void startFallbackSuggestion() { + // fall back to default wallpaper picker + new SubSettingLauncher(this) + .setDestination(WallpaperTypeSettings.class.getName()) + .setTitleRes(R.string.wallpaper_suggestion_title) + .setSourceMetricsCategory(SettingsEnums.DASHBOARD_SUMMARY) + .addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT) + .launch(); + } + + protected static boolean isWallpaperServiceEnabled(Context context) { + return context.getResources().getBoolean( + com.android.internal.R.bool.config_enableWallpaperService); + } +} diff --git a/src/com/android/settings/wallpaper/WallpaperSuggestionActivity.java b/src/com/android/settings/wallpaper/WallpaperSuggestionActivity.java index 80910a1ea5..57222f1a41 100644 --- a/src/com/android/settings/wallpaper/WallpaperSuggestionActivity.java +++ b/src/com/android/settings/wallpaper/WallpaperSuggestionActivity.java @@ -16,46 +16,31 @@ package com.android.settings.wallpaper; -import android.app.Activity; import android.app.WallpaperManager; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager; -import android.os.Bundle; + import androidx.annotation.VisibleForTesting; -import com.android.internal.logging.nano.MetricsProto; -import com.android.settings.R; -import com.android.settings.core.SubSettingLauncher; +import com.android.settings.display.WallpaperPreferenceController; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; +import com.android.settings.search.SearchIndexableRaw; +import com.android.settingslib.search.SearchIndexable; -public class WallpaperSuggestionActivity extends Activity { +import java.util.ArrayList; +import java.util.List; - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - final PackageManager pm = getPackageManager(); - final Intent intent = new Intent() - .setClassName(getString(R.string.config_wallpaper_picker_package), - getString(R.string.config_wallpaper_picker_class)) - .addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); - if (pm.resolveActivity(intent, 0) != null) { - startActivity(intent); - } else { - startFallbackSuggestion(); - } +@SearchIndexable +public class WallpaperSuggestionActivity extends StyleSuggestionActivityBase implements Indexable { - finish(); - } + private static final String WALLPAPER_FLAVOR_EXTRA = "com.android.launcher3.WALLPAPER_FLAVOR"; + private static final String WALLPAPER_FOCUS = "focus_wallpaper"; - @VisibleForTesting - void startFallbackSuggestion() { - // fall back to default wallpaper picker - new SubSettingLauncher(this) - .setDestination(WallpaperTypeSettings.class.getName()) - .setTitle(R.string.wallpaper_suggestion_title) - .setSourceMetricsCategory(MetricsProto.MetricsEvent.DASHBOARD_SUMMARY) - .addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT) - .launch(); + @Override + protected void addExtras(Intent intent) { + intent.putExtra(WALLPAPER_FLAVOR_EXTRA, WALLPAPER_FOCUS); } @VisibleForTesting @@ -68,8 +53,27 @@ public class WallpaperSuggestionActivity extends Activity { return manager.getWallpaperId(WallpaperManager.FLAG_SYSTEM) > 0; } - private static boolean isWallpaperServiceEnabled(Context context) { - return context.getResources().getBoolean( - com.android.internal.R.bool.config_enableWallpaperService); - } + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + private static final String SUPPORT_SEARCH_INDEX_KEY = "wallpaper_type"; + + @Override + public List<SearchIndexableRaw> getRawDataToIndex(Context context, + boolean enabled) { + final List<SearchIndexableRaw> result = new ArrayList<>(); + WallpaperPreferenceController controller = + new WallpaperPreferenceController(context, "dummy key"); + SearchIndexableRaw data = new SearchIndexableRaw(context); + data.title = controller.getTitle(); + data.screenTitle = data.title; + ComponentName component = controller.getComponentName(); + data.intentTargetPackage = component.getPackageName(); + data.intentTargetClass = component.getClassName(); + data.intentAction = Intent.ACTION_MAIN; + data.key = SUPPORT_SEARCH_INDEX_KEY; + data.keywords = controller.getKeywords(); + result.add(data); + return result; + } + }; } diff --git a/src/com/android/settings/wallpaper/WallpaperTypePreferenceController.java b/src/com/android/settings/wallpaper/WallpaperTypePreferenceController.java new file mode 100644 index 0000000000..95797863d4 --- /dev/null +++ b/src/com/android/settings/wallpaper/WallpaperTypePreferenceController.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2018 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.wallpaper; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.text.TextUtils; + +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.core.BasePreferenceController; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnStart; + +import java.util.List; +import java.util.stream.Collectors; + +public class WallpaperTypePreferenceController extends BasePreferenceController + implements LifecycleObserver, OnStart { + + private PreferenceScreen mScreen; + + public WallpaperTypePreferenceController(Context context, String key) { + super(context, key); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mScreen = screen; + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } + + @Override + public boolean handlePreferenceTreeClick(Preference preference) { + if (preference.getIntent() == null) { + return super.handlePreferenceTreeClick(preference); + } + mContext.startActivity(preference.getIntent()); + return true; + } + + @Override + public void onStart() { + populateWallpaperTypes(); + } + + private void populateWallpaperTypes() { + // Search for activities that satisfy the ACTION_SET_WALLPAPER action + final Intent intent = new Intent(Intent.ACTION_SET_WALLPAPER); + final PackageManager pm = mContext.getPackageManager(); + final List<ResolveInfo> rList = pm.queryIntentActivities(intent, + PackageManager.MATCH_DEFAULT_ONLY); + + removeUselessExistingPreference(rList); + mScreen.setOrderingAsAdded(false); + // Add Preference items for each of the matching activities + for (ResolveInfo info : rList) { + final String packageName = info.activityInfo.packageName; + Preference pref = mScreen.findPreference(packageName); + if (pref == null) { + pref = new Preference(mScreen.getContext()); + } + final Intent prefIntent = new Intent(intent).addFlags( + Intent.FLAG_ACTIVITY_FORWARD_RESULT); + prefIntent.setComponent(new ComponentName(packageName, info.activityInfo.name)); + pref.setIntent(prefIntent); + pref.setKey(packageName); + CharSequence label = info.loadLabel(pm); + if (label == null) { + label = packageName; + } + pref.setTitle(label); + pref.setIcon(info.loadIcon(pm)); + mScreen.addPreference(pref); + } + } + + private void removeUselessExistingPreference(List<ResolveInfo> rList) { + final int count = mScreen.getPreferenceCount(); + if (count <= 0) { + return; + } + for (int i = count - 1; i >= 0; i--) { + final Preference pref = mScreen.getPreference(i); + final List<ResolveInfo> result = rList.stream().filter( + rInfo -> TextUtils.equals(pref.getKey(), + rInfo.activityInfo.packageName)).collect(Collectors.toList()); + if (result.isEmpty()) { + mScreen.removePreference(pref); + } + } + } +}
\ No newline at end of file diff --git a/src/com/android/settings/wallpaper/WallpaperTypeSettings.java b/src/com/android/settings/wallpaper/WallpaperTypeSettings.java index 73a58fee06..2d4a16f2bd 100644 --- a/src/com/android/settings/wallpaper/WallpaperTypeSettings.java +++ b/src/com/android/settings/wallpaper/WallpaperTypeSettings.java @@ -16,30 +16,18 @@ package com.android.settings.wallpaper; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.os.Bundle; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; +import android.app.settings.SettingsEnums; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; -import com.android.settings.SettingsPreferenceFragment; -import com.android.settings.search.BaseSearchIndexProvider; -import com.android.settings.search.Indexable; -import com.android.settings.search.SearchIndexableRaw; +import com.android.settings.dashboard.DashboardFragment; -import java.util.ArrayList; -import java.util.List; -public class WallpaperTypeSettings extends SettingsPreferenceFragment implements Indexable { +public class WallpaperTypeSettings extends DashboardFragment { + private static final String TAG = "WallpaperTypeSettings"; @Override public int getMetricsCategory() { - return MetricsEvent.WALLPAPER_TYPE; + return SettingsEnums.WALLPAPER_TYPE; } @Override @@ -48,81 +36,12 @@ public class WallpaperTypeSettings extends SettingsPreferenceFragment implements } @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - addPreferencesFromResource(R.xml.wallpaper_settings); - populateWallpaperTypes(); - } - - private void populateWallpaperTypes() { - // Search for activities that satisfy the ACTION_SET_WALLPAPER action - final Intent intent = new Intent(Intent.ACTION_SET_WALLPAPER); - final PackageManager pm = getPackageManager(); - final List<ResolveInfo> rList = pm.queryIntentActivities(intent, - PackageManager.MATCH_DEFAULT_ONLY); - - final PreferenceScreen parent = getPreferenceScreen(); - parent.setOrderingAsAdded(false); - // Add Preference items for each of the matching activities - for (ResolveInfo info : rList) { - Preference pref = new Preference(getPrefContext()); - Intent prefIntent = new Intent(intent).addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); - prefIntent.setComponent(new ComponentName( - info.activityInfo.packageName, info.activityInfo.name)); - pref.setIntent(prefIntent); - CharSequence label = info.loadLabel(pm); - if (label == null) label = info.activityInfo.packageName; - pref.setTitle(label); - pref.setIcon(info.loadIcon(pm)); - parent.addPreference(pref); - } + protected String getLogTag() { + return TAG; } @Override - public boolean onPreferenceTreeClick(Preference preference) { - if (preference.getIntent() == null) { - return super.onPreferenceTreeClick(preference); - } - startActivity(preference.getIntent()); - finish(); - return true; + protected int getPreferenceScreenResId() { + return R.xml.wallpaper_settings; } - - public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = - new BaseSearchIndexProvider() { - @Override - public List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled) { - final List<SearchIndexableRaw> result = new ArrayList<SearchIndexableRaw>(); - - final Intent intent = new Intent(Intent.ACTION_SET_WALLPAPER); - final PackageManager pm = context.getPackageManager(); - final List<ResolveInfo> rList = pm.queryIntentActivities(intent, - PackageManager.MATCH_DEFAULT_ONLY); - - // Add indexable data for package that is in config_wallpaper_picker_package - final String wallpaperPickerPackage = - context.getString(R.string.config_wallpaper_picker_package); - for (ResolveInfo info : rList) { - if (!wallpaperPickerPackage.equals(info.activityInfo.packageName)) { - continue; - } - CharSequence label = info.loadLabel(pm); - if (label == null) label = info.activityInfo.packageName; - - SearchIndexableRaw data = new SearchIndexableRaw(context); - data.title = label.toString(); - data.key = "wallpaper_type_settings"; - data.screenTitle = context.getResources().getString( - R.string.wallpaper_settings_fragment_title); - data.intentAction = Intent.ACTION_SET_WALLPAPER; - data.intentTargetPackage = info.activityInfo.packageName; - data.intentTargetClass = info.activityInfo.name; - data.keywords = context.getString(R.string.keywords_wallpaper); - result.add(data); - } - - return result; - } - }; } diff --git a/src/com/android/settings/webview/WebViewAppPicker.java b/src/com/android/settings/webview/WebViewAppPicker.java index 6a8ab50681..b1dfd1454f 100644 --- a/src/com/android/settings/webview/WebViewAppPicker.java +++ b/src/com/android/settings/webview/WebViewAppPicker.java @@ -19,21 +19,21 @@ package com.android.settings.webview; import static android.provider.Settings.ACTION_WEBVIEW_SETTINGS; import android.app.Activity; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageItemInfo; import android.content.pm.PackageManager; -import androidx.annotation.VisibleForTesting; import android.text.TextUtils; import android.webkit.UserPackage; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.annotation.VisibleForTesting; + import com.android.settings.R; import com.android.settings.applications.defaultapps.DefaultAppPickerFragment; import com.android.settingslib.applications.DefaultAppInfo; -import com.android.settingslib.wrapper.PackageManagerWrapper; import java.util.ArrayList; import java.util.List; @@ -114,21 +114,20 @@ public class WebViewAppPicker extends DefaultAppPickerFragment { @Override public int getMetricsCategory() { - return MetricsEvent.WEBVIEW_IMPLEMENTATION; + return SettingsEnums.WEBVIEW_IMPLEMENTATION; } private static class WebViewAppInfo extends DefaultAppInfo { - public WebViewAppInfo(Context context, PackageManagerWrapper pm, + public WebViewAppInfo(Context context, PackageManager pm, int userId, PackageItemInfo packageItemInfo, String summary, boolean enabled) { - super(context, pm, packageItemInfo, summary, enabled); + super(context, pm, userId, packageItemInfo, summary, enabled); } @Override public CharSequence loadLabel() { String versionName = ""; try { - versionName = mPm.getPackageManager(). - getPackageInfo(packageItemInfo.packageName, 0).versionName; + versionName = mPm.getPackageInfo(packageItemInfo.packageName, 0).versionName; } catch (PackageManager.NameNotFoundException e) { } return String.format("%s %s", super.loadLabel(), versionName); @@ -137,9 +136,9 @@ public class WebViewAppPicker extends DefaultAppPickerFragment { @VisibleForTesting - DefaultAppInfo createDefaultAppInfo(Context context, PackageManagerWrapper pm, + DefaultAppInfo createDefaultAppInfo(Context context, PackageManager pm, PackageItemInfo packageItemInfo, String disabledReason) { - return new WebViewAppInfo(context, pm, packageItemInfo, disabledReason, + return new WebViewAppInfo(context, pm, mUserId, packageItemInfo, disabledReason, TextUtils.isEmpty(disabledReason) /* enabled */); } diff --git a/src/com/android/settings/wfd/WifiDisplaySettings.java b/src/com/android/settings/wfd/WifiDisplaySettings.java index 5ab0bcbe80..981f927ee0 100755 --- a/src/com/android/settings/wfd/WifiDisplaySettings.java +++ b/src/com/android/settings/wfd/WifiDisplaySettings.java @@ -16,7 +16,7 @@ package com.android.settings.wfd; -import android.app.AlertDialog; +import android.app.settings.SettingsEnums; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; @@ -39,14 +39,6 @@ import android.os.Handler; import android.os.Looper; import android.provider.SearchIndexableResource; import android.provider.Settings; -import androidx.preference.SwitchPreference; -import androidx.preference.ListPreference; -import androidx.preference.Preference; -import androidx.preference.Preference.OnPreferenceChangeListener; -import androidx.preference.PreferenceCategory; -import androidx.preference.PreferenceGroup; -import androidx.preference.PreferenceScreen; -import androidx.preference.PreferenceViewHolder; import android.util.Slog; import android.util.TypedValue; import android.view.Menu; @@ -59,13 +51,23 @@ import android.widget.EditText; import android.widget.ImageView; import android.widget.TextView; +import androidx.appcompat.app.AlertDialog; +import androidx.preference.ListPreference; +import androidx.preference.Preference; +import androidx.preference.Preference.OnPreferenceChangeListener; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceGroup; +import androidx.preference.PreferenceScreen; +import androidx.preference.PreferenceViewHolder; +import androidx.preference.SwitchPreference; + import com.android.internal.app.MediaRouteDialogPresenter; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; import com.android.settings.dashboard.SummaryLoader; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.List; @@ -78,6 +80,7 @@ import java.util.List; * on the system. In that case, the enable option will not be shown but other * remote display routes will continue to be made available. */ +@SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC) public final class WifiDisplaySettings extends SettingsPreferenceFragment implements Indexable { private static final String TAG = "WifiDisplaySettings"; private static final boolean DEBUG = false; @@ -124,7 +127,7 @@ public final class WifiDisplaySettings extends SettingsPreferenceFragment implem @Override public int getMetricsCategory() { - return MetricsEvent.WFD_WIFI_DISPLAY; + return SettingsEnums.WFD_WIFI_DISPLAY; } @Override @@ -133,6 +136,7 @@ public final class WifiDisplaySettings extends SettingsPreferenceFragment implem final Context context = getActivity(); mRouter = (MediaRouter) context.getSystemService(Context.MEDIA_ROUTER_SERVICE); + mRouter.setRouterGroupId(MediaRouter.MIRRORING_GROUP_ID); mDisplayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); mWifiP2pManager = (WifiP2pManager) context.getSystemService(Context.WIFI_P2P_SERVICE); mWifiP2pChannel = mWifiP2pManager.initialize(context, Looper.getMainLooper(), null); @@ -795,6 +799,7 @@ public final class WifiDisplaySettings extends SettingsPreferenceFragment implem mContext = context; mSummaryLoader = summaryLoader; mRouter = (MediaRouter) context.getSystemService(Context.MEDIA_ROUTER_SERVICE); + mRouter.setRouterGroupId(MediaRouter.MIRRORING_GROUP_ID); } @Override diff --git a/src/com/android/settings/widget/ActionBarShadowController.java b/src/com/android/settings/widget/ActionBarShadowController.java deleted file mode 100644 index cb5e38dcb6..0000000000 --- a/src/com/android/settings/widget/ActionBarShadowController.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright (C) 2017 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.app.ActionBar; -import android.app.Activity; -import androidx.annotation.VisibleForTesting; -import androidx.recyclerview.widget.RecyclerView; -import android.view.View; - -import com.android.settingslib.core.lifecycle.Lifecycle; -import com.android.settingslib.core.lifecycle.LifecycleObserver; -import com.android.settingslib.core.lifecycle.events.OnStart; -import com.android.settingslib.core.lifecycle.events.OnStop; - -/** - * A controller that adds shadow to actionbar when content view scrolls. - * <p/> - * It also works on custom views acting as an actionbar. - */ -public class ActionBarShadowController implements LifecycleObserver, OnStart, OnStop { - - @VisibleForTesting - static final float ELEVATION_HIGH = 8; - @VisibleForTesting - static final float ELEVATION_LOW = 0; - - @VisibleForTesting - ScrollChangeWatcher mScrollChangeWatcher; - private RecyclerView mRecyclerView; - private boolean isScrollWatcherAttached; - - public static ActionBarShadowController attachToRecyclerView(Activity activity, - Lifecycle lifecycle, RecyclerView recyclerView) { - return new ActionBarShadowController(activity, lifecycle, recyclerView); - } - - public static ActionBarShadowController attachToRecyclerView(View anchorView, - Lifecycle lifecycle, RecyclerView recyclerView) { - return new ActionBarShadowController(anchorView, lifecycle, recyclerView); - } - - private ActionBarShadowController(Activity activity, Lifecycle lifecycle, - RecyclerView recyclerView) { - mScrollChangeWatcher = new ScrollChangeWatcher(activity); - mRecyclerView = recyclerView; - attachScrollWatcher(); - lifecycle.addObserver(this); - } - - private ActionBarShadowController(View anchorView, Lifecycle lifecycle, - RecyclerView recyclerView) { - mScrollChangeWatcher = new ScrollChangeWatcher(anchorView); - mRecyclerView = recyclerView; - attachScrollWatcher(); - lifecycle.addObserver(this); - } - - @Override - public void onStop() { - detachScrollWatcher(); - } - - private void detachScrollWatcher() { - mRecyclerView.removeOnScrollListener(mScrollChangeWatcher); - isScrollWatcherAttached = false; - } - - @Override - public void onStart() { - attachScrollWatcher(); - } - - private void attachScrollWatcher() { - if (!isScrollWatcherAttached) { - isScrollWatcherAttached = true; - mRecyclerView.addOnScrollListener(mScrollChangeWatcher); - mScrollChangeWatcher.updateDropShadow(mRecyclerView); - } - } - - /** - * Update the drop shadow as the scrollable entity is scrolled. - */ - final class ScrollChangeWatcher extends RecyclerView.OnScrollListener { - - private final Activity mActivity; - private final View mAnchorView; - - public ScrollChangeWatcher(Activity activity) { - mActivity = activity; - mAnchorView = null; - } - - public ScrollChangeWatcher(View anchorView) { - mAnchorView = anchorView; - mActivity = null; - } - - // RecyclerView scrolled. - @Override - public void onScrolled(RecyclerView view, int dx, int dy) { - updateDropShadow(view); - } - - public void updateDropShadow(View view) { - final boolean shouldShowShadow = view.canScrollVertically(-1); - if (mAnchorView != null) { - mAnchorView.setElevation(shouldShowShadow ? ELEVATION_HIGH : ELEVATION_LOW); - } else if (mActivity != null) { // activity can become null when running monkey - final ActionBar actionBar = mActivity.getActionBar(); - if (actionBar != null) { - actionBar.setElevation(shouldShowShadow ? ELEVATION_HIGH : ELEVATION_LOW); - } - } - } - } - -} diff --git a/src/com/android/settings/widget/ActionButtonPreference.java b/src/com/android/settings/widget/ActionButtonPreference.java deleted file mode 100644 index 64a01c7e17..0000000000 --- a/src/com/android/settings/widget/ActionButtonPreference.java +++ /dev/null @@ -1,186 +0,0 @@ -/* - * Copyright (C) 2017 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 androidx.annotation.StringRes; -import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.view.View; -import android.widget.Button; - -import com.android.settings.R; - -public class ActionButtonPreference extends Preference { - - private final ButtonInfo mButton1Info = new ButtonInfo(); - private final ButtonInfo mButton2Info = new ButtonInfo(); - - public ActionButtonPreference(Context context, AttributeSet attrs, - int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - init(); - } - - public ActionButtonPreference(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - init(); - } - - public ActionButtonPreference(Context context, AttributeSet attrs) { - super(context, attrs); - init(); - } - - public ActionButtonPreference(Context context) { - super(context); - init(); - } - - private void init() { - setLayoutResource(R.layout.two_action_buttons); - setSelectable(false); - } - - @Override - public void onBindViewHolder(PreferenceViewHolder holder) { - super.onBindViewHolder(holder); - holder.setDividerAllowedAbove(false); - holder.setDividerAllowedBelow(false); - mButton1Info.mPositiveButton = (Button) holder.findViewById(R.id.button1_positive); - mButton1Info.mNegativeButton = (Button) holder.findViewById(R.id.button1_negative); - mButton2Info.mPositiveButton = (Button) holder.findViewById(R.id.button2_positive); - mButton2Info.mNegativeButton = (Button) holder.findViewById(R.id.button2_negative); - - mButton1Info.setUpButton(); - mButton2Info.setUpButton(); - } - - public ActionButtonPreference setButton1Text(@StringRes int textResId) { - final String newText = getContext().getString(textResId); - if (!TextUtils.equals(newText, mButton1Info.mText)) { - mButton1Info.mText = newText; - notifyChanged(); - } - return this; - } - - public ActionButtonPreference setButton1Enabled(boolean isEnabled) { - if (isEnabled != mButton1Info.mIsEnabled) { - mButton1Info.mIsEnabled = isEnabled; - notifyChanged(); - } - return this; - } - - public ActionButtonPreference setButton2Text(@StringRes int textResId) { - final String newText = getContext().getString(textResId); - if (!TextUtils.equals(newText, mButton2Info.mText)) { - mButton2Info.mText = newText; - notifyChanged(); - } - return this; - } - - public ActionButtonPreference setButton2Enabled(boolean isEnabled) { - if (isEnabled != mButton2Info.mIsEnabled) { - mButton2Info.mIsEnabled = isEnabled; - notifyChanged(); - } - return this; - } - - public ActionButtonPreference setButton1OnClickListener(View.OnClickListener listener) { - if (listener != mButton1Info.mListener) { - mButton1Info.mListener = listener; - notifyChanged(); - } - return this; - } - - public ActionButtonPreference setButton2OnClickListener(View.OnClickListener listener) { - if (listener != mButton2Info.mListener) { - mButton2Info.mListener = listener; - notifyChanged(); - } - return this; - } - - public ActionButtonPreference setButton1Positive(boolean isPositive) { - if (isPositive != mButton1Info.mIsPositive) { - mButton1Info.mIsPositive = isPositive; - notifyChanged(); - } - return this; - } - - public ActionButtonPreference setButton2Positive(boolean isPositive) { - if (isPositive != mButton2Info.mIsPositive) { - mButton2Info.mIsPositive = isPositive; - notifyChanged(); - } - return this; - } - public ActionButtonPreference setButton1Visible(boolean isPositive) { - if (isPositive != mButton1Info.mIsVisible) { - mButton1Info.mIsVisible = isPositive; - notifyChanged(); - } - return this; - } - - public ActionButtonPreference setButton2Visible(boolean isPositive) { - if (isPositive != mButton2Info.mIsVisible) { - mButton2Info.mIsVisible = isPositive; - notifyChanged(); - } - return this; - } - - static class ButtonInfo { - private Button mPositiveButton; - private Button mNegativeButton; - private CharSequence mText; - private View.OnClickListener mListener; - private boolean mIsPositive = true; - private boolean mIsEnabled = true; - private boolean mIsVisible = true; - - void setUpButton() { - setUpButton(mPositiveButton); - setUpButton(mNegativeButton); - if (!mIsVisible) { - mPositiveButton.setVisibility(View.INVISIBLE); - mNegativeButton.setVisibility(View.INVISIBLE); - } else if (mIsPositive) { - mPositiveButton.setVisibility(View.VISIBLE); - mNegativeButton.setVisibility(View.INVISIBLE); - } else { - mPositiveButton.setVisibility(View.INVISIBLE); - mNegativeButton.setVisibility(View.VISIBLE); - } - } - - private void setUpButton(Button button) { - button.setText(mText); - button.setOnClickListener(mListener); - button.setEnabled(mIsEnabled); - } - } -}
\ No newline at end of file diff --git a/src/com/android/settings/widget/AddPreference.java b/src/com/android/settings/widget/AddPreference.java new file mode 100644 index 0000000000..2183fd4f11 --- /dev/null +++ b/src/com/android/settings/widget/AddPreference.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2019 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.util.AttributeSet; +import android.view.View; + +import com.android.settings.R; +import com.android.settingslib.RestrictedPreference; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.PreferenceViewHolder; + +/** + * A preference with a plus button on the side representing an "add" action. The plus button will + * only be visible when a non-null click listener is registered. + */ +public class AddPreference extends RestrictedPreference implements View.OnClickListener { + + private OnAddClickListener mListener; + private View mWidgetFrame; + private View mAddWidget; + + public AddPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @VisibleForTesting + int getAddWidgetResId() { + return R.id.add_preference_widget; + } + + /** Sets a listener for clicks on the plus button. Passing null will cause the button to be + * hidden. */ + public void setOnAddClickListener(OnAddClickListener listener) { + mListener = listener; + if (mWidgetFrame != null) { + mWidgetFrame.setVisibility(shouldHideSecondTarget() ? View.GONE : View.VISIBLE); + } + } + + public void setAddWidgetEnabled(boolean enabled) { + if (mAddWidget != null) { + mAddWidget.setEnabled(enabled); + } + } + + @Override + protected int getSecondTargetResId() { + return R.layout.preference_widget_add; + } + + @Override + protected boolean shouldHideSecondTarget() { + return mListener == null; + } + + @Override + public void onBindViewHolder(PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + mWidgetFrame = holder.findViewById(android.R.id.widget_frame); + mAddWidget = holder.findViewById(getAddWidgetResId()); + mAddWidget.setEnabled(true); + mAddWidget.setOnClickListener(this); + } + + @Override + public void onClick(View view) { + if (view.getId() == getAddWidgetResId() && mListener != null) { + mListener.onAddClick(this); + } + } + + public interface OnAddClickListener { + void onAddClick(AddPreference p); + } +} diff --git a/src/com/android/settings/widget/AppCheckBoxPreference.java b/src/com/android/settings/widget/AppCheckBoxPreference.java index 23ee894b8d..3ce67ebf59 100644 --- a/src/com/android/settings/widget/AppCheckBoxPreference.java +++ b/src/com/android/settings/widget/AppCheckBoxPreference.java @@ -17,8 +17,13 @@ package com.android.settings.widget; import android.content.Context; -import androidx.preference.CheckBoxPreference; +import android.text.TextUtils; import android.util.AttributeSet; +import android.view.View; +import android.widget.LinearLayout; + +import androidx.preference.CheckBoxPreference; +import androidx.preference.PreferenceViewHolder; import com.android.settings.R; @@ -35,4 +40,15 @@ public class AppCheckBoxPreference extends CheckBoxPreference { super(context); setLayoutResource(R.layout.preference_app); } + + @Override + public void onBindViewHolder(PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + + final LinearLayout layout = (LinearLayout) holder.findViewById(R.id.summary_container); + if (layout != null) { + // If summary doesn't exist, make it gone + layout.setVisibility(TextUtils.isEmpty(getSummary()) ? View.GONE : View.VISIBLE); + } + } } diff --git a/src/com/android/settings/widget/AppPreference.java b/src/com/android/settings/widget/AppPreference.java deleted file mode 100644 index 3b716a31b9..0000000000 --- a/src/com/android/settings/widget/AppPreference.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (C) 2017 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 androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.view.View; -import android.widget.ProgressBar; - -import com.android.settings.R; - -public class AppPreference extends Preference { - - private int mProgress; - private boolean mProgressVisible; - - public AppPreference(Context context) { - super(context); - setLayoutResource(R.layout.preference_app); - } - - public AppPreference(Context context, AttributeSet attrs) { - super(context, attrs); - setLayoutResource(R.layout.preference_app); - } - - public void setProgress(int amount) { - mProgress = amount; - mProgressVisible = true; - notifyChanged(); - } - - @Override - public void onBindViewHolder(PreferenceViewHolder view) { - super.onBindViewHolder(view); - - view.findViewById(R.id.summary_container) - .setVisibility(TextUtils.isEmpty(getSummary()) ? View.GONE : View.VISIBLE); - final ProgressBar progress = (ProgressBar) view.findViewById(android.R.id.progress); - if (mProgressVisible) { - progress.setProgress(mProgress); - progress.setVisibility(View.VISIBLE); - } else { - progress.setVisibility(View.GONE); - } - } -} diff --git a/src/com/android/settings/widget/AppSwitchPreference.java b/src/com/android/settings/widget/AppSwitchPreference.java index b42bb3db25..506ab7642a 100644 --- a/src/com/android/settings/widget/AppSwitchPreference.java +++ b/src/com/android/settings/widget/AppSwitchPreference.java @@ -17,10 +17,8 @@ package com.android.settings.widget; import android.content.Context; + import androidx.preference.SwitchPreference; -import androidx.preference.PreferenceViewHolder; -import android.text.TextUtils; -import android.view.View; import com.android.settings.R; @@ -30,12 +28,4 @@ public class AppSwitchPreference extends SwitchPreference { super(context); setLayoutResource(R.layout.preference_app); } - - @Override - public void onBindViewHolder(PreferenceViewHolder view) { - super.onBindViewHolder(view); - - view.findViewById(R.id.summary_container) - .setVisibility(TextUtils.isEmpty(getSummary()) ? View.GONE : View.VISIBLE); - } } diff --git a/src/com/android/settings/widget/AspectRatioFrameLayout.java b/src/com/android/settings/widget/AspectRatioFrameLayout.java index 5eab257ffa..aa3d3c4ea1 100644 --- a/src/com/android/settings/widget/AspectRatioFrameLayout.java +++ b/src/com/android/settings/widget/AspectRatioFrameLayout.java @@ -16,10 +16,11 @@ package com.android.settings.widget; import android.content.Context; import android.content.res.TypedArray; -import androidx.annotation.VisibleForTesting; import android.util.AttributeSet; import android.widget.FrameLayout; +import androidx.annotation.VisibleForTesting; + import com.android.settings.R; /** diff --git a/src/com/android/settings/graph/BottomLabelLayout.java b/src/com/android/settings/widget/BottomLabelLayout.java index 20d97e5e4d..6323c4dcaa 100644 --- a/src/com/android/settings/graph/BottomLabelLayout.java +++ b/src/com/android/settings/widget/BottomLabelLayout.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2017 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 @@ -11,20 +12,19 @@ * 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.graph; +package com.android.settings.widget; import android.annotation.Nullable; import android.content.Context; -import androidx.annotation.VisibleForTesting; import android.util.AttributeSet; import android.view.Gravity; import android.view.View; import android.widget.LinearLayout; +import androidx.annotation.VisibleForTesting; + import com.android.settingslib.R; /** diff --git a/src/com/android/settings/widget/CardPreference.java b/src/com/android/settings/widget/CardPreference.java new file mode 100644 index 0000000000..20ea7109e4 --- /dev/null +++ b/src/com/android/settings/widget/CardPreference.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2019 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.util.AttributeSet; + +import androidx.preference.Preference; + +import com.android.settings.R; + +import com.google.android.material.card.MaterialCardView; + +/** + * Preference that wrapped by {@link MaterialCardView}, only support to set icon, title and summary + */ +public class CardPreference extends Preference { + public CardPreference(Context context) { + this(context, null /* attrs */); + } + + public CardPreference(Context context, AttributeSet attrs) { + super(context, attrs, R.attr.cardPreferenceStyle); + } +} diff --git a/src/com/android/settings/widget/ChartDataUsageView.java b/src/com/android/settings/widget/ChartDataUsageView.java deleted file mode 100644 index 2d9fe6fdcd..0000000000 --- a/src/com/android/settings/widget/ChartDataUsageView.java +++ /dev/null @@ -1,606 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settings.widget; - -import android.content.Context; -import android.content.res.Resources; -import android.net.NetworkPolicy; -import android.net.NetworkStatsHistory; -import android.net.TrafficStats; -import android.os.Handler; -import android.os.Message; -import android.text.Spannable; -import android.text.SpannableStringBuilder; -import android.text.TextUtils; -import android.text.format.DateUtils; -import android.text.format.Formatter; -import android.text.format.Formatter.BytesResult; -import android.text.format.Time; -import android.util.AttributeSet; -import android.util.MathUtils; -import android.view.MotionEvent; -import android.view.View; - -import com.android.settings.R; -import com.android.settings.widget.ChartSweepView.OnSweepListener; - -import java.util.Arrays; -import java.util.Calendar; -import java.util.Objects; - -import static android.net.TrafficStats.MB_IN_BYTES; - -/** - * Specific {@link ChartView} that displays {@link ChartNetworkSeriesView} along - * with {@link ChartSweepView} for inspection ranges and warning/limits. - */ -public class ChartDataUsageView extends ChartView { - - private static final int MSG_UPDATE_AXIS = 100; - private static final long DELAY_MILLIS = 250; - - private ChartGridView mGrid; - private ChartNetworkSeriesView mSeries; - private ChartNetworkSeriesView mDetailSeries; - - private NetworkStatsHistory mHistory; - - private ChartSweepView mSweepWarning; - private ChartSweepView mSweepLimit; - - private long mInspectStart; - private long mInspectEnd; - - private Handler mHandler; - - /** Current maximum value of {@link #mVert}. */ - private long mVertMax; - - public interface DataUsageChartListener { - public void onWarningChanged(); - public void onLimitChanged(); - public void requestWarningEdit(); - public void requestLimitEdit(); - } - - private DataUsageChartListener mListener; - - public ChartDataUsageView(Context context) { - this(context, null, 0); - } - - public ChartDataUsageView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public ChartDataUsageView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - init(new TimeAxis(), new InvertedChartAxis(new DataAxis())); - - mHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - final ChartSweepView sweep = (ChartSweepView) msg.obj; - updateVertAxisBounds(sweep); - updateEstimateVisible(); - - // we keep dispatching repeating updates until sweep is dropped - sendUpdateAxisDelayed(sweep, true); - } - }; - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - - mGrid = (ChartGridView) findViewById(R.id.grid); - mSeries = (ChartNetworkSeriesView) findViewById(R.id.series); - mDetailSeries = (ChartNetworkSeriesView) findViewById(R.id.detail_series); - mDetailSeries.setVisibility(View.GONE); - - mSweepLimit = (ChartSweepView) findViewById(R.id.sweep_limit); - mSweepWarning = (ChartSweepView) findViewById(R.id.sweep_warning); - - // prevent sweeps from crossing each other - mSweepWarning.setValidRangeDynamic(null, mSweepLimit); - mSweepLimit.setValidRangeDynamic(mSweepWarning, null); - - // mark neighbors for checking touch events against - mSweepLimit.setNeighbors(mSweepWarning); - mSweepWarning.setNeighbors(mSweepLimit); - - mSweepWarning.addOnSweepListener(mVertListener); - mSweepLimit.addOnSweepListener(mVertListener); - - mSweepWarning.setDragInterval(5 * MB_IN_BYTES); - mSweepLimit.setDragInterval(5 * MB_IN_BYTES); - - // tell everyone about our axis - mGrid.init(mHoriz, mVert); - mSeries.init(mHoriz, mVert); - mDetailSeries.init(mHoriz, mVert); - mSweepWarning.init(mVert); - mSweepLimit.init(mVert); - - setActivated(false); - } - - public void setListener(DataUsageChartListener listener) { - mListener = listener; - } - - public void bindNetworkStats(NetworkStatsHistory stats) { - mSeries.bindNetworkStats(stats); - mHistory = stats; - updateVertAxisBounds(null); - updateEstimateVisible(); - updatePrimaryRange(); - requestLayout(); - } - - public void bindDetailNetworkStats(NetworkStatsHistory stats) { - mDetailSeries.bindNetworkStats(stats); - mDetailSeries.setVisibility(stats != null ? View.VISIBLE : View.GONE); - if (mHistory != null) { - mDetailSeries.setEndTime(mHistory.getEnd()); - } - updateVertAxisBounds(null); - updateEstimateVisible(); - updatePrimaryRange(); - requestLayout(); - } - - public void bindNetworkPolicy(NetworkPolicy policy) { - if (policy == null) { - mSweepLimit.setVisibility(View.INVISIBLE); - mSweepLimit.setValue(-1); - mSweepWarning.setVisibility(View.INVISIBLE); - mSweepWarning.setValue(-1); - return; - } - - if (policy.limitBytes != NetworkPolicy.LIMIT_DISABLED) { - mSweepLimit.setVisibility(View.VISIBLE); - mSweepLimit.setEnabled(true); - mSweepLimit.setValue(policy.limitBytes); - } else { - mSweepLimit.setVisibility(View.INVISIBLE); - mSweepLimit.setEnabled(false); - mSweepLimit.setValue(-1); - } - - if (policy.warningBytes != NetworkPolicy.WARNING_DISABLED) { - mSweepWarning.setVisibility(View.VISIBLE); - mSweepWarning.setValue(policy.warningBytes); - } else { - mSweepWarning.setVisibility(View.INVISIBLE); - mSweepWarning.setValue(-1); - } - - updateVertAxisBounds(null); - requestLayout(); - invalidate(); - } - - /** - * Update {@link #mVert} to both show data from {@link NetworkStatsHistory} - * and controls from {@link NetworkPolicy}. - */ - private void updateVertAxisBounds(ChartSweepView activeSweep) { - final long max = mVertMax; - - long newMax = 0; - if (activeSweep != null) { - final int adjustAxis = activeSweep.shouldAdjustAxis(); - if (adjustAxis > 0) { - // hovering around upper edge, grow axis - newMax = max * 11 / 10; - } else if (adjustAxis < 0) { - // hovering around lower edge, shrink axis - newMax = max * 9 / 10; - } else { - newMax = max; - } - } - - // always show known data and policy lines - final long maxSweep = Math.max(mSweepWarning.getValue(), mSweepLimit.getValue()); - final long maxSeries = Math.max(mSeries.getMaxVisible(), mDetailSeries.getMaxVisible()); - final long maxVisible = Math.max(maxSeries, maxSweep) * 12 / 10; - final long maxDefault = Math.max(maxVisible, 50 * MB_IN_BYTES); - newMax = Math.max(maxDefault, newMax); - - // only invalidate when vertMax actually changed - if (newMax != mVertMax) { - mVertMax = newMax; - - final boolean changed = mVert.setBounds(0L, newMax); - mSweepWarning.setValidRange(0L, newMax); - mSweepLimit.setValidRange(0L, newMax); - - if (changed) { - mSeries.invalidatePath(); - mDetailSeries.invalidatePath(); - } - - mGrid.invalidate(); - - // since we just changed axis, make sweep recalculate its value - if (activeSweep != null) { - activeSweep.updateValueFromPosition(); - } - - // layout other sweeps to match changed axis - // TODO: find cleaner way of doing this, such as requesting full - // layout and making activeSweep discard its tracking MotionEvent. - if (mSweepLimit != activeSweep) { - layoutSweep(mSweepLimit); - } - if (mSweepWarning != activeSweep) { - layoutSweep(mSweepWarning); - } - } - } - - /** - * Control {@link ChartNetworkSeriesView#setEstimateVisible(boolean)} based - * on how close estimate comes to {@link #mSweepWarning}. - */ - private void updateEstimateVisible() { - final long maxEstimate = mSeries.getMaxEstimate(); - - // show estimate when near warning/limit - long interestLine = Long.MAX_VALUE; - if (mSweepWarning.isEnabled()) { - interestLine = mSweepWarning.getValue(); - } else if (mSweepLimit.isEnabled()) { - interestLine = mSweepLimit.getValue(); - } - - if (interestLine < 0) { - interestLine = Long.MAX_VALUE; - } - - final boolean estimateVisible = (maxEstimate >= interestLine * 7 / 10); - mSeries.setEstimateVisible(estimateVisible); - } - - private void sendUpdateAxisDelayed(ChartSweepView sweep, boolean force) { - if (force || !mHandler.hasMessages(MSG_UPDATE_AXIS, sweep)) { - mHandler.sendMessageDelayed( - mHandler.obtainMessage(MSG_UPDATE_AXIS, sweep), DELAY_MILLIS); - } - } - - private void clearUpdateAxisDelayed(ChartSweepView sweep) { - mHandler.removeMessages(MSG_UPDATE_AXIS, sweep); - } - - private OnSweepListener mVertListener = new OnSweepListener() { - @Override - public void onSweep(ChartSweepView sweep, boolean sweepDone) { - if (sweepDone) { - clearUpdateAxisDelayed(sweep); - updateEstimateVisible(); - - if (sweep == mSweepWarning && mListener != null) { - mListener.onWarningChanged(); - } else if (sweep == mSweepLimit && mListener != null) { - mListener.onLimitChanged(); - } - } else { - // while moving, kick off delayed grow/shrink axis updates - sendUpdateAxisDelayed(sweep, false); - } - } - - @Override - public void requestEdit(ChartSweepView sweep) { - if (sweep == mSweepWarning && mListener != null) { - mListener.requestWarningEdit(); - } else if (sweep == mSweepLimit && mListener != null) { - mListener.requestLimitEdit(); - } - } - }; - - @Override - public boolean onTouchEvent(MotionEvent event) { - if (isActivated()) return false; - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: { - return true; - } - case MotionEvent.ACTION_UP: { - setActivated(true); - return true; - } - default: { - return false; - } - } - } - - public long getInspectStart() { - return mInspectStart; - } - - public long getInspectEnd() { - return mInspectEnd; - } - - public long getWarningBytes() { - return mSweepWarning.getLabelValue(); - } - - public long getLimitBytes() { - return mSweepLimit.getLabelValue(); - } - - /** - * Set the exact time range that should be displayed, updating how - * {@link ChartNetworkSeriesView} paints. Moves inspection ranges to be the - * last "week" of available data, without triggering listener events. - */ - public void setVisibleRange(long visibleStart, long visibleEnd) { - final boolean changed = mHoriz.setBounds(visibleStart, visibleEnd); - mGrid.setBounds(visibleStart, visibleEnd); - mSeries.setBounds(visibleStart, visibleEnd); - mDetailSeries.setBounds(visibleStart, visibleEnd); - - mInspectStart = visibleStart; - mInspectEnd = visibleEnd; - - requestLayout(); - if (changed) { - mSeries.invalidatePath(); - mDetailSeries.invalidatePath(); - } - - updateVertAxisBounds(null); - updateEstimateVisible(); - updatePrimaryRange(); - } - - private void updatePrimaryRange() { - // prefer showing primary range on detail series, when available - if (mDetailSeries.getVisibility() == View.VISIBLE) { - mSeries.setSecondary(true); - } else { - mSeries.setSecondary(false); - } - } - - public static class TimeAxis implements ChartAxis { - private static final int FIRST_DAY_OF_WEEK = Calendar.getInstance().getFirstDayOfWeek() - 1; - - private long mMin; - private long mMax; - private float mSize; - - public TimeAxis() { - final long currentTime = System.currentTimeMillis(); - setBounds(currentTime - DateUtils.DAY_IN_MILLIS * 30, currentTime); - } - - @Override - public int hashCode() { - return Objects.hash(mMin, mMax, mSize); - } - - @Override - public boolean setBounds(long min, long max) { - if (mMin != min || mMax != max) { - mMin = min; - mMax = max; - return true; - } else { - return false; - } - } - - @Override - public boolean setSize(float size) { - if (mSize != size) { - mSize = size; - return true; - } else { - return false; - } - } - - @Override - public float convertToPoint(long value) { - return (mSize * (value - mMin)) / (mMax - mMin); - } - - @Override - public long convertToValue(float point) { - return (long) (mMin + ((point * (mMax - mMin)) / mSize)); - } - - @Override - public long buildLabel(Resources res, SpannableStringBuilder builder, long value) { - // TODO: convert to better string - builder.replace(0, builder.length(), Long.toString(value)); - return value; - } - - @Override - public float[] getTickPoints() { - final float[] ticks = new float[32]; - int i = 0; - - // tick mark for first day of each week - final Time time = new Time(); - time.set(mMax); - time.monthDay -= time.weekDay - FIRST_DAY_OF_WEEK; - time.hour = time.minute = time.second = 0; - - time.normalize(true); - long timeMillis = time.toMillis(true); - while (timeMillis > mMin) { - if (timeMillis <= mMax) { - ticks[i++] = convertToPoint(timeMillis); - } - time.monthDay -= 7; - time.normalize(true); - timeMillis = time.toMillis(true); - } - - return Arrays.copyOf(ticks, i); - } - - @Override - public int shouldAdjustAxis(long value) { - // time axis never adjusts - return 0; - } - } - - public static class DataAxis implements ChartAxis { - private long mMin; - private long mMax; - private float mSize; - - private static final boolean LOG_SCALE = false; - - @Override - public int hashCode() { - return Objects.hash(mMin, mMax, mSize); - } - - @Override - public boolean setBounds(long min, long max) { - if (mMin != min || mMax != max) { - mMin = min; - mMax = max; - return true; - } else { - return false; - } - } - - @Override - public boolean setSize(float size) { - if (mSize != size) { - mSize = size; - return true; - } else { - return false; - } - } - - @Override - public float convertToPoint(long value) { - if (LOG_SCALE) { - // derived polynomial fit to make lower values more visible - final double normalized = ((double) value - mMin) / (mMax - mMin); - final double fraction = Math.pow(10, - 0.36884343106175121463 * Math.log10(normalized) + -0.04328199452018252624); - return (float) (fraction * mSize); - } else { - return (mSize * (value - mMin)) / (mMax - mMin); - } - } - - @Override - public long convertToValue(float point) { - if (LOG_SCALE) { - final double normalized = point / mSize; - final double fraction = 1.3102228476089056629 - * Math.pow(normalized, 2.7111774693164631640); - return (long) (mMin + (fraction * (mMax - mMin))); - } else { - return (long) (mMin + ((point * (mMax - mMin)) / mSize)); - } - } - - private static final Object sSpanSize = new Object(); - private static final Object sSpanUnit = new Object(); - - @Override - public long buildLabel(Resources res, SpannableStringBuilder builder, long value) { - value = MathUtils.constrain(value, 0, TrafficStats.TB_IN_BYTES); - final BytesResult result = Formatter.formatBytes(res, value, - Formatter.FLAG_SHORTER | Formatter.FLAG_CALCULATE_ROUNDED); - setText(builder, sSpanSize, result.value, "^1"); - setText(builder, sSpanUnit, result.units, "^2"); - return result.roundedBytes; - } - - @Override - public float[] getTickPoints() { - final long range = mMax - mMin; - - // target about 16 ticks on screen, rounded to nearest power of 2 - final long tickJump = roundUpToPowerOfTwo(range / 16); - final int tickCount = (int) (range / tickJump); - final float[] tickPoints = new float[tickCount]; - long value = mMin; - for (int i = 0; i < tickPoints.length; i++) { - tickPoints[i] = convertToPoint(value); - value += tickJump; - } - - return tickPoints; - } - - @Override - public int shouldAdjustAxis(long value) { - final float point = convertToPoint(value); - if (point < mSize * 0.1) { - return -1; - } else if (point > mSize * 0.85) { - return 1; - } else { - return 0; - } - } - } - - private static void setText( - SpannableStringBuilder builder, Object key, CharSequence text, String bootstrap) { - int start = builder.getSpanStart(key); - int end = builder.getSpanEnd(key); - if (start == -1) { - start = TextUtils.indexOf(builder, bootstrap); - end = start + bootstrap.length(); - builder.setSpan(key, start, end, Spannable.SPAN_INCLUSIVE_INCLUSIVE); - } - builder.replace(start, end, text); - } - - private static long roundUpToPowerOfTwo(long i) { - // NOTE: borrowed from Hashtable.roundUpToPowerOfTwo() - - i--; // If input is a power of two, shift its high-order bit right - - // "Smear" the high-order bit all the way to the right - i |= i >>> 1; - i |= i >>> 2; - i |= i >>> 4; - i |= i >>> 8; - i |= i >>> 16; - i |= i >>> 32; - - i++; - - return i > 0 ? i : Long.MAX_VALUE; - } -} diff --git a/src/com/android/settings/widget/ChartGridView.java b/src/com/android/settings/widget/ChartGridView.java index b456a49733..de794823ce 100644 --- a/src/com/android/settings/widget/ChartGridView.java +++ b/src/com/android/settings/widget/ChartGridView.java @@ -16,6 +16,8 @@ package com.android.settings.widget; +import static com.android.settings.Utils.formatDateRange; + import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Resources; @@ -32,8 +34,6 @@ import android.view.View; import com.android.internal.util.Preconditions; import com.android.settings.R; -import static com.android.settings.Utils.formatDateRange; - /** * Background of {@link ChartView} that renders grid lines as requested by * {@link ChartAxis#getTickPoints()}. diff --git a/src/com/android/settings/widget/ChartNetworkSeriesView.java b/src/com/android/settings/widget/ChartNetworkSeriesView.java deleted file mode 100644 index 07c147924f..0000000000 --- a/src/com/android/settings/widget/ChartNetworkSeriesView.java +++ /dev/null @@ -1,340 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settings.widget; - -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.DashPathEffect; -import android.graphics.Paint; -import android.graphics.Paint.Style; -import android.graphics.Path; -import android.graphics.RectF; -import android.net.NetworkStatsHistory; -import android.util.AttributeSet; -import android.util.Log; -import android.view.View; - -import com.android.internal.util.Preconditions; -import com.android.settings.R; - -import static android.text.format.DateUtils.DAY_IN_MILLIS; -import static android.text.format.DateUtils.WEEK_IN_MILLIS; - -/** - * {@link NetworkStatsHistory} series to render inside a {@link ChartView}, - * using {@link ChartAxis} to map into screen coordinates. - */ -public class ChartNetworkSeriesView extends View { - private static final String TAG = "ChartNetworkSeriesView"; - private static final boolean LOGD = false; - - private static final boolean ESTIMATE_ENABLED = false; - - private ChartAxis mHoriz; - private ChartAxis mVert; - - private Paint mPaintStroke; - private Paint mPaintFill; - private Paint mPaintFillSecondary; - private Paint mPaintEstimate; - - private NetworkStatsHistory mStats; - - private Path mPathStroke; - private Path mPathFill; - private Path mPathEstimate; - - private int mSafeRegion; - - private long mStart; - private long mEnd; - - /** Series will be extended to reach this end time. */ - private long mEndTime = Long.MIN_VALUE; - - private boolean mPathValid = false; - private boolean mEstimateVisible = false; - private boolean mSecondary = false; - - private long mMax; - private long mMaxEstimate; - - public ChartNetworkSeriesView(Context context) { - this(context, null, 0); - } - - public ChartNetworkSeriesView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public ChartNetworkSeriesView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - final TypedArray a = context.obtainStyledAttributes( - attrs, R.styleable.ChartNetworkSeriesView, defStyle, 0); - - final int stroke = a.getColor(R.styleable.ChartNetworkSeriesView_strokeColor, Color.RED); - final int fill = a.getColor(R.styleable.ChartNetworkSeriesView_fillColor, Color.RED); - final int fillSecondary = a.getColor( - R.styleable.ChartNetworkSeriesView_fillColorSecondary, Color.RED); - final int safeRegion = a.getDimensionPixelSize( - R.styleable.ChartNetworkSeriesView_safeRegion, 0); - - setChartColor(stroke, fill, fillSecondary); - setSafeRegion(safeRegion); - setWillNotDraw(false); - - a.recycle(); - - mPathStroke = new Path(); - mPathFill = new Path(); - mPathEstimate = new Path(); - } - - void init(ChartAxis horiz, ChartAxis vert) { - mHoriz = Preconditions.checkNotNull(horiz, "missing horiz"); - mVert = Preconditions.checkNotNull(vert, "missing vert"); - } - - public void setChartColor(int stroke, int fill, int fillSecondary) { - mPaintStroke = new Paint(); - mPaintStroke.setStrokeWidth(4.0f * getResources().getDisplayMetrics().density); - mPaintStroke.setColor(stroke); - mPaintStroke.setStyle(Style.STROKE); - mPaintStroke.setAntiAlias(true); - - mPaintFill = new Paint(); - mPaintFill.setColor(fill); - mPaintFill.setStyle(Style.FILL); - mPaintFill.setAntiAlias(true); - - mPaintFillSecondary = new Paint(); - mPaintFillSecondary.setColor(fillSecondary); - mPaintFillSecondary.setStyle(Style.FILL); - mPaintFillSecondary.setAntiAlias(true); - - mPaintEstimate = new Paint(); - mPaintEstimate.setStrokeWidth(3.0f); - mPaintEstimate.setColor(fillSecondary); - mPaintEstimate.setStyle(Style.STROKE); - mPaintEstimate.setAntiAlias(true); - mPaintEstimate.setPathEffect(new DashPathEffect(new float[] { 10, 10 }, 1)); - } - - public void setSafeRegion(int safeRegion) { - mSafeRegion = safeRegion; - } - - public void bindNetworkStats(NetworkStatsHistory stats) { - mStats = stats; - invalidatePath(); - invalidate(); - } - - public void setBounds(long start, long end) { - mStart = start; - mEnd = end; - } - - public void setSecondary(boolean secondary) { - mSecondary = secondary; - } - - public void invalidatePath() { - mPathValid = false; - mMax = 0; - invalidate(); - } - - /** - * Erase any existing {@link Path} and generate series outline based on - * currently bound {@link NetworkStatsHistory} data. - */ - private void generatePath() { - if (LOGD) Log.d(TAG, "generatePath()"); - - mMax = 0; - mPathStroke.reset(); - mPathFill.reset(); - mPathEstimate.reset(); - mPathValid = true; - - // bail when not enough stats to render - if (mStats == null || mStats.size() < 2) { - return; - } - - final int width = getWidth(); - final int height = getHeight(); - - boolean started = false; - float lastX = 0; - float lastY = height; - long lastTime = mHoriz.convertToValue(lastX); - - // move into starting position - mPathStroke.moveTo(lastX, lastY); - mPathFill.moveTo(lastX, lastY); - - // TODO: count fractional data from first bucket crossing start; - // currently it only accepts first full bucket. - - long totalData = 0; - - NetworkStatsHistory.Entry entry = null; - - final int start = mStats.getIndexBefore(mStart); - final int end = mStats.getIndexAfter(mEnd); - for (int i = start; i <= end; i++) { - entry = mStats.getValues(i, entry); - - final long startTime = entry.bucketStart; - final long endTime = startTime + entry.bucketDuration; - - final float startX = mHoriz.convertToPoint(startTime); - final float endX = mHoriz.convertToPoint(endTime); - - // skip until we find first stats on screen - if (endX < 0) continue; - - // increment by current bucket total - totalData += entry.rxBytes + entry.txBytes; - - final float startY = lastY; - final float endY = mVert.convertToPoint(totalData); - - if (lastTime != startTime) { - // gap in buckets; line to start of current bucket - mPathStroke.lineTo(startX, startY); - mPathFill.lineTo(startX, startY); - } - - // always draw to end of current bucket - mPathStroke.lineTo(endX, endY); - mPathFill.lineTo(endX, endY); - - lastX = endX; - lastY = endY; - lastTime = endTime; - } - - // when data falls short, extend to requested end time - if (lastTime < mEndTime) { - lastX = mHoriz.convertToPoint(mEndTime); - - mPathStroke.lineTo(lastX, lastY); - mPathFill.lineTo(lastX, lastY); - } - - if (LOGD) { - final RectF bounds = new RectF(); - mPathFill.computeBounds(bounds, true); - Log.d(TAG, "onLayout() rendered with bounds=" + bounds.toString() + " and totalData=" - + totalData); - } - - // drop to bottom of graph from current location - mPathFill.lineTo(lastX, height); - mPathFill.lineTo(0, height); - - mMax = totalData; - - if (ESTIMATE_ENABLED) { - // build estimated data - mPathEstimate.moveTo(lastX, lastY); - - final long now = System.currentTimeMillis(); - final long bucketDuration = mStats.getBucketDuration(); - - // long window is average over two weeks - entry = mStats.getValues(lastTime - WEEK_IN_MILLIS * 2, lastTime, now, entry); - final long longWindow = (entry.rxBytes + entry.txBytes) * bucketDuration - / entry.bucketDuration; - - long futureTime = 0; - while (lastX < width) { - futureTime += bucketDuration; - - // short window is day average last week - final long lastWeekTime = lastTime - WEEK_IN_MILLIS + (futureTime % WEEK_IN_MILLIS); - entry = mStats.getValues(lastWeekTime - DAY_IN_MILLIS, lastWeekTime, now, entry); - final long shortWindow = (entry.rxBytes + entry.txBytes) * bucketDuration - / entry.bucketDuration; - - totalData += (longWindow * 7 + shortWindow * 3) / 10; - - lastX = mHoriz.convertToPoint(lastTime + futureTime); - lastY = mVert.convertToPoint(totalData); - - mPathEstimate.lineTo(lastX, lastY); - } - - mMaxEstimate = totalData; - } - - invalidate(); - } - - public void setEndTime(long endTime) { - mEndTime = endTime; - } - - public void setEstimateVisible(boolean estimateVisible) { - mEstimateVisible = ESTIMATE_ENABLED ? estimateVisible : false; - invalidate(); - } - - public long getMaxEstimate() { - return mMaxEstimate; - } - - public long getMaxVisible() { - final long maxVisible = mEstimateVisible ? mMaxEstimate : mMax; - if (maxVisible <= 0 && mStats != null) { - // haven't generated path yet; fall back to raw data - final NetworkStatsHistory.Entry entry = mStats.getValues(mStart, mEnd, null); - return entry.rxBytes + entry.txBytes; - } else { - return maxVisible; - } - } - - @Override - protected void onDraw(Canvas canvas) { - int save; - - if (!mPathValid) { - generatePath(); - } - - if (mEstimateVisible) { - save = canvas.save(); - canvas.clipRect(0, 0, getWidth(), getHeight()); - canvas.drawPath(mPathEstimate, mPaintEstimate); - canvas.restoreToCount(save); - } - - final Paint paintFill = mSecondary ? mPaintFillSecondary : mPaintFill; - - save = canvas.save(); - canvas.clipRect(mSafeRegion, 0, getWidth(), getHeight() - mSafeRegion); - canvas.drawPath(mPathFill, paintFill); - canvas.restoreToCount(save); - } -} diff --git a/src/com/android/settings/widget/ChartView.java b/src/com/android/settings/widget/ChartView.java index 30284bc384..768a717301 100644 --- a/src/com/android/settings/widget/ChartView.java +++ b/src/com/android/settings/widget/ChartView.java @@ -30,8 +30,8 @@ import com.android.settings.R; /** * Container for two-dimensional chart, drawn with a combination of - * {@link ChartGridView}, {@link ChartNetworkSeriesView} and {@link ChartSweepView} - * children. The entire chart uses {@link ChartAxis} to map between raw values + * {@link ChartGridView} and {@link ChartSweepView} children. The entire chart uses + * {@link ChartAxis} to map between raw values * and screen coordinates. */ public class ChartView extends FrameLayout { @@ -112,13 +112,7 @@ public class ChartView extends FrameLayout { parentRect.set(mContent); - if (child instanceof ChartNetworkSeriesView) { - // series are always laid out to fill entire graph area - // TODO: handle scrolling for series larger than content area - Gravity.apply(params.gravity, width, height, parentRect, childRect); - child.layout(childRect.left, childRect.top, childRect.right, childRect.bottom); - - } else if (child instanceof ChartGridView) { + if (child instanceof ChartGridView) { // Grid uses some extra room for labels Gravity.apply(params.gravity, width, height, parentRect, childRect); child.layout(childRect.left, childRect.top, childRect.right, diff --git a/src/com/android/settings/widget/DisabledCheckBoxPreference.java b/src/com/android/settings/widget/DisabledCheckBoxPreference.java index 3966864b1e..15c17dc88d 100644 --- a/src/com/android/settings/widget/DisabledCheckBoxPreference.java +++ b/src/com/android/settings/widget/DisabledCheckBoxPreference.java @@ -18,10 +18,12 @@ package com.android.settings.widget; import android.content.Context; import android.content.res.TypedArray; -import androidx.preference.CheckBoxPreference; -import androidx.preference.PreferenceViewHolder; import android.util.AttributeSet; import android.view.View; +import android.widget.TextView; + +import androidx.preference.CheckBoxPreference; +import androidx.preference.PreferenceViewHolder; /** * A CheckboxPreference that can disable its checkbox separate from its text. @@ -88,6 +90,12 @@ public class DisabledCheckBoxPreference extends CheckBoxPreference { mCheckBox = holder.findViewById(android.R.id.checkbox); enableCheckbox(mEnabledCheckBox); + + TextView title = (TextView) holder.findViewById(android.R.id.title); + if (title != null) { + title.setSingleLine(false); + title.setMaxLines(2); + } } @Override diff --git a/src/com/android/settings/widget/DonutView.java b/src/com/android/settings/widget/DonutView.java index 32b994aefa..13716636d9 100644 --- a/src/com/android/settings/widget/DonutView.java +++ b/src/com/android/settings/widget/DonutView.java @@ -25,7 +25,6 @@ import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.Typeface; import android.icu.text.DecimalFormatSymbols; -import androidx.annotation.ColorRes; import android.text.Layout; import android.text.Spannable; import android.text.SpannableString; @@ -37,7 +36,9 @@ import android.text.style.RelativeSizeSpan; import android.util.AttributeSet; import android.view.View; -import com.android.internal.annotations.VisibleForTesting; +import androidx.annotation.ColorRes; +import androidx.annotation.VisibleForTesting; + import com.android.settings.R; import com.android.settings.Utils; @@ -70,7 +71,8 @@ public class DonutView extends View { public DonutView(Context context, AttributeSet attrs) { super(context, attrs); mMeterBackgroundColor = context.getColor(R.color.meter_background_color); - mMeterConsumedColor = Utils.getDefaultColor(mContext, R.color.meter_consumed_color); + mMeterConsumedColor = Utils.getColorStateListDefaultColor(mContext, + R.color.meter_consumed_color); boolean applyColorAccent = true; Resources resources = context.getResources(); mStrokeWidth = resources.getDimension(R.dimen.storage_donut_thickness); @@ -107,7 +109,7 @@ public class DonutView extends View { if (applyColorAccent) { final ColorFilter mAccentColorFilter = new PorterDuffColorFilter( - Utils.getColorAttr(context, android.R.attr.colorAccent), + Utils.getColorAttrDefaultColor(context, android.R.attr.colorAccent), PorterDuff.Mode.SRC_IN); mBackgroundCircle.setColorFilter(mAccentColorFilter); mFilledArc.setColorFilter(mAccentColorFilter); @@ -120,7 +122,7 @@ public class DonutView extends View { : Paint.BIDI_RTL; mTextPaint = new TextPaint(); - mTextPaint.setColor(Utils.getColorAccent(getContext())); + mTextPaint.setColor(Utils.getColorAccentDefaultColor(getContext())); mTextPaint.setAntiAlias(true); mTextPaint.setTextSize( resources.getDimension(R.dimen.storage_donut_view_label_text_size)); @@ -128,7 +130,7 @@ public class DonutView extends View { mTextPaint.setBidiFlags(bidiFlags); mBigNumberPaint = new TextPaint(); - mBigNumberPaint.setColor(Utils.getColorAccent(getContext())); + mBigNumberPaint.setColor(Utils.getColorAccentDefaultColor(getContext())); mBigNumberPaint.setAntiAlias(true); mBigNumberPaint.setTextSize( resources.getDimension(R.dimen.storage_donut_view_percent_text_size)); @@ -206,7 +208,7 @@ public class DonutView extends View { R.dimen.storage_donut_view_shrunken_label_text_size)); } setContentDescription(getContext().getString( - R.string.join_many_items_middle, mPercentString, mFullString)); + R.string.join_two_unrelated_items, mPercentString, mFullString)); invalidate(); } diff --git a/src/com/android/settings/widget/DotsPageIndicator.java b/src/com/android/settings/widget/DotsPageIndicator.java index 096338bbbf..88e695105a 100644 --- a/src/com/android/settings/widget/DotsPageIndicator.java +++ b/src/com/android/settings/widget/DotsPageIndicator.java @@ -30,10 +30,12 @@ import android.graphics.Paint; import android.graphics.Path; import android.graphics.RectF; import android.os.Build; -import androidx.viewpager.widget.ViewPager; import android.util.AttributeSet; import android.view.View; import android.view.animation.Interpolator; + +import androidx.viewpager.widget.ViewPager; + import com.android.settings.R; import java.util.Arrays; diff --git a/src/com/android/settings/notification/EmptyTextSettings.java b/src/com/android/settings/widget/EmptyTextSettings.java index 3f8ccc6309..24f0a821f4 100644 --- a/src/com/android/settings/notification/EmptyTextSettings.java +++ b/src/com/android/settings/widget/EmptyTextSettings.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.settings.notification; +package com.android.settings.widget; import android.annotation.Nullable; import android.os.Bundle; @@ -24,7 +24,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.widget.TextView; -import com.android.settings.R; + import com.android.settings.SettingsPreferenceFragment; public abstract class EmptyTextSettings extends SettingsPreferenceFragment { diff --git a/src/com/android/settings/widget/EntityHeaderController.java b/src/com/android/settings/widget/EntityHeaderController.java index 0f3a8b066f..8e31290577 100644 --- a/src/com/android/settings/widget/EntityHeaderController.java +++ b/src/com/android/settings/widget/EntityHeaderController.java @@ -16,14 +16,11 @@ package com.android.settings.widget; -import static com.android.internal.logging.nano.MetricsProto.MetricsEvent - .ACTION_OPEN_APP_NOTIFICATION_SETTING; - import android.annotation.IdRes; import android.annotation.UserIdInt; import android.app.ActionBar; import android.app.Activity; -import android.app.Fragment; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; @@ -31,9 +28,6 @@ import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.UserHandle; -import androidx.annotation.IntDef; -import androidx.annotation.VisibleForTesting; -import androidx.recyclerview.widget.RecyclerView; import android.text.TextUtils; import android.util.IconDrawableFactory; import android.util.Log; @@ -43,14 +37,20 @@ import android.widget.ImageButton; import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.IntDef; +import androidx.annotation.VisibleForTesting; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.RecyclerView; + import com.android.settings.R; import com.android.settings.Utils; import com.android.settings.applications.AppInfoBase; -import com.android.settings.applications.LayoutPreference; import com.android.settings.applications.appinfo.AppInfoDashboardFragment; import com.android.settings.overlay.FeatureFactory; import com.android.settingslib.applications.ApplicationsState; import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.widget.ActionBarShadowController; +import com.android.settingslib.widget.LayoutPreference; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -59,12 +59,12 @@ public class EntityHeaderController { @IntDef({ActionType.ACTION_NONE, ActionType.ACTION_NOTIF_PREFERENCE, - ActionType.ACTION_DND_RULE_PREFERENCE,}) + ActionType.ACTION_EDIT_PREFERENCE,}) @Retention(RetentionPolicy.SOURCE) public @interface ActionType { int ACTION_NONE = 0; int ACTION_NOTIF_PREFERENCE = 1; - int ACTION_DND_RULE_PREFERENCE = 2; + int ACTION_EDIT_PREFERENCE = 2; } public static final String PREF_KEY_APP_HEADER = "pref_app_header"; @@ -97,7 +97,7 @@ public class EntityHeaderController { private boolean mIsInstantApp; - private View.OnClickListener mEditRuleNameOnClickListener; + private View.OnClickListener mEditOnClickListener; /** * Creates a new instance of the controller. @@ -222,8 +222,8 @@ public class EntityHeaderController { return this; } - public EntityHeaderController setEditZenRuleNameListener(View.OnClickListener listener) { - this.mEditRuleNameOnClickListener = listener; + public EntityHeaderController setEditListener(View.OnClickListener listener) { + this.mEditOnClickListener = listener; return this; } @@ -236,6 +236,7 @@ public class EntityHeaderController { pref.setOrder(-1000); pref.setSelectable(false); pref.setKey(PREF_KEY_APP_HEADER); + pref.setAllowDividerBelow(true); return pref; } @@ -317,10 +318,11 @@ public class EntityHeaderController { return this; } actionBar.setBackgroundDrawable( - new ColorDrawable(Utils.getColorAttr(activity, android.R.attr.colorPrimary))); + new ColorDrawable( + Utils.getColorAttrDefaultColor(activity, android.R.attr.colorPrimaryDark))); actionBar.setElevation(0); if (mRecyclerView != null && mLifecycle != null) { - ActionBarShadowController.attachToRecyclerView(mActivity, mLifecycle, mRecyclerView); + ActionBarShadowController.attachToView(mActivity, mLifecycle, mRecyclerView); } return this; @@ -339,13 +341,13 @@ public class EntityHeaderController { return; } switch (action) { - case ActionType.ACTION_DND_RULE_PREFERENCE: { - if (mEditRuleNameOnClickListener == null) { + case ActionType.ACTION_EDIT_PREFERENCE: { + if (mEditOnClickListener == null) { button.setVisibility(View.GONE); } else { - button.setImageResource(R.drawable.ic_mode_edit); + button.setImageResource(com.android.internal.R.drawable.ic_mode_edit); button.setVisibility(View.VISIBLE); - button.setOnClickListener(mEditRuleNameOnClickListener); + button.setOnClickListener(mEditOnClickListener); } return; } @@ -353,14 +355,13 @@ public class EntityHeaderController { if (mAppNotifPrefIntent == null) { button.setVisibility(View.GONE); } else { - button.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - FeatureFactory.getFactory(mAppContext).getMetricsFeatureProvider() - .actionWithSource(mAppContext, mMetricsCategory, - ACTION_OPEN_APP_NOTIFICATION_SETTING); - mFragment.startActivity(mAppNotifPrefIntent); - } + button.setOnClickListener(v -> { + FeatureFactory.getFactory(mAppContext).getMetricsFeatureProvider() + .action(SettingsEnums.PAGE_UNKNOWN, + SettingsEnums.ACTION_OPEN_APP_NOTIFICATION_SETTING, + mMetricsCategory, + null, 0); + mFragment.startActivity(mAppNotifPrefIntent); }); button.setVisibility(View.VISIBLE); } diff --git a/src/com/android/settings/widget/FixedLineSummaryPreference.java b/src/com/android/settings/widget/FixedLineSummaryPreference.java index 7187c7b6ee..534ab08a69 100644 --- a/src/com/android/settings/widget/FixedLineSummaryPreference.java +++ b/src/com/android/settings/widget/FixedLineSummaryPreference.java @@ -16,11 +16,13 @@ package com.android.settings.widget; import android.content.Context; import android.content.res.TypedArray; -import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; import android.text.TextUtils.TruncateAt; import android.util.AttributeSet; import android.widget.TextView; + +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + import com.android.settings.R; /** diff --git a/src/com/android/settings/widget/FloatingAppBarScrollingViewBehavior.java b/src/com/android/settings/widget/FloatingAppBarScrollingViewBehavior.java new file mode 100644 index 0000000000..23f6ccd4b9 --- /dev/null +++ b/src/com/android/settings/widget/FloatingAppBarScrollingViewBehavior.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2018 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.Color; +import android.util.AttributeSet; +import android.view.View; + +import androidx.annotation.VisibleForTesting; +import androidx.coordinatorlayout.widget.CoordinatorLayout; + +import com.google.android.material.appbar.AppBarLayout; + +/** + * This scrolling view behavior will set the background of the {@link AppBarLayout} as + * transparent and without the elevation. Also make header overlapped the scrolling child view. + */ +public class FloatingAppBarScrollingViewBehavior extends AppBarLayout.ScrollingViewBehavior { + private boolean initialized; + + public FloatingAppBarScrollingViewBehavior(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) { + boolean changed = super.onDependentViewChanged(parent, child, dependency); + if (!initialized && dependency instanceof AppBarLayout) { + initialized = true; + AppBarLayout appBarLayout = (AppBarLayout) dependency; + setAppBarLayoutTransparent(appBarLayout); + } + return changed; + } + + @VisibleForTesting + void setAppBarLayoutTransparent(AppBarLayout appBarLayout) { + appBarLayout.setBackgroundColor(Color.TRANSPARENT); + appBarLayout.setTargetElevation(0); + } + + @Override + protected boolean shouldHeaderOverlapScrollingChild() { + return true; + } +} diff --git a/src/com/android/settings/widget/GearPreference.java b/src/com/android/settings/widget/GearPreference.java index 200156b1cd..0a30100669 100644 --- a/src/com/android/settings/widget/GearPreference.java +++ b/src/com/android/settings/widget/GearPreference.java @@ -17,10 +17,11 @@ package com.android.settings.widget; import android.content.Context; -import androidx.preference.PreferenceViewHolder; import android.util.AttributeSet; import android.view.View; +import androidx.preference.PreferenceViewHolder; + import com.android.settings.R; import com.android.settingslib.RestrictedPreference; diff --git a/src/com/android/settings/widget/HighlightablePreferenceGroupAdapter.java b/src/com/android/settings/widget/HighlightablePreferenceGroupAdapter.java index 3a5548ea7a..313a204e5b 100644 --- a/src/com/android/settings/widget/HighlightablePreferenceGroupAdapter.java +++ b/src/com/android/settings/widget/HighlightablePreferenceGroupAdapter.java @@ -25,16 +25,17 @@ import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Color; import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.util.TypedValue; +import android.view.View; + import androidx.annotation.VisibleForTesting; import androidx.preference.PreferenceGroup; import androidx.preference.PreferenceGroupAdapter; import androidx.preference.PreferenceScreen; import androidx.preference.PreferenceViewHolder; import androidx.recyclerview.widget.RecyclerView; -import android.text.TextUtils; -import android.util.Log; -import android.util.TypedValue; -import android.view.View; import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; diff --git a/src/com/android/settings/widget/LabeledSeekBar.java b/src/com/android/settings/widget/LabeledSeekBar.java index 7725283e0c..14c3d7db01 100644 --- a/src/com/android/settings/widget/LabeledSeekBar.java +++ b/src/com/android/settings/widget/LabeledSeekBar.java @@ -17,12 +17,8 @@ package com.android.settings.widget; import android.content.Context; -import android.content.res.Configuration; import android.graphics.Rect; import android.os.Bundle; -import androidx.core.view.ViewCompat; -import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; -import androidx.customview.widget.ExploreByTouchHelper; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; @@ -31,6 +27,10 @@ import android.widget.RadioButton; import android.widget.RadioGroup; import android.widget.SeekBar; +import androidx.core.view.ViewCompat; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; +import androidx.customview.widget.ExploreByTouchHelper; + import java.util.List; /** diff --git a/src/com/android/settings/widget/MasterCheckBoxPreference.java b/src/com/android/settings/widget/MasterCheckBoxPreference.java index 8fe59e8b9a..48e09c9a6c 100644 --- a/src/com/android/settings/widget/MasterCheckBoxPreference.java +++ b/src/com/android/settings/widget/MasterCheckBoxPreference.java @@ -17,12 +17,13 @@ package com.android.settings.widget; import android.content.Context; -import androidx.preference.PreferenceViewHolder; import android.util.AttributeSet; import android.view.View; import android.view.View.OnClickListener; import android.widget.CheckBox; +import androidx.preference.PreferenceViewHolder; + import com.android.settings.R; import com.android.settingslib.TwoTargetPreference; diff --git a/src/com/android/settings/widget/MasterSwitchController.java b/src/com/android/settings/widget/MasterSwitchController.java index 1244d33651..91595848bd 100644 --- a/src/com/android/settings/widget/MasterSwitchController.java +++ b/src/com/android/settings/widget/MasterSwitchController.java @@ -17,7 +17,7 @@ package com.android.settings.widget; import androidx.preference.Preference; -import android.widget.Switch; + import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; /* diff --git a/src/com/android/settings/widget/MasterSwitchPreference.java b/src/com/android/settings/widget/MasterSwitchPreference.java index 765eae1293..42529cfd4d 100644 --- a/src/com/android/settings/widget/MasterSwitchPreference.java +++ b/src/com/android/settings/widget/MasterSwitchPreference.java @@ -17,12 +17,13 @@ package com.android.settings.widget; import android.content.Context; -import androidx.preference.PreferenceViewHolder; import android.util.AttributeSet; import android.view.View; import android.view.View.OnClickListener; import android.widget.Switch; +import androidx.preference.PreferenceViewHolder; + import com.android.settings.R; import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; import com.android.settingslib.TwoTargetPreference; diff --git a/src/com/android/settings/widget/RadioButtonPickerFragment.java b/src/com/android/settings/widget/RadioButtonPickerFragment.java index 8142d0bc71..8861c94a71 100644 --- a/src/com/android/settings/widget/RadioButtonPickerFragment.java +++ b/src/com/android/settings/widget/RadioButtonPickerFragment.java @@ -20,34 +20,47 @@ import android.content.Context; import android.os.Bundle; import android.os.UserHandle; import android.os.UserManager; -import androidx.annotation.LayoutRes; -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; import android.text.TextUtils; import android.util.ArrayMap; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import androidx.annotation.LayoutRes; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + import com.android.settings.R; import com.android.settings.Utils; import com.android.settings.core.InstrumentedPreferenceFragment; +import com.android.settings.core.PreferenceXmlParserUtils; +import com.android.settings.core.PreferenceXmlParserUtils.MetadataFlag; import com.android.settingslib.widget.CandidateInfo; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; import java.util.List; import java.util.Map; public abstract class RadioButtonPickerFragment extends InstrumentedPreferenceFragment implements RadioButtonPreference.OnClickListener { - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + @VisibleForTesting static final String EXTRA_FOR_WORK = "for_work"; + private static final String TAG = "RadioButtonPckrFrgmt"; + @VisibleForTesting + boolean mAppendStaticPreferences = false; private final Map<String, CandidateInfo> mCandidates = new ArrayMap<>(); protected UserManager mUserManager; protected int mUserId; + private int mIllustrationId; + private int mIllustrationPreviewId; + private VideoPreference mVideoPreference; @Override public void onAttach(Context context) { @@ -68,6 +81,19 @@ public abstract class RadioButtonPickerFragment extends InstrumentedPreferenceFr @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { super.onCreatePreferences(savedInstanceState, rootKey); + try { + // Check if the xml specifies if static preferences should go on the top or bottom + final List<Bundle> metadata = PreferenceXmlParserUtils.extractMetadata(getContext(), + getPreferenceScreenResId(), + MetadataFlag.FLAG_INCLUDE_PREF_SCREEN | + MetadataFlag.FLAG_NEED_PREF_APPEND); + mAppendStaticPreferences = metadata.get(0) + .getBoolean(PreferenceXmlParserUtils.METADATA_APPEND); + } catch (IOException e) { + Log.e(TAG, "Error trying to open xml file", e); + } catch (XmlPullParserException e) { + Log.e(TAG, "Error parsing xml", e); + } updateCandidates(); } @@ -141,7 +167,12 @@ public abstract class RadioButtonPickerFragment extends InstrumentedPreferenceFr final String systemDefaultKey = getSystemDefaultKey(); final PreferenceScreen screen = getPreferenceScreen(); screen.removeAll(); - addStaticPreferences(screen); + if (mIllustrationId != 0) { + addIllustration(screen); + } + if (!mAppendStaticPreferences) { + addStaticPreferences(screen); + } final int customLayoutResId = getRadioButtonPreferenceCustomLayoutResId(); if (shouldShowItemNone()) { @@ -167,6 +198,9 @@ public abstract class RadioButtonPickerFragment extends InstrumentedPreferenceFr } } mayCheckOnlyRadioButton(); + if (mAppendStaticPreferences) { + addStaticPreferences(screen); + } } @VisibleForTesting @@ -213,6 +247,23 @@ public abstract class RadioButtonPickerFragment extends InstrumentedPreferenceFr } } + /** + * Allows you to set an illustration at the top of this screen. Set the illustration id to 0 + * if you want to remove the illustration. + * @param illustrationId The res id for the raw of the illustration. + * @param previewId The res id for the drawable of the illustration + */ + protected void setIllustration(int illustrationId, int previewId) { + mIllustrationId = illustrationId; + mIllustrationPreviewId = previewId; + } + + private void addIllustration(PreferenceScreen screen) { + mVideoPreference = new VideoPreference(getContext()); + mVideoPreference.setVideo(mIllustrationId, mIllustrationPreviewId); + screen.addPreference(mVideoPreference); + } + protected abstract List<? extends CandidateInfo> getCandidates(); protected abstract String getDefaultKey(); diff --git a/src/com/android/settings/widget/RadioButtonPreference.java b/src/com/android/settings/widget/RadioButtonPreference.java index 9b5c170070..512fe4e5f7 100644 --- a/src/com/android/settings/widget/RadioButtonPreference.java +++ b/src/com/android/settings/widget/RadioButtonPreference.java @@ -17,14 +17,15 @@ package com.android.settings.widget; import android.content.Context; -import androidx.core.content.res.TypedArrayUtils; -import androidx.preference.CheckBoxPreference; -import androidx.preference.PreferenceViewHolder; import android.text.TextUtils; import android.util.AttributeSet; import android.view.View; import android.widget.TextView; +import androidx.core.content.res.TypedArrayUtils; +import androidx.preference.CheckBoxPreference; +import androidx.preference.PreferenceViewHolder; + import com.android.settings.R; /** @@ -43,6 +44,8 @@ public class RadioButtonPreference extends CheckBoxPreference { } private OnClickListener mListener = null; + private View appendix; + private int appendixVisibility = -1; public RadioButtonPreference(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); @@ -80,6 +83,10 @@ public class RadioButtonPreference extends CheckBoxPreference { if (summaryContainer != null) { summaryContainer.setVisibility( TextUtils.isEmpty(getSummary()) ? View.GONE : View.VISIBLE); + appendix = view.findViewById(R.id.appendix); + if (appendix != null && appendixVisibility != -1) { + appendix.setVisibility(appendixVisibility); + } } TextView title = (TextView) view.findViewById(android.R.id.title); @@ -88,4 +95,11 @@ public class RadioButtonPreference extends CheckBoxPreference { title.setMaxLines(3); } } + + public void setAppendixVisibility(int visibility) { + if (appendix != null) { + appendix.setVisibility(visibility); + } + appendixVisibility = visibility; + } } diff --git a/src/com/android/settings/widget/RadioButtonPreferenceWithExtraWidget.java b/src/com/android/settings/widget/RadioButtonPreferenceWithExtraWidget.java new file mode 100644 index 0000000000..19fd92377c --- /dev/null +++ b/src/com/android/settings/widget/RadioButtonPreferenceWithExtraWidget.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2019 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.view.View; +import android.widget.ImageView; + +import androidx.preference.PreferenceViewHolder; + +import com.android.settings.R; + +public class RadioButtonPreferenceWithExtraWidget extends RadioButtonPreference { + public static final int EXTRA_WIDGET_VISIBILITY_GONE = 0; + public static final int EXTRA_WIDGET_VISIBILITY_INFO = 1; + public static final int EXTRA_WIDGET_VISIBILITY_SETTING = 2; + + private View mExtraWidgetDivider; + private ImageView mExtraWidget; + + private int mExtraWidgetVisibility = EXTRA_WIDGET_VISIBILITY_GONE; + private View.OnClickListener mExtraWidgetOnClickListener; + + public RadioButtonPreferenceWithExtraWidget(Context context) { + super(context, null); + setLayoutResource(R.layout.preference_radio_with_extra_widget); + } + + @Override + public void onBindViewHolder(PreferenceViewHolder view) { + super.onBindViewHolder(view); + + mExtraWidget = (ImageView) view.findViewById(R.id.radio_extra_widget); + mExtraWidgetDivider = view.findViewById(R.id.radio_extra_widget_divider); + setExtraWidgetVisibility(mExtraWidgetVisibility); + + if (mExtraWidgetOnClickListener != null) { + setExtraWidgetOnClickListener(mExtraWidgetOnClickListener); + } + } + + public void setExtraWidgetVisibility(int visibility) { + mExtraWidgetVisibility = visibility; + if (mExtraWidget == null || mExtraWidgetDivider == null) { + return; + } + + if (visibility == EXTRA_WIDGET_VISIBILITY_GONE) { + mExtraWidget.setClickable(false); + mExtraWidget.setVisibility(View.GONE); + mExtraWidgetDivider.setVisibility(View.GONE); + } else { + mExtraWidget.setClickable(true); + mExtraWidget.setVisibility(View.VISIBLE); + mExtraWidgetDivider.setVisibility(View.VISIBLE); + if (mExtraWidgetVisibility == EXTRA_WIDGET_VISIBILITY_INFO) { + mExtraWidget.setImageResource(R.drawable.ic_settings_about); + mExtraWidget.setContentDescription( + getContext().getResources().getText(R.string.information_label)); + } else if (mExtraWidgetVisibility == EXTRA_WIDGET_VISIBILITY_SETTING) { + mExtraWidget.setImageResource(R.drawable.ic_settings_accent); + mExtraWidget.setContentDescription( + getContext().getResources().getText(R.string.settings_label)); + } + } + } + + public void setExtraWidgetOnClickListener(View.OnClickListener listener) { + mExtraWidgetOnClickListener = listener; + if (mExtraWidget != null) { + mExtraWidget.setEnabled(true); + mExtraWidget.setOnClickListener(listener); + } + } +}
\ No newline at end of file diff --git a/src/com/android/settings/widget/RestrictedAppPreference.java b/src/com/android/settings/widget/RestrictedAppPreference.java index 9518c693e3..8a2cc91e7d 100644 --- a/src/com/android/settings/widget/RestrictedAppPreference.java +++ b/src/com/android/settings/widget/RestrictedAppPreference.java @@ -18,15 +18,17 @@ package com.android.settings.widget; import android.content.Context; import android.os.UserHandle; -import androidx.preference.PreferenceManager; -import androidx.preference.PreferenceViewHolder; import android.text.TextUtils; import android.util.AttributeSet; import android.view.View; +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceViewHolder; + import com.android.settings.R; import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedPreferenceHelper; +import com.android.settingslib.widget.apppreference.AppPreference; /** * {@link AppPreference} that implements user restriction utilities using diff --git a/src/com/android/settings/widget/RtlCompatibleViewPager.java b/src/com/android/settings/widget/RtlCompatibleViewPager.java index ac28e7bf4c..68caaa78ea 100644 --- a/src/com/android/settings/widget/RtlCompatibleViewPager.java +++ b/src/com/android/settings/widget/RtlCompatibleViewPager.java @@ -17,12 +17,13 @@ package com.android.settings.widget; import android.content.Context; +import android.os.Parcel; import android.os.Parcelable; -import androidx.viewpager.widget.ViewPager; import android.text.TextUtils; import android.util.AttributeSet; import android.view.View; -import android.os.Parcel; + +import androidx.viewpager.widget.ViewPager; import java.util.Locale; diff --git a/src/com/android/settings/widget/ScrollToParentEditText.java b/src/com/android/settings/widget/ScrollToParentEditText.java index 705e918ea5..9d5394bb8f 100644 --- a/src/com/android/settings/widget/ScrollToParentEditText.java +++ b/src/com/android/settings/widget/ScrollToParentEditText.java @@ -21,9 +21,6 @@ import android.graphics.Rect; import android.util.AttributeSet; import android.view.View; import android.view.ViewParent; -import android.widget.EditText; - -import com.android.settings.widget.ImeAwareEditText; /** * An EditText that, instead of scrolling to itself when focused, will request scrolling to its diff --git a/src/com/android/settings/widget/SeekBarPreference.java b/src/com/android/settings/widget/SeekBarPreference.java index 2919b36ce5..bdd1ba9447 100644 --- a/src/com/android/settings/widget/SeekBarPreference.java +++ b/src/com/android/settings/widget/SeekBarPreference.java @@ -20,8 +20,6 @@ import android.content.Context; import android.content.res.TypedArray; import android.os.Parcel; import android.os.Parcelable; -import androidx.core.content.res.TypedArrayUtils; -import androidx.preference.PreferenceViewHolder; import android.text.TextUtils; import android.util.AttributeSet; import android.view.KeyEvent; @@ -30,7 +28,9 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.widget.SeekBar; import android.widget.SeekBar.OnSeekBarChangeListener; -import com.android.settings.widget.DefaultIndicatorSeekBar; +import androidx.core.content.res.TypedArrayUtils; +import androidx.preference.PreferenceViewHolder; + import com.android.settingslib.RestrictedPreference; /** @@ -69,6 +69,13 @@ public class SeekBarPreference extends RestrictedPreference com.android.internal.R.layout.preference_widget_seekbar); a.recycle(); + a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.Preference, defStyleAttr, defStyleRes); + final boolean isSelectable = a.getBoolean( + com.android.settings.R.styleable.Preference_android_selectable, false); + setSelectable(isSelectable); + a.recycle(); + setLayoutResource(layoutResId); } @@ -92,6 +99,15 @@ public class SeekBarPreference extends RestrictedPreference } @Override + public boolean isSelectable() { + if(isDisabledByAdmin()) { + return true; + } else { + return super.isSelectable(); + } + } + + @Override public void onBindViewHolder(PreferenceViewHolder view) { super.onBindViewHolder(view); view.itemView.setOnKeyListener(this); diff --git a/src/com/android/settings/widget/SettingsAppWidgetProvider.java b/src/com/android/settings/widget/SettingsAppWidgetProvider.java deleted file mode 100644 index adc386ac0c..0000000000 --- a/src/com/android/settings/widget/SettingsAppWidgetProvider.java +++ /dev/null @@ -1,954 +0,0 @@ -/* - * Copyright (C) 2009 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.app.ActivityManager; -import android.app.PendingIntent; -import android.appwidget.AppWidgetManager; -import android.appwidget.AppWidgetProvider; -import android.bluetooth.BluetoothAdapter; -import android.content.ComponentName; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.database.ContentObserver; -import android.hardware.display.DisplayManager; -import android.location.LocationManager; -import android.net.ConnectivityManager; -import android.net.Uri; -import android.net.wifi.WifiManager; -import android.os.AsyncTask; -import android.os.Handler; -import android.os.IPowerManager; -import android.os.PowerManager; -import android.os.Process; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.os.UserManager; -import android.provider.Settings; -import android.provider.Settings.Secure; -import android.util.Log; -import android.widget.RemoteViews; - -import com.android.settings.R; -import com.android.settings.bluetooth.Utils; -import com.android.settingslib.bluetooth.LocalBluetoothAdapter; -import com.android.settingslib.bluetooth.LocalBluetoothManager; - -/** - * Provides control of power-related settings from a widget. - */ -public class SettingsAppWidgetProvider extends AppWidgetProvider { - static final String TAG = "SettingsAppWidgetProvider"; - - static final ComponentName THIS_APPWIDGET = - new ComponentName("com.android.settings", - "com.android.settings.widget.SettingsAppWidgetProvider"); - - private static LocalBluetoothAdapter sLocalBluetoothAdapter = null; - - private static final int BUTTON_WIFI = 0; - private static final int BUTTON_BRIGHTNESS = 1; - private static final int BUTTON_SYNC = 2; - private static final int BUTTON_LOCATION = 3; - private static final int BUTTON_BLUETOOTH = 4; - - // This widget keeps track of two sets of states: - // "3-state": STATE_DISABLED, STATE_ENABLED, STATE_INTERMEDIATE - // "5-state": STATE_DISABLED, STATE_ENABLED, STATE_TURNING_ON, STATE_TURNING_OFF, STATE_UNKNOWN - private static final int STATE_DISABLED = 0; - private static final int STATE_ENABLED = 1; - private static final int STATE_TURNING_ON = 2; - private static final int STATE_TURNING_OFF = 3; - private static final int STATE_UNKNOWN = 4; - private static final int STATE_INTERMEDIATE = 5; - - // Position in the widget bar, to enable different graphics for left, center and right buttons - private static final int POS_LEFT = 0; - private static final int POS_CENTER = 1; - private static final int POS_RIGHT = 2; - - private static final int[] IND_DRAWABLE_OFF = { - R.drawable.appwidget_settings_ind_off_l_holo, - R.drawable.appwidget_settings_ind_off_c_holo, - R.drawable.appwidget_settings_ind_off_r_holo - }; - - private static final int[] IND_DRAWABLE_MID = { - R.drawable.appwidget_settings_ind_mid_l_holo, - R.drawable.appwidget_settings_ind_mid_c_holo, - R.drawable.appwidget_settings_ind_mid_r_holo - }; - - private static final int[] IND_DRAWABLE_ON = { - R.drawable.appwidget_settings_ind_on_l_holo, - R.drawable.appwidget_settings_ind_on_c_holo, - R.drawable.appwidget_settings_ind_on_r_holo - }; - - /** Minimum brightness at which the indicator is shown at half-full and ON */ - private static final float HALF_BRIGHTNESS_THRESHOLD = 0.3f; - /** Minimum brightness at which the indicator is shown at full */ - private static final float FULL_BRIGHTNESS_THRESHOLD = 0.8f; - - private static final StateTracker sWifiState = new WifiStateTracker(); - private static final StateTracker sBluetoothState = new BluetoothStateTracker(); - private static final StateTracker sLocationState = new LocationStateTracker(); - private static final StateTracker sSyncState = new SyncStateTracker(); - private static SettingsObserver sSettingsObserver; - - /** - * The state machine for a setting's toggling, tracking reality - * versus the user's intent. - * - * This is necessary because reality moves relatively slowly - * (turning on & off radio drivers), compared to user's - * expectations. - */ - private abstract static class StateTracker { - // Is the state in the process of changing? - private boolean mInTransition = false; - private Boolean mActualState = null; // initially not set - private Boolean mIntendedState = null; // initially not set - - // Did a toggle request arrive while a state update was - // already in-flight? If so, the mIntendedState needs to be - // requested when the other one is done, unless we happened to - // arrive at that state already. - private boolean mDeferredStateChangeRequestNeeded = false; - - /** - * User pressed a button to change the state. Something - * should immediately appear to the user afterwards, even if - * we effectively do nothing. Their press must be heard. - */ - public final void toggleState(Context context) { - int currentState = getTriState(context); - boolean newState = false; - switch (currentState) { - case STATE_ENABLED: - newState = false; - break; - case STATE_DISABLED: - newState = true; - break; - case STATE_INTERMEDIATE: - if (mIntendedState != null) { - newState = !mIntendedState; - } - break; - } - mIntendedState = newState; - if (mInTransition) { - // We don't send off a transition request if we're - // already transitioning. Makes our state tracking - // easier, and is probably nicer on lower levels. - // (even though they should be able to take it...) - mDeferredStateChangeRequestNeeded = true; - } else { - mInTransition = true; - requestStateChange(context, newState); - } - } - - /** - * Return the ID of the clickable container for the setting. - */ - public abstract int getContainerId(); - - /** - * Return the ID of the main large image button for the setting. - */ - public abstract int getButtonId(); - - /** - * Returns the small indicator image ID underneath the setting. - */ - public abstract int getIndicatorId(); - - /** - * Returns the resource ID of the setting's content description. - */ - public abstract int getButtonDescription(); - - /** - * Returns the resource ID of the image to show as a function of - * the on-vs-off state. - */ - public abstract int getButtonImageId(boolean on); - - /** - * Returns the position in the button bar - either POS_LEFT, POS_RIGHT or POS_CENTER. - */ - public int getPosition() { return POS_CENTER; } - - /** - * Updates the remote views depending on the state (off, on, - * turning off, turning on) of the setting. - */ - public final void setImageViewResources(Context context, RemoteViews views) { - int containerId = getContainerId(); - int buttonId = getButtonId(); - int indicatorId = getIndicatorId(); - int pos = getPosition(); - switch (getTriState(context)) { - case STATE_DISABLED: - views.setContentDescription(containerId, - getContentDescription(context, R.string.gadget_state_off)); - views.setImageViewResource(buttonId, getButtonImageId(false)); - views.setImageViewResource( - indicatorId, IND_DRAWABLE_OFF[pos]); - break; - case STATE_ENABLED: - views.setContentDescription(containerId, - getContentDescription(context, R.string.gadget_state_on)); - views.setImageViewResource(buttonId, getButtonImageId(true)); - views.setImageViewResource( - indicatorId, IND_DRAWABLE_ON[pos]); - break; - case STATE_INTERMEDIATE: - // In the transitional state, the bottom green bar - // shows the tri-state (on, off, transitioning), but - // the top dark-gray-or-bright-white logo shows the - // user's intent. This is much easier to see in - // sunlight. - if (isTurningOn()) { - views.setContentDescription(containerId, - getContentDescription(context, R.string.gadget_state_turning_on)); - views.setImageViewResource(buttonId, getButtonImageId(true)); - views.setImageViewResource( - indicatorId, IND_DRAWABLE_MID[pos]); - } else { - views.setContentDescription(containerId, - getContentDescription(context, R.string.gadget_state_turning_off)); - views.setImageViewResource(buttonId, getButtonImageId(false)); - views.setImageViewResource( - indicatorId, IND_DRAWABLE_OFF[pos]); - } - break; - } - } - - /** - * Returns the gadget state template populated with the gadget - * description and state. - */ - private final String getContentDescription(Context context, int stateResId) { - final String gadget = context.getString(getButtonDescription()); - final String state = context.getString(stateResId); - return context.getString(R.string.gadget_state_template, gadget, state); - } - - /** - * Update internal state from a broadcast state change. - */ - public abstract void onActualStateChange(Context context, Intent intent); - - /** - * Sets the value that we're now in. To be called from onActualStateChange. - * - * @param newState one of STATE_DISABLED, STATE_ENABLED, STATE_TURNING_ON, - * STATE_TURNING_OFF, STATE_UNKNOWN - */ - protected final void setCurrentState(Context context, int newState) { - final boolean wasInTransition = mInTransition; - switch (newState) { - case STATE_DISABLED: - mInTransition = false; - mActualState = false; - break; - case STATE_ENABLED: - mInTransition = false; - mActualState = true; - break; - case STATE_TURNING_ON: - mInTransition = true; - mActualState = false; - break; - case STATE_TURNING_OFF: - mInTransition = true; - mActualState = true; - break; - } - - if (wasInTransition && !mInTransition) { - if (mDeferredStateChangeRequestNeeded) { - Log.v(TAG, "processing deferred state change"); - if (mActualState != null && mIntendedState != null && - mIntendedState.equals(mActualState)) { - Log.v(TAG, "... but intended state matches, so no changes."); - } else if (mIntendedState != null) { - mInTransition = true; - requestStateChange(context, mIntendedState); - } - mDeferredStateChangeRequestNeeded = false; - } - } - } - - - /** - * If we're in a transition mode, this returns true if we're - * transitioning towards being enabled. - */ - public final boolean isTurningOn() { - return mIntendedState != null && mIntendedState; - } - - /** - * Returns simplified 3-state value from underlying 5-state. - * - * @param context - * @return STATE_ENABLED, STATE_DISABLED, or STATE_INTERMEDIATE - */ - public final int getTriState(Context context) { - if (mInTransition) { - // If we know we just got a toggle request recently - // (which set mInTransition), don't even ask the - // underlying interface for its state. We know we're - // changing. This avoids blocking the UI thread - // during UI refresh post-toggle if the underlying - // service state accessor has coarse locking on its - // state (to be fixed separately). - return STATE_INTERMEDIATE; - } - switch (getActualState(context)) { - case STATE_DISABLED: - return STATE_DISABLED; - case STATE_ENABLED: - return STATE_ENABLED; - default: - return STATE_INTERMEDIATE; - } - } - - /** - * Gets underlying actual state. - * - * @param context - * @return STATE_ENABLED, STATE_DISABLED, STATE_ENABLING, STATE_DISABLING, - * or or STATE_UNKNOWN. - */ - public abstract int getActualState(Context context); - - /** - * Actually make the desired change to the underlying radio - * API. - */ - protected abstract void requestStateChange(Context context, boolean desiredState); - } - - /** - * Subclass of StateTracker to get/set Wifi state. - */ - private static final class WifiStateTracker extends StateTracker { - public int getContainerId() { return R.id.btn_wifi; } - public int getButtonId() { return R.id.img_wifi; } - public int getIndicatorId() { return R.id.ind_wifi; } - public int getButtonDescription() { return R.string.gadget_wifi; } - public int getButtonImageId(boolean on) { - return on ? R.drawable.ic_appwidget_settings_wifi_on_holo - : R.drawable.ic_appwidget_settings_wifi_off_holo; - } - - @Override - public int getPosition() { return POS_LEFT; } - - @Override - public int getActualState(Context context) { - WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); - if (wifiManager != null) { - return wifiStateToFiveState(wifiManager.getWifiState()); - } - return STATE_UNKNOWN; - } - - @Override - protected void requestStateChange(Context context, final boolean desiredState) { - final WifiManager wifiManager = - (WifiManager) context.getSystemService(Context.WIFI_SERVICE); - if (wifiManager == null) { - Log.d(TAG, "No wifiManager."); - return; - } - - // Actually request the wifi change and persistent - // settings write off the UI thread, as it can take a - // user-noticeable amount of time, especially if there's - // disk contention. - new AsyncTask<Void, Void, Void>() { - @Override - protected Void doInBackground(Void... args) { - /** - * Disable tethering if enabling Wifi - */ - int wifiApState = wifiManager.getWifiApState(); - if (desiredState && ((wifiApState == WifiManager.WIFI_AP_STATE_ENABLING) || - (wifiApState == WifiManager.WIFI_AP_STATE_ENABLED))) { - final ConnectivityManager connectivityManager = - (ConnectivityManager) context.getSystemService( - Context.CONNECTIVITY_SERVICE); - connectivityManager.stopTethering(ConnectivityManager.TETHERING_WIFI); - } - - wifiManager.setWifiEnabled(desiredState); - return null; - } - }.execute(); - } - - @Override - public void onActualStateChange(Context context, Intent intent) { - if (!WifiManager.WIFI_STATE_CHANGED_ACTION.equals(intent.getAction())) { - return; - } - int wifiState = intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, -1); - setCurrentState(context, wifiStateToFiveState(wifiState)); - } - - /** - * Converts WifiManager's state values into our - * Wifi/Bluetooth-common state values. - */ - private static int wifiStateToFiveState(int wifiState) { - switch (wifiState) { - case WifiManager.WIFI_STATE_DISABLED: - return STATE_DISABLED; - case WifiManager.WIFI_STATE_ENABLED: - return STATE_ENABLED; - case WifiManager.WIFI_STATE_DISABLING: - return STATE_TURNING_OFF; - case WifiManager.WIFI_STATE_ENABLING: - return STATE_TURNING_ON; - default: - return STATE_UNKNOWN; - } - } - } - - /** - * Subclass of StateTracker to get/set Bluetooth state. - */ - private static final class BluetoothStateTracker extends StateTracker { - public int getContainerId() { return R.id.btn_bluetooth; } - public int getButtonId() { return R.id.img_bluetooth; } - public int getIndicatorId() { return R.id.ind_bluetooth; } - public int getButtonDescription() { return R.string.gadget_bluetooth; } - public int getButtonImageId(boolean on) { - return on ? R.drawable.ic_appwidget_settings_bluetooth_on_holo - : R.drawable.ic_appwidget_settings_bluetooth_off_holo; - } - - @Override - public int getActualState(Context context) { - if (sLocalBluetoothAdapter == null) { - LocalBluetoothManager manager = Utils.getLocalBtManager(context); - if (manager == null) { - return STATE_UNKNOWN; // On emulator? - } - sLocalBluetoothAdapter = manager.getBluetoothAdapter(); - if (sLocalBluetoothAdapter == null) { - return STATE_UNKNOWN; // On emulator? - } - } - return bluetoothStateToFiveState(sLocalBluetoothAdapter.getBluetoothState()); - } - - @Override - protected void requestStateChange(Context context, final boolean desiredState) { - if (sLocalBluetoothAdapter == null) { - Log.d(TAG, "No LocalBluetoothManager"); - return; - } - // Actually request the Bluetooth change and persistent - // settings write off the UI thread, as it can take a - // user-noticeable amount of time, especially if there's - // disk contention. - new AsyncTask<Void, Void, Void>() { - @Override - protected Void doInBackground(Void... args) { - sLocalBluetoothAdapter.setBluetoothEnabled(desiredState); - return null; - } - }.execute(); - } - - @Override - public void onActualStateChange(Context context, Intent intent) { - if (!BluetoothAdapter.ACTION_STATE_CHANGED.equals(intent.getAction())) { - return; - } - int bluetoothState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1); - setCurrentState(context, bluetoothStateToFiveState(bluetoothState)); - } - - /** - * Converts BluetoothAdapter's state values into our - * Wifi/Bluetooth-common state values. - */ - private static int bluetoothStateToFiveState(int bluetoothState) { - switch (bluetoothState) { - case BluetoothAdapter.STATE_OFF: - return STATE_DISABLED; - case BluetoothAdapter.STATE_ON: - return STATE_ENABLED; - case BluetoothAdapter.STATE_TURNING_ON: - return STATE_TURNING_ON; - case BluetoothAdapter.STATE_TURNING_OFF: - return STATE_TURNING_OFF; - default: - return STATE_UNKNOWN; - } - } - } - - /** - * Subclass of StateTracker for location state. - */ - private static final class LocationStateTracker extends StateTracker { - private int mCurrentLocationMode = Settings.Secure.LOCATION_MODE_OFF; - - public int getContainerId() { return R.id.btn_location; } - public int getButtonId() { return R.id.img_location; } - public int getIndicatorId() { return R.id.ind_location; } - public int getButtonDescription() { return R.string.gadget_location; } - public int getButtonImageId(boolean on) { - if (on) { - switch (mCurrentLocationMode) { - case Settings.Secure.LOCATION_MODE_HIGH_ACCURACY: - case Settings.Secure.LOCATION_MODE_SENSORS_ONLY: - return R.drawable.ic_appwidget_settings_location_on_holo; - default: - return R.drawable.ic_appwidget_settings_location_saving_holo; - } - } - - return R.drawable.ic_appwidget_settings_location_off_holo; - } - - @Override - public int getActualState(Context context) { - ContentResolver resolver = context.getContentResolver(); - mCurrentLocationMode = Settings.Secure.getInt(resolver, - Settings.Secure.LOCATION_MODE, Settings.Secure.LOCATION_MODE_OFF); - return (mCurrentLocationMode == Settings.Secure.LOCATION_MODE_OFF) - ? STATE_DISABLED : STATE_ENABLED; - } - - @Override - public void onActualStateChange(Context context, Intent unused) { - // Note: the broadcast location providers changed intent - // doesn't include an extras bundles saying what the new value is. - setCurrentState(context, getActualState(context)); - } - - @Override - public void requestStateChange(final Context context, final boolean desiredState) { - final ContentResolver resolver = context.getContentResolver(); - new AsyncTask<Void, Void, Boolean>() { - @Override - protected Boolean doInBackground(Void... args) { - final UserManager um = - (UserManager) context.getSystemService(Context.USER_SERVICE); - if (!um.hasUserRestriction(UserManager.DISALLOW_SHARE_LOCATION)) { - LocationManager lm = - (LocationManager) context.getSystemService( - Context.LOCATION_SERVICE); - boolean currentLocationEnabled = lm.isLocationEnabled(); - lm.setLocationEnabledForUser( - !currentLocationEnabled, Process.myUserHandle()); - return lm.isLocationEnabled(); - } - return getActualState(context) == STATE_ENABLED; - } - - @Override - protected void onPostExecute(Boolean result) { - setCurrentState( - context, - result ? STATE_ENABLED : STATE_DISABLED); - updateWidget(context); - } - }.execute(); - } - } - - /** - * Subclass of StateTracker for sync state. - */ - private static final class SyncStateTracker extends StateTracker { - public int getContainerId() { return R.id.btn_sync; } - public int getButtonId() { return R.id.img_sync; } - public int getIndicatorId() { return R.id.ind_sync; } - public int getButtonDescription() { return R.string.gadget_sync; } - public int getButtonImageId(boolean on) { - return on ? R.drawable.ic_appwidget_settings_sync_on_holo - : R.drawable.ic_appwidget_settings_sync_off_holo; - } - - @Override - public int getActualState(Context context) { - boolean on = ContentResolver.getMasterSyncAutomatically(); - return on ? STATE_ENABLED : STATE_DISABLED; - } - - @Override - public void onActualStateChange(Context context, Intent unused) { - setCurrentState(context, getActualState(context)); - } - - @Override - public void requestStateChange(final Context context, final boolean desiredState) { - final ConnectivityManager connManager = - (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - final boolean sync = ContentResolver.getMasterSyncAutomatically(); - - new AsyncTask<Void, Void, Boolean>() { - @Override - protected Boolean doInBackground(Void... args) { - // Turning sync on. - if (desiredState) { - if (!sync) { - ContentResolver.setMasterSyncAutomatically(true); - } - return true; - } - - // Turning sync off - if (sync) { - ContentResolver.setMasterSyncAutomatically(false); - } - return false; - } - - @Override - protected void onPostExecute(Boolean result) { - setCurrentState( - context, - result ? STATE_ENABLED : STATE_DISABLED); - updateWidget(context); - } - }.execute(); - } - } - - private static void checkObserver(Context context) { - if (sSettingsObserver == null) { - sSettingsObserver = new SettingsObserver(new Handler(), - context.getApplicationContext()); - sSettingsObserver.startObserving(); - } - } - - @Override - public void onUpdate(Context context, AppWidgetManager appWidgetManager, - int[] appWidgetIds) { - // Update each requested appWidgetId - RemoteViews view = buildUpdate(context); - - for (int i = 0; i < appWidgetIds.length; i++) { - appWidgetManager.updateAppWidget(appWidgetIds[i], view); - } - } - - @Override - public void onEnabled(Context context) { - checkObserver(context); - } - - @Override - public void onDisabled(Context context) { - if (sSettingsObserver != null) { - sSettingsObserver.stopObserving(); - sSettingsObserver = null; - } - } - - /** - * Load image for given widget and build {@link RemoteViews} for it. - */ - static RemoteViews buildUpdate(Context context) { - RemoteViews views = new RemoteViews(context.getPackageName(), - R.layout.widget); - views.setOnClickPendingIntent(R.id.btn_wifi, getLaunchPendingIntent(context, - BUTTON_WIFI)); - views.setOnClickPendingIntent(R.id.btn_brightness, - getLaunchPendingIntent(context, - BUTTON_BRIGHTNESS)); - views.setOnClickPendingIntent(R.id.btn_sync, - getLaunchPendingIntent(context, - BUTTON_SYNC)); - views.setOnClickPendingIntent(R.id.btn_location, - getLaunchPendingIntent(context, BUTTON_LOCATION)); - views.setOnClickPendingIntent(R.id.btn_bluetooth, - getLaunchPendingIntent(context, - BUTTON_BLUETOOTH)); - - updateButtons(views, context); - return views; - } - - /** - * Updates the widget when something changes, or when a button is pushed. - * - * @param context - */ - public static void updateWidget(Context context) { - RemoteViews views = buildUpdate(context); - // Update specific list of appWidgetIds if given, otherwise default to all - final AppWidgetManager gm = AppWidgetManager.getInstance(context); - gm.updateAppWidget(THIS_APPWIDGET, views); - checkObserver(context); - } - - /** - * Updates the buttons based on the underlying states of wifi, etc. - * - * @param views The RemoteViews to update. - * @param context - */ - private static void updateButtons(RemoteViews views, Context context) { - sWifiState.setImageViewResources(context, views); - sBluetoothState.setImageViewResources(context, views); - sLocationState.setImageViewResources(context, views); - sSyncState.setImageViewResources(context, views); - - if (getBrightnessMode(context)) { - views.setContentDescription(R.id.btn_brightness, - context.getString(R.string.gadget_brightness_template, - context.getString(R.string.gadget_brightness_state_auto))); - views.setImageViewResource(R.id.img_brightness, - R.drawable.ic_appwidget_settings_brightness_auto_holo); - views.setImageViewResource(R.id.ind_brightness, - R.drawable.appwidget_settings_ind_on_r_holo); - } else { - final int brightness = getBrightness(context); - final PowerManager pm = context.getSystemService(PowerManager.class); - // Set the icon - final int full = (int)(pm.getMaximumScreenBrightnessSetting() - * FULL_BRIGHTNESS_THRESHOLD); - final int half = (int)(pm.getMaximumScreenBrightnessSetting() - * HALF_BRIGHTNESS_THRESHOLD); - if (brightness > full) { - views.setContentDescription(R.id.btn_brightness, - context.getString(R.string.gadget_brightness_template, - context.getString(R.string.gadget_brightness_state_full))); - views.setImageViewResource(R.id.img_brightness, - R.drawable.ic_appwidget_settings_brightness_full_holo); - } else if (brightness > half) { - views.setContentDescription(R.id.btn_brightness, - context.getString(R.string.gadget_brightness_template, - context.getString(R.string.gadget_brightness_state_half))); - views.setImageViewResource(R.id.img_brightness, - R.drawable.ic_appwidget_settings_brightness_half_holo); - } else { - views.setContentDescription(R.id.btn_brightness, - context.getString(R.string.gadget_brightness_template, - context.getString(R.string.gadget_brightness_state_off))); - views.setImageViewResource(R.id.img_brightness, - R.drawable.ic_appwidget_settings_brightness_off_holo); - } - // Set the ON state - if (brightness > half) { - views.setImageViewResource(R.id.ind_brightness, - R.drawable.appwidget_settings_ind_on_r_holo); - } else { - views.setImageViewResource(R.id.ind_brightness, - R.drawable.appwidget_settings_ind_off_r_holo); - } - } - } - - /** - * Creates PendingIntent to notify the widget of a button click. - * - * @param context - * @return - */ - private static PendingIntent getLaunchPendingIntent(Context context, - int buttonId) { - Intent launchIntent = new Intent(); - launchIntent.setClass(context, SettingsAppWidgetProvider.class); - launchIntent.addCategory(Intent.CATEGORY_ALTERNATIVE); - launchIntent.setData(Uri.parse("custom:" + buttonId)); - PendingIntent pi = PendingIntent.getBroadcast(context, 0 /* no requestCode */, - launchIntent, 0 /* no flags */); - return pi; - } - - /** - * Receives and processes a button pressed intent or state change. - * - * @param context - * @param intent Indicates the pressed button. - */ - @Override - public void onReceive(Context context, Intent intent) { - super.onReceive(context, intent); - String action = intent.getAction(); - if (WifiManager.WIFI_STATE_CHANGED_ACTION.equals(action)) { - sWifiState.onActualStateChange(context, intent); - } else if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) { - sBluetoothState.onActualStateChange(context, intent); - } else if (LocationManager.MODE_CHANGED_ACTION.equals(action)) { - sLocationState.onActualStateChange(context, intent); - } else if (ContentResolver.ACTION_SYNC_CONN_STATUS_CHANGED.equals(action)) { - sSyncState.onActualStateChange(context, intent); - } else if (intent.hasCategory(Intent.CATEGORY_ALTERNATIVE)) { - Uri data = intent.getData(); - int buttonId = Integer.parseInt(data.getSchemeSpecificPart()); - if (buttonId == BUTTON_WIFI) { - sWifiState.toggleState(context); - } else if (buttonId == BUTTON_BRIGHTNESS) { - toggleBrightness(context); - } else if (buttonId == BUTTON_SYNC) { - sSyncState.toggleState(context); - } else if (buttonId == BUTTON_LOCATION) { - sLocationState.toggleState(context); - } else if (buttonId == BUTTON_BLUETOOTH) { - sBluetoothState.toggleState(context); - } - } else { - // Don't fall-through to updating the widget. The Intent - // was something unrelated or that our super class took - // care of. - return; - } - - // State changes fall through - updateWidget(context); - } - - /** - * Gets brightness level. - * - * @param context - * @return brightness level between 0 and 255. - */ - private static int getBrightness(Context context) { - try { - int brightness = Settings.System.getInt(context.getContentResolver(), - Settings.System.SCREEN_BRIGHTNESS); - return brightness; - } catch (Exception e) { - } - return 0; - } - - /** - * Gets state of brightness mode. - * - * @param context - * @return true if auto brightness is on. - */ - private static boolean getBrightnessMode(Context context) { - try { - int brightnessMode = Settings.System.getInt(context.getContentResolver(), - Settings.System.SCREEN_BRIGHTNESS_MODE); - return brightnessMode == Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC; - } catch (Exception e) { - Log.d(TAG, "getBrightnessMode: " + e); - } - return false; - } - - /** - * Increases or decreases the brightness. - * - * @param context - */ - private void toggleBrightness(Context context) { - try { - DisplayManager dm = context.getSystemService(DisplayManager.class); - PowerManager pm = context.getSystemService(PowerManager.class); - - ContentResolver cr = context.getContentResolver(); - int brightness = Settings.System.getInt(cr, - Settings.System.SCREEN_BRIGHTNESS); - int brightnessMode = Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL; - //Only get brightness setting if available - if (context.getResources().getBoolean( - com.android.internal.R.bool.config_automatic_brightness_available)) { - brightnessMode = Settings.System.getInt(cr, - Settings.System.SCREEN_BRIGHTNESS_MODE); - } - - // Rotate AUTO -> MINIMUM -> DEFAULT -> MAXIMUM - // Technically, not a toggle... - if (brightnessMode == Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC) { - brightness = pm.getMinimumScreenBrightnessSetting(); - brightnessMode = Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL; - } else if (brightness < pm.getDefaultScreenBrightnessSetting()) { - brightness = pm.getDefaultScreenBrightnessSetting(); - } else if (brightness < pm.getMaximumScreenBrightnessSetting()) { - brightness = pm.getMaximumScreenBrightnessSetting(); - } else { - brightnessMode = Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC; - brightness = pm.getMinimumScreenBrightnessSetting(); - } - - if (context.getResources().getBoolean( - com.android.internal.R.bool.config_automatic_brightness_available)) { - // Set screen brightness mode (automatic or manual) - Settings.System.putInt(context.getContentResolver(), - Settings.System.SCREEN_BRIGHTNESS_MODE, - brightnessMode); - } else { - // Make sure we set the brightness if automatic mode isn't available - brightnessMode = Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL; - } - if (brightnessMode == Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL) { - dm.setTemporaryBrightness(brightness); - Settings.System.putInt(cr, Settings.System.SCREEN_BRIGHTNESS, brightness); - } - } catch (Settings.SettingNotFoundException e) { - Log.d(TAG, "toggleBrightness: " + e); - } - } - - /** Observer to watch for changes to the BRIGHTNESS setting */ - private static class SettingsObserver extends ContentObserver { - - private Context mContext; - - SettingsObserver(Handler handler, Context context) { - super(handler); - mContext = context; - } - - void startObserving() { - ContentResolver resolver = mContext.getContentResolver(); - // Listen to brightness and brightness mode - resolver.registerContentObserver(Settings.System - .getUriFor(Settings.System.SCREEN_BRIGHTNESS), false, this); - resolver.registerContentObserver(Settings.System - .getUriFor(Settings.System.SCREEN_BRIGHTNESS_MODE), false, this); - resolver.registerContentObserver(Settings.System - .getUriFor(Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ), false, this); - } - - void stopObserving() { - mContext.getContentResolver().unregisterContentObserver(this); - } - - @Override - public void onChange(boolean selfChange) { - updateWidget(mContext); - } - } - -} diff --git a/src/com/android/settings/widget/SingleTargetGearPreference.java b/src/com/android/settings/widget/SingleTargetGearPreference.java new file mode 100644 index 0000000000..f6496ed4d8 --- /dev/null +++ b/src/com/android/settings/widget/SingleTargetGearPreference.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2018 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.util.AttributeSet; +import android.view.View; + +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + +import com.android.settings.R; + +/** + * A preference with single target and a gear icon on the side. + */ +public class SingleTargetGearPreference extends Preference { + public SingleTargetGearPreference(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(); + } + + public SingleTargetGearPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + public SingleTargetGearPreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public SingleTargetGearPreference(Context context) { + super(context); + init(); + } + + private void init() { + setLayoutResource(R.layout.preference_single_target); + setWidgetLayoutResource(R.layout.preference_widget_gear_optional_background); + } + + @Override + public void onBindViewHolder(PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + final View divider = holder.findViewById(com.android.settingslib.R.id.two_target_divider); + if (divider != null) { + divider.setVisibility(View.INVISIBLE); + } + } +} diff --git a/src/com/android/settings/widget/SlidingTabLayout.java b/src/com/android/settings/widget/SlidingTabLayout.java index ef024ef5a1..3ae4e3cc6c 100644 --- a/src/com/android/settings/widget/SlidingTabLayout.java +++ b/src/com/android/settings/widget/SlidingTabLayout.java @@ -17,7 +17,6 @@ package com.android.settings.widget; import android.content.Context; -import androidx.viewpager.widget.PagerAdapter; import android.util.AttributeSet; import android.view.Gravity; import android.view.LayoutInflater; @@ -26,6 +25,8 @@ import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.TextView; +import androidx.viewpager.widget.PagerAdapter; + import com.android.settings.R; /** diff --git a/src/com/android/settings/widget/SwitchBar.java b/src/com/android/settings/widget/SwitchBar.java index 857075bfb5..f8743d1fa4 100644 --- a/src/com/android/settings/widget/SwitchBar.java +++ b/src/com/android/settings/widget/SwitchBar.java @@ -18,27 +18,29 @@ package com.android.settings.widget; import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.res.TypedArray; -import android.graphics.Rect; +import android.graphics.drawable.Drawable; import android.os.Parcel; import android.os.Parcelable; -import androidx.annotation.ColorInt; -import androidx.annotation.StringRes; -import androidx.annotation.VisibleForTesting; import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.text.style.TextAppearanceSpan; import android.util.AttributeSet; import android.view.LayoutInflater; -import android.view.TouchDelegate; import android.view.View; import android.view.ViewGroup; import android.widget.CompoundButton; +import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.Switch; import android.widget.TextView; +import androidx.annotation.ColorInt; +import androidx.annotation.StringRes; +import androidx.annotation.VisibleForTesting; + import com.android.settings.R; import com.android.settings.overlay.FeatureFactory; import com.android.settingslib.RestrictedLockUtils; @@ -63,14 +65,15 @@ public class SwitchBar extends LinearLayout implements CompoundButton.OnCheckedC R.attr.switchBarMarginStart, R.attr.switchBarMarginEnd, R.attr.switchBarBackgroundColor, - R.attr.switchBarBackgroundActivatedColor}; + R.attr.switchBarBackgroundActivatedColor, + R.attr.switchBarRestrictionIcon}; private final List<OnSwitchChangeListener> mSwitchChangeListeners = new ArrayList<>(); private final MetricsFeatureProvider mMetricsFeatureProvider; private final TextAppearanceSpan mSummarySpan; private ToggleSwitch mSwitch; - private View mRestrictedIcon; + private ImageView mRestrictedIcon; private TextView mTextView; private String mLabel; private String mSummary; @@ -105,12 +108,16 @@ public class SwitchBar extends LinearLayout implements CompoundButton.OnCheckedC super(context, attrs, defStyleAttr, defStyleRes); LayoutInflater.from(context).inflate(R.layout.switch_bar, this); + // Set the whole SwitchBar focusable and clickable. + setFocusable(true); + setClickable(true); final TypedArray a = context.obtainStyledAttributes(attrs, XML_ATTRIBUTES); - int switchBarMarginStart = (int) a.getDimension(0, 0); - int switchBarMarginEnd = (int) a.getDimension(1, 0); + final int switchBarMarginStart = (int) a.getDimension(0, 0); + final int switchBarMarginEnd = (int) a.getDimension(1, 0); mBackgroundColor = a.getColor(2, 0); mBackgroundActivatedColor = a.getColor(3, 0); + final Drawable restrictedIconDrawable = a.getDrawable(4); a.recycle(); mTextView = findViewById(R.id.switch_text); @@ -122,6 +129,9 @@ public class SwitchBar extends LinearLayout implements CompoundButton.OnCheckedC // Prevent onSaveInstanceState() to be called as we are managing the state of the Switch // on our own mSwitch.setSaveEnabled(false); + // Set the ToggleSwitch non-focusable and non-clickable to avoid multiple focus. + mSwitch.setFocusable(false); + mSwitch.setClickable(false); lp = (MarginLayoutParams) mSwitch.getLayoutParams(); lp.setMarginEnd(switchBarMarginEnd); @@ -133,14 +143,20 @@ public class SwitchBar extends LinearLayout implements CompoundButton.OnCheckedC (switchView, isChecked) -> setTextViewLabelAndBackground(isChecked)); mRestrictedIcon = findViewById(R.id.restricted_icon); + mRestrictedIcon.setImageDrawable(restrictedIconDrawable); mRestrictedIcon.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mDisabledByAdmin) { - mMetricsFeatureProvider.count(mContext, - mMetricsTag + "/switch_bar|restricted", 1); + mMetricsFeatureProvider.action( + SettingsEnums.PAGE_UNKNOWN, + SettingsEnums.ACTION_SETTINGS_PREFERENCE_CHANGE, + SettingsEnums.PAGE_UNKNOWN, + mMetricsTag + "/switch_bar|restricted", + 1); + RestrictedLockUtils.sendShowAdminSupportDetailsIntent(context, - mEnforcedAdmin); + mEnforcedAdmin); } } }); @@ -151,6 +167,12 @@ public class SwitchBar extends LinearLayout implements CompoundButton.OnCheckedC mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider(); } + // Override the performClick method to eliminate redundant click. + @Override + public boolean performClick() { + return getDelegatingView().performClick(); + } + public void setMetricsTag(String tag) { mMetricsTag = tag; } @@ -227,14 +249,14 @@ public class SwitchBar extends LinearLayout implements CompoundButton.OnCheckedC mSwitch.setEnabled(false); mSwitch.setVisibility(View.GONE); mRestrictedIcon.setVisibility(View.VISIBLE); + mRestrictedIcon.setFocusable(false); + mRestrictedIcon.setClickable(false); } else { mDisabledByAdmin = false; mSwitch.setVisibility(View.VISIBLE); mRestrictedIcon.setVisibility(View.GONE); setEnabled(true); } - setTouchDelegate(new TouchDelegate(new Rect(0, 0, getWidth(), getHeight()), - getDelegatingView())); } public final ToggleSwitch getSwitch() { @@ -245,10 +267,6 @@ public class SwitchBar extends LinearLayout implements CompoundButton.OnCheckedC if (!isShowing()) { setVisibility(View.VISIBLE); mSwitch.setOnCheckedChangeListener(this); - // Make the entire bar work as a switch - post(() -> setTouchDelegate( - new TouchDelegate(new Rect(0, 0, getWidth(), getHeight()), - getDelegatingView()))); } } @@ -259,14 +277,6 @@ public class SwitchBar extends LinearLayout implements CompoundButton.OnCheckedC } } - @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) { - if ((w > 0) && (h > 0)) { - setTouchDelegate(new TouchDelegate(new Rect(0, 0, w, h), - getDelegatingView())); - } - } - public boolean isShowing() { return (getVisibility() == View.VISIBLE); } @@ -281,7 +291,12 @@ public class SwitchBar extends LinearLayout implements CompoundButton.OnCheckedC @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if (mLoggingIntialized) { - mMetricsFeatureProvider.count(mContext, mMetricsTag + "/switch_bar|" + isChecked, 1); + mMetricsFeatureProvider.action( + SettingsEnums.PAGE_UNKNOWN, + SettingsEnums.ACTION_SETTINGS_PREFERENCE_CHANGE, + SettingsEnums.PAGE_UNKNOWN, + mMetricsTag + "/switch_bar", + isChecked ? 1 : 0); } mLoggingIntialized = true; propagateChecked(isChecked); diff --git a/src/com/android/settings/widget/SwitchBarController.java b/src/com/android/settings/widget/SwitchBarController.java index ede02afbee..e471c7a1b7 100644 --- a/src/com/android/settings/widget/SwitchBarController.java +++ b/src/com/android/settings/widget/SwitchBarController.java @@ -17,6 +17,7 @@ package com.android.settings.widget; import android.widget.Switch; + import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; /* diff --git a/src/com/android/settings/widget/TintDrawable.java b/src/com/android/settings/widget/TintDrawable.java new file mode 100644 index 0000000000..13eb1211da --- /dev/null +++ b/src/com/android/settings/widget/TintDrawable.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2019 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.annotation.NonNull; +import android.annotation.Nullable; +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.content.res.Resources.Theme; +import android.content.res.TypedArray; +import android.graphics.drawable.DrawableWrapper; +import android.util.AttributeSet; +import android.util.Log; + +import com.android.settings.R; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; + +/** + * A Drawable that tints a contained Drawable, overriding the existing tint specified in the + * underlying drawable. This class should only be used in XML. + * + * @attr ref android.R.styleable#DrawableWrapper_drawable + * @attr ref R.styleable#TintDrawable_tint + */ +public class TintDrawable extends DrawableWrapper { + private ColorStateList mTint; + private int[] mThemeAttrs; + + /** No-arg constructor used by drawable inflation. */ + public TintDrawable() { + super(null); + } + + @Override + public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser, + @NonNull AttributeSet attrs, @Nullable Theme theme) + throws XmlPullParserException, IOException { + final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.TintDrawable); + + super.inflate(r, parser, attrs, theme); + + mThemeAttrs = a.extractThemeAttrs(); + updateStateFromTypedArray(a); + a.recycle(); + + applyTint(); + } + + @Override + public void applyTheme(Theme t) { + super.applyTheme(t); + + if (mThemeAttrs != null) { + final TypedArray a = t.resolveAttributes(mThemeAttrs, R.styleable.TintDrawable); + updateStateFromTypedArray(a); + a.recycle(); + } + + // Ensure tint is reapplied after applying the theme to ensure this drawables' + // tint overrides the underlying drawables' tint. + applyTint(); + } + + @Override + public boolean canApplyTheme() { + return (mThemeAttrs != null && mThemeAttrs.length > 0) || super.canApplyTheme(); + } + + private void updateStateFromTypedArray(@NonNull TypedArray a) { + if (a.hasValue(R.styleable.TintDrawable_android_drawable)) { + setDrawable(a.getDrawable(R.styleable.TintDrawable_android_drawable)); + } + if (a.hasValue(R.styleable.TintDrawable_android_tint)) { + mTint = a.getColorStateList(R.styleable.TintDrawable_android_tint); + } + } + + private void applyTint() { + if (getDrawable() != null && mTint != null) { + getDrawable().mutate().setTintList(mTint); + } + } +} diff --git a/src/com/android/settings/widget/TwoStateButtonPreference.java b/src/com/android/settings/widget/TwoStateButtonPreference.java index 0f71268975..eeee65def8 100644 --- a/src/com/android/settings/widget/TwoStateButtonPreference.java +++ b/src/com/android/settings/widget/TwoStateButtonPreference.java @@ -18,14 +18,15 @@ package com.android.settings.widget; import android.content.Context; import android.content.res.TypedArray; -import androidx.annotation.VisibleForTesting; -import androidx.core.content.res.TypedArrayUtils; import android.util.AttributeSet; import android.view.View; import android.widget.Button; +import androidx.annotation.VisibleForTesting; +import androidx.core.content.res.TypedArrayUtils; + import com.android.settings.R; -import com.android.settings.applications.LayoutPreference; +import com.android.settingslib.widget.LayoutPreference; /** * Preference that presents a button with two states(On vs Off) diff --git a/src/com/android/settings/widget/UpdatableListPreferenceDialogFragment.java b/src/com/android/settings/widget/UpdatableListPreferenceDialogFragment.java index 0243a6873e..bd7f78a694 100644 --- a/src/com/android/settings/widget/UpdatableListPreferenceDialogFragment.java +++ b/src/com/android/settings/widget/UpdatableListPreferenceDialogFragment.java @@ -15,22 +15,24 @@ */ package com.android.settings.widget; -import android.app.AlertDialog; import android.content.res.TypedArray; import android.os.Bundle; +import android.widget.ArrayAdapter; + import androidx.annotation.VisibleForTesting; -import androidx.preference.PreferenceDialogFragment; +import androidx.appcompat.app.AlertDialog.Builder; import androidx.preference.ListPreference; -import android.widget.ArrayAdapter; +import androidx.preference.PreferenceDialogFragmentCompat; + import com.android.settingslib.core.instrumentation.Instrumentable; import java.util.ArrayList; /** - * {@link PreferenceDialogFragment} that updates the available options + * {@link PreferenceDialogFragmentCompat} that updates the available options * when {@code onListPreferenceUpdated} is called." */ -public class UpdatableListPreferenceDialogFragment extends PreferenceDialogFragment implements +public class UpdatableListPreferenceDialogFragment extends PreferenceDialogFragmentCompat implements Instrumentable { private static final String SAVE_STATE_INDEX = "UpdatableListPreferenceDialogFragment.index"; @@ -82,8 +84,8 @@ public class UpdatableListPreferenceDialogFragment extends PreferenceDialogFragm @Override public void onDialogClosed(boolean positiveResult) { - final ListPreference preference = getListPreference(); if (positiveResult && mClickedDialogEntryIndex >= 0) { + final ListPreference preference = getListPreference(); final String value = mEntryValues[mClickedDialogEntryIndex].toString(); if (preference.callChangeListener(value)) { preference.setValue(value); @@ -113,7 +115,7 @@ public class UpdatableListPreferenceDialogFragment extends PreferenceDialogFragm } @Override - protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { + protected void onPrepareDialogBuilder(Builder builder) { super.onPrepareDialogBuilder(builder); final TypedArray a = getContext().obtainStyledAttributes( null, @@ -142,7 +144,8 @@ public class UpdatableListPreferenceDialogFragment extends PreferenceDialogFragm return mMetricsCategory; } - private ListPreference getListPreference() { + @VisibleForTesting + ListPreference getListPreference() { return (ListPreference) getPreference(); } diff --git a/src/com/android/settings/graph/UsageGraph.java b/src/com/android/settings/widget/UsageGraph.java index b9d517d5e8..505dc58b2c 100644 --- a/src/com/android/settings/graph/UsageGraph.java +++ b/src/com/android/settings/widget/UsageGraph.java @@ -1,18 +1,20 @@ /* * Copyright (C) 2016 The Android Open Source Project * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file - * except in compliance with the License. You may obtain a copy of the License at + * 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. + * 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.graph; +package com.android.settings.widget; import android.annotation.Nullable; import android.content.Context; @@ -28,12 +30,13 @@ import android.graphics.Paint.Style; import android.graphics.Path; import android.graphics.Shader.TileMode; import android.graphics.drawable.Drawable; -import androidx.annotation.VisibleForTesting; import android.util.AttributeSet; import android.util.SparseIntArray; import android.util.TypedValue; import android.view.View; +import androidx.annotation.VisibleForTesting; + import com.android.settings.fuelgauge.BatteryUtils; import com.android.settingslib.R; @@ -253,9 +256,15 @@ public class UsageGraph extends View { return; } + canvas.save(); + if (getLayoutDirection() == LAYOUT_DIRECTION_RTL) { + // Flip the canvas along the y-axis of the center of itself before drawing paths. + canvas.scale(-1, 1, canvas.getWidth() * 0.5f, 0); + } drawLinePath(canvas, mLocalProjectedPaths, mDottedPaint); drawFilledPath(canvas, mLocalPaths, mFillPaint); drawLinePath(canvas, mLocalPaths, mLinePaint); + canvas.restore(); BatteryUtils.logRuntime(LOG_TAG, "onDraw", startTime); } @@ -279,7 +288,11 @@ public class UsageGraph extends View { canvas.drawPath(mPath, paint); } - private void drawFilledPath(Canvas canvas, SparseIntArray localPaths, Paint paint) { + @VisibleForTesting + void drawFilledPath(Canvas canvas, SparseIntArray localPaths, Paint paint) { + if (localPaths.size() == 0) { + return; + } mPath.reset(); float lastStartX = localPaths.keyAt(0); mPath.moveTo(localPaths.keyAt(0), localPaths.valueAt(0)); diff --git a/src/com/android/settings/graph/UsageView.java b/src/com/android/settings/widget/UsageView.java index bcf04414cf..54e75b39c1 100644 --- a/src/com/android/settings/graph/UsageView.java +++ b/src/com/android/settings/widget/UsageView.java @@ -1,18 +1,20 @@ /* * Copyright (C) 2016 The Android Open Source Project * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file - * except in compliance with the License. You may obtain a copy of the License at + * 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. + * 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.graph; +package com.android.settings.widget; import android.content.Context; import android.content.res.TypedArray; @@ -24,6 +26,7 @@ import android.view.View; import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.TextView; + import com.android.settingslib.R; public class UsageView extends FrameLayout { diff --git a/src/com/android/settings/widget/ValidatedEditTextPreference.java b/src/com/android/settings/widget/ValidatedEditTextPreference.java index 8ffffb7f65..cc344ac67f 100644 --- a/src/com/android/settings/widget/ValidatedEditTextPreference.java +++ b/src/com/android/settings/widget/ValidatedEditTextPreference.java @@ -16,26 +16,26 @@ package com.android.settings.widget; -import android.app.AlertDialog; import android.content.Context; -import androidx.annotation.VisibleForTesting; -import androidx.preference.PreferenceViewHolder; import android.text.Editable; import android.text.InputType; import android.text.TextUtils; import android.text.TextWatcher; import android.util.AttributeSet; -import android.util.Log; import android.view.View; import android.widget.EditText; import android.widget.TextView; -import com.android.settingslib.CustomEditTextPreference; +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; +import androidx.preference.PreferenceViewHolder; + +import com.android.settingslib.CustomEditTextPreferenceCompat; /** * {@code EditTextPreference} that supports input validation. */ -public class ValidatedEditTextPreference extends CustomEditTextPreference { +public class ValidatedEditTextPreference extends CustomEditTextPreferenceCompat { public interface Validator { boolean isTextValid(String value); @@ -93,7 +93,8 @@ public class ValidatedEditTextPreference extends CustomEditTextPreference { textView.setInputType( InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); } else { - textView.setInputType(InputType.TYPE_CLASS_TEXT); + textView.setInputType( + InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); } } diff --git a/src/com/android/settings/widget/VideoPreference.java b/src/com/android/settings/widget/VideoPreference.java index 4ec98296e1..1af9065dbc 100644 --- a/src/com/android/settings/widget/VideoPreference.java +++ b/src/com/android/settings/widget/VideoPreference.java @@ -22,15 +22,18 @@ import android.content.res.TypedArray; import android.graphics.SurfaceTexture; import android.media.MediaPlayer; import android.net.Uri; -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; import android.util.AttributeSet; import android.util.Log; +import android.util.TypedValue; import android.view.Surface; import android.view.TextureView; import android.view.View; import android.widget.ImageView; +import android.widget.LinearLayout; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; import com.android.settings.R; @@ -47,35 +50,53 @@ public class VideoPreference extends Preference { MediaPlayer mMediaPlayer; @VisibleForTesting boolean mAnimationAvailable; - private boolean mVideoReady; + @VisibleForTesting + boolean mVideoReady; private boolean mVideoPaused; - private float mAspectRadio = 1.0f; + private float mAspectRatio = 1.0f; private int mPreviewResource; + private boolean mViewVisible; + private Surface mSurface; + private int mAnimationId; + private int mHeight = LinearLayout.LayoutParams.MATCH_PARENT - 1; // video height in pixels + + public VideoPreference(Context context) { + super(context); + mContext = context; + initialize(context, null); + } public VideoPreference(Context context, AttributeSet attrs) { super(context, attrs); mContext = context; + initialize(context, attrs); + } + + private void initialize(Context context, AttributeSet attrs) { TypedArray attributes = context.getTheme().obtainStyledAttributes( attrs, - com.android.settings.R.styleable.VideoPreference, + R.styleable.VideoPreference, 0, 0); try { - int animation = attributes.getResourceId(R.styleable.VideoPreference_animation, 0); + // if these are already set that means they were set dynamically and don't need + // to be loaded from xml + mAnimationId = mAnimationId == 0 + ? attributes.getResourceId(R.styleable.VideoPreference_animation, 0) + : mAnimationId; mVideoPath = new Uri.Builder().scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) .authority(context.getPackageName()) - .appendPath(String.valueOf(animation)) + .appendPath(String.valueOf(mAnimationId)) .build(); - mMediaPlayer = MediaPlayer.create(mContext, mVideoPath); + mPreviewResource = mPreviewResource == 0 + ? attributes.getResourceId(R.styleable.VideoPreference_preview, 0) + : mPreviewResource; + if (mPreviewResource == 0 && mAnimationId == 0) { + return; + } + initMediaPlayer(); if (mMediaPlayer != null && mMediaPlayer.getDuration() > 0) { setVisible(true); setLayoutResource(R.layout.video_preference); - - mPreviewResource = attributes.getResourceId( - R.styleable.VideoPreference_preview, 0); - - mMediaPlayer.setOnSeekCompleteListener(mp -> mVideoReady = true); - - mMediaPlayer.setOnPreparedListener(mediaPlayer -> mediaPlayer.setLooping(true)); mAnimationAvailable = true; updateAspectRatio(); } else { @@ -103,30 +124,22 @@ public class VideoPreference extends Preference { R.id.video_container); imageView.setImageResource(mPreviewResource); - layout.setAspectRatio(mAspectRadio); + layout.setAspectRatio(mAspectRatio); + if (mHeight >= LinearLayout.LayoutParams.MATCH_PARENT) { + layout.setLayoutParams(new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, mHeight)); + } + updateViewStates(imageView, playButton); - video.setOnClickListener(v -> { - if (mMediaPlayer != null) { - if (mMediaPlayer.isPlaying()) { - mMediaPlayer.pause(); - playButton.setVisibility(View.VISIBLE); - mVideoPaused = true; - } else { - mMediaPlayer.start(); - playButton.setVisibility(View.GONE); - mVideoPaused = false; - } - } - }); + video.setOnClickListener(v -> updateViewStates(imageView, playButton)); video.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() { @Override public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) { if (mMediaPlayer != null) { - mMediaPlayer.setSurface(new Surface(surfaceTexture)); - mVideoReady = false; - mMediaPlayer.seekTo(0); + mSurface = new Surface(surfaceTexture); + mMediaPlayer.setSurface(mSurface); } } @@ -143,6 +156,9 @@ public class VideoPreference extends Preference { @Override public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { + if (!mViewVisible) { + return; + } if (mVideoReady) { if (imageView.getVisibility() == View.VISIBLE) { imageView.setVisibility(View.GONE); @@ -160,26 +176,77 @@ public class VideoPreference extends Preference { }); } - @Override - public void onDetached() { + @VisibleForTesting + void updateViewStates(ImageView imageView, ImageView playButton) { if (mMediaPlayer != null) { - mMediaPlayer.stop(); - mMediaPlayer.reset(); - mMediaPlayer.release(); + if (mMediaPlayer.isPlaying()) { + mMediaPlayer.pause(); + playButton.setVisibility(View.VISIBLE); + imageView.setVisibility(View.VISIBLE); + mVideoPaused = true; + } else { + imageView.setVisibility(View.GONE); + playButton.setVisibility(View.GONE); + mMediaPlayer.start(); + mVideoPaused = false; + } } + } + + @Override + public void onDetached() { + releaseMediaPlayer(); super.onDetached(); } public void onViewVisible(boolean videoPaused) { + mViewVisible = true; mVideoPaused = videoPaused; - if (mVideoReady && mMediaPlayer != null && !mMediaPlayer.isPlaying()) { - mMediaPlayer.seekTo(0); - } + initMediaPlayer(); } public void onViewInvisible() { - if (mMediaPlayer != null && mMediaPlayer.isPlaying()) { - mMediaPlayer.pause(); + mViewVisible = false; + releaseMediaPlayer(); + } + + /** + * Sets the video for this preference. If a previous video was set this one will override it + * and properly release any resources and re-initialize the preference to play the new video. + * + * @param videoId The raw res id of the video + * @param previewId The drawable res id of the preview image to use if the video fails to load. + */ + public void setVideo(int videoId, int previewId) { + mAnimationId = videoId; + mPreviewResource = previewId; + releaseMediaPlayer(); + initialize(mContext, null); + } + + private void initMediaPlayer() { + if (mMediaPlayer == null) { + mMediaPlayer = MediaPlayer.create(mContext, mVideoPath); + // when the playback res is invalid or others, MediaPlayer create may fail + // and return null, so need add the null judgement. + if (mMediaPlayer != null) { + mMediaPlayer.seekTo(0); + mMediaPlayer.setOnSeekCompleteListener(mp -> mVideoReady = true); + mMediaPlayer.setOnPreparedListener(mediaPlayer -> mediaPlayer.setLooping(true)); + if (mSurface != null) { + mMediaPlayer.setSurface(mSurface); + } + } + } + } + + private void releaseMediaPlayer() { + if (mMediaPlayer != null) { + mMediaPlayer.stop(); + mMediaPlayer.reset(); + mMediaPlayer.release(); + mMediaPlayer = null; + mVideoReady = false; } } @@ -187,9 +254,17 @@ public class VideoPreference extends Preference { return mVideoPaused; } + /** + * sets the height of the video preference + * @param height in dp + */ + public void setHeight(float height) { + mHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, height, + mContext.getResources().getDisplayMetrics()); + } + @VisibleForTesting void updateAspectRatio() { - mAspectRadio = mMediaPlayer.getVideoWidth() / (float)mMediaPlayer.getVideoHeight(); + mAspectRatio = mMediaPlayer.getVideoWidth() / (float) mMediaPlayer.getVideoHeight(); } - } diff --git a/src/com/android/settings/widget/VideoPreferenceController.java b/src/com/android/settings/widget/VideoPreferenceController.java new file mode 100644 index 0000000000..78a837c559 --- /dev/null +++ b/src/com/android/settings/widget/VideoPreferenceController.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2018 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 androidx.preference.PreferenceScreen; + +import com.android.settings.core.BasePreferenceController; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnPause; +import com.android.settingslib.core.lifecycle.events.OnResume; + +public class VideoPreferenceController extends BasePreferenceController implements + LifecycleObserver, OnResume, OnPause { + + private VideoPreference mVideoPreference; + private boolean mVideoPaused; + + public VideoPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE_UNSEARCHABLE; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mVideoPreference = screen.findPreference(getPreferenceKey()); + } + + @Override + public void onPause() { + if (mVideoPreference != null) { + mVideoPaused = mVideoPreference.isVideoPaused(); + mVideoPreference.onViewInvisible(); + } + } + + @Override + public void onResume() { + if (mVideoPreference != null) { + mVideoPreference.onViewVisible(mVideoPaused); + } + } + +} diff --git a/src/com/android/settings/widget/WorkOnlyCategory.java b/src/com/android/settings/widget/WorkOnlyCategory.java index 449916b6cf..063b105dfb 100644 --- a/src/com/android/settings/widget/WorkOnlyCategory.java +++ b/src/com/android/settings/widget/WorkOnlyCategory.java @@ -16,9 +16,10 @@ package com.android.settings.widget; import android.content.Context; import android.os.UserManager; -import androidx.preference.PreferenceCategory; import android.util.AttributeSet; +import androidx.preference.PreferenceCategory; + import com.android.settings.SelfAvailablePreference; import com.android.settings.Utils; diff --git a/src/com/android/settings/wifi/AddNetworkFragment.java b/src/com/android/settings/wifi/AddNetworkFragment.java new file mode 100644 index 0000000000..52497fc3d6 --- /dev/null +++ b/src/com/android/settings/wifi/AddNetworkFragment.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2018 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.wifi; + +import android.app.Activity; +import android.app.settings.SettingsEnums; +import android.content.Intent; +import android.net.wifi.WifiConfiguration; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ImageButton; +import android.widget.TextView; + +import androidx.annotation.VisibleForTesting; + +import com.android.settings.R; +import com.android.settings.core.InstrumentedFragment; +import com.android.settings.wifi.dpp.WifiDppUtils; + +public class AddNetworkFragment extends InstrumentedFragment implements WifiConfigUiBase, + View.OnClickListener { + + final static String WIFI_CONFIG_KEY = "wifi_config_key"; + @VisibleForTesting + final static int SUBMIT_BUTTON_ID = android.R.id.button1; + @VisibleForTesting + final static int CANCEL_BUTTON_ID = android.R.id.button2; + final static int SSID_SCANNER_BUTTON_ID = R.id.ssid_scanner_button; + final static int PASSWORD_SCANNER_BUTTON_ID = R.id.password_scanner_button; + + private static final int REQUEST_CODE_WIFI_DPP_ENROLLEE_QR_CODE_SCANNER = 0; + + private WifiConfigController mUIController; + private Button mSubmitBtn; + private Button mCancelBtn; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.SETTINGS_WIFI_ADD_NETWORK; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + final View rootView = inflater.inflate(R.layout.wifi_add_network_view, container, false); + + final Button neutral = rootView.findViewById(android.R.id.button3); + if (neutral != null) { + neutral.setVisibility(View.GONE); + } + + mSubmitBtn = rootView.findViewById(SUBMIT_BUTTON_ID); + mCancelBtn = rootView.findViewById(CANCEL_BUTTON_ID); + final ImageButton ssidScannerButton = rootView.findViewById(SSID_SCANNER_BUTTON_ID); + final ImageButton passwordScannerButton = rootView.findViewById(PASSWORD_SCANNER_BUTTON_ID); + mSubmitBtn.setOnClickListener(this); + mCancelBtn.setOnClickListener(this); + ssidScannerButton.setOnClickListener(this); + passwordScannerButton.setOnClickListener(this); + mUIController = new WifiConfigController(this, rootView, null, getMode()); + + return rootView; + } + + @Override + public void onViewStateRestored(Bundle savedInstanceState) { + super.onViewStateRestored(savedInstanceState); + mUIController.updatePassword(); + } + + @Override + public void onClick(View view) { + String ssid = null; + + if (view.getId() == SUBMIT_BUTTON_ID) { + handleSubmitAction(); + } else if (view.getId() == CANCEL_BUTTON_ID) { + handleCancelAction(); + } else if (view.getId() == SSID_SCANNER_BUTTON_ID) { + final TextView ssidEditText = getView().findViewById(R.id.ssid); + ssid = ssidEditText.getText().toString(); + // No break and flows to case PASSWORD_SCANNER_BUTTON_ID + + // Launch QR code scanner to join a network. + startActivityForResult(WifiDppUtils.getEnrolleeQrCodeScannerIntent(ssid), + REQUEST_CODE_WIFI_DPP_ENROLLEE_QR_CODE_SCANNER); + } else if (view.getId() + == PASSWORD_SCANNER_BUTTON_ID) {// Launch QR code scanner to join a network. + startActivityForResult(WifiDppUtils.getEnrolleeQrCodeScannerIntent(ssid), + REQUEST_CODE_WIFI_DPP_ENROLLEE_QR_CODE_SCANNER); + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (requestCode == REQUEST_CODE_WIFI_DPP_ENROLLEE_QR_CODE_SCANNER) { + if (resultCode != Activity.RESULT_OK) { + return; + } + + final WifiConfiguration config = data.getParcelableExtra( + WifiDialogActivity.KEY_WIFI_CONFIGURATION); + successfullyFinish(config); + } + } + + @Override + public int getMode() { + return WifiConfigUiBase.MODE_CONNECT; + } + + @Override + public WifiConfigController getController() { + return mUIController; + } + + @Override + public void dispatchSubmit() { + handleSubmitAction(); + } + + @Override + public void setTitle(int id) { + getActivity().setTitle(id); + } + + @Override + public void setTitle(CharSequence title) { + getActivity().setTitle(title); + } + + @Override + public void setSubmitButton(CharSequence text) { + mSubmitBtn.setText(text); + } + + @Override + public void setCancelButton(CharSequence text) { + mCancelBtn.setText(text); + } + + @Override + public void setForgetButton(CharSequence text) { + // AddNetwork doesn't need forget button. + } + + @Override + public Button getSubmitButton() { + return mSubmitBtn; + } + + @Override + public Button getCancelButton() { + return mCancelBtn; + } + + @Override + public Button getForgetButton() { + // AddNetwork doesn't need forget button. + return null; + } + + @VisibleForTesting + void handleSubmitAction() { + successfullyFinish(mUIController.getConfig()); + } + + private void successfullyFinish(WifiConfiguration config) { + final Intent intent = new Intent(); + final Activity activity = getActivity(); + intent.putExtra(WIFI_CONFIG_KEY, config); + activity.setResult(Activity.RESULT_OK, intent); + activity.finish(); + } + + @VisibleForTesting + void handleCancelAction() { + final Activity activity = getActivity(); + activity.setResult(Activity.RESULT_CANCELED); + activity.finish(); + } +} diff --git a/src/com/android/settings/wifi/AddWifiNetworkPreference.java b/src/com/android/settings/wifi/AddWifiNetworkPreference.java new file mode 100644 index 0000000000..8216f86ff6 --- /dev/null +++ b/src/com/android/settings/wifi/AddWifiNetworkPreference.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2019 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.wifi; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.util.Log; +import android.widget.ImageButton; + +import androidx.annotation.DrawableRes; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + +import com.android.settings.R; +import com.android.settings.wifi.dpp.WifiDppUtils; + +/** + * The Preference for users to add Wi-Fi networks in WifiSettings + */ +public class AddWifiNetworkPreference extends Preference { + + private static final String TAG = "AddWifiNetworkPreference"; + + private final Drawable mScanIconDrawable; + + public AddWifiNetworkPreference(Context context) { + super(context); + + setLayoutResource(com.android.settingslib.R.layout.preference_access_point); + setWidgetLayoutResource(R.layout.wifi_button_preference_widget); + setIcon(R.drawable.ic_add_24dp); + setTitle(R.string.wifi_add_network); + + mScanIconDrawable = getDrawable(R.drawable.ic_scan_24dp); + } + + @Override + public void onBindViewHolder(PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + + final ImageButton scanButton = (ImageButton) holder.findViewById(R.id.button_icon); + scanButton.setImageDrawable(mScanIconDrawable); + scanButton.setContentDescription( + getContext().getString(R.string.wifi_dpp_scan_qr_code)); + scanButton.setOnClickListener(view -> { + getContext().startActivity( + WifiDppUtils.getEnrolleeQrCodeScannerIntent(/* ssid */ null)); + }); + } + + private Drawable getDrawable(@DrawableRes int iconResId) { + Drawable buttonIcon = null; + + try { + buttonIcon = getContext().getDrawable(iconResId); + } catch (Resources.NotFoundException exception) { + Log.e(TAG, "Resource does not exist: " + iconResId); + } + return buttonIcon; + } +} diff --git a/src/com/android/settings/wifi/AppStateChangeWifiStateBridge.java b/src/com/android/settings/wifi/AppStateChangeWifiStateBridge.java index c643b8ca05..19687fcfff 100644 --- a/src/com/android/settings/wifi/AppStateChangeWifiStateBridge.java +++ b/src/com/android/settings/wifi/AppStateChangeWifiStateBridge.java @@ -22,7 +22,6 @@ import android.content.Context; import com.android.internal.util.ArrayUtils; import com.android.settings.applications.AppStateAppOpsBridge; -import com.android.settings.applications.AppStateAppOpsBridge.PermissionState; import com.android.settingslib.applications.ApplicationsState; import com.android.settingslib.applications.ApplicationsState.AppEntry; import com.android.settingslib.applications.ApplicationsState.AppFilter; diff --git a/src/com/android/settings/wifi/CellularFallbackPreferenceController.java b/src/com/android/settings/wifi/CellularFallbackPreferenceController.java index 2fefb43e7a..cbb8fb8e26 100644 --- a/src/com/android/settings/wifi/CellularFallbackPreferenceController.java +++ b/src/com/android/settings/wifi/CellularFallbackPreferenceController.java @@ -18,61 +18,34 @@ package com.android.settings.wifi; import android.content.Context; import android.provider.Settings; -import androidx.preference.SwitchPreference; -import androidx.preference.Preference; -import android.text.TextUtils; -import com.android.settings.core.PreferenceControllerMixin; -import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settings.core.TogglePreferenceController; /** - * {@link AbstractPreferenceController} that controls whether we should fall back to celluar when + * CellularFallbackPreferenceController controls whether we should fall back to celluar when * wifi is bad. */ -public class CellularFallbackPreferenceController extends AbstractPreferenceController - implements PreferenceControllerMixin { +public class CellularFallbackPreferenceController extends TogglePreferenceController { - private static final String KEY_CELLULAR_FALLBACK = "wifi_cellular_data_fallback"; - - - public CellularFallbackPreferenceController(Context context) { - super(context); + public CellularFallbackPreferenceController(Context context, String key) { + super(context, key); } @Override - public boolean isAvailable() { - return !avoidBadWifiConfig(); + public int getAvailabilityStatus() { + return !avoidBadWifiConfig() ? AVAILABLE : UNSUPPORTED_ON_DEVICE; } @Override - public String getPreferenceKey() { - return KEY_CELLULAR_FALLBACK; + public boolean isChecked() { + return avoidBadWifiCurrentSettings(); } @Override - public boolean handlePreferenceTreeClick(Preference preference) { - if (!TextUtils.equals(preference.getKey(), KEY_CELLULAR_FALLBACK)) { - return false; - } - if (!(preference instanceof SwitchPreference)) { - return false; - } + public boolean setChecked(boolean isChecked) { // On: avoid bad wifi. Off: prompt. - String settingName = Settings.Global.NETWORK_AVOID_BAD_WIFI; - Settings.Global.putString(mContext.getContentResolver(), settingName, - ((SwitchPreference) preference).isChecked() ? "1" : null); - return true; - } - - @Override - public void updateState(Preference preference) { - final boolean currentSetting = avoidBadWifiCurrentSettings(); - // TODO: can this ever be null? The return value of avoidBadWifiConfig() can only - // change if the resources change, but if that happens the activity will be recreated... - if (preference != null) { - SwitchPreference pref = (SwitchPreference) preference; - pref.setChecked(currentSetting); - } + return Settings.Global.putString(mContext.getContentResolver(), + Settings.Global.NETWORK_AVOID_BAD_WIFI, isChecked ? "1" : null); } private boolean avoidBadWifiConfig() { @@ -84,4 +57,4 @@ public class CellularFallbackPreferenceController extends AbstractPreferenceCont return "1".equals(Settings.Global.getString(mContext.getContentResolver(), Settings.Global.NETWORK_AVOID_BAD_WIFI)); } -} +}
\ No newline at end of file diff --git a/src/com/android/settings/wifi/ChangeWifiStateDetails.java b/src/com/android/settings/wifi/ChangeWifiStateDetails.java index 1c59ddd02b..9841615a69 100644 --- a/src/com/android/settings/wifi/ChangeWifiStateDetails.java +++ b/src/com/android/settings/wifi/ChangeWifiStateDetails.java @@ -16,28 +16,22 @@ package com.android.settings.wifi; -import android.Manifest.permission; -import android.app.AlertDialog; import android.app.AppOpsManager; +import android.app.settings.SettingsEnums; import android.content.Context; import android.os.Bundle; -import androidx.preference.SwitchPreference; + +import androidx.appcompat.app.AlertDialog; import androidx.preference.Preference; import androidx.preference.Preference.OnPreferenceChangeListener; +import androidx.preference.SwitchPreference; -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.settings.R; import com.android.settings.applications.AppInfoWithHeader; import com.android.settings.applications.AppStateAppOpsBridge.PermissionState; -import com.android.settings.R; import com.android.settings.overlay.FeatureFactory; - import com.android.settings.wifi.AppStateChangeWifiStateBridge.WifiSettingsState; import com.android.settingslib.applications.ApplicationsState.AppEntry; -import com.android.settingslib.applications.ApplicationsState.AppFilter; - -import static android.app.AppOpsManager.MODE_ALLOWED; -import static android.app.AppOpsManager.MODE_IGNORED; public class ChangeWifiStateDetails extends AppInfoWithHeader implements OnPreferenceChangeListener { @@ -75,7 +69,7 @@ public class ChangeWifiStateDetails extends AppInfoWithHeader @Override public int getMetricsCategory() { - return MetricsEvent.CONFIGURE_WIFI; + return SettingsEnums.CONFIGURE_WIFI; } @Override @@ -99,8 +93,8 @@ public class ChangeWifiStateDetails extends AppInfoWithHeader } protected void logSpecialPermissionChange(boolean newState, String packageName) { - int logCategory = newState ? MetricsEvent.APP_SPECIAL_PERMISSION_SETTINGS_CHANGE_ALLOW - : MetricsEvent.APP_SPECIAL_PERMISSION_SETTINGS_CHANGE_DENY; + int logCategory = newState ? SettingsEnums.APP_SPECIAL_PERMISSION_SETTINGS_CHANGE_ALLOW + : SettingsEnums.APP_SPECIAL_PERMISSION_SETTINGS_CHANGE_DENY; FeatureFactory.getFactory(getContext()).getMetricsFeatureProvider().action(getContext(), logCategory, packageName); } diff --git a/src/com/android/settings/wifi/ConfigureWifiSettings.java b/src/com/android/settings/wifi/ConfigureWifiSettings.java index 818b295e0b..718f7fb196 100644 --- a/src/com/android/settings/wifi/ConfigureWifiSettings.java +++ b/src/com/android/settings/wifi/ConfigureWifiSettings.java @@ -17,6 +17,7 @@ package com.android.settings.wifi; import static android.content.Context.WIFI_SERVICE; +import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; import android.net.ConnectivityManager; @@ -24,18 +25,19 @@ import android.net.NetworkInfo; import android.net.wifi.WifiManager; import android.provider.SearchIndexableResource; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; import com.android.settings.wifi.p2p.WifiP2pPreferenceController; import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +@SearchIndexable public class ConfigureWifiSettings extends DashboardFragment { private static final String TAG = "ConfigureWifiSettings"; @@ -48,7 +50,7 @@ public class ConfigureWifiSettings extends DashboardFragment { @Override public int getMetricsCategory() { - return MetricsEvent.CONFIGURE_WIFI; + return SettingsEnums.CONFIGURE_WIFI; } @Override @@ -72,17 +74,20 @@ public class ConfigureWifiSettings extends DashboardFragment { @Override protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { - mWifiWakeupPreferenceController = new WifiWakeupPreferenceController(context, this); + mWifiWakeupPreferenceController = new WifiWakeupPreferenceController(context, this, + getSettingsLifecycle()); mUseOpenWifiPreferenceController = new UseOpenWifiPreferenceController(context, this, - getLifecycle()); + getSettingsLifecycle()); final WifiManager wifiManager = (WifiManager) getSystemService(WIFI_SERVICE); final List<AbstractPreferenceController> controllers = new ArrayList<>(); controllers.add(mWifiWakeupPreferenceController); - controllers.add(new NotifyOpenNetworksPreferenceController(context, getLifecycle())); + controllers.add(new NotifyOpenNetworksPreferenceController(context, + getSettingsLifecycle())); controllers.add(mUseOpenWifiPreferenceController); - controllers.add(new WifiInfoPreferenceController(context, getLifecycle(), wifiManager)); - controllers.add(new CellularFallbackPreferenceController(context)); - controllers.add(new WifiP2pPreferenceController(context, getLifecycle(), wifiManager)); + controllers.add(new WifiInfoPreferenceController(context, getSettingsLifecycle(), + wifiManager)); + controllers.add(new WifiP2pPreferenceController(context, getSettingsLifecycle(), + wifiManager)); return controllers; } diff --git a/src/com/android/settings/wifi/ConnectedAccessPointPreference.java b/src/com/android/settings/wifi/ConnectedAccessPointPreference.java index 1e3723e357..c7953c3f69 100644 --- a/src/com/android/settings/wifi/ConnectedAccessPointPreference.java +++ b/src/com/android/settings/wifi/ConnectedAccessPointPreference.java @@ -17,26 +17,28 @@ package com.android.settings.wifi; import android.content.Context; +import android.view.View; + import androidx.annotation.DrawableRes; +import androidx.fragment.app.Fragment; import androidx.preference.PreferenceViewHolder; -import android.view.View; import com.android.settings.R; import com.android.settingslib.wifi.AccessPoint; -import com.android.settingslib.wifi.AccessPointPreference; /** * An AP preference for the currently connected AP */ -public class ConnectedAccessPointPreference extends AccessPointPreference implements +public class ConnectedAccessPointPreference extends LongPressAccessPointPreference implements View.OnClickListener { private OnGearClickListener mOnGearClickListener; private boolean mIsCaptivePortal; public ConnectedAccessPointPreference(AccessPoint accessPoint, Context context, - UserBadgeCache cache, @DrawableRes int iconResId, boolean forSavedNetworks) { - super(accessPoint, context, cache, iconResId, forSavedNetworks); + UserBadgeCache cache, @DrawableRes int iconResId, boolean forSavedNetworks, + Fragment fragment) { + super(accessPoint, context, cache, forSavedNetworks, iconResId, fragment); } @Override diff --git a/src/com/android/settings/wifi/LinkablePreference.java b/src/com/android/settings/wifi/LinkablePreference.java index e4cb95bceb..9581e7a6ed 100644 --- a/src/com/android/settings/wifi/LinkablePreference.java +++ b/src/com/android/settings/wifi/LinkablePreference.java @@ -17,15 +17,19 @@ package com.android.settings.wifi; import android.content.Context; -import androidx.annotation.Nullable; -import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; import android.text.Spannable; import android.text.method.LinkMovementMethod; import android.text.style.TextAppearanceSpan; import android.util.AttributeSet; import android.widget.TextView; + +import androidx.annotation.Nullable; +import androidx.core.content.res.TypedArrayUtils; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + import com.android.settings.LinkifyUtils; +import com.android.settingslib.R; /** * A preference with a title that can have linkable content on click. @@ -36,19 +40,20 @@ public class LinkablePreference extends Preference { private CharSequence mContentTitle; private CharSequence mContentDescription; + public LinkablePreference(Context ctx, AttributeSet attrs, int defStyle) { super(ctx, attrs, defStyle); + setIcon(R.drawable.ic_info_outline_24dp); setSelectable(false); } public LinkablePreference(Context ctx, AttributeSet attrs) { - super(ctx, attrs); - setSelectable(false); + this(ctx, attrs, TypedArrayUtils.getAttr( + ctx, R.attr.footerPreferenceStyle, android.R.attr.preferenceStyle)); } public LinkablePreference(Context ctx) { - super(ctx); - setSelectable(false); + this(ctx, null); } @Override @@ -73,21 +78,20 @@ public class LinkablePreference extends Preference { boolean linked = LinkifyUtils.linkify(textView, contentBuilder, mClickListener); if (linked && mContentTitle != null) { - // Embolden and enlarge the title. - Spannable boldSpan = (Spannable) textView.getText(); - boldSpan.setSpan( - new TextAppearanceSpan( - getContext(), android.R.style.TextAppearance_Medium), + Spannable spannableContent = (Spannable) textView.getText(); + spannableContent.setSpan( + new TextAppearanceSpan(getContext(), android.R.style.TextAppearance_Small), 0, mContentTitle.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE); - textView.setText(boldSpan); + textView.setText(spannableContent); textView.setMovementMethod(new LinkMovementMethod()); } } /** * Sets the linkable text for the Preference title. + * * @param contentTitle text to set the Preference title. * @param contentDescription description text to append underneath title, can be null. * @param clickListener OnClickListener for the link portion of the text. diff --git a/src/com/android/settings/wifi/LongPressAccessPointPreference.java b/src/com/android/settings/wifi/LongPressAccessPointPreference.java index 213589b9de..c3c08f0c40 100644 --- a/src/com/android/settings/wifi/LongPressAccessPointPreference.java +++ b/src/com/android/settings/wifi/LongPressAccessPointPreference.java @@ -15,11 +15,11 @@ */ package com.android.settings.wifi; -import android.app.Fragment; import android.content.Context; + +import androidx.fragment.app.Fragment; import androidx.preference.PreferenceViewHolder; -import android.widget.ImageView; -import com.android.settings.R; + import com.android.settingslib.wifi.AccessPoint; import com.android.settingslib.wifi.AccessPointPreference; @@ -28,12 +28,6 @@ public class LongPressAccessPointPreference extends AccessPointPreference { private final Fragment mFragment; public LongPressAccessPointPreference(AccessPoint accessPoint, Context context, - UserBadgeCache cache, boolean forSavedNetworks, Fragment fragment) { - super(accessPoint, context, cache, forSavedNetworks); - mFragment = fragment; - } - - public LongPressAccessPointPreference(AccessPoint accessPoint, Context context, UserBadgeCache cache, boolean forSavedNetworks, int iconResId, Fragment fragment) { super(accessPoint, context, cache, iconResId, forSavedNetworks); mFragment = fragment; diff --git a/src/com/android/settings/wifi/NetworkRequestDialogActivity.java b/src/com/android/settings/wifi/NetworkRequestDialogActivity.java new file mode 100644 index 0000000000..5d89f7771a --- /dev/null +++ b/src/com/android/settings/wifi/NetworkRequestDialogActivity.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2018 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.wifi; + +import android.os.Bundle; + +import androidx.annotation.Nullable; +import androidx.fragment.app.FragmentActivity; + +/** + * When other applications request to have a wifi connection, framework will bring up this activity + * to let user select which wifi ap wanna to connect. This activity is just a door for framework + * call, and main functional process is at {@code NetworkRequestDialogFragment}. + */ +public class NetworkRequestDialogActivity extends FragmentActivity { + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + final NetworkRequestDialogFragment fragment = NetworkRequestDialogFragment.newInstance(); + fragment.show(getSupportFragmentManager(), "NetworkRequestDialogFragment"); + } +} diff --git a/src/com/android/settings/wifi/NetworkRequestDialogFragment.java b/src/com/android/settings/wifi/NetworkRequestDialogFragment.java new file mode 100644 index 0000000000..eb7d78fd78 --- /dev/null +++ b/src/com/android/settings/wifi/NetworkRequestDialogFragment.java @@ -0,0 +1,576 @@ +/* + * Copyright (C) 2018 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.wifi; + +import android.app.Activity; +import android.app.Dialog; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.net.wifi.ScanResult; +import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiManager; +import android.net.wifi.WifiManager.NetworkRequestMatchCallback; +import android.net.wifi.WifiManager.NetworkRequestUserSelectionCallback; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.BaseAdapter; +import android.widget.Button; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; +import androidx.preference.internal.PreferenceImageView; + +import com.android.settings.R; +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; +import com.android.settings.wifi.NetworkRequestErrorDialogFragment.ERROR_DIALOG_TYPE; +import com.android.settingslib.Utils; +import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.wifi.AccessPoint; +import com.android.settingslib.wifi.WifiTracker; +import com.android.settingslib.wifi.WifiTrackerFactory; + +import java.util.ArrayList; +import java.util.List; + +/** + * The Fragment sets up callback {@link NetworkRequestMatchCallback} with framework. To handle most + * behaviors of the callback when requesting wifi network, except for error message. When error + * happens, {@link NetworkRequestErrorDialogFragment} will be called to display error message. + */ +public class NetworkRequestDialogFragment extends InstrumentedDialogFragment implements + DialogInterface.OnClickListener, NetworkRequestMatchCallback { + + /** Message sent to us to stop scanning wifi and pop up timeout dialog. */ + private static final int MESSAGE_STOP_SCAN_WIFI_LIST = 0; + + /** + * Spec defines there should be 5 wifi ap on the list at most or just show all if {@code + * mShowLimitedItem} is false. + */ + private static final int MAX_NUMBER_LIST_ITEM = 5; + private boolean mShowLimitedItem = true; + + /** Delayed time to stop scanning wifi. */ + private static final int DELAY_TIME_STOP_SCAN_MS = 30 * 1000; + + @VisibleForTesting + final static String EXTRA_APP_NAME = "com.android.settings.wifi.extra.APP_NAME"; + final static String EXTRA_IS_SPECIFIED_SSID = + "com.android.settings.wifi.extra.REQUEST_IS_FOR_SINGLE_NETWORK"; + + private List<AccessPoint> mAccessPointList; + private FilterWifiTracker mFilterWifiTracker; + private AccessPointAdapter mDialogAdapter; + private NetworkRequestUserSelectionCallback mUserSelectionCallback; + private boolean mIsSpecifiedSsid; + private boolean mWaitingConnectCallback; + + public static NetworkRequestDialogFragment newInstance() { + NetworkRequestDialogFragment dialogFragment = new NetworkRequestDialogFragment(); + return dialogFragment; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Context context = getContext(); + + // Prepares title. + final LayoutInflater inflater = LayoutInflater.from(context); + final View customTitle = inflater.inflate(R.layout.network_request_dialog_title, null); + + final TextView title = customTitle.findViewById(R.id.network_request_title_text); + title.setText(getTitle()); + + final Intent intent = getActivity().getIntent(); + if (intent != null) { + mIsSpecifiedSsid = intent.getBooleanExtra(EXTRA_IS_SPECIFIED_SSID, false); + } + + final ProgressBar progressBar = customTitle.findViewById( + R.id.network_request_title_progress); + progressBar.setVisibility(View.VISIBLE); + + // Prepares adapter. + mDialogAdapter = new AccessPointAdapter(context, + R.layout.preference_access_point, getAccessPointList()); + + final AlertDialog.Builder builder = new AlertDialog.Builder(context) + .setCustomTitle(customTitle) + .setAdapter(mDialogAdapter, this) + .setNegativeButton(R.string.cancel, (dialog, which) -> onCancel(dialog)) + // Do nothings, will replace the onClickListener to avoid auto closing dialog. + .setNeutralButton(R.string.network_connection_request_dialog_showall, + null /* OnClickListener */); + if (mIsSpecifiedSsid) { + builder.setPositiveButton(R.string.wifi_connect, null /* OnClickListener */); + } + + // Clicking list item is to connect wifi ap. + final AlertDialog dialog = builder.create(); + dialog.getListView() + .setOnItemClickListener( + (parent, view, position, id) -> this.onClick(dialog, position)); + + // Don't dismiss dialog when touching outside. User reports it is easy to touch outside. + // This causes dialog to close. + setCancelable(false); + + dialog.setOnShowListener((dialogInterface) -> { + // Replace NeutralButton onClickListener to avoid closing dialog + final Button neutralBtn = dialog.getButton(AlertDialog.BUTTON_NEUTRAL); + neutralBtn.setVisibility(View.GONE); + neutralBtn.setOnClickListener(v -> { + mShowLimitedItem = false; + renewAccessPointList(null /* List<ScanResult> */); + notifyAdapterRefresh(); + neutralBtn.setVisibility(View.GONE); + }); + + // Replace Positive onClickListener to avoid closing dialog + if (mIsSpecifiedSsid) { + final Button positiveBtn = dialog.getButton(AlertDialog.BUTTON_POSITIVE); + positiveBtn.setOnClickListener(v -> { + // When clicking connect button, should connect to the first and the only one + // list item. + this.onClick(dialog, 0 /* position */); + }); + // Disable button in first, and enable it after there are some accesspoints in list. + positiveBtn.setEnabled(false); + } + }); + return dialog; + } + + private String getTitle() { + final Intent intent = getActivity().getIntent(); + String appName = ""; + if (intent != null) { + appName = intent.getStringExtra(EXTRA_APP_NAME); + } + + return getString(R.string.network_connection_request_dialog_title, appName); + } + + @NonNull + List<AccessPoint> getAccessPointList() { + // Initials list for adapter, in case of display crashing. + if (mAccessPointList == null) { + mAccessPointList = new ArrayList<>(); + } + return mAccessPointList; + } + + private BaseAdapter getDialogAdapter() { + return mDialogAdapter; + } + + @Override + public void onClick(DialogInterface dialog, int which) { + final List<AccessPoint> accessPointList = getAccessPointList(); + if (accessPointList.size() == 0) { + return; // Invalid values. + } + if (mUserSelectionCallback == null) { + return; // Callback is missing or not ready. + } + + if (which < accessPointList.size()) { + final AccessPoint selectedAccessPoint = accessPointList.get(which); + WifiConfiguration wifiConfig = selectedAccessPoint.getConfig(); + if (wifiConfig == null) { + wifiConfig = WifiUtils.getWifiConfig(selectedAccessPoint, /* scanResult */ + null, /* password */ null); + } + + if (wifiConfig != null) { + mUserSelectionCallback.select(wifiConfig); + + mWaitingConnectCallback = true; + updateConnectButton(false); + } + } + } + + @Override + public void onCancel(@NonNull DialogInterface dialog) { + super.onCancel(dialog); + // Finishes the activity when user clicks back key or outside of the dialog. + if (getActivity() != null) { + getActivity().finish(); + } + if (mUserSelectionCallback != null) { + mUserSelectionCallback.reject(); + } + } + + @Override + public void onPause() { + super.onPause(); + + mHandler.removeMessages(MESSAGE_STOP_SCAN_WIFI_LIST); + final WifiManager wifiManager = getContext().getApplicationContext() + .getSystemService(WifiManager.class); + if (wifiManager != null) { + wifiManager.unregisterNetworkRequestMatchCallback(this); + } + + if (mFilterWifiTracker != null) { + mFilterWifiTracker.onPause(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + + if (mFilterWifiTracker != null) { + mFilterWifiTracker.onDestroy(); + mFilterWifiTracker = null; + } + } + + private void showAllButton() { + final AlertDialog alertDialog = (AlertDialog) getDialog(); + if (alertDialog == null) { + return; + } + + final Button neutralBtn = alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL); + if (neutralBtn != null) { + neutralBtn.setVisibility(View.VISIBLE); + } + } + + private void updateConnectButton(boolean enabled) { + // The button is only showed in single SSID mode. + if (!mIsSpecifiedSsid) { + return; + } + + final AlertDialog alertDialog = (AlertDialog) getDialog(); + if (alertDialog == null) { + return; + } + + final Button positiveBtn = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); + if (positiveBtn != null) { + positiveBtn.setEnabled(enabled); + } + } + + private void hideProgressIcon() { + final AlertDialog alertDialog = (AlertDialog) getDialog(); + if (alertDialog == null) { + return; + } + + final View progress = alertDialog.findViewById(R.id.network_request_title_progress); + if (progress != null) { + progress.setVisibility(View.GONE); + } + } + + @Override + public void onResume() { + super.onResume(); + + final WifiManager wifiManager = getContext().getApplicationContext() + .getSystemService(WifiManager.class); + if (wifiManager != null) { + wifiManager.registerNetworkRequestMatchCallback(this, mHandler); + } + // Sets time-out to stop scanning. + mHandler.sendEmptyMessageDelayed(MESSAGE_STOP_SCAN_WIFI_LIST, DELAY_TIME_STOP_SCAN_MS); + + if (mFilterWifiTracker == null) { + mFilterWifiTracker = new FilterWifiTracker(getActivity(), getSettingsLifecycle()); + } + mFilterWifiTracker.onResume(); + } + + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MESSAGE_STOP_SCAN_WIFI_LIST: + removeMessages(MESSAGE_STOP_SCAN_WIFI_LIST); + stopScanningAndPopErrorDialog(ERROR_DIALOG_TYPE.TIME_OUT); + break; + default: + // Do nothing. + break; + } + } + }; + + protected void stopScanningAndPopErrorDialog(ERROR_DIALOG_TYPE type) { + // Dismisses current dialog. + final Dialog dialog = getDialog(); + if (dialog != null && dialog.isShowing()) { + dismiss(); + } + + // Throws error dialog. + final NetworkRequestErrorDialogFragment fragment = NetworkRequestErrorDialogFragment + .newInstance(); + final Bundle bundle = new Bundle(); + bundle.putSerializable(NetworkRequestErrorDialogFragment.DIALOG_TYPE, type); + fragment.setArguments(bundle); + fragment.show(getActivity().getSupportFragmentManager(), + NetworkRequestDialogFragment.class.getSimpleName()); + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.WIFI_SCANNING_NEEDED_DIALOG; + } + + private class AccessPointAdapter extends ArrayAdapter<AccessPoint> { + + private final int mResourceId; + private final LayoutInflater mInflater; + + public AccessPointAdapter(Context context, int resourceId, List<AccessPoint> objects) { + super(context, resourceId, objects); + mResourceId = resourceId; + mInflater = LayoutInflater.from(context); + } + + @Override + public View getView(int position, View view, ViewGroup parent) { + if (view == null) { + view = mInflater.inflate(mResourceId, parent, false); + + final View divider = view.findViewById( + com.android.settingslib.R.id.two_target_divider); + divider.setVisibility(View.GONE); + } + + final AccessPoint accessPoint = getItem(position); + + final TextView titleView = view.findViewById(android.R.id.title); + if (titleView != null) { + // Shows whole SSID for better UX. + titleView.setSingleLine(false); + titleView.setText(accessPoint.getTitle()); + } + + final TextView summary = view.findViewById(android.R.id.summary); + if (summary != null) { + final String summaryString = accessPoint.getSettingsSummary(); + if (TextUtils.isEmpty(summaryString)) { + summary.setVisibility(View.GONE); + } else { + summary.setVisibility(View.VISIBLE); + summary.setText(summaryString); + } + } + + final PreferenceImageView imageView = view.findViewById(android.R.id.icon); + final int level = accessPoint.getLevel(); + if (imageView != null) { + final Drawable drawable = getContext().getDrawable( + Utils.getWifiIconResource(level)); + drawable.setTintList( + Utils.getColorAttr(getContext(), android.R.attr.colorControlNormal)); + imageView.setImageDrawable(drawable); + } + + return view; + } + } + + @Override + public void onAbort() { + stopScanningAndPopErrorDialog(ERROR_DIALOG_TYPE.ABORT); + } + + @Override + public void onUserSelectionCallbackRegistration( + NetworkRequestUserSelectionCallback userSelectionCallback) { + mUserSelectionCallback = userSelectionCallback; + } + + @Override + public void onMatch(List<ScanResult> scanResults) { + // Shouldn't need to renew cached list, since input result is empty. + if (scanResults != null && scanResults.size() > 0) { + mHandler.removeMessages(MESSAGE_STOP_SCAN_WIFI_LIST); + renewAccessPointList(scanResults); + + notifyAdapterRefresh(); + } + } + + // Updates internal AccessPoint list from WifiTracker. scanResults are used to update key list + // of AccessPoint, and could be null if there is no necessary to update key list. + private void renewAccessPointList(List<ScanResult> scanResults) { + if (mFilterWifiTracker == null) { + return; + } + + // TODO(b/119846365): Checks if we could escalate the converting effort. + // Updates keys of scanResults into FilterWifiTracker for updating matched AccessPoints. + if (scanResults != null) { + mFilterWifiTracker.updateKeys(scanResults); + } + + // Re-gets matched AccessPoints from WifiTracker. + final List<AccessPoint> list = getAccessPointList(); + list.clear(); + list.addAll(mFilterWifiTracker.getAccessPoints()); + } + + @VisibleForTesting + void notifyAdapterRefresh() { + if (getDialogAdapter() != null) { + getDialogAdapter().notifyDataSetChanged(); + } + } + + @Override + public void onUserSelectionConnectSuccess(WifiConfiguration wificonfiguration) { + final Activity activity = getActivity(); + if (activity != null) { + Toast.makeText(activity, R.string.network_connection_connect_successful, + Toast.LENGTH_SHORT).show(); + activity.finish(); + } + } + + @Override + public void onUserSelectionConnectFailure(WifiConfiguration wificonfiguration) { + // Do nothing when selection is failed, let user could try again easily. + mWaitingConnectCallback = false; + updateConnectButton(true); + } + + private final class FilterWifiTracker { + private final List<String> mAccessPointKeys; + private final WifiTracker mWifiTracker; + + public FilterWifiTracker(Context context, Lifecycle lifecycle) { + mWifiTracker = WifiTrackerFactory.create(context, mWifiListener, + lifecycle, /* includeSaved */ true, /* includeScans */ true); + mAccessPointKeys = new ArrayList<>(); + } + + /** + * Updates key list from input. {@code onMatch()} may be called in multi-times according + * wifi scanning result, so needs patchwork here. + */ + public void updateKeys(List<ScanResult> scanResults) { + for (ScanResult scanResult : scanResults) { + final String key = AccessPoint.getKey(scanResult); + if (!mAccessPointKeys.contains(key)) { + mAccessPointKeys.add(key); + } + } + } + + /** + * Returns only AccessPoints whose key is in {@code mAccessPointKeys}. + * + * @return List of matched AccessPoints. + */ + public List<AccessPoint> getAccessPoints() { + final List<AccessPoint> allAccessPoints = mWifiTracker.getAccessPoints(); + final List<AccessPoint> result = new ArrayList<>(); + + // The order should be kept, because order means wifi score (sorting in WifiTracker). + int count = 0; + for (AccessPoint accessPoint : allAccessPoints) { + final String key = accessPoint.getKey(); + if (mAccessPointKeys.contains(key)) { + result.add(accessPoint); + + count++; + // Limits how many count of items could show. + if (mShowLimitedItem && count >= MAX_NUMBER_LIST_ITEM) { + break; + } + } + } + + // Update related UI buttons + if (mShowLimitedItem && (count >= MAX_NUMBER_LIST_ITEM)) { + showAllButton(); + } + if (count > 0) { + hideProgressIcon(); + } + // Enable connect button if there is Accesspoint item, except for the situation that + // user click but connected status doesn't come back yet. + if (count < 0) { + updateConnectButton(false); + } else if (!mWaitingConnectCallback) { + updateConnectButton(true); + } + + return result; + } + + private WifiTracker.WifiListener mWifiListener = new WifiTracker.WifiListener() { + + @Override + public void onWifiStateChanged(int state) { + notifyAdapterRefresh(); + } + + @Override + public void onConnectedChanged() { + notifyAdapterRefresh(); + } + + @Override + public void onAccessPointsChanged() { + notifyAdapterRefresh(); + } + }; + + public void onDestroy() { + if (mWifiTracker != null) { + mWifiTracker.onDestroy(); + } + } + + public void onResume() { + if (mWifiTracker != null) { + mWifiTracker.onStart(); + } + } + + public void onPause() { + if (mWifiTracker != null) { + mWifiTracker.onStop(); + } + } + } +} diff --git a/src/com/android/settings/wifi/NetworkRequestErrorDialogFragment.java b/src/com/android/settings/wifi/NetworkRequestErrorDialogFragment.java new file mode 100644 index 0000000000..261d3131e4 --- /dev/null +++ b/src/com/android/settings/wifi/NetworkRequestErrorDialogFragment.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2018 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.wifi; + +import android.app.Dialog; +import android.app.settings.SettingsEnums; +import android.content.DialogInterface; +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; + +import com.android.settings.R; +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; + +/** + * The dialog shows an error message when requesting network {@link NetworkRequestDialogFragment}. + * Contains multi-error types in {@code ERROR_DIALOG_TYPE}. + */ +public class NetworkRequestErrorDialogFragment extends InstrumentedDialogFragment { + + public static final String DIALOG_TYPE = "DIALOG_ERROR_TYPE"; + + public enum ERROR_DIALOG_TYPE {TIME_OUT, ABORT} + + public static NetworkRequestErrorDialogFragment newInstance() { + return new NetworkRequestErrorDialogFragment(); + } + + private NetworkRequestErrorDialogFragment() { + super(); + } + + @Override + public void onCancel(@NonNull DialogInterface dialog) { + super.onCancel(dialog); + // Wants to finish the activity when user clicks back key or outside of the dialog. + getActivity().finish(); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + // Gets error type to construct dialog. Default is TIME_OUT dialog. + ERROR_DIALOG_TYPE msgType = ERROR_DIALOG_TYPE.TIME_OUT; + if (getArguments() != null) { + msgType = (ERROR_DIALOG_TYPE) getArguments().getSerializable(DIALOG_TYPE); + } + + final AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); + if (msgType == ERROR_DIALOG_TYPE.TIME_OUT) { + builder.setMessage(R.string.network_connection_timeout_dialog_message) + .setPositiveButton(R.string.network_connection_timeout_dialog_ok, + (dialog, which) -> startScanningDialog()) + .setNegativeButton(R.string.cancel, (dialog, which) -> getActivity().finish()); + } else { + builder.setMessage(R.string.network_connection_errorstate_dialog_message) + .setPositiveButton(R.string.okay, (dialog, which) -> getActivity().finish()); + } + return builder.create(); + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.WIFI_SCANNING_NEEDED_DIALOG; + } + + protected void startScanningDialog() { + final NetworkRequestDialogFragment fragment = NetworkRequestDialogFragment.newInstance(); + fragment.show(getActivity().getSupportFragmentManager(), + NetworkRequestErrorDialogFragment.class.getSimpleName()); + } +} diff --git a/src/com/android/settings/wifi/NotifyOpenNetworksPreferenceController.java b/src/com/android/settings/wifi/NotifyOpenNetworksPreferenceController.java index baf273c21d..a46a82873a 100644 --- a/src/com/android/settings/wifi/NotifyOpenNetworksPreferenceController.java +++ b/src/com/android/settings/wifi/NotifyOpenNetworksPreferenceController.java @@ -22,10 +22,11 @@ import android.database.ContentObserver; import android.net.Uri; import android.os.Handler; import android.provider.Settings; -import androidx.preference.SwitchPreference; +import android.text.TextUtils; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.text.TextUtils; +import androidx.preference.SwitchPreference; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.core.AbstractPreferenceController; diff --git a/src/com/android/settings/wifi/RequestToggleWiFiActivity.java b/src/com/android/settings/wifi/RequestToggleWiFiActivity.java index c45db69ba0..879a93e328 100644 --- a/src/com/android/settings/wifi/RequestToggleWiFiActivity.java +++ b/src/com/android/settings/wifi/RequestToggleWiFiActivity.java @@ -23,13 +23,16 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ApplicationInfo; +import android.content.pm.PackageItemInfo; import android.content.pm.PackageManager; import android.net.wifi.WifiManager; import android.os.Bundle; -import androidx.annotation.NonNull; import android.text.TextUtils; import android.util.Log; import android.widget.Toast; + +import androidx.annotation.NonNull; + import com.android.internal.app.AlertActivity; import com.android.settings.R; @@ -80,7 +83,9 @@ public class RequestToggleWiFiActivity extends AlertActivity try { ApplicationInfo applicationInfo = getPackageManager().getApplicationInfo( packageName, 0); - mAppLabel = applicationInfo.loadSafeLabel(getPackageManager()); + mAppLabel = applicationInfo.loadSafeLabel(getPackageManager(), + PackageItemInfo.DEFAULT_MAX_LABEL_SIZE_PX, PackageItemInfo.SAFE_LABEL_FLAG_TRIM + | PackageItemInfo.SAFE_LABEL_FLAG_FIRST_LINE); } catch (PackageManager.NameNotFoundException e) { Log.e(LOG_TAG, "Couldn't find app with package name " + packageName); finish(); diff --git a/src/com/android/settings/wifi/SavedAccessPointsWifiSettings.java b/src/com/android/settings/wifi/SavedAccessPointsWifiSettings.java deleted file mode 100644 index 25c97b203e..0000000000 --- a/src/com/android/settings/wifi/SavedAccessPointsWifiSettings.java +++ /dev/null @@ -1,302 +0,0 @@ -/* - * 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.wifi; - -import android.annotation.Nullable; -import android.app.Activity; -import android.app.Dialog; -import android.content.Context; -import android.icu.text.Collator; -import android.net.wifi.WifiManager; -import android.os.Bundle; -import android.os.Handler; -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; -import android.util.Log; -import android.widget.Toast; - -import com.android.internal.logging.nano.MetricsProto; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.settings.R; -import com.android.settings.SettingsPreferenceFragment; -import com.android.settings.search.Indexable; -import com.android.settingslib.wifi.AccessPoint; -import com.android.settingslib.wifi.AccessPointPreference; -import com.android.settingslib.wifi.WifiSavedConfigUtils; - -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -/** - * UI to manage saved networks/access points. - * TODO(b/64806699): convert to {@link DashboardFragment} with {@link PreferenceController}s - */ -public class SavedAccessPointsWifiSettings extends SettingsPreferenceFragment - implements Indexable, WifiDialog.WifiDialogListener { - private static final String TAG = "SavedAccessPoints"; - @VisibleForTesting - static final int MSG_UPDATE_PREFERENCES = 1; - private static final Comparator<AccessPoint> SAVED_NETWORK_COMPARATOR = - new Comparator<AccessPoint>() { - final Collator mCollator = Collator.getInstance(); - @Override - public int compare(AccessPoint ap1, AccessPoint ap2) { - return mCollator.compare( - nullToEmpty(ap1.getConfigName()), nullToEmpty(ap2.getConfigName())); - } - - private String nullToEmpty(String string) { - return (string == null) ? "" : string; - } - }; - - @VisibleForTesting - final WifiManager.ActionListener mForgetListener = new WifiManager.ActionListener() { - @Override - public void onSuccess() { - postUpdatePreference(); - } - - @Override - public void onFailure(int reason) { - postUpdatePreference(); - } - }; - - @VisibleForTesting - final Handler mHandler = new Handler() { - @Override - public void handleMessage(android.os.Message msg) { - if (msg.what == MSG_UPDATE_PREFERENCES) { - initPreferences(); - } - } - }; - - private final WifiManager.ActionListener mSaveListener = new WifiManager.ActionListener() { - @Override - public void onSuccess() { - postUpdatePreference(); - } - @Override - public void onFailure(int reason) { - Activity activity = getActivity(); - if (activity != null) { - Toast.makeText(activity, - R.string.wifi_failed_save_message, - Toast.LENGTH_SHORT).show(); - } - } - }; - - private WifiDialog mDialog; - private WifiManager mWifiManager; - private AccessPoint mDlgAccessPoint; - private Bundle mAccessPointSavedState; - private AccessPoint mSelectedAccessPoint; - private Preference mAddNetworkPreference; - - private AccessPointPreference.UserBadgeCache mUserBadgeCache; - - // Instance state key - private static final String SAVE_DIALOG_ACCESS_POINT_STATE = "wifi_ap_state"; - - @Override - public int getMetricsCategory() { - return MetricsEvent.WIFI_SAVED_ACCESS_POINTS; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - addPreferencesFromResource(R.xml.wifi_display_saved_access_points); - mUserBadgeCache = new AccessPointPreference.UserBadgeCache(getPackageManager()); - } - - @Override - public void onResume() { - super.onResume(); - initPreferences(); - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - mWifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE); - - if (savedInstanceState != null) { - if (savedInstanceState.containsKey(SAVE_DIALOG_ACCESS_POINT_STATE)) { - mAccessPointSavedState = - savedInstanceState.getBundle(SAVE_DIALOG_ACCESS_POINT_STATE); - } - } - } - - private void initPreferences() { - PreferenceScreen preferenceScreen = getPreferenceScreen(); - final Context context = getPrefContext(); - - final List<AccessPoint> accessPoints = - WifiSavedConfigUtils.getAllConfigs(context, mWifiManager); - Collections.sort(accessPoints, SAVED_NETWORK_COMPARATOR); - cacheRemoveAllPrefs(preferenceScreen); - - final int accessPointsSize = accessPoints.size(); - for (int i = 0; i < accessPointsSize; ++i) { - AccessPoint ap = accessPoints.get(i); - String key = ap.getKey(); - LongPressAccessPointPreference preference = - (LongPressAccessPointPreference) getCachedPreference(key); - if (preference == null) { - preference = new LongPressAccessPointPreference( - ap, context, mUserBadgeCache, true, this); - preference.setKey(key); - preference.setIcon(null); - preferenceScreen.addPreference(preference); - } - preference.setOrder(i); - } - - removeCachedPrefs(preferenceScreen); - - if (mAddNetworkPreference == null) { - mAddNetworkPreference = new Preference(getPrefContext()); - mAddNetworkPreference.setIcon(R.drawable.ic_menu_add_inset); - mAddNetworkPreference.setTitle(R.string.wifi_add_network); - } - mAddNetworkPreference.setOrder(accessPointsSize); - preferenceScreen.addPreference(mAddNetworkPreference); - - if(getPreferenceScreen().getPreferenceCount() < 1) { - Log.w(TAG, "Saved networks activity loaded, but there are no saved networks!"); - } - } - - private void postUpdatePreference() { - if (!mHandler.hasMessages(MSG_UPDATE_PREFERENCES)) { - mHandler.sendEmptyMessage(MSG_UPDATE_PREFERENCES); - } - } - - private void showWifiDialog(@Nullable LongPressAccessPointPreference accessPoint) { - if (mDialog != null) { - removeDialog(WifiSettings.WIFI_DIALOG_ID); - mDialog = null; - } - - if (accessPoint != null) { - // Save the access point and edit mode - mDlgAccessPoint = accessPoint.getAccessPoint(); - } else { - // No access point is selected. Clear saved state. - mDlgAccessPoint = null; - mAccessPointSavedState = null; - } - - showDialog(WifiSettings.WIFI_DIALOG_ID); - } - - @Override - public Dialog onCreateDialog(int dialogId) { - switch (dialogId) { - case WifiSettings.WIFI_DIALOG_ID: - if (mDlgAccessPoint == null && mAccessPointSavedState == null) { - // Add new network - mDialog = WifiDialog.createFullscreen(getActivity(), this, null, - WifiConfigUiBase.MODE_CONNECT); - } else { - // Modify network - if (mDlgAccessPoint == null) { - // Restore AP from save state - mDlgAccessPoint = new AccessPoint(getActivity(), mAccessPointSavedState); - // Reset the saved access point data - mAccessPointSavedState = null; - } - mDialog = WifiDialog.createModal(getActivity(), this, mDlgAccessPoint, - WifiConfigUiBase.MODE_VIEW); - } - mSelectedAccessPoint = mDlgAccessPoint; - - return mDialog; - } - return super.onCreateDialog(dialogId); - } - - @Override - public int getDialogMetricsCategory(int dialogId) { - switch (dialogId) { - case WifiSettings.WIFI_DIALOG_ID: - return MetricsProto.MetricsEvent.DIALOG_WIFI_SAVED_AP_EDIT; - default: - return 0; - } - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - - // If the dialog is showing, save its state. - if (mDialog != null && mDialog.isShowing()) { - if (mDlgAccessPoint != null) { - mAccessPointSavedState = new Bundle(); - mDlgAccessPoint.saveWifiState(mAccessPointSavedState); - outState.putBundle(SAVE_DIALOG_ACCESS_POINT_STATE, mAccessPointSavedState); - } - } - } - - @Override - public void onForget(WifiDialog dialog) { - if (mSelectedAccessPoint != null) { - if (mSelectedAccessPoint.isPasspointConfig()) { - try { - mWifiManager.removePasspointConfiguration( - mSelectedAccessPoint.getPasspointFqdn()); - } catch (RuntimeException e) { - Log.e(TAG, "Failed to remove Passpoint configuration for " - + mSelectedAccessPoint.getConfigName()); - } - postUpdatePreference(); - } else { - // mForgetListener will call initPreferences upon completion - mWifiManager.forget(mSelectedAccessPoint.getConfig().networkId, mForgetListener); - } - mSelectedAccessPoint = null; - } - } - - @Override - public void onSubmit(WifiDialog dialog) { - mWifiManager.save(dialog.getController().getConfig(), mSaveListener); - } - - @Override - public boolean onPreferenceTreeClick(Preference preference) { - if (preference instanceof LongPressAccessPointPreference) { - showWifiDialog((LongPressAccessPointPreference) preference); - return true; - } else if (preference == mAddNetworkPreference) { - showWifiDialog(null); - return true; - } else { - return super.onPreferenceTreeClick(preference); - } - } -} diff --git a/src/com/android/settings/wifi/UseOpenWifiPreferenceController.java b/src/com/android/settings/wifi/UseOpenWifiPreferenceController.java index 6bc2d98f79..eef22167bb 100644 --- a/src/com/android/settings/wifi/UseOpenWifiPreferenceController.java +++ b/src/com/android/settings/wifi/UseOpenWifiPreferenceController.java @@ -1,7 +1,6 @@ package com.android.settings.wifi; import android.app.Activity; -import android.app.Fragment; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; @@ -13,11 +12,12 @@ import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.provider.Settings; -import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; +import android.text.TextUtils; + +import androidx.fragment.app.Fragment; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.text.TextUtils; +import androidx.preference.SwitchPreference; import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; diff --git a/src/com/android/settings/wifi/WifiAPITest.java b/src/com/android/settings/wifi/WifiAPITest.java index 35f700de81..87499f5891 100644 --- a/src/com/android/settings/wifi/WifiAPITest.java +++ b/src/com/android/settings/wifi/WifiAPITest.java @@ -18,16 +18,17 @@ package com.android.settings.wifi; import static android.content.Context.WIFI_SERVICE; -import android.app.AlertDialog; +import android.app.settings.SettingsEnums; import android.content.DialogInterface; import android.net.wifi.WifiManager; import android.os.Bundle; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; import android.text.Editable; import android.widget.EditText; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.appcompat.app.AlertDialog; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; @@ -86,7 +87,7 @@ public class WifiAPITest extends SettingsPreferenceFragment implements @Override public int getMetricsCategory() { - return MetricsEvent.TESTING; + return SettingsEnums.TESTING; } //============================ diff --git a/src/com/android/settings/wifi/WifiConfigController.java b/src/com/android/settings/wifi/WifiConfigController.java index 22de20c445..0be7c2b0d3 100644 --- a/src/com/android/settings/wifi/WifiConfigController.java +++ b/src/com/android/settings/wifi/WifiConfigController.java @@ -34,11 +34,10 @@ import android.net.wifi.WifiEnterpriseConfig; import android.net.wifi.WifiEnterpriseConfig.Eap; import android.net.wifi.WifiEnterpriseConfig.Phase2; import android.net.wifi.WifiInfo; -import android.os.Handler; +import android.net.wifi.WifiManager; import android.os.UserManager; import android.security.Credentials; import android.security.KeyStore; -import androidx.annotation.VisibleForTesting; import android.text.Editable; import android.text.InputType; import android.text.TextUtils; @@ -55,12 +54,17 @@ import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.EditText; +import android.widget.ImageButton; import android.widget.ScrollView; import android.widget.Spinner; import android.widget.TextView; +import androidx.annotation.VisibleForTesting; + import com.android.settings.ProxySelector; import com.android.settings.R; +import com.android.settings.wifi.details.WifiPrivacyPreferenceController; +import com.android.settings.wifi.dpp.WifiDppUtils; import com.android.settingslib.Utils; import com.android.settingslib.utils.ThreadUtils; import com.android.settingslib.wifi.AccessPoint; @@ -118,13 +122,16 @@ public class WifiConfigController implements TextWatcher, /* Phase2 methods supported by PEAP are limited */ - private final ArrayAdapter<String> mPhase2PeapAdapter; + private ArrayAdapter<String> mPhase2PeapAdapter; /* Full list of phase2 methods */ - private final ArrayAdapter<String> mPhase2FullAdapter; + private ArrayAdapter<String> mPhase2FullAdapter; // e.g. AccessPoint.SECURITY_NONE - private int mAccessPointSecurity; + @VisibleForTesting + int mAccessPointSecurity; private TextView mPasswordView; + private ImageButton mSsidScanButton; + private ImageButton mPasswordScanButton; private String mUnspecifiedCertString; private String mMultipleCertSetString; @@ -154,6 +161,7 @@ public class WifiConfigController implements TextWatcher, private Spinner mProxySettingsSpinner; private Spinner mMeteredSettingsSpinner; private Spinner mHiddenSettingsSpinner; + private Spinner mPrivacySettingsSpinner; private TextView mHiddenWarningView; private TextView mProxyHostView; private TextView mProxyPortView; @@ -171,6 +179,9 @@ public class WifiConfigController implements TextWatcher, private TextView mSsidView; private Context mContext; + private Integer mSecurityInPosition[]; + + private final WifiManager mWifiManager; public WifiConfigController(WifiConfigUiBase parent, View view, AccessPoint accessPoint, int mode) { @@ -178,11 +189,31 @@ public class WifiConfigController implements TextWatcher, mView = view; mAccessPoint = accessPoint; + mContext = mConfigUi.getContext(); + + // Init Wi-Fi manager + mWifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); + initWifiConfigController(accessPoint, mode); + } + + @VisibleForTesting + public WifiConfigController(WifiConfigUiBase parent, View view, AccessPoint accessPoint, + int mode, WifiManager wifiManager) { + mConfigUi = parent; + + mView = view; + mAccessPoint = accessPoint; + mContext = mConfigUi.getContext(); + mWifiManager = wifiManager; + initWifiConfigController(accessPoint, mode); + } + + private void initWifiConfigController(AccessPoint accessPoint, int mode) { + mAccessPointSecurity = (accessPoint == null) ? AccessPoint.SECURITY_NONE : accessPoint.getSecurity(); mMode = mode; - mContext = mConfigUi.getContext(); final Resources res = mContext.getResources(); mLevels = res.getStringArray(R.array.wifi_signal); @@ -211,6 +242,8 @@ public class WifiConfigController implements TextWatcher, mDoNotValidateEapServerString = mContext.getString(R.string.wifi_do_not_validate_eap_server); + mSsidScanButton = (ImageButton) mView.findViewById(R.id.ssid_scanner_button); + mPasswordScanButton = (ImageButton) mView.findViewById(R.id.password_scanner_button); mDialogContainer = mView.findViewById(R.id.dialog_scrollview); mIpSettingsSpinner = (Spinner) mView.findViewById(R.id.ip_settings); mIpSettingsSpinner.setOnItemSelectedListener(this); @@ -219,37 +252,26 @@ public class WifiConfigController implements TextWatcher, mSharedCheckBox = (CheckBox) mView.findViewById(R.id.shared); mMeteredSettingsSpinner = mView.findViewById(R.id.metered_settings); mHiddenSettingsSpinner = mView.findViewById(R.id.hidden_settings); + mPrivacySettingsSpinner = mView.findViewById(R.id.privacy_settings); + if (mContext.getResources().getBoolean( + com.android.internal.R.bool.config_wifi_connected_mac_randomization_supported)) { + View privacySettingsLayout = mView.findViewById(R.id.privacy_settings_fields); + privacySettingsLayout.setVisibility(View.VISIBLE); + } mHiddenSettingsSpinner.setOnItemSelectedListener(this); mHiddenWarningView = mView.findViewById(R.id.hidden_settings_warning); mHiddenWarningView.setVisibility( mHiddenSettingsSpinner.getSelectedItemPosition() == NOT_HIDDEN_NETWORK ? View.GONE : View.VISIBLE); + mSecurityInPosition = new Integer[AccessPoint.SECURITY_MAX_VAL]; if (mAccessPoint == null) { // new network - mConfigUi.setTitle(R.string.wifi_add_network); - - mSsidView = (TextView) mView.findViewById(R.id.ssid); - mSsidView.addTextChangedListener(this); - mSecuritySpinner = ((Spinner) mView.findViewById(R.id.security)); - mSecuritySpinner.setOnItemSelectedListener(this); - mView.findViewById(R.id.type).setVisibility(View.VISIBLE); - - showIpConfigFields(); - showProxyFields(); - mView.findViewById(R.id.wifi_advanced_toggle).setVisibility(View.VISIBLE); - // Hidden option can be changed only when the user adds a network manually. - mView.findViewById(R.id.hidden_settings_field).setVisibility(View.VISIBLE); - ((CheckBox) mView.findViewById(R.id.wifi_advanced_togglebox)) - .setOnCheckedChangeListener(this); - + configureSecuritySpinner(); mConfigUi.setSubmitButton(res.getString(R.string.wifi_save)); + mPasswordScanButton.setVisibility(View.GONE); } else { - if (!mAccessPoint.isPasspointConfig()) { - mConfigUi.setTitle(mAccessPoint.getSsid()); - } else { - mConfigUi.setTitle(mAccessPoint.getConfigName()); - } + mConfigUi.setTitle(mAccessPoint.getTitle()); ViewGroup group = (ViewGroup) mView.findViewById(R.id.info); @@ -260,6 +282,12 @@ public class WifiConfigController implements TextWatcher, mHiddenSettingsSpinner.setSelection(config.hiddenSSID ? HIDDEN_NETWORK : NOT_HIDDEN_NETWORK); + + final int prefMacValue = + WifiPrivacyPreferenceController.translateMacRandomizedValueToPrefValue( + config.macRandomizationSetting); + mPrivacySettingsSpinner.setSelection(prefMacValue); + if (config.getIpAssignment() == IpAssignment.STATIC) { mIpSettingsSpinner.setSelection(STATIC_IP); showAdvancedFields = true; @@ -333,8 +361,15 @@ public class WifiConfigController implements TextWatcher, if (config != null && config.isPasspoint()) { providerFriendlyName = config.providerFriendlyName; } + String suggestionOrSpecifierPackageName = null; + if (config != null + && (config.fromWifiNetworkSpecifier + || config.fromWifiNetworkSuggestion)) { + suggestionOrSpecifierPackageName = config.creatorName; + } String summary = AccessPoint.getSummary( - mConfigUi.getContext(), state, isEphemeral, providerFriendlyName); + mConfigUi.getContext(), /* ssid */ null, state, isEphemeral, + suggestionOrSpecifierPackageName); addRow(group, R.string.wifi_status, summary); } @@ -343,9 +378,14 @@ public class WifiConfigController implements TextWatcher, } WifiInfo info = mAccessPoint.getInfo(); - if (info != null && info.getLinkSpeed() != -1) { - addRow(group, R.string.wifi_speed, String.format( - res.getString(R.string.link_speed), info.getLinkSpeed())); + if (info != null && info.getTxLinkSpeedMbps() != WifiInfo.LINK_SPEED_UNKNOWN) { + addRow(group, R.string.tx_wifi_speed, String.format( + res.getString(R.string.tx_link_speed), info.getTxLinkSpeedMbps())); + } + + if (info != null && info.getRxLinkSpeedMbps() != WifiInfo.LINK_SPEED_UNKNOWN) { + addRow(group, R.string.rx_wifi_speed, String.format( + res.getString(R.string.rx_link_speed), info.getRxLinkSpeedMbps())); } if (info != null && info.getFrequency() != -1) { @@ -374,6 +414,11 @@ public class WifiConfigController implements TextWatcher, mConfigUi.setForgetButton(res.getString(R.string.wifi_forget)); } } + + if (!WifiDppUtils.isSupportEnrolleeQrCodeScanner(mContext, mAccessPointSecurity)) { + mPasswordScanButton.setVisibility(View.GONE); + } + mSsidScanButton.setVisibility(View.GONE); } if (!isSplitSystemUser()) { @@ -444,6 +489,13 @@ public class WifiConfigController implements TextWatcher, return false; } + boolean isValidSaePassword(String password) { + if (password.length() >= 1 && password.length() <= 63) { + return true; + } + return false; + } + boolean isSubmittable() { boolean enabled = false; boolean passwordInvalid = false; @@ -451,7 +503,9 @@ public class WifiConfigController implements TextWatcher, && ((mAccessPointSecurity == AccessPoint.SECURITY_WEP && mPasswordView.length() == 0) || (mAccessPointSecurity == AccessPoint.SECURITY_PSK - && !isValidPsk(mPasswordView.getText().toString())))) { + && !isValidPsk(mPasswordView.getText().toString())) + || (mAccessPointSecurity == AccessPoint.SECURITY_SAE + && !isValidSaePassword(mPasswordView.getText().toString())))) { passwordInvalid = true; } if ((mSsidView != null && mSsidView.length() == 0) @@ -465,7 +519,9 @@ public class WifiConfigController implements TextWatcher, } else { enabled = ipAndProxyFieldsAreValid(); } - if (mEapCaCertSpinner != null + if ((mAccessPointSecurity == AccessPoint.SECURITY_EAP || + mAccessPointSecurity == AccessPoint.SECURITY_EAP_SUITE_B) + && mEapCaCertSpinner != null && mView.findViewById(R.id.l_ca_cert).getVisibility() != View.GONE) { String caCertSelection = (String) mEapCaCertSpinner.getSelectedItem(); if (caCertSelection.equals(mUnspecifiedCertString)) { @@ -482,10 +538,11 @@ public class WifiConfigController implements TextWatcher, enabled = false; } } - if (mEapUserCertSpinner != null + if ((mAccessPointSecurity == AccessPoint.SECURITY_EAP || + mAccessPointSecurity == AccessPoint.SECURITY_EAP_SUITE_B) + && mEapUserCertSpinner != null && mView.findViewById(R.id.l_user_cert).getVisibility() != View.GONE - && ((String) mEapUserCertSpinner.getSelectedItem()) - .equals(mUnspecifiedCertString)) { + && mEapUserCertSpinner.getSelectedItem().equals(mUnspecifiedCertString)) { // Disallow submit if the user has not selected a user certificate for an EAP network // configuration. enabled = false; @@ -524,6 +581,35 @@ public class WifiConfigController implements TextWatcher, } } + /** + * Special handling for WPA2/WPA3 and OWE in Transition mode: The key + * SECURITY_PSK_SAE_TRANSITION and SECURITY_OWE_TRANSITION are pseudo keys which result by the + * scan results, but never appears in the saved networks. + * A saved network is either WPA3 for supporting devices or WPA2 for non-supporting devices, + * or, OWE for supporting devices or Open for non-supporting devices. + * + * @param accessPointSecurity Access point current security type + * @return Converted security type (if required) + */ + private int convertSecurityTypeForMatching(int accessPointSecurity) { + if (accessPointSecurity == AccessPoint.SECURITY_PSK_SAE_TRANSITION) { + if (mWifiManager.isWpa3SaeSupported()) { + return AccessPoint.SECURITY_SAE; + } else { + return AccessPoint.SECURITY_PSK; + } + } + if (accessPointSecurity == AccessPoint.SECURITY_OWE_TRANSITION) { + if (mWifiManager.isEnhancedOpenSupported()) { + return AccessPoint.SECURITY_OWE; + } else { + return AccessPoint.SECURITY_NONE; + } + } + + return accessPointSecurity; + } + public WifiConfiguration getConfig() { if (mMode == WifiConfigUiBase.MODE_VIEW) { return null; @@ -546,6 +632,8 @@ public class WifiConfigController implements TextWatcher, config.shared = mSharedCheckBox.isChecked(); + mAccessPointSecurity = convertSecurityTypeForMatching(mAccessPointSecurity); + switch (mAccessPointSecurity) { case AccessPoint.SECURITY_NONE: config.allowedKeyManagement.set(KeyMgmt.NONE); @@ -581,8 +669,18 @@ public class WifiConfigController implements TextWatcher, break; case AccessPoint.SECURITY_EAP: + case AccessPoint.SECURITY_EAP_SUITE_B: config.allowedKeyManagement.set(KeyMgmt.WPA_EAP); config.allowedKeyManagement.set(KeyMgmt.IEEE8021X); + if (mAccessPointSecurity == AccessPoint.SECURITY_EAP_SUITE_B) { + config.allowedKeyManagement.set(KeyMgmt.SUITE_B_192); + config.requirePMF = true; + config.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.GCMP_256); + config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.GCMP_256); + config.allowedGroupManagementCiphers.set(WifiConfiguration.GroupMgmtCipher + .BIP_GMAC_256); + // allowedSuiteBCiphers will be set according to certificate type + } config.enterpriseConfig = new WifiEnterpriseConfig(); int eapMethod = mEapMethodSpinner.getSelectedItemPosition(); int phase2Method = mPhase2Spinner.getSelectedItemPosition(); @@ -691,6 +789,20 @@ public class WifiConfigController implements TextWatcher, config.enterpriseConfig.setPassword(mPasswordView.getText().toString()); } break; + case AccessPoint.SECURITY_SAE: + config.allowedKeyManagement.set(KeyMgmt.SAE); + config.requirePMF = true; + if (mPasswordView.length() != 0) { + String password = mPasswordView.getText().toString(); + config.preSharedKey = '"' + password + '"'; + } + break; + + case AccessPoint.SECURITY_OWE: + config.allowedKeyManagement.set(KeyMgmt.OWE); + config.requirePMF = true; + break; + default: return null; } @@ -702,6 +814,13 @@ public class WifiConfigController implements TextWatcher, config.meteredOverride = mMeteredSettingsSpinner.getSelectedItemPosition(); } + if (mPrivacySettingsSpinner != null) { + final int macValue = + WifiPrivacyPreferenceController.translatePrefValueToMacRandomizedValue( + mPrivacySettingsSpinner.getSelectedItemPosition()); + config.macRandomizationSetting = macValue; + } + return config; } @@ -838,7 +957,9 @@ public class WifiConfigController implements TextWatcher, } private void showSecurityFields() { - if (mAccessPointSecurity == AccessPoint.SECURITY_NONE) { + if (mAccessPointSecurity == AccessPoint.SECURITY_NONE || + mAccessPointSecurity == AccessPoint.SECURITY_OWE || + mAccessPointSecurity == AccessPoint.SECURITY_OWE_TRANSITION) { mView.findViewById(R.id.security_fields).setVisibility(View.GONE); return; } @@ -857,7 +978,8 @@ public class WifiConfigController implements TextWatcher, } } - if (mAccessPointSecurity != AccessPoint.SECURITY_EAP) { + if (mAccessPointSecurity != AccessPoint.SECURITY_EAP && + mAccessPointSecurity != AccessPoint.SECURITY_EAP_SUITE_B) { mView.findViewById(R.id.eap).setVisibility(View.GONE); return; } @@ -1362,8 +1484,15 @@ public class WifiConfigController implements TextWatcher, @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { if (parent == mSecuritySpinner) { - mAccessPointSecurity = position; + // Convert menu position to actual Wi-Fi security type + mAccessPointSecurity = mSecurityInPosition[position]; showSecurityFields(); + + if (WifiDppUtils.isSupportEnrolleeQrCodeScanner(mContext, mAccessPointSecurity)) { + mSsidScanButton.setVisibility(View.VISIBLE); + } else { + mSsidScanButton.setVisibility(View.GONE); + } } else if (parent == mEapMethodSpinner || parent == mEapCaCertSpinner) { showSecurityFields(); } else if (parent == mPhase2Spinner @@ -1407,4 +1536,53 @@ public class WifiConfigController implements TextWatcher, public AccessPoint getAccessPoint() { return mAccessPoint; } + + private void configureSecuritySpinner() { + mConfigUi.setTitle(R.string.wifi_add_network); + + mSsidView = (TextView) mView.findViewById(R.id.ssid); + mSsidView.addTextChangedListener(this); + mSecuritySpinner = ((Spinner) mView.findViewById(R.id.security)); + mSecuritySpinner.setOnItemSelectedListener(this); + + ArrayAdapter<String> spinnerAdapter = new ArrayAdapter<String>(mContext, + android.R.layout.simple_spinner_item, android.R.id.text1); + spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + mSecuritySpinner.setAdapter(spinnerAdapter); + int idx = 0; + + // Populate the Wi-Fi security spinner with the various supported key management types + spinnerAdapter.add(mContext.getString(R.string.wifi_security_none)); + mSecurityInPosition[idx++] = AccessPoint.SECURITY_NONE; + if (mWifiManager.isEnhancedOpenSupported()) { + spinnerAdapter.add(mContext.getString(R.string.wifi_security_owe)); + mSecurityInPosition[idx++] = AccessPoint.SECURITY_OWE; + } + spinnerAdapter.add(mContext.getString(R.string.wifi_security_wep)); + mSecurityInPosition[idx++] = AccessPoint.SECURITY_WEP; + spinnerAdapter.add(mContext.getString(R.string.wifi_security_wpa_wpa2)); + mSecurityInPosition[idx++] = AccessPoint.SECURITY_PSK; + if (mWifiManager.isWpa3SaeSupported()) { + spinnerAdapter.add(mContext.getString(R.string.wifi_security_sae)); + mSecurityInPosition[idx++] = AccessPoint.SECURITY_SAE; + } + spinnerAdapter.add(mContext.getString(R.string.wifi_security_eap)); + mSecurityInPosition[idx++] = AccessPoint.SECURITY_EAP; + if (mWifiManager.isWpa3SuiteBSupported()) { + spinnerAdapter.add(mContext.getString(R.string.wifi_security_eap_suiteb)); + mSecurityInPosition[idx++] = AccessPoint.SECURITY_EAP_SUITE_B; + } + + spinnerAdapter.notifyDataSetChanged(); + + mView.findViewById(R.id.type).setVisibility(View.VISIBLE); + + showIpConfigFields(); + showProxyFields(); + mView.findViewById(R.id.wifi_advanced_toggle).setVisibility(View.VISIBLE); + // Hidden option can be changed only when the user adds a network manually. + mView.findViewById(R.id.hidden_settings_field).setVisibility(View.VISIBLE); + ((CheckBox) mView.findViewById(R.id.wifi_advanced_togglebox)) + .setOnCheckedChangeListener(this); + } } diff --git a/src/com/android/settings/wifi/WifiConnectListener.java b/src/com/android/settings/wifi/WifiConnectListener.java new file mode 100644 index 0000000000..b97fbc5e6e --- /dev/null +++ b/src/com/android/settings/wifi/WifiConnectListener.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2019 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.wifi; + +import android.content.Context; +import android.net.wifi.WifiManager; +import android.widget.Toast; + +import com.android.settings.R; + +/** + * A listener to display a toast on failure to connect + */ +public class WifiConnectListener implements WifiManager.ActionListener { + + private final Context mContext; + + public WifiConnectListener(Context context) { + mContext = context; + } + + @Override + public void onSuccess() { + } + + @Override + public void onFailure(int reason) { + if (mContext != null) { + Toast.makeText(mContext, + R.string.wifi_failed_connect_message, + Toast.LENGTH_SHORT).show(); + } + } +} diff --git a/src/com/android/settings/wifi/WifiConnectionPreferenceController.java b/src/com/android/settings/wifi/WifiConnectionPreferenceController.java new file mode 100644 index 0000000000..36c4455fef --- /dev/null +++ b/src/com/android/settings/wifi/WifiConnectionPreferenceController.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2018 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.wifi; + +import android.content.Context; +import android.os.Bundle; + +import androidx.preference.PreferenceGroup; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.core.SubSettingLauncher; +import com.android.settings.wifi.details.WifiNetworkDetailsFragment; +import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.wifi.AccessPoint; +import com.android.settingslib.wifi.AccessPointPreference; +import com.android.settingslib.wifi.WifiTracker; +import com.android.settingslib.wifi.WifiTrackerFactory; + +/** + * This places a preference into a PreferenceGroup owned by some parent + * controller class when there is a wifi connection present. + */ +public class WifiConnectionPreferenceController extends AbstractPreferenceController implements + WifiTracker.WifiListener { + + private static final String TAG = "WifiConnPrefCtrl"; + + private static final String KEY = "active_wifi_connection"; + + private UpdateListener mUpdateListener; + private Context mPrefContext; + private String mPreferenceGroupKey; + private PreferenceGroup mPreferenceGroup; + private WifiTracker mWifiTracker; + private AccessPointPreference mPreference; + private AccessPointPreference.UserBadgeCache mBadgeCache; + private int order; + private int mMetricsCategory; + + /** + * Used to notify a parent controller that this controller has changed in availability, or has + * updated the content in the preference that it manages. + */ + public interface UpdateListener { + void onChildrenUpdated(); + } + + /** + * @param context the context for the UI where we're placing the preference + * @param lifecycle for listening to lifecycle events for the UI + * @param updateListener for notifying a parent controller of changes + * @param preferenceGroupKey the key to use to lookup the PreferenceGroup where this controller + * will add its preference + * @param order the order that the preference added by this controller should use - + * useful when this preference needs to be ordered in a specific way + * relative to others in the PreferenceGroup + * @param metricsCategory - the category to use as the source when handling the click on the + * pref to go to the wifi connection detail page + */ + public WifiConnectionPreferenceController(Context context, Lifecycle lifecycle, + UpdateListener updateListener, String preferenceGroupKey, int order, + int metricsCategory) { + super(context); + mUpdateListener = updateListener; + mPreferenceGroupKey = preferenceGroupKey; + mWifiTracker = WifiTrackerFactory.create(context, this, lifecycle, true /* includeSaved */, + true /* includeScans */); + this.order = order; + mMetricsCategory = metricsCategory; + mBadgeCache = new AccessPointPreference.UserBadgeCache(context.getPackageManager()); + } + + @Override + public boolean isAvailable() { + return mWifiTracker.isConnected() && getCurrentAccessPoint() != null; + } + + @Override + public String getPreferenceKey() { + return KEY; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreferenceGroup = screen.findPreference(mPreferenceGroupKey); + mPrefContext = screen.getContext(); + update(); + } + + private AccessPoint getCurrentAccessPoint() { + for (AccessPoint accessPoint : mWifiTracker.getAccessPoints()) { + if (accessPoint.isActive()) { + return accessPoint; + } + } + return null; + } + + private void updatePreference(AccessPoint accessPoint) { + if (mPreference != null) { + mPreferenceGroup.removePreference(mPreference); + mPreference = null; + } + if (accessPoint == null) { + return; + } + if (mPrefContext != null) { + mPreference = new AccessPointPreference(accessPoint, mPrefContext, mBadgeCache, + R.drawable.ic_wifi_signal_0, false /* forSavedNetworks */); + mPreference.setKey(KEY); + mPreference.refresh(); + mPreference.setOrder(order); + + mPreference.setOnPreferenceClickListener(pref -> { + Bundle args = new Bundle(); + mPreference.getAccessPoint().saveWifiState(args); + new SubSettingLauncher(mPrefContext) + .setTitleRes(R.string.pref_title_network_details) + .setDestination(WifiNetworkDetailsFragment.class.getName()) + .setArguments(args) + .setSourceMetricsCategory(mMetricsCategory) + .launch(); + return true; + }); + mPreferenceGroup.addPreference(mPreference); + } + } + + private void update() { + AccessPoint connectedAccessPoint = null; + if (mWifiTracker.isConnected()) { + connectedAccessPoint = getCurrentAccessPoint(); + } + if (connectedAccessPoint == null) { + updatePreference(null); + } else { + if (mPreference == null || !mPreference.getAccessPoint().equals(connectedAccessPoint)) { + updatePreference(connectedAccessPoint); + } else if (mPreference != null) { + mPreference.refresh(); + } + } + mUpdateListener.onChildrenUpdated(); + } + + @Override + public void onWifiStateChanged(int state) { + update(); + } + + @Override + public void onConnectedChanged() { + update(); + } + + @Override + public void onAccessPointsChanged() { + update(); + } +} diff --git a/src/com/android/settings/wifi/WifiDetailPreference.java b/src/com/android/settings/wifi/WifiDetailPreference.java deleted file mode 100644 index f7ddac7c6f..0000000000 --- a/src/com/android/settings/wifi/WifiDetailPreference.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2009 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.wifi; - -import android.content.Context; -import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.widget.TextView; - -import com.android.settings.R; - -/** - * A Preference to be used with the Wifi Network Detail Fragment that allows a summary text to be - * set inside the widget resource - */ -public class WifiDetailPreference extends Preference { - private String mDetailText; - - public WifiDetailPreference(Context context, AttributeSet attrs) { - super(context, attrs); - setWidgetLayoutResource(R.layout.preference_widget_summary); - } - - public void setDetailText(String text) { - if (TextUtils.equals(mDetailText, text)) return; - mDetailText = text; - notifyChanged(); - } - - @Override - public void onBindViewHolder(PreferenceViewHolder view) { - super.onBindViewHolder(view); - TextView textView = ((TextView) view.findViewById(R.id.widget_summary)); - textView.setText(mDetailText); - textView.setPadding(0, 0, 10, 0); - } -} diff --git a/src/com/android/settings/wifi/WifiDialog.java b/src/com/android/settings/wifi/WifiDialog.java index 79500e0b7a..875f35d68a 100644 --- a/src/com/android/settings/wifi/WifiDialog.java +++ b/src/com/android/settings/wifi/WifiDialog.java @@ -16,22 +16,35 @@ package com.android.settings.wifi; -import android.app.AlertDialog; +import android.annotation.StyleRes; import android.content.Context; import android.content.DialogInterface; +import android.content.Intent; import android.os.Bundle; import android.view.View; import android.widget.Button; +import android.widget.ImageButton; +import android.widget.TextView; + +import androidx.appcompat.app.AlertDialog; import com.android.settings.R; import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.wifi.AccessPoint; -public class WifiDialog extends AlertDialog implements WifiConfigUiBase, DialogInterface.OnClickListener { +public class WifiDialog extends AlertDialog implements WifiConfigUiBase, + DialogInterface.OnClickListener { public interface WifiDialogListener { - void onForget(WifiDialog dialog); - void onSubmit(WifiDialog dialog); + default void onForget(WifiDialog dialog) { + } + + default void onSubmit(WifiDialog dialog) { + } + + default void onScan(WifiDialog dialog, String ssid) { + } } private static final int BUTTON_SUBMIT = DialogInterface.BUTTON_POSITIVE; @@ -45,14 +58,6 @@ public class WifiDialog extends AlertDialog implements WifiConfigUiBase, DialogI private WifiConfigController mController; private boolean mHideSubmitButton; - - /** Creates a WifiDialog with fullscreen style. It displays in fullscreen mode. */ - public static WifiDialog createFullscreen(Context context, WifiDialogListener listener, - AccessPoint accessPoint, int mode) { - return new WifiDialog(context, listener, accessPoint, mode, - R.style.Theme_Settings_NoActionBar, false /* hideSubmitButton */); - } - /** * Creates a WifiDialog with no additional style. It displays as a dialog above the current * view. @@ -60,11 +65,21 @@ public class WifiDialog extends AlertDialog implements WifiConfigUiBase, DialogI public static WifiDialog createModal(Context context, WifiDialogListener listener, AccessPoint accessPoint, int mode) { return new WifiDialog(context, listener, accessPoint, mode, 0 /* style */, - mode == WifiConfigUiBase.MODE_VIEW /* hideSubmitButton*/); + mode == WifiConfigUiBase.MODE_VIEW /* hideSubmitButton */); + } + + /** + * Creates a WifiDialog with customized style. It displays as a dialog above the current + * view. + */ + public static WifiDialog createModal(Context context, WifiDialogListener listener, + AccessPoint accessPoint, int mode, @StyleRes int style) { + return new WifiDialog(context, listener, accessPoint, mode, style, + mode == WifiConfigUiBase.MODE_VIEW /* hideSubmitButton */); } /* package */ WifiDialog(Context context, WifiDialogListener listener, AccessPoint accessPoint, - int mode, int style, boolean hideSubmitButton) { + int mode, @StyleRes int style, boolean hideSubmitButton) { super(context, style); mMode = mode; mListener = listener; @@ -79,9 +94,8 @@ public class WifiDialog extends AlertDialog implements WifiConfigUiBase, DialogI @Override protected void onCreate(Bundle savedInstanceState) { - mView = getLayoutInflater().inflate(R.layout.wifi_dialog, null); + mView = getLayoutInflater().inflate(R.layout.wifi_dialog, /* root */ null); setView(mView); - setInverseBackgroundForced(true); mController = new WifiConfigController(this, mView, mAccessPoint, mMode); super.onCreate(savedInstanceState); @@ -98,9 +112,38 @@ public class WifiDialog extends AlertDialog implements WifiConfigUiBase, DialogI } } + @Override + protected void onStart() { + View.OnClickListener onClickScannerButtonListener = v -> { + if (mListener == null) { + return; + } + + String ssid = null; + if (mAccessPoint == null) { + final TextView ssidEditText = findViewById(R.id.ssid); + ssid = ssidEditText.getText().toString(); + } else { + ssid = mAccessPoint.getSsidStr(); + } + mListener.onScan(/* WifiDialog */ this, ssid); + }; + + final ImageButton ssidScannerButton = findViewById(R.id.ssid_scanner_button); + ssidScannerButton.setOnClickListener(onClickScannerButtonListener); + + final ImageButton passwordScannerButton = findViewById(R.id.password_scanner_button); + passwordScannerButton.setOnClickListener(onClickScannerButtonListener); + + if (mHideSubmitButton) { + ssidScannerButton.setVisibility(View.GONE); + passwordScannerButton.setVisibility(View.GONE); + } + } + public void onRestoreInstanceState(Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); - mController.updatePassword(); + super.onRestoreInstanceState(savedInstanceState); + mController.updatePassword(); } @Override @@ -121,7 +164,7 @@ public class WifiDialog extends AlertDialog implements WifiConfigUiBase, DialogI case BUTTON_FORGET: if (WifiUtils.isNetworkLockedDown(getContext(), mAccessPoint.getConfig())) { RestrictedLockUtils.sendShowAdminSupportDetailsIntent(getContext(), - RestrictedLockUtils.getDeviceOwner(getContext())); + RestrictedLockUtilsInternal.getDeviceOwner(getContext())); return; } mListener.onForget(this); diff --git a/src/com/android/settings/wifi/WifiDialogActivity.java b/src/com/android/settings/wifi/WifiDialogActivity.java index 02ce0e4d4e..7782786763 100644 --- a/src/com/android/settings/wifi/WifiDialogActivity.java +++ b/src/com/android/settings/wifi/WifiDialogActivity.java @@ -24,23 +24,23 @@ import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiManager; import android.net.wifi.WifiManager.ActionListener; import android.os.Bundle; -import androidx.annotation.VisibleForTesting; import android.util.Log; +import androidx.annotation.VisibleForTesting; + +import com.android.settings.R; import com.android.settings.SetupWizardUtils; +import com.android.settings.wifi.dpp.WifiDppUtils; import com.android.settingslib.wifi.AccessPoint; -import com.android.setupwizardlib.util.WizardManagerHelper; + +import com.google.android.setupcompat.util.WizardManagerHelper; public class WifiDialogActivity extends Activity implements WifiDialog.WifiDialogListener, DialogInterface.OnDismissListener { private static final String TAG = "WifiDialogActivity"; - private static final int RESULT_CONNECTED = RESULT_FIRST_USER; - private static final int RESULT_FORGET = RESULT_FIRST_USER + 1; - - private static final String KEY_ACCESS_POINT_STATE = "access_point_state"; - private static final String KEY_WIFI_CONFIGURATION = "wifi_configuration"; + public static final String KEY_ACCESS_POINT_STATE = "access_point_state"; /** * Boolean extra indicating whether this activity should connect to an access point on the @@ -51,25 +51,41 @@ public class WifiDialogActivity extends Activity implements WifiDialog.WifiDialo @VisibleForTesting static final String KEY_CONNECT_FOR_CALLER = "connect_for_caller"; + public static final String KEY_WIFI_CONFIGURATION = "wifi_configuration"; + + private static final int RESULT_CONNECTED = RESULT_FIRST_USER; + private static final int RESULT_FORGET = RESULT_FIRST_USER + 1; + + private static final int REQUEST_CODE_WIFI_DPP_ENROLLEE_QR_CODE_SCANNER = 0; + + private WifiDialog mDialog; + + private Intent mIntent; + @Override protected void onCreate(Bundle savedInstanceState) { - final Intent intent = getIntent(); - if (WizardManagerHelper.isSetupWizardIntent(intent)) { - setTheme(SetupWizardUtils.getTransparentTheme(intent)); + mIntent = getIntent(); + if (WizardManagerHelper.isSetupWizardIntent(mIntent)) { + setTheme(SetupWizardUtils.getTransparentTheme(mIntent)); } super.onCreate(savedInstanceState); - final Bundle accessPointState = intent.getBundleExtra(KEY_ACCESS_POINT_STATE); + final Bundle accessPointState = mIntent.getBundleExtra(KEY_ACCESS_POINT_STATE); AccessPoint accessPoint = null; if (accessPointState != null) { accessPoint = new AccessPoint(this, accessPointState); } - WifiDialog dialog = WifiDialog.createModal( - this, this, accessPoint, WifiConfigUiBase.MODE_CONNECT); - dialog.show(); - dialog.setOnDismissListener(this); + if (WizardManagerHelper.isAnySetupWizard(getIntent())) { + mDialog = WifiDialog.createModal(this, this, accessPoint, + WifiConfigUiBase.MODE_CONNECT, R.style.SuwAlertDialogThemeCompat_Light); + } else { + mDialog = WifiDialog.createModal( + this, this, accessPoint, WifiConfigUiBase.MODE_CONNECT); + } + mDialog.show(); + mDialog.setOnDismissListener(this); } @Override @@ -79,6 +95,15 @@ public class WifiDialogActivity extends Activity implements WifiDialog.WifiDialo } @Override + public void onDestroy() { + super.onDestroy(); + if (mDialog != null && mDialog.isShowing()) { + mDialog.dismiss(); + mDialog = null; + } + } + + @Override public void onForget(WifiDialog dialog) { final WifiManager wifiManager = getSystemService(WifiManager.class); final AccessPoint accessPoint = dialog.getController().getAccessPoint(); @@ -146,6 +171,30 @@ public class WifiDialogActivity extends Activity implements WifiDialog.WifiDialo @Override public void onDismiss(DialogInterface dialogInterface) { + mDialog = null; finish(); } + + @Override + public void onScan(WifiDialog dialog, String ssid) { + Intent intent = WifiDppUtils.getEnrolleeQrCodeScannerIntent(ssid); + WizardManagerHelper.copyWizardManagerExtras(mIntent, intent); + + // Launch QR code scanner to join a network. + startActivityForResult(intent, REQUEST_CODE_WIFI_DPP_ENROLLEE_QR_CODE_SCANNER); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (requestCode == REQUEST_CODE_WIFI_DPP_ENROLLEE_QR_CODE_SCANNER) { + if (resultCode != RESULT_OK) { + return; + } + + setResult(RESULT_CONNECTED, data); + finish(); + } + } } diff --git a/src/com/android/settings/wifi/WifiEnabler.java b/src/com/android/settings/wifi/WifiEnabler.java index 018119b3e5..536ea613b5 100644 --- a/src/com/android/settings/wifi/WifiEnabler.java +++ b/src/com/android/settings/wifi/WifiEnabler.java @@ -16,6 +16,7 @@ package com.android.settings.wifi; +import android.app.settings.SettingsEnums; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -28,14 +29,14 @@ import android.net.wifi.WifiManager; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; -import androidx.annotation.VisibleForTesting; import android.widget.Toast; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.annotation.VisibleForTesting; + import com.android.settings.R; import com.android.settings.widget.SwitchWidgetController; -import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; +import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.WirelessUtils; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; @@ -161,12 +162,12 @@ public class WifiEnabler implements SwitchWidgetController.OnSwitchChangeListene mSwitchWidget.setEnabled(true); } - if (RestrictedLockUtils.hasBaseUserRestriction(mContext, + if (RestrictedLockUtilsInternal.hasBaseUserRestriction(mContext, UserManager.DISALLOW_CONFIG_TETHERING, UserHandle.myUserId())) { mSwitchWidget.setEnabled(false); } else { - final EnforcedAdmin admin = RestrictedLockUtils.checkIfRestrictionEnforced(mContext, - UserManager.DISALLOW_CONFIG_TETHERING, UserHandle.myUserId()); + final EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced( + mContext, UserManager.DISALLOW_CONFIG_TETHERING, UserHandle.myUserId()); mSwitchWidget.setDisabledByAdmin(admin); } } @@ -208,10 +209,10 @@ public class WifiEnabler implements SwitchWidgetController.OnSwitchChangeListene } if (isChecked) { - mMetricsFeatureProvider.action(mContext, MetricsEvent.ACTION_WIFI_ON); + mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_WIFI_ON); } else { // Log if user was connected at the time of switching off. - mMetricsFeatureProvider.action(mContext, MetricsEvent.ACTION_WIFI_OFF, + mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_WIFI_OFF, mConnected.get()); } if (!mWifiManager.setWifiEnabled(isChecked)) { diff --git a/src/com/android/settings/wifi/WifiInfo.java b/src/com/android/settings/wifi/WifiInfo.java index 3f57376f60..a131f18637 100644 --- a/src/com/android/settings/wifi/WifiInfo.java +++ b/src/com/android/settings/wifi/WifiInfo.java @@ -16,9 +16,9 @@ package com.android.settings.wifi; +import android.app.settings.SettingsEnums; import android.os.Bundle; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; @@ -36,6 +36,6 @@ public class WifiInfo extends SettingsPreferenceFragment { @Override public int getMetricsCategory() { - return MetricsEvent.TESTING; + return SettingsEnums.TESTING; } } diff --git a/src/com/android/settings/wifi/WifiInfoPreferenceController.java b/src/com/android/settings/wifi/WifiInfoPreferenceController.java index ec17b08d12..2a8c7b5755 100644 --- a/src/com/android/settings/wifi/WifiInfoPreferenceController.java +++ b/src/com/android/settings/wifi/WifiInfoPreferenceController.java @@ -23,10 +23,11 @@ import android.content.IntentFilter; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.provider.Settings; +import android.text.TextUtils; + import androidx.core.text.BidiFormatter; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.text.TextUtils; import com.android.settings.R; import com.android.settings.Utils; @@ -97,15 +98,15 @@ public class WifiInfoPreferenceController extends AbstractPreferenceController public void updateWifiInfo() { if (mWifiMacAddressPref != null) { final WifiInfo wifiInfo = mWifiManager.getConnectionInfo(); - final int macRandomizationMode = Settings.Global.getInt(mContext.getContentResolver(), - Settings.Global.WIFI_CONNECTED_MAC_RANDOMIZATION_ENABLED, 0); + final boolean macRandomizationSupported = mContext.getResources().getBoolean( + com.android.internal.R.bool.config_wifi_connected_mac_randomization_supported); final String macAddress = wifiInfo == null ? null : wifiInfo.getMacAddress(); - if (TextUtils.isEmpty(macAddress)) { - mWifiMacAddressPref.setSummary(R.string.status_unavailable); - } else if (macRandomizationMode == 1 - && WifiInfo.DEFAULT_MAC_ADDRESS.equals(macAddress)) { + if (macRandomizationSupported && WifiInfo.DEFAULT_MAC_ADDRESS.equals(macAddress)) { mWifiMacAddressPref.setSummary(R.string.wifi_status_mac_randomized); + } else if (TextUtils.isEmpty(macAddress) + || WifiInfo.DEFAULT_MAC_ADDRESS.equals(macAddress)) { + mWifiMacAddressPref.setSummary(R.string.status_unavailable); } else { mWifiMacAddressPref.setSummary(macAddress); } diff --git a/src/com/android/settings/wifi/WifiMasterSwitchPreferenceController.java b/src/com/android/settings/wifi/WifiMasterSwitchPreferenceController.java index cbbe8ba2c0..6a4774bb1d 100644 --- a/src/com/android/settings/wifi/WifiMasterSwitchPreferenceController.java +++ b/src/com/android/settings/wifi/WifiMasterSwitchPreferenceController.java @@ -16,13 +16,14 @@ package com.android.settings.wifi; import android.content.Context; + import androidx.preference.PreferenceScreen; -import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.R; -import com.android.settings.widget.SummaryUpdater; -import com.android.settings.widget.MasterSwitchPreference; +import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.widget.MasterSwitchController; +import com.android.settings.widget.MasterSwitchPreference; +import com.android.settings.widget.SummaryUpdater; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; import com.android.settingslib.core.lifecycle.LifecycleObserver; @@ -52,7 +53,7 @@ public class WifiMasterSwitchPreferenceController extends AbstractPreferenceCont @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - mWifiPreference = (MasterSwitchPreference) screen.findPreference(KEY_TOGGLE_WIFI); + mWifiPreference = screen.findPreference(KEY_TOGGLE_WIFI); } @Override diff --git a/src/com/android/settings/wifi/WifiNoInternetDialog.java b/src/com/android/settings/wifi/WifiNoInternetDialog.java index e641f2cc09..eb42097fa1 100644 --- a/src/com/android/settings/wifi/WifiNoInternetDialog.java +++ b/src/com/android/settings/wifi/WifiNoInternetDialog.java @@ -16,6 +16,11 @@ package com.android.settings.wifi; +import static android.net.ConnectivityManager.ACTION_PROMPT_LOST_VALIDATION; +import static android.net.ConnectivityManager.ACTION_PROMPT_PARTIAL_CONNECTIVITY; +import static android.net.ConnectivityManager.ACTION_PROMPT_UNVALIDATED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED; + import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -37,11 +42,6 @@ import com.android.internal.app.AlertActivity; import com.android.internal.app.AlertController; import com.android.settings.R; -import static android.net.ConnectivityManager.ACTION_PROMPT_LOST_VALIDATION; -import static android.net.ConnectivityManager.ACTION_PROMPT_PARTIAL_CONNECTIVITY; -import static android.net.ConnectivityManager.ACTION_PROMPT_UNVALIDATED; -import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED; - public final class WifiNoInternetDialog extends AlertActivity implements DialogInterface.OnClickListener { private static final String TAG = "WifiNoInternetDialog"; @@ -52,6 +52,7 @@ public final class WifiNoInternetDialog extends AlertActivity implements private ConnectivityManager.NetworkCallback mNetworkCallback; private CheckBox mAlwaysAllow; private String mAction; + private boolean mButtonClicked; private boolean isKnownAction(Intent intent) { return intent.getAction().equals(ACTION_PROMPT_UNVALIDATED) @@ -169,14 +170,31 @@ public final class WifiNoInternetDialog extends AlertActivity implements mCM.unregisterNetworkCallback(mNetworkCallback); mNetworkCallback = null; } + + // If the user exits the no Internet or partial connectivity dialog without taking any + // action, disconnect the network, because once the dialog has been dismissed there is no + // way to use the network. + // + // Unfortunately, AlertDialog does not seem to offer any good way to get an onCancel or + // onDismiss callback. So we implement this ourselves. + if (isFinishing() && !mButtonClicked) { + if (ACTION_PROMPT_PARTIAL_CONNECTIVITY.equals(mAction)) { + mCM.setAcceptPartialConnectivity(mNetwork, false /* accept */, false /* always */); + } else if (ACTION_PROMPT_UNVALIDATED.equals(mAction)) { + mCM.setAcceptUnvalidated(mNetwork, false /* accept */, false /* always */); + } + } super.onDestroy(); } + @Override public void onClick(DialogInterface dialog, int which) { if (which != BUTTON_NEGATIVE && which != BUTTON_POSITIVE) return; final boolean always = mAlwaysAllow.isChecked(); final String what, action; + mButtonClicked = true; + if (ACTION_PROMPT_UNVALIDATED.equals(mAction)) { what = "NO_INTERNET"; final boolean accept = (which == BUTTON_POSITIVE); diff --git a/src/com/android/settings/wifi/WifiPickerActivity.java b/src/com/android/settings/wifi/WifiPickerActivity.java index 51904418f8..a590a0f5c9 100644 --- a/src/com/android/settings/wifi/WifiPickerActivity.java +++ b/src/com/android/settings/wifi/WifiPickerActivity.java @@ -16,12 +16,14 @@ package com.android.settings.wifi; import android.content.Intent; -import androidx.preference.PreferenceFragment; + +import androidx.preference.PreferenceFragmentCompat; import com.android.settings.ButtonBarHandler; import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.wifi.p2p.WifiP2pSettings; +import com.android.settings.wifi.savedaccesspoints.SavedAccessPointsWifiSettings; public class WifiPickerActivity extends SettingsActivity implements ButtonBarHandler { @@ -45,7 +47,7 @@ public class WifiPickerActivity extends SettingsActivity implements ButtonBarHan return false; } - /* package */ Class<? extends PreferenceFragment> getWifiSettingsClass() { + /* package */ Class<? extends PreferenceFragmentCompat> getWifiSettingsClass() { return WifiSettings.class; } } diff --git a/src/com/android/settings/wifi/WifiScanModeActivity.java b/src/com/android/settings/wifi/WifiScanModeActivity.java index 2c0d0d5713..53427299df 100644 --- a/src/com/android/settings/wifi/WifiScanModeActivity.java +++ b/src/com/android/settings/wifi/WifiScanModeActivity.java @@ -16,10 +16,8 @@ package com.android.settings.wifi; -import android.app.Activity; -import android.app.AlertDialog; import android.app.Dialog; -import android.app.DialogFragment; +import android.app.settings.SettingsEnums; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.ApplicationInfo; @@ -27,15 +25,19 @@ import android.content.pm.PackageManager; import android.net.wifi.WifiManager; import android.os.Bundle; import android.provider.Settings; +import android.text.TextUtils; + +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.FragmentActivity; -import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; /** * This activity requests users permission to allow scanning even when Wi-Fi is turned off */ -public class WifiScanModeActivity extends Activity { +public class WifiScanModeActivity extends FragmentActivity { private DialogFragment mDialog; private String mApp; @@ -66,7 +68,7 @@ public class WifiScanModeActivity extends Activity { private void createDialog() { if (mDialog == null) { mDialog = AlertDialogFragment.newInstance(mApp); - mDialog.show(getFragmentManager(), "dialog"); + mDialog.show(getSupportFragmentManager(), "dialog"); } } @@ -125,13 +127,15 @@ public class WifiScanModeActivity extends Activity { @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.DIALOG_WIFI_SCAN_MODE; + return SettingsEnums.DIALOG_WIFI_SCAN_MODE; } @Override public Dialog onCreateDialog(Bundle savedInstanceState) { return new AlertDialog.Builder(getActivity()) - .setMessage(getString(R.string.wifi_scan_always_turnon_message, mApp)) + .setMessage(TextUtils.isEmpty(mApp) ? + getString(R.string.wifi_scan_always_turn_on_message_unknown) : + getString(R.string.wifi_scan_always_turnon_message, mApp)) .setPositiveButton(R.string.wifi_scan_always_confirm_allow, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { diff --git a/src/com/android/settings/wifi/WifiScanningRequiredFragment.java b/src/com/android/settings/wifi/WifiScanningRequiredFragment.java index 9db766cb3b..fa9b21a518 100644 --- a/src/com/android/settings/wifi/WifiScanningRequiredFragment.java +++ b/src/com/android/settings/wifi/WifiScanningRequiredFragment.java @@ -16,8 +16,8 @@ package com.android.settings.wifi; import android.app.Activity; -import android.app.AlertDialog; import android.app.Dialog; +import android.app.settings.SettingsEnums; import android.content.ActivityNotFoundException; import android.content.ContentResolver; import android.content.Context; @@ -25,12 +25,13 @@ import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; import android.provider.Settings; -import androidx.annotation.VisibleForTesting; import android.text.TextUtils; import android.util.Log; import android.widget.Toast; -import com.android.internal.logging.nano.MetricsProto; +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; + import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; import com.android.settingslib.HelpUtils; @@ -59,7 +60,7 @@ public class WifiScanningRequiredFragment extends InstrumentedDialogFragment imp @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.WIFI_SCANNING_NEEDED_DIALOG; + return SettingsEnums.WIFI_SCANNING_NEEDED_DIALOG; } @Override diff --git a/src/com/android/settings/wifi/WifiSettings.java b/src/com/android/settings/wifi/WifiSettings.java index 7aeaba6566..8c4bfa28ad 100644 --- a/src/com/android/settings/wifi/WifiSettings.java +++ b/src/com/android/settings/wifi/WifiSettings.java @@ -22,8 +22,10 @@ import static android.os.UserManager.DISALLOW_CONFIG_WIFI; import android.annotation.NonNull; import android.app.Activity; import android.app.Dialog; +import android.app.settings.SettingsEnums; import android.content.ContentResolver; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.content.res.Resources; import android.net.ConnectivityManager; @@ -31,17 +33,15 @@ import android.net.Network; import android.net.NetworkInfo; import android.net.NetworkInfo.State; import android.net.NetworkRequest; +import android.net.NetworkTemplate; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiManager; -import android.nfc.NfcAdapter; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.PowerManager; import android.provider.Settings; -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; -import androidx.preference.PreferenceCategory; +import android.util.FeatureFlagUtils; import android.util.Log; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; @@ -50,13 +50,19 @@ import android.view.MenuItem; import android.view.View; import android.widget.Toast; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; + import com.android.settings.LinkifyUtils; import com.android.settings.R; import com.android.settings.RestrictedSettingsFragment; import com.android.settings.SettingsActivity; +import com.android.settings.core.FeatureFlags; import com.android.settings.core.SubSettingLauncher; import com.android.settings.dashboard.SummaryLoader; +import com.android.settings.datausage.DataUsageUtils; +import com.android.settings.datausage.DataUsagePreference; import com.android.settings.location.ScanningSettings; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; @@ -64,10 +70,14 @@ import com.android.settings.search.SearchIndexableRaw; import com.android.settings.widget.SummaryUpdater.OnSummaryChangeListener; import com.android.settings.widget.SwitchBarController; import com.android.settings.wifi.details.WifiNetworkDetailsFragment; +import com.android.settings.wifi.dpp.WifiDppUtils; import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; +import com.android.settingslib.search.SearchIndexable; import com.android.settingslib.wifi.AccessPoint; import com.android.settingslib.wifi.AccessPoint.AccessPointListener; import com.android.settingslib.wifi.AccessPointPreference; +import com.android.settingslib.wifi.WifiSavedConfigUtils; import com.android.settingslib.wifi.WifiTracker; import com.android.settingslib.wifi.WifiTrackerFactory; @@ -82,31 +92,37 @@ import java.util.List; * The second is for Setup Wizard, with a simplified interface that hides the action bar * and menus. */ +@SearchIndexable public class WifiSettings extends RestrictedSettingsFragment implements Indexable, WifiTracker.WifiListener, AccessPointListener, - WifiDialog.WifiDialogListener { + WifiDialog.WifiDialogListener, DialogInterface.OnDismissListener { private static final String TAG = "WifiSettings"; private static final int MENU_ID_CONNECT = Menu.FIRST + 6; - private static final int MENU_ID_FORGET = Menu.FIRST + 7; + @VisibleForTesting + static final int MENU_ID_FORGET = Menu.FIRST + 7; private static final int MENU_ID_MODIFY = Menu.FIRST + 8; - private static final int MENU_ID_WRITE_NFC = Menu.FIRST + 9; public static final int WIFI_DIALOG_ID = 1; - private static final int WRITE_NFC_DIALOG_ID = 6; + + @VisibleForTesting + static final int ADD_NETWORK_REQUEST = 2; // Instance state keys private static final String SAVE_DIALOG_MODE = "dialog_mode"; private static final String SAVE_DIALOG_ACCESS_POINT_STATE = "wifi_ap_state"; - private static final String SAVED_WIFI_NFC_DIALOG_STATE = "wifi_nfc_dlg_state"; private static final String PREF_KEY_EMPTY_WIFI_LIST = "wifi_empty_list"; private static final String PREF_KEY_CONNECTED_ACCESS_POINTS = "connected_access_point"; private static final String PREF_KEY_ACCESS_POINTS = "access_points"; - private static final String PREF_KEY_ADDITIONAL_SETTINGS = "additional_settings"; private static final String PREF_KEY_CONFIGURE_WIFI_SETTINGS = "configure_settings"; private static final String PREF_KEY_SAVED_NETWORKS = "saved_networks"; + private static final String PREF_KEY_STATUS_MESSAGE = "wifi_status_message"; + @VisibleForTesting + static final String PREF_KEY_DATA_USAGE = "wifi_data_usage"; + + private static final int REQUEST_CODE_WIFI_DPP_ENROLLEE_QR_CODE_SCANNER = 0; private static boolean isVerboseLoggingEnabled() { return WifiTracker.sVerboseLogging || Log.isLoggable(TAG, Log.VERBOSE); @@ -138,7 +154,6 @@ public class WifiSettings extends RestrictedSettingsFragment private AccessPoint mSelectedAccessPoint; private WifiDialog mDialog; - private WriteWifiConfigToNfcDialog mWifiToNfcDialog; private View mProgressHeader; @@ -157,19 +172,23 @@ public class WifiSettings extends RestrictedSettingsFragment private int mDialogMode; private AccessPoint mDlgAccessPoint; private Bundle mAccessPointSavedState; - private Bundle mWifiNfcDialogSavedState; - private WifiTracker mWifiTracker; + @VisibleForTesting + WifiTracker mWifiTracker; private String mOpenSsid; private AccessPointPreference.UserBadgeCache mUserBadgeCache; private PreferenceCategory mConnectedAccessPointPreferenceCategory; private PreferenceCategory mAccessPointsPreferenceCategory; - private PreferenceCategory mAdditionalSettingsPreferenceCategory; - private Preference mAddPreference; - private Preference mConfigureWifiSettingsPreference; - private Preference mSavedNetworksPreference; + @VisibleForTesting + AddWifiNetworkPreference mAddWifiNetworkPreference; + @VisibleForTesting + Preference mConfigureWifiSettingsPreference; + @VisibleForTesting + Preference mSavedNetworksPreference; + @VisibleForTesting + DataUsagePreference mDataUsagePreference; private LinkablePreference mStatusMessagePreference; // For Search @@ -192,7 +211,7 @@ public class WifiSettings extends RestrictedSettingsFragment super.onViewCreated(view, savedInstanceState); final Activity activity = getActivity(); if (activity != null) { - mProgressHeader = setPinnedHeaderView(R.layout.wifi_progress_header) + mProgressHeader = setPinnedHeaderView(R.layout.progress_header) .findViewById(R.id.progress_bar_animation); setProgressBarVisible(false); } @@ -221,18 +240,16 @@ public class WifiSettings extends RestrictedSettingsFragment (PreferenceCategory) findPreference(PREF_KEY_CONNECTED_ACCESS_POINTS); mAccessPointsPreferenceCategory = (PreferenceCategory) findPreference(PREF_KEY_ACCESS_POINTS); - mAdditionalSettingsPreferenceCategory = - (PreferenceCategory) findPreference(PREF_KEY_ADDITIONAL_SETTINGS); mConfigureWifiSettingsPreference = findPreference(PREF_KEY_CONFIGURE_WIFI_SETTINGS); mSavedNetworksPreference = findPreference(PREF_KEY_SAVED_NETWORKS); - - Context prefContext = getPrefContext(); - mAddPreference = new Preference(prefContext); - mAddPreference.setIcon(R.drawable.ic_menu_add_inset); - mAddPreference.setTitle(R.string.wifi_add_network); - mStatusMessagePreference = new LinkablePreference(prefContext); - + mAddWifiNetworkPreference = new AddWifiNetworkPreference(getPrefContext()); + mStatusMessagePreference = (LinkablePreference) findPreference(PREF_KEY_STATUS_MESSAGE); mUserBadgeCache = new AccessPointPreference.UserBadgeCache(getPackageManager()); + mDataUsagePreference = findPreference(PREF_KEY_DATA_USAGE); + mDataUsagePreference.setVisible(DataUsageUtils.hasWifiRadio(getContext())); + mDataUsagePreference.setTemplate(NetworkTemplate.buildTemplateWifiWildcard(), + 0 /*subId*/, + null /*service*/); } @Override @@ -240,7 +257,7 @@ public class WifiSettings extends RestrictedSettingsFragment super.onActivityCreated(savedInstanceState); mWifiTracker = WifiTrackerFactory.create( - getActivity(), this, getLifecycle(), true, true); + getActivity(), this, getSettingsLifecycle(), true, true); mWifiManager = mWifiTracker.getManager(); final Activity activity = getActivity(); @@ -248,61 +265,45 @@ public class WifiSettings extends RestrictedSettingsFragment mConnectivityManager = getActivity().getSystemService(ConnectivityManager.class); } - mConnectListener = new WifiManager.ActionListener() { - @Override - public void onSuccess() { - } - @Override - public void onFailure(int reason) { - Activity activity = getActivity(); - if (activity != null) { - Toast.makeText(activity, - R.string.wifi_failed_connect_message, - Toast.LENGTH_SHORT).show(); - } - } - }; + mConnectListener = new WifiConnectListener(getActivity()); mSaveListener = new WifiManager.ActionListener() { - @Override - public void onSuccess() { - } - @Override - public void onFailure(int reason) { - Activity activity = getActivity(); - if (activity != null) { - Toast.makeText(activity, - R.string.wifi_failed_save_message, - Toast.LENGTH_SHORT).show(); - } - } - }; + @Override + public void onSuccess() { + } + + @Override + public void onFailure(int reason) { + Activity activity = getActivity(); + if (activity != null) { + Toast.makeText(activity, + R.string.wifi_failed_save_message, + Toast.LENGTH_SHORT).show(); + } + } + }; mForgetListener = new WifiManager.ActionListener() { - @Override - public void onSuccess() { - } - @Override - public void onFailure(int reason) { - Activity activity = getActivity(); - if (activity != null) { - Toast.makeText(activity, - R.string.wifi_failed_forget_message, - Toast.LENGTH_SHORT).show(); - } - } - }; + @Override + public void onSuccess() { + } + + @Override + public void onFailure(int reason) { + Activity activity = getActivity(); + if (activity != null) { + Toast.makeText(activity, + R.string.wifi_failed_forget_message, + Toast.LENGTH_SHORT).show(); + } + } + }; if (savedInstanceState != null) { mDialogMode = savedInstanceState.getInt(SAVE_DIALOG_MODE); if (savedInstanceState.containsKey(SAVE_DIALOG_ACCESS_POINT_STATE)) { mAccessPointSavedState = - savedInstanceState.getBundle(SAVE_DIALOG_ACCESS_POINT_STATE); - } - - if (savedInstanceState.containsKey(SAVED_WIFI_NFC_DIALOG_STATE)) { - mWifiNfcDialogSavedState = - savedInstanceState.getBundle(SAVED_WIFI_NFC_DIALOG_STATE); + savedInstanceState.getBundle(SAVE_DIALOG_ACCESS_POINT_STATE); } } @@ -368,7 +369,7 @@ public class WifiSettings extends RestrictedSettingsFragment private WifiEnabler createWifiEnabler() { final SettingsActivity activity = (SettingsActivity) getActivity(); return new WifiEnabler(activity, new SwitchBarController(activity.getSwitchBar()), - mMetricsFeatureProvider); + mMetricsFeatureProvider); } @Override @@ -409,6 +410,19 @@ public class WifiSettings extends RestrictedSettingsFragment public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); + if (requestCode == ADD_NETWORK_REQUEST) { + handleAddNetworkRequest(resultCode, data); + return; + } else if (requestCode == REQUEST_CODE_WIFI_DPP_ENROLLEE_QR_CODE_SCANNER) { + if (resultCode == Activity.RESULT_OK) { + if (mDialog != null) { + mDialog.dismiss(); + } + mWifiTracker.resumeScanning(); + } + return; + } + final boolean formerlyRestricted = mIsRestricted; mIsRestricted = isUiRestricted(); if (formerlyRestricted && !mIsRestricted @@ -420,15 +434,14 @@ public class WifiSettings extends RestrictedSettingsFragment @Override public int getMetricsCategory() { - return MetricsEvent.WIFI; + return SettingsEnums.WIFI; } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); - - // If the dialog is showing, save its state. - if (mDialog != null && mDialog.isShowing()) { + // If dialog has been shown, save its state. + if (mDialog != null) { outState.putInt(SAVE_DIALOG_MODE, mDialogMode); if (mDlgAccessPoint != null) { mAccessPointSavedState = new Bundle(); @@ -436,48 +449,38 @@ public class WifiSettings extends RestrictedSettingsFragment outState.putBundle(SAVE_DIALOG_ACCESS_POINT_STATE, mAccessPointSavedState); } } - - if (mWifiToNfcDialog != null && mWifiToNfcDialog.isShowing()) { - Bundle savedState = new Bundle(); - mWifiToNfcDialog.saveState(savedState); - outState.putBundle(SAVED_WIFI_NFC_DIALOG_STATE, savedState); - } } @Override public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo info) { - Preference preference = (Preference) view.getTag(); - - if (preference instanceof LongPressAccessPointPreference) { - mSelectedAccessPoint = - ((LongPressAccessPointPreference) preference).getAccessPoint(); - menu.setHeaderTitle(mSelectedAccessPoint.getSsid()); - if (mSelectedAccessPoint.isConnectable()) { - menu.add(Menu.NONE, MENU_ID_CONNECT, 0, R.string.wifi_menu_connect); - } + Preference preference = (Preference) view.getTag(); - WifiConfiguration config = mSelectedAccessPoint.getConfig(); - // Some configs are ineditable - if (WifiUtils.isNetworkLockedDown(getActivity(), config)) { - return; - } + if (preference instanceof LongPressAccessPointPreference) { + mSelectedAccessPoint = + ((LongPressAccessPointPreference) preference).getAccessPoint(); + menu.setHeaderTitle(mSelectedAccessPoint.getTitle()); + if (mSelectedAccessPoint.isConnectable()) { + menu.add(Menu.NONE, MENU_ID_CONNECT, 0 /* order */, R.string.wifi_connect); + } - if (mSelectedAccessPoint.isSaved() || mSelectedAccessPoint.isEphemeral()) { - // Allow forgetting a network if either the network is saved or ephemerally - // connected. (In the latter case, "forget" blacklists the network so it won't - // be used again, ephemerally). - menu.add(Menu.NONE, MENU_ID_FORGET, 0, R.string.wifi_menu_forget); - } - if (mSelectedAccessPoint.isSaved()) { - menu.add(Menu.NONE, MENU_ID_MODIFY, 0, R.string.wifi_menu_modify); - NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(getActivity()); - if (nfcAdapter != null && nfcAdapter.isEnabled() && - mSelectedAccessPoint.getSecurity() != AccessPoint.SECURITY_NONE) { - // Only allow writing of NFC tags for password-protected networks. - menu.add(Menu.NONE, MENU_ID_WRITE_NFC, 0, R.string.wifi_menu_write_to_nfc); - } - } + WifiConfiguration config = mSelectedAccessPoint.getConfig(); + // Some configs are ineditable + if (WifiUtils.isNetworkLockedDown(getActivity(), config)) { + return; + } + + // "forget" for normal saved network. And "disconnect" for ephemeral network because it + // could only be disconnected and be put in blacklists so it won't be used again. + if (mSelectedAccessPoint.isSaved() || mSelectedAccessPoint.isEphemeral()) { + final int stringId = mSelectedAccessPoint.isEphemeral() ? + R.string.wifi_disconnect_button_text : R.string.forget; + menu.add(Menu.NONE, MENU_ID_FORGET, 0 /* order */, stringId); + } + + if (mSelectedAccessPoint.isSaved() && !mSelectedAccessPoint.isActive()) { + menu.add(Menu.NONE, MENU_ID_MODIFY, 0 /* order */, R.string.wifi_modify); } + } } @Override @@ -490,7 +493,10 @@ public class WifiSettings extends RestrictedSettingsFragment boolean isSavedNetwork = mSelectedAccessPoint.isSaved(); if (isSavedNetwork) { connect(mSelectedAccessPoint.getConfig(), isSavedNetwork); - } else if (mSelectedAccessPoint.getSecurity() == AccessPoint.SECURITY_NONE) { + } else if ((mSelectedAccessPoint.getSecurity() == AccessPoint.SECURITY_NONE) || + (mSelectedAccessPoint.getSecurity() == AccessPoint.SECURITY_OWE) || + (mSelectedAccessPoint.getSecurity() + == AccessPoint.SECURITY_OWE_TRANSITION)) { /** Bypass dialog for unsecured networks */ mSelectedAccessPoint.generateOpenNetworkConfig(); connect(mSelectedAccessPoint.getConfig(), isSavedNetwork); @@ -507,10 +513,6 @@ public class WifiSettings extends RestrictedSettingsFragment showDialog(mSelectedAccessPoint, WifiConfigUiBase.MODE_MODIFY); return true; } - case MENU_ID_WRITE_NFC: - showDialog(WRITE_NFC_DIALOG_ID); - return true; - } return super.onContextItemSelected(item); } @@ -535,22 +537,26 @@ public class WifiSettings extends RestrictedSettingsFragment * Bypass dialog and connect to unsecured networks, or previously connected saved * networks, or Passpoint provided networks. */ - WifiConfiguration config = mSelectedAccessPoint.getConfig(); - if (mSelectedAccessPoint.getSecurity() == AccessPoint.SECURITY_NONE) { - mSelectedAccessPoint.generateOpenNetworkConfig(); - connect(mSelectedAccessPoint.getConfig(), mSelectedAccessPoint.isSaved()); - } else if (mSelectedAccessPoint.isSaved() && config != null - && config.getNetworkSelectionStatus() != null - && config.getNetworkSelectionStatus().getHasEverConnected()) { - connect(config, true /* isSavedNetwork */); - } else if (mSelectedAccessPoint.isPasspoint()) { - // Access point provided by an installed Passpoint provider, connect using - // the associated config. - connect(config, true /* isSavedNetwork */); - } else { - showDialog(mSelectedAccessPoint, WifiConfigUiBase.MODE_CONNECT); + switch (WifiUtils.getConnectingType(mSelectedAccessPoint)) { + case WifiUtils.CONNECT_TYPE_OSU_PROVISION: + mSelectedAccessPoint.startOsuProvisioning(mConnectListener); + mClickedConnect = true; + break; + + case WifiUtils.CONNECT_TYPE_OPEN_NETWORK: + mSelectedAccessPoint.generateOpenNetworkConfig(); + connect(mSelectedAccessPoint.getConfig(), mSelectedAccessPoint.isSaved()); + break; + + case WifiUtils.CONNECT_TYPE_SAVED_NETWORK: + connect(mSelectedAccessPoint.getConfig(), true /* isSavedNetwork */); + break; + + default: + showDialog(mSelectedAccessPoint, WifiConfigUiBase.MODE_CONNECT); + break; } - } else if (preference == mAddPreference) { + } else if (preference == mAddWifiNetworkPreference) { onAddNetworkPressed(); } else { return super.onPreferenceTreeClick(preference); @@ -563,7 +569,7 @@ public class WifiSettings extends RestrictedSettingsFragment WifiConfiguration config = accessPoint.getConfig(); if (WifiUtils.isNetworkLockedDown(getActivity(), config) && accessPoint.isActive()) { RestrictedLockUtils.sendShowAdminSupportDetailsIntent(getActivity(), - RestrictedLockUtils.getDeviceOwner(getActivity())); + RestrictedLockUtilsInternal.getDeviceOwner(getActivity())); return; } } @@ -584,46 +590,38 @@ public class WifiSettings extends RestrictedSettingsFragment public Dialog onCreateDialog(int dialogId) { switch (dialogId) { case WIFI_DIALOG_ID: - if (mDlgAccessPoint == null && mAccessPointSavedState == null) { - // add new network - mDialog = WifiDialog - .createFullscreen(getActivity(), this, mDlgAccessPoint, mDialogMode); - } else { - // modify network - if (mDlgAccessPoint == null) { - // restore AP from save state - mDlgAccessPoint = new AccessPoint(getActivity(), mAccessPointSavedState); - // Reset the saved access point data - mAccessPointSavedState = null; - } - mDialog = WifiDialog - .createModal(getActivity(), this, mDlgAccessPoint, mDialogMode); + // modify network + if (mDlgAccessPoint == null && mAccessPointSavedState != null) { + // restore AP from save state + mDlgAccessPoint = new AccessPoint(getActivity(), mAccessPointSavedState); + // Reset the saved access point data + mAccessPointSavedState = null; } - + mDialog = WifiDialog + .createModal(getActivity(), this, mDlgAccessPoint, mDialogMode); mSelectedAccessPoint = mDlgAccessPoint; return mDialog; - case WRITE_NFC_DIALOG_ID: - if (mSelectedAccessPoint != null) { - mWifiToNfcDialog = new WriteWifiConfigToNfcDialog( - getActivity(), - mSelectedAccessPoint.getSecurity()); - } else if (mWifiNfcDialogSavedState != null) { - mWifiToNfcDialog = new WriteWifiConfigToNfcDialog(getActivity(), - mWifiNfcDialogSavedState); - } - - return mWifiToNfcDialog; } return super.onCreateDialog(dialogId); } @Override + public void onDialogShowing() { + super.onDialogShowing(); + setOnDismissListener(this); + } + + @Override + public void onDismiss(DialogInterface dialog) { + // We don't keep any dialog object when dialog was dismissed. + mDialog = null; + } + + @Override public int getDialogMetricsCategory(int dialogId) { switch (dialogId) { case WIFI_DIALOG_ID: - return MetricsEvent.DIALOG_WIFI_AP_EDIT; - case WRITE_NFC_DIALOG_ID: - return MetricsEvent.DIALOG_WIFI_WRITE_NFC; + return SettingsEnums.DIALOG_WIFI_AP_EDIT; default: return 0; } @@ -671,14 +669,14 @@ public class WifiSettings extends RestrictedSettingsFragment case WifiManager.WIFI_STATE_ENABLING: removeConnectedAccessPointPreference(); - mAccessPointsPreferenceCategory.removeAll(); + removeAccessPointPreference(); addMessagePreference(R.string.wifi_starting); setProgressBarVisible(true); break; case WifiManager.WIFI_STATE_DISABLING: removeConnectedAccessPointPreference(); - mAccessPointsPreferenceCategory.removeAll(); + removeAccessPointPreference(); addMessagePreference(R.string.wifi_stopping); break; @@ -725,7 +723,10 @@ public class WifiSettings extends RestrictedSettingsFragment } boolean hasAvailableAccessPoints = false; - mAccessPointsPreferenceCategory.removePreference(mStatusMessagePreference); + mStatusMessagePreference.setVisible(false); + mConnectedAccessPointPreferenceCategory.setVisible(true); + mAccessPointsPreferenceCategory.setVisible(true); + cacheRemoveAllPrefs(mAccessPointsPreferenceCategory); int index = @@ -748,7 +749,9 @@ public class WifiSettings extends RestrictedSettingsFragment preference.setKey(key); preference.setOrder(index); if (mOpenSsid != null && mOpenSsid.equals(accessPoint.getSsidStr()) - && accessPoint.getSecurity() != AccessPoint.SECURITY_NONE) { + && (accessPoint.getSecurity() != AccessPoint.SECURITY_NONE && + accessPoint.getSecurity() != AccessPoint.SECURITY_OWE && + accessPoint.getSecurity() != AccessPoint.SECURITY_OWE_TRANSITION)) { if (!accessPoint.isSaved() || isDisabledByWrongPassword(accessPoint)) { onPreferenceTreeClick(preference); mOpenSsid = null; @@ -760,8 +763,8 @@ public class WifiSettings extends RestrictedSettingsFragment } } removeCachedPrefs(mAccessPointsPreferenceCategory); - mAddPreference.setOrder(index); - mAccessPointsPreferenceCategory.addPreference(mAddPreference); + mAddWifiNetworkPreference.setOrder(index); + mAccessPointsPreferenceCategory.addPreference(mAddWifiNetworkPreference); setAdditionalSettingsSummaries(); if (!hasAvailableAccessPoints) { @@ -786,10 +789,11 @@ public class WifiSettings extends RestrictedSettingsFragment } @NonNull - private ConnectedAccessPointPreference createConnectedAccessPointPreference( - AccessPoint accessPoint) { - return new ConnectedAccessPointPreference(accessPoint, getPrefContext(), mUserBadgeCache, - R.drawable.ic_wifi_signal_0, false /* forSavedNetworks */); + @VisibleForTesting + ConnectedAccessPointPreference createConnectedAccessPointPreference( + AccessPoint accessPoint, Context context) { + return new ConnectedAccessPointPreference(accessPoint, context, mUserBadgeCache, + R.drawable.ic_wifi_signal_0, false /* forSavedNetworks */, this); } /** @@ -842,7 +846,7 @@ public class WifiSettings extends RestrictedSettingsFragment */ private void addConnectedAccessPointPreference(AccessPoint connectedAp) { final ConnectedAccessPointPreference pref = - createConnectedAccessPointPreference(connectedAp); + createConnectedAccessPointPreference(connectedAp, getPrefContext()); registerCaptivePortalNetworkCallback(getCurrentWifiNetwork(), pref); // Launch details page or captive portal on click. @@ -910,9 +914,25 @@ public class WifiSettings extends RestrictedSettingsFragment } } + private void launchAddNetworkFragment() { + new SubSettingLauncher(getContext()) + .setTitleRes(R.string.wifi_add_network) + .setDestination(AddNetworkFragment.class.getName()) + .setSourceMetricsCategory(getMetricsCategory()) + .setResultListener(this, ADD_NETWORK_REQUEST) + .launch(); + } + private void launchNetworkDetailsFragment(ConnectedAccessPointPreference pref) { + final AccessPoint accessPoint = pref.getAccessPoint(); + final Context context = getContext(); + final CharSequence title = + FeatureFlagUtils.isEnabled(context, FeatureFlags.WIFI_DETAILS_DATAUSAGE_HEADER) + ? accessPoint.getTitle() + : context.getText(R.string.pref_title_network_details); + new SubSettingLauncher(getContext()) - .setTitle(R.string.pref_title_network_details) + .setTitleText(title) .setDestination(WifiNetworkDetailsFragment.class.getName()) .setArguments(pref.getExtras()) .setSourceMetricsCategory(getMetricsCategory()) @@ -930,32 +950,61 @@ public class WifiSettings extends RestrictedSettingsFragment unregisterCaptivePortalNetworkCallback(); } - private void setAdditionalSettingsSummaries() { - mAdditionalSettingsPreferenceCategory.addPreference(mConfigureWifiSettingsPreference); + private void removeAccessPointPreference() { + mAccessPointsPreferenceCategory.removeAll(); + mAccessPointsPreferenceCategory.setVisible(false); + } + + @VisibleForTesting + void setAdditionalSettingsSummaries() { mConfigureWifiSettingsPreference.setSummary(getString( isWifiWakeupEnabled() ? R.string.wifi_configure_settings_preference_summary_wakeup_on : R.string.wifi_configure_settings_preference_summary_wakeup_off)); - int numSavedNetworks = mWifiTracker.getNumSavedNetworks(); + + final List<AccessPoint> savedNetworks = + WifiSavedConfigUtils.getAllConfigs(getContext(), mWifiManager); + final int numSavedNetworks = (savedNetworks != null) ? savedNetworks.size() : 0; + mSavedNetworksPreference.setVisible(numSavedNetworks > 0); if (numSavedNetworks > 0) { - mAdditionalSettingsPreferenceCategory.addPreference(mSavedNetworksPreference); mSavedNetworksPreference.setSummary( - getResources().getQuantityString(R.plurals.wifi_saved_access_points_summary, - numSavedNetworks, numSavedNetworks)); + getSavedNetworkSettingsSummaryText(savedNetworks, numSavedNetworks)); + } + } + + private String getSavedNetworkSettingsSummaryText( + List<AccessPoint> savedNetworks, int numSavedNetworks) { + int numSavedPasspointNetworks = 0; + for (AccessPoint savedNetwork : savedNetworks) { + if (savedNetwork.isPasspointConfig() || savedNetwork.isPasspoint()) { + numSavedPasspointNetworks++; + } + } + final int numSavedNormalNetworks = numSavedNetworks - numSavedPasspointNetworks; + + if (numSavedNetworks == numSavedNormalNetworks) { + return getResources().getQuantityString(R.plurals.wifi_saved_access_points_summary, + numSavedNormalNetworks, numSavedNormalNetworks); + } else if (numSavedNetworks == numSavedPasspointNetworks) { + return getResources().getQuantityString( + R.plurals.wifi_saved_passpoint_access_points_summary, + numSavedPasspointNetworks, numSavedPasspointNetworks); } else { - mAdditionalSettingsPreferenceCategory.removePreference(mSavedNetworksPreference); + return getResources().getQuantityString(R.plurals.wifi_saved_all_access_points_summary, + numSavedNetworks, numSavedNetworks); } } private boolean isWifiWakeupEnabled() { - PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); - ContentResolver contentResolver = getContentResolver(); + final Context context = getContext(); + final PowerManager powerManager = context.getSystemService(PowerManager.class); + final ContentResolver contentResolver = context.getContentResolver(); return Settings.Global.getInt(contentResolver, - Settings.Global.WIFI_WAKEUP_ENABLED, 0) == 1 + Settings.Global.WIFI_WAKEUP_ENABLED, 0) == 1 && Settings.Global.getInt(contentResolver, - Settings.Global.WIFI_SCAN_ALWAYS_AVAILABLE, 0) == 1 + Settings.Global.WIFI_SCAN_ALWAYS_AVAILABLE, 0) == 1 && Settings.Global.getInt(contentResolver, - Settings.Global.AIRPLANE_MODE_ON, 0) == 0 + Settings.Global.AIRPLANE_MODE_ON, 0) == 0 && !powerManager.isPowerSaveMode(); } @@ -971,20 +1020,19 @@ public class WifiSettings extends RestrictedSettingsFragment final LinkifyUtils.OnClickListener clickListener = () -> new SubSettingLauncher(getContext()) .setDestination(ScanningSettings.class.getName()) - .setTitle(R.string.location_scanning_screen_title) + .setTitleRes(R.string.location_scanning_screen_title) .setSourceMetricsCategory(getMetricsCategory()) .launch(); mStatusMessagePreference.setText(title, description, clickListener); removeConnectedAccessPointPreference(); - mAccessPointsPreferenceCategory.removeAll(); - mAccessPointsPreferenceCategory.addPreference(mStatusMessagePreference); + removeAccessPointPreference(); + mStatusMessagePreference.setVisible(true); } private void addMessagePreference(int messageId) { mStatusMessagePreference.setTitle(messageId); - removeConnectedAccessPointPreference(); - mAccessPointsPreferenceCategory.removeAll(); - mAccessPointsPreferenceCategory.addPreference(mStatusMessagePreference); + mStatusMessagePreference.setVisible(true); + } protected void setProgressBarVisible(boolean visible) { @@ -1017,6 +1065,13 @@ public class WifiSettings extends RestrictedSettingsFragment } } + @Override + public void onScan(WifiDialog dialog, String ssid) { + // Launch QR code scanner to join a network. + startActivityForResult(WifiDppUtils.getEnrolleeQrCodeScannerIntent(ssid), + REQUEST_CODE_WIFI_DPP_ENROLLEE_QR_CODE_SCANNER); + } + /* package */ void submit(WifiConfigController configController) { final WifiConfiguration config = configController.getConfig(); @@ -1039,7 +1094,7 @@ public class WifiSettings extends RestrictedSettingsFragment } /* package */ void forget() { - mMetricsFeatureProvider.action(getActivity(), MetricsEvent.ACTION_WIFI_FORGET); + mMetricsFeatureProvider.action(getActivity(), SettingsEnums.ACTION_WIFI_FORGET); if (!mSelectedAccessPoint.isSaved()) { if (mSelectedAccessPoint.getNetworkInfo() != null && mSelectedAccessPoint.getNetworkInfo().getState() != State.DISCONNECTED) { @@ -1052,7 +1107,12 @@ public class WifiSettings extends RestrictedSettingsFragment return; } } else if (mSelectedAccessPoint.getConfig().isPasspoint()) { - mWifiManager.removePasspointConfiguration(mSelectedAccessPoint.getConfig().FQDN); + try { + mWifiManager.removePasspointConfiguration(mSelectedAccessPoint.getConfig().FQDN); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Failed to remove Passpoint configuration with error: " + e); + return; + } } else { mWifiManager.forget(mSelectedAccessPoint.getConfig().networkId, mForgetListener); } @@ -1065,7 +1125,7 @@ public class WifiSettings extends RestrictedSettingsFragment protected void connect(final WifiConfiguration config, boolean isSavedNetwork) { // Log subtype if configuration is a saved network. - mMetricsFeatureProvider.action(getVisibilityLogger(), MetricsEvent.ACTION_WIFI_CONNECT, + mMetricsFeatureProvider.action(getContext(), SettingsEnums.ACTION_WIFI_CONNECT, isSavedNetwork); mWifiManager.connect(config, mConnectListener); mClickedConnect = true; @@ -1073,19 +1133,34 @@ public class WifiSettings extends RestrictedSettingsFragment protected void connect(final int networkId, boolean isSavedNetwork) { // Log subtype if configuration is a saved network. - mMetricsFeatureProvider.action(getActivity(), MetricsEvent.ACTION_WIFI_CONNECT, + mMetricsFeatureProvider.action(getActivity(), SettingsEnums.ACTION_WIFI_CONNECT, isSavedNetwork); mWifiManager.connect(networkId, mConnectListener); } + @VisibleForTesting + void handleAddNetworkRequest(int result, Intent data) { + if (result == Activity.RESULT_OK) { + handleAddNetworkSubmitEvent(data); + } + mWifiTracker.resumeScanning(); + } + + private void handleAddNetworkSubmitEvent(Intent data) { + final WifiConfiguration wifiConfiguration = data.getParcelableExtra( + AddNetworkFragment.WIFI_CONFIG_KEY); + if (wifiConfiguration != null) { + mWifiManager.save(wifiConfiguration, mSaveListener); + } + } + /** * Called when "add network" button is pressed. */ - /* package */ void onAddNetworkPressed() { - mMetricsFeatureProvider.action(getActivity(), MetricsEvent.ACTION_WIFI_ADD_NETWORK); + private void onAddNetworkPressed() { // No exact access point is selected. mSelectedAccessPoint = null; - showDialog(null, WifiConfigUiBase.MODE_CONNECT); + launchAddNetworkFragment(); } @Override @@ -1116,25 +1191,26 @@ public class WifiSettings extends RestrictedSettingsFragment } public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = - new BaseSearchIndexProvider() { - @Override - public List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled) { - final List<SearchIndexableRaw> result = new ArrayList<>(); - final Resources res = context.getResources(); - - // Add fragment title if we are showing this fragment - if (res.getBoolean(R.bool.config_show_wifi_settings)) { - SearchIndexableRaw data = new SearchIndexableRaw(context); - data.title = res.getString(R.string.wifi_settings); - data.screenTitle = res.getString(R.string.wifi_settings); - data.keywords = res.getString(R.string.keywords_wifi); - data.key = DATA_KEY_REFERENCE; - result.add(data); - } + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableRaw> getRawDataToIndex(Context context, + boolean enabled) { + final List<SearchIndexableRaw> result = new ArrayList<>(); + final Resources res = context.getResources(); + + // Add fragment title if we are showing this fragment + if (res.getBoolean(R.bool.config_show_wifi_settings)) { + SearchIndexableRaw data = new SearchIndexableRaw(context); + data.title = res.getString(R.string.wifi_settings); + data.screenTitle = res.getString(R.string.wifi_settings); + data.keywords = res.getString(R.string.keywords_wifi); + data.key = DATA_KEY_REFERENCE; + result.add(data); + } - return result; - } - }; + return result; + } + }; private static class SummaryProvider implements SummaryLoader.SummaryProvider, OnSummaryChangeListener { @@ -1167,7 +1243,7 @@ public class WifiSettings extends RestrictedSettingsFragment = new SummaryLoader.SummaryProviderFactory() { @Override public SummaryLoader.SummaryProvider createSummaryProvider(Activity activity, - SummaryLoader summaryLoader) { + SummaryLoader summaryLoader) { return new SummaryProvider(activity, summaryLoader); } }; diff --git a/src/com/android/settings/wifi/WifiSliceBuilder.java b/src/com/android/settings/wifi/WifiSliceBuilder.java deleted file mode 100644 index 68aa9a5b8d..0000000000 --- a/src/com/android/settings/wifi/WifiSliceBuilder.java +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Copyright (C) 2018 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.wifi; - -import static android.app.slice.Slice.EXTRA_TOGGLE_STATE; -import static android.provider.SettingsSlicesContract.KEY_WIFI; - -import static androidx.slice.builders.ListBuilder.ICON_IMAGE; - -import android.annotation.ColorInt; -import android.app.PendingIntent; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.net.Uri; -import android.net.wifi.WifiInfo; -import android.net.wifi.WifiManager; -import android.net.wifi.WifiSsid; -import android.provider.SettingsSlicesContract; - -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.settings.R; -import com.android.settings.SubSettings; -import com.android.settings.Utils; -import com.android.settings.search.DatabaseIndexingUtils; -import com.android.settings.slices.SliceBroadcastReceiver; -import com.android.settings.slices.SliceBuilderUtils; - -import androidx.slice.Slice; -import androidx.slice.builders.ListBuilder; -import androidx.slice.builders.SliceAction; - -import androidx.core.graphics.drawable.IconCompat; -import android.text.TextUtils; - -/** - * Utility class to build a Wifi Slice, and handle all associated actions. - */ -public class WifiSliceBuilder { - - /** - * Backing Uri for the Wifi Slice. - */ - public static final Uri WIFI_URI = new Uri.Builder() - .scheme(ContentResolver.SCHEME_CONTENT) - .authority(SettingsSlicesContract.AUTHORITY) - .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) - .appendPath(KEY_WIFI) - .build(); - - /** - * Action notifying a change on the Wifi Slice. - */ - public static final String ACTION_WIFI_SLICE_CHANGED = - "com.android.settings.wifi.action.WIFI_CHANGED"; - - public static final IntentFilter INTENT_FILTER = new IntentFilter(); - - static { - INTENT_FILTER.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION); - INTENT_FILTER.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION); - } - - private WifiSliceBuilder() { - } - - /** - * Return a Wifi Slice bound to {@link #WIFI_URI}. - * <p> - * Note that you should register a listener for {@link #INTENT_FILTER} to get changes for Wifi. - */ - public static Slice getSlice(Context context) { - final boolean isWifiEnabled = isWifiEnabled(context); - final IconCompat icon = IconCompat.createWithResource(context, - R.drawable.ic_settings_wireless); - final String title = context.getString(R.string.wifi_settings); - final CharSequence summary = getSummary(context); - @ColorInt final int color = Utils.getColorAccent(context); - final PendingIntent toggleAction = getBroadcastIntent(context); - final PendingIntent primaryAction = getPrimaryAction(context); - final SliceAction primarySliceAction = new SliceAction(primaryAction, icon, title); - final SliceAction toggleSliceAction = new SliceAction(toggleAction, null /* actionTitle */, - isWifiEnabled); - - return new ListBuilder(context, WIFI_URI, ListBuilder.INFINITY) - .setAccentColor(color) - .addRow(b -> b - .setTitle(title) - .setSubtitle(summary) - .addEndItem(toggleSliceAction) - .setPrimaryAction(primarySliceAction)) - .build(); - } - - /** - * Update the current wifi status to the boolean value keyed by - * {@link android.app.slice.Slice#EXTRA_TOGGLE_STATE} on {@param intent}. - */ - public static void handleUriChange(Context context, Intent intent) { - final WifiManager wifiManager = context.getSystemService(WifiManager.class); - final boolean newState = intent.getBooleanExtra(EXTRA_TOGGLE_STATE, - wifiManager.isWifiEnabled()); - wifiManager.setWifiEnabled(newState); - // Do not notifyChange on Uri. The service takes longer to update the current value than it - // does for the Slice to check the current value again. Let {@link SliceBroadcastRelay} - // handle it. - } - - public static Intent getIntent(Context context) { - final String screenTitle = context.getText(R.string.wifi_settings).toString(); - final Uri contentUri = new Uri.Builder().appendPath(KEY_WIFI).build(); - final Intent intent = DatabaseIndexingUtils.buildSearchResultPageIntent(context, - WifiSettings.class.getName(), KEY_WIFI, screenTitle, - MetricsEvent.DIALOG_WIFI_AP_EDIT) - .setClassName(context.getPackageName(), SubSettings.class.getName()) - .setData(contentUri); - - return intent; - } - - private static boolean isWifiEnabled(Context context) { - final WifiManager wifiManager = context.getSystemService(WifiManager.class); - - switch (wifiManager.getWifiState()) { - case WifiManager.WIFI_STATE_ENABLED: - case WifiManager.WIFI_STATE_ENABLING: - return true; - case WifiManager.WIFI_STATE_DISABLED: - case WifiManager.WIFI_STATE_DISABLING: - case WifiManager.WIFI_STATE_UNKNOWN: - default: - return false; - } - } - - private static CharSequence getSummary(Context context) { - final WifiManager wifiManager = context.getSystemService(WifiManager.class); - - switch (wifiManager.getWifiState()) { - case WifiManager.WIFI_STATE_ENABLED: - final String ssid = WifiInfo.removeDoubleQuotes(wifiManager.getConnectionInfo() - .getSSID()); - if (TextUtils.equals(ssid, WifiSsid.NONE)) { - return context.getText(R.string.disconnected); - } - return ssid; - case WifiManager.WIFI_STATE_ENABLING: - return context.getText(R.string.disconnected); - case WifiManager.WIFI_STATE_DISABLED: - case WifiManager.WIFI_STATE_DISABLING: - return context.getText(R.string.switch_off_text); - case WifiManager.WIFI_STATE_UNKNOWN: - default: - return ""; - } - } - - private static PendingIntent getPrimaryAction(Context context) { - final Intent intent = getIntent(context); - return PendingIntent.getActivity(context, 0 /* requestCode */, - intent, 0 /* flags */); - } - - private static PendingIntent getBroadcastIntent(Context context) { - final Intent intent = new Intent(ACTION_WIFI_SLICE_CHANGED); - intent.setClass(context, SliceBroadcastReceiver.class); - return PendingIntent.getBroadcast(context, 0 /* requestCode */, intent, - PendingIntent.FLAG_CANCEL_CURRENT); - } -} diff --git a/src/com/android/settings/wifi/WifiStatusTest.java b/src/com/android/settings/wifi/WifiStatusTest.java index 4c3873cbb1..ca7f5f7fec 100644 --- a/src/com/android/settings/wifi/WifiStatusTest.java +++ b/src/com/android/settings/wifi/WifiStatusTest.java @@ -64,7 +64,8 @@ public class WifiStatusTest extends Activity { private TextView mIPAddr; private TextView mMACAddr; private TextView mNetworkId; - private TextView mLinkSpeed; + private TextView mTxLinkSpeed; + private TextView mRxLinkSpeed; private TextView mScanList; @@ -142,7 +143,8 @@ public class WifiStatusTest extends Activity { mIPAddr = (TextView) findViewById(R.id.ipaddr); mMACAddr = (TextView) findViewById(R.id.macaddr); mNetworkId = (TextView) findViewById(R.id.networkid); - mLinkSpeed = (TextView) findViewById(R.id.link_speed); + mTxLinkSpeed = (TextView) findViewById(R.id.tx_link_speed); + mRxLinkSpeed = (TextView) findViewById(R.id.rx_link_speed); mScanList = (TextView) findViewById(R.id.scan_list); @@ -186,7 +188,8 @@ public class WifiStatusTest extends Activity { append((ipAddr >>>= 8) & 0xff); mIPAddr.setText(ipBuf); - mLinkSpeed.setText(String.valueOf(wifiInfo.getLinkSpeed())+" Mbps"); + mTxLinkSpeed.setText(String.valueOf(wifiInfo.getTxLinkSpeedMbps())+" Mbps"); + mRxLinkSpeed.setText(String.valueOf(wifiInfo.getRxLinkSpeedMbps())+" Mbps"); mMACAddr.setText(wifiInfo.getMacAddress()); mNetworkId.setText(String.valueOf(wifiInfo.getNetworkId())); mRSSI.setText(String.valueOf(wifiInfo.getRssi())); @@ -296,7 +299,8 @@ public class WifiStatusTest extends Activity { WifiInfo info = mWifiManager.getConnectionInfo(); String summary = AccessPoint.getSummary(this, info.getSSID(), networkInfo.getDetailedState(), - info.getNetworkId() == WifiConfiguration.INVALID_NETWORK_ID, null); + info.getNetworkId() == WifiConfiguration.INVALID_NETWORK_ID, + /* suggestionOrSpecifierPackageName */ null); mNetworkState.setText(summary); } } diff --git a/src/com/android/settings/wifi/WifiSummaryUpdater.java b/src/com/android/settings/wifi/WifiSummaryUpdater.java index d0b4f60694..86961132c4 100644 --- a/src/com/android/settings/wifi/WifiSummaryUpdater.java +++ b/src/com/android/settings/wifi/WifiSummaryUpdater.java @@ -24,9 +24,10 @@ import android.net.ConnectivityManager; import android.net.NetworkScoreManager; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; -import androidx.annotation.VisibleForTesting; import android.text.TextUtils; +import androidx.annotation.VisibleForTesting; + import com.android.settings.R; import com.android.settings.widget.SummaryUpdater; import com.android.settingslib.wifi.WifiStatusTracker; diff --git a/src/com/android/settings/wifi/WifiUtils.java b/src/com/android/settings/wifi/WifiUtils.java index 99b77d9e36..c4df567624 100644 --- a/src/com/android/settings/wifi/WifiUtils.java +++ b/src/com/android/settings/wifi/WifiUtils.java @@ -22,11 +22,14 @@ import android.content.ContentResolver; import android.content.Context; import android.content.pm.PackageManager; import android.net.NetworkCapabilities; +import android.net.wifi.ScanResult; import android.net.wifi.WifiConfiguration; import android.provider.Settings; import android.text.TextUtils; -import com.android.settingslib.wrapper.PackageManagerWrapper; +import com.android.settingslib.wifi.AccessPoint; + +import java.nio.charset.StandardCharsets; public class WifiUtils { @@ -40,7 +43,7 @@ public class WifiUtils { if (TextUtils.isEmpty(ssid)) { return false; } - return ssid.length() > SSID_ASCII_MAX_LENGTH; + return ssid.getBytes(StandardCharsets.UTF_8).length > SSID_ASCII_MAX_LENGTH; } public static boolean isSSIDTooShort(String ssid) { @@ -61,8 +64,9 @@ public class WifiUtils { /** * This method is a stripped and negated version of WifiConfigStore.canModifyNetwork. + * * @param context Context of caller - * @param config The WiFi config. + * @param config The WiFi config. * @return true if Settings cannot modify the config due to lockDown. */ public static boolean isNetworkLockedDown(Context context, WifiConfiguration config) { @@ -72,7 +76,7 @@ public class WifiUtils { final DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE); - final PackageManagerWrapper pm = new PackageManagerWrapper(context.getPackageManager()); + final PackageManager pm = context.getPackageManager(); // Check if device has DPM capability. If it has and dpm is still null, then we // treat this case with suspicion and bail out. @@ -109,4 +113,174 @@ public class WifiUtils { return (capabilities != null && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL)); } + + /** + * Provides a simple way to generate a new {@link WifiConfiguration} obj from + * {@link ScanResult} or {@link AccessPoint}. Either {@code accessPoint} or {@code scanResult + * } input should be not null for retrieving information, otherwise will throw + * IllegalArgumentException. + * This method prefers to take {@link AccessPoint} input in priority. Therefore this method + * will take {@link AccessPoint} input as preferred data extraction source when you input + * both {@link AccessPoint} and {@link ScanResult}, and ignore {@link ScanResult} input. + * + * Duplicated and simplified method from {@link WifiConfigController#getConfig()}. + * TODO(b/120827021): Should be removed if the there is have a common one in shared place (e.g. + * SettingsLib). + * + * @param accessPoint Input data for retrieving WifiConfiguration. + * @param scanResult Input data for retrieving WifiConfiguration. + * @return WifiConfiguration obj based on input. + */ + public static WifiConfiguration getWifiConfig(AccessPoint accessPoint, ScanResult scanResult, + String password) { + if (accessPoint == null && scanResult == null) { + throw new IllegalArgumentException( + "At least one of AccessPoint and ScanResult input is required."); + } + + final WifiConfiguration config = new WifiConfiguration(); + final int security; + + if (accessPoint == null) { + config.SSID = AccessPoint.convertToQuotedString(scanResult.SSID); + security = getAccessPointSecurity(scanResult); + } else { + if (!accessPoint.isSaved()) { + config.SSID = AccessPoint.convertToQuotedString( + accessPoint.getSsidStr()); + } else { + config.networkId = accessPoint.getConfig().networkId; + config.hiddenSSID = accessPoint.getConfig().hiddenSSID; + } + security = accessPoint.getSecurity(); + } + + switch (security) { + case AccessPoint.SECURITY_NONE: + config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE); + break; + + case AccessPoint.SECURITY_WEP: + config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE); + config.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN); + config.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.SHARED); + if (!TextUtils.isEmpty(password)) { + int length = password.length(); + // WEP-40, WEP-104, and 256-bit WEP (WEP-232?) + if ((length == 10 || length == 26 || length == 58) + && password.matches("[0-9A-Fa-f]*")) { + config.wepKeys[0] = password; + } else { + config.wepKeys[0] = '"' + password + '"'; + } + } + break; + + case AccessPoint.SECURITY_PSK: + config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK); + if (!TextUtils.isEmpty(password)) { + if (password.matches("[0-9A-Fa-f]{64}")) { + config.preSharedKey = password; + } else { + config.preSharedKey = '"' + password + '"'; + } + } + break; + + case AccessPoint.SECURITY_EAP: + case AccessPoint.SECURITY_EAP_SUITE_B: + config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_EAP); + config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.IEEE8021X); + if (security == AccessPoint.SECURITY_EAP_SUITE_B) { + config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.SUITE_B_192); + config.requirePMF = true; + config.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.GCMP_256); + config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.GCMP_256); + config.allowedGroupManagementCiphers.set(WifiConfiguration.GroupMgmtCipher + .BIP_GMAC_256); + // allowedSuiteBCiphers will be set according to certificate type + } + + if (!TextUtils.isEmpty(password)) { + config.enterpriseConfig.setPassword(password); + } + break; + case AccessPoint.SECURITY_SAE: + config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.SAE); + config.requirePMF = true; + if (!TextUtils.isEmpty(password)) { + config.preSharedKey = '"' + password + '"'; + } + break; + + case AccessPoint.SECURITY_OWE: + config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.OWE); + config.requirePMF = true; + break; + + default: + break; + } + + return config; + } + + + /** + * Gets security value from ScanResult. + * + * Duplicated method from {@link AccessPoint#getSecurity(ScanResult)}. + * TODO(b/120827021): Should be removed if the there is have a common one in shared place (e.g. + * SettingsLib). + * + * @param result ScanResult + * @return Related security value based on {@link AccessPoint}. + */ + public static int getAccessPointSecurity(ScanResult result) { + if (result.capabilities.contains("WEP")) { + return AccessPoint.SECURITY_WEP; + } else if (result.capabilities.contains("SAE")) { + return AccessPoint.SECURITY_SAE; + } else if (result.capabilities.contains("PSK")) { + return AccessPoint.SECURITY_PSK; + } else if (result.capabilities.contains("EAP_SUITE_B_192")) { + return AccessPoint.SECURITY_EAP_SUITE_B; + } else if (result.capabilities.contains("EAP")) { + return AccessPoint.SECURITY_EAP; + } else if (result.capabilities.contains("OWE")) { + return AccessPoint.SECURITY_OWE; + } + + return AccessPoint.SECURITY_NONE; + } + + + public static final int CONNECT_TYPE_OTHERS = 0; + public static final int CONNECT_TYPE_OPEN_NETWORK = 1; + public static final int CONNECT_TYPE_SAVED_NETWORK = 2; + public static final int CONNECT_TYPE_OSU_PROVISION = 3; + + /** + * Gets the connecting type of {@link AccessPoint}. + */ + public static int getConnectingType(AccessPoint accessPoint) { + final WifiConfiguration config = accessPoint.getConfig(); + if (accessPoint.isOsuProvider()) { + return CONNECT_TYPE_OSU_PROVISION; + } else if ((accessPoint.getSecurity() == AccessPoint.SECURITY_NONE) || + (accessPoint.getSecurity() == AccessPoint.SECURITY_OWE) || + (accessPoint.getSecurity() == AccessPoint.SECURITY_OWE_TRANSITION)) { + return CONNECT_TYPE_OPEN_NETWORK; + } else if (accessPoint.isSaved() && config != null + && config.getNetworkSelectionStatus() != null + && config.getNetworkSelectionStatus().getHasEverConnected()) { + return CONNECT_TYPE_SAVED_NETWORK; + } else if (accessPoint.isPasspoint()) { + // Access point provided by an installed Passpoint provider, connect using + // the associated config. + return CONNECT_TYPE_SAVED_NETWORK; + } else { + return CONNECT_TYPE_OTHERS; + } + } } diff --git a/src/com/android/settings/wifi/WifiWakeupPreferenceController.java b/src/com/android/settings/wifi/WifiWakeupPreferenceController.java index 0c088257ab..11a58af4c4 100644 --- a/src/com/android/settings/wifi/WifiWakeupPreferenceController.java +++ b/src/com/android/settings/wifi/WifiWakeupPreferenceController.java @@ -18,29 +18,37 @@ package com.android.settings.wifi; import static com.android.settings.wifi.ConfigureWifiSettings.WIFI_WAKEUP_REQUEST_CODE; -import android.app.Fragment; import android.app.Service; +import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.location.LocationManager; import android.provider.Settings; +import android.text.TextUtils; + import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; +import androidx.fragment.app.Fragment; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.text.TextUtils; +import androidx.preference.SwitchPreference; import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.utils.AnnotationSpan; import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnPause; +import com.android.settingslib.core.lifecycle.events.OnResume; /** * {@link PreferenceControllerMixin} that controls whether the Wi-Fi Wakeup feature should be * enabled. */ -public class WifiWakeupPreferenceController extends AbstractPreferenceController { +public class WifiWakeupPreferenceController extends AbstractPreferenceController implements + LifecycleObserver, OnPause, OnResume { private static final String TAG = "WifiWakeupPrefController"; private static final String KEY_ENABLE_WIFI_WAKEUP = "enable_wifi_wakeup"; @@ -51,23 +59,33 @@ public class WifiWakeupPreferenceController extends AbstractPreferenceController SwitchPreference mPreference; @VisibleForTesting LocationManager mLocationManager; + private final BroadcastReceiver mLocationReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + updateState(mPreference); + } + }; + private final IntentFilter mLocationFilter = + new IntentFilter(LocationManager.MODE_CHANGED_ACTION); - public WifiWakeupPreferenceController(Context context, DashboardFragment fragment) { + public WifiWakeupPreferenceController(Context context, DashboardFragment fragment, + Lifecycle lifecycle) { super(context); mFragment = fragment; mLocationManager = (LocationManager) context.getSystemService(Service.LOCATION_SERVICE); + lifecycle.addObserver(this); } @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - mPreference = (SwitchPreference) screen.findPreference(KEY_ENABLE_WIFI_WAKEUP); + mPreference = screen.findPreference(KEY_ENABLE_WIFI_WAKEUP); updateState(mPreference); } @Override public boolean isAvailable() { - return true; + return true; } @Override @@ -79,15 +97,22 @@ public class WifiWakeupPreferenceController extends AbstractPreferenceController return false; } - if (!mLocationManager.isLocationEnabled()) { - final Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS); - mFragment.startActivity(intent); - } else if (getWifiWakeupEnabled()) { + // TODO(b/132391311): WifiWakeupPreferenceController is essentially reimplementing + // TogglePreferenceController. Refactor it into TogglePreferenceController. + + // Toggle wifi-wakeup setting between 1/0 based on its current state, and some other checks. + if (isWifiWakeupAvailable()) { setWifiWakeupEnabled(false); - } else if (!getWifiScanningEnabled()) { - showScanningDialog(); } else { - setWifiWakeupEnabled(true); + if (!mLocationManager.isLocationEnabled()) { + final Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS); + mFragment.startActivityForResult(intent, WIFI_WAKEUP_REQUEST_CODE); + return true; + } else if (!getWifiScanningEnabled()) { + showScanningDialog(); + } else { + setWifiWakeupEnabled(true); + } } updateState(mPreference); @@ -106,9 +131,7 @@ public class WifiWakeupPreferenceController extends AbstractPreferenceController } final SwitchPreference enableWifiWakeup = (SwitchPreference) preference; - enableWifiWakeup.setChecked(getWifiWakeupEnabled() - && getWifiScanningEnabled() - && mLocationManager.isLocationEnabled()); + enableWifiWakeup.setChecked(isWifiWakeupAvailable()); if (!mLocationManager.isLocationEnabled()) { preference.setSummary(getNoLocationSummary()); } else { @@ -116,7 +139,8 @@ public class WifiWakeupPreferenceController extends AbstractPreferenceController } } - @VisibleForTesting CharSequence getNoLocationSummary() { + @VisibleForTesting + CharSequence getNoLocationSummary() { AnnotationSpan.LinkInfo linkInfo = new AnnotationSpan.LinkInfo("link", null); CharSequence locationText = mContext.getText(R.string.wifi_wakeup_summary_no_location); return AnnotationSpan.linkify(locationText, linkInfo); @@ -126,7 +150,7 @@ public class WifiWakeupPreferenceController extends AbstractPreferenceController if (requestCode != WIFI_WAKEUP_REQUEST_CODE) { return; } - if (mLocationManager.isLocationEnabled()) { + if (mLocationManager.isLocationEnabled() && getWifiScanningEnabled()) { setWifiWakeupEnabled(true); } updateState(mPreference); @@ -149,8 +173,27 @@ public class WifiWakeupPreferenceController extends AbstractPreferenceController Settings.Global.WIFI_WAKEUP_ENABLED, 0) == 1; } + /** + * Wifi wakeup is available only when both location and Wi-Fi scanning are enabled. + */ + private boolean isWifiWakeupAvailable() { + return getWifiWakeupEnabled() + && getWifiScanningEnabled() + && mLocationManager.isLocationEnabled(); + } + private void setWifiWakeupEnabled(boolean enabled) { Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.WIFI_WAKEUP_ENABLED, enabled ? 1 : 0); } + + @Override + public void onResume() { + mContext.registerReceiver(mLocationReceiver, mLocationFilter); + } + + @Override + public void onPause() { + mContext.unregisterReceiver(mLocationReceiver); + } } diff --git a/src/com/android/settings/wifi/WriteWifiConfigToNfcDialog.java b/src/com/android/settings/wifi/WriteWifiConfigToNfcDialog.java deleted file mode 100644 index 3fe22c215f..0000000000 --- a/src/com/android/settings/wifi/WriteWifiConfigToNfcDialog.java +++ /dev/null @@ -1,287 +0,0 @@ -/* - * 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.wifi; - -import android.app.Activity; -import android.app.AlertDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.net.wifi.WifiManager; -import android.nfc.FormatException; -import android.nfc.NdefMessage; -import android.nfc.NdefRecord; -import android.nfc.NfcAdapter; -import android.nfc.Tag; -import android.nfc.tech.Ndef; -import android.os.Bundle; -import android.os.PowerManager; -import android.text.Editable; -import android.text.InputType; -import android.text.TextWatcher; -import android.util.Log; -import android.view.View; -import android.view.inputmethod.InputMethodManager; -import android.widget.Button; -import android.widget.CheckBox; -import android.widget.CompoundButton; -import android.widget.ProgressBar; -import android.widget.TextView; - -import com.android.settings.R; -import com.android.settingslib.wifi.AccessPoint; - -import java.io.IOException; - -class WriteWifiConfigToNfcDialog extends AlertDialog - implements TextWatcher, View.OnClickListener, CompoundButton.OnCheckedChangeListener { - - private static final String NFC_TOKEN_MIME_TYPE = "application/vnd.wfa.wsc"; - - private static final String TAG = WriteWifiConfigToNfcDialog.class.getName().toString(); - private static final String PASSWORD_FORMAT = "102700%s%s"; - private static final int HEX_RADIX = 16; - private static final char[] hexArray = "0123456789ABCDEF".toCharArray(); - private static final String SECURITY = "security"; - - private final PowerManager.WakeLock mWakeLock; - - private View mView; - private Button mSubmitButton; - private Button mCancelButton; - private TextView mPasswordView; - private TextView mLabelView; - private CheckBox mPasswordCheckBox; - private ProgressBar mProgressBar; - private WifiManager mWifiManager; - private String mWpsNfcConfigurationToken; - private Context mContext; - private int mSecurity; - - WriteWifiConfigToNfcDialog(Context context, int security) { - super(context); - - mContext = context; - mWakeLock = ((PowerManager) context.getSystemService(Context.POWER_SERVICE)) - .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "WriteWifiConfigToNfcDialog:wakeLock"); - mSecurity = security; - mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); - } - - WriteWifiConfigToNfcDialog(Context context, Bundle savedState) { - super(context); - - mContext = context; - mWakeLock = ((PowerManager) context.getSystemService(Context.POWER_SERVICE)) - .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "WriteWifiConfigToNfcDialog:wakeLock"); - mSecurity = savedState.getInt(SECURITY); - mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); - } - - @Override - public void onCreate(Bundle savedInstanceState) { - mView = getLayoutInflater().inflate(R.layout.write_wifi_config_to_nfc, null); - - setView(mView); - setInverseBackgroundForced(true); - setTitle(R.string.setup_wifi_nfc_tag); - setCancelable(true); - setButton(DialogInterface.BUTTON_NEUTRAL, - mContext.getResources().getString(R.string.write_tag), (OnClickListener) null); - setButton(DialogInterface.BUTTON_NEGATIVE, - mContext.getResources().getString(com.android.internal.R.string.cancel), - (OnClickListener) null); - - mPasswordView = mView.findViewById(R.id.password); - mLabelView = mView.findViewById(R.id.password_label); - mPasswordView.addTextChangedListener(this); - mPasswordCheckBox = mView.findViewById(R.id.show_password); - mPasswordCheckBox.setOnCheckedChangeListener(this); - mProgressBar = mView.findViewById(R.id.progress_bar); - - super.onCreate(savedInstanceState); - - mSubmitButton = getButton(DialogInterface.BUTTON_NEUTRAL); - mSubmitButton.setOnClickListener(this); - mSubmitButton.setEnabled(false); - - mCancelButton = getButton(DialogInterface.BUTTON_NEGATIVE); - } - - @Override - public void onClick(View v) { - mWakeLock.acquire(); - - String password = mPasswordView.getText().toString(); - String wpsNfcConfigurationToken = mWifiManager.getCurrentNetworkWpsNfcConfigurationToken(); - String passwordHex = byteArrayToHexString(password.getBytes()); - - String passwordLength = password.length() >= HEX_RADIX - ? Integer.toString(password.length(), HEX_RADIX) - : "0" + Character.forDigit(password.length(), HEX_RADIX); - - passwordHex = String.format(PASSWORD_FORMAT, passwordLength, passwordHex).toLowerCase(); - - if (wpsNfcConfigurationToken != null && wpsNfcConfigurationToken.contains(passwordHex)) { - mWpsNfcConfigurationToken = wpsNfcConfigurationToken; - - Activity activity = getOwnerActivity(); - NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(activity); - - nfcAdapter.enableReaderMode(activity, new NfcAdapter.ReaderCallback() { - @Override - public void onTagDiscovered(Tag tag) { - handleWriteNfcEvent(tag); - } - }, NfcAdapter.FLAG_READER_NFC_A | - NfcAdapter.FLAG_READER_NFC_B | - NfcAdapter.FLAG_READER_NFC_BARCODE | - NfcAdapter.FLAG_READER_NFC_F | - NfcAdapter.FLAG_READER_NFC_V, - null); - - mPasswordView.setVisibility(View.GONE); - mPasswordCheckBox.setVisibility(View.GONE); - mSubmitButton.setVisibility(View.GONE); - InputMethodManager imm = (InputMethodManager) - getOwnerActivity().getSystemService(Context.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(mPasswordView.getWindowToken(), 0); - - mLabelView.setText(R.string.status_awaiting_tap); - - mView.findViewById(R.id.password_layout).setTextAlignment(View.TEXT_ALIGNMENT_CENTER); - mProgressBar.setVisibility(View.VISIBLE); - } else { - mLabelView.setText(R.string.status_invalid_password); - } - } - - public void saveState(Bundle state) { - state.putInt(SECURITY, mSecurity); - } - - private void handleWriteNfcEvent(Tag tag) { - Ndef ndef = Ndef.get(tag); - - if (ndef != null) { - if (ndef.isWritable()) { - NdefRecord record = NdefRecord.createMime( - NFC_TOKEN_MIME_TYPE, - hexStringToByteArray(mWpsNfcConfigurationToken)); - try { - ndef.connect(); - ndef.writeNdefMessage(new NdefMessage(record)); - getOwnerActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - mProgressBar.setVisibility(View.GONE); - } - }); - setViewText(mLabelView, R.string.status_write_success); - setViewText(mCancelButton, com.android.internal.R.string.done_label); - } catch (IOException e) { - setViewText(mLabelView, R.string.status_failed_to_write); - Log.e(TAG, "Unable to write Wi-Fi config to NFC tag.", e); - return; - } catch (FormatException e) { - setViewText(mLabelView, R.string.status_failed_to_write); - Log.e(TAG, "Unable to write Wi-Fi config to NFC tag.", e); - return; - } - } else { - setViewText(mLabelView, R.string.status_tag_not_writable); - Log.e(TAG, "Tag is not writable"); - } - } else { - setViewText(mLabelView, R.string.status_tag_not_writable); - Log.e(TAG, "Tag does not support NDEF"); - } - } - - @Override - public void dismiss() { - if (mWakeLock.isHeld()) { - mWakeLock.release(); - } - - super.dismiss(); - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - enableSubmitIfAppropriate(); - } - - private void enableSubmitIfAppropriate() { - - if (mPasswordView != null) { - if (mSecurity == AccessPoint.SECURITY_WEP) { - mSubmitButton.setEnabled(mPasswordView.length() > 0); - } else if (mSecurity == AccessPoint.SECURITY_PSK) { - mSubmitButton.setEnabled(mPasswordView.length() >= 8); - } - } else { - mSubmitButton.setEnabled(false); - } - - } - - private void setViewText(final TextView view, final int resid) { - getOwnerActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - view.setText(resid); - } - }); - } - - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - mPasswordView.setInputType( - InputType.TYPE_CLASS_TEXT | - (isChecked - ? InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD - : InputType.TYPE_TEXT_VARIATION_PASSWORD)); - } - - private static byte[] hexStringToByteArray(String s) { - int len = s.length(); - byte[] data = new byte[len / 2]; - - for (int i = 0; i < len; i += 2) { - data[i / 2] = (byte) ((Character.digit(s.charAt(i), HEX_RADIX) << 4) - + Character.digit(s.charAt(i + 1), HEX_RADIX)); - } - - return data; - } - - private static String byteArrayToHexString(byte[] bytes) { - char[] hexChars = new char[bytes.length * 2]; - for ( int j = 0; j < bytes.length; j++ ) { - int v = bytes[j] & 0xFF; - hexChars[j * 2] = hexArray[v >>> 4]; - hexChars[j * 2 + 1] = hexArray[v & 0x0F]; - } - return new String(hexChars); - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) {} - - @Override - public void afterTextChanged(Editable s) {} -} diff --git a/src/com/android/settings/wifi/calling/ListWithEntrySummaryPreference.java b/src/com/android/settings/wifi/calling/ListWithEntrySummaryPreference.java index c24ff2bc0b..a44fcbeba7 100644 --- a/src/com/android/settings/wifi/calling/ListWithEntrySummaryPreference.java +++ b/src/com/android/settings/wifi/calling/ListWithEntrySummaryPreference.java @@ -16,7 +16,6 @@ package com.android.settings.wifi.calling; -import android.app.AlertDialog.Builder; import android.content.Context; import android.content.DialogInterface; import android.content.res.TypedArray; @@ -31,7 +30,7 @@ import android.widget.ArrayAdapter; import android.widget.ListAdapter; import android.widget.RadioButton; import android.widget.TextView; - +import androidx.appcompat.app.AlertDialog.Builder; import com.android.settings.CustomListPreference; import com.android.settings.R; diff --git a/src/com/android/settings/wifi/calling/WifiCallingSettings.java b/src/com/android/settings/wifi/calling/WifiCallingSettings.java index 5d11c55593..8a342c8a68 100644 --- a/src/com/android/settings/wifi/calling/WifiCallingSettings.java +++ b/src/com/android/settings/wifi/calling/WifiCallingSettings.java @@ -16,10 +16,10 @@ package com.android.settings.wifi.calling; -import android.app.Fragment; -import android.app.FragmentManager; +import android.app.settings.SettingsEnums; +import android.content.Intent; import android.os.Bundle; -import androidx.legacy.app.FragmentPagerAdapter; +import android.provider.Settings; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.util.Log; @@ -27,11 +27,16 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import androidx.annotation.VisibleForTesting; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentPagerAdapter; + import com.android.ims.ImsManager; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.util.CollectionUtils; import com.android.settings.R; import com.android.settings.core.InstrumentedFragment; +import com.android.settings.network.SubscriptionUtil; import com.android.settings.search.actionbar.SearchMenuController; import com.android.settings.support.actionbar.HelpMenuController; import com.android.settings.support.actionbar.HelpResourceProvider; @@ -73,7 +78,7 @@ public class WifiCallingSettings extends InstrumentedFragment implements HelpRes @Override public int getMetricsCategory() { - return MetricsEvent.WIFI_CALLING; + return SettingsEnums.WIFI_CALLING; } @Override @@ -87,10 +92,30 @@ public class WifiCallingSettings extends InstrumentedFragment implements HelpRes mPagerAdapter = new WifiCallingViewPagerAdapter(getChildFragmentManager(), mViewPager); mViewPager.setAdapter(mPagerAdapter); mViewPager.addOnPageChangeListener(new InternalViewPagerListener()); - + maybeSetViewForSubId(); return view; } + private void maybeSetViewForSubId() { + if (mSil == null) { + return; + } + Intent intent = getActivity().getIntent(); + if (intent == null) { + return; + } + int subId = intent.getIntExtra(Settings.EXTRA_SUB_ID, + SubscriptionManager.INVALID_SUBSCRIPTION_ID); + if (SubscriptionManager.isValidSubscriptionId(subId)) { + for (SubscriptionInfo subInfo : mSil) { + if (subId == subInfo.getSubscriptionId()) { + mViewPager.setCurrentItem(mSil.indexOf(subInfo)); + break; + } + } + } + } + @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); @@ -121,7 +146,8 @@ public class WifiCallingSettings extends InstrumentedFragment implements HelpRes return R.string.help_uri_wifi_calling; } - private final class WifiCallingViewPagerAdapter extends FragmentPagerAdapter { + @VisibleForTesting + final class WifiCallingViewPagerAdapter extends FragmentPagerAdapter { private final RtlCompatibleViewPager mViewPager; public WifiCallingViewPagerAdapter(FragmentManager fragmentManager, @@ -167,17 +193,29 @@ public class WifiCallingSettings extends InstrumentedFragment implements HelpRes } } + @VisibleForTesting + boolean isWfcEnabledByPlatform(SubscriptionInfo info) { + ImsManager imsManager = ImsManager.getInstance(getActivity(), info.getSimSlotIndex()); + return imsManager.isWfcEnabledByPlatform(); + } + + @VisibleForTesting + boolean isWfcProvisionedOnDevice(SubscriptionInfo info) { + ImsManager imsManager = ImsManager.getInstance(getActivity(), info.getSimSlotIndex()); + return imsManager.isWfcProvisionedOnDevice(); + } + private void updateSubList() { - mSil = SubscriptionManager.from(getActivity()).getActiveSubscriptionInfoList(); + mSil = SubscriptionUtil.getActiveSubscriptions( + getContext().getSystemService(SubscriptionManager.class)); // Only config Wfc if it's enabled by platform. if (mSil == null) { return; } for (int i = 0; i < mSil.size(); ) { - ImsManager imsManager = ImsManager.getInstance(getActivity(), - mSil.get(i).getSimSlotIndex()); - if (!imsManager.isWfcEnabledByPlatform()) { + final SubscriptionInfo info = mSil.get(i); + if (!isWfcEnabledByPlatform(info) || !isWfcProvisionedOnDevice(info)) { mSil.remove(i); } else { i++; diff --git a/src/com/android/settings/wifi/calling/WifiCallingSettingsForSub.java b/src/com/android/settings/wifi/calling/WifiCallingSettingsForSub.java index 898f2bea0c..252193d3f9 100644 --- a/src/com/android/settings/wifi/calling/WifiCallingSettingsForSub.java +++ b/src/com/android/settings/wifi/calling/WifiCallingSettingsForSub.java @@ -17,7 +17,7 @@ package com.android.settings.wifi.calling; import android.app.Activity; -import android.app.AlertDialog; +import android.app.settings.SettingsEnums; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; @@ -26,10 +26,6 @@ import android.content.IntentFilter; import android.content.res.Resources; import android.os.Bundle; import android.os.PersistableBundle; -import androidx.preference.ListPreference; -import androidx.preference.Preference; -import androidx.preference.Preference.OnPreferenceClickListener; -import androidx.preference.PreferenceScreen; import android.telephony.CarrierConfigManager; import android.telephony.PhoneStateListener; import android.telephony.SubscriptionManager; @@ -43,11 +39,15 @@ import android.view.ViewGroup; import android.widget.Switch; import android.widget.TextView; +import androidx.appcompat.app.AlertDialog; +import androidx.preference.Preference; +import androidx.preference.Preference.OnPreferenceClickListener; +import androidx.preference.PreferenceScreen; + import com.android.ims.ImsConfig; import com.android.ims.ImsException; import com.android.ims.ImsManager; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.telephony.Phone; import com.android.settings.R; import com.android.settings.SettingsActivity; @@ -98,6 +98,7 @@ public class WifiCallingSettingsForSub extends SettingsPreferenceFragment private int mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; private ImsManager mImsManager; + private TelephonyManager mTelephonyManager; private final PhoneStateListener mPhoneStateListener = new PhoneStateListener() { /* @@ -145,20 +146,17 @@ public class WifiCallingSettingsForSub extends SettingsPreferenceFragment } }; + /* + * Launch carrier emergency address managemnent activity + */ private final OnPreferenceClickListener mUpdateAddressListener = - new OnPreferenceClickListener() { - /* - * Launch carrier emergency address managemnent activity - */ - @Override - public boolean onPreferenceClick(Preference preference) { - Intent carrierAppIntent = getCarrierActivityIntent(); - if (carrierAppIntent != null) { - carrierAppIntent.putExtra(EXTRA_LAUNCH_CARRIER_APP, LAUCH_APP_UPDATE); - startActivity(carrierAppIntent); - } - return true; + preference -> { + Intent carrierAppIntent = getCarrierActivityIntent(); + if (carrierAppIntent != null) { + carrierAppIntent.putExtra(EXTRA_LAUNCH_CARRIER_APP, LAUCH_APP_UPDATE); + startActivity(carrierAppIntent); } + return true; }; private final ProvisioningManager.Callback mProvisioningCallback = @@ -178,8 +176,6 @@ public class WifiCallingSettingsForSub extends SettingsPreferenceFragment public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - final SettingsActivity activity = (SettingsActivity) getActivity(); - mEmptyView = getView().findViewById(android.R.id.empty); setEmptyView(mEmptyView); final Resources res = getResourcesForSubId(); @@ -235,7 +231,7 @@ public class WifiCallingSettingsForSub extends SettingsPreferenceFragment @Override public int getMetricsCategory() { - return MetricsEvent.WIFI_CALLING_FOR_SUB; + return SettingsEnums.WIFI_CALLING_FOR_SUB; } @Override @@ -266,14 +262,16 @@ public class WifiCallingSettingsForSub extends SettingsPreferenceFragment mImsManager = getImsManager(); - mButtonWfcMode = (ListWithEntrySummaryPreference) findPreference(BUTTON_WFC_MODE); + mTelephonyManager = ((TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE)) + .createForSubscriptionId(mSubId); + + mButtonWfcMode = findPreference(BUTTON_WFC_MODE); mButtonWfcMode.setOnPreferenceChangeListener(this); - mButtonWfcRoamingMode = (ListWithEntrySummaryPreference) findPreference( - BUTTON_WFC_ROAMING_MODE); + mButtonWfcRoamingMode = findPreference(BUTTON_WFC_ROAMING_MODE); mButtonWfcRoamingMode.setOnPreferenceChangeListener(this); - mUpdateAddress = (Preference) findPreference(PREFERENCE_EMERGENCY_ADDRESS); + mUpdateAddress = findPreference(PREFERENCE_EMERGENCY_ADDRESS); mUpdateAddress.setOnPreferenceClickListener(mUpdateAddressListener); mIntentFilter = new IntentFilter(); @@ -362,9 +360,7 @@ public class WifiCallingSettingsForSub extends SettingsPreferenceFragment updateBody(); if (mImsManager.isWfcEnabledByPlatform()) { - TelephonyManager tm = - (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); - tm.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); + mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); mSwitchBar.addOnSwitchChangeListener(this); @@ -433,7 +429,7 @@ public class WifiCallingSettingsForSub extends SettingsPreferenceFragment new SubSettingLauncher(context) .setDestination(WifiCallingDisclaimerFragment.class.getName()) .setArguments(args) - .setTitle(R.string.wifi_calling_settings_title) + .setTitleRes(R.string.wifi_calling_settings_title) .setSourceMetricsCategory(getMetricsCategory()) .setResultListener(this, REQUEST_CHECK_WFC_DISCLAIMER) .launch(); @@ -602,6 +598,6 @@ public class WifiCallingSettingsForSub extends SettingsPreferenceFragment @VisibleForTesting Resources getResourcesForSubId() { - return SubscriptionManager.getResourcesForSubId(getActivity(), mSubId, false); + return SubscriptionManager.getResourcesForSubId(getContext(), mSubId, false); } } diff --git a/src/com/android/settings/wifi/calling/WifiCallingSliceHelper.java b/src/com/android/settings/wifi/calling/WifiCallingSliceHelper.java index a80e9d03c9..70eef2cc57 100644 --- a/src/com/android/settings/wifi/calling/WifiCallingSliceHelper.java +++ b/src/com/android/settings/wifi/calling/WifiCallingSliceHelper.java @@ -18,31 +18,33 @@ package com.android.settings.wifi.calling; import static android.app.slice.Slice.EXTRA_TOGGLE_STATE; +import static com.android.settings.slices.CustomSliceRegistry.WIFI_CALLING_PREFERENCE_URI; +import static com.android.settings.slices.CustomSliceRegistry.WIFI_CALLING_URI; + import android.app.PendingIntent; import android.content.ComponentName; -import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.PersistableBundle; import android.provider.Settings; -import androidx.core.graphics.drawable.IconCompat; import android.telephony.CarrierConfigManager; import android.telephony.SubscriptionManager; -import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.Log; +import androidx.annotation.VisibleForTesting; +import androidx.core.graphics.drawable.IconCompat; import androidx.slice.Slice; import androidx.slice.builders.ListBuilder; +import androidx.slice.builders.ListBuilder.RowBuilder; import androidx.slice.builders.SliceAction; +import com.android.ims.ImsConfig; import com.android.ims.ImsManager; -import com.android.internal.annotations.VisibleForTesting; import com.android.settings.R; -import com.android.settings.slices.SettingsSliceProvider; +import com.android.settings.Utils; import com.android.settings.slices.SliceBroadcastReceiver; -import com.android.settings.slices.SliceBuilderUtils; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; @@ -66,12 +68,34 @@ public class WifiCallingSliceHelper { public static final String PATH_WIFI_CALLING = "wifi_calling"; /** + * Settings slice path to wifi calling preference setting. + */ + public static final String PATH_WIFI_CALLING_PREFERENCE = + "wifi_calling_preference"; + + /** * Action passed for changes to wifi calling slice (toggle). */ public static final String ACTION_WIFI_CALLING_CHANGED = "com.android.settings.wifi.calling.action.WIFI_CALLING_CHANGED"; /** + * Action passed when user selects wifi only preference. + */ + public static final String ACTION_WIFI_CALLING_PREFERENCE_WIFI_ONLY = + "com.android.settings.slice.action.WIFI_CALLING_PREFERENCE_WIFI_ONLY"; + /** + * Action passed when user selects wifi preferred preference. + */ + public static final String ACTION_WIFI_CALLING_PREFERENCE_WIFI_PREFERRED = + "com.android.settings.slice.action.WIFI_CALLING_PREFERENCE_WIFI_PREFERRED"; + /** + * Action passed when user selects cellular preferred preference. + */ + public static final String ACTION_WIFI_CALLING_PREFERENCE_CELLULAR_PREFERRED = + "com.android.settings.slice.action.WIFI_CALLING_PREFERENCE_CELLULAR_PREFERRED"; + + /** * Action for Wifi calling Settings activity which * allows setting configuration for Wifi calling * related settings @@ -80,20 +104,10 @@ public class WifiCallingSliceHelper { "android.settings.WIFI_CALLING_SETTINGS"; /** - * Full {@link Uri} for the Wifi Calling Slice. - */ - public static final Uri WIFI_CALLING_URI = new Uri.Builder() - .scheme(ContentResolver.SCHEME_CONTENT) - .authority(SettingsSliceProvider.SLICE_AUTHORITY) - .appendPath(PATH_WIFI_CALLING) - .build(); - - /** * Timeout for querying wifi calling setting from ims manager. */ private static final int TIMEOUT_MILLIS = 2000; - protected SubscriptionManager mSubscriptionManager; private final Context mContext; @VisibleForTesting @@ -115,14 +129,10 @@ public class WifiCallingSliceHelper { */ public Slice createWifiCallingSlice(Uri sliceUri) { final int subId = getDefaultVoiceSubId(); - final String carrierName = getSimCarrierName(); if (subId <= SubscriptionManager.INVALID_SUBSCRIPTION_ID) { Log.d(TAG, "Invalid subscription Id"); - return getNonActionableWifiCallingSlice( - mContext.getString(R.string.wifi_calling_settings_title), - mContext.getString(R.string.wifi_calling_not_supported, carrierName), - sliceUri, getSettingsIntent(mContext)); + return null; } final ImsManager imsManager = getImsManager(subId); @@ -130,10 +140,7 @@ public class WifiCallingSliceHelper { if (!imsManager.isWfcEnabledByPlatform() || !imsManager.isWfcProvisionedOnDevice()) { Log.d(TAG, "Wifi calling is either not provisioned or not enabled by Platform"); - return getNonActionableWifiCallingSlice( - mContext.getString(R.string.wifi_calling_settings_title), - mContext.getString(R.string.wifi_calling_not_supported, carrierName), - sliceUri, getSettingsIntent(mContext)); + return null; } try { @@ -148,18 +155,15 @@ public class WifiCallingSliceHelper { // Activation needed for the next action of the user // Give instructions to go to settings app return getNonActionableWifiCallingSlice( - mContext.getString(R.string.wifi_calling_settings_title), - mContext.getString( + mContext.getText(R.string.wifi_calling_settings_title), + mContext.getText( R.string.wifi_calling_settings_activation_instructions), sliceUri, getActivityIntent(ACTION_WIFI_CALLING_SETTINGS_ACTIVITY)); } - return getWifiCallingSlice(sliceUri, mContext, isWifiCallingEnabled); + return getWifiCallingSlice(sliceUri, isWifiCallingEnabled); } catch (InterruptedException | TimeoutException | ExecutionException e) { Log.e(TAG, "Unable to read the current WiFi calling status", e); - return getNonActionableWifiCallingSlice( - mContext.getString(R.string.wifi_calling_settings_title), - mContext.getString(R.string.wifi_calling_turn_on), - sliceUri, getActivityIntent(ACTION_WIFI_CALLING_SETTINGS_ACTIVITY)); + return null; } } @@ -174,41 +178,184 @@ public class WifiCallingSliceHelper { final ExecutorService executor = Executors.newSingleThreadExecutor(); executor.execute(isWifiOnTask); - Boolean isWifiEnabledByUser = false; - isWifiEnabledByUser = isWifiOnTask.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); - - return isWifiEnabledByUser && imsManager.isNonTtyOrTtyOnVolteEnabled(); + return isWifiOnTask.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS) + && imsManager.isNonTtyOrTtyOnVolteEnabled(); } /** * Builds a toggle slice where the intent takes you to the wifi calling page and the toggle * enables/disables wifi calling. */ - private Slice getWifiCallingSlice(Uri sliceUri, Context mContext, - boolean isWifiCallingEnabled) { - + private Slice getWifiCallingSlice(Uri sliceUri, boolean isWifiCallingEnabled) { final IconCompat icon = IconCompat.createWithResource(mContext, R.drawable.wifi_signal); - final String title = mContext.getString(R.string.wifi_calling_settings_title); + return new ListBuilder(mContext, sliceUri, ListBuilder.INFINITY) - .setColor(R.color.material_blue_500) - .addRow(b -> b - .setTitle(title) + .setAccentColor(Utils.getColorAccentDefaultColor(mContext)) + .addRow(new RowBuilder() + .setTitle(mContext.getText(R.string.wifi_calling_settings_title)) .addEndItem( - new SliceAction( + SliceAction.createToggle( getBroadcastIntent(ACTION_WIFI_CALLING_CHANGED), null /* actionTitle */, isWifiCallingEnabled)) - .setPrimaryAction(new SliceAction( + .setPrimaryAction(SliceAction.createDeeplink( getActivityIntent(ACTION_WIFI_CALLING_SETTINGS_ACTIVITY), icon, - title))) + ListBuilder.ICON_IMAGE, + mContext.getText(R.string.wifi_calling_settings_title)))) .build(); } + /** + * Returns Slice object for wifi calling preference. + * + * If wifi calling is not turned on, this method will return a slice to turn on wifi calling. + * + * If wifi calling preference is not user editable, this method will return a slice to display + * appropriate message. + * + * If wifi calling preference can be changed, this method will return a slice with 3 or 4 rows: + * Header Row: current preference settings + * Row 1: wifi only option with ACTION_WIFI_CALLING_PREFERENCE_WIFI_ONLY, if wifi only option + * is editable + * Row 2: wifi preferred option with ACTION_WIFI_CALLING_PREFERENCE_WIFI_PREFERRED + * Row 3: cellular preferred option with ACTION_WIFI_CALLING_PREFERENCE_CELLULAR_PREFERRED + */ + public Slice createWifiCallingPreferenceSlice(Uri sliceUri) { + final int subId = getDefaultVoiceSubId(); + + if (subId <= SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + Log.d(TAG, "Invalid Subscription Id"); + return null; + } + + final boolean isWifiCallingPrefEditable = isCarrierConfigManagerKeyEnabled( + CarrierConfigManager.KEY_EDITABLE_WFC_MODE_BOOL, subId, false); + final boolean isWifiOnlySupported = isCarrierConfigManagerKeyEnabled( + CarrierConfigManager.KEY_CARRIER_WFC_SUPPORTS_WIFI_ONLY_BOOL, subId, true); + final ImsManager imsManager = getImsManager(subId); + + if (!imsManager.isWfcEnabledByPlatform() + || !imsManager.isWfcProvisionedOnDevice()) { + Log.d(TAG, "Wifi calling is either not provisioned or not enabled by platform"); + return null; + } + + if (!isWifiCallingPrefEditable) { + Log.d(TAG, "Wifi calling preference is not editable"); + return null; + } + + boolean isWifiCallingEnabled = false; + int wfcMode = -1; + try { + isWifiCallingEnabled = isWifiCallingEnabled(imsManager); + wfcMode = getWfcMode(imsManager); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + Log.e(TAG, "Unable to get wifi calling preferred mode", e); + return null; + } + if (!isWifiCallingEnabled) { + // wifi calling is not enabled. Ask user to enable wifi calling + return getNonActionableWifiCallingSlice( + mContext.getText(R.string.wifi_calling_mode_title), + mContext.getText(R.string.wifi_calling_turn_on), + sliceUri, getActivityIntent(ACTION_WIFI_CALLING_SETTINGS_ACTIVITY)); + } + // Return the slice to change wifi calling preference + return getWifiCallingPreferenceSlice( + isWifiOnlySupported, wfcMode, sliceUri); + } + + /** + * Returns actionable wifi calling preference slice. + * + * @param isWifiOnlySupported adds row for wifi only if this is true + * @param currentWfcPref current Preference {@link ImsConfig} + * @param sliceUri sliceUri + * @return Slice for actionable wifi calling preference settings + */ + private Slice getWifiCallingPreferenceSlice(boolean isWifiOnlySupported, + int currentWfcPref, + Uri sliceUri) { + final IconCompat icon = IconCompat.createWithResource(mContext, R.drawable.wifi_signal); + // Top row shows information on current preference state + ListBuilder listBuilder = new ListBuilder(mContext, sliceUri, ListBuilder.INFINITY) + .setAccentColor(Utils.getColorAccentDefaultColor(mContext)); + listBuilder.setHeader(new ListBuilder.HeaderBuilder() + .setTitle(mContext.getText(R.string.wifi_calling_mode_title)) + .setSubtitle(getWifiCallingPreferenceSummary(currentWfcPref)) + .setPrimaryAction(SliceAction.createDeeplink( + getActivityIntent(ACTION_WIFI_CALLING_SETTINGS_ACTIVITY), + icon, + ListBuilder.ICON_IMAGE, + mContext.getText(R.string.wifi_calling_mode_title)))); + + if (isWifiOnlySupported) { + listBuilder.addRow(wifiPreferenceRowBuilder(listBuilder, + com.android.internal.R.string.wfc_mode_wifi_only_summary, + ACTION_WIFI_CALLING_PREFERENCE_WIFI_ONLY, + currentWfcPref == ImsConfig.WfcModeFeatureValueConstants.WIFI_ONLY)); + } + + listBuilder.addRow(wifiPreferenceRowBuilder(listBuilder, + com.android.internal.R.string.wfc_mode_wifi_preferred_summary, + ACTION_WIFI_CALLING_PREFERENCE_WIFI_PREFERRED, + currentWfcPref == ImsConfig.WfcModeFeatureValueConstants.WIFI_PREFERRED)); + + listBuilder.addRow(wifiPreferenceRowBuilder(listBuilder, + com.android.internal.R.string.wfc_mode_cellular_preferred_summary, + ACTION_WIFI_CALLING_PREFERENCE_CELLULAR_PREFERRED, + currentWfcPref == ImsConfig.WfcModeFeatureValueConstants.CELLULAR_PREFERRED)); + + return listBuilder.build(); + } + + /** + * Returns RowBuilder for a new row containing specific wifi calling preference. + * + * @param listBuilder ListBuilder that will be the parent for this RowBuilder + * @param preferenceTitleResId resource Id for the preference row title + * @param action action to be added for the row + * @return RowBuilder for the row + */ + private RowBuilder wifiPreferenceRowBuilder(ListBuilder listBuilder, + int preferenceTitleResId, String action, boolean checked) { + final IconCompat icon = + IconCompat.createWithResource(mContext, R.drawable.radio_button_check); + return new RowBuilder() + .setTitle(mContext.getText(preferenceTitleResId)) + .setTitleItem(SliceAction.createToggle(getBroadcastIntent(action), + icon, mContext.getText(preferenceTitleResId), checked)); + } + + + /** + * Returns the String describing wifi calling preference mentioned in wfcMode + * + * @param wfcMode ImsConfig constant for the preference {@link ImsConfig} + * @return summary/name of the wifi calling preference + */ + private CharSequence getWifiCallingPreferenceSummary(int wfcMode) { + switch (wfcMode) { + case ImsConfig.WfcModeFeatureValueConstants.WIFI_ONLY: + return mContext.getText( + com.android.internal.R.string.wfc_mode_wifi_only_summary); + case ImsConfig.WfcModeFeatureValueConstants.WIFI_PREFERRED: + return mContext.getText( + com.android.internal.R.string.wfc_mode_wifi_preferred_summary); + case ImsConfig.WfcModeFeatureValueConstants.CELLULAR_PREFERRED: + return mContext.getText( + com.android.internal.R.string.wfc_mode_cellular_preferred_summary); + default: + return null; + } + } + protected ImsManager getImsManager(int subId) { return ImsManager.getInstance(mContext, SubscriptionManager.getPhoneId(subId)); } - private Integer getWfcMode(ImsManager imsManager) + private int getWfcMode(ImsManager imsManager) throws InterruptedException, ExecutionException, TimeoutException { FutureTask<Integer> wfcModeTask = new FutureTask<>(new Callable<Integer>() { @Override @@ -233,7 +380,7 @@ public class WifiCallingSliceHelper { if (subId > SubscriptionManager.INVALID_SUBSCRIPTION_ID) { final ImsManager imsManager = getImsManager(subId); if (imsManager.isWfcEnabledByPlatform() - || imsManager.isWfcProvisionedOnDevice()) { + && imsManager.isWfcProvisionedOnDevice()) { final boolean currentValue = imsManager.isWfcEnabledByUser() && imsManager.isNonTtyOrTtyOnVolteEnabled(); final boolean newValue = intent.getBooleanExtra(EXTRA_TOGGLE_STATE, @@ -251,29 +398,82 @@ public class WifiCallingSliceHelper { } // notify change in slice in any case to get re-queried. This would result in displaying // appropriate message with the updated setting. - final Uri uri = SliceBuilderUtils.getUri(PATH_WIFI_CALLING, false /*isPlatformSlice*/); - mContext.getContentResolver().notifyChange(uri, null); + mContext.getContentResolver().notifyChange(WIFI_CALLING_URI, null); + } + + /** + * Handles wifi calling preference Setting change from wifi calling preference Slice and posts + * notification for the change. Should be called when intent action is one of the below + * ACTION_WIFI_CALLING_PREFERENCE_WIFI_ONLY + * ACTION_WIFI_CALLING_PREFERENCE_WIFI_PREFERRED + * ACTION_WIFI_CALLING_PREFERENCE_CELLULAR_PREFERRED + * + * @param intent intent + */ + public void handleWifiCallingPreferenceChanged(Intent intent) { + final int subId = getDefaultVoiceSubId(); + final int errorValue = -1; + + if (subId > SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + final boolean isWifiCallingPrefEditable = isCarrierConfigManagerKeyEnabled( + CarrierConfigManager.KEY_EDITABLE_WFC_MODE_BOOL, subId, false); + final boolean isWifiOnlySupported = isCarrierConfigManagerKeyEnabled( + CarrierConfigManager.KEY_CARRIER_WFC_SUPPORTS_WIFI_ONLY_BOOL, subId, true); + + ImsManager imsManager = getImsManager(subId); + if (isWifiCallingPrefEditable + && imsManager.isWfcEnabledByPlatform() + && imsManager.isWfcProvisionedOnDevice() + && imsManager.isWfcEnabledByUser() + && imsManager.isNonTtyOrTtyOnVolteEnabled()) { + // Change the preference only when wifi calling is enabled + // And when wifi calling preference is editable for the current carrier + final int currentValue = imsManager.getWfcMode(false); + int newValue = errorValue; + switch (intent.getAction()) { + case ACTION_WIFI_CALLING_PREFERENCE_WIFI_ONLY: + if (isWifiOnlySupported) { + // change to wifi_only when wifi_only is enabled. + newValue = ImsConfig.WfcModeFeatureValueConstants.WIFI_ONLY; + } + break; + case ACTION_WIFI_CALLING_PREFERENCE_WIFI_PREFERRED: + newValue = ImsConfig.WfcModeFeatureValueConstants.WIFI_PREFERRED; + break; + case ACTION_WIFI_CALLING_PREFERENCE_CELLULAR_PREFERRED: + newValue = ImsConfig.WfcModeFeatureValueConstants.CELLULAR_PREFERRED; + break; + } + if (newValue != errorValue && newValue != currentValue) { + // Update the setting only when there is a valid update + imsManager.setWfcMode(newValue, false); + } + } + } + + // notify change in slice in any case to get re-queried. This would result in displaying + // appropriate message. + mContext.getContentResolver().notifyChange(WIFI_CALLING_PREFERENCE_URI, null); } /** * Returns Slice with the title and subtitle provided as arguments with wifi signal Icon. * - * @param title Title of the slice + * @param title Title of the slice * @param subtitle Subtitle of the slice * @param sliceUri slice uri * @return Slice with title and subtitle */ - // TODO(b/79548264) asses different scenarios and return null instead of non-actionable slice - private Slice getNonActionableWifiCallingSlice(String title, String subtitle, Uri sliceUri, - PendingIntent primaryActionIntent) { + private Slice getNonActionableWifiCallingSlice(CharSequence title, CharSequence subtitle, + Uri sliceUri, PendingIntent primaryActionIntent) { final IconCompat icon = IconCompat.createWithResource(mContext, R.drawable.wifi_signal); return new ListBuilder(mContext, sliceUri, ListBuilder.INFINITY) - .setColor(R.color.material_blue_500) - .addRow(b -> b + .setAccentColor(Utils.getColorAccentDefaultColor(mContext)) + .addRow(new RowBuilder() .setTitle(title) .setSubtitle(subtitle) - .setPrimaryAction(new SliceAction( - primaryActionIntent, icon, + .setPrimaryAction(SliceAction.createDeeplink( + primaryActionIntent, icon, ListBuilder.SMALL_IMAGE, title))) .build(); } @@ -281,8 +481,8 @@ public class WifiCallingSliceHelper { /** * Returns {@code true} when the key is enabled for the carrier, and {@code false} otherwise. */ - private boolean isCarrierConfigManagerKeyEnabled(Context mContext, String key, - int subId, boolean defaultValue) { + protected boolean isCarrierConfigManagerKeyEnabled(String key, int subId, + boolean defaultValue) { final CarrierConfigManager configManager = getCarrierConfigManager(mContext); boolean ret = false; if (configManager != null) { @@ -302,9 +502,6 @@ public class WifiCallingSliceHelper { * Returns the current default voice subId obtained from SubscriptionManager */ protected int getDefaultVoiceSubId() { - if (mSubscriptionManager == null) { - mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class); - } return SubscriptionManager.getDefaultVoiceSubscriptionId(); } @@ -350,6 +547,7 @@ public class WifiCallingSliceHelper { private PendingIntent getBroadcastIntent(String action) { final Intent intent = new Intent(action); intent.setClass(mContext, SliceBroadcastReceiver.class); + intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); return PendingIntent.getBroadcast(mContext, 0 /* requestCode */, intent, PendingIntent.FLAG_CANCEL_CURRENT); } @@ -362,17 +560,4 @@ public class WifiCallingSliceHelper { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); return PendingIntent.getActivity(mContext, 0 /* requestCode */, intent, 0 /* flags */); } - - /** - * Returns carrier id name of the current Subscription - */ - private String getSimCarrierName() { - final TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class); - final CharSequence carrierName = telephonyManager.getSimCarrierIdName(); - if (carrierName == null) { - return mContext.getString(R.string.carrier); - } - return carrierName.toString(); - } - } diff --git a/src/com/android/settings/wifi/details/AddDevicePreferenceController.java b/src/com/android/settings/wifi/details/AddDevicePreferenceController.java new file mode 100644 index 0000000000..f2b3d75339 --- /dev/null +++ b/src/com/android/settings/wifi/details/AddDevicePreferenceController.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2019 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.wifi.details; + +import android.content.Context; +import android.content.Intent; +import android.net.wifi.WifiManager; +import android.util.Log; + +import androidx.preference.Preference; + +import com.android.settings.core.BasePreferenceController; +import com.android.settings.wifi.dpp.WifiDppUtils; + +import com.android.settingslib.wifi.AccessPoint; + +/** + * {@link BasePreferenceController} that launches Wi-Fi Easy Connect configurator flow + */ +public class AddDevicePreferenceController extends BasePreferenceController { + + private static final String TAG = "AddDevicePreferenceController"; + + private static final String KEY_ADD_DEVICE = "add_device_to_network"; + + private AccessPoint mAccessPoint; + private WifiManager mWifiManager; + + public AddDevicePreferenceController(Context context) { + super(context, KEY_ADD_DEVICE); + + mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + } + + public AddDevicePreferenceController init(AccessPoint accessPoint) { + mAccessPoint = accessPoint; + + return this; + } + + @Override + public int getAvailabilityStatus() { + if (WifiDppUtils.isSupportConfiguratorQrCodeScanner(mContext, mAccessPoint)) { + return AVAILABLE; + } else { + return CONDITIONALLY_UNAVAILABLE; + } + } + + @Override + public boolean handlePreferenceTreeClick(Preference preference) { + if (KEY_ADD_DEVICE.equals(preference.getKey())) { + WifiDppUtils.showLockScreen(mContext, () -> launchWifiDppConfiguratorQrCodeScanner()); + return true; /* click is handled */ + } + + return false; /* click is not handled */ + } + + private void launchWifiDppConfiguratorQrCodeScanner() { + final Intent intent = WifiDppUtils.getConfiguratorQrCodeScannerIntentOrNull(mContext, + mWifiManager, mAccessPoint); + + if (intent == null) { + Log.e(TAG, "Launch Wi-Fi QR code scanner with a wrong Wi-Fi network!"); + } else { + mContext.startActivity(intent); + } + } +} diff --git a/src/com/android/settings/wifi/details/WifiDetailPreferenceController.java b/src/com/android/settings/wifi/details/WifiDetailPreferenceController.java index dece92787d..5e49b2e749 100644 --- a/src/com/android/settings/wifi/details/WifiDetailPreferenceController.java +++ b/src/com/android/settings/wifi/details/WifiDetailPreferenceController.java @@ -21,12 +21,16 @@ import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED; import static android.net.NetworkCapabilities.TRANSPORT_WIFI; import android.app.Activity; -import android.app.Fragment; +import android.app.AlertDialog; +import android.app.settings.SettingsEnums; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; +import android.graphics.drawable.VectorDrawable; import android.net.ConnectivityManager; import android.net.ConnectivityManager.NetworkCallback; import android.net.LinkAddress; @@ -40,40 +44,49 @@ import android.net.RouteInfo; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; +import android.os.CountDownTimer; import android.os.Handler; -import androidx.core.text.BidiFormatter; -import androidx.preference.Preference; -import androidx.preference.PreferenceCategory; -import androidx.preference.PreferenceScreen; import android.text.TextUtils; +import android.util.FeatureFlagUtils; import android.util.Log; import android.widget.ImageView; import android.widget.Toast; -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.logging.nano.MetricsProto; +import androidx.annotation.VisibleForTesting; +import androidx.core.text.BidiFormatter; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceScreen; + import com.android.settings.R; import com.android.settings.Utils; -import com.android.settings.applications.LayoutPreference; +import com.android.settings.core.FeatureFlags; import com.android.settings.core.PreferenceControllerMixin; -import com.android.settings.widget.ActionButtonPreference; +import com.android.settings.datausage.WifiDataUsageSummaryPreferenceController; +import com.android.settings.development.featureflags.FeatureFlagPersistent; import com.android.settings.widget.EntityHeaderController; -import com.android.settings.wifi.WifiDetailPreference; import com.android.settings.wifi.WifiDialog; import com.android.settings.wifi.WifiDialog.WifiDialogListener; import com.android.settings.wifi.WifiUtils; +import com.android.settings.wifi.dpp.WifiDppUtils; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnPause; import com.android.settingslib.core.lifecycle.events.OnResume; +import com.android.settingslib.widget.ActionButtonsPreference; +import com.android.settingslib.widget.LayoutPreference; import com.android.settingslib.wifi.AccessPoint; +import com.android.settingslib.wifi.WifiTracker; +import com.android.settingslib.wifi.WifiTrackerFactory; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.net.UnknownHostException; +import java.time.Duration; import java.util.StringJoiner; import java.util.stream.Collectors; @@ -91,16 +104,22 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController @VisibleForTesting static final String KEY_HEADER = "connection_header"; @VisibleForTesting + static final String KEY_DATA_USAGE_HEADER = "status_header"; + @VisibleForTesting static final String KEY_BUTTONS_PREF = "buttons"; @VisibleForTesting static final String KEY_SIGNAL_STRENGTH_PREF = "signal_strength"; @VisibleForTesting - static final String KEY_LINK_SPEED = "link_speed"; + static final String KEY_TX_LINK_SPEED = "tx_link_speed"; + @VisibleForTesting + static final String KEY_RX_LINK_SPEED = "rx_link_speed"; @VisibleForTesting static final String KEY_FREQUENCY_PREF = "frequency"; @VisibleForTesting static final String KEY_SECURITY_PREF = "security"; @VisibleForTesting + static final String KEY_SSID_PREF = "ssid"; + @VisibleForTesting static final String KEY_MAC_ADDRESS_PREF = "mac_address"; @VisibleForTesting static final String KEY_IP_ADDRESS_PREF = "ip_address"; @@ -115,9 +134,23 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController @VisibleForTesting static final String KEY_IPV6_ADDRESSES_PREF = "ipv6_addresses"; + private static final int STATE_NONE = 1; + private static final int STATE_ENABLE_WIFI = 2; + private static final int STATE_ENABLE_WIFI_FAILED = 3; + private static final int STATE_CONNECTING = 4; + private static final int STATE_CONNECTED = 5; + private static final int STATE_FAILED = 6; + private static final int STATE_NOT_IN_RANGE = 7; + private static final int STATE_DISCONNECTED = 8; + private static final long TIMEOUT = Duration.ofSeconds(10).toMillis(); + + // Be static to avoid too much object not be reset. + @VisibleForTesting + static CountDownTimer mTimer; + private AccessPoint mAccessPoint; private final ConnectivityManager mConnectivityManager; - private final Fragment mFragment; + private final PreferenceFragmentCompat mFragment; private final Handler mHandler; private LinkProperties mLinkProperties; private Network mNetwork; @@ -128,22 +161,33 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController private WifiConfiguration mWifiConfig; private WifiInfo mWifiInfo; private final WifiManager mWifiManager; + private final WifiTracker mWifiTracker; private final MetricsFeatureProvider mMetricsFeatureProvider; + private boolean mIsOutOfRange; + private boolean mIsEphemeral; + private boolean mConnected; + private int mConnectingState; + private WifiManager.ActionListener mConnectListener; // UI elements - in order of appearance - private ActionButtonPreference mButtonsPref; + private ActionButtonsPreference mButtonsPref; private EntityHeaderController mEntityHeaderController; - private WifiDetailPreference mSignalStrengthPref; - private WifiDetailPreference mLinkSpeedPref; - private WifiDetailPreference mFrequencyPref; - private WifiDetailPreference mSecurityPref; - private WifiDetailPreference mMacAddressPref; - private WifiDetailPreference mIpAddressPref; - private WifiDetailPreference mGatewayPref; - private WifiDetailPreference mSubnetPref; - private WifiDetailPreference mDnsPref; + private Preference mSignalStrengthPref; + private Preference mTxLinkSpeedPref; + private Preference mRxLinkSpeedPref; + private Preference mFrequencyPref; + private Preference mSecurityPref; + private Preference mSsidPref; + private Preference mMacAddressPref; + private Preference mIpAddressPref; + private Preference mGatewayPref; + private Preference mSubnetPref; + private Preference mDnsPref; private PreferenceCategory mIpv6Category; private Preference mIpv6AddressPref; + private Lifecycle mLifecycle; + Preference mDataUsageSummaryPref; + WifiDataUsageSummaryPreferenceController mSummaryHeaderController; private final IconInjector mIconInjector; private final IntentFilter mFilter; @@ -164,7 +208,7 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController // fall through case WifiManager.NETWORK_STATE_CHANGED_ACTION: case WifiManager.RSSI_CHANGED_ACTION: - updateInfo(); + refreshPage(); break; } } @@ -179,7 +223,7 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController public void onLinkPropertiesChanged(Network network, LinkProperties lp) { if (network.equals(mNetwork) && !lp.equals(mLinkProperties)) { mLinkProperties = lp; - updateIpLayerInfo(); + refreshIpLayerInfo(); } } @@ -201,26 +245,57 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController if (hasCapabilityChanged(nc, NET_CAPABILITY_VALIDATED) || hasCapabilityChanged(nc, NET_CAPABILITY_CAPTIVE_PORTAL) || hasCapabilityChanged(nc, NET_CAPABILITY_PARTIAL_CONNECTIVITY)) { - refreshNetworkState(); + mAccessPoint.update(mWifiConfig, mWifiInfo, mNetworkInfo); + refreshEntityHeader(); } mNetworkCapabilities = nc; - updateIpLayerInfo(); + refreshButtons(); + refreshIpLayerInfo(); } } @Override public void onLost(Network network) { - if (network.equals(mNetwork)) { + // Ephemeral network not a saved network, leave detail page once disconnected + if (mIsEphemeral && network.equals(mNetwork)) { exitActivity(); } } }; + @VisibleForTesting + final WifiTracker.WifiListener mWifiListener = new WifiTracker.WifiListener() { + /** Called when the state of Wifi has changed. */ + public void onWifiStateChanged(int state) { + Log.d(TAG, "onWifiStateChanged(" + state + ")"); + if (mConnectingState == STATE_ENABLE_WIFI && state == WifiManager.WIFI_STATE_ENABLED) { + updateConnectingState(STATE_CONNECTING); + } else if (mConnectingState != STATE_NONE && state == WifiManager.WIFI_STATE_DISABLED) { + // update as disconnected once Wi-Fi disabled since may not received + // onConnectedChanged for this case. + updateConnectingState(STATE_DISCONNECTED); + } + } + + /** Called when the connection state of wifi has changed. */ + public void onConnectedChanged() { + refreshPage(); + } + + /** + * Called to indicate the list of AccessPoints has been updated and + * {@link WifiTracker#getAccessPoints()} should be called to get the updated list. + */ + public void onAccessPointsChanged() { + refreshPage(); + } + }; + public static WifiDetailPreferenceController newInstance( AccessPoint accessPoint, ConnectivityManager connectivityManager, Context context, - Fragment fragment, + PreferenceFragmentCompat fragment, Handler handler, Lifecycle lifecycle, WifiManager wifiManager, @@ -235,7 +310,7 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController AccessPoint accessPoint, ConnectivityManager connectivityManager, Context context, - Fragment fragment, + PreferenceFragmentCompat fragment, Handler handler, Lifecycle lifecycle, WifiManager wifiManager, @@ -258,7 +333,31 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController mFilter.addAction(WifiManager.RSSI_CHANGED_ACTION); mFilter.addAction(WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION); + mLifecycle = lifecycle; lifecycle.addObserver(this); + + mWifiTracker = WifiTrackerFactory.create( + mFragment.getActivity(), + mWifiListener, + mLifecycle, + true /*includeSaved*/, + true /*includeScans*/); + mConnected = mAccessPoint.isActive(); + // When lost the network connection, WifiInfo/NetworkInfo will be clear. So causes we + // could not check if the AccessPoint is ephemeral. Need to cache it in first. + mIsEphemeral = mAccessPoint.isEphemeral(); + mConnectingState = STATE_NONE; + mConnectListener = new WifiManager.ActionListener() { + @Override + public void onSuccess() { + // Do nothing + } + + @Override + public void onFailure(int reason) { + updateConnectingState(STATE_FAILED); + } + }; } @Override @@ -278,55 +377,89 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController setupEntityHeader(screen); - mButtonsPref = ((ActionButtonPreference) screen.findPreference(KEY_BUTTONS_PREF)) + mButtonsPref = ((ActionButtonsPreference) screen.findPreference(KEY_BUTTONS_PREF)) .setButton1Text(R.string.forget) - .setButton1Positive(false) + .setButton1Icon(R.drawable.ic_settings_delete) .setButton1OnClickListener(view -> forgetNetwork()) .setButton2Text(R.string.wifi_sign_in_button_text) - .setButton2Positive(true) - .setButton2OnClickListener(view -> signIntoNetwork()); - - mSignalStrengthPref = - (WifiDetailPreference) screen.findPreference(KEY_SIGNAL_STRENGTH_PREF); - mLinkSpeedPref = (WifiDetailPreference) screen.findPreference(KEY_LINK_SPEED); - mFrequencyPref = (WifiDetailPreference) screen.findPreference(KEY_FREQUENCY_PREF); - mSecurityPref = (WifiDetailPreference) screen.findPreference(KEY_SECURITY_PREF); - - mMacAddressPref = (WifiDetailPreference) screen.findPreference(KEY_MAC_ADDRESS_PREF); - mIpAddressPref = (WifiDetailPreference) screen.findPreference(KEY_IP_ADDRESS_PREF); - mGatewayPref = (WifiDetailPreference) screen.findPreference(KEY_GATEWAY_PREF); - mSubnetPref = (WifiDetailPreference) screen.findPreference(KEY_SUBNET_MASK_PREF); - mDnsPref = (WifiDetailPreference) screen.findPreference(KEY_DNS_PREF); - - mIpv6Category = (PreferenceCategory) screen.findPreference(KEY_IPV6_CATEGORY); + .setButton2Icon(R.drawable.ic_settings_sign_in) + .setButton2OnClickListener(view -> signIntoNetwork()) + .setButton3Text(R.string.wifi_connect) + .setButton3Icon(R.drawable.ic_settings_wireless) + .setButton3OnClickListener(view -> connectNetwork()) + .setButton3Enabled(true) + .setButton4Text(R.string.share) + .setButton4Icon(R.drawable.ic_qrcode_24dp) + .setButton4OnClickListener(view -> shareNetwork()); + + mSignalStrengthPref = screen.findPreference(KEY_SIGNAL_STRENGTH_PREF); + mTxLinkSpeedPref = screen.findPreference(KEY_TX_LINK_SPEED); + mRxLinkSpeedPref = screen.findPreference(KEY_RX_LINK_SPEED); + mFrequencyPref = screen.findPreference(KEY_FREQUENCY_PREF); + mSecurityPref = screen.findPreference(KEY_SECURITY_PREF); + + mSsidPref = screen.findPreference(KEY_SSID_PREF); + mMacAddressPref = screen.findPreference(KEY_MAC_ADDRESS_PREF); + mIpAddressPref = screen.findPreference(KEY_IP_ADDRESS_PREF); + mGatewayPref = screen.findPreference(KEY_GATEWAY_PREF); + mSubnetPref = screen.findPreference(KEY_SUBNET_MASK_PREF); + mDnsPref = screen.findPreference(KEY_DNS_PREF); + + mIpv6Category = screen.findPreference(KEY_IPV6_CATEGORY); mIpv6AddressPref = screen.findPreference(KEY_IPV6_ADDRESSES_PREF); - mSecurityPref.setDetailText(mAccessPoint.getSecurityString(false /* concise */)); + mSecurityPref.setSummary(mAccessPoint.getSecurityString(/* concise */ false)); } private void setupEntityHeader(PreferenceScreen screen) { - LayoutPreference headerPref = (LayoutPreference) screen.findPreference(KEY_HEADER); + LayoutPreference headerPref = screen.findPreference(KEY_HEADER); + + if (usingDataUsageHeader(mContext)) { + headerPref.setVisible(false); + mDataUsageSummaryPref = screen.findPreference(KEY_DATA_USAGE_HEADER); + mDataUsageSummaryPref.setVisible(true); + mSummaryHeaderController = + new WifiDataUsageSummaryPreferenceController(mFragment.getActivity(), + mLifecycle, (PreferenceFragmentCompat) mFragment, mAccessPoint.getSsid()); + return; + } + mEntityHeaderController = EntityHeaderController.newInstance( mFragment.getActivity(), mFragment, headerPref.findViewById(R.id.entity_header)); ImageView iconView = headerPref.findViewById(R.id.entity_header_icon); - iconView.setBackground( - mContext.getDrawable(R.drawable.ic_settings_widget_background)); + iconView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); - mEntityHeaderController.setLabel(mAccessPoint.getSsidStr()); + mEntityHeaderController.setLabel(mAccessPoint.getTitle()); + } + + private void refreshEntityHeader() { + if (usingDataUsageHeader(mContext)) { + mSummaryHeaderController.updateState(mDataUsageSummaryPref); + } else { + mEntityHeaderController + .setSummary( + mAccessPoint.getSettingsSummary(true /*convertSavedAsDisconnected*/)) + .setRecyclerView(mFragment.getListView(), mLifecycle) + .done(mFragment.getActivity(), true /* rebind */); + } + } + + private void updateNetworkInfo() { + mNetwork = mWifiManager.getCurrentNetwork(); + mLinkProperties = mConnectivityManager.getLinkProperties(mNetwork); + mNetworkCapabilities = mConnectivityManager.getNetworkCapabilities(mNetwork); } @Override public void onResume() { // Ensure mNetwork is set before any callbacks above are delivered, since our // NetworkCallback only looks at changes to mNetwork. - mNetwork = mWifiManager.getCurrentNetwork(); - mLinkProperties = mConnectivityManager.getLinkProperties(mNetwork); - mNetworkCapabilities = mConnectivityManager.getNetworkCapabilities(mNetwork); - updateInfo(); + updateNetworkInfo(); + refreshPage(); mContext.registerReceiver(mReceiver, mFilter); mConnectivityManager.registerNetworkCallback(mNetworkRequest, mNetworkCallback, mHandler); @@ -343,48 +476,80 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController mConnectivityManager.unregisterNetworkCallback(mNetworkCallback); } - private void updateInfo() { - // No need to fetch LinkProperties and NetworkCapabilities, they are updated by the - // callbacks. mNetwork doesn't change except in onResume. - mNetworkInfo = mConnectivityManager.getNetworkInfo(mNetwork); - mWifiInfo = mWifiManager.getConnectionInfo(); - if (mNetwork == null || mNetworkInfo == null || mWifiInfo == null) { - exitActivity(); + private void refreshPage() { + if(!updateAccessPoint()) { return; } - // Update whether the forget button should be displayed. - mButtonsPref.setButton1Visible(canForgetNetwork()); + Log.d(TAG, "Update UI!"); + + // refresh header + refreshEntityHeader(); - refreshNetworkState(); + // refresh Buttons + refreshButtons(); // Update Connection Header icon and Signal Strength Preference refreshRssiViews(); - + // Frequency Pref + refreshFrequency(); + // Transmit Link Speed Pref + refreshTxSpeed(); + // Receive Link Speed Pref + refreshRxSpeed(); + // IP related information + refreshIpLayerInfo(); + // SSID Pref + refreshSsid(); // MAC Address Pref - mMacAddressPref.setDetailText(mWifiInfo.getMacAddress()); + refreshMacAddress(); + } - // Link Speed Pref - int linkSpeedMbps = mWifiInfo.getLinkSpeed(); - mLinkSpeedPref.setVisible(linkSpeedMbps >= 0); - mLinkSpeedPref.setDetailText(mContext.getString( - R.string.link_speed, mWifiInfo.getLinkSpeed())); + @VisibleForTesting + boolean updateAccessPoint() { + boolean changed = false; + // remember mIsOutOfRange as old before updated + boolean oldState = mIsOutOfRange; + updateAccessPointFromScannedList(); + + if (mAccessPoint.isActive()) { + updateNetworkInfo(); + mNetworkInfo = mConnectivityManager.getNetworkInfo(mNetwork); + mWifiInfo = mWifiManager.getConnectionInfo(); + if (mNetwork == null || mNetworkInfo == null || mWifiInfo == null) { + // Once connected, can't get mNetwork immediately, return false and wait for + // next time to update UI. also reset {@code mIsOutOfRange} + mIsOutOfRange = oldState; + return false; + } + changed |= mAccessPoint.update(mWifiConfig, mWifiInfo, mNetworkInfo); + } - // Frequency Pref - final int frequency = mWifiInfo.getFrequency(); - String band = null; - if (frequency >= AccessPoint.LOWER_FREQ_24GHZ - && frequency < AccessPoint.HIGHER_FREQ_24GHZ) { - band = mContext.getResources().getString(R.string.wifi_band_24ghz); - } else if (frequency >= AccessPoint.LOWER_FREQ_5GHZ - && frequency < AccessPoint.HIGHER_FREQ_5GHZ) { - band = mContext.getResources().getString(R.string.wifi_band_5ghz); - } else { - Log.e(TAG, "Unexpected frequency " + frequency); + // signal level changed + changed |= mRssiSignalLevel != mAccessPoint.getLevel(); + // In/Out of range changed + changed |= oldState != mIsOutOfRange; + // connect state changed + if (mConnected != mAccessPoint.isActive()) { + mConnected = mAccessPoint.isActive(); + changed = true; + updateConnectingState(mAccessPoint.isActive() ? STATE_CONNECTED : STATE_DISCONNECTED); } - mFrequencyPref.setDetailText(band); - updateIpLayerInfo(); + return changed; + } + + private void updateAccessPointFromScannedList() { + mIsOutOfRange = true; + + for (AccessPoint ap : mWifiTracker.getAccessPoints()) { + if (mAccessPoint.matches(ap)) { + mAccessPoint = ap; + mWifiConfig = ap.getConfig(); + mIsOutOfRange = !mAccessPoint.isReachable(); + return; + } + } } private void exitActivity() { @@ -394,46 +559,191 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController mFragment.getActivity().finish(); } - private void refreshNetworkState() { - mAccessPoint.update(mWifiConfig, mWifiInfo, mNetworkInfo); - mEntityHeaderController.setSummary(mAccessPoint.getSettingsSummary()) - .done(mFragment.getActivity(), true /* rebind */); - } - private void refreshRssiViews() { int signalLevel = mAccessPoint.getLevel(); + // Disappears signal view if not in range. e.g. for saved networks. + if (mIsOutOfRange) { + mSignalStrengthPref.setVisible(false); + mRssiSignalLevel = -1; + return; + } + if (mRssiSignalLevel == signalLevel) { return; } mRssiSignalLevel = signalLevel; Drawable wifiIcon = mIconInjector.getIcon(mRssiSignalLevel); - wifiIcon.setTint(Utils.getColorAccent(mContext)); - mEntityHeaderController.setIcon(wifiIcon).done(mFragment.getActivity(), true /* rebind */); + if (mEntityHeaderController != null) { + mEntityHeaderController + .setIcon(redrawIconForHeader(wifiIcon)).done(mFragment.getActivity(), + true /* rebind */); + } Drawable wifiIconDark = wifiIcon.getConstantState().newDrawable().mutate(); - wifiIconDark.setTint(mContext.getResources().getColor( - R.color.wifi_details_icon_color, mContext.getTheme())); + wifiIconDark.setTintList(Utils.getColorAttr(mContext, android.R.attr.colorControlNormal)); mSignalStrengthPref.setIcon(wifiIconDark); - mSignalStrengthPref.setDetailText(mSignalStr[mRssiSignalLevel]); + mSignalStrengthPref.setSummary(mSignalStr[mRssiSignalLevel]); + mSignalStrengthPref.setVisible(true); } - private void updatePreference(WifiDetailPreference pref, String detailText) { + private Drawable redrawIconForHeader(Drawable original) { + final int iconSize = mContext.getResources().getDimensionPixelSize( + R.dimen.wifi_detail_page_header_image_size); + final int actualWidth = original.getMinimumWidth(); + final int actualHeight = original.getMinimumHeight(); + + if ((actualWidth == iconSize && actualHeight == iconSize) + || !VectorDrawable.class.isInstance(original)) { + return original; + } + + // clear tint list to make sure can set 87% black after enlarge + original.setTintList(null); + + // enlarge icon size + final Bitmap bitmap = Utils.createBitmap(original, + iconSize /*width*/, + iconSize /*height*/); + Drawable newIcon = new BitmapDrawable(null /*resource*/, bitmap); + + // config color for 87% black after enlarge + newIcon.setTintList(Utils.getColorAttr(mContext, android.R.attr.textColorPrimary)); + + return newIcon; + } + + private void refreshFrequency() { + if (mWifiInfo == null) { + mFrequencyPref.setVisible(false); + return; + } + + final int frequency = mWifiInfo.getFrequency(); + String band = null; + if (frequency >= AccessPoint.LOWER_FREQ_24GHZ + && frequency < AccessPoint.HIGHER_FREQ_24GHZ) { + band = mContext.getResources().getString(R.string.wifi_band_24ghz); + } else if (frequency >= AccessPoint.LOWER_FREQ_5GHZ + && frequency < AccessPoint.HIGHER_FREQ_5GHZ) { + band = mContext.getResources().getString(R.string.wifi_band_5ghz); + } else { + Log.e(TAG, "Unexpected frequency " + frequency); + // Connecting state is unstable, make it disappeared if unexpected + if (mConnectingState == STATE_CONNECTING) { + mFrequencyPref.setVisible(false); + } + return; + } + mFrequencyPref.setSummary(band); + mFrequencyPref.setVisible(true); + } + + private void refreshTxSpeed() { + if (mWifiInfo == null) { + mTxLinkSpeedPref.setVisible(false); + return; + } + + int txLinkSpeedMbps = mWifiInfo.getTxLinkSpeedMbps(); + mTxLinkSpeedPref.setVisible(txLinkSpeedMbps >= 0); + mTxLinkSpeedPref.setSummary(mContext.getString( + R.string.tx_link_speed, mWifiInfo.getTxLinkSpeedMbps())); + } + + private void refreshRxSpeed() { + if (mWifiInfo == null) { + mRxLinkSpeedPref.setVisible(false); + return; + } + + int rxLinkSpeedMbps = mWifiInfo.getRxLinkSpeedMbps(); + mRxLinkSpeedPref.setVisible(rxLinkSpeedMbps >= 0); + mRxLinkSpeedPref.setSummary(mContext.getString( + R.string.rx_link_speed, mWifiInfo.getRxLinkSpeedMbps())); + } + + private void refreshSsid() { + if (mAccessPoint.isPasspoint() || mAccessPoint.isOsuProvider()) { + mSsidPref.setVisible(true); + mSsidPref.setSummary(mAccessPoint.getSsidStr()); + } else { + mSsidPref.setVisible(false); + } + } + + private void refreshMacAddress() { + String macAddress = getMacAddress(); + if (macAddress == null) { + mMacAddressPref.setVisible(false); + return; + } + + mMacAddressPref.setVisible(true); + mMacAddressPref.setSummary(macAddress); + } + + private String getMacAddress() { + if (mWifiInfo != null) { + // get MAC address from connected network information + return mWifiInfo.getMacAddress(); + } + + // return randomized MAC address + if (mWifiConfig != null && + mWifiConfig.macRandomizationSetting == WifiConfiguration.RANDOMIZATION_PERSISTENT) { + return mWifiConfig.getRandomizedMacAddress().toString(); + } + + // return device MAC address + final String[] macAddresses = mWifiManager.getFactoryMacAddresses(); + if (macAddresses != null && macAddresses.length > 0) { + return macAddresses[0]; + } + + Log.e(TAG, "Can't get device MAC address!"); + return null; + } + + private void updatePreference(Preference pref, String detailText) { if (!TextUtils.isEmpty(detailText)) { - pref.setDetailText(detailText); + pref.setSummary(detailText); pref.setVisible(true); } else { pref.setVisible(false); } } - private void updateIpLayerInfo() { - mButtonsPref.setButton2Visible(canSignIntoNetwork()); - mButtonsPref.setVisible(canSignIntoNetwork() || canForgetNetwork()); + private void refreshButtons() { + // Ephemeral network won't be removed permanently, but be putted in blacklist. + mButtonsPref.setButton1Text( + mIsEphemeral ? R.string.wifi_disconnect_button_text : R.string.forget); + + boolean canForgetNetwork = canForgetNetwork(); + boolean canSignIntoNetwork = canSignIntoNetwork(); + boolean canConnectNetwork = canConnectNetwork(); + boolean canShareNetwork = canShareNetwork(); + + mButtonsPref.setButton1Visible(canForgetNetwork); + mButtonsPref.setButton2Visible(canSignIntoNetwork); + mButtonsPref.setButton3Visible(canConnectNetwork); + mButtonsPref.setButton4Visible(canShareNetwork); + mButtonsPref.setVisible(canForgetNetwork + || canSignIntoNetwork + || canConnectNetwork + || canShareNetwork); + } + + private boolean canConnectNetwork() { + // Display connect button for disconnected AP even not in the range. + return !mAccessPoint.isActive(); + } - if (mNetwork == null || mLinkProperties == null) { + private void refreshIpLayerInfo() { + // Hide IP layer info if not a connected network. + if (!mAccessPoint.isActive() || mNetwork == null || mLinkProperties == null) { mIpAddressPref.setVisible(false); mSubnetPref.setVisible(false); mGatewayPref.setVisible(false); @@ -488,7 +798,7 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController private static String ipv4PrefixLengthToSubnetMask(int prefixLength) { try { InetAddress all = InetAddress.getByAddress( - new byte[] {(byte) 255, (byte) 255, (byte) 255, (byte) 255}); + new byte[]{(byte) 255, (byte) 255, (byte) 255, (byte) 255}); return NetworkUtils.getNetworkPart(all, prefixLength).getHostAddress(); } catch (UnknownHostException e) { return null; @@ -499,7 +809,8 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController * Returns whether the network represented by this preference can be forgotten. */ private boolean canForgetNetwork() { - return (mWifiInfo != null && mWifiInfo.isEphemeral()) || canModifyNetwork(); + return (mWifiInfo != null && mWifiInfo.isEphemeral()) || canModifyNetwork() + || mAccessPoint.isPasspoint() || mAccessPoint.isPasspointConfig(); } /** @@ -513,7 +824,15 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController * Returns whether the user can sign into the network represented by this preference. */ private boolean canSignIntoNetwork() { - return WifiUtils.canSignIntoNetwork(mNetworkCapabilities); + return mAccessPoint.isActive() && WifiUtils.canSignIntoNetwork(mNetworkCapabilities); + } + + /** + * Returns whether the user can share the network represented by this preference with QR code. + */ + private boolean canShareNetwork() { + return mAccessPoint.getConfig() != null && + WifiDppUtils.isSupportConfiguratorQrCodeGenerator(mContext, mAccessPoint); } /** @@ -522,33 +841,86 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController private void forgetNetwork() { if (mWifiInfo != null && mWifiInfo.isEphemeral()) { mWifiManager.disableEphemeralNetwork(mWifiInfo.getSSID()); - } else if (mWifiConfig != null) { - if (mWifiConfig.isPasspoint()) { - mWifiManager.removePasspointConfiguration(mWifiConfig.FQDN); - } else { - mWifiManager.forget(mWifiConfig.networkId, null /* action listener */); + } else if (mAccessPoint.isPasspoint() || mAccessPoint.isPasspointConfig()) { + // Post a dialog to confirm if user really want to forget the passpoint network. + if (FeatureFlagPersistent.isEnabled(mContext, FeatureFlags.NETWORK_INTERNET_V2)) { + showConfirmForgetDialog(); + return; + } + + try { + mWifiManager.removePasspointConfiguration(mAccessPoint.getPasspointFqdn()); + } catch (RuntimeException e) { + Log.e(TAG, "Failed to remove Passpoint configuration for " + + mAccessPoint.getPasspointFqdn()); } + } else if (mWifiConfig != null) { + mWifiManager.forget(mWifiConfig.networkId, null /* action listener */); } + mMetricsFeatureProvider.action( - mFragment.getActivity(), MetricsProto.MetricsEvent.ACTION_WIFI_FORGET); + mFragment.getActivity(), SettingsEnums.ACTION_WIFI_FORGET); mFragment.getActivity().finish(); } + @VisibleForTesting + protected void showConfirmForgetDialog() { + final AlertDialog dialog = new AlertDialog.Builder(mContext) + .setPositiveButton(R.string.forget, ((dialog1, which) -> { + try { + mWifiManager.removePasspointConfiguration(mAccessPoint.getPasspointFqdn()); + } catch (RuntimeException e) { + Log.e(TAG, "Failed to remove Passpoint configuration for " + + mAccessPoint.getPasspointFqdn()); + } + mMetricsFeatureProvider.action( + mFragment.getActivity(), SettingsEnums.ACTION_WIFI_FORGET); + mFragment.getActivity().finish(); + })) + .setNegativeButton(R.string.cancel, null /* listener */) + .setTitle(R.string.wifi_forget_dialog_title) + .setMessage(R.string.forget_passpoint_dialog_message) + .create(); + dialog.show(); + } + + /** + * Show QR code to share the network represented by this preference. + */ + private void launchWifiDppConfiguratorActivity() { + final Intent intent = WifiDppUtils.getConfiguratorQrCodeGeneratorIntentOrNull(mContext, + mWifiManager, mAccessPoint); + + if (intent == null) { + Log.e(TAG, "Launch Wi-Fi DPP QR code generator with a wrong Wi-Fi network!"); + } else { + mMetricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, + SettingsEnums.ACTION_SETTINGS_SHARE_WIFI_QR_CODE, + SettingsEnums.SETTINGS_WIFI_DPP_CONFIGURATOR, + /* key */ null, + /* value */ Integer.MIN_VALUE); + + mContext.startActivity(intent); + } + } + + /** + * Share the wifi network with QR code. + */ + private void shareNetwork() { + WifiDppUtils.showLockScreen(mContext, () -> launchWifiDppConfiguratorActivity()); + } + /** * Sign in to the captive portal found on this wifi network associated with this preference. */ private void signIntoNetwork() { mMetricsFeatureProvider.action( - mFragment.getActivity(), MetricsProto.MetricsEvent.ACTION_WIFI_SIGNIN); + mFragment.getActivity(), SettingsEnums.ACTION_WIFI_SIGNIN); mConnectivityManager.startCaptivePortalApp(mNetwork); } @Override - public void onForget(WifiDialog dialog) { - // can't forget network from a 'modify' dialog - } - - @Override public void onSubmit(WifiDialog dialog) { if (dialog.getController() != null) { mWifiManager.save(dialog.getController().getConfig(), new WifiManager.ActionListener() { @@ -584,4 +956,185 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController return mContext.getDrawable(Utils.getWifiIconResource(level)).mutate(); } } + + private boolean usingDataUsageHeader(Context context) { + return FeatureFlagUtils.isEnabled(context, FeatureFlags.WIFI_DETAILS_DATAUSAGE_HEADER); + } + + @VisibleForTesting + void connectNetwork() { + final Activity activity = mFragment.getActivity(); + // error handling, connected/saved network should have mWifiConfig. + if (mWifiConfig == null) { + Toast.makeText(activity, + R.string.wifi_failed_connect_message, + Toast.LENGTH_SHORT).show(); + return; + } + + // init state before connect + mConnectingState = STATE_NONE; + + if (mWifiManager.isWifiEnabled()) { + updateConnectingState(STATE_CONNECTING); + } else { + // Enable Wi-Fi automatically to connect AP + updateConnectingState(STATE_ENABLE_WIFI); + } + } + + private void updateConnectingState(int state) { + final Activity activity = mFragment.getActivity(); + Log.d(TAG, "updateConnectingState from " + mConnectingState + " to " + state); + switch (mConnectingState) { + case STATE_NONE: + case STATE_ENABLE_WIFI: + if (state == STATE_ENABLE_WIFI) { + Log.d(TAG, "Turn on Wi-Fi automatically!"); + updateConnectedButton(STATE_ENABLE_WIFI); + Toast.makeText(activity, + R.string.wifi_turned_on_message, + Toast.LENGTH_SHORT).show(); + mWifiManager.setWifiEnabled(true); + // start timer for error handling + startTimer(); + } else if (state == STATE_CONNECTING) { + Log.d(TAG, "connecting..."); + updateConnectedButton(STATE_CONNECTING); + if (mAccessPoint.isPasspoint()) { + mWifiManager.connect(mWifiConfig, mConnectListener); + } else { + mWifiManager.connect(mWifiConfig.networkId, mConnectListener); + } + // start timer for error handling since framework didn't call back if failed + startTimer(); + } else if (state == STATE_ENABLE_WIFI_FAILED) { + Log.e(TAG, "Wi-Fi failed to enable network!"); + stopTimer(); + // reset state + state = STATE_NONE; + Toast.makeText(activity, + R.string.wifi_failed_connect_message, + Toast.LENGTH_SHORT).show(); + updateConnectedButton(STATE_ENABLE_WIFI_FAILED); + } + // Do not break here for disconnected event. + case STATE_CONNECTED: + if (state == STATE_DISCONNECTED) { + Log.d(TAG, "disconnected"); + // reset state + state = STATE_NONE; + updateConnectedButton(STATE_DISCONNECTED); + refreshPage(); + // clear for getting MAC Address from saved configuration + mWifiInfo = null; + } + break; + case STATE_CONNECTING: + if (state == STATE_CONNECTED) { + Log.d(TAG, "connected"); + stopTimer(); + updateConnectedButton(STATE_CONNECTED); + Toast.makeText(activity, + mContext.getString(R.string.wifi_connected_to_message, + mAccessPoint.getTitle()), + Toast.LENGTH_SHORT).show(); + + refreshPage(); + } else if (state == STATE_NOT_IN_RANGE) { + Log.d(TAG, "AP not in range"); + stopTimer(); + // reset state + state = STATE_NONE; + Toast.makeText(activity, + R.string.wifi_not_in_range_message, + Toast.LENGTH_SHORT).show(); + updateConnectedButton(STATE_NOT_IN_RANGE); + } else if (state == STATE_FAILED) { + Log.d(TAG, "failed"); + stopTimer(); + // reset state + state = STATE_NONE; + Toast.makeText(activity, + R.string.wifi_failed_connect_message, + Toast.LENGTH_SHORT).show(); + updateConnectedButton(STATE_FAILED); + } + break; + default: + Log.e(TAG, "Invalid state : " + mConnectingState); + // don't update invalid state + return; + } + + mConnectingState = state; + } + + private void updateConnectedButton(int state) { + switch (state) { + case STATE_ENABLE_WIFI: + case STATE_CONNECTING: + mButtonsPref.setButton3Text(R.string.wifi_connecting) + .setButton3Enabled(false); + break; + case STATE_CONNECTED: + // init button state and set as invisible + mButtonsPref.setButton3Text(R.string.wifi_connect) + .setButton3Icon(R.drawable.ic_settings_wireless) + .setButton3Enabled(true) + .setButton3Visible(false); + break; + case STATE_DISCONNECTED: + case STATE_NOT_IN_RANGE: + case STATE_FAILED: + case STATE_ENABLE_WIFI_FAILED: + mButtonsPref.setButton3Text(R.string.wifi_connect) + .setButton3Icon(R.drawable.ic_settings_wireless) + .setButton3Enabled(true) + .setButton3Visible(true); + break; + default: + Log.e(TAG, "Invalid connect button state : " + state); + break; + } + } + + private void startTimer() { + if (mTimer != null) { + stopTimer(); + } + + mTimer = new CountDownTimer(TIMEOUT, TIMEOUT + 1) { + @Override + public void onTick(long millisUntilFinished) { + // Do nothing + } + @Override + public void onFinish() { + if (mFragment == null || mFragment.getActivity() == null) { + Log.d(TAG, "Ignore timeout since activity not exist!"); + return; + } + Log.e(TAG, "Timeout for state:" + mConnectingState); + if (mConnectingState == STATE_ENABLE_WIFI) { + updateConnectingState(STATE_ENABLE_WIFI_FAILED); + } else if (mConnectingState == STATE_CONNECTING) { + updateAccessPointFromScannedList(); + if (mIsOutOfRange) { + updateConnectingState(STATE_NOT_IN_RANGE); + } else { + updateConnectingState(STATE_FAILED); + } + } + } + }; + mTimer.start(); + } + + private void stopTimer() { + if (mTimer == null) return; + + mTimer.cancel(); + mTimer = null; + } } diff --git a/src/com/android/settings/wifi/details/WifiMeteredPreferenceController.java b/src/com/android/settings/wifi/details/WifiMeteredPreferenceController.java index 097a49be3e..5f4e9d0adb 100644 --- a/src/com/android/settings/wifi/details/WifiMeteredPreferenceController.java +++ b/src/com/android/settings/wifi/details/WifiMeteredPreferenceController.java @@ -20,27 +20,26 @@ import android.app.backup.BackupManager; import android.content.Context; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiManager; -import android.provider.Settings; + import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; import androidx.preference.DropDownPreference; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.text.TextUtils; import com.android.settings.core.BasePreferenceController; -import com.android.settings.core.PreferenceControllerMixin; +import com.android.settings.wifi.WifiDialog; import com.android.settingslib.core.AbstractPreferenceController; /** * {@link AbstractPreferenceController} that controls whether the wifi network is metered or not */ public class WifiMeteredPreferenceController extends BasePreferenceController implements - Preference.OnPreferenceChangeListener { + Preference.OnPreferenceChangeListener, WifiDialog.WifiDialogListener { private static final String KEY_WIFI_METERED = "metered"; private WifiConfiguration mWifiConfiguration; private WifiManager mWifiManager; + private Preference mPreference; public WifiMeteredPreferenceController(Context context, WifiConfiguration wifiConfiguration) { super(context, KEY_WIFI_METERED); @@ -85,4 +84,25 @@ public class WifiMeteredPreferenceController extends BasePreferenceController im private void updateSummary(DropDownPreference preference, int meteredOverride) { preference.setSummary(preference.getEntries()[meteredOverride]); } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + } + + @Override + public void onSubmit(WifiDialog dialog) { + if (dialog.getController() != null) { + final WifiConfiguration newConfig = dialog.getController().getConfig(); + if (newConfig == null || mWifiConfiguration == null) { + return; + } + + if (newConfig.meteredOverride != mWifiConfiguration.meteredOverride) { + mWifiConfiguration = newConfig; + onPreferenceChange(mPreference, String.valueOf(newConfig.meteredOverride)); + } + } + } } diff --git a/src/com/android/settings/wifi/details/WifiNetworkDetailsFragment.java b/src/com/android/settings/wifi/details/WifiNetworkDetailsFragment.java index 34380359e6..b645d60bbd 100644 --- a/src/com/android/settings/wifi/details/WifiNetworkDetailsFragment.java +++ b/src/com/android/settings/wifi/details/WifiNetworkDetailsFragment.java @@ -18,6 +18,7 @@ package com.android.settings.wifi.details; import static com.android.settings.wifi.WifiSettings.WIFI_DIALOG_ID; import android.app.Dialog; +import android.app.settings.SettingsEnums; import android.content.Context; import android.net.ConnectivityManager; import android.net.wifi.WifiManager; @@ -28,13 +29,12 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; -import com.android.internal.logging.nano.MetricsProto; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.wifi.WifiConfigUiBase; import com.android.settings.wifi.WifiDialog; import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.wifi.AccessPoint; @@ -47,12 +47,14 @@ import java.util.List; * <p>The AccessPoint should be saved to the intent Extras when launching this class via * {@link AccessPoint#saveWifiState(Bundle)} in order to properly render this page. */ -public class WifiNetworkDetailsFragment extends DashboardFragment { +public class WifiNetworkDetailsFragment extends DashboardFragment implements + WifiDialog.WifiDialogListener { private static final String TAG = "WifiNetworkDetailsFrg"; private AccessPoint mAccessPoint; private WifiDetailPreferenceController mWifiDetailPreferenceController; + private List<WifiDialog.WifiDialogListener> mWifiDialogListeners = new ArrayList<>(); @Override public void onAttach(Context context) { @@ -62,7 +64,7 @@ public class WifiNetworkDetailsFragment extends DashboardFragment { @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.WIFI_NETWORK_DETAILS; + return SettingsEnums.WIFI_NETWORK_DETAILS; } @Override @@ -78,7 +80,7 @@ public class WifiNetworkDetailsFragment extends DashboardFragment { @Override public int getDialogMetricsCategory(int dialogId) { if (dialogId == WIFI_DIALOG_ID) { - return MetricsEvent.DIALOG_WIFI_AP_EDIT; + return SettingsEnums.DIALOG_WIFI_AP_EDIT; } return 0; } @@ -89,7 +91,7 @@ public class WifiNetworkDetailsFragment extends DashboardFragment { || mAccessPoint == null) { return null; } - return WifiDialog.createModal(getActivity(), mWifiDetailPreferenceController, mAccessPoint, + return WifiDialog.createModal(getActivity(), this, mAccessPoint, WifiConfigUiBase.MODE_MODIFY); } @@ -97,7 +99,7 @@ public class WifiNetworkDetailsFragment extends DashboardFragment { @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { MenuItem item = menu.add(0, Menu.FIRST, 0, R.string.wifi_modify); - item.setIcon(R.drawable.ic_mode_edit); + item.setIcon(com.android.internal.R.drawable.ic_mode_edit); item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); super.onCreateOptionsMenu(menu, inflater); } @@ -108,7 +110,7 @@ public class WifiNetworkDetailsFragment extends DashboardFragment { case Menu.FIRST: if (!mWifiDetailPreferenceController.canModifyNetwork()) { RestrictedLockUtils.sendShowAdminSupportDetailsIntent(getContext(), - RestrictedLockUtils.getDeviceOwner(getContext())); + RestrictedLockUtilsInternal.getDeviceOwner(getContext())); } else { showDialog(WIFI_DIALOG_ID); } @@ -122,19 +124,44 @@ public class WifiNetworkDetailsFragment extends DashboardFragment { protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { final List<AbstractPreferenceController> controllers = new ArrayList<>(); final ConnectivityManager cm = context.getSystemService(ConnectivityManager.class); + mWifiDetailPreferenceController = WifiDetailPreferenceController.newInstance( mAccessPoint, cm, context, this, new Handler(Looper.getMainLooper()), // UI thread. - getLifecycle(), + getSettingsLifecycle(), context.getSystemService(WifiManager.class), mMetricsFeatureProvider); controllers.add(mWifiDetailPreferenceController); - controllers.add(new WifiMeteredPreferenceController(context, mAccessPoint.getConfig())); + controllers.add(new AddDevicePreferenceController(context).init(mAccessPoint)); + + final WifiMeteredPreferenceController meteredPreferenceController = + new WifiMeteredPreferenceController(context, mAccessPoint.getConfig()); + controllers.add(meteredPreferenceController); + + final WifiPrivacyPreferenceController privacyController = + new WifiPrivacyPreferenceController(context); + privacyController.setWifiConfiguration(mAccessPoint.getConfig()); + privacyController.setIsEphemeral(mAccessPoint.isEphemeral()); + privacyController.setIsPasspoint( + mAccessPoint.isPasspoint() || mAccessPoint.isPasspointConfig()); + controllers.add(privacyController); + + // Sets callback listener for wifi dialog. + mWifiDialogListeners.add(mWifiDetailPreferenceController); + mWifiDialogListeners.add(privacyController); + mWifiDialogListeners.add(meteredPreferenceController); return controllers; } + + @Override + public void onSubmit(WifiDialog dialog) { + for (WifiDialog.WifiDialogListener listener : mWifiDialogListeners) { + listener.onSubmit(dialog); + } + } } diff --git a/src/com/android/settings/wifi/details/WifiPrivacyPreferenceController.java b/src/com/android/settings/wifi/details/WifiPrivacyPreferenceController.java new file mode 100644 index 0000000000..950cc131f4 --- /dev/null +++ b/src/com/android/settings/wifi/details/WifiPrivacyPreferenceController.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2018 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.wifi.details; + +import android.content.Context; +import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.DropDownPreference; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.wifi.WifiDialog; +import com.android.settingslib.core.AbstractPreferenceController; + +/** + * {@link AbstractPreferenceController} that controls whether the wifi network is mac randomized + * or not + */ +public class WifiPrivacyPreferenceController extends BasePreferenceController implements + Preference.OnPreferenceChangeListener, WifiDialog.WifiDialogListener { + + private static final String KEY_WIFI_PRIVACY = "privacy"; + private WifiConfiguration mWifiConfiguration; + private WifiManager mWifiManager; + private boolean mIsEphemeral = false; + private boolean mIsPasspoint = false; + private Preference mPreference; + + public WifiPrivacyPreferenceController(Context context) { + super(context, KEY_WIFI_PRIVACY); + mWifiConfiguration = null; + mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + } + + public void setWifiConfiguration(WifiConfiguration wifiConfiguration) { + mWifiConfiguration = wifiConfiguration; + } + + public void setIsEphemeral(boolean isEphemeral) { + mIsEphemeral = isEphemeral; + } + + public void setIsPasspoint(boolean isPasspoint) { + mIsPasspoint = isPasspoint; + } + + @Override + public int getAvailabilityStatus() { + return mContext.getResources().getBoolean( + com.android.internal.R.bool.config_wifi_connected_mac_randomization_supported) ? + AVAILABLE : CONDITIONALLY_UNAVAILABLE; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + } + + @Override + public void updateState(Preference preference) { + final DropDownPreference dropDownPreference = (DropDownPreference) preference; + final int randomizationLevel = getRandomizationValue(); + dropDownPreference.setValue(Integer.toString(randomizationLevel)); + updateSummary(dropDownPreference, randomizationLevel); + + // Makes preference not selectable, when this is a ephemeral network. + if (mIsEphemeral || mIsPasspoint) { + preference.setSelectable(false); + dropDownPreference.setSummary(R.string.wifi_privacy_settings_ephemeral_summary); + } + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + if (mWifiConfiguration != null) { + mWifiConfiguration.macRandomizationSetting = Integer.parseInt((String) newValue); + mWifiManager.updateNetwork(mWifiConfiguration); + + // To activate changing, we need to reconnect network. WiFi will auto connect to + // current network after disconnect(). Only needed when this is connected network. + final WifiInfo wifiInfo = mWifiManager.getConnectionInfo(); + if (wifiInfo != null && wifiInfo.getNetworkId() == mWifiConfiguration.networkId) { + mWifiManager.disconnect(); + } + } + updateSummary((DropDownPreference) preference, Integer.parseInt((String) newValue)); + return true; + } + + @VisibleForTesting + int getRandomizationValue() { + if (mWifiConfiguration != null) { + return mWifiConfiguration.macRandomizationSetting; + } + return WifiConfiguration.RANDOMIZATION_PERSISTENT; + } + + private static final int PREF_RANDOMIZATION_PERSISTENT = 0; + private static final int PREF_RANDOMIZATION_NONE = 1; + + /** + * Returns preference index value. + * + * @param macRandomized is mac randomized value + * @return index value of preference + */ + public static int translateMacRandomizedValueToPrefValue(int macRandomized) { + return (macRandomized == WifiConfiguration.RANDOMIZATION_PERSISTENT) + ? PREF_RANDOMIZATION_PERSISTENT : PREF_RANDOMIZATION_NONE; + } + + /** + * Returns mac randomized value. + * + * @param prefMacRandomized is preference index value + * @return mac randomized value + */ + public static int translatePrefValueToMacRandomizedValue(int prefMacRandomized) { + return (prefMacRandomized == PREF_RANDOMIZATION_PERSISTENT) + ? WifiConfiguration.RANDOMIZATION_PERSISTENT : WifiConfiguration.RANDOMIZATION_NONE; + } + + private void updateSummary(DropDownPreference preference, int macRandomized) { + // Translates value here to set RANDOMIZATION_PERSISTENT as first item in UI for better UX. + final int prefMacRandomized = translateMacRandomizedValueToPrefValue(macRandomized); + preference.setSummary(preference.getEntries()[prefMacRandomized]); + } + + @Override + public void onSubmit(WifiDialog dialog) { + if (dialog.getController() != null) { + final WifiConfiguration newConfig = dialog.getController().getConfig(); + if (newConfig == null || mWifiConfiguration == null) { + return; + } + + if (newConfig.macRandomizationSetting != mWifiConfiguration.macRandomizationSetting) { + mWifiConfiguration = newConfig; + onPreferenceChange(mPreference, String.valueOf(newConfig.macRandomizationSetting)); + } + } + } +} diff --git a/src/com/android/settings/wifi/dpp/WifiDppAddDeviceFragment.java b/src/com/android/settings/wifi/dpp/WifiDppAddDeviceFragment.java new file mode 100644 index 0000000000..3a9308e2f1 --- /dev/null +++ b/src/com/android/settings/wifi/dpp/WifiDppAddDeviceFragment.java @@ -0,0 +1,377 @@ +/* + * Copyright (C) 2018 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.wifi.dpp; + +import android.app.ActionBar; +import android.app.Activity; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.net.wifi.EasyConnectStatusCallback; +import android.net.wifi.WifiManager; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.ProgressBar; + +import androidx.lifecycle.ViewModelProviders; + +import com.android.settings.R; + +import java.util.concurrent.Executor; + +/** + * After getting Wi-Fi network information and(or) QR code, this fragment config a device to connect + * to the Wi-Fi network. + */ +public class WifiDppAddDeviceFragment extends WifiDppQrCodeBaseFragment { + private static final String TAG = "WifiDppAddDeviceFragment"; + + private ProgressBar mProgressBar; + private ImageView mWifiApPictureView; + private Button mChooseDifferentNetwork; + private Button mButtonLeft; + private Button mButtonRight; + + private int mLatestStatusCode = WifiDppUtils.EASY_CONNECT_EVENT_FAILURE_NONE; + + // Key for Bundle usage + private static final String KEY_LATEST_ERROR_CODE = "key_latest_error_code"; + + private class EasyConnectConfiguratorStatusCallback extends EasyConnectStatusCallback { + @Override + public void onEnrolleeSuccess(int newNetworkId) { + // Do nothing + } + + @Override + public void onConfiguratorSuccess(int code) { + showSuccessUi(/* isConfigurationChange */ false); + } + + @Override + public void onFailure(int code) { + Log.d(TAG, "EasyConnectConfiguratorStatusCallback.onFailure " + code); + + showErrorUi(code, /* isConfigurationChange */ false); + } + + @Override + public void onProgress(int code) { + // Do nothing + } + } + + private void showSuccessUi(boolean isConfigurationChange) { + setHeaderIconImageResource(R.drawable.ic_devices_check_circle_green); + mTitle.setText(R.string.wifi_dpp_wifi_shared_with_device); + mProgressBar.setVisibility(isGoingInitiator() ? View.VISIBLE : View.INVISIBLE); + mSummary.setVisibility(View.INVISIBLE); + mWifiApPictureView.setImageResource(R.drawable.wifi_dpp_success); + mChooseDifferentNetwork.setVisibility(View.INVISIBLE); + mButtonLeft.setText(R.string.wifi_dpp_add_another_device); + mButtonLeft.setOnClickListener(v -> getFragmentManager().popBackStack()); + mButtonRight.setText(R.string.done); + mButtonRight.setOnClickListener(v -> { + final Activity activity = getActivity(); + activity.setResult(Activity.RESULT_OK); + activity.finish(); + }); + + if (!isConfigurationChange) { + mLatestStatusCode = WifiDppUtils.EASY_CONNECT_EVENT_SUCCESS; + changeFocusAndAnnounceChange(mButtonRight, mTitle); + } + } + + private void showErrorUi(int code, boolean isConfigurationChange) { + switch (code) { + case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_INVALID_URI: + mSummary.setText(R.string.wifi_dpp_could_not_detect_valid_qr_code); + break; + + case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_AUTHENTICATION: + mSummary.setText(R.string.wifi_dpp_failure_authentication_or_configuration); + break; + + case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_NOT_COMPATIBLE: + mSummary.setText(R.string.wifi_dpp_failure_not_compatible); + break; + + case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_CONFIGURATION: + mSummary.setText(R.string.wifi_dpp_failure_authentication_or_configuration); + break; + + case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_BUSY: + if (isConfigurationChange) { + return; + } + + if (code == mLatestStatusCode) { + throw(new IllegalStateException("Tried restarting EasyConnectSession but still" + + "receiving EASY_CONNECT_EVENT_FAILURE_BUSY")); + } + + mLatestStatusCode = code; + final WifiManager wifiManager = + getContext().getSystemService(WifiManager.class); + wifiManager.stopEasyConnectSession(); + startWifiDppConfiguratorInitiator(); + return; + + case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_TIMEOUT: + mSummary.setText(R.string.wifi_dpp_failure_timeout); + break; + + case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_GENERIC: + mSummary.setText(R.string.wifi_dpp_failure_generic); + break; + + case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_NOT_SUPPORTED: + mSummary.setText(getString(R.string.wifi_dpp_failure_not_supported, getSsid())); + break; + + case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_INVALID_NETWORK: + throw(new IllegalStateException("Wi-Fi DPP configurator used a non-PSK/non-SAE" + + "network to handshake")); + + default: + throw(new IllegalStateException("Unexpected Wi-Fi DPP error")); + } + + mTitle.setText(R.string.wifi_dpp_could_not_add_device); + mWifiApPictureView.setImageResource(R.drawable.wifi_dpp_error); + mChooseDifferentNetwork.setVisibility(View.INVISIBLE); + if (hasRetryButton(code)) { + mButtonRight.setText(R.string.retry); + } else { + mButtonRight.setText(R.string.done); + mButtonRight.setOnClickListener(v -> getActivity().finish()); + mButtonLeft.setVisibility(View.INVISIBLE); + } + + if (isGoingInitiator()) { + mSummary.setText(R.string.wifi_dpp_sharing_wifi_with_this_device); + } + + mProgressBar.setVisibility(isGoingInitiator() ? View.VISIBLE : View.INVISIBLE); + mButtonRight.setVisibility(isGoingInitiator() ? View.INVISIBLE : View.VISIBLE); + + if (!isConfigurationChange) { + mLatestStatusCode = code; + changeFocusAndAnnounceChange(mButtonRight, mSummary); + } + } + + private boolean hasRetryButton(int code) { + switch (code) { + case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_INVALID_URI: + case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_NOT_COMPATIBLE: + return false; + + default: + break; + } + + return true; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.SETTINGS_WIFI_DPP_CONFIGURATOR; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (savedInstanceState != null) { + mLatestStatusCode = savedInstanceState.getInt(KEY_LATEST_ERROR_CODE); + } + + final WifiDppInitiatorViewModel model = + ViewModelProviders.of(this).get(WifiDppInitiatorViewModel.class); + + model.getStatusCode().observe(this, statusCode -> { + // After configuration change, observe callback will be triggered, + // do nothing for this case if a handshake does not end + if (model.isGoingInitiator()) { + return; + } + + int code = statusCode.intValue(); + if (code == WifiDppUtils.EASY_CONNECT_EVENT_SUCCESS) { + new EasyConnectConfiguratorStatusCallback().onConfiguratorSuccess(code); + } else { + new EasyConnectConfiguratorStatusCallback().onFailure(code); + } + }); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + final ActionBar actionBar = getActivity().getActionBar(); + if (actionBar != null) { + actionBar.hide(); + } + } + + @Override + public final View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.wifi_dpp_add_device_fragment, container, + /* attachToRoot */ false); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + setHeaderIconImageResource(R.drawable.ic_devices_other_opaque_black); + + mProgressBar = view.findViewById(R.id.indeterminate_bar); + + final WifiQrCode wifiQrCode = ((WifiDppConfiguratorActivity) getActivity()) + .getWifiDppQrCode(); + final String information = wifiQrCode.getInformation(); + if (TextUtils.isEmpty(information)) { + mTitle.setText(R.string.wifi_dpp_device_found); + } else { + mTitle.setText(information); + } + + updateSummary(); + mWifiApPictureView = view.findViewById(R.id.wifi_ap_picture_view); + + mChooseDifferentNetwork = view.findViewById(R.id.choose_different_network); + mChooseDifferentNetwork.setOnClickListener(v -> + mClickChooseDifferentNetworkListener.onClickChooseDifferentNetwork() + ); + + mButtonLeft = view.findViewById(R.id.button_left); + mButtonLeft.setText(R.string.cancel); + mButtonLeft.setOnClickListener(v -> getActivity().finish()); + + mButtonRight = view.findViewById(R.id.button_right); + mButtonRight.setText(R.string.wifi_dpp_share_wifi); + mButtonRight.setOnClickListener(v -> { + mProgressBar.setVisibility(View.VISIBLE); + mButtonRight.setVisibility(View.INVISIBLE); + startWifiDppConfiguratorInitiator(); + updateSummary(); + mTitleSummaryContainer.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); + }); + + if (savedInstanceState != null) { + if (mLatestStatusCode == WifiDppUtils.EASY_CONNECT_EVENT_SUCCESS) { + showSuccessUi(/* isConfigurationChange */ true); + } else if (mLatestStatusCode == WifiDppUtils.EASY_CONNECT_EVENT_FAILURE_NONE) { + mProgressBar.setVisibility(isGoingInitiator() ? View.VISIBLE : View.INVISIBLE); + mButtonRight.setVisibility(isGoingInitiator() ? View.INVISIBLE : View.VISIBLE); + } else { + showErrorUi(mLatestStatusCode, /* isConfigurationChange */ true); + } + } else { + changeFocusAndAnnounceChange(mButtonRight, mTitleSummaryContainer); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + outState.putInt(KEY_LATEST_ERROR_CODE, mLatestStatusCode); + + super.onSaveInstanceState(outState); + } + + private String getSsid() { + final WifiNetworkConfig wifiNetworkConfig = ((WifiDppConfiguratorActivity) getActivity()) + .getWifiNetworkConfig(); + if (!WifiNetworkConfig.isValidConfig(wifiNetworkConfig)) { + throw new IllegalStateException("Invalid Wi-Fi network for configuring"); + } + return wifiNetworkConfig.getSsid(); + } + + private void startWifiDppConfiguratorInitiator() { + final WifiQrCode wifiQrCode = ((WifiDppConfiguratorActivity) getActivity()) + .getWifiDppQrCode(); + final String qrCode = wifiQrCode.getQrCode(); + final int networkId = + ((WifiDppConfiguratorActivity) getActivity()).getWifiNetworkConfig().getNetworkId(); + final WifiDppInitiatorViewModel model = + ViewModelProviders.of(this).get(WifiDppInitiatorViewModel.class); + + model.startEasyConnectAsConfiguratorInitiator(qrCode, networkId); + } + + // Container Activity must implement this interface + public interface OnClickChooseDifferentNetworkListener { + public void onClickChooseDifferentNetwork(); + } + OnClickChooseDifferentNetworkListener mClickChooseDifferentNetworkListener; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + + mClickChooseDifferentNetworkListener = (OnClickChooseDifferentNetworkListener) context; + } + + @Override + public void onDetach() { + mClickChooseDifferentNetworkListener = null; + + super.onDetach(); + } + + // Check is Easy Connect handshaking or not + private boolean isGoingInitiator() { + final WifiDppInitiatorViewModel model = + ViewModelProviders.of(this).get(WifiDppInitiatorViewModel.class); + + return model.isGoingInitiator(); + } + + private void updateSummary() { + if (isGoingInitiator()) { + mSummary.setText(R.string.wifi_dpp_sharing_wifi_with_this_device); + } else { + mSummary.setText(getString(R.string.wifi_dpp_add_device_to_wifi, getSsid())); + } + } + + /** + * This fragment will change UI display and text messages for events. To improve Talkback user + * experienience, using this method to focus on a right component and announce a changed text + * after an UI changing event. + * + * @param focusView The UI component which will be focused + * @param announceView The UI component's text will be talked + */ + private void changeFocusAndAnnounceChange(View focusView, View announceView) { + focusView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); + announceView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + } +} diff --git a/src/com/android/settings/wifi/dpp/WifiDppChooseSavedWifiNetworkFragment.java b/src/com/android/settings/wifi/dpp/WifiDppChooseSavedWifiNetworkFragment.java new file mode 100644 index 0000000000..ddba93331a --- /dev/null +++ b/src/com/android/settings/wifi/dpp/WifiDppChooseSavedWifiNetworkFragment.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2018 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.wifi.dpp; + +import android.app.ActionBar; +import android.app.settings.SettingsEnums; +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.widget.Button; +import android.widget.ListView; + +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; + +import com.android.settings.R; + +/** + * After a camera APP scanned a Wi-Fi DPP QR code, it can trigger + * {@code WifiDppConfiguratorActivity} to start with this fragment to choose a saved Wi-Fi network. + */ +public class WifiDppChooseSavedWifiNetworkFragment extends WifiDppQrCodeBaseFragment { + private static final String TAG_FRAGMENT_WIFI_NETWORK_LIST = "wifi_network_list_fragment"; + + private ListView mSavedWifiNetworkList; + private Button mButtonLeft; + private Button mButtonRight; + + @Override + public int getMetricsCategory() { + return SettingsEnums.SETTINGS_WIFI_DPP_CONFIGURATOR; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + final ActionBar actionBar = getActivity().getActionBar(); + if (actionBar != null) { + actionBar.hide(); + } + + /** Embeded WifiNetworkListFragment as child fragment within + * WifiDppChooseSavedWifiNetworkFragment. */ + final FragmentManager fragmentManager = getChildFragmentManager(); + final WifiNetworkListFragment fragment = new WifiNetworkListFragment(); + final Bundle args = getArguments(); + if (args != null) { + fragment.setArguments(args); + } + final FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); + fragmentTransaction.replace(R.id.wifi_network_list_container, fragment, + TAG_FRAGMENT_WIFI_NETWORK_LIST); + fragmentTransaction.commit(); + } + + @Override + public final View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.wifi_dpp_choose_saved_wifi_network_fragment, container, + /* attachToRoot */ false); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + setHeaderIconImageResource(R.drawable.ic_wifi_signal_4); + + mTitle.setText(R.string.wifi_dpp_choose_network); + mSummary.setText(R.string.wifi_dpp_choose_network_to_connect_device); + + mButtonLeft = view.findViewById(R.id.button_left); + mButtonLeft.setText(R.string.cancel); + mButtonLeft.setOnClickListener(v -> { + String action = null; + final Intent intent = getActivity().getIntent(); + if (intent != null) { + action = intent.getAction(); + } + if (WifiDppConfiguratorActivity.ACTION_CONFIGURATOR_QR_CODE_SCANNER.equals(action) || + WifiDppConfiguratorActivity + .ACTION_CONFIGURATOR_QR_CODE_GENERATOR.equals(action)) { + getFragmentManager().popBackStack(); + } else { + getActivity().finish(); + } + }); + + mButtonRight = view.findViewById(R.id.button_right); + mButtonRight.setVisibility(View.GONE); + + if (savedInstanceState == null) { + // For Talkback to describe this fragment + mTitleSummaryContainer.sendAccessibilityEvent( + AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + } + } +} diff --git a/src/com/android/settings/wifi/dpp/WifiDppConfiguratorActivity.java b/src/com/android/settings/wifi/dpp/WifiDppConfiguratorActivity.java new file mode 100644 index 0000000000..5dfcc25612 --- /dev/null +++ b/src/com/android/settings/wifi/dpp/WifiDppConfiguratorActivity.java @@ -0,0 +1,401 @@ +/* + * Copyright (C) 2018 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.wifi.dpp; + +import android.app.ActionBar; +import android.app.settings.SettingsEnums; +import android.content.Intent; +import android.net.Uri; +import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.os.Bundle; +import android.provider.Settings; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; + +import com.android.settings.R; +import com.android.settings.core.InstrumentedActivity; + +import java.util.List; + +/** + * To provision "other" device with specified Wi-Fi network. + * + * Uses different intents to specify different provisioning ways. + * + * For intent action {@code ACTION_CONFIGURATOR_QR_CODE_SCANNER} and + * {@code android.settings.WIFI_DPP_CONFIGURATOR_QR_CODE_GENERATOR}, specify the Wi-Fi network to be + * provisioned in: + * + * {@code WifiDppUtils.EXTRA_WIFI_SECURITY} + * {@code WifiDppUtils.EXTRA_WIFI_SSID} + * {@code WifiDppUtils.EXTRA_WIFI_PRE_SHARED_KEY} + * {@code WifiDppUtils.EXTRA_WIFI_HIDDEN_SSID} + * + * For intent action {@link Settings#ACTION_PROCESS_WIFI_EASY_CONNECT_URI}, specify Wi-Fi + * Easy Connect bootstrapping information string in Intent's data URI. + */ +public class WifiDppConfiguratorActivity extends InstrumentedActivity implements + WifiNetworkConfig.Retriever, + WifiDppQrCodeGeneratorFragment.OnQrCodeGeneratorFragmentAddButtonClickedListener, + WifiDppQrCodeScannerFragment.OnScanWifiDppSuccessListener, + WifiDppAddDeviceFragment.OnClickChooseDifferentNetworkListener, + WifiNetworkListFragment.OnChooseNetworkListener { + + private static final String TAG = "WifiDppConfiguratorActivity"; + + public static final String ACTION_CONFIGURATOR_QR_CODE_SCANNER = + "android.settings.WIFI_DPP_CONFIGURATOR_QR_CODE_SCANNER"; + public static final String ACTION_CONFIGURATOR_QR_CODE_GENERATOR = + "android.settings.WIFI_DPP_CONFIGURATOR_QR_CODE_GENERATOR"; + + // Key for Bundle usage + private static final String KEY_QR_CODE = "key_qr_code"; + private static final String KEY_WIFI_SECURITY = "key_wifi_security"; + private static final String KEY_WIFI_SSID = "key_wifi_ssid"; + private static final String KEY_WIFI_PRESHARED_KEY = "key_wifi_preshared_key"; + private static final String KEY_WIFI_HIDDEN_SSID = "key_wifi_hidden_ssid"; + private static final String KEY_WIFI_NETWORK_ID = "key_wifi_network_id"; + private static final String KEY_IS_HOTSPOT = "key_is_hotspot"; + + private FragmentManager mFragmentManager; + + /** The Wi-Fi network which will be configured */ + private WifiNetworkConfig mWifiNetworkConfig; + + /** The Wi-Fi DPP QR code from intent ACTION_PROCESS_WIFI_EASY_CONNECT_URI */ + private WifiQrCode mWifiDppQrCode; + + /** Secret extra that allows fake networks to show in UI for testing purposes */ + private boolean mIsTest; + + @Override + public int getMetricsCategory() { + return SettingsEnums.SETTINGS_WIFI_DPP_CONFIGURATOR; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.wifi_dpp_activity); + mFragmentManager = getSupportFragmentManager(); + + if (savedInstanceState != null) { + String qrCode = savedInstanceState.getString(KEY_QR_CODE); + + mWifiDppQrCode = WifiQrCode.getValidWifiDppQrCodeOrNull(qrCode); + + final String security = savedInstanceState.getString(KEY_WIFI_SECURITY); + final String ssid = savedInstanceState.getString(KEY_WIFI_SSID); + final String preSharedKey = savedInstanceState.getString(KEY_WIFI_PRESHARED_KEY); + final boolean hiddenSsid = savedInstanceState.getBoolean(KEY_WIFI_HIDDEN_SSID); + final int networkId = savedInstanceState.getInt(KEY_WIFI_NETWORK_ID); + final boolean isHotspot = savedInstanceState.getBoolean(KEY_IS_HOTSPOT); + + mWifiNetworkConfig = WifiNetworkConfig.getValidConfigOrNull(security, ssid, + preSharedKey, hiddenSsid, networkId, isHotspot); + } else { + handleIntent(getIntent()); + } + + ActionBar actionBar = getActionBar(); + if (actionBar != null) { + actionBar.setElevation(0); + actionBar.setDisplayShowTitleEnabled(false); + } + } + + private void handleIntent(Intent intent) { + boolean cancelActivity = false; + WifiNetworkConfig config; + switch (intent.getAction()) { + case ACTION_CONFIGURATOR_QR_CODE_SCANNER: + config = WifiNetworkConfig.getValidConfigOrNull(intent); + if (config == null) { + cancelActivity = true; + } else { + mWifiNetworkConfig = config; + showQrCodeScannerFragment(/* addToBackStack= */ false); + } + break; + case ACTION_CONFIGURATOR_QR_CODE_GENERATOR: + config = WifiNetworkConfig.getValidConfigOrNull(intent); + if (config == null) { + cancelActivity = true; + } else { + mWifiNetworkConfig = config; + showQrCodeGeneratorFragment(); + } + break; + case Settings.ACTION_PROCESS_WIFI_EASY_CONNECT_URI: + final Uri uri = intent.getData(); + final String uriString = (uri == null) ? null : uri.toString(); + mIsTest = intent.getBooleanExtra(WifiDppUtils.EXTRA_TEST, false); + mWifiDppQrCode = WifiQrCode.getValidWifiDppQrCodeOrNull(uriString); + final boolean isDppSupported = WifiDppUtils.isWifiDppEnabled(this); + if (!isDppSupported) { + Log.d(TAG, "Device doesn't support Wifi DPP"); + } + if (mWifiDppQrCode == null || !isDppSupported) { + cancelActivity = true; + } else { + final WifiNetworkConfig connectedConfig = getConnectedWifiNetworkConfigOrNull(); + if (connectedConfig == null || !connectedConfig.isSupportWifiDpp(this)) { + showChooseSavedWifiNetworkFragment(/* addToBackStack */ false); + } else { + mWifiNetworkConfig = connectedConfig; + showAddDeviceFragment(/* addToBackStack */ false); + } + } + break; + default: + cancelActivity = true; + Log.e(TAG, "Launch with an invalid action"); + } + + if (cancelActivity) { + finish(); + } + } + + private void showQrCodeScannerFragment(boolean addToBackStack) { + WifiDppQrCodeScannerFragment fragment = + (WifiDppQrCodeScannerFragment) mFragmentManager.findFragmentByTag( + WifiDppUtils.TAG_FRAGMENT_QR_CODE_SCANNER); + + if (fragment == null) { + fragment = new WifiDppQrCodeScannerFragment(); + } else { + if (fragment.isVisible()) { + return; + } + + // When the fragment in back stack but not on top of the stack, we can simply pop + // stack because current fragment transactions are arranged in an order + mFragmentManager.popBackStackImmediate(); + return; + } + final FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction(); + + fragmentTransaction.replace(R.id.fragment_container, fragment, + WifiDppUtils.TAG_FRAGMENT_QR_CODE_SCANNER); + if (addToBackStack) { + fragmentTransaction.addToBackStack(/* name */ null); + } + fragmentTransaction.commit(); + } + + private void showQrCodeGeneratorFragment() { + WifiDppQrCodeGeneratorFragment fragment = + (WifiDppQrCodeGeneratorFragment) mFragmentManager.findFragmentByTag( + WifiDppUtils.TAG_FRAGMENT_QR_CODE_GENERATOR); + + if (fragment == null) { + fragment = new WifiDppQrCodeGeneratorFragment(); + } else { + if (fragment.isVisible()) { + return; + } + + // When the fragment in back stack but not on top of the stack, we can simply pop + // stack because current fragment transactions are arranged in an order + mFragmentManager.popBackStackImmediate(); + return; + } + final FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction(); + + fragmentTransaction.replace(R.id.fragment_container, fragment, + WifiDppUtils.TAG_FRAGMENT_QR_CODE_GENERATOR); + fragmentTransaction.commit(); + } + + private void showChooseSavedWifiNetworkFragment(boolean addToBackStack) { + WifiDppChooseSavedWifiNetworkFragment fragment = + (WifiDppChooseSavedWifiNetworkFragment) mFragmentManager.findFragmentByTag( + WifiDppUtils.TAG_FRAGMENT_CHOOSE_SAVED_WIFI_NETWORK); + + if (fragment == null) { + fragment = new WifiDppChooseSavedWifiNetworkFragment(); + if (mIsTest) { + Bundle bundle = new Bundle(); + bundle.putBoolean(WifiDppUtils.EXTRA_TEST, true); + fragment.setArguments(bundle); + } + } else { + if (fragment.isVisible()) { + return; + } + + // When the fragment in back stack but not on top of the stack, we can simply pop + // stack because current fragment transactions are arranged in an order + mFragmentManager.popBackStackImmediate(); + return; + } + final FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction(); + + fragmentTransaction.replace(R.id.fragment_container, fragment, + WifiDppUtils.TAG_FRAGMENT_CHOOSE_SAVED_WIFI_NETWORK); + if (addToBackStack) { + fragmentTransaction.addToBackStack(/* name */ null); + } + fragmentTransaction.commit(); + } + + private void showAddDeviceFragment(boolean addToBackStack) { + WifiDppAddDeviceFragment fragment = + (WifiDppAddDeviceFragment) mFragmentManager.findFragmentByTag( + WifiDppUtils.TAG_FRAGMENT_ADD_DEVICE); + + if (fragment == null) { + fragment = new WifiDppAddDeviceFragment(); + } else { + if (fragment.isVisible()) { + return; + } + + // When the fragment in back stack but not on top of the stack, we can simply pop + // stack because current fragment transactions are arranged in an order + mFragmentManager.popBackStackImmediate(); + return; + } + final FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction(); + + fragmentTransaction.replace(R.id.fragment_container, fragment, + WifiDppUtils.TAG_FRAGMENT_ADD_DEVICE); + if (addToBackStack) { + fragmentTransaction.addToBackStack(/* name */ null); + } + fragmentTransaction.commit(); + } + + @Override + public WifiNetworkConfig getWifiNetworkConfig() { + return mWifiNetworkConfig; + } + + public WifiQrCode getWifiDppQrCode() { + return mWifiDppQrCode; + } + + @VisibleForTesting + boolean setWifiNetworkConfig(WifiNetworkConfig config) { + if(!WifiNetworkConfig.isValidConfig(config)) { + return false; + } else { + mWifiNetworkConfig = new WifiNetworkConfig(config); + return true; + } + } + + @VisibleForTesting + boolean setWifiDppQrCode(WifiQrCode wifiQrCode) { + if (wifiQrCode == null) { + return false; + } + + if (!WifiQrCode.SCHEME_DPP.equals(wifiQrCode.getScheme())) { + return false; + } + + mWifiDppQrCode = new WifiQrCode(wifiQrCode.getQrCode()); + return true; + } + + @Override + public boolean onNavigateUp() { + if (!mFragmentManager.popBackStackImmediate()) { + finish(); + } + return true; + } + + @Override + public void onQrCodeGeneratorFragmentAddButtonClicked() { + showQrCodeScannerFragment(/* addToBackStack */ true); + } + + @Override + public void onScanWifiDppSuccess(WifiQrCode wifiQrCode) { + mWifiDppQrCode = wifiQrCode; + + showAddDeviceFragment(/* addToBackStack */ true); + } + + @Override + public void onClickChooseDifferentNetwork() { + showChooseSavedWifiNetworkFragment(/* addToBackStack */ true); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + if (mWifiDppQrCode != null) { + outState.putString(KEY_QR_CODE, mWifiDppQrCode.getQrCode()); + } + + if (mWifiNetworkConfig != null) { + outState.putString(KEY_WIFI_SECURITY, mWifiNetworkConfig.getSecurity()); + outState.putString(KEY_WIFI_SSID, mWifiNetworkConfig.getSsid()); + outState.putString(KEY_WIFI_PRESHARED_KEY, mWifiNetworkConfig.getPreSharedKey()); + outState.putBoolean(KEY_WIFI_HIDDEN_SSID, mWifiNetworkConfig.getHiddenSsid()); + outState.putInt(KEY_WIFI_NETWORK_ID, mWifiNetworkConfig.getNetworkId()); + outState.putBoolean(KEY_IS_HOTSPOT, mWifiNetworkConfig.isHotspot()); + } + + super.onSaveInstanceState(outState); + } + + @Override + public void onChooseNetwork(WifiNetworkConfig wifiNetworkConfig) { + mWifiNetworkConfig = new WifiNetworkConfig(wifiNetworkConfig); + + showAddDeviceFragment(/* addToBackStack */ true); + } + + private WifiNetworkConfig getConnectedWifiNetworkConfigOrNull() { + final WifiManager wifiManager = getSystemService(WifiManager.class); + if (!wifiManager.isWifiEnabled()) { + return null; + } + + final WifiInfo connectionInfo = wifiManager.getConnectionInfo(); + if (connectionInfo == null) { + return null; + } + + final int connectionNetworkId = connectionInfo.getNetworkId(); + final List<WifiConfiguration> configs = wifiManager.getConfiguredNetworks(); + for (WifiConfiguration wifiConfiguration : configs) { + if (wifiConfiguration.networkId == connectionNetworkId) { + return WifiNetworkConfig.getValidConfigOrNull( + WifiDppUtils.getSecurityString(wifiConfiguration), + wifiConfiguration.getPrintableSsid(), + wifiConfiguration.preSharedKey, + /* hiddenSsid */ false, + wifiConfiguration.networkId, + /* isHotspot */ false); + } + } + + return null; + } +} diff --git a/src/com/android/settings/wifi/dpp/WifiDppEnrolleeActivity.java b/src/com/android/settings/wifi/dpp/WifiDppEnrolleeActivity.java new file mode 100644 index 0000000000..2229895c6c --- /dev/null +++ b/src/com/android/settings/wifi/dpp/WifiDppEnrolleeActivity.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2018 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.wifi.dpp; + +import android.app.ActionBar; +import android.app.settings.SettingsEnums; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; + +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; + +import com.android.settings.R; +import com.android.settings.core.InstrumentedActivity; + +import com.google.android.setupcompat.util.WizardManagerHelper; + +/** + * To provision "this" device with specified Wi-Fi network. + * + * To use intent action {@code ACTION_ENROLLEE_QR_CODE_SCANNER}, specify the SSID string of the + * Wi-Fi network to be provisioned in {@code WifiDppUtils.EXTRA_WIFI_SSID}. + */ +public class WifiDppEnrolleeActivity extends InstrumentedActivity implements + WifiDppQrCodeScannerFragment.OnScanWifiDppSuccessListener { + private static final String TAG = "WifiDppEnrolleeActivity"; + + public static final String ACTION_ENROLLEE_QR_CODE_SCANNER = + "android.settings.WIFI_DPP_ENROLLEE_QR_CODE_SCANNER"; + + private FragmentManager mFragmentManager; + + @Override + public int getMetricsCategory() { + return SettingsEnums.SETTINGS_WIFI_DPP_ENROLLEE; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (WizardManagerHelper.isAnySetupWizard(getIntent())) { + setTheme(R.style.LightTheme_SettingsBase_SetupWizard); + } + + setContentView(R.layout.wifi_dpp_activity); + mFragmentManager = getSupportFragmentManager(); + + if (savedInstanceState == null) { + handleIntent(getIntent()); + } + + ActionBar actionBar = getActionBar(); + if (actionBar != null) { + actionBar.setElevation(0); + actionBar.setDisplayShowTitleEnabled(false); + } + } + + private void handleIntent(Intent intent) { + switch (intent.getAction()) { + case ACTION_ENROLLEE_QR_CODE_SCANNER: + String ssid = intent.getStringExtra(WifiDppUtils.EXTRA_WIFI_SSID); + showQrCodeScannerFragment(/* addToBackStack */ false, ssid); + break; + default: + Log.e(TAG, "Launch with an invalid action"); + finish(); + } + } + + private void showQrCodeScannerFragment(boolean addToBackStack, String ssid) { + WifiDppQrCodeScannerFragment fragment = + (WifiDppQrCodeScannerFragment) mFragmentManager.findFragmentByTag( + WifiDppUtils.TAG_FRAGMENT_QR_CODE_SCANNER); + + if (fragment == null) { + fragment = new WifiDppQrCodeScannerFragment(ssid); + } else { + if (fragment.isVisible()) { + return; + } + + // When the fragment in back stack but not on top of the stack, we can simply pop + // stack because current fragment transactions are arranged in an order + mFragmentManager.popBackStackImmediate(); + return; + } + final FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction(); + + fragmentTransaction.replace(R.id.fragment_container, fragment, + WifiDppUtils.TAG_FRAGMENT_QR_CODE_SCANNER); + if (addToBackStack) { + fragmentTransaction.addToBackStack(/* name */ null); + } + fragmentTransaction.commit(); + } + + @Override + public boolean onNavigateUp(){ + finish(); + return true; + } + + @Override + public void onScanWifiDppSuccess(WifiQrCode wifiQrCode) { + // Do nothing + } +} diff --git a/src/com/android/settings/wifi/dpp/WifiDppInitiatorViewModel.java b/src/com/android/settings/wifi/dpp/WifiDppInitiatorViewModel.java new file mode 100644 index 0000000000..24e5ebeb24 --- /dev/null +++ b/src/com/android/settings/wifi/dpp/WifiDppInitiatorViewModel.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2019 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.wifi.dpp; + +import android.app.Application; +import android.net.wifi.EasyConnectStatusCallback; +import android.net.wifi.WifiManager; + +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.AndroidViewModel; + +public class WifiDppInitiatorViewModel extends AndroidViewModel { + private MutableLiveData<Integer> mEnrolleeSuccessNetworkId; + private MutableLiveData<Integer> mStatusCode; + private boolean mIsGoingInitiator; + + public WifiDppInitiatorViewModel(Application application) { + super(application); + } + + public MutableLiveData<Integer> getEnrolleeSuccessNetworkId() { + if (mEnrolleeSuccessNetworkId == null) { + mEnrolleeSuccessNetworkId = new MutableLiveData<>(); + } + + return mEnrolleeSuccessNetworkId; + } + + public MutableLiveData<Integer> getStatusCode() { + if (mStatusCode == null) { + mStatusCode = new MutableLiveData<>(); + } + + return mStatusCode; + } + + public boolean isGoingInitiator() { + return mIsGoingInitiator; + } + + public void startEasyConnectAsConfiguratorInitiator(String qrCode, int networkId) { + mIsGoingInitiator = true; + final WifiManager wifiManager = getApplication().getSystemService(WifiManager.class); + + wifiManager.startEasyConnectAsConfiguratorInitiator(qrCode, networkId, + WifiManager.EASY_CONNECT_NETWORK_ROLE_STA, getApplication().getMainExecutor(), + new EasyConnectDelegateCallback()); + } + + public void startEasyConnectAsEnrolleeInitiator(String qrCode) { + mIsGoingInitiator = true; + final WifiManager wifiManager = getApplication().getSystemService(WifiManager.class); + + wifiManager.startEasyConnectAsEnrolleeInitiator(qrCode, getApplication().getMainExecutor(), + new EasyConnectDelegateCallback()); + } + + private class EasyConnectDelegateCallback extends EasyConnectStatusCallback { + @Override + public void onEnrolleeSuccess(int newNetworkId) { + mIsGoingInitiator = false; + mEnrolleeSuccessNetworkId.setValue(newNetworkId); + } + + @Override + public void onConfiguratorSuccess(int code) { + mIsGoingInitiator = false; + mStatusCode.setValue(WifiDppUtils.EASY_CONNECT_EVENT_SUCCESS); + } + + @Override + public void onFailure(int code) { + mIsGoingInitiator = false; + mStatusCode.setValue(code); + } + + @Override + public void onProgress(int code) { + // Do nothing + } + } +} diff --git a/src/com/android/settings/wifi/dpp/WifiDppQrCodeBaseFragment.java b/src/com/android/settings/wifi/dpp/WifiDppQrCodeBaseFragment.java new file mode 100644 index 0000000000..eafbe68f56 --- /dev/null +++ b/src/com/android/settings/wifi/dpp/WifiDppQrCodeBaseFragment.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2018 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.wifi.dpp; + +import android.os.Bundle; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.settings.R; +import com.android.settings.core.InstrumentedFragment; + +/** + * There are below 4 fragments for Wi-Fi DPP UI flow, to reduce redundant code of UI components, + * this parent fragment instantiates common UI components + * + * {@code WifiDppQrCodeScannerFragment} + * {@code WifiDppQrCodeGeneratorFragment} + * {@code WifiDppChooseSavedWifiNetworkFragment} + * {@code WifiDppAddDeviceFragment} + */ +public abstract class WifiDppQrCodeBaseFragment extends InstrumentedFragment { + private ImageView mHeaderIcon; + private ImageView mDevicesCheckCircleGreenHeaderIcon; + protected TextView mTitle; + protected TextView mSummary; + protected View mTitleSummaryContainer; + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + mHeaderIcon = view.findViewById(android.R.id.icon); + mDevicesCheckCircleGreenHeaderIcon = + view.findViewById(R.id.devices_check_circle_green_icon); + mTitle = view.findViewById(android.R.id.title); + mSummary = view.findViewById(android.R.id.summary); + + // This is the LinearLayout which groups mTitle and mSummary for Talkback to announce the + // content in a way that reflects its natural groupings. + mTitleSummaryContainer = view.findViewById(R.id.title_summary_container); + } + + protected void setHeaderIconImageResource(int resId) { + // ic_devices_check_circle_green is a LayerDrawable, + // it has different size from other VectorDrawable icons + if (resId == R.drawable.ic_devices_check_circle_green) { + mHeaderIcon.setVisibility(View.GONE); + mDevicesCheckCircleGreenHeaderIcon.setVisibility(View.VISIBLE); + } else { + mDevicesCheckCircleGreenHeaderIcon.setVisibility(View.GONE); + mHeaderIcon.setImageResource(resId); + mHeaderIcon.setVisibility(View.VISIBLE); + } + } +} diff --git a/src/com/android/settings/wifi/dpp/WifiDppQrCodeGeneratorFragment.java b/src/com/android/settings/wifi/dpp/WifiDppQrCodeGeneratorFragment.java new file mode 100644 index 0000000000..d388931407 --- /dev/null +++ b/src/com/android/settings/wifi/dpp/WifiDppQrCodeGeneratorFragment.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2018 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.wifi.dpp; + +import android.app.ActionBar; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.settings.R; +import com.android.settings.wifi.qrcode.QrCodeGenerator; + +import com.google.zxing.WriterException; + +/** + * After sharing a saved Wi-Fi network, {@code WifiDppConfiguratorActivity} start with this fragment + * to generate a Wi-Fi DPP QR code for other device to initiate as an enrollee. + */ +public class WifiDppQrCodeGeneratorFragment extends WifiDppQrCodeBaseFragment { + private static final String TAG = "WifiDppQrCodeGeneratorFragment"; + + private ImageView mQrCodeView; + private TextView mPasswordView; + private String mQrCode; + + @Override + public int getMetricsCategory() { + return SettingsEnums.SETTINGS_WIFI_DPP_CONFIGURATOR; + } + + // Container Activity must implement this interface + public interface OnQrCodeGeneratorFragmentAddButtonClickedListener { + public void onQrCodeGeneratorFragmentAddButtonClicked(); + } + OnQrCodeGeneratorFragmentAddButtonClickedListener mListener; + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + // setTitle for Talkback + final WifiNetworkConfig wifiNetworkConfig = getWifiNetworkConfigFromHostActivity(); + if (wifiNetworkConfig.isHotspot()) { + getActivity().setTitle(R.string.wifi_dpp_share_hotspot); + } else { + getActivity().setTitle(R.string.wifi_dpp_share_wifi); + } + + setHasOptionsMenu(true); + final ActionBar actionBar = getActivity().getActionBar(); + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.show(); + } + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + + mListener = (OnQrCodeGeneratorFragmentAddButtonClickedListener) context; + } + + @Override + public void onDetach() { + mListener = null; + + super.onDetach(); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + final MenuItem menuItem = menu.findItem(Menu.FIRST); + if (menuItem != null) { + menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); + } + + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public final View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.wifi_dpp_qrcode_generator_fragment, container, + /* attachToRoot */ false); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + mQrCodeView = view.findViewById(R.id.qrcode_view); + + setHeaderIconImageResource(R.drawable.ic_qrcode_24dp); + + final WifiNetworkConfig wifiNetworkConfig = getWifiNetworkConfigFromHostActivity(); + if (wifiNetworkConfig.isHotspot()) { + mTitle.setText(R.string.wifi_dpp_share_hotspot); + } else { + mTitle.setText(R.string.wifi_dpp_share_wifi); + } + + final String password = wifiNetworkConfig.getPreSharedKey(); + mPasswordView = view.findViewById(R.id.password); + if (TextUtils.isEmpty(password)) { + mSummary.setText(getString( + R.string.wifi_dpp_scan_open_network_qr_code_with_another_device, + wifiNetworkConfig.getSsid())); + + mPasswordView.setVisibility(View.GONE); + } else { + mSummary.setText(getString(R.string.wifi_dpp_scan_qr_code_with_another_device, + wifiNetworkConfig.getSsid())); + + if (wifiNetworkConfig.isHotspot()) { + mPasswordView.setText(getString(R.string.wifi_dpp_hotspot_password, password)); + } else { + mPasswordView.setText(getString(R.string.wifi_dpp_wifi_password, password)); + } + } + + mQrCode = wifiNetworkConfig.getQrCode(); + setQrCode(); + } + + private void setQrCode() { + try { + final int qrcodeSize = getContext().getResources().getDimensionPixelSize( + R.dimen.qrcode_size); + final Bitmap bmp = QrCodeGenerator.encodeQrCode(mQrCode, qrcodeSize); + mQrCodeView.setImageBitmap(bmp); + } catch (WriterException e) { + Log.e(TAG, "Error generatting QR code bitmap " + e); + } + } + + WifiNetworkConfig getWifiNetworkConfigFromHostActivity() { + final WifiNetworkConfig wifiNetworkConfig = ((WifiNetworkConfig.Retriever) getActivity()) + .getWifiNetworkConfig(); + if (!WifiNetworkConfig.isValidConfig(wifiNetworkConfig)) { + throw new IllegalStateException("Invalid Wi-Fi network for configuring"); + } + + return wifiNetworkConfig; + } +} diff --git a/src/com/android/settings/wifi/dpp/WifiDppQrCodeScannerFragment.java b/src/com/android/settings/wifi/dpp/WifiDppQrCodeScannerFragment.java new file mode 100644 index 0000000000..d595f793fe --- /dev/null +++ b/src/com/android/settings/wifi/dpp/WifiDppQrCodeScannerFragment.java @@ -0,0 +1,740 @@ +/* + * Copyright (C) 2018 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.wifi.dpp; + +import android.app.ActionBar; +import android.app.Activity; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.Intent; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.SurfaceTexture; +import android.net.wifi.EasyConnectStatusCallback; +import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiManager; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.text.TextUtils; +import android.util.Log; +import android.util.Size; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.TextureView; +import android.view.TextureView.SurfaceTextureListener; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.annotation.StringRes; +import androidx.annotation.UiThread; +import androidx.annotation.VisibleForTesting; +import androidx.lifecycle.ViewModelProviders; + +import com.android.settings.R; +import com.android.settings.wifi.WifiDialogActivity; +import com.android.settings.wifi.qrcode.QrCamera; +import com.android.settings.wifi.qrcode.QrDecorateView; + +import com.android.settingslib.wifi.AccessPoint; +import com.android.settingslib.wifi.WifiTracker; +import com.android.settingslib.wifi.WifiTrackerFactory; + +import java.util.ArrayList; +import java.util.List; + +public class WifiDppQrCodeScannerFragment extends WifiDppQrCodeBaseFragment implements + SurfaceTextureListener, + QrCamera.ScannerCallback, + WifiManager.ActionListener, + WifiTracker.WifiListener { + private static final String TAG = "WifiDppQrCodeScanner"; + + /** Message sent to hide error message */ + private static final int MESSAGE_HIDE_ERROR_MESSAGE = 1; + + /** Message sent to show error message */ + private static final int MESSAGE_SHOW_ERROR_MESSAGE = 2; + + /** Message sent to manipulate Wi-Fi DPP QR code */ + private static final int MESSAGE_SCAN_WIFI_DPP_SUCCESS = 3; + + /** Message sent to manipulate ZXing Wi-Fi QR code */ + private static final int MESSAGE_SCAN_ZXING_WIFI_FORMAT_SUCCESS = 4; + + private static final long SHOW_ERROR_MESSAGE_INTERVAL = 10000; + private static final long SHOW_SUCCESS_SQUARE_INTERVAL = 1000; + + // Key for Bundle usage + private static final String KEY_IS_CONFIGURATOR_MODE = "key_is_configurator_mode"; + private static final String KEY_LATEST_ERROR_CODE = "key_latest_error_code"; + private static final String KEY_WIFI_CONFIGURATION = "key_wifi_configuration"; + + private static final int ARG_RESTART_CAMERA = 1; + + private ProgressBar mProgressBar; + private QrCamera mCamera; + private TextureView mTextureView; + private QrDecorateView mDecorateView; + private TextView mErrorMessage; + + /** true if the fragment working for configurator, false enrollee*/ + private boolean mIsConfiguratorMode; + + /** The SSID of the Wi-Fi network which the user specify to enroll */ + private String mSsid; + + /** QR code data scanned by camera */ + private WifiQrCode mWifiQrCode; + + /** The WifiConfiguration connecting for enrollee usage */ + private WifiConfiguration mEnrolleeWifiConfiguration; + + private int mLatestStatusCode = WifiDppUtils.EASY_CONNECT_EVENT_FAILURE_NONE; + + private WifiTracker mWifiTracker; + + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MESSAGE_HIDE_ERROR_MESSAGE: + mErrorMessage.setVisibility(View.INVISIBLE); + break; + + case MESSAGE_SHOW_ERROR_MESSAGE: + final String errorMessage = (String) msg.obj; + + mErrorMessage.setVisibility(View.VISIBLE); + mErrorMessage.setText(errorMessage); + mErrorMessage.sendAccessibilityEvent( + AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + + // Cancel any pending messages to hide error view and requeue the message so + // user has time to see error + removeMessages(MESSAGE_HIDE_ERROR_MESSAGE); + sendEmptyMessageDelayed(MESSAGE_HIDE_ERROR_MESSAGE, + SHOW_ERROR_MESSAGE_INTERVAL); + + if (msg.arg1 == ARG_RESTART_CAMERA) { + mProgressBar.setVisibility(View.INVISIBLE); + mDecorateView.setFocused(false); + restartCamera(); + } + break; + + case MESSAGE_SCAN_WIFI_DPP_SUCCESS: + if (mScanWifiDppSuccessListener == null) { + // mScanWifiDppSuccessListener may be null after onDetach(), do nothing here + return; + } + mScanWifiDppSuccessListener.onScanWifiDppSuccess((WifiQrCode)msg.obj); + + if (!mIsConfiguratorMode) { + mProgressBar.setVisibility(View.VISIBLE); + startWifiDppEnrolleeInitiator((WifiQrCode)msg.obj); + updateEnrolleeSummary(); + mSummary.sendAccessibilityEvent( + AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + } + + notifyUserForQrCodeRecognition(); + break; + + case MESSAGE_SCAN_ZXING_WIFI_FORMAT_SUCCESS: + // We may get 2 WifiConfiguration if the QR code has no password in it, + // one for open network and one for enhanced open network. + final WifiManager wifiManager = + getContext().getSystemService(WifiManager.class); + final WifiNetworkConfig qrCodeWifiNetworkConfig = + (WifiNetworkConfig)msg.obj; + final List<WifiConfiguration> qrCodeWifiConfigurations = + qrCodeWifiNetworkConfig.getWifiConfigurations(); + + // Adds all Wi-Fi networks in QR code to the set of configured networks and + // connects to it if it's reachable. + boolean hasReachableWifiNetwork = false; + for (WifiConfiguration qrCodeWifiConfiguration : qrCodeWifiConfigurations) { + final int id = wifiManager.addNetwork(qrCodeWifiConfiguration); + if (id == -1) { + continue; + } + wifiManager.enableNetwork(id, /* attemptConnect */ false); + if (isReachableWifiNetwork(qrCodeWifiConfiguration)) { + hasReachableWifiNetwork = true; + mEnrolleeWifiConfiguration = qrCodeWifiConfiguration; + wifiManager.connect(id, + /* listener */ WifiDppQrCodeScannerFragment.this); + } + } + + if (hasReachableWifiNetwork == false) { + showErrorMessageAndRestartCamera( + R.string.wifi_dpp_check_connection_try_again); + return; + } + + mMetricsFeatureProvider.action( + mMetricsFeatureProvider.getAttribution(getActivity()), + SettingsEnums.ACTION_SETTINGS_ENROLL_WIFI_QR_CODE, + SettingsEnums.SETTINGS_WIFI_DPP_ENROLLEE, + /* key */ null, + /* value */ Integer.MIN_VALUE); + + notifyUserForQrCodeRecognition(); + break; + + default: + return; + } + } + }; + + @UiThread + private void notifyUserForQrCodeRecognition() { + if (mCamera != null) { + mCamera.stop(); + } + + mDecorateView.setFocused(true); + mErrorMessage.setVisibility(View.INVISIBLE); + + WifiDppUtils.triggerVibrationForQrCodeRecognition(getContext()); + } + + private boolean isReachableWifiNetwork(WifiConfiguration wifiConfiguration) { + final List<AccessPoint> scannedAccessPoints = mWifiTracker.getAccessPoints(); + + for (AccessPoint scannedAccessPoint : scannedAccessPoints) { + if (scannedAccessPoint.matches(wifiConfiguration) && + scannedAccessPoint.isReachable()) { + return true; + } + } + return false; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (savedInstanceState != null) { + mIsConfiguratorMode = savedInstanceState.getBoolean(KEY_IS_CONFIGURATOR_MODE); + mLatestStatusCode = savedInstanceState.getInt(KEY_LATEST_ERROR_CODE); + mEnrolleeWifiConfiguration = savedInstanceState.getParcelable(KEY_WIFI_CONFIGURATION); + } + + final WifiDppInitiatorViewModel model = + ViewModelProviders.of(this).get(WifiDppInitiatorViewModel.class); + + model.getEnrolleeSuccessNetworkId().observe(this, networkId -> { + // After configuration change, observe callback will be triggered, + // do nothing for this case if a handshake does not end + if (model.isGoingInitiator()) { + return; + } + + new EasyConnectEnrolleeStatusCallback().onEnrolleeSuccess(networkId.intValue()); + }); + + model.getStatusCode().observe(this, statusCode -> { + // After configuration change, observe callback will be triggered, + // do nothing for this case if a handshake does not end + if (model.isGoingInitiator()) { + return; + } + + int code = statusCode.intValue(); + Log.d(TAG, "Easy connect enrollee callback onFailure " + code); + new EasyConnectEnrolleeStatusCallback().onFailure(code); + }); + } + + @Override + public void onPause() { + if (mCamera != null) { + mCamera.stop(); + } + + super.onPause(); + } + + @Override + public void onResume() { + super.onResume(); + + if (!isGoingInitiator()) { + restartCamera(); + } + } + + @Override + public int getMetricsCategory() { + if (mIsConfiguratorMode) { + return SettingsEnums.SETTINGS_WIFI_DPP_CONFIGURATOR; + } else { + return SettingsEnums.SETTINGS_WIFI_DPP_ENROLLEE; + } + } + + // Container Activity must implement this interface + public interface OnScanWifiDppSuccessListener { + public void onScanWifiDppSuccess(WifiQrCode wifiQrCode); + } + OnScanWifiDppSuccessListener mScanWifiDppSuccessListener; + + /** + * Configurator container activity of the fragment should create instance with this constructor. + */ + public WifiDppQrCodeScannerFragment() { + super(); + + mIsConfiguratorMode = true; + } + + /** + * Enrollee container activity of the fragment should create instance with this constructor and + * specify the SSID string of the WI-Fi network to be provisioned. + */ + public WifiDppQrCodeScannerFragment(String ssid) { + super(); + + mIsConfiguratorMode = false; + mSsid = ssid; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + mWifiTracker = WifiTrackerFactory.create(getActivity(), /* wifiListener */ this, + getSettingsLifecycle(), /* includeSaved */ false, /* includeScans */ true); + + // setTitle for Talkback + if (mIsConfiguratorMode) { + getActivity().setTitle(R.string.wifi_dpp_add_device_to_network); + } else { + getActivity().setTitle(R.string.wifi_dpp_scan_qr_code); + } + + final ActionBar actionBar = getActivity().getActionBar(); + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.show(); + } + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + + mScanWifiDppSuccessListener = (OnScanWifiDppSuccessListener) context; + } + + @Override + public void onDetach() { + mScanWifiDppSuccessListener = null; + + super.onDetach(); + } + + @Override + public final View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.wifi_dpp_qrcode_scanner_fragment, container, + /* attachToRoot */ false); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + mTextureView = (TextureView) view.findViewById(R.id.preview_view); + mTextureView.setSurfaceTextureListener(this); + + mDecorateView = (QrDecorateView) view.findViewById(R.id.decorate_view); + + setHeaderIconImageResource(R.drawable.ic_scan_24dp); + + mProgressBar = view.findViewById(R.id.indeterminate_bar); + mProgressBar.setVisibility(isGoingInitiator() ? View.VISIBLE : View.INVISIBLE); + + if (mIsConfiguratorMode) { + mTitle.setText(R.string.wifi_dpp_add_device_to_network); + + WifiNetworkConfig wifiNetworkConfig = ((WifiNetworkConfig.Retriever) getActivity()) + .getWifiNetworkConfig(); + if (!WifiNetworkConfig.isValidConfig(wifiNetworkConfig)) { + throw new IllegalStateException("Invalid Wi-Fi network for configuring"); + } + mSummary.setText(getString(R.string.wifi_dpp_center_qr_code, + wifiNetworkConfig.getSsid())); + } else { + mTitle.setText(R.string.wifi_dpp_scan_qr_code); + + updateEnrolleeSummary(); + } + + mErrorMessage = view.findViewById(R.id.error_message); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + menu.removeItem(Menu.FIRST); + + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { + initCamera(surface); + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { + // Do nothing + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { + destroyCamera(); + return true; + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surface) { + // Do nothing + } + + @Override + public Size getViewSize() { + return new Size(mTextureView.getWidth(), mTextureView.getHeight()); + } + + @Override + public Rect getFramePosition(Size previewSize, int cameraOrientation) { + return new Rect(0, 0, previewSize.getHeight(), previewSize.getHeight()); + } + + @Override + public void setTransform(Matrix transform) { + mTextureView.setTransform(transform); + } + + @Override + public boolean isValid(String qrCode) { + try { + mWifiQrCode = new WifiQrCode(qrCode); + } catch (IllegalArgumentException e) { + showErrorMessage(R.string.wifi_dpp_could_not_detect_valid_qr_code); + return false; + } + + final String scheme = mWifiQrCode.getScheme(); + + // When SSID is specified for enrollee, avoid to connect to the Wi-Fi of different SSID + if (!mIsConfiguratorMode && WifiQrCode.SCHEME_ZXING_WIFI_NETWORK_CONFIG.equals(scheme)) { + final String ssidQrCode = mWifiQrCode.getWifiNetworkConfig().getSsid(); + if (!TextUtils.isEmpty(mSsid) && !mSsid.equals(ssidQrCode)) { + showErrorMessage(R.string.wifi_dpp_could_not_detect_valid_qr_code); + return false; + } + } + + // It's impossible to provision other device with ZXing Wi-Fi Network config format + if (mIsConfiguratorMode && WifiQrCode.SCHEME_ZXING_WIFI_NETWORK_CONFIG.equals(scheme)) { + showErrorMessage(R.string.wifi_dpp_could_not_detect_valid_qr_code); + return false; + } + + return true; + } + + /** + * This method is only called when QrCamera.ScannerCallback.isValid returns true; + */ + @Override + public void handleSuccessfulResult(String qrCode) { + switch (mWifiQrCode.getScheme()) { + case WifiQrCode.SCHEME_DPP: + handleWifiDpp(); + break; + + case WifiQrCode.SCHEME_ZXING_WIFI_NETWORK_CONFIG: + handleZxingWifiFormat(); + break; + + default: + // continue below + } + } + + private void handleWifiDpp() { + Message message = mHandler.obtainMessage(MESSAGE_SCAN_WIFI_DPP_SUCCESS); + message.obj = new WifiQrCode(mWifiQrCode.getQrCode()); + + mHandler.sendMessageDelayed(message, SHOW_SUCCESS_SQUARE_INTERVAL); + } + + private void handleZxingWifiFormat() { + Message message = mHandler.obtainMessage(MESSAGE_SCAN_ZXING_WIFI_FORMAT_SUCCESS); + message.obj = new WifiQrCode(mWifiQrCode.getQrCode()).getWifiNetworkConfig(); + + mHandler.sendMessageDelayed(message, SHOW_SUCCESS_SQUARE_INTERVAL); + } + + @Override + public void handleCameraFailure() { + destroyCamera(); + } + + private void initCamera(SurfaceTexture surface) { + // Check if the camera has already created. + if (mCamera == null) { + mCamera = new QrCamera(getContext(), this); + + if (isGoingInitiator()) { + if (mDecorateView != null) { + mDecorateView.setFocused(true); + } + } else { + mCamera.start(surface); + } + } + } + + private void destroyCamera() { + if (mCamera != null) { + mCamera.stop(); + mCamera = null; + } + } + + private void showErrorMessage(@StringRes int messageResId) { + final Message message = mHandler.obtainMessage(MESSAGE_SHOW_ERROR_MESSAGE, + getString(messageResId)); + message.sendToTarget(); + } + + private void showErrorMessageAndRestartCamera(@StringRes int messageResId) { + final Message message = mHandler.obtainMessage(MESSAGE_SHOW_ERROR_MESSAGE, + getString(messageResId)); + message.arg1 = ARG_RESTART_CAMERA; + message.sendToTarget(); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + outState.putBoolean(KEY_IS_CONFIGURATOR_MODE, mIsConfiguratorMode); + outState.putInt(KEY_LATEST_ERROR_CODE, mLatestStatusCode); + outState.putParcelable(KEY_WIFI_CONFIGURATION, mEnrolleeWifiConfiguration); + + super.onSaveInstanceState(outState); + } + + private class EasyConnectEnrolleeStatusCallback extends EasyConnectStatusCallback { + @Override + public void onEnrolleeSuccess(int newNetworkId) { + + // Connect to the new network. + final WifiManager wifiManager = getContext().getSystemService(WifiManager.class); + final List<WifiConfiguration> wifiConfigs = + wifiManager.getPrivilegedConfiguredNetworks(); + for (WifiConfiguration wifiConfig : wifiConfigs) { + if (wifiConfig.networkId == newNetworkId) { + mLatestStatusCode = WifiDppUtils.EASY_CONNECT_EVENT_SUCCESS; + mEnrolleeWifiConfiguration = wifiConfig; + wifiManager.connect(wifiConfig, WifiDppQrCodeScannerFragment.this); + return; + } + } + + Log.e(TAG, "Invalid networkId " + newNetworkId); + mLatestStatusCode = EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_GENERIC; + updateEnrolleeSummary(); + showErrorMessageAndRestartCamera(R.string.wifi_dpp_check_connection_try_again); + } + + @Override + public void onConfiguratorSuccess(int code) { + // Do nothing + } + + @Override + public void onFailure(int code) { + Log.d(TAG, "EasyConnectEnrolleeStatusCallback.onFailure " + code); + + int errorMessageResId = 0; + switch (code) { + case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_INVALID_URI: + errorMessageResId = R.string.wifi_dpp_could_not_detect_valid_qr_code; + break; + + case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_AUTHENTICATION: + errorMessageResId = R.string.wifi_dpp_failure_authentication_or_configuration; + break; + + case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_NOT_COMPATIBLE: + errorMessageResId = R.string.wifi_dpp_failure_not_compatible; + break; + + case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_CONFIGURATION: + errorMessageResId = R.string.wifi_dpp_failure_authentication_or_configuration; + break; + + case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_BUSY: + if (code == mLatestStatusCode) { + throw(new IllegalStateException("stopEasyConnectSession and try again for" + + "EASY_CONNECT_EVENT_FAILURE_BUSY but still failed")); + } + + mLatestStatusCode = code; + final WifiManager wifiManager = + getContext().getSystemService(WifiManager.class); + wifiManager.stopEasyConnectSession(); + startWifiDppEnrolleeInitiator(mWifiQrCode); + return; + + case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_TIMEOUT: + errorMessageResId = R.string.wifi_dpp_failure_timeout; + break; + + case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_GENERIC: + errorMessageResId = R.string.wifi_dpp_failure_generic; + break; + + case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_NOT_SUPPORTED: + throw(new IllegalStateException("EASY_CONNECT_EVENT_FAILURE_NOT_SUPPORTED" + + " should be a configurator only error")); + + case EasyConnectStatusCallback.EASY_CONNECT_EVENT_FAILURE_INVALID_NETWORK: + throw(new IllegalStateException("EASY_CONNECT_EVENT_FAILURE_INVALID_NETWORK" + + " should be a configurator only error")); + + default: + throw(new IllegalStateException("Unexpected Wi-Fi DPP error")); + } + + mLatestStatusCode = code; + updateEnrolleeSummary(); + showErrorMessageAndRestartCamera(errorMessageResId); + } + + @Override + public void onProgress(int code) { + // Do nothing + } + } + + private void startWifiDppEnrolleeInitiator(WifiQrCode wifiQrCode) { + final WifiDppInitiatorViewModel model = + ViewModelProviders.of(this).get(WifiDppInitiatorViewModel.class); + + model.startEasyConnectAsEnrolleeInitiator(wifiQrCode.getQrCode()); + } + + @Override + public void onSuccess() { + final Intent resultIntent = new Intent(); + resultIntent.putExtra(WifiDialogActivity.KEY_WIFI_CONFIGURATION, + mEnrolleeWifiConfiguration); + + final Activity hostActivity = getActivity(); + hostActivity.setResult(Activity.RESULT_OK, resultIntent); + hostActivity.finish(); + } + + @Override + public void onFailure(int reason) { + Log.d(TAG, "Wi-Fi connect onFailure reason - " + reason); + showErrorMessageAndRestartCamera(R.string.wifi_dpp_check_connection_try_again); + } + + // Check is Easy Connect handshaking or not + private boolean isGoingInitiator() { + final WifiDppInitiatorViewModel model = + ViewModelProviders.of(this).get(WifiDppInitiatorViewModel.class); + + return model.isGoingInitiator(); + } + + /** + * To resume camera decoding task after handshake fail or Wi-Fi connection fail. + */ + private void restartCamera() { + if (mCamera == null) { + Log.d(TAG, "mCamera is not available for restarting camera"); + return; + } + + if (mCamera.isDecodeTaskAlive()) { + mCamera.stop(); + } + + final SurfaceTexture surfaceTexture = mTextureView.getSurfaceTexture(); + if (surfaceTexture == null) { + throw new IllegalStateException("SurfaceTexture is not ready for restarting camera"); + } + + mCamera.start(surfaceTexture); + } + + private void updateEnrolleeSummary() { + if (isGoingInitiator()) { + mSummary.setText(R.string.wifi_dpp_connecting); + } else { + String description; + if (TextUtils.isEmpty(mSsid)) { + description = getString(R.string.wifi_dpp_scan_qr_code_join_unknown_network, mSsid); + } else { + description = getString(R.string.wifi_dpp_scan_qr_code_join_network, mSsid); + } + mSummary.setText(description); + } + } + + /** Called when the state of Wifi has changed. */ + @Override + public void onWifiStateChanged(int state) { + // Do nothing. + } + + /** Called when the connection state of wifi has changed. */ + @Override + public void onConnectedChanged() { + // Do nothing. + } + + /** + * Called to indicate the list of AccessPoints has been updated and + * getAccessPoints should be called to get the latest information. + */ + @Override + public void onAccessPointsChanged() { + // Do nothing. + } + + @VisibleForTesting + protected boolean isDecodeTaskAlive() { + return mCamera != null && mCamera.isDecodeTaskAlive(); + } +} diff --git a/src/com/android/settings/wifi/dpp/WifiDppUtils.java b/src/com/android/settings/wifi/dpp/WifiDppUtils.java new file mode 100644 index 0000000000..7e15064b25 --- /dev/null +++ b/src/com/android/settings/wifi/dpp/WifiDppUtils.java @@ -0,0 +1,448 @@ +/* + * Copyright (C) 2018 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.wifi.dpp; + +import android.app.KeyguardManager; +import android.content.Context; +import android.content.Intent; +import android.hardware.biometrics.BiometricPrompt; +import android.net.wifi.WifiConfiguration.KeyMgmt; +import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiManager; +import android.os.CancellationSignal; +import android.os.Handler; +import android.os.Looper; +import android.os.VibrationEffect; +import android.os.Vibrator; +import android.text.TextUtils; +import android.util.FeatureFlagUtils; + +import com.android.settings.R; + +import com.android.settingslib.wifi.AccessPoint; + +import java.util.List; + +import java.time.Duration; + +/** + * Here are the items shared by both WifiDppConfiguratorActivity & WifiDppEnrolleeActivity + * + * @see WifiQrCode + */ +public class WifiDppUtils { + /** + * The fragment tag specified to FragmentManager for container activities to manage fragments. + */ + public static final String TAG_FRAGMENT_QR_CODE_SCANNER = "qr_code_scanner_fragment"; + + /** + * @see #TAG_FRAGMENT_QR_CODE_SCANNER + */ + public static final String TAG_FRAGMENT_QR_CODE_GENERATOR = "qr_code_generator_fragment"; + + /** + * @see #TAG_FRAGMENT_QR_CODE_SCANNER + */ + public static final String TAG_FRAGMENT_CHOOSE_SAVED_WIFI_NETWORK = + "choose_saved_wifi_network_fragment"; + + /** + * @see #TAG_FRAGMENT_QR_CODE_SCANNER + */ + public static final String TAG_FRAGMENT_ADD_DEVICE = "add_device_fragment"; + + /** The data is from {@code com.android.settingslib.wifi.AccessPoint.securityToString} */ + public static final String EXTRA_WIFI_SECURITY = "security"; + + /** The data corresponding to {@code WifiConfiguration} SSID */ + public static final String EXTRA_WIFI_SSID = "ssid"; + + /** The data corresponding to {@code WifiConfiguration} preSharedKey */ + public static final String EXTRA_WIFI_PRE_SHARED_KEY = "preSharedKey"; + + /** The data corresponding to {@code WifiConfiguration} hiddenSSID */ + public static final String EXTRA_WIFI_HIDDEN_SSID = "hiddenSsid"; + + /** The data corresponding to {@code WifiConfiguration} networkId */ + public static final String EXTRA_WIFI_NETWORK_ID = "networkId"; + + /** The data to recognize if it's a Wi-Fi hotspot for configuration */ + public static final String EXTRA_IS_HOTSPOT = "isHotspot"; + + /** Used by {@link android.provider.Settings#ACTION_PROCESS_WIFI_EASY_CONNECT_URI} to + * indicate test mode UI should be shown. Test UI does not make API calls. Value is a boolean.*/ + public static final String EXTRA_TEST = "test"; + + /** + * Default status code for Easy Connect + */ + public static final int EASY_CONNECT_EVENT_FAILURE_NONE = 0; + + /** + * Success status code for Easy Connect. + */ + public static final int EASY_CONNECT_EVENT_SUCCESS = 1; + + private static final Duration VIBRATE_DURATION_QR_CODE_RECOGNITION = Duration.ofMillis(3); + + /** + * Returns whether the device support WiFi DPP. + */ + public static boolean isWifiDppEnabled(Context context) { + final WifiManager manager = context.getSystemService(WifiManager.class); + return manager.isEasyConnectSupported(); + } + + /** + * Returns an intent to launch QR code scanner for Wi-Fi DPP enrollee. + * + * After enrollee success, the callee activity will return connecting WifiConfiguration by + * putExtra {@code WifiDialogActivity.KEY_WIFI_CONFIGURATION} for + * {@code Activity#setResult(int resultCode, Intent data)}. The calling object should check + * if it's available before using it. + * + * @param ssid The data corresponding to {@code WifiConfiguration} SSID + * @return Intent for launching QR code scanner + */ + public static Intent getEnrolleeQrCodeScannerIntent(String ssid) { + final Intent intent = new Intent( + WifiDppEnrolleeActivity.ACTION_ENROLLEE_QR_CODE_SCANNER); + if (!TextUtils.isEmpty(ssid)) { + intent.putExtra(EXTRA_WIFI_SSID, ssid); + } + return intent; + } + + private static String getPresharedKey(WifiManager wifiManager, + WifiConfiguration wifiConfiguration) { + final List<WifiConfiguration> privilegedWifiConfiguratios = + wifiManager.getPrivilegedConfiguredNetworks(); + + for (WifiConfiguration privilegedWifiConfiguration : privilegedWifiConfiguratios) { + if (privilegedWifiConfiguration.networkId == wifiConfiguration.networkId) { + // WEP uses a shared key hence the AuthAlgorithm.SHARED is used + // to identify it. + if (wifiConfiguration.allowedKeyManagement.get(KeyMgmt.NONE) + && wifiConfiguration.allowedAuthAlgorithms.get( + WifiConfiguration.AuthAlgorithm.SHARED)) { + return privilegedWifiConfiguration + .wepKeys[privilegedWifiConfiguration.wepTxKeyIndex]; + } else { + return privilegedWifiConfiguration.preSharedKey; + } + } + } + return wifiConfiguration.preSharedKey; + } + + private static String removeFirstAndLastDoubleQuotes(String str) { + if (TextUtils.isEmpty(str)) { + return str; + } + + int begin = 0; + int end = str.length() - 1; + if (str.charAt(begin) == '\"') { + begin++; + } + if (str.charAt(end) == '\"') { + end--; + } + return str.substring(begin, end+1); + } + + static String getSecurityString(WifiConfiguration config) { + if (config.allowedKeyManagement.get(KeyMgmt.SAE)) { + return WifiQrCode.SECURITY_SAE; + } + if (config.allowedKeyManagement.get(KeyMgmt.OWE)) { + return WifiQrCode.SECURITY_NO_PASSWORD; + } + if (config.allowedKeyManagement.get(KeyMgmt.WPA_PSK) || + config.allowedKeyManagement.get(KeyMgmt.WPA2_PSK)) { + return WifiQrCode.SECURITY_WPA_PSK; + } + return (config.wepKeys[0] == null) ? + WifiQrCode.SECURITY_NO_PASSWORD : WifiQrCode.SECURITY_WEP; + } + + /** + * Returns an intent to launch QR code generator. It may return null if the security is not + * supported by QR code generator. + * + * Do not use this method for Wi-Fi hotspot network, use + * {@code getHotspotConfiguratorIntentOrNull} instead. + * + * @param context The context to use for the content resolver + * @param wifiManager An instance of {@link WifiManager} + * @param accessPoint An instance of {@link AccessPoint} + * @return Intent for launching QR code generator + */ + public static Intent getConfiguratorQrCodeGeneratorIntentOrNull(Context context, + WifiManager wifiManager, AccessPoint accessPoint) { + final Intent intent = new Intent(context, WifiDppConfiguratorActivity.class); + if (isSupportConfiguratorQrCodeGenerator(context, accessPoint)) { + intent.setAction(WifiDppConfiguratorActivity.ACTION_CONFIGURATOR_QR_CODE_GENERATOR); + } else { + return null; + } + + final WifiConfiguration wifiConfiguration = accessPoint.getConfig(); + setConfiguratorIntentExtra(intent, wifiManager, wifiConfiguration); + + // For a transition mode Wi-Fi AP, creates a QR code that's compatible with more devices + if (accessPoint.getSecurity() == AccessPoint.SECURITY_PSK_SAE_TRANSITION) { + intent.putExtra(EXTRA_WIFI_SECURITY, WifiQrCode.SECURITY_WPA_PSK); + } + + return intent; + } + + /** + * Returns an intent to launch QR code scanner. It may return null if the security is not + * supported by QR code scanner. + * + * @param context The context to use for the content resolver + * @param wifiManager An instance of {@link WifiManager} + * @param accessPoint An instance of {@link AccessPoint} + * @return Intent for launching QR code scanner + */ + public static Intent getConfiguratorQrCodeScannerIntentOrNull(Context context, + WifiManager wifiManager, AccessPoint accessPoint) { + final Intent intent = new Intent(context, WifiDppConfiguratorActivity.class); + if (isSupportConfiguratorQrCodeScanner(context, accessPoint)) { + intent.setAction(WifiDppConfiguratorActivity.ACTION_CONFIGURATOR_QR_CODE_SCANNER); + } else { + return null; + } + + final WifiConfiguration wifiConfiguration = accessPoint.getConfig(); + setConfiguratorIntentExtra(intent, wifiManager, wifiConfiguration); + + if (wifiConfiguration.networkId == WifiConfiguration.INVALID_NETWORK_ID) { + throw new IllegalArgumentException("Invalid network ID"); + } else { + intent.putExtra(EXTRA_WIFI_NETWORK_ID, wifiConfiguration.networkId); + } + + return intent; + } + + /** + * Returns an intent to launch QR code generator for the Wi-Fi hotspot. It may return null if + * the security is not supported by QR code generator. + * + * @param context The context to use for the content resolver + * @param wifiManager An instance of {@link WifiManager} + * @param wifiConfiguration {@link WifiConfiguration} of the Wi-Fi hotspot + * @return Intent for launching QR code generator + */ + public static Intent getHotspotConfiguratorIntentOrNull(Context context, + WifiManager wifiManager, WifiConfiguration wifiConfiguration) { + final Intent intent = new Intent(context, WifiDppConfiguratorActivity.class); + if (isSupportHotspotConfiguratorQrCodeGenerator(wifiConfiguration)) { + intent.setAction(WifiDppConfiguratorActivity.ACTION_CONFIGURATOR_QR_CODE_GENERATOR); + } else { + return null; + } + + setConfiguratorIntentExtra(intent, wifiManager, wifiConfiguration); + + intent.putExtra(EXTRA_WIFI_NETWORK_ID, WifiConfiguration.INVALID_NETWORK_ID); + intent.putExtra(EXTRA_IS_HOTSPOT, true); + + return intent; + } + + /** + * Set all extra except {@code EXTRA_WIFI_NETWORK_ID} for the intent to + * launch configurator activity later. + * + * @param intent the target to set extra + * @param wifiManager an instance of {@code WifiManager} + * @param wifiConfiguration the Wi-Fi network for launching configurator activity + */ + private static void setConfiguratorIntentExtra(Intent intent, WifiManager wifiManager, + WifiConfiguration wifiConfiguration) { + final String ssid = removeFirstAndLastDoubleQuotes(wifiConfiguration.SSID); + final String security = getSecurityString(wifiConfiguration); + + // When the value of this key is read, the actual key is not returned, just a "*". + // Call privileged system API to obtain actual key. + final String preSharedKey = removeFirstAndLastDoubleQuotes(getPresharedKey(wifiManager, + wifiConfiguration)); + + if (!TextUtils.isEmpty(ssid)) { + intent.putExtra(EXTRA_WIFI_SSID, ssid); + } + if (!TextUtils.isEmpty(security)) { + intent.putExtra(EXTRA_WIFI_SECURITY, security); + } + if (!TextUtils.isEmpty(preSharedKey)) { + intent.putExtra(EXTRA_WIFI_PRE_SHARED_KEY, preSharedKey); + } + } + + /** + * Shows authentication screen to confirm credentials (pin, pattern or password) for the current + * user of the device. + * + * @param context The {@code Context} used to get {@code KeyguardManager} service + * @param successRunnable The {@code Runnable} which will be executed if the user does not setup + * device security or if lock screen is unlocked + */ + public static void showLockScreen(Context context, Runnable successRunnable) { + final KeyguardManager keyguardManager = (KeyguardManager) context.getSystemService( + Context.KEYGUARD_SERVICE); + + if (keyguardManager.isKeyguardSecure()) { + final BiometricPrompt.AuthenticationCallback authenticationCallback = + new BiometricPrompt.AuthenticationCallback() { + @Override + public void onAuthenticationSucceeded( + BiometricPrompt.AuthenticationResult result) { + successRunnable.run(); + } + + @Override + public void onAuthenticationError(int errorCode, CharSequence errString) { + //Do nothing + } + }; + + final BiometricPrompt.Builder builder = new BiometricPrompt.Builder(context) + .setTitle(context.getText(R.string.wifi_dpp_lockscreen_title)); + + if (keyguardManager.isDeviceSecure()) { + builder.setDeviceCredentialAllowed(true); + } + + final BiometricPrompt bp = builder.build(); + final Handler handler = new Handler(Looper.getMainLooper()); + bp.authenticate(new CancellationSignal(), + runnable -> handler.post(runnable), + authenticationCallback); + } else { + successRunnable.run(); + } + } + + /** + * Checks if QR code scanner supports to config other devices with the Wi-Fi network + * + * @param context The context to use for {@link WifiManager#isEasyConnectSupported()} + * @param accessPoint The {@link AccessPoint} of the Wi-Fi network + */ + public static boolean isSupportConfiguratorQrCodeScanner(Context context, + AccessPoint accessPoint) { + if (accessPoint.isPasspoint()) { + return false; + } + return isSupportWifiDpp(context, accessPoint.getSecurity()); + } + + /** + * Checks if QR code generator supports to config other devices with the Wi-Fi network + * + * @param context The context to use for {@code WifiManager} + * @param accessPoint The {@link AccessPoint} of the Wi-Fi network + */ + public static boolean isSupportConfiguratorQrCodeGenerator(Context context, + AccessPoint accessPoint) { + if (accessPoint.isPasspoint()) { + return false; + } + return isSupportZxing(context, accessPoint.getSecurity()); + } + + /** + * Checks if this device supports to be configured by the Wi-Fi network of the security + * + * @param context The context to use for {@code WifiManager} + * @param accesspointSecurity The security constants defined in {@link AccessPoint} + */ + public static boolean isSupportEnrolleeQrCodeScanner(Context context, + int accesspointSecurity) { + return isSupportWifiDpp(context, accesspointSecurity) || + isSupportZxing(context, accesspointSecurity); + } + + private static boolean isSupportHotspotConfiguratorQrCodeGenerator( + WifiConfiguration wifiConfiguration) { + // QR code generator produces QR code with ZXing's Wi-Fi network config format, + // it supports PSK and WEP and non security + // KeyMgmt.NONE is for WEP or non security + return wifiConfiguration.allowedKeyManagement.get(KeyMgmt.WPA2_PSK) || + wifiConfiguration.allowedKeyManagement.get(KeyMgmt.NONE); + } + + private static boolean isSupportWifiDpp(Context context, int accesspointSecurity) { + if (!isWifiDppEnabled(context)) { + return false; + } + + // DPP 1.0 only supports SAE and PSK. + final WifiManager wifiManager = context.getSystemService(WifiManager.class); + switch (accesspointSecurity) { + case AccessPoint.SECURITY_SAE: + if (wifiManager.isWpa3SaeSupported()) { + return true; + } + break; + case AccessPoint.SECURITY_PSK: + case AccessPoint.SECURITY_PSK_SAE_TRANSITION: + return true; + default: + } + return false; + } + + private static boolean isSupportZxing(Context context, int accesspointSecurity) { + final WifiManager wifiManager = context.getSystemService(WifiManager.class); + switch (accesspointSecurity) { + case AccessPoint.SECURITY_PSK: + case AccessPoint.SECURITY_WEP: + case AccessPoint.SECURITY_NONE: + case AccessPoint.SECURITY_PSK_SAE_TRANSITION: + case AccessPoint.SECURITY_OWE_TRANSITION: + return true; + case AccessPoint.SECURITY_SAE: + if (wifiManager.isWpa3SaeSupported()) { + return true; + } + break; + case AccessPoint.SECURITY_OWE: + if (wifiManager.isEnhancedOpenSupported()) { + return true; + } + break; + default: + } + return false; + } + + static void triggerVibrationForQrCodeRecognition(Context context) { + Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); + if (vibrator == null) { + return; + } + vibrator.vibrate(VibrationEffect.createOneShot( + VIBRATE_DURATION_QR_CODE_RECOGNITION.toMillis(), + VibrationEffect.DEFAULT_AMPLITUDE)); + } +} diff --git a/src/com/android/settings/wifi/dpp/WifiNetworkConfig.java b/src/com/android/settings/wifi/dpp/WifiNetworkConfig.java new file mode 100644 index 0000000000..7423561286 --- /dev/null +++ b/src/com/android/settings/wifi/dpp/WifiNetworkConfig.java @@ -0,0 +1,324 @@ +/* + * Copyright (C) 2018 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.wifi.dpp; + +import static com.android.settings.wifi.dpp.WifiQrCode.SECURITY_NO_PASSWORD; +import static com.android.settings.wifi.dpp.WifiQrCode.SECURITY_SAE; +import static com.android.settings.wifi.dpp.WifiQrCode.SECURITY_WEP; +import static com.android.settings.wifi.dpp.WifiQrCode.SECURITY_WPA_PSK; + +import android.content.Context; +import android.content.Intent; +import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiConfiguration.AuthAlgorithm; +import android.net.wifi.WifiConfiguration.KeyMgmt; +import android.net.wifi.WifiManager; +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; + +import java.util.ArrayList; +import java.util.List; + +/** + * Wraps the parameters of ZXing reader library's Wi-Fi Network config format. + * Please check {@code WifiQrCode} for detail of the format. + * + * Checks below members of {@code WifiDppUtils} for more information. + * EXTRA_WIFI_SECURITY / EXTRA_WIFI_SSID / EXTRA_WIFI_PRE_SHARED_KEY / EXTRA_WIFI_HIDDEN_SSID / + * EXTRA_QR_CODE + */ +public class WifiNetworkConfig { + + static final String FAKE_SSID = "fake network"; + static final String FAKE_PASSWORD = "password"; + private static final String TAG = "WifiNetworkConfig"; + + private String mSecurity; + private String mSsid; + private String mPreSharedKey; + private boolean mHiddenSsid; + private int mNetworkId; + private boolean mIsHotspot; + + @VisibleForTesting + WifiNetworkConfig(String security, String ssid, String preSharedKey, + boolean hiddenSsid, int networkId, boolean isHotspot) { + mSecurity = security; + mSsid = ssid; + mPreSharedKey = preSharedKey; + mHiddenSsid = hiddenSsid; + mNetworkId = networkId; + mIsHotspot = isHotspot; + } + + public WifiNetworkConfig(WifiNetworkConfig config) { + mSecurity = config.mSecurity; + mSsid = config.mSsid; + mPreSharedKey = config.mPreSharedKey; + mHiddenSsid = config.mHiddenSsid; + mNetworkId = config.mNetworkId; + mIsHotspot = config.mIsHotspot; + } + + /** + * Wi-Fi DPP activities should implement this interface for fragments to retrieve the + * WifiNetworkConfig for configuration + */ + public interface Retriever { + public WifiNetworkConfig getWifiNetworkConfig(); + } + + /** + * Retrieve WifiNetworkConfig from below 2 intents + * + * android.settings.WIFI_DPP_CONFIGURATOR_QR_CODE_GENERATOR + * android.settings.WIFI_DPP_CONFIGURATOR_QR_CODE_SCANNER + */ + public static WifiNetworkConfig getValidConfigOrNull(Intent intent) { + final String security = intent.getStringExtra(WifiDppUtils.EXTRA_WIFI_SECURITY); + final String ssid = intent.getStringExtra(WifiDppUtils.EXTRA_WIFI_SSID); + final String preSharedKey = intent.getStringExtra(WifiDppUtils.EXTRA_WIFI_PRE_SHARED_KEY); + final boolean hiddenSsid = intent.getBooleanExtra(WifiDppUtils.EXTRA_WIFI_HIDDEN_SSID, + false); + final int networkId = intent.getIntExtra(WifiDppUtils.EXTRA_WIFI_NETWORK_ID, + WifiConfiguration.INVALID_NETWORK_ID); + final boolean isHotspot = intent.getBooleanExtra(WifiDppUtils.EXTRA_IS_HOTSPOT, false); + + return getValidConfigOrNull(security, ssid, preSharedKey, hiddenSsid, networkId, isHotspot); + } + + public static WifiNetworkConfig getValidConfigOrNull(String security, String ssid, + String preSharedKey, boolean hiddenSsid, int networkId, boolean isHotspot) { + if (!isValidConfig(security, ssid, preSharedKey, hiddenSsid)) { + return null; + } + + return new WifiNetworkConfig(security, ssid, preSharedKey, hiddenSsid, networkId, + isHotspot); + } + + public static boolean isValidConfig(WifiNetworkConfig config) { + if (config == null) { + return false; + } else { + return isValidConfig(config.mSecurity, config.mSsid, config.mPreSharedKey, + config.mHiddenSsid); + } + } + + public static boolean isValidConfig(String security, String ssid, String preSharedKey, + boolean hiddenSsid) { + if (!TextUtils.isEmpty(security) && !SECURITY_NO_PASSWORD.equals(security)) { + if (TextUtils.isEmpty(preSharedKey)) { + return false; + } + } + + if (!hiddenSsid && TextUtils.isEmpty(ssid)) { + return false; + } + + return true; + } + + /** + * Escaped special characters "\", ";", ":", "," with a backslash + * See https://github.com/zxing/zxing/wiki/Barcode-Contents + */ + private String escapeSpecialCharacters(String str) { + if (TextUtils.isEmpty(str)) { + return str; + } + + StringBuilder buf = new StringBuilder(); + for (int i = 0; i < str.length(); i++) { + char ch = str.charAt(i); + if (ch =='\\' || ch == ',' || ch == ';' || ch == ':') { + buf.append('\\'); + } + buf.append(ch); + } + + return buf.toString(); + } + + /** + * Construct a barcode string for WiFi network login. + * See https://en.wikipedia.org/wiki/QR_code#WiFi_network_login + */ + public String getQrCode() { + final String empty = ""; + String barcode = new StringBuilder("WIFI:") + .append("S:") + .append(escapeSpecialCharacters(mSsid)) + .append(";") + .append("T:") + .append(TextUtils.isEmpty(mSecurity) ? empty : mSecurity) + .append(";") + .append("P:") + .append(TextUtils.isEmpty(mPreSharedKey) ? empty + : escapeSpecialCharacters(mPreSharedKey)) + .append(";") + .append("H:") + .append(mHiddenSsid) + .append(";;") + .toString(); + return barcode; + } + + public String getSecurity() { + return mSecurity; + } + + public String getSsid() { + return mSsid; + } + + public String getPreSharedKey() { + return mPreSharedKey; + } + + public boolean getHiddenSsid() { + return mHiddenSsid; + } + + public int getNetworkId() { + return mNetworkId; + } + + public boolean isHotspot() { + return mIsHotspot; + } + + public boolean isSupportWifiDpp(Context context) { + if (!WifiDppUtils.isWifiDppEnabled(context)) { + return false; + } + + if (TextUtils.isEmpty(mSecurity)) { + return false; + } + + // DPP 1.0 only supports SAE and PSK. + final WifiManager wifiManager = context.getSystemService(WifiManager.class); + switch (mSecurity) { + case SECURITY_SAE: + if (wifiManager.isWpa3SaeSupported()) { + return true; + } + break; + case SECURITY_WPA_PSK: + return true; + default: + } + return false; + } + + /** + * This is a simplified method from {@code WifiConfigController.getConfig()} + * + * TODO (b/129021867): WifiConfiguration is a deprecated class, should replace it with + * {@code android.net.wifi.WifiNetworkSuggestion} + * + * @return When it's a open network, returns 2 WifiConfiguration in the List, the 1st is + * open network and the 2nd is enhanced open network. Returns 1 WifiConfiguration in the + * List for all other supported Wi-Fi securities. + */ + List<WifiConfiguration> getWifiConfigurations() { + final List<WifiConfiguration> wifiConfigurations = new ArrayList<>(); + + if (!isValidConfig(this)) { + return wifiConfigurations; + } + + if (TextUtils.isEmpty(mSecurity) || SECURITY_NO_PASSWORD.equals(mSecurity)) { + // TODO (b/129835824): we add both open network and enhanced open network to WifiManager + // for android Q, should improve it in the future. + final WifiConfiguration openNetworkWifiConfiguration = getBasicWifiConfiguration(); + openNetworkWifiConfiguration.allowedKeyManagement.set(KeyMgmt.NONE); + wifiConfigurations.add(openNetworkWifiConfiguration); + + final WifiConfiguration enhancedOpenNetworkWifiConfiguration = + getBasicWifiConfiguration(); + enhancedOpenNetworkWifiConfiguration.allowedKeyManagement.set(KeyMgmt.OWE); + enhancedOpenNetworkWifiConfiguration.requirePMF = true; + wifiConfigurations.add(enhancedOpenNetworkWifiConfiguration); + return wifiConfigurations; + } + + final WifiConfiguration wifiConfiguration = getBasicWifiConfiguration(); + if (mSecurity.startsWith(SECURITY_WEP)) { + wifiConfiguration.allowedKeyManagement.set(KeyMgmt.NONE); + wifiConfiguration.allowedAuthAlgorithms.set(AuthAlgorithm.OPEN); + wifiConfiguration.allowedAuthAlgorithms.set(AuthAlgorithm.SHARED); + + // WEP-40, WEP-104, and 256-bit WEP (WEP-232?) + final int length = mPreSharedKey.length(); + if ((length == 10 || length == 26 || length == 58) + && mPreSharedKey.matches("[0-9A-Fa-f]*")) { + wifiConfiguration.wepKeys[0] = mPreSharedKey; + } else { + wifiConfiguration.wepKeys[0] = addQuotationIfNeeded(mPreSharedKey); + } + } else if (mSecurity.startsWith(SECURITY_WPA_PSK)) { + wifiConfiguration.allowedKeyManagement.set(KeyMgmt.WPA_PSK); + + if (mPreSharedKey.matches("[0-9A-Fa-f]{64}")) { + wifiConfiguration.preSharedKey = mPreSharedKey; + } else { + wifiConfiguration.preSharedKey = addQuotationIfNeeded(mPreSharedKey); + } + } else if (mSecurity.startsWith(SECURITY_SAE)) { + wifiConfiguration.allowedKeyManagement.set(KeyMgmt.SAE); + wifiConfiguration.requirePMF = true; + if (mPreSharedKey.length() != 0) { + wifiConfiguration.preSharedKey = addQuotationIfNeeded(mPreSharedKey); + } + } else { + Log.w(TAG, "Unsupported security"); + return wifiConfigurations; + } + + wifiConfigurations.add(wifiConfiguration); + return wifiConfigurations; + } + + private WifiConfiguration getBasicWifiConfiguration() { + final WifiConfiguration wifiConfiguration = new WifiConfiguration(); + + wifiConfiguration.SSID = addQuotationIfNeeded(mSsid); + wifiConfiguration.hiddenSSID = mHiddenSsid; + wifiConfiguration.networkId = mNetworkId; + return wifiConfiguration; + } + + private String addQuotationIfNeeded(String input) { + if (TextUtils.isEmpty(input)) { + return ""; + } + + if (input.length() >= 2 && input.startsWith("\"") && input.endsWith("\"")) { + return input; + } + + StringBuilder sb = new StringBuilder(); + sb.append("\"").append(input).append("\""); + return sb.toString(); + } +} diff --git a/src/com/android/settings/wifi/dpp/WifiNetworkListFragment.java b/src/com/android/settings/wifi/dpp/WifiNetworkListFragment.java new file mode 100644 index 0000000000..fa2dc99ef2 --- /dev/null +++ b/src/com/android/settings/wifi/dpp/WifiNetworkListFragment.java @@ -0,0 +1,346 @@ +/* + * Copyright (C) 2019 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.wifi.dpp; + +import android.app.Activity; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.Intent; +import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiManager; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.widget.Toast; + +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; + +import com.android.settings.R; +import com.android.settings.SettingsPreferenceFragment; +import com.android.settings.core.SubSettingLauncher; +import com.android.settings.wifi.AddNetworkFragment; +import com.android.settingslib.wifi.AccessPoint; +import com.android.settingslib.wifi.AccessPointPreference; +import com.android.settingslib.wifi.WifiSavedConfigUtils; +import com.android.settingslib.wifi.WifiTracker; +import com.android.settingslib.wifi.WifiTrackerFactory; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class WifiNetworkListFragment extends SettingsPreferenceFragment implements + WifiTracker.WifiListener, AccessPoint.AccessPointListener { + private static final String TAG = "WifiNetworkListFragment"; + + private static final String WIFI_CONFIG_KEY = "wifi_config_key"; + private static final String PREF_KEY_ACCESS_POINTS = "access_points"; + + static final int ADD_NETWORK_REQUEST = 1; + + private PreferenceCategory mAccessPointsPreferenceCategory; + private AccessPointPreference.UserBadgeCache mUserBadgeCache; + private Preference mAddPreference; + // Only shows up if mIsTest == true + private Preference mFakeNetworkPreference; + + private WifiManager mWifiManager; + private WifiTracker mWifiTracker; + + private WifiManager.ActionListener mSaveListener; + private boolean mIsTest; + + // Container Activity must implement this interface + public interface OnChooseNetworkListener { + public void onChooseNetwork(WifiNetworkConfig wifiNetworkConfig); + } + + OnChooseNetworkListener mOnChooseNetworkListener; + + @Override + public int getMetricsCategory() { + return SettingsEnums.SETTINGS_WIFI_DPP_CONFIGURATOR; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (!(context instanceof OnChooseNetworkListener)) { + throw new IllegalArgumentException("Invalid context type"); + } + mOnChooseNetworkListener = (OnChooseNetworkListener) context; + } + + @Override + public void onDetach() { + mOnChooseNetworkListener = null; + super.onDetach(); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + mWifiTracker = WifiTrackerFactory.create(getActivity(), this, + getSettingsLifecycle(), /* includeSaved */true, /* includeScans */ true); + mWifiManager = mWifiTracker.getManager(); + + final Bundle args = getArguments(); + if (args != null) { + mIsTest = args.getBoolean(WifiDppUtils.EXTRA_TEST, false); + } + + mSaveListener = new WifiManager.ActionListener() { + @Override + public void onSuccess() { + // Do nothing. + } + + @Override + public void onFailure(int reason) { + Activity activity = getActivity(); + if (activity != null) { + Toast.makeText(activity, + R.string.wifi_failed_save_message, + Toast.LENGTH_SHORT).show(); + } + } + }; + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (requestCode == ADD_NETWORK_REQUEST) { + if (resultCode == Activity.RESULT_OK) { + handleAddNetworkSubmitEvent(data); + } + mWifiTracker.resumeScanning(); + } + } + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.wifi_dpp_network_list); + + mAccessPointsPreferenceCategory = (PreferenceCategory) findPreference( + PREF_KEY_ACCESS_POINTS); + + mFakeNetworkPreference = new Preference(getPrefContext()); + mFakeNetworkPreference.setIcon(R.drawable.ic_wifi_signal_0); + mFakeNetworkPreference.setKey("fake_key"); + mFakeNetworkPreference.setTitle("fake network"); + + mAddPreference = new Preference(getPrefContext()); + mAddPreference.setIcon(R.drawable.ic_add_24dp); + mAddPreference.setTitle(R.string.wifi_add_network); + + mUserBadgeCache = new AccessPointPreference.UserBadgeCache(getPackageManager()); + } + + /** Called when the state of Wifi has changed. */ + @Override + public void onWifiStateChanged(int state) { + final int wifiState = mWifiManager.getWifiState(); + switch (wifiState) { + case WifiManager.WIFI_STATE_ENABLED: + updateAccessPointPreferences(); + break; + + case WifiManager.WIFI_STATE_ENABLING: + case WifiManager.WIFI_STATE_DISABLING: + removeAccessPointPreferences(); + break; + } + } + + /** Called when the connection state of wifi has changed. */ + @Override + public void onConnectedChanged() { + // Do nothing. + } + + /** + * Called to indicate the list of AccessPoints has been updated and + * getAccessPoints should be called to get the latest information. + */ + @Override + public void onAccessPointsChanged() { + updateAccessPointPreferences(); + } + + @Override + public void onAccessPointChanged(final AccessPoint accessPoint) { + Log.d(TAG, "onAccessPointChanged (singular) callback initiated"); + View view = getView(); + if (view != null) { + view.post(() -> { + final Object tag = accessPoint.getTag(); + if (tag != null) { + ((AccessPointPreference) tag).refresh(); + } + }); + } + } + + @Override + public void onLevelChanged(AccessPoint accessPoint) { + ((AccessPointPreference) accessPoint.getTag()).onLevelChanged(); + } + + @Override + public boolean onPreferenceTreeClick(Preference preference) { + if (preference instanceof AccessPointPreference) { + final AccessPoint selectedAccessPoint = + ((AccessPointPreference) preference).getAccessPoint(); + if (selectedAccessPoint == null) { + return false; + } + + // Launch WifiDppAddDeviceFragment to start DPP in Configurator-Initiator role. + final WifiConfiguration wifiConfig = selectedAccessPoint.getConfig(); + if (wifiConfig == null) { + throw new IllegalArgumentException("Invalid access point"); + } + final WifiNetworkConfig networkConfig = WifiNetworkConfig.getValidConfigOrNull( + selectedAccessPoint.getSecurityString(/* concise */ true), + wifiConfig.getPrintableSsid(), wifiConfig.preSharedKey, /* hiddenSsid */ false, + wifiConfig.networkId, /* isHotspot */ false); + if (mOnChooseNetworkListener != null) { + mOnChooseNetworkListener.onChooseNetwork(networkConfig); + } + } else if (preference == mAddPreference) { + launchAddNetworkFragment(); + } else if (preference == mFakeNetworkPreference) { + if (mOnChooseNetworkListener != null) { + mOnChooseNetworkListener.onChooseNetwork( + new WifiNetworkConfig( + WifiQrCode.SECURITY_WPA_PSK, + /* ssid */ WifiNetworkConfig.FAKE_SSID, + /* preSharedKey */ WifiNetworkConfig.FAKE_PASSWORD, + /* hiddenSsid */ true, + /* networkId */ WifiConfiguration.INVALID_NETWORK_ID, + /* isHotspot*/ false)); + } + } else { + return super.onPreferenceTreeClick(preference); + } + return true; + } + + private void handleAddNetworkSubmitEvent(Intent data) { + final WifiConfiguration wifiConfiguration = data.getParcelableExtra(WIFI_CONFIG_KEY); + if (wifiConfiguration != null) { + mWifiManager.save(wifiConfiguration, mSaveListener); + } + } + + private boolean isValidForDppConfiguration(AccessPoint accessPoint) { + final int security = accessPoint.getSecurity(); + + // DPP 1.0 only support SAE and PSK. + if (!(security == AccessPoint.SECURITY_PSK || security == AccessPoint.SECURITY_SAE)) { + return false; + } + + return true; + } + + private void launchAddNetworkFragment() { + new SubSettingLauncher(getContext()) + .setTitleRes(R.string.wifi_add_network) + .setDestination(AddNetworkFragment.class.getName()) + .setSourceMetricsCategory(getMetricsCategory()) + .setResultListener(this, ADD_NETWORK_REQUEST) + .launch(); + } + + private void removeAccessPointPreferences() { + mAccessPointsPreferenceCategory.removeAll(); + mAccessPointsPreferenceCategory.setVisible(false); + } + + private void updateAccessPointPreferences() { + // in case state has changed + if (!mWifiManager.isWifiEnabled()) { + return; + } + + List<AccessPoint> savedAccessPoints = + WifiSavedConfigUtils.getAllConfigs(getContext(), mWifiManager); + + savedAccessPoints = savedAccessPoints.stream() + .filter(accessPoint -> isValidForDppConfiguration(accessPoint)) + .map(accessPoint -> getScannedAccessPointIfAvailable(accessPoint)) + .sorted((ap1, ap2) -> { + // orders reachable Wi-Fi networks on top + if (ap1.isReachable() && !ap2.isReachable()) { + return -1; + } else if (!ap1.isReachable() && ap2.isReachable()) { + return 1; + } + + String ap1Title = nullToEmpty(ap1.getTitle()); + String ap2Title = nullToEmpty(ap2.getTitle()); + + return ap1Title.compareToIgnoreCase(ap2Title); + }).collect(Collectors.toList()); + + int index = 0; + mAccessPointsPreferenceCategory.removeAll(); + for (AccessPoint savedAccessPoint : savedAccessPoints) { + final AccessPointPreference preference = + createAccessPointPreference(savedAccessPoint); + + preference.setOrder(index++); + preference.setEnabled(savedAccessPoint.isReachable()); + savedAccessPoint.setListener(this); + + preference.refresh(); + mAccessPointsPreferenceCategory.addPreference(preference); + } + mAddPreference.setOrder(index); + mAccessPointsPreferenceCategory.addPreference(mAddPreference); + + if (mIsTest) { + mAccessPointsPreferenceCategory.addPreference(mFakeNetworkPreference); + } + } + + private String nullToEmpty(String string) { + return (string == null) ? "" : string; + } + + // Replaces with an AccessPoint from scanned result for signal information + private AccessPoint getScannedAccessPointIfAvailable(AccessPoint savedAccessPoint) { + final List<AccessPoint> scannedAccessPoints = mWifiTracker.getAccessPoints(); + final WifiConfiguration savedWifiConfiguration = savedAccessPoint.getConfig(); + for (AccessPoint scannedAccessPoint : scannedAccessPoints) { + if (scannedAccessPoint.matches(savedWifiConfiguration)) { + return scannedAccessPoint; + } + } + return savedAccessPoint; + } + + private AccessPointPreference createAccessPointPreference(AccessPoint accessPoint) { + return new AccessPointPreference(accessPoint, getPrefContext(), mUserBadgeCache, + R.drawable.ic_wifi_signal_0, /* forSavedNetworks */ false); + } +} diff --git a/src/com/android/settings/wifi/dpp/WifiQrCode.java b/src/com/android/settings/wifi/dpp/WifiQrCode.java new file mode 100644 index 0000000000..40ae111343 --- /dev/null +++ b/src/com/android/settings/wifi/dpp/WifiQrCode.java @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2018 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.wifi.dpp; + +import android.net.wifi.WifiConfiguration; +import android.text.TextUtils; + +import androidx.annotation.VisibleForTesting; + +import java.util.Arrays; +import java.util.List; +import java.util.regex.Pattern; + +/** + * Supports to parse 2 types of QR code + * + * 1. Standard Wi-Fi DPP bootstrapping information or + * 2. ZXing reader library's Wi-Fi Network config format described in + * https://github.com/zxing/zxing/wiki/Barcode-Contents#wi-fi-network-config-android-ios-11 + * + * ZXing reader library's Wi-Fi Network config format example: + * + * WIFI:T:WPA;S:mynetwork;P:mypass;; + * + * parameter Example Description + * T WPA Authentication type; can be WEP or WPA, or 'nopass' for no password. Or, + * omit for no password. + * S mynetwork Network SSID. Required. Enclose in double quotes if it is an ASCII name, + * but could be interpreted as hex (i.e. "ABCD") + * P mypass Password, ignored if T is "nopass" (in which case it may be omitted). + * Enclose in double quotes if it is an ASCII name, but could be interpreted as + * hex (i.e. "ABCD") + * H true Optional. True if the network SSID is hidden. + * + */ +public class WifiQrCode { + public static final String SCHEME_DPP = "DPP"; + public static final String SCHEME_ZXING_WIFI_NETWORK_CONFIG = "WIFI"; + public static final String PREFIX_DPP = "DPP:"; + public static final String PREFIX_ZXING_WIFI_NETWORK_CONFIG = "WIFI:"; + + public static final String PREFIX_DPP_PUBLIC_KEY = "K:"; + public static final String PREFIX_DPP_INFORMATION = "I:"; + + public static final String PREFIX_ZXING_SECURITY = "T:"; + public static final String PREFIX_ZXING_SSID = "S:"; + public static final String PREFIX_ZXING_PASSWORD = "P:"; + public static final String PREFIX_ZXING_HIDDEN_SSID = "H:"; + + public static final String DELIMITER_QR_CODE = ";"; + + // Ignores password if security is SECURITY_NO_PASSWORD or absent + public static final String SECURITY_NO_PASSWORD = "nopass"; //open network or OWE + public static final String SECURITY_WEP = "WEP"; + public static final String SECURITY_WPA_PSK = "WPA"; + public static final String SECURITY_SAE = "SAE"; + + private String mQrCode; + + /** + * SCHEME_DPP for standard Wi-Fi device provision protocol; SCHEME_ZXING_WIFI_NETWORK_CONFIG + * for ZXing reader library' Wi-Fi Network config format + */ + private String mScheme; + + // Data from parsed Wi-Fi DPP QR code + private String mPublicKey; + private String mInformation; + + // Data from parsed ZXing reader library's Wi-Fi Network config format + private WifiNetworkConfig mWifiNetworkConfig; + + public WifiQrCode(String qrCode) throws IllegalArgumentException { + if (TextUtils.isEmpty(qrCode)) { + throw new IllegalArgumentException("Empty QR code"); + } + + mQrCode = qrCode; + + if (qrCode.startsWith(PREFIX_DPP)) { + mScheme = SCHEME_DPP; + parseWifiDppQrCode(qrCode); + } else if (qrCode.startsWith(PREFIX_ZXING_WIFI_NETWORK_CONFIG)) { + mScheme = SCHEME_ZXING_WIFI_NETWORK_CONFIG; + parseZxingWifiQrCode(qrCode); + } else { + throw new IllegalArgumentException("Invalid scheme"); + } + } + + /** Parses Wi-Fi DPP QR code string */ + private void parseWifiDppQrCode(String qrCode) throws IllegalArgumentException { + List keyValueList = getKeyValueList(qrCode, PREFIX_DPP, DELIMITER_QR_CODE); + + String publicKey = getValueOrNull(keyValueList, PREFIX_DPP_PUBLIC_KEY); + if (TextUtils.isEmpty(publicKey)) { + throw new IllegalArgumentException("Invalid format"); + } + mPublicKey = publicKey; + + mInformation = getValueOrNull(keyValueList, PREFIX_DPP_INFORMATION); + } + + /** Parses ZXing reader library's Wi-Fi Network config format */ + private void parseZxingWifiQrCode(String qrCode) throws IllegalArgumentException { + List keyValueList = getKeyValueList(qrCode, PREFIX_ZXING_WIFI_NETWORK_CONFIG, + DELIMITER_QR_CODE); + + String security = getValueOrNull(keyValueList, PREFIX_ZXING_SECURITY); + String ssid = getValueOrNull(keyValueList, PREFIX_ZXING_SSID); + String password = getValueOrNull(keyValueList, PREFIX_ZXING_PASSWORD); + String hiddenSsidString = getValueOrNull(keyValueList, PREFIX_ZXING_HIDDEN_SSID); + + boolean hiddenSsid = "true".equalsIgnoreCase(hiddenSsidString); + + //"\", ";", "," and ":" are escaped with a backslash "\", should remove at first + security = removeBackSlash(security); + ssid = removeBackSlash(ssid); + password = removeBackSlash(password); + + mWifiNetworkConfig = WifiNetworkConfig.getValidConfigOrNull(security, ssid, password, + hiddenSsid, WifiConfiguration.INVALID_NETWORK_ID, /* isHotspot */ false); + + if (mWifiNetworkConfig == null) { + throw new IllegalArgumentException("Invalid format"); + } + } + + /** + * Splits key/value pairs from qrCode + * + * @param qrCode the QR code raw string + * @param prefixQrCode the string before all key/value pairs in qrCode + * @param delimiter the string to split key/value pairs, can't contain a backslash + * @return a list contains string of key/value (e.g. K:key1) + */ + private List<String> getKeyValueList(String qrCode, String prefixQrCode, + String delimiter) { + String keyValueString = qrCode.substring(prefixQrCode.length()); + + // Should not treat \delimiter as a delimiter + String regex = "(?<!\\\\)" + Pattern.quote(delimiter); + + List<String> result = Arrays.asList(keyValueString.split(regex)); + return result; + } + + private String getValueOrNull(List<String> keyValueList, String prefix) { + for (String keyValue : keyValueList) { + if (keyValue.startsWith(prefix)) { + return keyValue.substring(prefix.length()); + } + } + + return null; + } + + @VisibleForTesting + String removeBackSlash(String input) { + if (input == null) { + return null; + } + + StringBuilder sb = new StringBuilder(); + boolean backSlash = false; + for (char ch : input.toCharArray()) { + if (ch != '\\') { + sb.append(ch); + backSlash = false; + } else { + if (backSlash) { + sb.append(ch); + backSlash = false; + continue; + } + + backSlash = true; + } + } + + return sb.toString(); + } + + public String getQrCode() { + return mQrCode; + } + + /** + * Uses to check type of QR code + * + * SCHEME_DPP for standard Wi-Fi device provision protocol; SCHEME_ZXING_WIFI_NETWORK_CONFIG + * for ZXing reader library' Wi-Fi Network config format + */ + public String getScheme() { + return mScheme; + } + + /** Available when {@code getScheme()} returns SCHEME_DPP */ + @VisibleForTesting + String getPublicKey() { + return mPublicKey; + } + + /** May be available when {@code getScheme()} returns SCHEME_DPP */ + public String getInformation() { + return mInformation; + } + + /** Available when {@code getScheme()} returns SCHEME_ZXING_WIFI_NETWORK_CONFIG */ + public WifiNetworkConfig getWifiNetworkConfig() { + if (mWifiNetworkConfig == null) { + return null; + } + + return new WifiNetworkConfig(mWifiNetworkConfig); + } + + public static WifiQrCode getValidWifiDppQrCodeOrNull(String qrCode) { + WifiQrCode wifiQrCode; + try { + wifiQrCode = new WifiQrCode(qrCode); + } catch(IllegalArgumentException e) { + return null; + } + + if (SCHEME_DPP.equals(wifiQrCode.getScheme())) { + return wifiQrCode; + } + + return null; + } +} diff --git a/src/com/android/settings/wifi/p2p/P2pCategoryPreferenceController.java b/src/com/android/settings/wifi/p2p/P2pCategoryPreferenceController.java index fed363d9dc..032649c149 100644 --- a/src/com/android/settings/wifi/p2p/P2pCategoryPreferenceController.java +++ b/src/com/android/settings/wifi/p2p/P2pCategoryPreferenceController.java @@ -17,6 +17,7 @@ package com.android.settings.wifi.p2p; import android.content.Context; + import androidx.preference.Preference; import androidx.preference.PreferenceGroup; import androidx.preference.PreferenceScreen; @@ -41,7 +42,7 @@ public abstract class P2pCategoryPreferenceController extends AbstractPreference @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - mCategory = (PreferenceGroup) screen.findPreference(getPreferenceKey()); + mCategory = screen.findPreference(getPreferenceKey()); } public void removeAllChildren() { diff --git a/src/com/android/settings/wifi/p2p/P2pThisDevicePreferenceController.java b/src/com/android/settings/wifi/p2p/P2pThisDevicePreferenceController.java index 84ce1ee04c..ade423a486 100644 --- a/src/com/android/settings/wifi/p2p/P2pThisDevicePreferenceController.java +++ b/src/com/android/settings/wifi/p2p/P2pThisDevicePreferenceController.java @@ -18,9 +18,10 @@ package com.android.settings.wifi.p2p; import android.content.Context; import android.net.wifi.p2p.WifiP2pDevice; +import android.text.TextUtils; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.text.TextUtils; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.core.AbstractPreferenceController; diff --git a/src/com/android/settings/wifi/p2p/WifiP2pPeer.java b/src/com/android/settings/wifi/p2p/WifiP2pPeer.java index 5a087e527a..6caef94c6f 100644 --- a/src/com/android/settings/wifi/p2p/WifiP2pPeer.java +++ b/src/com/android/settings/wifi/p2p/WifiP2pPeer.java @@ -19,10 +19,12 @@ package com.android.settings.wifi.p2p; import android.content.Context; import android.net.wifi.WifiManager; import android.net.wifi.p2p.WifiP2pDevice; -import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; import android.text.TextUtils; import android.widget.ImageView; + +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + import com.android.settings.R; public class WifiP2pPeer extends Preference { diff --git a/src/com/android/settings/wifi/p2p/WifiP2pPersistentGroup.java b/src/com/android/settings/wifi/p2p/WifiP2pPersistentGroup.java index 4d2b43364a..702942ca7f 100644 --- a/src/com/android/settings/wifi/p2p/WifiP2pPersistentGroup.java +++ b/src/com/android/settings/wifi/p2p/WifiP2pPersistentGroup.java @@ -18,8 +18,8 @@ package com.android.settings.wifi.p2p; import android.content.Context; import android.net.wifi.p2p.WifiP2pGroup; + import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; public class WifiP2pPersistentGroup extends Preference { diff --git a/src/com/android/settings/wifi/p2p/WifiP2pPreferenceController.java b/src/com/android/settings/wifi/p2p/WifiP2pPreferenceController.java index 78af007bdb..86cce1ef1c 100644 --- a/src/com/android/settings/wifi/p2p/WifiP2pPreferenceController.java +++ b/src/com/android/settings/wifi/p2p/WifiP2pPreferenceController.java @@ -15,11 +15,14 @@ */ package com.android.settings.wifi.p2p; +import android.app.Service; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.location.LocationManager; import android.net.wifi.WifiManager; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; @@ -48,6 +51,17 @@ public class WifiP2pPreferenceController extends AbstractPreferenceController } }; private final IntentFilter mFilter = new IntentFilter(WifiManager.WIFI_STATE_CHANGED_ACTION); + private final LocationManager mLocationManager; + private final BroadcastReceiver mLocationReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (mWifiDirectPref != null) { + updateState(mWifiDirectPref); + } + } + }; + private final IntentFilter mLocationFilter = + new IntentFilter(LocationManager.MODE_CHANGED_ACTION); private Preference mWifiDirectPref; @@ -56,6 +70,7 @@ public class WifiP2pPreferenceController extends AbstractPreferenceController super(context); mWifiManager = wifiManager; lifecycle.addObserver(this); + mLocationManager = (LocationManager) context.getSystemService(Service.LOCATION_SERVICE); } @Override @@ -66,13 +81,21 @@ public class WifiP2pPreferenceController extends AbstractPreferenceController } @Override + public void updateState(Preference preference) { + super.updateState(preference); + preference.setEnabled(mLocationManager.isLocationEnabled() && mWifiManager.isWifiEnabled()); + } + + @Override public void onResume() { mContext.registerReceiver(mReceiver, mFilter); + mContext.registerReceiver(mLocationReceiver, mLocationFilter); } @Override public void onPause() { mContext.unregisterReceiver(mReceiver); + mContext.unregisterReceiver(mLocationReceiver); } @Override @@ -87,7 +110,9 @@ public class WifiP2pPreferenceController extends AbstractPreferenceController private void togglePreferences() { if (mWifiDirectPref != null) { - mWifiDirectPref.setEnabled(mWifiManager.isWifiEnabled()); + mWifiDirectPref.setEnabled( + mWifiManager.isWifiEnabled() + && mLocationManager.isLocationEnabled()); } } } diff --git a/src/com/android/settings/wifi/p2p/WifiP2pSettings.java b/src/com/android/settings/wifi/p2p/WifiP2pSettings.java index 302981d9d6..f1ac6cd1a5 100644 --- a/src/com/android/settings/wifi/p2p/WifiP2pSettings.java +++ b/src/com/android/settings/wifi/p2p/WifiP2pSettings.java @@ -17,8 +17,8 @@ package com.android.settings.wifi.p2p; import android.app.Activity; -import android.app.AlertDialog; import android.app.Dialog; +import android.app.settings.SettingsEnums; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; @@ -34,12 +34,11 @@ import android.net.wifi.p2p.WifiP2pGroup; import android.net.wifi.p2p.WifiP2pGroupList; import android.net.wifi.p2p.WifiP2pInfo; import android.net.wifi.p2p.WifiP2pManager; +import android.net.wifi.p2p.WifiP2pManager.DeviceInfoListener; import android.net.wifi.p2p.WifiP2pManager.PeerListListener; import android.net.wifi.p2p.WifiP2pManager.PersistentGroupInfoListener; import android.os.Bundle; import android.os.SystemProperties; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; import android.text.InputFilter; import android.text.TextUtils; import android.util.Log; @@ -49,7 +48,10 @@ import android.view.MenuItem; import android.widget.EditText; import android.widget.Toast; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import androidx.appcompat.app.AlertDialog; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; import com.android.settingslib.core.AbstractPreferenceController; @@ -61,7 +63,7 @@ import java.util.List; * Displays Wi-fi p2p settings UI */ public class WifiP2pSettings extends DashboardFragment - implements PersistentGroupInfoListener, PeerListListener { + implements PersistentGroupInfoListener, PeerListListener, DeviceInfoListener { private static final String TAG = "WifiP2pSettings"; private static final boolean DBG = false; @@ -84,6 +86,7 @@ public class WifiP2pSettings extends DashboardFragment private boolean mWifiP2pSearching; private int mConnectedDevices; private boolean mLastGroupFormed = false; + private boolean mIsIgnoreInitConnectionInfoCallback = false; private P2pPeerCategoryPreferenceController mPeerCategoryController; private P2pPersistentCategoryPreferenceController mPersistentCategoryController; @@ -130,11 +133,14 @@ public class WifiP2pSettings extends DashboardFragment startSearch(); } mLastGroupFormed = wifip2pinfo.groupFormed; + mIsIgnoreInitConnectionInfoCallback = true; } else if (WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION.equals(action)) { - mThisDevice = (WifiP2pDevice) intent.getParcelableExtra( - WifiP2pManager.EXTRA_WIFI_P2P_DEVICE); - if (DBG) Log.d(TAG, "Update device info: " + mThisDevice); - mThisDevicePreferenceController.updateDeviceName(mThisDevice); + // Do not use WifiP2pManager.EXTRA_WIFI_P2P_DEVICE from the extras, as the system + // broadcast does not contain the device's MAC. + // Requesting our own device info as an app holding the NETWORK_SETTINGS permission + // ensures that the MAC address will be available in the result. + if (DBG) Log.d(TAG, "This device changed. Requesting device info."); + mWifiP2pManager.requestDeviceInfo(mChannel, WifiP2pSettings.this); } else if (WifiP2pManager.WIFI_P2P_DISCOVERY_CHANGED_ACTION.equals(action)) { int discoveryState = intent.getIntExtra(WifiP2pManager.EXTRA_DISCOVERY_STATE, WifiP2pManager.WIFI_P2P_DISCOVERY_STOPPED); @@ -164,7 +170,7 @@ public class WifiP2pSettings extends DashboardFragment @Override public int getMetricsCategory() { - return MetricsEvent.WIFI_P2P; + return SettingsEnums.WIFI_P2P; } @Override @@ -337,6 +343,23 @@ public class WifiP2pSettings extends DashboardFragment getActivity().registerReceiver(mReceiver, mIntentFilter); if (mWifiP2pManager != null) { mWifiP2pManager.requestPeers(mChannel, WifiP2pSettings.this); + mWifiP2pManager.requestDeviceInfo(mChannel, WifiP2pSettings.this); + mIsIgnoreInitConnectionInfoCallback = false; + mWifiP2pManager.requestNetworkInfo(mChannel, networkInfo -> { + mWifiP2pManager.requestConnectionInfo(mChannel, wifip2pinfo -> { + if (!mIsIgnoreInitConnectionInfoCallback) { + if (networkInfo.isConnected()) { + if (DBG) { + Log.d(TAG, "Connected"); + } + } else if (!mLastGroupFormed) { + // Find peers when p2p doesn't connected. + startSearch(); + } + mLastGroupFormed = wifip2pinfo.groupFormed; + } + }); + }); } } @@ -506,13 +529,13 @@ public class WifiP2pSettings extends DashboardFragment public int getDialogMetricsCategory(int dialogId) { switch (dialogId) { case DIALOG_DISCONNECT: - return MetricsEvent.DIALOG_WIFI_P2P_DISCONNECT; + return SettingsEnums.DIALOG_WIFI_P2P_DISCONNECT; case DIALOG_CANCEL_CONNECT: - return MetricsEvent.DIALOG_WIFI_P2P_CANCEL_CONNECT; + return SettingsEnums.DIALOG_WIFI_P2P_CANCEL_CONNECT; case DIALOG_RENAME: - return MetricsEvent.DIALOG_WIFI_P2P_RENAME; + return SettingsEnums.DIALOG_WIFI_P2P_RENAME; case DIALOG_DELETE_GROUP: - return MetricsEvent.DIALOG_WIFI_P2P_DELETE_GROUP; + return SettingsEnums.DIALOG_WIFI_P2P_DELETE_GROUP; } return 0; } @@ -573,6 +596,13 @@ public class WifiP2pSettings extends DashboardFragment handlePeersChanged(); } + @Override + public void onDeviceInfoAvailable(WifiP2pDevice wifiP2pDevice) { + mThisDevice = wifiP2pDevice; + if (DBG) Log.d(TAG, "Update device info: " + mThisDevice); + mThisDevicePreferenceController.updateDeviceName(mThisDevice); + } + private void handleP2pStateChanged() { updateSearchMenu(false); mThisDevicePreferenceController.setEnabled(mWifiP2pEnabled); diff --git a/src/com/android/settings/wifi/qrcode/QrCamera.java b/src/com/android/settings/wifi/qrcode/QrCamera.java new file mode 100644 index 0000000000..c682587955 --- /dev/null +++ b/src/com/android/settings/wifi/qrcode/QrCamera.java @@ -0,0 +1,461 @@ +/* + * Copyright (C) 2018 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.wifi.qrcode; + +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.SurfaceTexture; +import android.hardware.Camera; +import android.hardware.Camera.CameraInfo; +import android.hardware.Camera.Parameters; +import android.os.AsyncTask; +import android.os.Handler; +import android.os.Message; +import android.util.ArrayMap; +import android.util.Log; +import android.util.Size; +import android.view.Surface; +import android.view.WindowManager; + +import androidx.annotation.VisibleForTesting; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.BinaryBitmap; +import com.google.zxing.DecodeHintType; +import com.google.zxing.MultiFormatReader; +import com.google.zxing.ReaderException; +import com.google.zxing.Result; +import com.google.zxing.common.HybridBinarizer; + +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.Semaphore; + +/** + * Manage the camera for the QR scanner and help the decoder to get the image inside the scanning + * frame. Caller prepares a {@link SurfaceTexture} then call {@link #start(SurfaceTexture)} to + * start QR Code scanning. The scanning result will return by ScannerCallback interface. Caller + * can also call {@link #stop()} to halt QR Code scanning before the result returned. + */ +public class QrCamera extends Handler { + private static final String TAG = "QrCamera"; + + private static final int MSG_AUTO_FOCUS = 1; + + /** + * The max allowed difference between picture size ratio and preview size ratio. + * Uses to filter the picture sizes of similar preview size ratio, for example, if a preview + * size is 1920x1440, MAX_RATIO_DIFF 0.1 could allow picture size of 720x480 or 352x288 or + * 176x44 but not 1920x1080. + */ + private static double MAX_RATIO_DIFF = 0.1; + + private static long AUTOFOCUS_INTERVAL_MS = 1500L; + + private static Map<DecodeHintType, List<BarcodeFormat>> HINTS = new ArrayMap<>(); + private static List<BarcodeFormat> FORMATS = new ArrayList<>(); + + static { + FORMATS.add(BarcodeFormat.QR_CODE); + HINTS.put(DecodeHintType.POSSIBLE_FORMATS, FORMATS); + } + + private Camera mCamera; + private Size mPreviewSize; + private WeakReference<Context> mContext; + private ScannerCallback mScannerCallback; + private MultiFormatReader mReader; + private DecodingTask mDecodeTask; + private int mCameraOrientation; + private Camera.Parameters mParameters; + + public QrCamera(Context context, ScannerCallback callback) { + mContext = new WeakReference<Context>(context); + mScannerCallback = callback; + mReader = new MultiFormatReader(); + mReader.setHints(HINTS); + } + + /** + * The function start camera preview and capture pictures to decode QR code continuously in a + * background task. + * + * @param surface The surface to be used for live preview. + */ + public void start(SurfaceTexture surface) { + if (mDecodeTask == null) { + mDecodeTask = new DecodingTask(surface); + // Execute in the separate thread pool to prevent block other AsyncTask. + mDecodeTask.executeOnExecutor(Executors.newSingleThreadExecutor()); + } + } + + /** + * The function stop camera preview and background decode task. Caller call this function when + * the surface is being destroyed. + */ + public void stop() { + removeMessages(MSG_AUTO_FOCUS); + if (mDecodeTask != null) { + mDecodeTask.cancel(true); + mDecodeTask = null; + } + if (mCamera != null) { + mCamera.stopPreview(); + } + } + + /** The scanner which includes this QrCamera class should implement this */ + public interface ScannerCallback { + + /** + * The function used to handle the decoding result of the QR code. + * + * @param result the result QR code after decoding. + */ + void handleSuccessfulResult(String result); + + /** Request the QR code scanner to handle the failure happened. */ + void handleCameraFailure(); + + /** + * The function used to get the background View size. + * + * @return Includes the background view size. + */ + Size getViewSize(); + + /** + * The function used to get the frame position inside the view + * + * @param previewSize Is the preview size set by camera + * @param cameraOrientation Is the orientation of current Camera + * @return The rectangle would like to crop from the camera preview shot. + */ + Rect getFramePosition(Size previewSize, int cameraOrientation); + + /** + * Sets the transform to associate with preview area. + * + * @param transform The transform to apply to the content of preview + */ + void setTransform(Matrix transform); + + /** + * Verify QR code is valid or not. The camera will stop scanning if this callback returns + * true. + * + * @param qrCode The result QR code after decoding. + * @return Returns true if qrCode hold valid information. + */ + boolean isValid(String qrCode); + } + + private void setCameraParameter() { + mParameters = mCamera.getParameters(); + mPreviewSize = getBestPreviewSize(mParameters); + mParameters.setPreviewSize(mPreviewSize.getWidth(), mPreviewSize.getHeight()); + Size pictureSize = getBestPictureSize(mParameters); + mParameters.setPreviewSize(pictureSize.getWidth(), pictureSize.getHeight()); + + if (mParameters.getSupportedFlashModes().contains(Parameters.FLASH_MODE_OFF)) { + mParameters.setFlashMode(Parameters.FLASH_MODE_OFF); + } + + final List<String> supportedFocusModes = mParameters.getSupportedFocusModes(); + if (supportedFocusModes.contains(Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) { + mParameters.setFocusMode(Parameters.FOCUS_MODE_CONTINUOUS_PICTURE); + } else if (supportedFocusModes.contains(Parameters.FOCUS_MODE_AUTO)) { + mParameters.setFocusMode(Parameters.FOCUS_MODE_AUTO); + } + mCamera.setParameters(mParameters); + } + + private boolean startPreview() { + if (mContext.get() == null) { + return false; + } + + final WindowManager winManager = + (WindowManager) mContext.get().getSystemService(Context.WINDOW_SERVICE); + final int rotation = winManager.getDefaultDisplay().getRotation(); + int degrees = 0; + switch (rotation) { + case Surface.ROTATION_0: + degrees = 0; + break; + case Surface.ROTATION_90: + degrees = 90; + break; + case Surface.ROTATION_180: + degrees = 180; + break; + case Surface.ROTATION_270: + degrees = 270; + break; + } + final int rotateDegrees = (mCameraOrientation - degrees + 360) % 360; + mCamera.setDisplayOrientation(rotateDegrees); + mCamera.startPreview(); + if (mParameters.getFocusMode() == Parameters.FOCUS_MODE_AUTO) { + mCamera.autoFocus(/* Camera.AutoFocusCallback */ null); + sendMessageDelayed(obtainMessage(MSG_AUTO_FOCUS), AUTOFOCUS_INTERVAL_MS); + } + return true; + } + + private class DecodingTask extends AsyncTask<Void, Void, String> { + private QrYuvLuminanceSource mImage; + private SurfaceTexture mSurface; + + private DecodingTask(SurfaceTexture surface) { + mSurface = surface; + } + + @Override + protected String doInBackground(Void... tmp) { + if (!initCamera(mSurface)) { + return null; + } + + final Semaphore imageGot = new Semaphore(0); + while (true) { + // This loop will try to capture preview image continuously until a valid QR Code + // decoded. The caller can also call {@link #stop()} to inturrupts scanning loop. + mCamera.setOneShotPreviewCallback( + (imageData, camera) -> { + mImage = getFrameImage(imageData); + imageGot.release(); + }); + try { + // Semaphore.acquire() blocking until permit is available, or the thread is + // interrupted. + imageGot.acquire(); + Result qrCode = null; + try { + qrCode = + mReader.decodeWithState( + new BinaryBitmap(new HybridBinarizer(mImage))); + } catch (ReaderException e) { + // No logging since every time the reader cannot decode the + // image, this ReaderException will be thrown. + } finally { + mReader.reset(); + } + if (qrCode != null) { + if (mScannerCallback.isValid(qrCode.getText())) { + return qrCode.getText(); + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return null; + } + } + } + + @Override + protected void onPostExecute(String qrCode) { + if (qrCode != null) { + mScannerCallback.handleSuccessfulResult(qrCode); + } + } + + private boolean initCamera(SurfaceTexture surface) { + final int numberOfCameras = Camera.getNumberOfCameras(); + Camera.CameraInfo cameraInfo = new Camera.CameraInfo(); + try { + for (int i = 0; i < numberOfCameras; ++i) { + Camera.getCameraInfo(i, cameraInfo); + if (cameraInfo.facing == CameraInfo.CAMERA_FACING_BACK) { + mCamera = Camera.open(i); + mCamera.setPreviewTexture(surface); + mCameraOrientation = cameraInfo.orientation; + break; + } + } + if (mCamera == null) { + Log.e(TAG, "Cannot find available back camera."); + mScannerCallback.handleCameraFailure(); + return false; + } + setCameraParameter(); + setTransformationMatrix(mScannerCallback.getViewSize()); + if (!startPreview()) { + Log.e(TAG, "Error to init Camera"); + mCamera = null; + mScannerCallback.handleCameraFailure(); + return false; + } + return true; + } catch (IOException e) { + Log.e(TAG, "Error to init Camera"); + mCamera = null; + mScannerCallback.handleCameraFailure(); + return false; + } + } + } + + /** Set transfom matrix to crop and center the preview picture */ + private void setTransformationMatrix(Size viewSize) { + // Check aspect ratio, can only handle square view. + final int viewRatio = (int)getRatio(viewSize.getWidth(), viewSize.getHeight()); + + final boolean isPortrait = mContext.get().getResources().getConfiguration().orientation + == Configuration.ORIENTATION_PORTRAIT ? true : false; + + final int previewWidth = isPortrait ? mPreviewSize.getWidth() : mPreviewSize.getHeight(); + final int previewHeight = isPortrait ? mPreviewSize.getHeight() : mPreviewSize.getWidth(); + final float ratioPreview = (float) getRatio(previewWidth, previewHeight); + + // Calculate transformation matrix. + float scaleX = 1.0f; + float scaleY = 1.0f; + if (previewWidth > previewHeight) { + scaleY = scaleX / ratioPreview; + } else { + scaleX = scaleY / ratioPreview; + } + + // Set the transform matrix. + final Matrix matrix = new Matrix(); + matrix.setScale(scaleX, scaleY); + mScannerCallback.setTransform(matrix); + } + + private QrYuvLuminanceSource getFrameImage(byte[] imageData) { + final Rect frame = mScannerCallback.getFramePosition(mPreviewSize, mCameraOrientation); + final QrYuvLuminanceSource image = new QrYuvLuminanceSource(imageData, + mPreviewSize.getWidth(), mPreviewSize.getHeight()); + return (QrYuvLuminanceSource) + image.crop(frame.left, frame.top, frame.width(), frame.height()); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_AUTO_FOCUS: + // Calling autoFocus(null) will only trigger the camera to focus once. In order + // to make the camera continuously auto focus during scanning, need to periodly + // trigger it. + mCamera.autoFocus(/* Camera.AutoFocusCallback */ null); + sendMessageDelayed(obtainMessage(MSG_AUTO_FOCUS), AUTOFOCUS_INTERVAL_MS); + break; + default: + Log.d(TAG, "Unexpected Message: " + msg.what); + } + } + + /** Get best preview size from the list of camera supported preview sizes. Compares the + * preview size and aspect ratio to choose the best one. */ + private Size getBestPreviewSize(Camera.Parameters parameters) { + final double minRatioDiffPercent = 0.1; + final Size windowSize = mScannerCallback.getViewSize(); + final double winRatio = getRatio(windowSize.getWidth(), windowSize.getHeight()); + double bestChoiceRatio = 0; + Size bestChoice = new Size(0, 0); + for (Camera.Size size : parameters.getSupportedPreviewSizes()) { + double ratio = getRatio(size.width, size.height); + if (size.height * size.width > bestChoice.getWidth() * bestChoice.getHeight() + && (Math.abs(bestChoiceRatio - winRatio) / winRatio > minRatioDiffPercent + || Math.abs(ratio - winRatio) / winRatio <= minRatioDiffPercent)) { + bestChoice = new Size(size.width, size.height); + bestChoiceRatio = getRatio(size.width, size.height); + } + } + return bestChoice; + } + + /** Get best picture size from the list of camera supported picture sizes. Compares the + * picture size and aspect ratio to choose the best one. */ + private Size getBestPictureSize(Camera.Parameters parameters) { + final Camera.Size previewSize = parameters.getPreviewSize(); + final double previewRatio = getRatio(previewSize.width, previewSize.height); + List<Size> bestChoices = new ArrayList<>(); + final List<Size> similarChoices = new ArrayList<>(); + + // Filter by ratio + for (Camera.Size size : parameters.getSupportedPictureSizes()) { + double ratio = getRatio(size.width, size.height); + if (ratio == previewRatio) { + bestChoices.add(new Size(size.width, size.height)); + } else if (Math.abs(ratio - previewRatio) < MAX_RATIO_DIFF) { + similarChoices.add(new Size(size.width, size.height)); + } + } + + if (bestChoices.size() == 0 && similarChoices.size() == 0) { + Log.d(TAG, "No proper picture size, return default picture size"); + Camera.Size defaultPictureSize = parameters.getPictureSize(); + return new Size(defaultPictureSize.width, defaultPictureSize.height); + } + + if (bestChoices.size() == 0) { + bestChoices = similarChoices; + } + + // Get the best by area + int bestAreaDifference = Integer.MAX_VALUE; + Size bestChoice = null; + final int previewArea = previewSize.width * previewSize.height; + for (Size size : bestChoices) { + int areaDifference = Math.abs(size.getWidth() * size.getHeight() - previewArea); + if (areaDifference < bestAreaDifference) { + bestAreaDifference = areaDifference; + bestChoice = size; + } + } + return bestChoice; + } + + private double getRatio(double x, double y) { + return (x < y) ? x / y : y / x; + } + + @VisibleForTesting + protected void decodeImage(BinaryBitmap image) { + Result qrCode = null; + + try { + qrCode = mReader.decodeWithState(image); + } catch (ReaderException e) { + } finally { + mReader.reset(); + } + + if (qrCode != null) { + mScannerCallback.handleSuccessfulResult(qrCode.getText()); + } + } + + /** + * After {@link #start(SurfaceTexture)}, DecodingTask runs continuously to capture images and + * decode QR code. DecodingTask become null After {@link #stop()}. + * + * Uses this method in test case to prevent power consumption problem. + */ + public boolean isDecodeTaskAlive() { + return mDecodeTask != null; + } +} diff --git a/src/com/android/settings/wifi/qrcode/QrCodeGenerator.java b/src/com/android/settings/wifi/qrcode/QrCodeGenerator.java new file mode 100644 index 0000000000..d9682d2421 --- /dev/null +++ b/src/com/android/settings/wifi/qrcode/QrCodeGenerator.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2018 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.wifi.qrcode; + +import android.graphics.Bitmap; +import android.graphics.Color; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; +import com.google.zxing.MultiFormatWriter; +import com.google.zxing.WriterException; +import com.google.zxing.common.BitMatrix; + +import java.nio.charset.CharsetEncoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +public final class QrCodeGenerator { + /** + * Generates a barcode image with {@code contents}. + * + * @param contents The contents to encode in the barcode + * @param size The preferred image size in pixels + * @return Barcode bitmap + */ + public static Bitmap encodeQrCode(String contents, int size) + throws WriterException, IllegalArgumentException { + final Map<EncodeHintType, Object> hints = new HashMap<>(); + if (!isIso88591(contents)) { + hints.put(EncodeHintType.CHARACTER_SET, StandardCharsets.UTF_8.name()); + } + + final BitMatrix qrBits = new MultiFormatWriter().encode(contents, BarcodeFormat.QR_CODE, + size, size, hints); + final Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.RGB_565); + for (int x = 0; x < size; x++) { + for (int y = 0; y < size; y++) { + bitmap.setPixel(x, y, qrBits.get(x, y) ? Color.BLACK : Color.WHITE); + } + } + return bitmap; + } + + private static boolean isIso88591(String contents) { + CharsetEncoder encoder = StandardCharsets.ISO_8859_1.newEncoder(); + return encoder.canEncode(contents); + } +} diff --git a/src/com/android/settings/wifi/qrcode/QrDecorateView.java b/src/com/android/settings/wifi/qrcode/QrDecorateView.java new file mode 100644 index 0000000000..5df0cd0a7c --- /dev/null +++ b/src/com/android/settings/wifi/qrcode/QrDecorateView.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2018 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.wifi.qrcode; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.View; + +import com.android.settings.R; + +/** + * Draws the lines at the corner of the inner frame. + */ +public class QrDecorateView extends View { + private static final float CORNER_STROKE_WIDTH = 4f; // 4dp + private static final float CORNER_LINE_LENGTH = 264f; // 264dp + private static final float CORNER_RADIUS = 16f; // 16dp + + final private int mCornerColor; + final private int mFocusedCornerColor; + final private int mBackgroundColor; + + final private Paint mStrokePaint; + final private Paint mTransparentPaint; + final private Paint mBackgroundPaint; + + final private float mRadius; + final private float mInnerRidus; + + private Bitmap mMaskBitmap; + private Canvas mMaskCanvas; + + private RectF mOuterFrame; + private RectF mInnerFrame; + + private boolean mFocused; + + public QrDecorateView(Context context) { + this(context, null); + } + + public QrDecorateView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public QrDecorateView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public QrDecorateView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + mFocused = false; + mRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, CORNER_RADIUS, + getResources().getDisplayMetrics()); + // Inner radius needs to minus stroke width for keeping the width of border consistent. + mInnerRidus = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + CORNER_RADIUS - CORNER_STROKE_WIDTH, getResources().getDisplayMetrics()); + + mCornerColor = context.getResources().getColor(R.color.qr_corner_line_color); + mFocusedCornerColor = context.getResources().getColor(R.color.qr_focused_corner_line_color); + mBackgroundColor = context.getResources().getColor(R.color.qr_background_color); + + mStrokePaint = new Paint(); + mStrokePaint.setAntiAlias(true); + + mTransparentPaint = new Paint(); + mTransparentPaint.setAntiAlias(true); + mTransparentPaint.setColor(getResources().getColor(android.R.color.transparent)); + mTransparentPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); + + mBackgroundPaint = new Paint(); + mBackgroundPaint.setColor(mBackgroundColor); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + + if(mMaskBitmap == null) { + mMaskBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888); + mMaskCanvas = new Canvas(mMaskBitmap); + } + + calculateFramePos(); + } + + @Override + protected void onDraw(Canvas canvas) { + // Set frame line color. + mStrokePaint.setColor(mFocused ? mFocusedCornerColor : mCornerColor); + // Draw background color. + mMaskCanvas.drawColor(mBackgroundColor); + // Draw outer corner. + mMaskCanvas.drawRoundRect(mOuterFrame, mRadius, mRadius, mStrokePaint); + // Draw inner transparent corner. + mMaskCanvas.drawRoundRect(mInnerFrame, mInnerRidus, mInnerRidus, mTransparentPaint); + + canvas.drawBitmap(mMaskBitmap, 0, 0, mBackgroundPaint); + super.onDraw(canvas); + } + + private void calculateFramePos() { + final int centralX = getWidth() / 2; + final int centralY = getHeight() / 2; + final float cornerLineLength = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + CORNER_LINE_LENGTH, getResources().getDisplayMetrics()) / 2; + final float strokeWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + CORNER_STROKE_WIDTH, getResources().getDisplayMetrics()); + + mOuterFrame = new RectF(centralX - cornerLineLength, centralY - cornerLineLength, + centralX + cornerLineLength, centralY + cornerLineLength); + mInnerFrame = new RectF(mOuterFrame.left + strokeWidth, mOuterFrame.top + strokeWidth, + mOuterFrame.right - strokeWidth, mOuterFrame.bottom - strokeWidth); + } + + // Draws green lines if focused. Otherwise, draws white lines. + public void setFocused(boolean focused) { + mFocused = focused; + invalidate(); + } +} diff --git a/src/com/android/settings/wifi/qrcode/QrYuvLuminanceSource.java b/src/com/android/settings/wifi/qrcode/QrYuvLuminanceSource.java new file mode 100644 index 0000000000..874a758642 --- /dev/null +++ b/src/com/android/settings/wifi/qrcode/QrYuvLuminanceSource.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2018 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.wifi.qrcode; + +import com.google.zxing.LuminanceSource; + +/** + * This helper class implements crop method to crop preview picture. + */ +public class QrYuvLuminanceSource extends LuminanceSource { + + private byte[] mYuvData; + private int mWidth; + private int mHeight; + + public QrYuvLuminanceSource(byte[] yuvData, int width, int height) { + super(width, height); + + mWidth = width; + mHeight = height; + mYuvData = yuvData; + } + + @Override + public boolean isCropSupported() { + return true; + } + + @Override + public LuminanceSource crop(int left, int top, int crop_width, int crop_height) { + final byte[] newImage = new byte[crop_width * crop_height]; + int inputOffset = top * mWidth + left; + + if (left + crop_width > mWidth || top + crop_height > mHeight) { + throw new IllegalArgumentException("cropped rectangle does not fit within image data."); + } + + for (int y = 0; y < crop_height; y++) { + System.arraycopy(mYuvData, inputOffset, newImage, y * crop_width, crop_width); + inputOffset += mWidth; + } + return new QrYuvLuminanceSource(newImage, crop_width, crop_height); + } + + @Override + public byte[] getRow(int y, byte[] row) { + if (y < 0 || y >= mHeight) { + throw new IllegalArgumentException("Requested row is outside the image: " + y); + } + if (row == null || row.length < mWidth) { + row = new byte[mWidth]; + } + System.arraycopy(mYuvData, y * mWidth, row, 0, mWidth); + return row; + } + + @Override + public byte[] getMatrix() { + return mYuvData; + } +} diff --git a/src/com/android/settings/wifi/savedaccesspoints/SavedAccessPointsPreferenceController.java b/src/com/android/settings/wifi/savedaccesspoints/SavedAccessPointsPreferenceController.java new file mode 100644 index 0000000000..82c6028738 --- /dev/null +++ b/src/com/android/settings/wifi/savedaccesspoints/SavedAccessPointsPreferenceController.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2018 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.wifi.savedaccesspoints; + + +import android.content.Context; +import android.net.wifi.WifiManager; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceGroup; +import androidx.preference.PreferenceScreen; + +import com.android.settings.core.BasePreferenceController; +import com.android.settings.utils.PreferenceGroupChildrenCache; +import com.android.settings.R; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.utils.ThreadUtils; +import com.android.settingslib.wifi.AccessPoint; +import com.android.settingslib.wifi.AccessPointPreference; +import com.android.settingslib.wifi.AccessPointPreference.UserBadgeCache; +import com.android.settingslib.wifi.WifiSavedConfigUtils; + +import java.util.Collections; +import java.util.List; + +/** + * Controller that manages a PreferenceGroup, which contains a list of saved access points. + */ +public class SavedAccessPointsPreferenceController extends BasePreferenceController implements + LifecycleObserver, OnStart, Preference.OnPreferenceClickListener, + WifiManager.ActionListener { + + private static final String TAG = "SavedAPPrefCtrl"; + + private final WifiManager mWifiManager; + private final PreferenceGroupChildrenCache mChildrenCache; + + private final UserBadgeCache mUserBadgeCache; + private PreferenceGroup mPreferenceGroup; + private SavedAccessPointsWifiSettings mHost; + + public SavedAccessPointsPreferenceController(Context context, + String preferenceKey) { + super(context, preferenceKey); + mUserBadgeCache = new AccessPointPreference.UserBadgeCache(context.getPackageManager()); + mWifiManager = context.getSystemService(WifiManager.class); + mChildrenCache = new PreferenceGroupChildrenCache(); + } + + public SavedAccessPointsPreferenceController setHost(SavedAccessPointsWifiSettings host) { + mHost = host; + return this; + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreferenceGroup = screen.findPreference(getPreferenceKey()); + } + + @Override + public void onStart() { + refreshSavedAccessPoints(); + } + + public void postRefreshSavedAccessPoints() { + ThreadUtils.postOnMainThread(() -> refreshSavedAccessPoints()); + } + + @Override + public boolean onPreferenceClick(Preference preference) { + if (mHost != null) { + mHost.showWifiPage((AccessPointPreference) preference); + } + return false; + } + + @Override + public void onSuccess() { + postRefreshSavedAccessPoints(); + } + + @Override + public void onFailure(int reason) { + postRefreshSavedAccessPoints(); + } + + @VisibleForTesting + void refreshSavedAccessPoints() { + if (mPreferenceGroup == null) { + Log.w(TAG, "PreferenceGroup is null, skipping."); + return; + } + final Context prefContext = mPreferenceGroup.getContext(); + + final List<AccessPoint> accessPoints = + WifiSavedConfigUtils.getAllConfigs(mContext, mWifiManager); + Collections.sort(accessPoints, SavedNetworkComparator.INSTANCE); + mChildrenCache.cacheRemoveAllPrefs(mPreferenceGroup); + + final int accessPointsSize = accessPoints.size(); + for (int i = 0; i < accessPointsSize; ++i) { + AccessPoint ap = accessPoints.get(i); + + if (mHost != null && mHost.isSubscriptionsFeatureEnabled() + && ap.isPasspointConfig()) { + continue; + } + + String key = ap.getKey(); + AccessPointPreference preference = + (AccessPointPreference) mChildrenCache.getCachedPreference(key); + if (preference == null) { + preference = new AccessPointPreference(ap, prefContext, mUserBadgeCache, true); + preference.setKey(key); + preference.setIcon(null); + preference.setOnPreferenceClickListener(this); + mPreferenceGroup.addPreference(preference); + } + preference.setOrder(i); + } + + mChildrenCache.removeCachedPrefs(mPreferenceGroup); + + if (mPreferenceGroup.getPreferenceCount() < 1) { + Log.w(TAG, "Saved networks activity loaded, but there are no saved networks!"); + mPreferenceGroup.setVisible(false); + } else { + mPreferenceGroup.setVisible(true); + } + + if (mHost != null && !mHost.isSubscriptionsFeatureEnabled()) { + mPreferenceGroup.setVisible(true); + mPreferenceGroup.setTitle(null); + mPreferenceGroup.setLayoutResource(R.layout.preference_category_no_label); + } + } +} diff --git a/src/com/android/settings/wifi/savedaccesspoints/SavedAccessPointsWifiSettings.java b/src/com/android/settings/wifi/savedaccesspoints/SavedAccessPointsWifiSettings.java new file mode 100644 index 0000000000..4daf7da550 --- /dev/null +++ b/src/com/android/settings/wifi/savedaccesspoints/SavedAccessPointsWifiSettings.java @@ -0,0 +1,131 @@ +/* + * 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.wifi.savedaccesspoints; + +import android.annotation.Nullable; +import android.app.Dialog; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.DialogInterface; +import android.net.wifi.WifiManager; +import android.os.Bundle; +import android.util.FeatureFlagUtils; +import android.util.Log; + +import com.android.settings.R; +import com.android.settings.core.FeatureFlags; +import com.android.settings.core.SubSettingLauncher; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.development.featureflags.FeatureFlagPersistent; +import com.android.settings.wifi.WifiConfigUiBase; +import com.android.settings.wifi.WifiDialog; +import com.android.settings.wifi.WifiSettings; +import com.android.settings.wifi.details.WifiNetworkDetailsFragment; +import com.android.settingslib.wifi.AccessPoint; +import com.android.settingslib.wifi.AccessPointPreference; + +/** + * UI to manage saved networks/access points. + */ +public class SavedAccessPointsWifiSettings extends DashboardFragment { + + private static final String TAG = "SavedAccessPoints"; + + private Bundle mAccessPointSavedState; + private AccessPoint mSelectedAccessPoint; + + // Instance state key + private static final String SAVE_DIALOG_ACCESS_POINT_STATE = "wifi_ap_state"; + + @Override + public int getMetricsCategory() { + return SettingsEnums.WIFI_SAVED_ACCESS_POINTS; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.wifi_display_saved_access_points; + } + + @Override + protected String getLogTag() { + return TAG; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + use(SavedAccessPointsPreferenceController.class) + .setHost(this); + use(SubscribedAccessPointsPreferenceController.class) + .setHost(this); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (savedInstanceState != null) { + if (savedInstanceState.containsKey(SAVE_DIALOG_ACCESS_POINT_STATE)) { + mAccessPointSavedState = + savedInstanceState.getBundle(SAVE_DIALOG_ACCESS_POINT_STATE); + } + } + } + + public void showWifiPage(@Nullable AccessPointPreference accessPoint) { + removeDialog(WifiSettings.WIFI_DIALOG_ID); + + if (accessPoint != null) { + // Save the access point and edit mode + mSelectedAccessPoint = accessPoint.getAccessPoint(); + } else { + // No access point is selected. Clear saved state. + mSelectedAccessPoint = null; + mAccessPointSavedState = null; + } + + if (mSelectedAccessPoint == null) { + mSelectedAccessPoint = new AccessPoint(getActivity(), mAccessPointSavedState); + } + final Bundle savedState = new Bundle(); + mSelectedAccessPoint.saveWifiState(savedState); + + new SubSettingLauncher(getContext()) + .setTitleText(mSelectedAccessPoint.getTitle()) + .setDestination(WifiNetworkDetailsFragment.class.getName()) + .setArguments(savedState) + .setSourceMetricsCategory(getMetricsCategory()) + .launch(); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + // If the dialog is showing (indicated by the existence of mSelectedAccessPoint), then we + // save its state. + if (mSelectedAccessPoint != null) { + mAccessPointSavedState = new Bundle(); + mSelectedAccessPoint.saveWifiState(mAccessPointSavedState); + outState.putBundle(SAVE_DIALOG_ACCESS_POINT_STATE, mAccessPointSavedState); + } + } + + boolean isSubscriptionsFeatureEnabled() { + return FeatureFlagUtils.isEnabled(getContext(), FeatureFlags.MOBILE_NETWORK_V2) + && FeatureFlagPersistent.isEnabled(getContext(), FeatureFlags.NETWORK_INTERNET_V2); + } +} diff --git a/src/com/android/settings/wifi/savedaccesspoints/SavedNetworkComparator.java b/src/com/android/settings/wifi/savedaccesspoints/SavedNetworkComparator.java new file mode 100644 index 0000000000..cff438787f --- /dev/null +++ b/src/com/android/settings/wifi/savedaccesspoints/SavedNetworkComparator.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2018 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.wifi.savedaccesspoints; + +import android.icu.text.Collator; + +import com.android.settingslib.wifi.AccessPoint; + +import java.util.Comparator; + +public final class SavedNetworkComparator { + public static final Comparator<AccessPoint> INSTANCE = + new Comparator<AccessPoint>() { + final Collator mCollator = Collator.getInstance(); + + @Override + public int compare(AccessPoint ap1, AccessPoint ap2) { + return mCollator.compare( + nullToEmpty(ap1.getConfigName()), nullToEmpty(ap2.getConfigName())); + } + + private String nullToEmpty(String string) { + return (string == null) ? "" : string; + } + }; +} diff --git a/src/com/android/settings/wifi/savedaccesspoints/SubscribedAccessPointsPreferenceController.java b/src/com/android/settings/wifi/savedaccesspoints/SubscribedAccessPointsPreferenceController.java new file mode 100644 index 0000000000..048999a581 --- /dev/null +++ b/src/com/android/settings/wifi/savedaccesspoints/SubscribedAccessPointsPreferenceController.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2019 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.wifi.savedaccesspoints; + + +import android.content.Context; +import android.net.wifi.WifiManager; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceGroup; +import androidx.preference.PreferenceScreen; + +import com.android.settings.core.BasePreferenceController; +import com.android.settings.utils.PreferenceGroupChildrenCache; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.utils.ThreadUtils; +import com.android.settingslib.wifi.AccessPoint; +import com.android.settingslib.wifi.AccessPointPreference; +import com.android.settingslib.wifi.AccessPointPreference.UserBadgeCache; +import com.android.settingslib.wifi.WifiSavedConfigUtils; + +import java.util.Collections; +import java.util.List; + +/** + * Controller that manages a PreferenceGroup, which contains a list of subscribed access points. + */ +// TODO(b/127206629): Code refactor to avoid duplicated coding after removed feature flag. +public class SubscribedAccessPointsPreferenceController extends BasePreferenceController implements + LifecycleObserver, OnStart, Preference.OnPreferenceClickListener, + WifiManager.ActionListener { + + private static final String TAG = "SubscribedAPPrefCtrl"; + + private final WifiManager mWifiManager; + private final PreferenceGroupChildrenCache mChildrenCache; + private final UserBadgeCache mUserBadgeCache; + private PreferenceGroup mPreferenceGroup; + private SavedAccessPointsWifiSettings mHost; + + public SubscribedAccessPointsPreferenceController(Context context, + String preferenceKey) { + super(context, preferenceKey); + mUserBadgeCache = new AccessPointPreference.UserBadgeCache(context.getPackageManager()); + mWifiManager = context.getSystemService(WifiManager.class); + mChildrenCache = new PreferenceGroupChildrenCache(); + } + + public SubscribedAccessPointsPreferenceController setHost(SavedAccessPointsWifiSettings host) { + mHost = host; + return this; + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreferenceGroup = screen.findPreference(getPreferenceKey()); + } + + @Override + public void onStart() { + refreshSubscribedAccessPoints(); + } + + public void postRefreshSubscribedAccessPoints() { + ThreadUtils.postOnMainThread(() -> refreshSubscribedAccessPoints()); + } + + @Override + public boolean onPreferenceClick(Preference preference) { + if (mHost != null) { + mHost.showWifiPage((AccessPointPreference) preference); + } + return false; + } + + @Override + public void onSuccess() { + postRefreshSubscribedAccessPoints(); + } + + @Override + public void onFailure(int reason) { + postRefreshSubscribedAccessPoints(); + } + + @VisibleForTesting + void refreshSubscribedAccessPoints() { + if (mPreferenceGroup == null) { + Log.w(TAG, "PreferenceGroup is null, skipping."); + return; + } + + if (mHost != null && !mHost.isSubscriptionsFeatureEnabled()) { + mPreferenceGroup.setVisible(false); + return; + } + + final Context prefContext = mPreferenceGroup.getContext(); + + final List<AccessPoint> accessPoints = + WifiSavedConfigUtils.getAllConfigs(mContext, mWifiManager); + Collections.sort(accessPoints, SavedNetworkComparator.INSTANCE); + mChildrenCache.cacheRemoveAllPrefs(mPreferenceGroup); + + final int accessPointsSize = accessPoints.size(); + for (int i = 0; i < accessPointsSize; ++i) { + AccessPoint ap = accessPoints.get(i); + if (!ap.isPasspointConfig()) { + continue; + } + + final String key = ap.getKey(); + AccessPointPreference preference = + (AccessPointPreference) mChildrenCache.getCachedPreference(key); + if (preference == null) { + preference = new AccessPointPreference(ap, prefContext, mUserBadgeCache, true); + preference.setKey(key); + preference.setIcon(null); + preference.setOnPreferenceClickListener(this); + mPreferenceGroup.addPreference(preference); + } + preference.setOrder(i); + } + + mChildrenCache.removeCachedPrefs(mPreferenceGroup); + + if (mPreferenceGroup.getPreferenceCount() < 1) { + Log.w(TAG, "Subscribed networks activity loaded," + + " but there are no subscribed networks!"); + mPreferenceGroup.setVisible(false); + } else { + mPreferenceGroup.setVisible(true); + } + } +} diff --git a/src/com/android/settings/wifi/slice/ConnectToWifiHandler.java b/src/com/android/settings/wifi/slice/ConnectToWifiHandler.java new file mode 100644 index 0000000000..5c92d81da2 --- /dev/null +++ b/src/com/android/settings/wifi/slice/ConnectToWifiHandler.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2019 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.wifi.slice; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.wifi.WifiManager; +import android.os.Bundle; + +import androidx.annotation.VisibleForTesting; + +import com.android.settings.wifi.WifiConnectListener; +import com.android.settings.wifi.WifiDialogActivity; +import com.android.settings.wifi.WifiUtils; +import com.android.settingslib.wifi.AccessPoint; + +/** + * This receiver helps connect to Wi-Fi network + */ +public class ConnectToWifiHandler extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + if (context == null || intent == null) { + return; + } + + final Network network = intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK); + final Bundle accessPointState = intent.getBundleExtra( + WifiDialogActivity.KEY_ACCESS_POINT_STATE); + + if (network != null) { + WifiScanWorker.clearClickedWifi(); + final ConnectivityManager cm = context.getSystemService(ConnectivityManager.class); + // start captive portal app to sign in to network + cm.startCaptivePortalApp(network); + } else if (accessPointState != null) { + connect(context, new AccessPoint(context, accessPointState)); + } + } + + @VisibleForTesting + void connect(Context context, AccessPoint accessPoint) { + ContextualWifiScanWorker.saveSession(); + WifiScanWorker.saveClickedWifi(accessPoint); + + final WifiConnectListener connectListener = new WifiConnectListener(context); + switch (WifiUtils.getConnectingType(accessPoint)) { + case WifiUtils.CONNECT_TYPE_OSU_PROVISION: + accessPoint.startOsuProvisioning(connectListener); + break; + + case WifiUtils.CONNECT_TYPE_OPEN_NETWORK: + accessPoint.generateOpenNetworkConfig(); + + case WifiUtils.CONNECT_TYPE_SAVED_NETWORK: + final WifiManager wifiManager = context.getSystemService(WifiManager.class); + wifiManager.connect(accessPoint.getConfig(), connectListener); + break; + } + } +} diff --git a/src/com/android/settings/wifi/slice/ContextualWifiScanWorker.java b/src/com/android/settings/wifi/slice/ContextualWifiScanWorker.java new file mode 100644 index 0000000000..5e69b8add6 --- /dev/null +++ b/src/com/android/settings/wifi/slice/ContextualWifiScanWorker.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2019 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.wifi.slice; + +import android.content.Context; +import android.net.Uri; +import android.os.SystemClock; + +import com.android.settings.slices.SliceBackgroundWorker; + +/** + * {@link SliceBackgroundWorker} for Wi-Fi, used by {@link ContextualWifiSlice}. + */ +public class ContextualWifiScanWorker extends WifiScanWorker { + + private static long sVisibleUiSessionToken; + private static long sActiveSession; + + public ContextualWifiScanWorker(Context context, Uri uri) { + super(context, uri); + } + + /** + * Starts a new visible UI session for the purpose of automatically starting captive portal. + * + * A visible UI session is defined as a duration of time when a UI screen is visible to user. + * Going to a sub-page and coming out breaks the continuation, leaving the page and coming back + * breaks it too. + */ + public static void newVisibleUiSession() { + sVisibleUiSessionToken = SystemClock.elapsedRealtime(); + } + + static void saveSession() { + sActiveSession = sVisibleUiSessionToken; + } + + @Override + protected void clearClickedWifiOnSliceUnpinned() { + // Do nothing for contextual Wi-Fi slice + } + + @Override + protected boolean isSessionValid() { + if (sVisibleUiSessionToken != sActiveSession) { + clearClickedWifi(); + return false; + } + return true; + } +}
\ No newline at end of file diff --git a/src/com/android/settings/wifi/slice/ContextualWifiSlice.java b/src/com/android/settings/wifi/slice/ContextualWifiSlice.java new file mode 100644 index 0000000000..97b9241abd --- /dev/null +++ b/src/com/android/settings/wifi/slice/ContextualWifiSlice.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2018 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.wifi.slice; + +import android.content.Context; +import android.net.NetworkCapabilities; +import android.net.Uri; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.net.wifi.WifiSsid; +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; +import androidx.slice.Slice; + +import com.android.settings.overlay.FeatureFactory; +import com.android.settings.slices.CustomSliceRegistry; +import com.android.settings.slices.CustomSliceable; + +/** + * {@link CustomSliceable} for Wi-Fi, used by contextual homepage. + */ +public class ContextualWifiSlice extends WifiSlice { + + private static final String TAG = "ContextualWifiSlice"; + @VisibleForTesting + static long sActiveUiSession = -1000; + @VisibleForTesting + static boolean sPreviouslyDisplayed; + + public ContextualWifiSlice(Context context) { + super(context); + } + + @Override + public Uri getUri() { + return CustomSliceRegistry.CONTEXTUAL_WIFI_SLICE_URI; + } + + @Override + public Slice getSlice() { + final long currentUiSession = FeatureFactory.getFactory(mContext) + .getSlicesFeatureProvider().getUiSessionToken(); + if (currentUiSession != sActiveUiSession) { + sActiveUiSession = currentUiSession; + sPreviouslyDisplayed = false; + } + if (!sPreviouslyDisplayed && hasWorkingNetwork()) { + Log.d(TAG, "Wifi is connected, no point showing any suggestion."); + return null; + } + // Set sPreviouslyDisplayed to true - we will show *something* on the screen. So we should + // keep showing this card to keep UI stable, even if wifi connects to a network later. + sPreviouslyDisplayed = true; + + return super.getSlice(); + } + + private boolean hasWorkingNetwork() { + return !TextUtils.equals(getActiveSSID(), WifiSsid.NONE) && hasInternetAccess(); + } + + private String getActiveSSID() { + if (mWifiManager.getWifiState() != WifiManager.WIFI_STATE_ENABLED) { + return WifiSsid.NONE; + } + return WifiInfo.removeDoubleQuotes(mWifiManager.getConnectionInfo().getSSID()); + } + + private boolean hasInternetAccess() { + final NetworkCapabilities nc = mConnectivityManager.getNetworkCapabilities( + mWifiManager.getCurrentNetwork()); + return nc != null + && !nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL) + && !nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_PARTIAL_CONNECTIVITY) + && nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED); + } + + @Override + public Class getBackgroundWorkerClass() { + return ContextualWifiScanWorker.class; + } +} diff --git a/src/com/android/settings/wifi/slice/WifiScanWorker.java b/src/com/android/settings/wifi/slice/WifiScanWorker.java new file mode 100644 index 0000000000..a5bd74dea6 --- /dev/null +++ b/src/com/android/settings/wifi/slice/WifiScanWorker.java @@ -0,0 +1,253 @@ +/* + * Copyright (C) 2019 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.wifi.slice; + +import static android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL; +import static android.net.NetworkCapabilities.NET_CAPABILITY_PARTIAL_CONNECTIVITY; +import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI; + +import static com.android.settings.wifi.slice.WifiSlice.DEFAULT_EXPANDED_ROW_COUNT; + +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.ConnectivityManager.NetworkCallback; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.net.Uri; +import android.net.wifi.WifiInfo; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.UserHandle; +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; + +import com.android.internal.util.Preconditions; +import com.android.settings.slices.SliceBackgroundWorker; +import com.android.settingslib.wifi.AccessPoint; +import com.android.settingslib.wifi.WifiTracker; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@link SliceBackgroundWorker} for Wi-Fi, used by {@link WifiSlice}. + */ +public class WifiScanWorker extends SliceBackgroundWorker<AccessPoint> implements + WifiTracker.WifiListener { + + private static final String TAG = "WifiScanWorker"; + + @VisibleForTesting + WifiNetworkCallback mNetworkCallback; + + private final Context mContext; + private final ConnectivityManager mConnectivityManager; + private final WifiTracker mWifiTracker; + + private static String sClickedWifiSsid; + + public WifiScanWorker(Context context, Uri uri) { + super(context, uri); + mContext = context; + mConnectivityManager = context.getSystemService(ConnectivityManager.class); + mWifiTracker = new WifiTracker(mContext, this /* wifiListener */, + true /* includeSaved */, true /* includeScans */); + } + + @Override + protected void onSlicePinned() { + mWifiTracker.onStart(); + onAccessPointsChanged(); + } + + @Override + protected void onSliceUnpinned() { + mWifiTracker.onStop(); + unregisterNetworkCallback(); + clearClickedWifiOnSliceUnpinned(); + } + + @Override + public void close() { + mWifiTracker.onDestroy(); + } + + @Override + public void onWifiStateChanged(int state) { + notifySliceChange(); + } + + @Override + public void onConnectedChanged() { + } + + @Override + public void onAccessPointsChanged() { + // in case state has changed + if (!mWifiTracker.getManager().isWifiEnabled()) { + updateResults(null); + return; + } + // AccessPoints are sorted by the WifiTracker + final List<AccessPoint> accessPoints = mWifiTracker.getAccessPoints(); + final List<AccessPoint> resultList = new ArrayList<>(); + for (AccessPoint ap : accessPoints) { + if (ap.isReachable()) { + resultList.add(clone(ap)); + if (resultList.size() >= DEFAULT_EXPANDED_ROW_COUNT) { + break; + } + } + } + updateResults(resultList); + } + + private AccessPoint clone(AccessPoint accessPoint) { + final Bundle savedState = new Bundle(); + accessPoint.saveWifiState(savedState); + return new AccessPoint(mContext, savedState); + } + + @Override + protected boolean areListsTheSame(List<AccessPoint> a, List<AccessPoint> b) { + if (!a.equals(b)) { + return false; + } + + // compare access point states one by one + final int listSize = a.size(); + for (int i = 0; i < listSize; i++) { + if (a.get(i).getDetailedState() != b.get(i).getDetailedState()) { + return false; + } + } + return true; + } + + static void saveClickedWifi(AccessPoint accessPoint) { + sClickedWifiSsid = accessPoint.getSsidStr(); + } + + static void clearClickedWifi() { + sClickedWifiSsid = null; + } + + static boolean isWifiClicked(WifiInfo info) { + final String ssid = WifiInfo.removeDoubleQuotes(info.getSSID()); + return !TextUtils.isEmpty(ssid) && TextUtils.equals(ssid, sClickedWifiSsid); + } + + protected void clearClickedWifiOnSliceUnpinned() { + clearClickedWifi(); + } + + protected boolean isSessionValid() { + return true; + } + + public void registerNetworkCallback(Network wifiNetwork) { + if (wifiNetwork == null) { + return; + } + + if (mNetworkCallback != null && mNetworkCallback.isSameNetwork(wifiNetwork)) { + return; + } + + unregisterNetworkCallback(); + + mNetworkCallback = new WifiNetworkCallback(wifiNetwork); + mConnectivityManager.registerNetworkCallback( + new NetworkRequest.Builder() + .clearCapabilities() + .addTransportType(TRANSPORT_WIFI) + .build(), + mNetworkCallback, + new Handler(Looper.getMainLooper())); + } + + public void unregisterNetworkCallback() { + if (mNetworkCallback != null) { + try { + mConnectivityManager.unregisterNetworkCallback(mNetworkCallback); + } catch (RuntimeException e) { + Log.e(TAG, "Unregistering CaptivePortalNetworkCallback failed.", e); + } + mNetworkCallback = null; + } + } + + class WifiNetworkCallback extends NetworkCallback { + + private final Network mNetwork; + private boolean mIsCaptivePortal; + private boolean mHasPartialConnectivity; + private boolean mIsValidated; + + WifiNetworkCallback(Network network) { + mNetwork = Preconditions.checkNotNull(network); + } + + @Override + public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) { + if (!isSameNetwork(network)) { + return; + } + + final boolean prevIsCaptivePortal = mIsCaptivePortal; + final boolean prevHasPartialConnectivity = mHasPartialConnectivity; + final boolean prevIsValidated = mIsValidated; + + mIsCaptivePortal = nc.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL); + mHasPartialConnectivity = nc.hasCapability(NET_CAPABILITY_PARTIAL_CONNECTIVITY); + mIsValidated = nc.hasCapability(NET_CAPABILITY_VALIDATED); + + if (prevIsCaptivePortal == mIsCaptivePortal + && prevHasPartialConnectivity == mHasPartialConnectivity + && prevIsValidated == mIsValidated) { + return; + } + + notifySliceChange(); + + // Automatically start captive portal + if (!prevIsCaptivePortal && mIsCaptivePortal + && isWifiClicked(mWifiTracker.getManager().getConnectionInfo()) + && isSessionValid()) { + final Intent intent = new Intent(mContext, ConnectToWifiHandler.class) + .putExtra(ConnectivityManager.EXTRA_NETWORK, network) + .addFlags(Intent.FLAG_RECEIVER_FOREGROUND); + // Sending a broadcast in the system process needs to specify a user + mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT); + } + } + + /** + * Returns true if the supplied network is not null and is the same as the originally + * supplied value. + */ + public boolean isSameNetwork(Network network) { + return mNetwork.equals(network); + } + } +} diff --git a/src/com/android/settings/wifi/slice/WifiSlice.java b/src/com/android/settings/wifi/slice/WifiSlice.java new file mode 100644 index 0000000000..e5c8de59f8 --- /dev/null +++ b/src/com/android/settings/wifi/slice/WifiSlice.java @@ -0,0 +1,357 @@ +/* + * Copyright (C) 2018 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.wifi.slice; + +import static android.app.slice.Slice.EXTRA_TOGGLE_STATE; +import static android.provider.SettingsSlicesContract.KEY_WIFI; + +import static com.android.settings.slices.CustomSliceRegistry.WIFI_SLICE_URI; + +import android.annotation.ColorInt; +import android.app.PendingIntent; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.Intent; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.net.ConnectivityManager; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo; +import android.net.Uri; +import android.net.wifi.WifiManager; +import android.os.Bundle; +import android.text.TextUtils; + +import androidx.annotation.VisibleForTesting; +import androidx.core.graphics.drawable.IconCompat; +import androidx.slice.Slice; +import androidx.slice.builders.ListBuilder; +import androidx.slice.builders.SliceAction; + +import com.android.settings.R; +import com.android.settings.SubSettings; +import com.android.settings.Utils; +import com.android.settings.core.SubSettingLauncher; +import com.android.settings.slices.CustomSliceable; +import com.android.settings.slices.SliceBackgroundWorker; +import com.android.settings.slices.SliceBuilderUtils; +import com.android.settings.wifi.WifiDialogActivity; +import com.android.settings.wifi.WifiSettings; +import com.android.settings.wifi.WifiUtils; +import com.android.settings.wifi.details.WifiNetworkDetailsFragment; +import com.android.settingslib.wifi.AccessPoint; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * {@link CustomSliceable} for Wi-Fi, used by generic clients. + */ +public class WifiSlice implements CustomSliceable { + + @VisibleForTesting + static final int DEFAULT_EXPANDED_ROW_COUNT = 3; + + protected final Context mContext; + protected final WifiManager mWifiManager; + protected final ConnectivityManager mConnectivityManager; + + public WifiSlice(Context context) { + mContext = context; + mWifiManager = mContext.getSystemService(WifiManager.class); + mConnectivityManager = mContext.getSystemService(ConnectivityManager.class); + } + + @Override + public Uri getUri() { + return WIFI_SLICE_URI; + } + + @Override + public Slice getSlice() { + // Reload theme for switching dark mode on/off + mContext.getTheme().applyStyle(R.style.Theme_Settings_Home, true /* force */); + + final boolean isWifiEnabled = isWifiEnabled(); + ListBuilder listBuilder = getHeaderRow(isWifiEnabled); + if (!isWifiEnabled) { + WifiScanWorker.clearClickedWifi(); + return listBuilder.build(); + } + + final WifiScanWorker worker = SliceBackgroundWorker.getInstance(getUri()); + final List<AccessPoint> apList = worker != null ? worker.getResults() : null; + final int apCount = apList == null ? 0 : apList.size(); + final boolean isFirstApActive = apCount > 0 && apList.get(0).isActive(); + handleNetworkCallback(worker, isFirstApActive); + + // Need a loading text when results are not ready or out of date. + boolean needLoadingRow = true; + // Skip checking the existence of the first access point if it's active + int index = isFirstApActive ? 1 : 0; + // This loop checks the existence of reachable APs to determine the validity of the current + // AP list. + for (; index < apCount; index++) { + if (apList.get(index).isReachable()) { + needLoadingRow = false; + break; + } + } + + // Add AP rows + final CharSequence placeholder = mContext.getText(R.string.summary_placeholder); + for (int i = 0; i < DEFAULT_EXPANDED_ROW_COUNT; i++) { + if (i < apCount) { + listBuilder.addRow(getAccessPointRow(apList.get(i))); + } else if (needLoadingRow) { + listBuilder.addRow(getLoadingRow(placeholder)); + needLoadingRow = false; + } else { + listBuilder.addRow(new ListBuilder.RowBuilder() + .setTitle(placeholder) + .setSubtitle(placeholder)); + } + } + return listBuilder.build(); + } + + private void handleNetworkCallback(WifiScanWorker worker, boolean isFirstApActive) { + if (worker == null) { + return; + } + if (isFirstApActive) { + worker.registerNetworkCallback(mWifiManager.getCurrentNetwork()); + } else { + worker.unregisterNetworkCallback(); + } + } + + private ListBuilder getHeaderRow(boolean isWifiEnabled) { + final IconCompat icon = IconCompat.createWithResource(mContext, + R.drawable.ic_settings_wireless); + final String title = mContext.getString(R.string.wifi_settings); + final PendingIntent toggleAction = getBroadcastIntent(mContext); + final PendingIntent primaryAction = getPrimaryAction(); + final SliceAction primarySliceAction = SliceAction.createDeeplink(primaryAction, icon, + ListBuilder.ICON_IMAGE, title); + final SliceAction toggleSliceAction = SliceAction.createToggle(toggleAction, + null /* actionTitle */, isWifiEnabled); + + return new ListBuilder(mContext, getUri(), ListBuilder.INFINITY) + .setAccentColor(COLOR_NOT_TINTED) + .setKeywords(getKeywords()) + .addRow(new ListBuilder.RowBuilder() + .setTitle(title) + .addEndItem(toggleSliceAction) + .setPrimaryAction(primarySliceAction)); + } + + private ListBuilder.RowBuilder getAccessPointRow(AccessPoint accessPoint) { + final boolean isCaptivePortal = accessPoint.isActive() && isCaptivePortal(); + final CharSequence title = accessPoint.getTitle(); + final CharSequence summary = getAccessPointSummary(accessPoint, isCaptivePortal); + final IconCompat levelIcon = getAccessPointLevelIcon(accessPoint); + final ListBuilder.RowBuilder rowBuilder = new ListBuilder.RowBuilder() + .setTitleItem(levelIcon, ListBuilder.ICON_IMAGE) + .setTitle(title) + .setSubtitle(summary) + .setPrimaryAction(getAccessPointAction(accessPoint, isCaptivePortal, levelIcon, + title)); + + if (isCaptivePortal) { + rowBuilder.addEndItem(getCaptivePortalEndAction(accessPoint, title)); + } else { + final IconCompat endIcon = getEndIcon(accessPoint); + if (endIcon != null) { + rowBuilder.addEndItem(endIcon, ListBuilder.ICON_IMAGE); + } + } + return rowBuilder; + } + + private CharSequence getAccessPointSummary(AccessPoint accessPoint, boolean isCaptivePortal) { + if (isCaptivePortal) { + return mContext.getText(R.string.wifi_tap_to_sign_in); + } + + final CharSequence summary = accessPoint.getSettingsSummary(); + return TextUtils.isEmpty(summary) ? mContext.getText(R.string.disconnected) : summary; + } + + private IconCompat getAccessPointLevelIcon(AccessPoint accessPoint) { + final Drawable d = mContext.getDrawable( + com.android.settingslib.Utils.getWifiIconResource(accessPoint.getLevel())); + + final @ColorInt int color; + if (accessPoint.isActive()) { + final NetworkInfo.State state = accessPoint.getNetworkInfo().getState(); + if (state == NetworkInfo.State.CONNECTED) { + color = Utils.getColorAccentDefaultColor(mContext); + } else { // connecting + color = Utils.getDisabled(mContext, Utils.getColorAttrDefaultColor(mContext, + android.R.attr.colorControlNormal)); + } + } else { + color = Utils.getColorAttrDefaultColor(mContext, android.R.attr.colorControlNormal); + } + + d.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)); + return Utils.createIconWithDrawable(d); + } + + private IconCompat getEndIcon(AccessPoint accessPoint) { + if (accessPoint.isActive()) { + return null; + } else if (accessPoint.getSecurity() != AccessPoint.SECURITY_NONE) { + return IconCompat.createWithResource(mContext, R.drawable.ic_friction_lock_closed); + } else if (accessPoint.isMetered()) { + return IconCompat.createWithResource(mContext, R.drawable.ic_friction_money); + } + return null; + } + + private SliceAction getCaptivePortalEndAction(AccessPoint accessPoint, CharSequence title) { + return getAccessPointAction(accessPoint, false /* isCaptivePortal */, + IconCompat.createWithResource(mContext, R.drawable.ic_settings_accent), title); + } + + private SliceAction getAccessPointAction(AccessPoint accessPoint, boolean isCaptivePortal, + IconCompat icon, CharSequence title) { + final int requestCode = accessPoint.hashCode(); + if (isCaptivePortal) { + final Intent intent = new Intent(mContext, ConnectToWifiHandler.class) + .putExtra(ConnectivityManager.EXTRA_NETWORK, mWifiManager.getCurrentNetwork()); + return getBroadcastAction(requestCode, intent, icon, title); + } + + final Bundle extras = new Bundle(); + accessPoint.saveWifiState(extras); + + if (accessPoint.isActive()) { + final Intent intent = new SubSettingLauncher(mContext) + .setTitleRes(R.string.pref_title_network_details) + .setDestination(WifiNetworkDetailsFragment.class.getName()) + .setArguments(extras) + .setSourceMetricsCategory(SettingsEnums.WIFI) + .toIntent(); + return getActivityAction(requestCode, intent, icon, title); + } else if (WifiUtils.getConnectingType(accessPoint) != WifiUtils.CONNECT_TYPE_OTHERS) { + final Intent intent = new Intent(mContext, ConnectToWifiHandler.class) + .putExtra(WifiDialogActivity.KEY_ACCESS_POINT_STATE, extras); + return getBroadcastAction(requestCode, intent, icon, title); + } else { + final Intent intent = new Intent(mContext, WifiDialogActivity.class) + .putExtra(WifiDialogActivity.KEY_ACCESS_POINT_STATE, extras); + return getActivityAction(requestCode, intent, icon, title); + } + } + + private SliceAction getActivityAction(int requestCode, Intent intent, IconCompat icon, + CharSequence title) { + final PendingIntent pi = PendingIntent.getActivity(mContext, requestCode, intent, + 0 /* flags */); + return SliceAction.createDeeplink(pi, icon, ListBuilder.ICON_IMAGE, title); + } + + private SliceAction getBroadcastAction(int requestCode, Intent intent, IconCompat icon, + CharSequence title) { + intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); + final PendingIntent pi = PendingIntent.getBroadcast(mContext, requestCode, intent, + PendingIntent.FLAG_UPDATE_CURRENT); + return SliceAction.create(pi, icon, ListBuilder.ICON_IMAGE, title); + } + + private ListBuilder.RowBuilder getLoadingRow(CharSequence placeholder) { + final CharSequence title = mContext.getText(R.string.wifi_empty_list_wifi_on); + + // for aligning to the Wi-Fi AP's name + final IconCompat emptyIcon = Utils.createIconWithDrawable( + new ColorDrawable(Color.TRANSPARENT)); + + return new ListBuilder.RowBuilder() + .setTitleItem(emptyIcon, ListBuilder.ICON_IMAGE) + .setTitle(placeholder) + .setSubtitle(title); + } + + private boolean isCaptivePortal() { + final NetworkCapabilities nc = mConnectivityManager.getNetworkCapabilities( + mWifiManager.getCurrentNetwork()); + return WifiUtils.canSignIntoNetwork(nc); + } + + /** + * Update the current wifi status to the boolean value keyed by + * {@link android.app.slice.Slice#EXTRA_TOGGLE_STATE} on {@param intent}. + */ + @Override + public void onNotifyChange(Intent intent) { + final boolean newState = intent.getBooleanExtra(EXTRA_TOGGLE_STATE, + mWifiManager.isWifiEnabled()); + mWifiManager.setWifiEnabled(newState); + // Do not notifyChange on Uri. The service takes longer to update the current value than it + // does for the Slice to check the current value again. Let {@link WifiScanWorker} + // handle it. + } + + @Override + public Intent getIntent() { + final String screenTitle = mContext.getText(R.string.wifi_settings).toString(); + final Uri contentUri = new Uri.Builder().appendPath(KEY_WIFI).build(); + final Intent intent = SliceBuilderUtils.buildSearchResultPageIntent(mContext, + WifiSettings.class.getName(), KEY_WIFI, screenTitle, + SettingsEnums.DIALOG_WIFI_AP_EDIT) + .setClassName(mContext.getPackageName(), SubSettings.class.getName()) + .setData(contentUri); + + return intent; + } + + private boolean isWifiEnabled() { + switch (mWifiManager.getWifiState()) { + case WifiManager.WIFI_STATE_ENABLED: + case WifiManager.WIFI_STATE_ENABLING: + return true; + default: + return false; + } + } + + private PendingIntent getPrimaryAction() { + final Intent intent = getIntent(); + return PendingIntent.getActivity(mContext, 0 /* requestCode */, + intent, 0 /* flags */); + } + + private Set<String> getKeywords() { + final String keywords = mContext.getString(R.string.keywords_wifi); + return Arrays.asList(TextUtils.split(keywords, ",")) + .stream() + .map(String::trim) + .collect(Collectors.toSet()); + } + + @Override + public Class getBackgroundWorkerClass() { + return WifiScanWorker.class; + } +} diff --git a/src/com/android/settings/wifi/tether/TetherService.java b/src/com/android/settings/wifi/tether/TetherService.java index 3d58c99e63..34daccf6ae 100644 --- a/src/com/android/settings/wifi/tether/TetherService.java +++ b/src/com/android/settings/wifi/tether/TetherService.java @@ -42,7 +42,7 @@ import android.text.TextUtils; import android.util.ArrayMap; import android.util.Log; -import com.android.internal.annotations.VisibleForTesting; +import androidx.annotation.VisibleForTesting; import com.android.settings.Utils; diff --git a/src/com/android/settings/wifi/tether/WifiTetherApBandPreferenceController.java b/src/com/android/settings/wifi/tether/WifiTetherApBandPreferenceController.java index 2071075903..3a85f7be4f 100644 --- a/src/com/android/settings/wifi/tether/WifiTetherApBandPreferenceController.java +++ b/src/com/android/settings/wifi/tether/WifiTetherApBandPreferenceController.java @@ -19,10 +19,11 @@ package com.android.settings.wifi.tether; import android.content.Context; import android.content.res.Resources; import android.net.wifi.WifiConfiguration; +import android.util.Log; + import androidx.annotation.VisibleForTesting; import androidx.preference.ListPreference; import androidx.preference.Preference; -import android.util.Log; import com.android.settings.R; diff --git a/src/com/android/settings/wifi/tether/WifiTetherAutoOffPreferenceController.java b/src/com/android/settings/wifi/tether/WifiTetherAutoOffPreferenceController.java index d8c3cfd8a3..01a0b5707c 100644 --- a/src/com/android/settings/wifi/tether/WifiTetherAutoOffPreferenceController.java +++ b/src/com/android/settings/wifi/tether/WifiTetherAutoOffPreferenceController.java @@ -18,8 +18,9 @@ package com.android.settings.wifi.tether; import android.content.Context; import android.provider.Settings; -import androidx.preference.SwitchPreference; + import androidx.preference.Preference; +import androidx.preference.SwitchPreference; import com.android.settings.core.BasePreferenceController; diff --git a/src/com/android/settings/wifi/tether/WifiTetherBasePreferenceController.java b/src/com/android/settings/wifi/tether/WifiTetherBasePreferenceController.java index e689916475..94e9209def 100644 --- a/src/com/android/settings/wifi/tether/WifiTetherBasePreferenceController.java +++ b/src/com/android/settings/wifi/tether/WifiTetherBasePreferenceController.java @@ -19,6 +19,7 @@ package com.android.settings.wifi.tether; import android.content.Context; import android.net.ConnectivityManager; import android.net.wifi.WifiManager; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; diff --git a/src/com/android/settings/wifi/tether/WifiTetherPasswordPreferenceController.java b/src/com/android/settings/wifi/tether/WifiTetherPasswordPreferenceController.java index 465170a2c4..8d9e8586f4 100644 --- a/src/com/android/settings/wifi/tether/WifiTetherPasswordPreferenceController.java +++ b/src/com/android/settings/wifi/tether/WifiTetherPasswordPreferenceController.java @@ -18,9 +18,10 @@ package com.android.settings.wifi.tether; import android.content.Context; import android.net.wifi.WifiConfiguration; +import android.text.TextUtils; + import androidx.preference.EditTextPreference; import androidx.preference.Preference; -import android.text.TextUtils; import com.android.settings.R; import com.android.settings.widget.ValidatedEditTextPreference; diff --git a/src/com/android/settings/wifi/tether/WifiTetherPreferenceController.java b/src/com/android/settings/wifi/tether/WifiTetherPreferenceController.java index 3e0297e741..8f6d489e7a 100644 --- a/src/com/android/settings/wifi/tether/WifiTetherPreferenceController.java +++ b/src/com/android/settings/wifi/tether/WifiTetherPreferenceController.java @@ -24,10 +24,11 @@ import android.net.ConnectivityManager; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiManager; import android.provider.Settings; +import android.text.BidiFormatter; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.text.BidiFormatter; import com.android.settings.R; import com.android.settings.Utils; @@ -42,9 +43,6 @@ public class WifiTetherPreferenceController extends AbstractPreferenceController implements PreferenceControllerMixin, LifecycleObserver, OnStart, OnStop { private static final String WIFI_TETHER_SETTINGS = "wifi_tether"; - private static final IntentFilter AIRPLANE_INTENT_FILTER = new IntentFilter( - Intent.ACTION_AIRPLANE_MODE_CHANGED); - private static final int ID_NULL = -1; private final ConnectivityManager mConnectivityManager; private final String[] mWifiRegexs; @@ -102,8 +100,6 @@ public class WifiTetherPreferenceController extends AbstractPreferenceController @Override public void onStart() { if (mPreference != null) { - mContext.registerReceiver(mReceiver, AIRPLANE_INTENT_FILTER); - clearSummaryForAirplaneMode(); if (mWifiTetherSoftApManager != null) { mWifiTetherSoftApManager.registerSoftApCallback(); } @@ -113,7 +109,6 @@ public class WifiTetherPreferenceController extends AbstractPreferenceController @Override public void onStop() { if (mPreference != null) { - mContext.unregisterReceiver(mReceiver); if (mWifiTetherSoftApManager != null) { mWifiTetherSoftApManager.unRegisterSoftApCallback(); } @@ -145,19 +140,6 @@ public class WifiTetherPreferenceController extends AbstractPreferenceController }); } - // - // Everything below is copied from WifiApEnabler - // - private final BroadcastReceiver mReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - if (Intent.ACTION_AIRPLANE_MODE_CHANGED.equals(action)) { - clearSummaryForAirplaneMode(R.string.wifi_hotspot_off_subtext); - } - } - }; - @VisibleForTesting void handleWifiApStateChanged(int state, int reason) { switch (state) { @@ -173,7 +155,6 @@ public class WifiTetherPreferenceController extends AbstractPreferenceController break; case WifiManager.WIFI_AP_STATE_DISABLED: mPreference.setSummary(R.string.wifi_hotspot_off_subtext); - clearSummaryForAirplaneMode(); break; default: if (reason == WifiManager.SAP_START_FAILURE_NO_CHANNEL) { @@ -181,7 +162,6 @@ public class WifiTetherPreferenceController extends AbstractPreferenceController } else { mPreference.setSummary(R.string.wifi_error); } - clearSummaryForAirplaneMode(); } } @@ -193,21 +173,4 @@ public class WifiTetherPreferenceController extends AbstractPreferenceController BidiFormatter.getInstance().unicodeWrap( (wifiConfig == null) ? s : wifiConfig.SSID))); } - - private void clearSummaryForAirplaneMode() { - clearSummaryForAirplaneMode(ID_NULL); - } - - private void clearSummaryForAirplaneMode(int defaultId) { - boolean isAirplaneMode = Settings.Global.getInt(mContext.getContentResolver(), - Settings.Global.AIRPLANE_MODE_ON, 0) != 0; - if (isAirplaneMode) { - mPreference.setSummary(R.string.wifi_tether_disabled_by_airplane); - } else if (defaultId != ID_NULL){ - mPreference.setSummary(defaultId); - } - } - // - // Everything above is copied from WifiApEnabler - // } diff --git a/src/com/android/settings/wifi/tether/WifiTetherSSIDPreferenceController.java b/src/com/android/settings/wifi/tether/WifiTetherSSIDPreferenceController.java index 176573f584..1197db436c 100644 --- a/src/com/android/settings/wifi/tether/WifiTetherSSIDPreferenceController.java +++ b/src/com/android/settings/wifi/tether/WifiTetherSSIDPreferenceController.java @@ -16,15 +16,23 @@ package com.android.settings.wifi.tether; +import android.app.settings.SettingsEnums; import android.content.Context; +import android.content.Intent; import android.net.wifi.WifiConfiguration; +import android.util.Log; +import android.view.View; + import androidx.annotation.VisibleForTesting; import androidx.preference.EditTextPreference; import androidx.preference.Preference; -import android.util.Log; +import com.android.settings.R; +import com.android.settings.overlay.FeatureFactory; import com.android.settings.widget.ValidatedEditTextPreference; -import com.android.settings.wifi.WifiUtils; +import com.android.settings.wifi.dpp.WifiDppUtils; + +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; public class WifiTetherSSIDPreferenceController extends WifiTetherBasePreferenceController implements ValidatedEditTextPreference.Validator { @@ -37,10 +45,14 @@ public class WifiTetherSSIDPreferenceController extends WifiTetherBasePreference private String mSSID; private WifiDeviceNameTextValidator mWifiDeviceNameTextValidator; + private final MetricsFeatureProvider mMetricsFeatureProvider; + public WifiTetherSSIDPreferenceController(Context context, OnTetherConfigUpdateListener listener) { super(context, listener); + mWifiDeviceNameTextValidator = new WifiDeviceNameTextValidator(); + mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider(); } @Override @@ -53,12 +65,27 @@ public class WifiTetherSSIDPreferenceController extends WifiTetherBasePreference final WifiConfiguration config = mWifiManager.getWifiApConfiguration(); if (config != null) { mSSID = config.SSID; - Log.d(TAG, "Updating SSID in Preference, " + mSSID); } else { mSSID = DEFAULT_SSID; - Log.d(TAG, "Updating to default SSID in Preference, " + mSSID); } ((ValidatedEditTextPreference) mPreference).setValidator(this); + + if (mWifiManager.isWifiApEnabled() && config != null) { + final Intent intent = WifiDppUtils.getHotspotConfiguratorIntentOrNull(mContext, + mWifiManager, config); + + if (intent == null) { + Log.e(TAG, "Invalid security to share hotspot"); + ((WifiTetherSsidPreference) mPreference).setButtonVisible(false); + } else { + ((WifiTetherSsidPreference) mPreference).setButtonOnClickListener( + view -> shareHotspotNetwork(intent)); + ((WifiTetherSsidPreference) mPreference).setButtonVisible(true); + } + } else { + ((WifiTetherSsidPreference) mPreference).setButtonVisible(false); + } + updateSsidDisplay((EditTextPreference) mPreference); } @@ -83,4 +110,21 @@ public class WifiTetherSSIDPreferenceController extends WifiTetherBasePreference preference.setText(mSSID); preference.setSummary(mSSID); } + + private void shareHotspotNetwork(Intent intent) { + WifiDppUtils.showLockScreen(mContext, () -> { + mMetricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, + SettingsEnums.ACTION_SETTINGS_SHARE_WIFI_HOTSPOT_QR_CODE, + SettingsEnums.SETTINGS_WIFI_DPP_CONFIGURATOR, + /* key */ null, + /* value */ Integer.MIN_VALUE); + + mContext.startActivity(intent); + }); + } + + @VisibleForTesting + boolean isQrCodeButtonAvailable() { + return ((WifiTetherSsidPreference) mPreference).isQrCodeButtonAvailable(); + } } diff --git a/src/com/android/settings/wifi/tether/WifiTetherSecurityPreferenceController.java b/src/com/android/settings/wifi/tether/WifiTetherSecurityPreferenceController.java index 0f08cbb709..f97a20911e 100644 --- a/src/com/android/settings/wifi/tether/WifiTetherSecurityPreferenceController.java +++ b/src/com/android/settings/wifi/tether/WifiTetherSecurityPreferenceController.java @@ -1,8 +1,8 @@ package com.android.settings.wifi.tether; import android.content.Context; -import android.content.res.Resources; import android.net.wifi.WifiConfiguration; + import androidx.preference.ListPreference; import androidx.preference.Preference; diff --git a/src/com/android/settings/wifi/tether/WifiTetherSettings.java b/src/com/android/settings/wifi/tether/WifiTetherSettings.java index 0de7404354..aecc053033 100644 --- a/src/com/android/settings/wifi/tether/WifiTetherSettings.java +++ b/src/com/android/settings/wifi/tether/WifiTetherSettings.java @@ -19,6 +19,7 @@ package com.android.settings.wifi.tether; import static android.net.ConnectivityManager.ACTION_TETHER_STATE_CHANGED; import static android.net.wifi.WifiManager.WIFI_AP_STATE_CHANGED_ACTION; +import android.app.settings.SettingsEnums; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -27,26 +28,40 @@ import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiManager; import android.os.Bundle; import android.os.UserManager; -import androidx.annotation.VisibleForTesting; +import android.provider.SearchIndexableResource; import android.util.Log; -import com.android.internal.logging.nano.MetricsProto; +import androidx.annotation.VisibleForTesting; + import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.dashboard.RestrictedDashboardFragment; +import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.widget.SwitchBar; import com.android.settings.widget.SwitchBarController; +import com.android.settingslib.TetherUtil; import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +@SearchIndexable public class WifiTetherSettings extends RestrictedDashboardFragment implements WifiTetherBasePreferenceController.OnTetherConfigUpdateListener { private static final String TAG = "WifiTetherSettings"; private static final IntentFilter TETHER_STATE_CHANGE_FILTER; - private static final String KEY_WIFI_TETHER_AUTO_OFF = "wifi_tether_auto_turn_off"; + private static final String KEY_WIFI_TETHER_SCREEN = "wifi_tether_settings_screen"; + @VisibleForTesting + static final String KEY_WIFI_TETHER_NETWORK_NAME = "wifi_tether_network_name"; + @VisibleForTesting + static final String KEY_WIFI_TETHER_NETWORK_PASSWORD = "wifi_tether_network_password"; + @VisibleForTesting + static final String KEY_WIFI_TETHER_AUTO_OFF = "wifi_tether_auto_turn_off"; + @VisibleForTesting + static final String KEY_WIFI_TETHER_NETWORK_AP_BAND = "wifi_tether_network_ap_band"; private WifiTetherSwitchBarController mSwitchBarController; private WifiTetherSSIDPreferenceController mSSIDPreferenceController; @@ -56,6 +71,7 @@ public class WifiTetherSettings extends RestrictedDashboardFragment private WifiManager mWifiManager; private boolean mRestartWifiApAfterConfigChange; + private boolean mUnavailable; @VisibleForTesting TetherChangeReceiver mTetherChangeReceiver; @@ -71,7 +87,7 @@ public class WifiTetherSettings extends RestrictedDashboardFragment @Override public int getMetricsCategory() { - return MetricsProto.MetricsEvent.WIFI_TETHER_SETTINGS; + return SettingsEnums.WIFI_TETHER_SETTINGS; } @Override @@ -80,28 +96,52 @@ public class WifiTetherSettings extends RestrictedDashboardFragment } @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + setIfOnlyAvailableForAdmins(true); + if (isUiRestricted()) { + mUnavailable = true; + } + } + + @Override public void onAttach(Context context) { super.onAttach(context); mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); mTetherChangeReceiver = new TetherChangeReceiver(); + + mSSIDPreferenceController = use(WifiTetherSSIDPreferenceController.class); + mSecurityPreferenceController = use(WifiTetherSecurityPreferenceController.class); + mPasswordPreferenceController = use(WifiTetherPasswordPreferenceController.class); + mApBandPreferenceController = use(WifiTetherApBandPreferenceController.class); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); + if (mUnavailable) { + return; + } // Assume we are in a SettingsActivity. This is only safe because we currently use // SettingsActivity as base for all preference fragments. final SettingsActivity activity = (SettingsActivity) getActivity(); final SwitchBar switchBar = activity.getSwitchBar(); mSwitchBarController = new WifiTetherSwitchBarController(activity, new SwitchBarController(switchBar)); - getLifecycle().addObserver(mSwitchBarController); + getSettingsLifecycle().addObserver(mSwitchBarController); switchBar.show(); } @Override public void onStart() { super.onStart(); + if (mUnavailable) { + if (!isUiRestrictedByOnlyAdmin()) { + getEmptyTextView().setText(R.string.tethering_settings_not_available); + } + getPreferenceScreen().removeAll(); + return; + } final Context context = getContext(); if (context != null) { context.registerReceiver(mTetherChangeReceiver, TETHER_STATE_CHANGE_FILTER); @@ -111,6 +151,9 @@ public class WifiTetherSettings extends RestrictedDashboardFragment @Override public void onStop() { super.onStop(); + if (mUnavailable) { + return; + } final Context context = getContext(); if (context != null) { context.unregisterReceiver(mTetherChangeReceiver); @@ -125,18 +168,19 @@ public class WifiTetherSettings extends RestrictedDashboardFragment @Override protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { + return buildPreferenceControllers(context, this::onTetherConfigUpdated); + } + + private static List<AbstractPreferenceController> buildPreferenceControllers(Context context, + WifiTetherBasePreferenceController.OnTetherConfigUpdateListener listener) { final List<AbstractPreferenceController> controllers = new ArrayList<>(); - mSSIDPreferenceController = new WifiTetherSSIDPreferenceController(context, this); - mSecurityPreferenceController = new WifiTetherSecurityPreferenceController(context, this); - mPasswordPreferenceController = new WifiTetherPasswordPreferenceController(context, this); - mApBandPreferenceController = new WifiTetherApBandPreferenceController(context, this); - - controllers.add(mSSIDPreferenceController); - controllers.add(mSecurityPreferenceController); - controllers.add(mPasswordPreferenceController); - controllers.add(mApBandPreferenceController); + controllers.add(new WifiTetherSSIDPreferenceController(context, listener)); + controllers.add(new WifiTetherSecurityPreferenceController(context, listener)); + controllers.add(new WifiTetherPasswordPreferenceController(context, listener)); + controllers.add(new WifiTetherApBandPreferenceController(context, listener)); controllers.add( new WifiTetherAutoOffPreferenceController(context, KEY_WIFI_TETHER_AUTO_OFF)); + return controllers; } @@ -187,6 +231,39 @@ public class WifiTetherSettings extends RestrictedDashboardFragment .updateDisplay(); } + public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + public List<SearchIndexableResource> getXmlResourcesToIndex( + Context context, boolean enabled) { + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = R.xml.wifi_tether_settings; + return Arrays.asList(sir); + } + + @Override + public List<String> getNonIndexableKeys(Context context) { + final List<String> keys = super.getNonIndexableKeys(context); + + if (!TetherUtil.isTetherAvailable(context)) { + keys.add(KEY_WIFI_TETHER_NETWORK_NAME); + keys.add(KEY_WIFI_TETHER_NETWORK_PASSWORD); + keys.add(KEY_WIFI_TETHER_AUTO_OFF); + keys.add(KEY_WIFI_TETHER_NETWORK_AP_BAND); + } + + // Remove duplicate + keys.add(KEY_WIFI_TETHER_SCREEN); + return keys; + } + + @Override + public List<AbstractPreferenceController> createPreferenceControllers( + Context context) { + return buildPreferenceControllers(context, null /* listener */); + } + }; + @VisibleForTesting class TetherChangeReceiver extends BroadcastReceiver { @Override diff --git a/src/com/android/settings/wifi/tether/WifiTetherSsidPreference.java b/src/com/android/settings/wifi/tether/WifiTetherSsidPreference.java new file mode 100644 index 0000000000..4343fcc399 --- /dev/null +++ b/src/com/android/settings/wifi/tether/WifiTetherSsidPreference.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2019 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.wifi.tether; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.ImageButton; + +import androidx.annotation.DrawableRes; +import androidx.preference.PreferenceViewHolder; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.settings.R; +import com.android.settings.widget.ValidatedEditTextPreference; + +/** + * Support a QR code share button for {@code EditTextPreference} that supports input validation. + */ +public class WifiTetherSsidPreference extends ValidatedEditTextPreference { + private static final String TAG = "WifiTetherSsidPreference"; + + private Drawable mShareIconDrawable; + private View.OnClickListener mClickListener; + private boolean mVisible; + + public WifiTetherSsidPreference(Context context, AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + initialize(); + } + + public WifiTetherSsidPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + initialize(); + } + + public WifiTetherSsidPreference(Context context, AttributeSet attrs) { + super(context, attrs); + + initialize(); + } + + public WifiTetherSsidPreference(Context context) { + super(context); + + initialize(); + } + + private void initialize() { + // TODO(b/129019971): use methods of divider line in parent object + setLayoutResource(com.android.settingslib.R.layout.preference_two_target); + setWidgetLayoutResource(R.layout.wifi_button_preference_widget); + + mShareIconDrawable = getDrawable(R.drawable.ic_qrcode_24dp); + } + + @Override + public void onBindViewHolder(PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + + final ImageButton shareButton = (ImageButton) holder.findViewById(R.id.button_icon); + final View dividerView = holder.findViewById(R.id.two_target_divider); + + if (mVisible) { + shareButton.setOnClickListener(mClickListener); + shareButton.setVisibility(View.VISIBLE); + shareButton.setContentDescription( + getContext().getString(R.string.wifi_dpp_share_hotspot)); + shareButton.setImageDrawable(mShareIconDrawable); + + dividerView.setVisibility(View.VISIBLE); + } else { + shareButton.setVisibility(View.GONE); + + dividerView.setVisibility(View.GONE); + } + } + + public void setButtonOnClickListener(View.OnClickListener listener) { + mClickListener = listener; + } + + public void setButtonVisible(boolean visible) { + mVisible = visible; + } + + private Drawable getDrawable(@DrawableRes int iconResId) { + Drawable buttonIcon = null; + + try { + buttonIcon = getContext().getDrawable(iconResId); + } catch (Resources.NotFoundException exception) { + Log.e(TAG, "Resource does not exist: " + iconResId); + } + return buttonIcon; + } + + @VisibleForTesting + boolean isQrCodeButtonAvailable() { + return mVisible && mClickListener != null; + } +} diff --git a/src/com/android/settings/wifi/tether/WifiTetherSwitchBarController.java b/src/com/android/settings/wifi/tether/WifiTetherSwitchBarController.java index 554cc0e320..0f31d19535 100644 --- a/src/com/android/settings/wifi/tether/WifiTetherSwitchBarController.java +++ b/src/com/android/settings/wifi/tether/WifiTetherSwitchBarController.java @@ -27,6 +27,7 @@ import android.net.wifi.WifiManager; import android.os.Handler; import android.os.Looper; import android.provider.Settings; + import androidx.annotation.VisibleForTesting; import com.android.settings.datausage.DataSaverBackend; @@ -60,7 +61,6 @@ public class WifiTetherSwitchBarController implements SwitchWidgetController.OnS static { WIFI_INTENT_FILTER = new IntentFilter(WifiManager.WIFI_AP_STATE_CHANGED_ACTION); - WIFI_INTENT_FILTER.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED); } WifiTetherSwitchBarController(Context context, SwitchWidgetController switchBar) { @@ -118,8 +118,6 @@ public class WifiTetherSwitchBarController implements SwitchWidgetController.OnS final int state = intent.getIntExtra( WifiManager.EXTRA_WIFI_AP_STATE, WifiManager.WIFI_AP_STATE_FAILED); handleWifiApStateChanged(state); - } else if (Intent.ACTION_AIRPLANE_MODE_CHANGED.equals(action)) { - updateWifiSwitch(); } } }; @@ -153,13 +151,7 @@ public class WifiTetherSwitchBarController implements SwitchWidgetController.OnS } private void updateWifiSwitch() { - boolean isAirplaneMode = Settings.Global.getInt(mContext.getContentResolver(), - Settings.Global.AIRPLANE_MODE_ON, 0) != 0; - if (!isAirplaneMode) { - mSwitchBar.setEnabled(!mDataSaverBackend.isDataSaverEnabled()); - } else { - mSwitchBar.setEnabled(false); - } + mSwitchBar.setEnabled(!mDataSaverBackend.isDataSaverEnabled()); } @Override diff --git a/src/com/android/settings/wrapper/OverlayManagerWrapper.java b/src/com/android/settings/wrapper/OverlayManagerWrapper.java deleted file mode 100644 index 371504ff55..0000000000 --- a/src/com/android/settings/wrapper/OverlayManagerWrapper.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (C) 2018 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.wrapper; - -import android.content.Context; -import android.content.om.IOverlayManager; -import android.content.om.OverlayInfo; -import android.os.RemoteException; -import android.os.ServiceManager; - -import java.util.ArrayList; -import java.util.List; - -public class OverlayManagerWrapper { - - private final IOverlayManager mOverlayManager; - - public OverlayManagerWrapper(IOverlayManager overlayManager) { - mOverlayManager = overlayManager; - } - - public OverlayManagerWrapper() { - this(IOverlayManager.Stub.asInterface(ServiceManager.getService(Context.OVERLAY_SERVICE))); - } - - public List<OverlayInfo> getOverlayInfosForTarget(String overlay, int userId) { - if (mOverlayManager == null) { - return new ArrayList<>(); - } - try { - List<android.content.om.OverlayInfo> infos - = mOverlayManager.getOverlayInfosForTarget(overlay, userId); - ArrayList<OverlayInfo> result = new ArrayList<>(infos.size()); - for (int i = 0; i < infos.size(); i++) { - result.add(new OverlayInfo(infos.get(i))); - } - return result; - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - public boolean setEnabled(String overlay, boolean enabled, int userId) { - if (mOverlayManager == null) { - return false; - } - try { - return mOverlayManager.setEnabled(overlay, enabled, userId); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - public boolean setEnabledExclusiveInCategory(String overlay, int userId) { - if (mOverlayManager == null) { - return false; - } - try { - return mOverlayManager.setEnabledExclusiveInCategory(overlay, userId); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - public static class OverlayInfo { - - public static final String CATEGORY_THEME = android.content.om.OverlayInfo.CATEGORY_THEME; - public final String packageName; - public final String category; - public final int priority; - private final boolean mEnabled; - - public OverlayInfo(String packageName, String category, boolean enabled, int priority) { - this.packageName = packageName; - this.category = category; - mEnabled = enabled; - this.priority = priority; - } - - public OverlayInfo(android.content.om.OverlayInfo info) { - mEnabled = info.isEnabled(); - category = info.category; - packageName = info.packageName; - priority = info.priority; - } - - public boolean isEnabled() { - return mEnabled; - } - } -} |