getPreferenceType() {
+ return ColoredSwitchPreference.class;
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/accounts/AccountAutoSyncPreferenceController.java b/CarSettings/src/com/android/car/settings/accounts/AccountAutoSyncPreferenceController.java
new file mode 100644
index 00000000..56cd9169
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/accounts/AccountAutoSyncPreferenceController.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.car.settings.accounts;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.os.UserHandle;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.TwoStatePreference;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.ConfirmationDialogFragment;
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.PreferenceController;
+
+/**
+ * Controller for the preference that allows the user to toggle automatic syncing of accounts.
+ *
+ * Copied from {@link com.android.settings.users.AutoSyncDataPreferenceController}
+ */
+public class AccountAutoSyncPreferenceController extends PreferenceController {
+
+ private final UserHandle mUserHandle;
+ /**
+ * Argument key to store a value that indicates whether the account auto sync is being enabled
+ * or disabled.
+ */
+ @VisibleForTesting
+ static final String KEY_ENABLING = "ENABLING";
+ /** Argument key to store user handle. */
+ @VisibleForTesting
+ static final String KEY_USER_HANDLE = "USER_HANDLE";
+
+ @VisibleForTesting
+ final ConfirmationDialogFragment.ConfirmListener mConfirmListener = arguments -> {
+ boolean enabling = arguments.getBoolean(KEY_ENABLING);
+ UserHandle userHandle = arguments.getParcelable(KEY_USER_HANDLE);
+ ContentResolver.setMasterSyncAutomaticallyAsUser(enabling, userHandle.getIdentifier());
+ getPreference().setChecked(enabling);
+ };
+
+ public AccountAutoSyncPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ mUserHandle = UserHandle.of(UserHandle.myUserId());
+ }
+
+ @Override
+ protected Class getPreferenceType() {
+ return TwoStatePreference.class;
+ }
+
+ @Override
+ protected void updateState(TwoStatePreference preference) {
+ preference.setChecked(ContentResolver.getMasterSyncAutomaticallyAsUser(
+ mUserHandle.getIdentifier()));
+ }
+
+ @Override
+ protected void onCreateInternal() {
+ // If the dialog is still up, reattach the preference
+ ConfirmationDialogFragment dialog =
+ (ConfirmationDialogFragment) getFragmentController().findDialogByTag(
+ ConfirmationDialogFragment.TAG);
+
+ ConfirmationDialogFragment.resetListeners(
+ dialog,
+ mConfirmListener,
+ /* rejectListener= */ null,
+ /* neutralListener= */ null);
+ }
+
+ @Override
+ protected boolean handlePreferenceChanged(TwoStatePreference preference, Object checked) {
+ getFragmentController().showDialog(
+ getAutoSyncChangeConfirmationDialogFragment((boolean) checked),
+ ConfirmationDialogFragment.TAG);
+ // The dialog will change the state of the preference if the user confirms, so don't handle
+ // it here
+ return false;
+ }
+
+ private ConfirmationDialogFragment getAutoSyncChangeConfirmationDialogFragment(
+ boolean enabling) {
+ int dialogTitle;
+ int dialogMessage;
+
+ if (enabling) {
+ dialogTitle = R.string.data_usage_auto_sync_on_dialog_title;
+ dialogMessage = R.string.data_usage_auto_sync_on_dialog;
+ } else {
+ dialogTitle = R.string.data_usage_auto_sync_off_dialog_title;
+ dialogMessage = R.string.data_usage_auto_sync_off_dialog;
+ }
+
+ ConfirmationDialogFragment dialogFragment =
+ new ConfirmationDialogFragment.Builder(getContext())
+ .setTitle(dialogTitle).setMessage(dialogMessage)
+ .setPositiveButton(R.string.allow, mConfirmListener)
+ .setNegativeButton(R.string.do_not_allow, /* rejectListener= */ null)
+ .addArgumentBoolean(KEY_ENABLING, enabling)
+ .addArgumentParcelable(KEY_USER_HANDLE, mUserHandle)
+ .build();
+
+ return dialogFragment;
+ }
+}
\ No newline at end of file
diff --git a/CarSettings/src/com/android/car/settings/accounts/AccountDetailsBasePreferenceController.java b/CarSettings/src/com/android/car/settings/accounts/AccountDetailsBasePreferenceController.java
new file mode 100644
index 00000000..6d2775b8
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/accounts/AccountDetailsBasePreferenceController.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.settings.accounts;
+
+import android.accounts.Account;
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+import android.os.UserHandle;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.Logger;
+import com.android.car.settings.common.PreferenceController;
+import com.android.car.ui.preference.CarUiTwoActionIconPreference;
+import com.android.settingslib.accounts.AuthenticatorHelper;
+
+/** Base controller for preferences that shows the details of an account. */
+public abstract class AccountDetailsBasePreferenceController
+ extends PreferenceController {
+ private static final Logger LOG = new Logger(AccountDetailsBasePreferenceController.class);
+
+ private Account mAccount;
+ private UserHandle mUserHandle;
+
+ public AccountDetailsBasePreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController,
+ CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ }
+
+ /** Sets the account that the details are shown for. */
+ public void setAccount(Account account) {
+ mAccount = account;
+ }
+
+ /** Returns the account the details are shown for. */
+ public Account getAccount() {
+ return mAccount;
+ }
+
+ /** Sets the UserHandle used by the controller. */
+ public void setUserHandle(UserHandle userHandle) {
+ mUserHandle = userHandle;
+ }
+
+ /** Returns the UserHandle used by the controller. */
+ public UserHandle getUserHandle() {
+ return mUserHandle;
+ }
+
+ @Override
+ protected Class getPreferenceType() {
+ return CarUiTwoActionIconPreference.class;
+ }
+
+ /**
+ * Verifies that the controller was properly initialized with
+ * {@link #setAccount(Account)} and {@link #setUserHandle(UserHandle)}.
+ *
+ * @throws IllegalStateException if the account or user handle are {@code null}
+ */
+ @Override
+ @CallSuper
+ protected void checkInitialized() {
+ LOG.v("checkInitialized");
+ if (mAccount == null) {
+ throw new IllegalStateException(
+ "AccountDetailsBasePreferenceController must be initialized by calling "
+ + "setAccount(Account)");
+ }
+ if (mUserHandle == null) {
+ throw new IllegalStateException(
+ "AccountDetailsBasePreferenceController must be initialized by calling "
+ + "setUserHandle(UserHandle)");
+ }
+ }
+
+ @Override
+ @CallSuper
+ protected void updateState(CarUiTwoActionIconPreference preference) {
+ preference.setTitle(mAccount.name);
+ // Get the icon corresponding to the account's type and set it.
+ AuthenticatorHelper helper = getAuthenticatorHelper();
+ preference.setIcon(helper.getDrawableForType(getContext(), mAccount.type));
+ }
+
+ @VisibleForTesting
+ AuthenticatorHelper getAuthenticatorHelper() {
+ return new AuthenticatorHelper(getContext(), mUserHandle, null);
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/accounts/AccountDetailsFragment.java b/CarSettings/src/com/android/car/settings/accounts/AccountDetailsFragment.java
new file mode 100644
index 00000000..b1179957
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/accounts/AccountDetailsFragment.java
@@ -0,0 +1,135 @@
+/*
+ * 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.car.settings.accounts;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.content.Context;
+import android.content.pm.UserInfo;
+import android.os.Bundle;
+import android.os.UserHandle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import androidx.annotation.XmlRes;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.SettingsFragment;
+import com.android.car.ui.toolbar.ToolbarController;
+import com.android.settingslib.accounts.AuthenticatorHelper;
+
+import java.util.Arrays;
+
+/**
+ * Shows account details, and delete account option.
+ */
+public class AccountDetailsFragment extends SettingsFragment implements
+ AuthenticatorHelper.OnAccountsUpdateListener {
+ public static final String EXTRA_ACCOUNT = "extra_account";
+ public static final String EXTRA_ACCOUNT_LABEL = "extra_account_label";
+ public static final String EXTRA_USER_INFO = "extra_user_info";
+
+ private Account mAccount;
+ private UserInfo mUserInfo;
+ private AuthenticatorHelper mAuthenticatorHelper;
+
+ /**
+ * Creates a new AccountDetailsFragment.
+ *
+ * Passes the provided account, label, and user info to the fragment via fragment arguments.
+ */
+ public static AccountDetailsFragment newInstance(Account account, CharSequence label,
+ UserInfo userInfo) {
+ AccountDetailsFragment
+ accountDetailsFragment = new AccountDetailsFragment();
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(EXTRA_ACCOUNT, account);
+ bundle.putCharSequence(EXTRA_ACCOUNT_LABEL, label);
+ bundle.putParcelable(EXTRA_USER_INFO, userInfo);
+ accountDetailsFragment.setArguments(bundle);
+ return accountDetailsFragment;
+ }
+
+ @Override
+ @XmlRes
+ protected int getPreferenceScreenResId() {
+ return R.xml.account_details_fragment;
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+
+ mAccount = getArguments().getParcelable(EXTRA_ACCOUNT);
+ mUserInfo = getArguments().getParcelable(EXTRA_USER_INFO);
+
+ use(AccountDetailsPreferenceController.class, R.string.pk_account_details)
+ .setAccount(mAccount);
+ use(AccountDetailsPreferenceController.class, R.string.pk_account_details)
+ .setUserHandle(mUserInfo.getUserHandle());
+
+ use(AccountSyncPreferenceController.class, R.string.pk_account_sync)
+ .setAccount(mAccount);
+ use(AccountDetailsSettingController.class, R.string.pk_account_settings)
+ .setAccount(mAccount);
+ use(AccountSyncPreferenceController.class, R.string.pk_account_sync)
+ .setUserHandle(mUserInfo.getUserHandle());
+ }
+
+ @Override
+ protected void setupToolbar(@NonNull ToolbarController toolbar) {
+ super.setupToolbar(toolbar);
+
+ // Set the fragment's title
+ toolbar.setTitle(getArguments().getCharSequence(EXTRA_ACCOUNT_LABEL));
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ mAuthenticatorHelper = new AuthenticatorHelper(getContext(), mUserInfo.getUserHandle(),
+ this);
+ mAuthenticatorHelper.listenToAccountUpdates();
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ mAuthenticatorHelper.stopListeningToAccountUpdates();
+ }
+
+ @Override
+ public void onAccountsUpdate(UserHandle userHandle) {
+ if (!accountExists()) {
+ // The account was deleted. Pop back.
+ goBack();
+ }
+ }
+
+ /** Returns whether the account being shown by this fragment still exists. */
+ @VisibleForTesting
+ boolean accountExists() {
+ if (mAccount == null) {
+ return false;
+ }
+
+ Account[] accounts = AccountManager.get(getContext()).getAccountsByTypeAsUser(mAccount.type,
+ mUserInfo.getUserHandle());
+
+ return Arrays.asList(accounts).contains(mAccount);
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/accounts/AccountDetailsPreferenceController.java b/CarSettings/src/com/android/car/settings/accounts/AccountDetailsPreferenceController.java
new file mode 100644
index 00000000..f07c9471
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/accounts/AccountDetailsPreferenceController.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.car.settings.accounts;
+
+import static android.os.UserManager.DISALLOW_MODIFY_ACCOUNTS;
+
+import static com.android.car.settings.enterprise.ActionDisabledByAdminDialogFragment.DISABLED_BY_ADMIN_CONFIRM_DIALOG_TAG;
+import static com.android.car.settings.enterprise.EnterpriseUtils.hasUserRestrictionByDpm;
+import static com.android.car.settings.enterprise.EnterpriseUtils.hasUserRestrictionByUm;
+
+import android.accounts.AccountManager;
+import android.accounts.AccountManagerCallback;
+import android.accounts.AuthenticatorException;
+import android.accounts.OperationCanceledException;
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.ActivityResultCallback;
+import com.android.car.settings.common.ConfirmationDialogFragment;
+import com.android.car.settings.common.ErrorDialog;
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.Logger;
+import com.android.car.settings.enterprise.EnterpriseUtils;
+import com.android.car.settings.profiles.ProfileHelper;
+
+import java.io.IOException;
+
+/**
+ * Controller for the preference that shows the details of an account. It also handles a secondary
+ * button for account removal.
+ */
+public class AccountDetailsPreferenceController extends AccountDetailsBasePreferenceController
+ implements ActivityResultCallback {
+ private static final Logger LOG = new Logger(AccountDetailsPreferenceController.class);
+ private static final int REMOVE_ACCOUNT_REQUEST = 101;
+
+ private AccountManagerCallback mCallback =
+ future -> {
+ // If already out of this screen, don't proceed.
+ if (!isStarted()) {
+ return;
+ }
+
+ boolean done = true;
+ boolean success = false;
+ try {
+ Bundle result = future.getResult();
+ Intent removeIntent = result.getParcelable(AccountManager.KEY_INTENT);
+ if (removeIntent != null) {
+ done = false;
+ getFragmentController().startActivityForResult(new Intent(removeIntent),
+ REMOVE_ACCOUNT_REQUEST, this);
+ } else {
+ success = future.getResult().getBoolean(AccountManager.KEY_BOOLEAN_RESULT);
+ }
+ } catch (OperationCanceledException | IOException | AuthenticatorException e) {
+ LOG.v("removeAccount error: " + e);
+ }
+ if (done) {
+ if (!success) {
+ showErrorDialog();
+ } else {
+ getFragmentController().goBack();
+ }
+ }
+ };
+
+ private ConfirmationDialogFragment.ConfirmListener mConfirmListener = arguments -> {
+ AccountManager.get(getContext()).removeAccountAsUser(getAccount(), /* activity= */ null,
+ mCallback, null, getUserHandle());
+ ConfirmationDialogFragment dialog =
+ (ConfirmationDialogFragment) getFragmentController().findDialogByTag(
+ ConfirmationDialogFragment.TAG);
+ if (dialog != null) {
+ dialog.dismiss();
+ }
+ };
+
+ public AccountDetailsPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ }
+
+ @Override
+ @CallSuper
+ protected void onCreateInternal() {
+ super.onCreateInternal();
+ ConfirmationDialogFragment dialog =
+ (ConfirmationDialogFragment) getFragmentController().findDialogByTag(
+ ConfirmationDialogFragment.TAG);
+ ConfirmationDialogFragment.resetListeners(
+ dialog,
+ mConfirmListener,
+ /* rejectListener= */ null,
+ /* neutralListener= */ null);
+
+ getPreference().setSecondaryActionVisible(
+ getSecondaryActionAvailabilityStatus() == AVAILABLE
+ || getSecondaryActionAvailabilityStatus() == AVAILABLE_FOR_VIEWING);
+ getPreference().setSecondaryActionEnabled(
+ getSecondaryActionAvailabilityStatus() != DISABLED_FOR_PROFILE);
+
+ getPreference().setOnSecondaryActionClickListener(this::onRemoveAccountClicked);
+ }
+
+ @Override
+ public void processActivityResult(int requestCode, int resultCode, Intent data) {
+ if (!isStarted()) {
+ return;
+ }
+ if (requestCode == REMOVE_ACCOUNT_REQUEST) {
+ // Activity result code may not adequately reflect the account removal status, so
+ // KEY_BOOLEAN_RESULT is used here instead. If the intent does not have this value
+ // included, a no-op will be performed and the account update listener in
+ // {@link AccountDetailsFragment} will still handle the back navigation upon removal.
+ if (data != null && data.hasExtra(AccountManager.KEY_BOOLEAN_RESULT)) {
+ boolean success = data.getBooleanExtra(AccountManager.KEY_BOOLEAN_RESULT, false);
+ if (success) {
+ getFragmentController().goBack();
+ } else {
+ showErrorDialog();
+ }
+ }
+ }
+ }
+
+ private int getSecondaryActionAvailabilityStatus() {
+ ProfileHelper profileHelper = getProfileHelper();
+ if (profileHelper.canCurrentProcessModifyAccounts()) {
+ return AVAILABLE;
+ }
+ if (profileHelper.isDemoOrGuest()
+ || hasUserRestrictionByUm(getContext(), DISALLOW_MODIFY_ACCOUNTS)) {
+ return DISABLED_FOR_PROFILE;
+ }
+ return AVAILABLE_FOR_VIEWING;
+ }
+
+ private void onRemoveAccountClicked() {
+ if (hasUserRestrictionByDpm(getContext(), DISALLOW_MODIFY_ACCOUNTS)) {
+ showActionDisabledByAdminDialog();
+ }
+ ConfirmationDialogFragment dialog =
+ new ConfirmationDialogFragment.Builder(getContext())
+ .setTitle(R.string.really_remove_account_title)
+ .setMessage(R.string.really_remove_account_message)
+ .setNegativeButton(android.R.string.cancel, null)
+ .setPositiveButton(R.string.remove_account_title, mConfirmListener)
+ .build();
+
+ getFragmentController().showDialog(dialog, ConfirmationDialogFragment.TAG);
+ }
+
+ private void showErrorDialog() {
+ getFragmentController().showDialog(
+ ErrorDialog.newInstance(R.string.remove_account_error_title), /* tag= */ null);
+ }
+
+ private void showActionDisabledByAdminDialog() {
+ getFragmentController().showDialog(
+ EnterpriseUtils.getActionDisabledByAdminDialog(getContext(),
+ DISALLOW_MODIFY_ACCOUNTS),
+ DISABLED_BY_ADMIN_CONFIRM_DIALOG_TAG);
+ }
+
+ @VisibleForTesting
+ ProfileHelper getProfileHelper() {
+ return ProfileHelper.getInstance(getContext());
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/accounts/AccountDetailsSettingController.java b/CarSettings/src/com/android/car/settings/accounts/AccountDetailsSettingController.java
new file mode 100644
index 00000000..056e643a
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/accounts/AccountDetailsSettingController.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.car.settings.accounts;
+
+import android.accounts.Account;
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+import android.os.Bundle;
+
+import androidx.annotation.CallSuper;
+import androidx.preference.Preference;
+
+import com.android.car.settings.common.ExtraSettingsPreferenceController;
+import com.android.car.settings.common.FragmentController;
+
+import java.util.Map;
+
+/**
+ * Injects preferences from other system applications at a placeholder location into the account
+ * details fragment. This class is and extension of {@link ExtraSettingsPreferenceController} which
+ * is needed to check what all preferences to show in the account details page.
+ */
+public class AccountDetailsSettingController extends ExtraSettingsPreferenceController {
+
+ private static final String METADATA_IA_ACCOUNT = "com.android.settings.ia.account";
+ private Account mAccount;
+
+ public AccountDetailsSettingController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions restrictionInfo) {
+ super(context, preferenceKey, fragmentController, restrictionInfo);
+ }
+
+ /** Sets the account that the preferences are being shown for. */
+ public void setAccount(Account account) {
+ mAccount = account;
+ }
+
+ @Override
+ @CallSuper
+ protected void checkInitialized() {
+ if (mAccount == null) {
+ throw new IllegalStateException(
+ "AccountDetailsSettingController must be initialized by calling "
+ + "setAccount(Account)");
+ }
+ }
+
+ @Override
+ @CallSuper
+ protected void addExtraSettings(Map preferenceBundleMap) {
+ for (Preference setting : preferenceBundleMap.keySet()) {
+ if (mAccount != null && !mAccount.type.equals(
+ preferenceBundleMap.get(setting).getString(METADATA_IA_ACCOUNT))) {
+ continue;
+ }
+ getPreference().addPreference(setting);
+ }
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/accounts/AccountDetailsWithSyncStatusPreferenceController.java b/CarSettings/src/com/android/car/settings/accounts/AccountDetailsWithSyncStatusPreferenceController.java
new file mode 100644
index 00000000..caeb1448
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/accounts/AccountDetailsWithSyncStatusPreferenceController.java
@@ -0,0 +1,168 @@
+/*
+ * 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.car.settings.accounts;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.SyncAdapterType;
+import android.content.SyncInfo;
+import android.content.SyncStatusInfo;
+import android.content.SyncStatusObserver;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.FragmentController;
+import com.android.car.ui.preference.CarUiTwoActionIconPreference;
+import com.android.settingslib.utils.ThreadUtils;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Controller for the preference that shows information about an account, including info about
+ * failures. It also handles a secondary button for account syncing.
+ */
+public class AccountDetailsWithSyncStatusPreferenceController extends
+ AccountDetailsBasePreferenceController {
+ private Object mStatusChangeListenerHandle;
+ private SyncStatusObserver mSyncStatusObserver =
+ which -> ThreadUtils.postOnMainThread(() -> {
+ // The observer call may occur even if the fragment hasn't been started, so
+ // only force an update if the fragment hasn't been stopped.
+ if (isStarted()) {
+ refreshUi();
+ }
+ });
+
+ public AccountDetailsWithSyncStatusPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ }
+
+ /**
+ * Registers the account update and sync status change callbacks.
+ */
+ @Override
+ protected void onStartInternal() {
+ mStatusChangeListenerHandle = ContentResolver.addStatusChangeListener(
+ ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE
+ | ContentResolver.SYNC_OBSERVER_TYPE_STATUS
+ | ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, mSyncStatusObserver);
+ }
+
+ /**
+ * Unregisters the account update and sync status change callbacks.
+ */
+ @Override
+ protected void onStopInternal() {
+ if (mStatusChangeListenerHandle != null) {
+ ContentResolver.removeStatusChangeListener(mStatusChangeListenerHandle);
+ }
+ }
+
+ @Override
+ protected void updateState(CarUiTwoActionIconPreference preference) {
+ super.updateState(preference);
+ if (isSyncFailing()) {
+ preference.setSummary(R.string.sync_is_failing);
+ } else {
+ preference.setSummary("");
+ }
+ updateSyncButton();
+ }
+
+ private boolean isSyncFailing() {
+ int userId = getUserHandle().getIdentifier();
+ List currentSyncs = getCurrentSyncs(userId);
+ boolean syncIsFailing = false;
+
+ Set syncAdapters = AccountSyncHelper.getVisibleSyncAdaptersForAccount(
+ getContext(), getAccount(), getUserHandle());
+ for (SyncAdapterType syncAdapter : syncAdapters) {
+ String authority = syncAdapter.authority;
+
+ SyncStatusInfo status = ContentResolver.getSyncStatusAsUser(getAccount(), authority,
+ userId);
+ boolean syncEnabled = ContentResolver.getSyncAutomaticallyAsUser(getAccount(),
+ authority, userId);
+ boolean activelySyncing = AccountSyncHelper.isSyncing(getAccount(), currentSyncs,
+ authority);
+
+ AccountSyncHelper.SyncState syncState = AccountSyncHelper.getSyncState(status,
+ syncEnabled, activelySyncing);
+
+ boolean syncIsPending = status != null && status.pending;
+ if (syncState == AccountSyncHelper.SyncState.FAILED && !activelySyncing
+ && !syncIsPending) {
+ syncIsFailing = true;
+ }
+ }
+
+ return syncIsFailing;
+ }
+
+ private void updateSyncButton() {
+ // Set the button to either request or cancel sync, depending on the current state
+ boolean hasActiveSyncs = !getCurrentSyncs(
+ getUserHandle().getIdentifier()).isEmpty();
+
+ // If there are active syncs, clicking the button with cancel them. Otherwise, clicking the
+ // button will start them.
+ getPreference().setSecondaryActionIcon(
+ hasActiveSyncs ? R.drawable.ic_sync_cancel : R.drawable.ic_sync);
+ getPreference().setOnSecondaryActionClickListener(hasActiveSyncs
+ ? this::cancelSyncForEnabledProviders
+ : this::requestSyncForEnabledProviders);
+ }
+
+ private void requestSyncForEnabledProviders() {
+ int userId = getUserHandle().getIdentifier();
+
+ Set adapters = AccountSyncHelper.getSyncableSyncAdaptersForAccount(
+ getAccount(), getUserHandle());
+ for (SyncAdapterType adapter : adapters) {
+ requestSync(adapter.authority, userId);
+ }
+ }
+
+ private void cancelSyncForEnabledProviders() {
+ int userId = getUserHandle().getIdentifier();
+
+ Set adapters = AccountSyncHelper.getSyncableSyncAdaptersForAccount(
+ getAccount(), getUserHandle());
+ for (SyncAdapterType adapter : adapters) {
+ cancelSync(adapter.authority, userId);
+ }
+ }
+
+ @VisibleForTesting
+ List getCurrentSyncs(int userId) {
+ return ContentResolver.getCurrentSyncsAsUser(userId);
+ }
+
+ @VisibleForTesting
+ void requestSync(String authority, int userId) {
+ AccountSyncHelper.requestSyncIfAllowed(getAccount(), authority, userId);
+ }
+
+ @VisibleForTesting
+ void cancelSync(String authority, int userId) {
+ ContentResolver.cancelSyncAsUser(getAccount(), authority, userId);
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/accounts/AccountGroupPreferenceController.java b/CarSettings/src/com/android/car/settings/accounts/AccountGroupPreferenceController.java
new file mode 100644
index 00000000..a01fbc82
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/accounts/AccountGroupPreferenceController.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.settings.accounts;
+
+import static android.os.UserManager.DISALLOW_MODIFY_ACCOUNTS;
+
+import static com.android.car.settings.enterprise.EnterpriseUtils.hasUserRestrictionByUm;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.PreferenceGroup;
+
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.profiles.ProfileDetailsBasePreferenceController;
+import com.android.car.settings.profiles.ProfileHelper;
+
+/**
+ * Controller for displaying accounts and associated actions.
+ * Ensures changes can only be made by the appropriate users.
+ */
+public class AccountGroupPreferenceController extends
+ ProfileDetailsBasePreferenceController {
+
+ public AccountGroupPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ }
+
+ @Override
+ protected Class getPreferenceType() {
+ return PreferenceGroup.class;
+ }
+
+ @Override
+ protected void onCreateInternal() {
+ super.onCreateInternal();
+ setClickableWhileDisabled(getPreference(), /* clickable= */ true, p -> getProfileHelper()
+ .runClickableWhileDisabled(getContext(), getFragmentController()));
+ }
+
+ @Override
+ protected int getDefaultAvailabilityStatus() {
+ ProfileHelper profileHelper = getProfileHelper();
+ boolean isCurrentUser = profileHelper.isCurrentProcessUser(getUserInfo());
+ boolean canModifyAccounts = getProfileHelper().canCurrentProcessModifyAccounts();
+
+ if (isCurrentUser && canModifyAccounts) {
+ return AVAILABLE;
+ }
+ if (!isCurrentUser || profileHelper.isDemoOrGuest()
+ || hasUserRestrictionByUm(getContext(), DISALLOW_MODIFY_ACCOUNTS)) {
+ return DISABLED_FOR_PROFILE;
+ }
+ return AVAILABLE_FOR_VIEWING;
+ }
+
+ @VisibleForTesting
+ ProfileHelper getProfileHelper() {
+ return ProfileHelper.getInstance(getContext());
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/accounts/AccountListPreferenceController.java b/CarSettings/src/com/android/car/settings/accounts/AccountListPreferenceController.java
new file mode 100644
index 00000000..4fd534fd
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/accounts/AccountListPreferenceController.java
@@ -0,0 +1,345 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.settings.accounts;
+
+import static android.os.UserManager.DISALLOW_MODIFY_ACCOUNTS;
+
+import static com.android.car.settings.enterprise.EnterpriseUtils.hasUserRestrictionByUm;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.UserInfo;
+import android.graphics.drawable.Drawable;
+import android.os.UserHandle;
+
+import androidx.collection.ArrayMap;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceCategory;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.PreferenceController;
+import com.android.car.settings.profiles.ProfileHelper;
+import com.android.car.settings.profiles.ProfileUtils;
+import com.android.car.ui.preference.CarUiPreference;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.settingslib.accounts.AuthenticatorHelper;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Controller for listing accounts.
+ *
+ * Largely derived from {@link com.android.settings.accounts.AccountPreferenceController}
+ */
+public class AccountListPreferenceController extends
+ PreferenceController implements
+ AuthenticatorHelper.OnAccountsUpdateListener {
+ private static final String NO_ACCOUNT_PREF_KEY = "no_accounts_added";
+
+ private final ArrayMap mPreferences = new ArrayMap<>();
+
+ private UserInfo mUserInfo;
+ private AuthenticatorHelper mAuthenticatorHelper;
+ private String[] mAuthorities;
+ private boolean mListenerRegistered = false;
+ private boolean mNeedToRefreshUserInfo = false;
+
+ private final BroadcastReceiver mUserUpdateReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ onUsersUpdate();
+ }
+ };
+
+ public AccountListPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ mUserInfo = ProfileHelper.getInstance(context).getCurrentProcessUserInfo();
+ mAuthenticatorHelper = createAuthenticatorHelper();
+ }
+
+ /** Sets the account authorities that are available. */
+ public void setAuthorities(String[] authorities) {
+ mAuthorities = authorities;
+ }
+
+ @Override
+ protected Class getPreferenceType() {
+ return PreferenceCategory.class;
+ }
+
+ @Override
+ protected void updateState(PreferenceCategory preference) {
+ forceUpdateAccountsCategory();
+ }
+
+ @Override
+ protected void onCreateInternal() {
+ super.onCreateInternal();
+ setClickableWhileDisabled(getPreference(), /* clickable= */ true, p -> getProfileHelper()
+ .runClickableWhileDisabled(getContext(), getFragmentController()));
+ }
+
+ @Override
+ protected int getDefaultAvailabilityStatus() {
+ ProfileHelper profileHelper = getProfileHelper();
+ boolean canModifyAccounts = profileHelper.canCurrentProcessModifyAccounts();
+
+ if (canModifyAccounts) {
+ return AVAILABLE;
+ }
+
+ if (profileHelper.isDemoOrGuest()
+ || hasUserRestrictionByUm(getContext(), DISALLOW_MODIFY_ACCOUNTS)) {
+ return DISABLED_FOR_PROFILE;
+ }
+
+ return AVAILABLE_FOR_VIEWING;
+ }
+
+ /**
+ * Registers the account update and user update callbacks.
+ */
+ @Override
+ protected void onStartInternal() {
+ mAuthenticatorHelper.listenToAccountUpdates();
+ registerForUserEvents();
+ mListenerRegistered = true;
+
+ /* refresh UserInfo only when restarting */
+ if (mNeedToRefreshUserInfo) {
+ onUsersUpdate();
+ }
+ }
+
+ /**
+ * Unregisters the account update and user update callbacks.
+ */
+ @Override
+ protected void onStopInternal() {
+ mAuthenticatorHelper.stopListeningToAccountUpdates();
+ unregisterForUserEvents();
+ mListenerRegistered = false;
+ mNeedToRefreshUserInfo = true;
+ }
+
+ @Override
+ public void onAccountsUpdate(UserHandle userHandle) {
+ if (userHandle.equals(mUserInfo.getUserHandle())) {
+ forceUpdateAccountsCategory();
+ }
+ }
+
+ @VisibleForTesting
+ void onUsersUpdate() {
+ mUserInfo = ProfileUtils.getUserInfo(getContext(), mUserInfo.id);
+ forceUpdateAccountsCategory();
+ }
+
+ @VisibleForTesting
+ AuthenticatorHelper createAuthenticatorHelper() {
+ return new AuthenticatorHelper(getContext(), mUserInfo.getUserHandle(), this);
+ }
+
+ private boolean onAccountPreferenceClicked(AccountPreference preference) {
+ // Show the account's details when an account is clicked on.
+ getFragmentController().launchFragment(AccountDetailsFragment.newInstance(
+ preference.getAccount(), preference.getLabel(), mUserInfo));
+ return true;
+ }
+
+ /** Forces a refresh of the account preferences. */
+ private void forceUpdateAccountsCategory() {
+ // Set the category title and include the user's name
+ getPreference().setTitle(
+ getContext().getString(R.string.account_list_title, mUserInfo.name));
+
+ // Recreate the authentication helper to refresh the list of enabled accounts
+ mAuthenticatorHelper.stopListeningToAccountUpdates();
+ mAuthenticatorHelper = createAuthenticatorHelper();
+ if (mListenerRegistered) {
+ mAuthenticatorHelper.listenToAccountUpdates();
+ }
+
+ Set preferencesToRemove = new HashSet<>(mPreferences.keySet());
+ List extends Preference> preferences = getAccountPreferences(preferencesToRemove);
+ // Add all preferences that aren't already shown. Manually set the order so that existing
+ // preferences are reordered correctly.
+ for (int i = 0; i < preferences.size(); i++) {
+ Preference pref = preferences.get(i);
+ pref.setOrder(i);
+ mPreferences.put(pref.getKey(), pref);
+ getPreference().addPreference(pref);
+ }
+
+ for (String key : preferencesToRemove) {
+ getPreference().removePreference(mPreferences.get(key));
+ mPreferences.remove(key);
+ }
+ }
+
+ /**
+ * Returns a list of preferences corresponding to the accounts for the current user.
+ *
+ * Derived from
+ * {@link com.android.settings.accounts.AccountPreferenceController#getAccountTypePreferences}
+ *
+ * @param preferencesToRemove the current preferences shown; only preferences to be removed will
+ * remain after method execution
+ */
+ private List extends Preference> getAccountPreferences(
+ Set preferencesToRemove) {
+ String[] accountTypes = mAuthenticatorHelper.getEnabledAccountTypes();
+ ArrayList accountPreferences =
+ new ArrayList<>(accountTypes.length);
+
+ for (int i = 0; i < accountTypes.length; i++) {
+ String accountType = accountTypes[i];
+ // Skip showing any account that does not have any of the requested authorities
+ if (!accountTypeHasAnyRequestedAuthorities(accountType)) {
+ continue;
+ }
+ CharSequence label = mAuthenticatorHelper.getLabelForType(getContext(), accountType);
+ if (label == null) {
+ continue;
+ }
+
+ Account[] accounts = AccountManager.get(getContext())
+ .getAccountsByTypeAsUser(accountType, mUserInfo.getUserHandle());
+ Drawable icon = mAuthenticatorHelper.getDrawableForType(getContext(), accountType);
+
+ // Add a preference row for each individual account
+ for (Account account : accounts) {
+ String key = AccountPreference.buildKey(account);
+ AccountPreference preference = (AccountPreference) mPreferences.getOrDefault(key,
+ new AccountPreference(getContext(), account, label, icon));
+ preference.setOnPreferenceClickListener(
+ (Preference pref) -> onAccountPreferenceClicked((AccountPreference) pref));
+
+ accountPreferences.add(preference);
+ preferencesToRemove.remove(key);
+ }
+ mAuthenticatorHelper.preloadDrawableForType(getContext(), accountType);
+ }
+
+ // If there are no accounts, return the "no account added" preference.
+ if (accountPreferences.isEmpty()) {
+ preferencesToRemove.remove(NO_ACCOUNT_PREF_KEY);
+ return Arrays.asList(mPreferences.getOrDefault(NO_ACCOUNT_PREF_KEY,
+ createNoAccountsAddedPreference()));
+ }
+
+ Collections.sort(accountPreferences, Comparator.comparing(
+ (AccountPreference a) -> a.getSummary().toString())
+ .thenComparing((AccountPreference a) -> a.getTitle().toString()));
+
+ return accountPreferences;
+ }
+
+ private Preference createNoAccountsAddedPreference() {
+ CarUiPreference emptyPreference = new CarUiPreference(getContext());
+ emptyPreference.setTitle(R.string.no_accounts_added);
+ emptyPreference.setKey(NO_ACCOUNT_PREF_KEY);
+ emptyPreference.setSelectable(false);
+
+ return emptyPreference;
+ }
+
+ private void registerForUserEvents() {
+ IntentFilter filter = new IntentFilter(Intent.ACTION_USER_INFO_CHANGED);
+ getContext().registerReceiver(mUserUpdateReceiver, filter);
+ }
+
+ private void unregisterForUserEvents() {
+ getContext().unregisterReceiver(mUserUpdateReceiver);
+ }
+
+
+ /**
+ * Returns whether the account type has any of the authorities requested by the caller.
+ *
+ * Derived from {@link AccountPreferenceController#accountTypeHasAnyRequestedAuthorities}
+ */
+ private boolean accountTypeHasAnyRequestedAuthorities(String accountType) {
+ if (mAuthorities == null || mAuthorities.length == 0) {
+ // No authorities required
+ return true;
+ }
+ ArrayList authoritiesForType =
+ mAuthenticatorHelper.getAuthoritiesForAccountType(accountType);
+ if (authoritiesForType == null) {
+ return false;
+ }
+ for (int j = 0; j < mAuthorities.length; j++) {
+ if (authoritiesForType.contains(mAuthorities[j])) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @VisibleForTesting
+ ProfileHelper getProfileHelper() {
+ return ProfileHelper.getInstance(getContext());
+ }
+
+ private static class AccountPreference extends CarUiPreference {
+ /** Account that this Preference represents. */
+ private final Account mAccount;
+ private final CharSequence mLabel;
+
+ private AccountPreference(Context context, Account account, CharSequence label,
+ Drawable icon) {
+ super(context);
+ mAccount = account;
+ mLabel = label;
+
+ setKey(buildKey(account));
+ setTitle(account.name);
+ setSummary(label);
+ setIcon(icon);
+ setShowChevron(false);
+ }
+
+ /**
+ * Build a unique preference key based on the account.
+ */
+ public static String buildKey(Account account) {
+ return String.valueOf(account.hashCode());
+ }
+
+ public Account getAccount() {
+ return mAccount;
+ }
+
+ public CharSequence getLabel() {
+ return mLabel;
+ }
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/accounts/AccountSyncDetailsFragment.java b/CarSettings/src/com/android/car/settings/accounts/AccountSyncDetailsFragment.java
new file mode 100644
index 00000000..409073d5
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/accounts/AccountSyncDetailsFragment.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.car.settings.accounts;
+
+import android.accounts.Account;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.UserHandle;
+
+import androidx.annotation.XmlRes;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.SettingsFragment;
+
+/**
+ * Shows details for syncing an account.
+ */
+public class AccountSyncDetailsFragment extends SettingsFragment {
+ private static final String EXTRA_ACCOUNT = "extra_account";
+ private static final String EXTRA_USER_HANDLE = "extra_user_handle";
+
+ /**
+ * Creates a new AccountSyncDetailsFragment.
+ *
+ * Passes the provided account and user handle to the fragment via fragment arguments.
+ */
+ public static AccountSyncDetailsFragment newInstance(Account account, UserHandle userHandle) {
+ AccountSyncDetailsFragment accountSyncDetailsFragment = new AccountSyncDetailsFragment();
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(EXTRA_ACCOUNT, account);
+ bundle.putParcelable(EXTRA_USER_HANDLE, userHandle);
+ accountSyncDetailsFragment.setArguments(bundle);
+ return accountSyncDetailsFragment;
+ }
+
+ @Override
+ @XmlRes
+ protected int getPreferenceScreenResId() {
+ return R.xml.account_sync_details_fragment;
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+
+ Account account = getArguments().getParcelable(EXTRA_ACCOUNT);
+ UserHandle userHandle = getArguments().getParcelable(EXTRA_USER_HANDLE);
+
+ use(AccountDetailsWithSyncStatusPreferenceController.class,
+ R.string.pk_account_details_with_sync)
+ .setAccount(account);
+ use(AccountDetailsWithSyncStatusPreferenceController.class,
+ R.string.pk_account_details_with_sync)
+ .setUserHandle(userHandle);
+
+ use(AccountSyncDetailsPreferenceController.class, R.string.pk_account_sync_details)
+ .setAccount(account);
+ use(AccountSyncDetailsPreferenceController.class, R.string.pk_account_sync_details)
+ .setUserHandle(userHandle);
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/accounts/AccountSyncDetailsPreferenceController.java b/CarSettings/src/com/android/car/settings/accounts/AccountSyncDetailsPreferenceController.java
new file mode 100644
index 00000000..d3aaba2a
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/accounts/AccountSyncDetailsPreferenceController.java
@@ -0,0 +1,385 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.settings.accounts;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.app.Activity;
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.content.SyncAdapterType;
+import android.content.SyncInfo;
+import android.content.SyncStatusInfo;
+import android.content.SyncStatusObserver;
+import android.content.pm.PackageManager;
+import android.os.UserHandle;
+import android.text.format.DateFormat;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.collection.ArrayMap;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceGroup;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.Logger;
+import com.android.car.settings.common.PreferenceController;
+import com.android.settingslib.accounts.AuthenticatorHelper;
+import com.android.settingslib.utils.ThreadUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Controller that presents all visible sync adapters for an account.
+ *
+ *
Largely derived from {@link com.android.settings.accounts.AccountSyncSettings}.
+ */
+public class AccountSyncDetailsPreferenceController extends
+ PreferenceController implements
+ AuthenticatorHelper.OnAccountsUpdateListener {
+ private static final Logger LOG = new Logger(AccountSyncDetailsPreferenceController.class);
+ /**
+ * Preferences are keyed by authority so that existing SyncPreferences can be reused on account
+ * sync.
+ */
+ private final Map mSyncPreferences = new ArrayMap<>();
+ private Account mAccount;
+ private UserHandle mUserHandle;
+ private AuthenticatorHelper mAuthenticatorHelper;
+ private Object mStatusChangeListenerHandle;
+ private SyncStatusObserver mSyncStatusObserver =
+ which -> ThreadUtils.postOnMainThread(() -> {
+ // The observer call may occur even if the fragment hasn't been started, so
+ // only force an update if the fragment hasn't been stopped.
+ if (isStarted()) {
+ forceUpdateSyncCategory();
+ }
+ });
+
+ public AccountSyncDetailsPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ }
+
+ /** Sets the account that the sync preferences are being shown for. */
+ public void setAccount(Account account) {
+ mAccount = account;
+ }
+
+ /** Sets the user handle used by the controller. */
+ public void setUserHandle(UserHandle userHandle) {
+ mUserHandle = userHandle;
+ }
+
+ @Override
+ protected Class getPreferenceType() {
+ return PreferenceGroup.class;
+ }
+
+ /**
+ * Verifies that the controller was properly initialized with {@link #setAccount(Account)} and
+ * {@link #setUserHandle(UserHandle)}.
+ *
+ * @throws IllegalStateException if the account or user handle is {@code null}
+ */
+ @Override
+ protected void checkInitialized() {
+ LOG.v("checkInitialized");
+ if (mAccount == null) {
+ throw new IllegalStateException(
+ "AccountSyncDetailsPreferenceController must be initialized by calling "
+ + "setAccount(Account)");
+ }
+ if (mUserHandle == null) {
+ throw new IllegalStateException(
+ "AccountSyncDetailsPreferenceController must be initialized by calling "
+ + "setUserHandle(UserHandle)");
+ }
+ }
+
+ /**
+ * Initializes the authenticator helper.
+ */
+ @Override
+ protected void onCreateInternal() {
+ mAuthenticatorHelper = new AuthenticatorHelper(getContext(), mUserHandle, /* listener= */
+ this);
+ }
+
+ /**
+ * Registers the account update and sync status change callbacks.
+ */
+ @Override
+ protected void onStartInternal() {
+ mAuthenticatorHelper.listenToAccountUpdates();
+
+ mStatusChangeListenerHandle = ContentResolver.addStatusChangeListener(
+ ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE
+ | ContentResolver.SYNC_OBSERVER_TYPE_STATUS
+ | ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, mSyncStatusObserver);
+ }
+
+ /**
+ * Unregisters the account update and sync status change callbacks.
+ */
+ @Override
+ protected void onStopInternal() {
+ mAuthenticatorHelper.stopListeningToAccountUpdates();
+ if (mStatusChangeListenerHandle != null) {
+ ContentResolver.removeStatusChangeListener(mStatusChangeListenerHandle);
+ }
+ }
+
+ @Override
+ public void onAccountsUpdate(UserHandle userHandle) {
+ // Only force a refresh if accounts have changed for the current user.
+ if (userHandle.equals(mUserHandle)) {
+ forceUpdateSyncCategory();
+ }
+ }
+
+ @Override
+ public void updateState(PreferenceGroup preferenceGroup) {
+ // Add preferences for each account if the controller should be available
+ forceUpdateSyncCategory();
+ }
+
+ /**
+ * Handles toggling/syncing when a sync preference is clicked on.
+ *
+ * Largely derived from
+ * {@link com.android.settings.accounts.AccountSyncSettings#onPreferenceTreeClick}.
+ */
+ private boolean onSyncPreferenceClicked(SyncPreference preference) {
+ String authority = preference.getKey();
+ String packageName = preference.getPackageName();
+ int uid = preference.getUid();
+ if (preference.isOneTimeSyncMode()) {
+ // If the sync adapter doesn't have access to the account we either
+ // request access by starting an activity if possible or kick off the
+ // sync which will end up posting an access request notification.
+ if (requestAccountAccessIfNeeded(packageName, uid)) {
+ return true;
+ }
+ requestSync(authority);
+ } else {
+ boolean syncOn = preference.isChecked();
+ int userId = mUserHandle.getIdentifier();
+ boolean oldSyncState = ContentResolver.getSyncAutomaticallyAsUser(mAccount,
+ authority, userId);
+ if (syncOn != oldSyncState) {
+ // Toggling this switch triggers sync but we may need a user approval. If the
+ // sync adapter doesn't have access to the account we either request access by
+ // starting an activity if possible or kick off the sync which will end up
+ // posting an access request notification.
+ if (syncOn && requestAccountAccessIfNeeded(packageName, uid)) {
+ return true;
+ }
+ // If we're enabling sync, this will request a sync as well.
+ ContentResolver.setSyncAutomaticallyAsUser(mAccount, authority, syncOn, userId);
+ if (syncOn) {
+ requestSync(authority);
+ } else {
+ cancelSync(authority);
+ }
+ }
+ }
+ return true;
+ }
+
+ private void requestSync(String authority) {
+ AccountSyncHelper.requestSyncIfAllowed(mAccount, authority, mUserHandle.getIdentifier());
+ }
+
+ private void cancelSync(String authority) {
+ ContentResolver.cancelSyncAsUser(mAccount, authority, mUserHandle.getIdentifier());
+ }
+
+ /**
+ * Requests account access if needed.
+ *
+ *
Copied from
+ * {@link com.android.settings.accounts.AccountSyncSettings#requestAccountAccessIfNeeded}.
+ */
+ private boolean requestAccountAccessIfNeeded(String packageName, int uid) {
+ if (packageName == null) {
+ return false;
+ }
+
+ AccountManager accountManager = getContext().getSystemService(AccountManager.class);
+ if (!accountManager.hasAccountAccess(mAccount, packageName, mUserHandle)) {
+ IntentSender intent = accountManager.createRequestAccountAccessIntentSenderAsUser(
+ mAccount, packageName, mUserHandle);
+ if (intent != null) {
+ try {
+ getFragmentController().startIntentSenderForResult(intent,
+ uid, /* fillInIntent= */ null, /* flagsMask= */ 0,
+ /* flagsValues= */ 0, /* options= */ null,
+ this::onAccountRequestApproved);
+ return true;
+ } catch (IntentSender.SendIntentException e) {
+ LOG.e("Error requesting account access", e);
+ }
+ }
+ }
+ return false;
+ }
+
+ /** Handles a sync adapter refresh when an account request was approved. */
+ public void onAccountRequestApproved(int uid, int resultCode, @Nullable Intent data) {
+ if (resultCode == Activity.RESULT_OK) {
+ for (SyncPreference pref : mSyncPreferences.values()) {
+ if (pref.getUid() == uid) {
+ onSyncPreferenceClicked(pref);
+ return;
+ }
+ }
+ }
+ }
+
+ /** Forces a refresh of the sync adapter preferences. */
+ private void forceUpdateSyncCategory() {
+ Set preferencesToRemove = new HashSet<>(mSyncPreferences.keySet());
+ List preferences = getSyncPreferences(preferencesToRemove);
+
+ // Sort the preferences, add the ones that need to be added, and remove the ones that need
+ // to be removed. Manually set the order so that existing preferences are reordered
+ // correctly.
+ Collections.sort(preferences, Comparator.comparing(
+ (SyncPreference a) -> a.getTitle().toString())
+ .thenComparing((SyncPreference a) -> a.getSummary().toString()));
+
+ for (int i = 0; i < preferences.size(); i++) {
+ SyncPreference pref = preferences.get(i);
+ pref.setOrder(i);
+ mSyncPreferences.put(pref.getKey(), pref);
+ getPreference().addPreference(pref);
+ }
+
+ for (String key : preferencesToRemove) {
+ getPreference().removePreference(mSyncPreferences.get(key));
+ mSyncPreferences.remove(key);
+ }
+ }
+
+ /**
+ * Returns a list of preferences corresponding to the visible sync adapters for the current
+ * user.
+ *
+ * Derived from {@link com.android.settings.accounts.AccountSyncSettings#setFeedsState}
+ * and {@link com.android.settings.accounts.AccountSyncSettings#updateAccountSwitches}.
+ *
+ * @param preferencesToRemove the keys for the preferences currently being shown; only the keys
+ * for preferences to be removed will remain after method execution
+ */
+ private List getSyncPreferences(Set preferencesToRemove) {
+ int userId = mUserHandle.getIdentifier();
+ PackageManager packageManager = getContext().getPackageManager();
+ List currentSyncs = ContentResolver.getCurrentSyncsAsUser(userId);
+ // Whether one time sync is enabled rather than automtic sync
+ boolean oneTimeSyncMode = !ContentResolver.getMasterSyncAutomaticallyAsUser(userId);
+
+ List syncPreferences = new ArrayList<>();
+
+ Set syncAdapters = AccountSyncHelper.getVisibleSyncAdaptersForAccount(
+ getContext(), mAccount, mUserHandle);
+ for (SyncAdapterType syncAdapter : syncAdapters) {
+ String authority = syncAdapter.authority;
+
+ int uid;
+ try {
+ uid = packageManager.getPackageUidAsUser(syncAdapter.getPackageName(), userId);
+ } catch (PackageManager.NameNotFoundException e) {
+ LOG.e("No uid for package" + syncAdapter.getPackageName(), e);
+ // If we can't get the Uid for the package hosting the sync adapter, don't show it
+ continue;
+ }
+
+ // If we've reached this point, the sync adapter should be shown. If a preference for
+ // the sync adapter already exists, update its state. Otherwise, create a new
+ // preference.
+ SyncPreference pref = mSyncPreferences.getOrDefault(authority,
+ new SyncPreference(getContext(), authority));
+ pref.setUid(uid);
+ pref.setPackageName(syncAdapter.getPackageName());
+ pref.setOnPreferenceClickListener(
+ (Preference p) -> onSyncPreferenceClicked((SyncPreference) p));
+
+ CharSequence title = AccountSyncHelper.getTitle(getContext(), authority, mUserHandle);
+ pref.setTitle(title);
+
+ // Keep track of preferences that need to be added and removed
+ syncPreferences.add(pref);
+ preferencesToRemove.remove(authority);
+
+ SyncStatusInfo status = ContentResolver.getSyncStatusAsUser(mAccount, authority,
+ userId);
+ boolean syncEnabled = ContentResolver.getSyncAutomaticallyAsUser(mAccount, authority,
+ userId);
+ boolean activelySyncing = AccountSyncHelper.isSyncing(mAccount, currentSyncs,
+ authority);
+
+ // The preference should be checked if one one-time sync or regular sync is enabled
+ boolean checked = oneTimeSyncMode || syncEnabled;
+ pref.setChecked(checked);
+
+ String summary = getSummary(status, syncEnabled, activelySyncing);
+ pref.setSummary(summary);
+
+ // Update the sync state so the icon is updated
+ AccountSyncHelper.SyncState syncState = AccountSyncHelper.getSyncState(status,
+ syncEnabled, activelySyncing);
+ pref.setSyncState(syncState);
+ pref.setOneTimeSyncMode(oneTimeSyncMode);
+ }
+
+ return syncPreferences;
+ }
+
+ private String getSummary(SyncStatusInfo status, boolean syncEnabled, boolean activelySyncing) {
+ long successEndTime = (status == null) ? 0 : status.lastSuccessTime;
+ // Set the summary based on the current syncing state
+ if (!syncEnabled) {
+ return getContext().getString(R.string.sync_disabled);
+ } else if (activelySyncing) {
+ return getContext().getString(R.string.sync_in_progress);
+ } else if (successEndTime != 0) {
+ Date date = new Date();
+ date.setTime(successEndTime);
+ String timeString = formatSyncDate(date);
+ return getContext().getString(R.string.last_synced, timeString);
+ }
+ return "";
+ }
+
+ @VisibleForTesting
+ String formatSyncDate(Date date) {
+ return DateFormat.getDateFormat(getContext()).format(date) + " " + DateFormat.getTimeFormat(
+ getContext()).format(date);
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/accounts/AccountSyncHelper.java b/CarSettings/src/com/android/car/settings/accounts/AccountSyncHelper.java
new file mode 100644
index 00000000..cbc7ca5d
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/accounts/AccountSyncHelper.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.settings.accounts;
+
+import android.accounts.Account;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.SyncAdapterType;
+import android.content.SyncInfo;
+import android.content.SyncStatusInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ProviderInfo;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.text.TextUtils;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.car.settings.common.Logger;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/** Helper that provides utility methods for account syncing. */
+class AccountSyncHelper {
+ private static final Logger LOG = new Logger(AccountSyncHelper.class);
+
+ private AccountSyncHelper() {
+ }
+
+ /** Returns the visible sync adapters available for an account. */
+ static Set getVisibleSyncAdaptersForAccount(Context context, Account account,
+ UserHandle userHandle) {
+ Set syncableAdapters = getSyncableSyncAdaptersForAccount(account,
+ userHandle);
+
+ syncableAdapters.removeIf(
+ (SyncAdapterType syncAdapter) -> !isVisible(context, syncAdapter, userHandle));
+
+ return syncableAdapters;
+ }
+
+ /** Returns the syncable sync adapters available for an account. */
+ static Set getSyncableSyncAdaptersForAccount(Account account,
+ UserHandle userHandle) {
+ Set adapters = new HashSet<>();
+
+ SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypesAsUser(
+ userHandle.getIdentifier());
+ for (int i = 0; i < syncAdapters.length; i++) {
+ SyncAdapterType syncAdapter = syncAdapters[i];
+ String authority = syncAdapter.authority;
+
+ // If the sync adapter is not for this account type, don't include it
+ if (!syncAdapter.accountType.equals(account.type)) {
+ continue;
+ }
+
+ boolean isSyncable = ContentResolver.getIsSyncableAsUser(account, authority,
+ userHandle.getIdentifier()) > 0;
+ // If the adapter is not syncable, don't include it
+ if (!isSyncable) {
+ continue;
+ }
+
+ adapters.add(syncAdapter);
+ }
+
+ return adapters;
+ }
+
+ /**
+ * Requests a sync if it is allowed.
+ *
+ * Derived from
+ * {@link com.android.settings.accounts.AccountSyncSettings#requestOrCancelSync}.
+ *
+ * @return {@code true} if sync was requested, {@code false} otherwise.
+ */
+ static boolean requestSyncIfAllowed(Account account, String authority, int userId) {
+ if (!syncIsAllowed(account, authority, userId)) {
+ return false;
+ }
+
+ Bundle extras = new Bundle();
+ extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
+ ContentResolver.requestSyncAsUser(account, authority, userId, extras);
+ return true;
+ }
+
+ /**
+ * Returns the label for a given sync authority.
+ *
+ * @return the title if available, and an empty CharSequence otherwise
+ */
+ static CharSequence getTitle(Context context, String authority, UserHandle userHandle) {
+ PackageManager packageManager = context.getPackageManager();
+ ProviderInfo providerInfo = packageManager.resolveContentProviderAsUser(
+ authority, /* flags= */ 0, userHandle.getIdentifier());
+ if (providerInfo == null) {
+ return "";
+ }
+
+ return providerInfo.loadLabel(packageManager);
+ }
+
+ /** Returns whether a sync adapter is currently syncing for the account being shown. */
+ static boolean isSyncing(Account account, List currentSyncs, String authority) {
+ for (SyncInfo syncInfo : currentSyncs) {
+ if (syncInfo.account.equals(account) && syncInfo.authority.equals(authority)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** Returns the current sync state based on sync status information. */
+ static SyncState getSyncState(SyncStatusInfo status, boolean syncEnabled,
+ boolean activelySyncing) {
+ boolean initialSync = status != null && status.initialize;
+ boolean syncIsPending = status != null && status.pending;
+ boolean lastSyncFailed = syncEnabled && status != null && status.lastFailureTime != 0
+ && status.getLastFailureMesgAsInt(0)
+ != ContentResolver.SYNC_ERROR_SYNC_ALREADY_IN_PROGRESS;
+ if (activelySyncing && !initialSync) {
+ return SyncState.ACTIVE;
+ } else if (syncIsPending && !initialSync) {
+ return SyncState.PENDING;
+ } else if (lastSyncFailed) {
+ return SyncState.FAILED;
+ }
+ return SyncState.NONE;
+ }
+
+ @VisibleForTesting
+ static boolean syncIsAllowed(Account account, String authority, int userId) {
+ boolean oneTimeSyncMode = !ContentResolver.getMasterSyncAutomaticallyAsUser(userId);
+ boolean syncEnabled = ContentResolver.getSyncAutomaticallyAsUser(account, authority,
+ userId);
+ return oneTimeSyncMode || syncEnabled;
+ }
+
+ private static boolean isVisible(Context context, SyncAdapterType syncAdapter,
+ UserHandle userHandle) {
+ String authority = syncAdapter.authority;
+
+ if (!syncAdapter.isUserVisible()) {
+ // If the sync adapter is not visible, don't show it
+ return false;
+ }
+
+ try {
+ context.getPackageManager().getPackageUidAsUser(syncAdapter.getPackageName(),
+ userHandle.getIdentifier());
+ } catch (PackageManager.NameNotFoundException e) {
+ LOG.e("No uid for package" + syncAdapter.getPackageName(), e);
+ // If we can't get the Uid for the package hosting the sync adapter, don't show it
+ return false;
+ }
+
+ CharSequence title = getTitle(context, authority, userHandle);
+ if (TextUtils.isEmpty(title)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /** Denotes a sync adapter state. */
+ public enum SyncState {
+ /** The sync adapter is actively syncing. */
+ ACTIVE,
+ /** The sync adapter is waiting to start syncing. */
+ PENDING,
+ /** The sync adapter's last attempt to sync failed. */
+ FAILED,
+ /** Nothing to note about the sync adapter's sync state. */
+ NONE;
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/accounts/AccountSyncPreferenceController.java b/CarSettings/src/com/android/car/settings/accounts/AccountSyncPreferenceController.java
new file mode 100644
index 00000000..54e6fd04
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/accounts/AccountSyncPreferenceController.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.car.settings.accounts;
+
+import android.accounts.Account;
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.SyncAdapterType;
+import android.os.UserHandle;
+
+import androidx.preference.Preference;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.Logger;
+import com.android.car.settings.common.PreferenceController;
+
+/**
+ * Controller for the account syncing preference.
+ *
+ * Largely derived from {@link com.android.settings.accounts.AccountSyncPreferenceController}.
+ */
+public class AccountSyncPreferenceController extends PreferenceController {
+ private static final Logger LOG = new Logger(AccountSyncPreferenceController.class);
+ private Account mAccount;
+ private UserHandle mUserHandle;
+
+ public AccountSyncPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ }
+
+ /** Sets the account that the sync preferences are being shown for. */
+ public void setAccount(Account account) {
+ mAccount = account;
+ }
+
+ /** Sets the user handle used by the controller. */
+ public void setUserHandle(UserHandle userHandle) {
+ mUserHandle = userHandle;
+ }
+
+ @Override
+ protected Class getPreferenceType() {
+ return Preference.class;
+ }
+
+ /**
+ * Verifies that the controller was properly initialized with
+ * {@link #setAccount(Account)} and {@link #setUserHandle(UserHandle)}.
+ *
+ * @throws IllegalStateException if the account is {@code null}
+ */
+ @Override
+ protected void checkInitialized() {
+ LOG.v("checkInitialized");
+ if (mAccount == null) {
+ throw new IllegalStateException(
+ "AccountSyncPreferenceController must be initialized by calling "
+ + "setAccount(Account)");
+ }
+ if (mUserHandle == null) {
+ throw new IllegalStateException(
+ "AccountSyncPreferenceController must be initialized by calling "
+ + "setUserHandle(UserHandle)");
+ }
+ }
+
+ @Override
+ protected void updateState(Preference preference) {
+ preference.setSummary(getSummary());
+ }
+
+ @Override
+ protected boolean handlePreferenceClicked(Preference preference) {
+ getFragmentController().launchFragment(
+ AccountSyncDetailsFragment.newInstance(mAccount, mUserHandle));
+ return true;
+ }
+
+ private CharSequence getSummary() {
+ int userId = mUserHandle.getIdentifier();
+ SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypesAsUser(userId);
+ int total = 0;
+ int enabled = 0;
+ if (syncAdapters != null) {
+ for (int i = 0, n = syncAdapters.length; i < n; i++) {
+ SyncAdapterType sa = syncAdapters[i];
+ // If the sync adapter isn't for this account type or if the user is not visible,
+ // don't show it.
+ if (!sa.accountType.equals(mAccount.type) || !sa.isUserVisible()) {
+ continue;
+ }
+ int syncState =
+ ContentResolver.getIsSyncableAsUser(mAccount, sa.authority, userId);
+ if (syncState > 0) {
+ // If the sync adapter is syncable, add it to the count of items that can be
+ // synced.
+ total++;
+
+ // If sync is enabled for the sync adapter at the master level or at the account
+ // level, add it to the count of items that are enabled.
+ boolean syncEnabled = ContentResolver.getSyncAutomaticallyAsUser(
+ mAccount, sa.authority, userId);
+ boolean oneTimeSyncMode =
+ !ContentResolver.getMasterSyncAutomaticallyAsUser(userId);
+ if (oneTimeSyncMode || syncEnabled) {
+ enabled++;
+ }
+ }
+ }
+ }
+ if (enabled == 0) {
+ return getContext().getText(R.string.account_sync_summary_all_off);
+ } else if (enabled == total) {
+ return getContext().getText(R.string.account_sync_summary_all_on);
+ } else {
+ return getContext().getString(R.string.account_sync_summary_some_on, enabled, total);
+ }
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/accounts/AccountTypesHelper.java b/CarSettings/src/com/android/car/settings/accounts/AccountTypesHelper.java
new file mode 100644
index 00000000..f45e3f68
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/accounts/AccountTypesHelper.java
@@ -0,0 +1,192 @@
+/*
+ * 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.car.settings.accounts;
+
+import android.accounts.AccountManager;
+import android.accounts.AuthenticatorDescription;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
+import android.os.UserHandle;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.settingslib.accounts.AuthenticatorHelper;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Utility that maintains a set of authorized account types.
+ */
+public class AccountTypesHelper {
+ /** Callback invoked when the set of authorized account types changes. */
+ public interface OnChangeListener {
+ /** Called when the set of authorized account types changes. */
+ void onAuthorizedAccountTypesChanged();
+ }
+
+ private final Context mContext;
+ private UserHandle mUserHandle;
+ private AuthenticatorHelper mAuthenticatorHelper;
+ private List mAuthorities;
+ private Set mAccountTypesFilter;
+ private Set mAccountTypesExclusionFilter;
+ private Set mAuthorizedAccountTypes;
+ private OnChangeListener mListener;
+
+ public AccountTypesHelper(Context context) {
+ mContext = context;
+
+ // Default to hardcoded Bluetooth account type.
+ mAccountTypesExclusionFilter = new HashSet<>();
+ mAccountTypesExclusionFilter.add("com.android.bluetooth.pbapsink");
+ setAccountTypesExclusionFilter(mAccountTypesExclusionFilter);
+
+ mUserHandle = UserHandle.of(UserHandle.myUserId());
+ mAuthenticatorHelper = new AuthenticatorHelper(mContext, mUserHandle,
+ userHandle -> {
+ // Only force a refresh if accounts have changed for the current user.
+ if (userHandle.equals(mUserHandle)) {
+ updateAuthorizedAccountTypes(false /* isForced */);
+ }
+ });
+ }
+
+ /** Sets the authorities that the user has. */
+ public void setAuthorities(List authorities) {
+ mAuthorities = authorities;
+ }
+
+ /** Sets the filter for accounts that should be shown. */
+ public void setAccountTypesFilter(Set accountTypesFilter) {
+ mAccountTypesFilter = accountTypesFilter;
+ }
+
+ /** Sets the filter for accounts that should NOT be shown. */
+ protected void setAccountTypesExclusionFilter(Set accountTypesExclusionFilter) {
+ mAccountTypesExclusionFilter = accountTypesExclusionFilter;
+ }
+
+ /** Sets the callback invoked when the set of authorized account types changes. */
+ public void setOnChangeListener(OnChangeListener listener) {
+ mListener = listener;
+ }
+
+ /**
+ * Updates the set of authorized account types.
+ *
+ * Derived from
+ * {@link com.android.settings.accounts.ChooseAccountActivity#onAuthDescriptionsUpdated}
+ */
+ @VisibleForTesting
+ void updateAuthorizedAccountTypes(boolean isForced) {
+ AccountManager accountManager = AccountManager.get(mContext);
+ AuthenticatorDescription[] authenticatorDescriptions =
+ accountManager.getAuthenticatorTypesAsUser(mUserHandle.getIdentifier());
+
+ Set authorizedAccountTypes = new HashSet<>();
+ for (AuthenticatorDescription authenticatorDescription : authenticatorDescriptions) {
+ String accountType = authenticatorDescription.type;
+
+ List accountAuthorities =
+ mAuthenticatorHelper.getAuthoritiesForAccountType(accountType);
+
+ // If there are specific authorities required, we need to check whether they are
+ // included in the account type.
+ boolean authorized =
+ (mAuthorities == null || mAuthorities.isEmpty() || accountAuthorities == null
+ || !Collections.disjoint(accountAuthorities, mAuthorities));
+
+ // If there is an account type filter, make sure this account type is included.
+ authorized = authorized && (mAccountTypesFilter == null
+ || mAccountTypesFilter.contains(accountType));
+
+ // Check if the account type is in the exclusion list.
+ authorized = authorized && (mAccountTypesExclusionFilter == null
+ || !mAccountTypesExclusionFilter.contains(accountType));
+
+ if (authorized) {
+ authorizedAccountTypes.add(accountType);
+ }
+ }
+
+ if (isForced || !Objects.equals(mAuthorizedAccountTypes, authorizedAccountTypes)) {
+ mAuthorizedAccountTypes = authorizedAccountTypes;
+ if (mListener != null) {
+ mListener.onAuthorizedAccountTypesChanged();
+ }
+ }
+ }
+
+ /** Returns the set of authorized account types, initializing the set first if necessary. */
+ public Set getAuthorizedAccountTypes() {
+ if (mAuthorizedAccountTypes == null) {
+ updateAuthorizedAccountTypes(false /* isForced */);
+ }
+ return mAuthorizedAccountTypes;
+ }
+
+ /** Forces an update of the authorized account types. */
+ public void forceUpdate() {
+ updateAuthorizedAccountTypes(true /* isForced */);
+ }
+
+ /** Starts listening for account updates. */
+ public void listenToAccountUpdates() {
+ mAuthenticatorHelper.listenToAccountUpdates();
+ }
+
+ /** Stops listening for account updates. */
+ public void stopListeningToAccountUpdates() {
+ mAuthenticatorHelper.stopListeningToAccountUpdates();
+ }
+
+ /**
+ * Gets the label associated with a particular account type. Returns {@code null} if none found.
+ *
+ * @param accountType the type of account
+ */
+ public CharSequence getLabelForType(String accountType) {
+ return mAuthenticatorHelper.getLabelForType(mContext, accountType);
+ }
+
+ /**
+ * Gets an icon associated with a particular account type. Returns a default icon if none found.
+ *
+ * @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.
+ */
+ public Drawable getDrawableForType(String accountType) {
+ return mAuthenticatorHelper.getDrawableForType(mContext, accountType);
+ }
+
+ /** Used for testing to trigger an account update. */
+ @VisibleForTesting
+ AuthenticatorHelper getAuthenticatorHelper() {
+ return mAuthenticatorHelper;
+ }
+
+ @VisibleForTesting
+ void setAuthenticatorHelper(AuthenticatorHelper helper) {
+ mAuthenticatorHelper = helper;
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/accounts/AddAccountActivity.java b/CarSettings/src/com/android/car/settings/accounts/AddAccountActivity.java
new file mode 100644
index 00000000..f497b31a
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/accounts/AddAccountActivity.java
@@ -0,0 +1,207 @@
+/*
+ * 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.car.settings.accounts;
+
+import static android.content.Intent.EXTRA_USER;
+
+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.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.widget.Toast;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.Logger;
+
+import java.io.IOException;
+
+/**
+ * Entry point Activity for account setup. Works as follows
+ *
+ *
+ * - After receiving an account type from ChooseAccountFragment, this Activity launches the
+ * account setup specified by AccountManager.
+ *
- After the account setup, this Activity finishes without showing anything.
+ *
+ */
+public class AddAccountActivity extends Activity {
+ /**
+ * A boolean to keep the state of whether add account has already been called.
+ */
+ private static final String KEY_ADD_CALLED = "AddAccountCalled";
+ /**
+ * Extra parameter to identify the caller. Applications may display a
+ * different UI if the call is made from Settings or from a specific
+ * application.
+ */
+ private static final String KEY_CALLER_IDENTITY = "pendingIntent";
+ private static final String SHOULD_NOT_RESOLVE = "SHOULDN'T RESOLVE!";
+
+ private static final Logger LOG = new Logger(AddAccountActivity.class);
+ private static final String ALLOW_SKIP = "allowSkip";
+
+ /* package */ static final String EXTRA_SELECTED_ACCOUNT = "selected_account";
+
+ // Show additional info regarding the use of a device with multiple users
+ static final String EXTRA_HAS_MULTIPLE_USERS = "hasMultipleUsers";
+
+ // Need a specific request code for add account activity.
+ private static final int ADD_ACCOUNT_REQUEST = 2001;
+
+ private UserManager mUserManager;
+ private UserHandle mUserHandle;
+ private PendingIntent mPendingIntent;
+ private boolean mAddAccountCalled;
+
+ private final AccountManagerCallback mCallback = new AccountManagerCallback() {
+ @Override
+ public void run(AccountManagerFuture future) {
+ if (!future.isDone()) {
+ LOG.v("Account manager future is not done.");
+ finish();
+ }
+ boolean done = true;
+ try {
+ Bundle result = future.getResult();
+ Intent intent = result.getParcelable(AccountManager.KEY_INTENT);
+ if (intent != null) {
+ done = false;
+ Bundle addAccountOptions = new Bundle();
+ addAccountOptions.putBoolean(EXTRA_HAS_MULTIPLE_USERS,
+ hasMultipleUsers(AddAccountActivity.this));
+ addAccountOptions.putParcelable(EXTRA_USER, mUserHandle);
+ intent.putExtras(addAccountOptions);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivityForResultAsUser(
+ new Intent(intent), ADD_ACCOUNT_REQUEST, mUserHandle);
+ } else {
+ setResult(RESULT_OK);
+ if (mPendingIntent != null) {
+ mPendingIntent.cancel();
+ mPendingIntent = null;
+ }
+ }
+ LOG.v("account added: " + result);
+ } catch (OperationCanceledException | IOException | AuthenticatorException e) {
+ LOG.v("addAccount error: " + e);
+ } finally {
+ if (done) {
+ finish();
+ }
+ }
+ }
+ };
+
+ /**
+ * Creates an intent to start the {@link AddAccountActivity} to add an account of the given
+ * account type.
+ */
+ public static Intent createAddAccountActivityIntent(Context context, String accountType) {
+ Intent intent = new Intent(context, AddAccountActivity.class);
+ intent.putExtra(EXTRA_SELECTED_ACCOUNT, accountType);
+ return intent;
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putBoolean(KEY_ADD_CALLED, mAddAccountCalled);
+ LOG.v("saved");
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (savedInstanceState != null) {
+ mAddAccountCalled = savedInstanceState.getBoolean(KEY_ADD_CALLED);
+ LOG.v("Restored from previous add account call: " + mAddAccountCalled);
+ }
+
+ mUserManager = UserManager.get(getApplicationContext());
+ mUserHandle = UserHandle.of(UserHandle.myUserId());
+
+ if (mUserManager.hasUserRestriction(UserManager.DISALLOW_MODIFY_ACCOUNTS)) {
+ // We aren't allowed to add an account.
+ Toast.makeText(
+ this, R.string.user_cannot_add_accounts_message, Toast.LENGTH_LONG)
+ .show();
+ finish();
+ return;
+ }
+
+ if (mAddAccountCalled) {
+ // We already called add account - maybe the callback was lost.
+ finish();
+ return;
+ }
+ addAccount(getIntent().getStringExtra(EXTRA_SELECTED_ACCOUNT));
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ setResult(resultCode);
+ if (mPendingIntent != null) {
+ mPendingIntent.cancel();
+ mPendingIntent = null;
+ }
+ finish();
+ }
+
+ private void addAccount(String accountType) {
+ Bundle addAccountOptions = new Bundle();
+ addAccountOptions.putBoolean(EXTRA_HAS_MULTIPLE_USERS, hasMultipleUsers(this));
+ addAccountOptions.putBoolean(ALLOW_SKIP, true);
+
+ /*
+ * The identityIntent is for the purposes of establishing the identity
+ * of the caller and isn't intended for launching activities, services
+ * or broadcasts.
+ */
+ Intent identityIntent = new Intent();
+ identityIntent.setComponent(new ComponentName(SHOULD_NOT_RESOLVE, SHOULD_NOT_RESOLVE));
+ identityIntent.setAction(SHOULD_NOT_RESOLVE);
+ identityIntent.addCategory(SHOULD_NOT_RESOLVE);
+
+ mPendingIntent =
+ PendingIntent.getBroadcast(this, 0, identityIntent, PendingIntent.FLAG_IMMUTABLE);
+ addAccountOptions.putParcelable(KEY_CALLER_IDENTITY, mPendingIntent);
+
+ AccountManager.get(this).addAccountAsUser(
+ accountType,
+ /* authTokenType= */ null,
+ /* requiredFeatures= */ null,
+ addAccountOptions,
+ null,
+ mCallback,
+ /* handler= */ null,
+ mUserHandle);
+ mAddAccountCalled = true;
+ }
+
+ private boolean hasMultipleUsers(Context context) {
+ return ((UserManager) context.getSystemService(Context.USER_SERVICE))
+ .getUsers().size() > 1;
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/accounts/AddAccountPreferenceController.java b/CarSettings/src/com/android/car/settings/accounts/AddAccountPreferenceController.java
new file mode 100644
index 00000000..1952cd75
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/accounts/AddAccountPreferenceController.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.settings.accounts;
+
+import static android.app.Activity.RESULT_OK;
+import static android.os.UserManager.DISALLOW_MODIFY_ACCOUNTS;
+
+import static com.android.car.settings.enterprise.EnterpriseUtils.hasUserRestrictionByUm;
+import static com.android.car.settings.enterprise.EnterpriseUtils.isDeviceManaged;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.Preference;
+
+import com.android.car.settings.admin.NewUserDisclaimerActivity;
+import com.android.car.settings.common.ActivityResultCallback;
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.Logger;
+import com.android.car.settings.common.PreferenceController;
+import com.android.car.settings.profiles.ProfileHelper;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Business Logic for preference starts the add account flow.
+ */
+public class AddAccountPreferenceController extends PreferenceController
+ implements ActivityResultCallback {
+ public static final int NEW_USER_DISCLAIMER_REQUEST = 1;
+ private static final Logger LOG = new Logger(AddAccountPreferenceController.class);
+
+ private String[] mAuthorities;
+ private String[] mAccountTypes;
+
+ public AddAccountPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController,
+ CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ }
+
+ /** Sets the account authorities that are available. */
+ public AddAccountPreferenceController setAuthorities(String[] authorities) {
+ mAuthorities = authorities;
+ return this;
+ }
+
+ /** Sets the account authorities that are available. */
+ public AddAccountPreferenceController setAccountTypes(String[] accountTypes) {
+ mAccountTypes = accountTypes;
+ return this;
+ }
+
+ @Override
+ protected Class getPreferenceType() {
+ return Preference.class;
+ }
+
+ @Override
+ protected void onCreateInternal() {
+ super.onCreateInternal();
+ setClickableWhileDisabled(getPreference(), /* clickable= */ true, p -> {
+ if (!getProfileHelper().isNewUserDisclaimerAcknolwedged(getContext())) {
+ LOG.d("isNewUserDisclaimerAcknolwedged returns false, start activity");
+ startNewUserDisclaimerActivityForResult();
+ return;
+ }
+ getProfileHelper()
+ .runClickableWhileDisabled(getContext(), getFragmentController());
+ });
+ }
+
+ @Override
+ protected boolean handlePreferenceClicked(Preference preference) {
+ AccountTypesHelper helper = getAccountTypesHelper();
+
+ if (mAuthorities != null) {
+ helper.setAuthorities(Arrays.asList(mAuthorities));
+ }
+ if (mAccountTypes != null) {
+ helper.setAccountTypesFilter(
+ new HashSet<>(Arrays.asList(mAccountTypes)));
+ }
+ launchAddAccount();
+ return true;
+ }
+
+ @Override
+ public void processActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+ if (requestCode == NEW_USER_DISCLAIMER_REQUEST && resultCode == RESULT_OK) {
+ LOG.d("New user disclaimer acknowledged, launching add account.");
+ launchAddAccount();
+ } else {
+ LOG.e("New user disclaimer failed with result " + resultCode);
+ }
+ }
+
+ private void launchAddAccount() {
+ Set authorizedAccountTypes = getAccountTypesHelper().getAuthorizedAccountTypes();
+
+ // Skip the choose account screen if there is only one account type and the device is not
+ // managed by a device owner.
+ // TODO (b/213894991) add check for profile owner when work profile is supported
+ if (authorizedAccountTypes.size() == 1 && !isDeviceManaged(getContext())) {
+ String accountType = authorizedAccountTypes.iterator().next();
+ getContext().startActivity(
+ AddAccountActivity.createAddAccountActivityIntent(getContext(), accountType));
+ } else {
+ getFragmentController().launchFragment(new ChooseAccountFragment());
+ }
+ }
+
+ @Override
+ protected int getDefaultAvailabilityStatus() {
+ if (isDeviceManaged(getContext())
+ && !getProfileHelper().isNewUserDisclaimerAcknolwedged(getContext())) {
+ return AVAILABLE_FOR_VIEWING;
+ }
+ if (getProfileHelper().canCurrentProcessModifyAccounts()) {
+ return AVAILABLE;
+ }
+ if (getProfileHelper().isDemoOrGuest()
+ || hasUserRestrictionByUm(getContext(), DISALLOW_MODIFY_ACCOUNTS)) {
+ return DISABLED_FOR_PROFILE;
+ }
+ return AVAILABLE_FOR_VIEWING;
+ }
+
+ private void startNewUserDisclaimerActivityForResult() {
+ getFragmentController().startActivityForResult(
+ new Intent(getContext(), NewUserDisclaimerActivity.class),
+ NEW_USER_DISCLAIMER_REQUEST, /* callback= */ this);
+ }
+
+ @VisibleForTesting
+ ProfileHelper getProfileHelper() {
+ return ProfileHelper.getInstance(getContext());
+ }
+
+ @VisibleForTesting
+ AccountTypesHelper getAccountTypesHelper() {
+ return new AccountTypesHelper(getContext());
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/accounts/ChooseAccountFragment.java b/CarSettings/src/com/android/car/settings/accounts/ChooseAccountFragment.java
new file mode 100644
index 00000000..b149fe4c
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/accounts/ChooseAccountFragment.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.car.settings.accounts;
+
+import android.content.Context;
+import android.content.Intent;
+import android.provider.Settings;
+
+import androidx.annotation.XmlRes;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.SettingsFragment;
+
+import java.util.Arrays;
+import java.util.HashSet;
+
+/**
+ * Lists accounts the user can add.
+ */
+public class ChooseAccountFragment extends SettingsFragment {
+ @Override
+ @XmlRes
+ protected int getPreferenceScreenResId() {
+ return R.xml.choose_account_fragment;
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ Intent intent = requireActivity().getIntent();
+ ChooseAccountPreferenceController preferenceController = use(
+ ChooseAccountPreferenceController.class, R.string.pk_add_account);
+
+ String[] authorities = intent.getStringArrayExtra(Settings.EXTRA_AUTHORITIES);
+ if (authorities != null) {
+ preferenceController.setAuthorities(Arrays.asList(authorities));
+ }
+
+ String[] accountTypesForFilter = intent.getStringArrayExtra(Settings.EXTRA_ACCOUNT_TYPES);
+ if (accountTypesForFilter != null) {
+ preferenceController.setAccountTypesFilter(
+ new HashSet<>(Arrays.asList(accountTypesForFilter)));
+ }
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/accounts/ChooseAccountPreferenceController.java b/CarSettings/src/com/android/car/settings/accounts/ChooseAccountPreferenceController.java
new file mode 100644
index 00000000..dc9f405a
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/accounts/ChooseAccountPreferenceController.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.settings.accounts;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.collection.ArrayMap;
+import androidx.preference.PreferenceGroup;
+
+import com.android.car.settings.common.ActivityResultCallback;
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.PreferenceController;
+import com.android.car.ui.preference.CarUiPreference;
+import com.android.settingslib.accounts.AuthenticatorHelper;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Controller for showing the user the list of accounts they can add.
+ *
+ * Largely derived from {@link com.android.settings.accounts.ChooseAccountActivity}
+ */
+public class ChooseAccountPreferenceController extends
+ PreferenceController implements ActivityResultCallback {
+ @VisibleForTesting
+ static final int ADD_ACCOUNT_REQUEST_CODE = 100;
+
+ private AccountTypesHelper mAccountTypesHelper;
+ private ArrayMap mPreferences = new ArrayMap<>();
+ private boolean mHasPendingBack = false;
+
+ public ChooseAccountPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ mAccountTypesHelper = new AccountTypesHelper(context);
+ mAccountTypesHelper.setOnChangeListener(this::forceUpdateAccountsCategory);
+ }
+
+ /** Sets the authorities that the user has. */
+ public void setAuthorities(List authorities) {
+ mAccountTypesHelper.setAuthorities(authorities);
+ }
+
+ /** Sets the filter for accounts that should be shown. */
+ public void setAccountTypesFilter(Set accountTypesFilter) {
+ mAccountTypesHelper.setAccountTypesFilter(accountTypesFilter);
+ }
+
+ /** Sets the filter for accounts that should NOT be shown. */
+ protected void setAccountTypesExclusionFilter(Set accountTypesExclusionFilterFilter) {
+ mAccountTypesHelper.setAccountTypesExclusionFilter(accountTypesExclusionFilterFilter);
+ }
+
+ @Override
+ protected Class getPreferenceType() {
+ return PreferenceGroup.class;
+ }
+
+ @Override
+ protected void updateState(PreferenceGroup preferenceGroup) {
+ mAccountTypesHelper.forceUpdate();
+ }
+
+ /**
+ * Registers the account update callback.
+ */
+ @Override
+ protected void onStartInternal() {
+ mAccountTypesHelper.listenToAccountUpdates();
+
+ if (mHasPendingBack) {
+ mHasPendingBack = false;
+
+ // Post the fragment navigation because FragmentManager may still be executing
+ // transactions during onStart.
+ new Handler().post(() -> getFragmentController().goBack());
+ }
+ }
+
+ /**
+ * Unregisters the account update callback.
+ */
+ @Override
+ protected void onStopInternal() {
+ mAccountTypesHelper.stopListeningToAccountUpdates();
+ }
+
+ /** Forces a refresh of the authenticator description preferences. */
+ private void forceUpdateAccountsCategory() {
+ Set preferencesToRemove = new HashSet<>(mPreferences.keySet());
+ List preferences =
+ getAuthenticatorDescriptionPreferences(preferencesToRemove);
+ // Add all preferences that aren't already shown
+ for (int i = 0; i < preferences.size(); i++) {
+ AuthenticatorDescriptionPreference preference = preferences.get(i);
+ preference.setOrder(i);
+ String key = preference.getKey();
+ getPreference().addPreference(preference);
+ mPreferences.put(key, preference);
+ }
+
+ // Remove all preferences that should no longer be shown
+ for (String key : preferencesToRemove) {
+ getPreference().removePreference(mPreferences.get(key));
+ mPreferences.remove(key);
+ }
+ }
+
+ /**
+ * Returns a list of preferences corresponding to the account types the user can add.
+ *
+ * Derived from
+ * {@link com.android.settings.accounts.ChooseAccountActivity#onAuthDescriptionsUpdated}
+ *
+ * @param preferencesToRemove the current preferences shown; will contain the preferences that
+ * need to be removed from the screen after method execution
+ */
+ private List getAuthenticatorDescriptionPreferences(
+ Set preferencesToRemove) {
+ ArrayList authenticatorDescriptionPreferences =
+ new ArrayList<>();
+ Set authorizedAccountTypes = mAccountTypesHelper.getAuthorizedAccountTypes();
+ // Create list of account providers to show on page.
+ for (String accountType : authorizedAccountTypes) {
+ CharSequence label = mAccountTypesHelper.getLabelForType(accountType);
+ Drawable icon = mAccountTypesHelper.getDrawableForType(accountType);
+
+ // Add a preference for the provider to the list and remove it from preferencesToRemove.
+ AuthenticatorDescriptionPreference preference = mPreferences.getOrDefault(accountType,
+ new AuthenticatorDescriptionPreference(getContext(), accountType, label, icon));
+ preference.setOnPreferenceClickListener(
+ pref -> {
+ Intent intent = AddAccountActivity.createAddAccountActivityIntent(
+ getContext(), preference.getAccountType());
+ getFragmentController().startActivityForResult(intent,
+ ADD_ACCOUNT_REQUEST_CODE, /* callback= */ this);
+ return true;
+ });
+ authenticatorDescriptionPreferences.add(preference);
+ preferencesToRemove.remove(accountType);
+ }
+
+ Collections.sort(authenticatorDescriptionPreferences);
+
+ return authenticatorDescriptionPreferences;
+ }
+
+ /** Used for testing to trigger an account update. */
+ @VisibleForTesting
+ AccountTypesHelper getAccountTypesHelper() {
+ return mAccountTypesHelper;
+ }
+
+ @VisibleForTesting
+ void setAuthenticatorHelper(AuthenticatorHelper helper) {
+ mAccountTypesHelper.setAuthenticatorHelper(helper);
+ }
+
+ @Override
+ public void processActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+ if (requestCode == ADD_ACCOUNT_REQUEST_CODE) {
+ if (isStarted()) {
+ getFragmentController().goBack();
+ } else {
+ mHasPendingBack = true;
+ }
+ }
+ }
+
+ /** Handles adding accounts. */
+ interface AddAccountListener {
+ /** Handles adding an account. */
+ void addAccount(String accountType);
+ }
+
+ private static class AuthenticatorDescriptionPreference extends CarUiPreference {
+ private final String mType;
+
+ AuthenticatorDescriptionPreference(Context context, String accountType, CharSequence label,
+ Drawable icon) {
+ super(context);
+ mType = accountType;
+
+ setKey(accountType);
+ setTitle(label);
+ setIcon(icon);
+ setShowChevron(false);
+ }
+
+ private String getAccountType() {
+ return mType;
+ }
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/accounts/SyncPreference.java b/CarSettings/src/com/android/car/settings/accounts/SyncPreference.java
new file mode 100644
index 00000000..047c8e53
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/accounts/SyncPreference.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.car.settings.accounts;
+
+import android.content.Context;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.preference.PreferenceViewHolder;
+
+import com.android.car.settings.R;
+import com.android.car.ui.preference.CarUiSwitchPreference;
+
+/**
+ * A preference that represents the state of a sync adapter.
+ *
+ * Largely derived from {@link com.android.settings.accounts.SyncStateSwitchPreference}.
+ */
+public class SyncPreference extends CarUiSwitchPreference {
+ private int mUid;
+ private String mPackageName;
+ private AccountSyncHelper.SyncState mSyncState = AccountSyncHelper.SyncState.NONE;
+ private boolean mOneTimeSyncMode = false;
+
+ public SyncPreference(Context context, String authority) {
+ super(context);
+ setKey(authority);
+ setPersistent(false);
+ updateIcon();
+ }
+
+ @Override
+ public void onBindViewHolder(PreferenceViewHolder view) {
+ super.onBindViewHolder(view);
+
+ View switchView = view.findViewById(com.android.internal.R.id.switch_widget);
+ if (mOneTimeSyncMode) {
+ switchView.setVisibility(View.GONE);
+
+ /*
+ * Override the summary. Fill in the %1$s with the existing summary
+ * (what ends up happening is the old summary is shown on the next
+ * line).
+ */
+ TextView summary = (TextView) view.findViewById(android.R.id.summary);
+ summary.setText(getContext().getString(R.string.sync_one_time_sync, getSummary()));
+ } else {
+ switchView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ /** Updates the preference icon based on the current syncing state. */
+ private void updateIcon() {
+ switch (mSyncState) {
+ case ACTIVE:
+ setIcon(R.drawable.ic_sync_anim);
+ getIcon().setTintList(getContext().getColorStateList(R.color.icon_color_default));
+ break;
+ case PENDING:
+ setIcon(R.drawable.ic_sync);
+ getIcon().setTintList(getContext().getColorStateList(R.color.icon_color_default));
+ break;
+ case FAILED:
+ setIcon(R.drawable.ic_sync_problem);
+ getIcon().setTintList(getContext().getColorStateList(R.color.icon_color_default));
+ break;
+ default:
+ setIcon(null);
+ setIconSpaceReserved(true);
+ break;
+ }
+ }
+
+ /** Sets the sync state for this preference. */
+ public void setSyncState(AccountSyncHelper.SyncState state) {
+ mSyncState = state;
+ // Force a manual update of the icon since the sync state affects what is shown.
+ updateIcon();
+ }
+
+ /**
+ * Returns whether or not the sync adapter is in one-time sync mode.
+ *
+ *
One-time sync mode means that the sync adapter is not being automatically synced but
+ * can be manually synced (i.e. a one time sync).
+ */
+ public boolean isOneTimeSyncMode() {
+ return mOneTimeSyncMode;
+ }
+
+ /** Sets whether one-time sync mode is on for this preference. */
+ public void setOneTimeSyncMode(boolean oneTimeSyncMode) {
+ mOneTimeSyncMode = oneTimeSyncMode;
+ // Force a refresh so that onBindViewHolder is called
+ notifyChanged();
+ }
+
+ /**
+ * Returns the uid corresponding to the sync adapter's package.
+ *
+ *
This can be used to create an intent to request account access via
+ * {@link android.accounts.AccountManager#createRequestAccountAccessIntentSenderAsUser}.
+ */
+ public int getUid() {
+ return mUid;
+ }
+
+ /** Sets the uid for this preference. */
+ public void setUid(int uid) {
+ mUid = uid;
+ }
+
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ public void setPackageName(String packageName) {
+ mPackageName = packageName;
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/admin/FactoryResetActivity.java b/CarSettings/src/com/android/car/settings/admin/FactoryResetActivity.java
new file mode 100644
index 00000000..86ad7413
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/admin/FactoryResetActivity.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.settings.admin;
+
+import static android.car.drivingstate.CarDrivingStateEvent.DRIVING_STATE_PARKED;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.car.Car;
+import android.car.ICarResultReceiver;
+import android.car.drivingstate.CarDrivingStateEvent;
+import android.car.drivingstate.CarDrivingStateManager;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.widget.Toast;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.Logger;
+
+
+/**
+ * Activity shown when a factory request is imminent, it gives the user the option to reset now or
+ * wait until the device is rebooted / resumed from suspend.
+ */
+public final class FactoryResetActivity extends Activity {
+ private static final String EXTRA_FACTORY_RESET_CALLBACK = "factory_reset_callback";
+ private static final int FACTORY_RESET_NOTIFICATION_ID = 42;
+ private static final Logger LOG = new Logger(FactoryResetActivity.class);
+ private ICarResultReceiver mCallback;
+ private Car mCar;
+ private CarDrivingStateManager mCarDrivingStateManager;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Intent intent = getIntent();
+ Object binder = null;
+
+ try {
+ binder = intent.getExtra(EXTRA_FACTORY_RESET_CALLBACK);
+ mCallback = ICarResultReceiver.Stub.asInterface((IBinder) binder);
+ } catch (Exception e) {
+ LOG.w("error getting IResultReveiver from " + EXTRA_FACTORY_RESET_CALLBACK
+ + " extra (" + binder + ") on " + intent, e);
+ }
+
+ if (mCallback == null) {
+ LOG.wtf("no ICarResultReceiver / " + EXTRA_FACTORY_RESET_CALLBACK
+ + " extra on " + intent);
+ finish();
+ return;
+ }
+
+ // Connect to car service
+ mCar = Car.createCar(this);
+ mCarDrivingStateManager = (CarDrivingStateManager) mCar.getCarManager(
+ Car.CAR_DRIVING_STATE_SERVICE);
+ showMore();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ finish();
+ }
+
+ private void showMore() {
+ CarDrivingStateEvent state = mCarDrivingStateManager.getCurrentCarDrivingState();
+ switch (state.eventValue) {
+ case DRIVING_STATE_PARKED:
+ showFactoryResetDialog();
+ break;
+ default:
+ showFactoryResetToast();
+ }
+ }
+
+ private void showFactoryResetDialog() {
+ AlertDialog dialog = new AlertDialog.Builder(/* context= */ this,
+ com.android.internal.R.style.Theme_DeviceDefault_Dialog_Alert)
+ .setTitle(R.string.factory_reset_parked_title)
+ .setMessage(R.string.factory_reset_parked_text)
+ .setPositiveButton(R.string.factory_reset_later_button,
+ (d, which) -> factoryResetLater())
+ .setNegativeButton(R.string.factory_reset_now_button,
+ (d, which) -> factoryResetNow())
+ .setCancelable(false)
+ .setOnDismissListener((d) -> finish())
+ .create();
+
+ dialog.show();
+ }
+
+ private void showFactoryResetToast() {
+ showToast(R.string.factory_reset_driving_text);
+ finish();
+ }
+
+ private void factoryResetNow() {
+ LOG.i("Factory reset acknowledged; finishing it");
+
+ try {
+ mCallback.send(/* resultCode= */ 0, /* resultData= */ null);
+
+ // Cancel pending intent and notification
+ getSystemService(NotificationManager.class).cancel(FACTORY_RESET_NOTIFICATION_ID);
+ PendingIntent.getActivity(this, FACTORY_RESET_NOTIFICATION_ID, getIntent(),
+ PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT).cancel();
+ } catch (Exception e) {
+ LOG.e("error factory resetting or cancelling notification / intent", e);
+ return;
+ } finally {
+ finish();
+ }
+ }
+
+ private void factoryResetLater() {
+ LOG.i("Delaying factory reset.");
+ showToast(R.string.factory_reset_later_text);
+ finish();
+ }
+
+ private void showToast(int resId) {
+ Toast.makeText(this, resId, Toast.LENGTH_LONG).show();
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/admin/NewUserDisclaimerActivity.java b/CarSettings/src/com/android/car/settings/admin/NewUserDisclaimerActivity.java
new file mode 100644
index 00000000..e14a820e
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/admin/NewUserDisclaimerActivity.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.settings.admin;
+
+import android.car.Car;
+import android.car.admin.CarDevicePolicyManager;
+import android.content.Intent;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.widget.Button;
+
+import androidx.fragment.app.FragmentActivity;
+
+import com.android.car.admin.ui.ManagedDeviceTextView;
+import com.android.car.settings.R;
+import com.android.car.settings.common.ConfirmationDialogFragment;
+import com.android.car.settings.common.Logger;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.settingslib.core.lifecycle.HideNonSystemOverlayMixin;
+
+/**
+ * Shows a disclaimer dialog when a new user is added in a device that is managed by a device owner.
+ *
+ *
The dialog text will contain the message from
+ * {@code ManagedDeviceTextView.getManagedDeviceText}.
+ *
+ *
The dialog contains two buttons: one to acknowlege the disclaimer; the other to launch
+ * {@code Settings.ACTION_ENTERPRISE_PRIVACY_SETTINGS} for more details. Note: when
+ * {@code Settings.ACTION_ENTERPRISE_PRIVACY_SETTINGS} is closed, the same dialog will be shown.
+ *
+ *
Clicking anywhere outside the dialog will dimiss the dialog.
+ */
+public final class NewUserDisclaimerActivity extends FragmentActivity {
+ @VisibleForTesting
+ static final Logger LOG = new Logger(NewUserDisclaimerActivity.class);
+ @VisibleForTesting
+ static final String DIALOG_TAG = "NewUserDisclaimerActivity.ConfirmationDialogFragment";
+ private static final int LEARN_MORE_RESULT_CODE = 1;
+
+ private Car mCar;
+ private CarDevicePolicyManager mCarDevicePolicyManager;
+ private Button mAcceptButton;
+ private ConfirmationDialogFragment mConfirmationDialog;
+ private boolean mLearnMoreLaunched;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getLifecycle().addObserver(new HideNonSystemOverlayMixin(this));
+ getCarDevicePolicyManager();
+ setupConfirmationDialog();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ showConfirmationDialog();
+ getCarDevicePolicyManager().setUserDisclaimerShown(getUser());
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode != LEARN_MORE_RESULT_CODE) {
+ LOG.w("onActivityResult(), invalid request code: " + requestCode);
+ return;
+ }
+ mLearnMoreLaunched = false;
+ }
+
+ private ConfirmationDialogFragment setupConfirmationDialog() {
+ String managedByOrganizationText = ManagedDeviceTextView.getManagedDeviceText(this)
+ .toString();
+ String managedProfileText = getResources().getString(
+ R.string.new_user_managed_device_text);
+
+ mConfirmationDialog = new ConfirmationDialogFragment.Builder(getApplicationContext())
+ .setTitle(R.string.new_user_managed_device_title)
+ .setMessage(managedByOrganizationText + System.lineSeparator()
+ + System.lineSeparator() + managedProfileText)
+ .setPositiveButton(R.string.new_user_managed_device_acceptance,
+ arguments -> onAccept())
+ .setNeutralButton(R.string.new_user_managed_device_learn_more,
+ arguments -> onLearnMoreClicked())
+ .setDismissListener((arguments, positiveResult) -> onDialogDimissed())
+ .build();
+
+ return mConfirmationDialog;
+ }
+
+ private void showConfirmationDialog() {
+ if (mConfirmationDialog == null) {
+ setupConfirmationDialog();
+ }
+ mConfirmationDialog.show(getSupportFragmentManager(), DIALOG_TAG);
+ }
+
+ private void onAccept() {
+ LOG.d("user accepted");
+ getCarDevicePolicyManager().setUserDisclaimerAcknowledged(getUser());
+ setResult(RESULT_OK);
+ finish();
+ }
+
+ private void onLearnMoreClicked() {
+ mLearnMoreLaunched = true;
+ startActivityForResult(new Intent(Settings.ACTION_ENTERPRISE_PRIVACY_SETTINGS),
+ LEARN_MORE_RESULT_CODE);
+ }
+
+ private void onDialogDimissed() {
+ if (mLearnMoreLaunched) {
+ return;
+ }
+ finish();
+ }
+
+ private CarDevicePolicyManager getCarDevicePolicyManager() {
+ LOG.d("getCarDevicePolicyManager for user: " + getUser());
+ if (mCarDevicePolicyManager != null) {
+ return mCarDevicePolicyManager;
+ }
+ if (mCar == null) {
+ mCar = Car.createCar(this);
+ }
+ mCarDevicePolicyManager = (CarDevicePolicyManager) mCar.getCarManager(
+ Car.CAR_DEVICE_POLICY_SERVICE);
+ return mCarDevicePolicyManager;
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/AllAppsPreferenceController.java b/CarSettings/src/com/android/car/settings/applications/AllAppsPreferenceController.java
new file mode 100644
index 00000000..58430477
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/AllAppsPreferenceController.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.settings.applications;
+
+import android.app.usage.UsageStats;
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+
+import androidx.preference.Preference;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.PreferenceController;
+
+import java.util.List;
+
+/**
+ * Controller that shows when there is no recently used apps.
+ * Sets availability based on if there are recent apps and sets summary depending on number of
+ * non-system apps.
+ */
+public class AllAppsPreferenceController extends PreferenceController implements
+ RecentAppsItemManager.RecentAppStatsListener,
+ InstalledAppCountItemManager.InstalledAppCountListener {
+
+ // In most cases, device has recently opened apps. So, assume true by default.
+ private boolean mAreThereRecentlyUsedApps = true;
+
+ public AllAppsPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ }
+
+ @Override
+ protected Class getPreferenceType() {
+ return Preference.class;
+ }
+
+ @Override
+ public int getDefaultAvailabilityStatus() {
+ return mAreThereRecentlyUsedApps ? CONDITIONALLY_UNAVAILABLE : AVAILABLE;
+ }
+
+ @Override
+ public void onRecentAppStatsLoaded(List recentAppStats) {
+ mAreThereRecentlyUsedApps = !recentAppStats.isEmpty();
+ refreshUi();
+ }
+
+ @Override
+ public void onInstalledAppCountLoaded(int appCount) {
+ getPreference().setSummary(getContext().getResources().getString(
+ R.string.apps_view_all_apps_title, appCount));
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/AppListFragment.java b/CarSettings/src/com/android/car/settings/applications/AppListFragment.java
new file mode 100644
index 00000000..3bd137ad
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/AppListFragment.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 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.car.settings.applications;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+
+import com.android.car.settings.common.SettingsFragment;
+
+/**
+ * Fragment base class for use in cases where a list of applications is displayed. Includes a
+ * a shared preference instance that can be used to show/hide system apps in the list.
+ */
+public abstract class AppListFragment extends SettingsFragment {
+
+ protected static final String SHARED_PREFERENCE_PATH =
+ "com.android.car.settings.applications.AppListFragment";
+ protected static final String KEY_HIDE_SYSTEM =
+ "com.android.car.settings.applications.HIDE_SYSTEM";
+
+ private boolean mHideSystem = true;
+
+ private SharedPreferences mSharedPreferences;
+ private SharedPreferences.OnSharedPreferenceChangeListener mSharedPreferenceChangeListener =
+ (sharedPreferences, key) -> {
+ if (KEY_HIDE_SYSTEM.equals(key)) {
+ mHideSystem = sharedPreferences.getBoolean(KEY_HIDE_SYSTEM,
+ /* defaultValue= */ true);
+ onToggleShowSystemApps(!mHideSystem);
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mSharedPreferences =
+ getContext().getSharedPreferences(SHARED_PREFERENCE_PATH, Context.MODE_PRIVATE);
+ if (savedInstanceState != null) {
+ mHideSystem = savedInstanceState.getBoolean(KEY_HIDE_SYSTEM,
+ /* defaultValue= */ true);
+ mSharedPreferences.edit().putBoolean(KEY_HIDE_SYSTEM, mHideSystem).apply();
+ } else {
+ mSharedPreferences.edit().putBoolean(KEY_HIDE_SYSTEM, true).apply();
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ onToggleShowSystemApps(!mHideSystem);
+ mSharedPreferences.registerOnSharedPreferenceChangeListener(
+ mSharedPreferenceChangeListener);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ mSharedPreferences.unregisterOnSharedPreferenceChangeListener(
+ mSharedPreferenceChangeListener);
+ }
+
+ /** Called when a user toggles the option to show system applications in the list. */
+ protected abstract void onToggleShowSystemApps(boolean showSystem);
+
+ /** Returns {@code true} if system applications should be shown in the list. */
+ protected final boolean shouldShowSystemApps() {
+ return !mHideSystem;
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putBoolean(KEY_HIDE_SYSTEM, mHideSystem);
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/ApplicationActionButtonsPreferenceController.java b/CarSettings/src/com/android/car/settings/applications/ApplicationActionButtonsPreferenceController.java
new file mode 100644
index 00000000..fa26d8ab
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/ApplicationActionButtonsPreferenceController.java
@@ -0,0 +1,580 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.settings.applications;
+
+import static android.app.Activity.RESULT_FIRST_USER;
+import static android.app.Activity.RESULT_OK;
+
+import static com.android.car.settings.applications.ApplicationsUtils.isKeepEnabledPackage;
+import static com.android.car.settings.applications.ApplicationsUtils.isProfileOrDeviceOwner;
+import static com.android.car.settings.common.ActionButtonsPreference.ActionButtons;
+import static com.android.car.settings.enterprise.ActionDisabledByAdminDialogFragment.DISABLED_BY_ADMIN_CONFIRM_DIALOG_TAG;
+import static com.android.car.settings.enterprise.EnterpriseUtils.BLOCKED_UNINSTALL_APP;
+
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.admin.DevicePolicyManager;
+import android.car.drivingstate.CarUxRestrictions;
+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.UserHandle;
+import android.os.UserManager;
+import android.util.ArraySet;
+import android.view.View;
+import android.widget.Toast;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.ActionButtonInfo;
+import com.android.car.settings.common.ActionButtonsPreference;
+import com.android.car.settings.common.ActivityResultCallback;
+import com.android.car.settings.common.ConfirmationDialogFragment;
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.Logger;
+import com.android.car.settings.common.PreferenceController;
+import com.android.car.settings.enterprise.ActionDisabledByAdminDialogFragment;
+import com.android.car.settings.enterprise.DeviceAdminAddActivity;
+import com.android.car.settings.enterprise.EnterpriseUtils;
+import com.android.car.settings.profiles.ProfileHelper;
+import com.android.settingslib.Utils;
+import com.android.settingslib.applications.ApplicationsState;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Shows actions associated with an application, like uninstall and forceStop.
+ *
+ * To uninstall an app, it must not be:
+ *
+ * - a system bundled app
+ *
- system signed
+ *
- managed by an active admin from a device policy
+ *
- a device or profile owner
+ *
- the only home app
+ *
- the default home app
+ *
- for a user with the {@link UserManager#DISALLOW_APPS_CONTROL} restriction
+ *
- for a user with the {@link UserManager#DISALLOW_UNINSTALL_APPS} restriction
+ *
+ *
+ * For apps that cannot be uninstalled, a disable option is shown instead (or enable if the app
+ * is already disabled).
+ */
+public class ApplicationActionButtonsPreferenceController extends
+ PreferenceController implements ActivityResultCallback {
+ private static final Logger LOG = new Logger(
+ ApplicationActionButtonsPreferenceController.class);
+
+ private static final List FORCE_STOP_RESTRICTIONS =
+ Arrays.asList(UserManager.DISALLOW_APPS_CONTROL);
+ private static final List UNINSTALL_RESTRICTIONS =
+ Arrays.asList(UserManager.DISALLOW_UNINSTALL_APPS, UserManager.DISALLOW_APPS_CONTROL);
+ private static final List DISABLE_RESTRICTIONS =
+ Arrays.asList(UserManager.DISALLOW_APPS_CONTROL);
+
+ @VisibleForTesting
+ static final String DISABLE_CONFIRM_DIALOG_TAG =
+ "com.android.car.settings.applications.DisableConfirmDialog";
+ @VisibleForTesting
+ static final String FORCE_STOP_CONFIRM_DIALOG_TAG =
+ "com.android.car.settings.applications.ForceStopConfirmDialog";
+
+ @VisibleForTesting
+ static final int UNINSTALL_REQUEST_CODE = 10;
+
+ @VisibleForTesting
+ static final int UNINSTALL_DEVICE_ADMIN_REQUEST_CODE = 11;
+
+ private DevicePolicyManager mDpm;
+ private PackageManager mPm;
+ private UserManager mUserManager;
+ private ProfileHelper mProfileHelper;
+ private ApplicationsState.Session mSession;
+
+ private ApplicationsState.AppEntry mAppEntry;
+ private ApplicationsState mApplicationsState;
+ private String mPackageName;
+ private PackageInfo mPackageInfo;
+
+ private String mRestriction;
+
+ @VisibleForTesting
+ final ConfirmationDialogFragment.ConfirmListener mForceStopConfirmListener =
+ new ConfirmationDialogFragment.ConfirmListener() {
+ @Override
+ public void onConfirm(@Nullable Bundle arguments) {
+ LOG.d("Stopping package " + mPackageName);
+ getContext().getSystemService(ActivityManager.class)
+ .forceStopPackage(mPackageName);
+ int userId = UserHandle.getUserId(mAppEntry.info.uid);
+ mApplicationsState.invalidatePackage(mPackageName, userId);
+ Toast.makeText(getContext(), getContext().getResources()
+ .getString(R.string.force_stop_success_toast_text,
+ mAppEntry.info.loadLabel(mPm)), Toast.LENGTH_LONG).show();
+ }
+ };
+
+ private final View.OnClickListener mForceStopClickListener = i -> {
+ if (ignoreActionBecauseItsDisabledByAdmin(FORCE_STOP_RESTRICTIONS)) return;
+ ConfirmationDialogFragment dialogFragment =
+ new ConfirmationDialogFragment.Builder(getContext())
+ .setTitle(R.string.force_stop_dialog_title)
+ .setMessage(R.string.force_stop_dialog_text)
+ .setPositiveButton(android.R.string.ok,
+ mForceStopConfirmListener)
+ .setNegativeButton(android.R.string.cancel, /* rejectListener= */ null)
+ .build();
+ getFragmentController().showDialog(dialogFragment, FORCE_STOP_CONFIRM_DIALOG_TAG);
+ };
+
+ @VisibleForTesting
+ final BroadcastReceiver mCheckKillProcessesReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ boolean enabled = getResultCode() != Activity.RESULT_CANCELED;
+ LOG.d("Got broadcast response: Restart status for " + mPackageName + " " + enabled);
+ updateForceStopButtonInner(enabled);
+ }
+ };
+
+ @VisibleForTesting
+ final ConfirmationDialogFragment.ConfirmListener mDisableConfirmListener = i -> {
+ mPm.setApplicationEnabledSetting(mPackageName,
+ PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER, /* flags= */ 0);
+ updateUninstallButtonInner(false);
+ };
+
+ private final View.OnClickListener mDisableClickListener = i -> {
+ if (ignoreActionBecauseItsDisabledByAdmin(DISABLE_RESTRICTIONS)) return;
+ ConfirmationDialogFragment dialogFragment =
+ new ConfirmationDialogFragment.Builder(getContext())
+ .setMessage(getContext().getString(R.string.app_disable_dialog_text))
+ .setPositiveButton(R.string.app_disable_dialog_positive,
+ mDisableConfirmListener)
+ .setNegativeButton(android.R.string.cancel, /* rejectListener= */ null)
+ .build();
+ getFragmentController().showDialog(dialogFragment, DISABLE_CONFIRM_DIALOG_TAG);
+ };
+
+ private final View.OnClickListener mEnableClickListener = i -> {
+ if (ignoreActionBecauseItsDisabledByAdmin(DISABLE_RESTRICTIONS)) return;
+ mPm.setApplicationEnabledSetting(mPackageName,
+ PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, /* flags= */ 0);
+ updateUninstallButtonInner(true);
+ };
+
+ private final View.OnClickListener mUninstallClickListener = i -> {
+ if (ignoreActionBecauseItsDisabledByAdmin(UNINSTALL_RESTRICTIONS)) return;
+ Uri packageUri = Uri.parse("package:" + mPackageName);
+ if (mDpm.packageHasActiveAdmins(mPackageName)) {
+ // Show Device Admin app details screen to deactivate the device admin before it can
+ // be uninstalled.
+ Intent deviceAdminIntent = new Intent(getContext(), DeviceAdminAddActivity.class)
+ .putExtra(DeviceAdminAddActivity.EXTRA_DEVICE_ADMIN_PACKAGE_NAME, mPackageName);
+ getFragmentController().startActivityForResult(deviceAdminIntent,
+ UNINSTALL_DEVICE_ADMIN_REQUEST_CODE, /* callback= */ this);
+ } else {
+ Intent uninstallIntent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri);
+ uninstallIntent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
+ getFragmentController().startActivityForResult(uninstallIntent, UNINSTALL_REQUEST_CODE,
+ /* callback= */ this);
+ }
+ };
+
+ private final ApplicationsState.Callbacks mApplicationStateCallbacks =
+ new ApplicationsState.Callbacks() {
+ @Override
+ public void onRunningStateChanged(boolean running) {
+ }
+
+ @Override
+ public void onPackageListChanged() {
+ refreshUi();
+ }
+
+ @Override
+ public void onRebuildComplete(ArrayList apps) {
+ }
+
+ @Override
+ public void onPackageIconChanged() {
+ }
+
+ @Override
+ public void onPackageSizeChanged(String packageName) {
+ }
+
+ @Override
+ public void onAllSizesComputed() {
+ }
+
+ @Override
+ public void onLauncherInfoChanged() {
+ }
+
+ @Override
+ public void onLoadEntriesCompleted() {
+ }
+ };
+
+ public ApplicationActionButtonsPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ mDpm = context.getSystemService(DevicePolicyManager.class);
+ mPm = context.getPackageManager();
+ mUserManager = UserManager.get(context);
+ mProfileHelper = ProfileHelper.getInstance(context);
+ }
+
+ @Override
+ protected Class getPreferenceType() {
+ return ActionButtonsPreference.class;
+ }
+
+ /** Sets the {@link ApplicationsState.AppEntry} which is used to load the app name and icon. */
+ public ApplicationActionButtonsPreferenceController setAppEntry(
+ ApplicationsState.AppEntry appEntry) {
+ mAppEntry = appEntry;
+ return this;
+ }
+
+ /** Sets the {@link ApplicationsState} which is used to load the app name and icon. */
+ public ApplicationActionButtonsPreferenceController setAppState(
+ ApplicationsState applicationsState) {
+ mApplicationsState = applicationsState;
+ return this;
+ }
+
+ /**
+ * Set the packageName, which is used to perform actions on a particular package.
+ */
+ public ApplicationActionButtonsPreferenceController setPackageName(String packageName) {
+ mPackageName = packageName;
+ return this;
+ }
+
+ @Override
+ protected void checkInitialized() {
+ if (mAppEntry == null || mApplicationsState == null || mPackageName == null) {
+ throw new IllegalStateException(
+ "AppEntry, AppState, and PackageName should be set before calling this "
+ + "function");
+ }
+ }
+
+ @Override
+ protected void onCreateInternal() {
+ ConfirmationDialogFragment.resetListeners(
+ (ConfirmationDialogFragment) getFragmentController().findDialogByTag(
+ DISABLE_CONFIRM_DIALOG_TAG),
+ mDisableConfirmListener,
+ /* rejectListener= */ null,
+ /* neutralListener= */ null);
+ ConfirmationDialogFragment.resetListeners(
+ (ConfirmationDialogFragment) getFragmentController().findDialogByTag(
+ FORCE_STOP_CONFIRM_DIALOG_TAG),
+ mForceStopConfirmListener,
+ /* rejectListener= */ null,
+ /* neutralListener= */ null);
+ getPreference().getButton(ActionButtons.BUTTON2)
+ .setText(R.string.force_stop)
+ .setIcon(R.drawable.ic_warning)
+ .setOnClickListener(mForceStopClickListener)
+ .setEnabled(false);
+ mSession = mApplicationsState.newSession(mApplicationStateCallbacks);
+ }
+
+ @Override
+ protected void onStartInternal() {
+ mSession.onResume();
+ }
+
+ @Override
+ protected void onStopInternal() {
+ mSession.onPause();
+ }
+
+ @Override
+ protected void updateState(ActionButtonsPreference preference) {
+ refreshAppEntry();
+ if (mAppEntry == null) {
+ getFragmentController().goBack();
+ return;
+ }
+ updateForceStopButton();
+ updateUninstallButton();
+ }
+
+ private void refreshAppEntry() {
+ mAppEntry = mApplicationsState.getEntry(mPackageName, UserHandle.myUserId());
+ if (mAppEntry != null) {
+ try {
+ mPackageInfo = mPm.getPackageInfo(mPackageName,
+ PackageManager.MATCH_DISABLED_COMPONENTS | PackageManager.MATCH_ANY_USER
+ | PackageManager.GET_SIGNATURES | PackageManager.GET_PERMISSIONS);
+ } catch (PackageManager.NameNotFoundException e) {
+ LOG.e("Exception when retrieving package:" + mPackageName, e);
+ mPackageInfo = null;
+ }
+ } else {
+ mPackageInfo = null;
+ }
+ }
+
+ private void updateForceStopButton() {
+ if (mDpm.packageHasActiveAdmins(mPackageName)) {
+ updateForceStopButtonInner(/* enabled= */ false);
+ } else if ((mAppEntry.info.flags & ApplicationInfo.FLAG_STOPPED) == 0) {
+ // If the app isn't explicitly stopped, then always show the force stop button.
+ updateForceStopButtonInner(/* enabled= */ true);
+ } else {
+ Intent intent = new Intent(Intent.ACTION_QUERY_PACKAGE_RESTART,
+ Uri.fromParts("package", mPackageName, /* fragment= */ null));
+ intent.putExtra(Intent.EXTRA_PACKAGES, new String[]{mPackageName});
+ intent.putExtra(Intent.EXTRA_UID, mAppEntry.info.uid);
+ intent.putExtra(Intent.EXTRA_USER_HANDLE,
+ UserHandle.getUserId(mAppEntry.info.uid));
+ LOG.d("Sending broadcast to query restart status for " + mPackageName);
+ getContext().sendOrderedBroadcastAsUser(intent,
+ UserHandle.CURRENT,
+ android.Manifest.permission.HANDLE_QUERY_PACKAGE_RESTART,
+ mCheckKillProcessesReceiver,
+ /* scheduler= */ null,
+ Activity.RESULT_CANCELED,
+ /* initialData= */ null,
+ /* initialExtras= */ null);
+ }
+ }
+
+ private void updateForceStopButtonInner(boolean enabled) {
+ if (enabled) {
+ Boolean shouldDisable = shouldDisableButtonBecauseOfUserRestriction("Force Stop",
+ UserManager.DISALLOW_APPS_CONTROL);
+ if (shouldDisable != null) {
+ if (shouldDisable) {
+ enabled = false;
+ } else {
+ mRestriction = UserManager.DISALLOW_APPS_CONTROL;
+ }
+ }
+ }
+
+ getPreference().getButton(ActionButtons.BUTTON2).setEnabled(enabled);
+ }
+
+ private void updateUninstallButtonInner(boolean isAppEnabled) {
+ ActionButtonInfo uninstallButton = getPreference().getButton(ActionButtons.BUTTON1);
+ if (isBundledApp()) {
+ if (isAppEnabled) {
+ uninstallButton.setText(R.string.disable_text).setIcon(
+ R.drawable.ic_block).setOnClickListener(mDisableClickListener);
+ } else {
+ uninstallButton.setText(R.string.enable_text).setIcon(
+ R.drawable.ic_check_circle).setOnClickListener(mEnableClickListener);
+ }
+ } else {
+ uninstallButton.setText(R.string.uninstall_text).setIcon(
+ R.drawable.ic_delete).setOnClickListener(mUninstallClickListener);
+ }
+
+ uninstallButton.setEnabled(!shouldDisableUninstallButton());
+ }
+
+ private void updateUninstallButton() {
+ updateUninstallButtonInner(isAppEnabled());
+ }
+
+ private boolean shouldDisableUninstallButton() {
+ if (shouldDisableUninstallForHomeApp()) {
+ LOG.d("Uninstall disabled for home app");
+ return true;
+ }
+
+ if (isAppEnabled() && isKeepEnabledPackage(getContext(), mPackageName)) {
+ LOG.d("Disable button disabled for keep enabled package");
+ return true;
+ }
+
+ if (Utils.isSystemPackage(getContext().getResources(), mPm, mPackageInfo)) {
+ LOG.d("Uninstall disabled for system package");
+ return true;
+ }
+
+ // We don't allow uninstalling profile/device owner on any profile because if it's a system
+ // app, "uninstall" is actually "downgrade to the system version + disable", and
+ // "downgrade" will clear data on all profiles.
+ if (isProfileOrDeviceOwner(mPackageName, mDpm, mProfileHelper)) {
+ LOG.d("Uninstall disabled because package is profile or device owner");
+ return true;
+ }
+
+ if (mDpm.isUninstallInQueue(mPackageName)) {
+ LOG.d("Uninstall disabled because intent is already queued");
+ return true;
+ }
+
+ Boolean shouldDisable = shouldDisableButtonBecauseOfUserRestriction("Uninstall",
+ UserManager.DISALLOW_APPS_CONTROL);
+ if (shouldDisable != null) return shouldDisable;
+
+ shouldDisable = shouldDisableButtonBecauseOfUserRestriction("Uninstall",
+ UserManager.DISALLOW_UNINSTALL_APPS);
+ if (shouldDisable != null) return shouldDisable;
+
+ return false;
+ }
+
+ /**
+ * Checks whether a button should be disabled because the user has the given restriction
+ * (and whether the restriction was was set by a device admin).
+ *
+ * @param button action name (for logging purposes)
+ * @param restriction user restriction
+ *
+ * @return {@code null} if the user doesn't have the restriction, {@value Boolean#TRUE} if it
+ * should be disabled because of {@link UserManager} restrictions, or {@value Boolean#FALSE} if
+ * should not be disabled because of {@link DevicePolicyManager} restrictions (in which case
+ * {@link #mRestriction} is updated with the restriction).
+ */
+ @Nullable
+ private Boolean shouldDisableButtonBecauseOfUserRestriction(String button, String restriction) {
+ if (!mUserManager.hasUserRestriction(restriction)) return null;
+
+ UserHandle user = UserHandle.getUserHandleForUid(mAppEntry.info.uid);
+
+ if (mUserManager.hasBaseUserRestriction(restriction, user)) {
+ LOG.d(button + " disabled because " + user + " has " + restriction + " restriction");
+ return Boolean.TRUE;
+ }
+
+ LOG.d(button + " NOT disabled because " + user + " has " + restriction + " restriction but "
+ + "it was set by a device admin (it will show a dialog explaining that instead)");
+ mRestriction = restriction;
+ return Boolean.FALSE;
+ }
+
+ /**
+ * Returns {@code true} if the package is a Home app that should not be uninstalled. We don't
+ * risk downgrading bundled home apps because that can interfere with home-key resolution. We
+ * can't allow removal of the only home app, and we don't want to allow removal of an
+ * explicitly preferred home app. The user can go to Home settings and pick a different app,
+ * after which we'll permit removal of the now-not-default app.
+ */
+ private boolean shouldDisableUninstallForHomeApp() {
+ Set homePackages = new ArraySet<>();
+ // Get list of "home" apps and trace through any meta-data references.
+ List homeActivities = new ArrayList<>();
+ ComponentName currentDefaultHome = mPm.getHomeActivities(homeActivities);
+ for (int i = 0; i < homeActivities.size(); i++) {
+ ResolveInfo ri = homeActivities.get(i);
+ String activityPkg = ri.activityInfo.packageName;
+ homePackages.add(activityPkg);
+
+ // Also make sure to include anything proxying for the home app.
+ Bundle metadata = ri.activityInfo.metaData;
+ if (metadata != null) {
+ String metaPkg = metadata.getString(ActivityManager.META_HOME_ALTERNATE);
+ if (signaturesMatch(metaPkg, activityPkg)) {
+ homePackages.add(metaPkg);
+ }
+ }
+ }
+
+ if (homePackages.contains(mPackageName)) {
+ if (isBundledApp()) {
+ // Don't risk a downgrade.
+ return true;
+ } else if (currentDefaultHome == null) {
+ // No preferred default. Permit uninstall only when there is more than one
+ // candidate.
+ return (homePackages.size() == 1);
+ } else {
+ // Explicit default home app. Forbid uninstall of that one, but permit it for
+ // installed-but-inactive ones.
+ return mPackageName.equals(currentDefaultHome.getPackageName());
+ }
+ } else {
+ // Not a home app.
+ return false;
+ }
+ }
+
+ private boolean signaturesMatch(String pkg1, String pkg2) {
+ if (pkg1 != null && pkg2 != null) {
+ try {
+ int match = mPm.checkSignatures(pkg1, pkg2);
+ if (match >= PackageManager.SIGNATURE_MATCH) {
+ return true;
+ }
+ } catch (Exception e) {
+ // e.g. package not found during lookup. Possibly bad input.
+ // Just return false as this isn't a reason to crash given the use case.
+ }
+ }
+ return false;
+ }
+
+ private boolean isBundledApp() {
+ return (mAppEntry.info.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
+ }
+
+ private boolean isAppEnabled() {
+ return mAppEntry.info.enabled && !(mAppEntry.info.enabledSetting
+ == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED);
+ }
+
+ @Override
+ public void processActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+ if (requestCode == UNINSTALL_REQUEST_CODE
+ || requestCode == UNINSTALL_DEVICE_ADMIN_REQUEST_CODE) {
+ if (resultCode == RESULT_OK) {
+ getFragmentController().goBack();
+ } else if (resultCode == RESULT_FIRST_USER) {
+ showUninstallBlockedByAdminDialog();
+ LOG.e("Uninstall failed");
+ }
+ }
+ }
+
+ private void showUninstallBlockedByAdminDialog() {
+ getFragmentController().showDialog(
+ EnterpriseUtils.getActionDisabledByAdminDialog(getContext(),
+ BLOCKED_UNINSTALL_APP, mPackageName),
+ DISABLED_BY_ADMIN_CONFIRM_DIALOG_TAG);
+ }
+
+ private boolean ignoreActionBecauseItsDisabledByAdmin(List restrictions) {
+ if (mRestriction == null || !restrictions.contains(mRestriction)) return false;
+
+ LOG.d("Ignoring action because of " + mRestriction);
+ getFragmentController().showDialog(ActionDisabledByAdminDialogFragment.newInstance(
+ mRestriction, UserHandle.myUserId()), DISABLED_BY_ADMIN_CONFIRM_DIALOG_TAG);
+ return true;
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/ApplicationDetailsFragment.java b/CarSettings/src/com/android/car/settings/applications/ApplicationDetailsFragment.java
new file mode 100644
index 00000000..f3685f2c
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/ApplicationDetailsFragment.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.car.settings.applications;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.os.UserHandle;
+
+import androidx.annotation.XmlRes;
+
+import com.android.car.settings.R;
+import com.android.car.settings.applications.appinfo.AppAllServicesPreferenceController;
+import com.android.car.settings.applications.appinfo.HibernationSwitchPreferenceController;
+import com.android.car.settings.common.Logger;
+import com.android.car.settings.common.SettingsFragment;
+import com.android.settingslib.applications.ApplicationsState;
+
+/**
+ * Shows details about an application.
+ */
+public class ApplicationDetailsFragment extends SettingsFragment {
+ private static final Logger LOG = new Logger(ApplicationDetailsFragment.class);
+ public static final String EXTRA_PACKAGE_NAME = "extra_package_name";
+
+ private PackageManager mPm;
+
+ private String mPackageName;
+ private PackageInfo mPackageInfo;
+ private ApplicationsState mAppState;
+ private ApplicationsState.AppEntry mAppEntry;
+
+ /** Creates an instance of this fragment, passing packageName as an argument. */
+ public static ApplicationDetailsFragment getInstance(String packageName) {
+ ApplicationDetailsFragment applicationDetailFragment = new ApplicationDetailsFragment();
+ Bundle bundle = new Bundle();
+ bundle.putString(EXTRA_PACKAGE_NAME, packageName);
+ applicationDetailFragment.setArguments(bundle);
+ return applicationDetailFragment;
+ }
+
+ @Override
+ @XmlRes
+ protected int getPreferenceScreenResId() {
+ return R.xml.application_details_fragment;
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ mPm = context.getPackageManager();
+
+ // These should be loaded before onCreate() so that the controller operates as expected.
+ mPackageName = getArguments().getString(EXTRA_PACKAGE_NAME);
+
+ mAppState = ApplicationsState.getInstance(requireActivity().getApplication());
+
+ retrieveAppEntry();
+
+ use(ApplicationPreferenceController.class,
+ R.string.pk_application_details_app)
+ .setAppEntry(mAppEntry).setAppState(mAppState);
+ use(ApplicationActionButtonsPreferenceController.class,
+ R.string.pk_application_details_action_buttons)
+ .setAppEntry(mAppEntry).setAppState(mAppState).setPackageName(mPackageName);
+ use(AppAllServicesPreferenceController.class,
+ R.string.pk_all_services_settings).setPackageName(mPackageName);
+ use(NotificationsPreferenceController.class,
+ R.string.pk_application_details_notifications).setPackageInfo(mPackageInfo);
+ use(PermissionsPreferenceController.class,
+ R.string.pk_application_details_permissions).setPackageName(mPackageName);
+ use(StoragePreferenceController.class,
+ R.string.pk_application_details_storage)
+ .setAppEntry(mAppEntry).setAppState(mAppState).setPackageName(mPackageName);
+ use(PrioritizeAppPerformancePreferenceController.class,
+ R.string.pk_application_details_prioritize_app_performance)
+ .setPackageInfo(mPackageInfo);
+ use(HibernationSwitchPreferenceController.class,
+ R.string.pk_hibernation_switch)
+ .setPackageName(mPackageName);
+ use(VersionPreferenceController.class,
+ R.string.pk_application_details_version).setPackageInfo(mPackageInfo);
+ }
+
+ private void retrieveAppEntry() {
+ mAppEntry = mAppState.getEntry(mPackageName, UserHandle.myUserId());
+ if (mAppEntry != null) {
+ try {
+ mPackageInfo = mPm.getPackageInfo(mPackageName,
+ PackageManager.MATCH_DISABLED_COMPONENTS | PackageManager.MATCH_ANY_USER
+ | PackageManager.GET_SIGNATURES | PackageManager.GET_PERMISSIONS);
+ } catch (PackageManager.NameNotFoundException e) {
+ LOG.e("Exception when retrieving package:" + mPackageName, e);
+ mPackageInfo = null;
+ }
+ } else {
+ mPackageInfo = null;
+ }
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/ApplicationListItemManager.java b/CarSettings/src/com/android/car/settings/applications/ApplicationListItemManager.java
new file mode 100644
index 00000000..e3b9b62f
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/ApplicationListItemManager.java
@@ -0,0 +1,266 @@
+/*
+ * 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.car.settings.applications;
+
+import android.os.Handler;
+import android.os.storage.VolumeInfo;
+
+import androidx.lifecycle.Lifecycle;
+
+import com.android.car.settings.common.Logger;
+import com.android.settingslib.applications.ApplicationsState;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Class used to load the applications installed on the system with their metadata.
+ */
+// TODO: consolidate with AppEntryListManager.
+public class ApplicationListItemManager implements ApplicationsState.Callbacks {
+ /**
+ * Callback that is called once the list of applications are loaded.
+ */
+ public interface AppListItemListener {
+ /**
+ * Called when the data is successfully loaded from {@link ApplicationsState.Callbacks} and
+ * icon, title and summary are set for all the applications.
+ */
+ void onDataLoaded(ArrayList apps);
+ }
+
+ private static final Logger LOG = new Logger(ApplicationListItemManager.class);
+ private static final String APP_NAME_UNKNOWN = "APP NAME UNKNOWN";
+
+ private final VolumeInfo mVolumeInfo;
+ private final Lifecycle mLifecycle;
+ private final ApplicationsState mAppState;
+ private final List mAppListItemListeners = new ArrayList<>();
+ private final Handler mHandler;
+ private final int mMillisecondUpdateInterval;
+ // Milliseconds that warnIfNotAllLoadedInTime method waits before comparing mAppsToLoad and
+ // mLoadedApps to log any apps that failed to load.
+ private final int mMaxAppLoadWaitInterval;
+
+ private ApplicationsState.Session mSession;
+ private ApplicationsState.AppFilter mAppFilter;
+ private Comparator mAppEntryComparator;
+ // Contains all of the apps that we are expecting to load.
+ private Set mAppsToLoad = new HashSet<>();
+ // Contains all apps that have been successfully loaded.
+ private ArrayList mLoadedApps = new ArrayList<>();
+
+ // Indicates whether onRebuildComplete's throttling is off and it is ready to render updates.
+ // onRebuildComplete uses throttling to prevent it from being called too often, since the
+ // animation can be choppy if the refresh rate is too high.
+ private boolean mReadyToRenderUpdates = true;
+ // Parameter we use to call onRebuildComplete method when the throttling is off and we are
+ // "ReadyToRenderUpdates" again.
+ private ArrayList mDeferredAppsToUpload;
+
+ public ApplicationListItemManager(VolumeInfo volumeInfo, Lifecycle lifecycle,
+ ApplicationsState appState, int millisecondUpdateInterval,
+ int maxWaitIntervalToFinishLoading) {
+ mVolumeInfo = volumeInfo;
+ mLifecycle = lifecycle;
+ mAppState = appState;
+ mHandler = new Handler();
+ mMillisecondUpdateInterval = millisecondUpdateInterval;
+ mMaxAppLoadWaitInterval = maxWaitIntervalToFinishLoading;
+ }
+
+ /**
+ * Registers a listener that will be notified once the data is loaded.
+ */
+ public void registerListener(AppListItemListener appListItemListener) {
+ if (!mAppListItemListeners.contains(appListItemListener) && appListItemListener != null) {
+ mAppListItemListeners.add(appListItemListener);
+ }
+ }
+
+ /**
+ * Unregisters the listener.
+ */
+ public void unregisterlistener(AppListItemListener appListItemListener) {
+ mAppListItemListeners.remove(appListItemListener);
+ }
+
+ /**
+ * Resumes the session and starts meauring app loading time on fragment start.
+ */
+ public void onFragmentStart() {
+ mSession.onResume();
+ warnIfNotAllLoadedInTime();
+ }
+
+ /**
+ * Pause the session on fragment stop.
+ */
+ public void onFragmentStop() {
+ mSession.onPause();
+ }
+
+ /**
+ * Starts the new session and start loading the list of installed applications on the device.
+ * This list will be filtered out based on the {@link ApplicationsState.AppFilter} provided.
+ * Once the list is ready, {@link AppListItemListener#onDataLoaded} will be called.
+ *
+ * @param appFilter based on which the list of applications will be filtered before
+ * returning.
+ * @param appEntryComparator comparator based on which the application list will be sorted.
+ */
+ public void startLoading(ApplicationsState.AppFilter appFilter,
+ Comparator appEntryComparator) {
+ if (mSession != null) {
+ LOG.w("Loading already started but restart attempted.");
+ return; // Prevent leaking sessions.
+ }
+ mAppFilter = appFilter;
+ mAppEntryComparator = appEntryComparator;
+ mSession = mAppState.newSession(this, mLifecycle);
+ }
+
+ /**
+ * Rebuilds the list of applications using the provided {@link ApplicationsState.AppFilter}.
+ * The filter will be used for all subsequent loading. Once the list is ready, {@link
+ * AppListItemListener#onDataLoaded} will be called.
+ */
+ public void rebuildWithFilter(ApplicationsState.AppFilter appFilter) {
+ mAppFilter = appFilter;
+ rebuild();
+ }
+
+ @Override
+ public void onPackageIconChanged() {
+ rebuild();
+ }
+
+ @Override
+ public void onPackageSizeChanged(String packageName) {
+ rebuild();
+ }
+
+ @Override
+ public void onAllSizesComputed() {
+ rebuild();
+ }
+
+ @Override
+ public void onLauncherInfoChanged() {
+ rebuild();
+ }
+
+ @Override
+ public void onLoadEntriesCompleted() {
+ rebuild();
+ }
+
+ @Override
+ public void onRunningStateChanged(boolean running) {
+ }
+
+ @Override
+ public void onPackageListChanged() {
+ rebuild();
+ }
+
+ @Override
+ public void onRebuildComplete(ArrayList apps) {
+ // Checking for apps.size prevents us from unnecessarily triggering throttling and blocking
+ // subsequent updates.
+ if (apps.size() == 0) {
+ return;
+ }
+
+ if (mReadyToRenderUpdates) {
+ mReadyToRenderUpdates = false;
+ mLoadedApps = new ArrayList<>();
+
+ for (ApplicationsState.AppEntry app : apps) {
+ if (isLoaded(app)) {
+ mLoadedApps.add(app);
+ }
+ }
+
+ for (AppListItemListener appListItemListener : mAppListItemListeners) {
+ appListItemListener.onDataLoaded(mLoadedApps);
+ }
+
+ mHandler.postDelayed(() -> {
+ mReadyToRenderUpdates = true;
+ if (mDeferredAppsToUpload != null) {
+ onRebuildComplete(mDeferredAppsToUpload);
+ mDeferredAppsToUpload = null;
+ }
+ }, mMillisecondUpdateInterval);
+ } else {
+ mDeferredAppsToUpload = apps;
+ }
+
+ // Add all apps that are not already contained in mAppsToLoad Set, since we want it to be an
+ // exhaustive Set of all apps to be loaded.
+ mAppsToLoad.addAll(apps);
+ }
+
+ private boolean isLoaded(ApplicationsState.AppEntry app) {
+ return app.label != null && app.sizeStr != null && app.icon != null;
+ }
+
+ private void warnIfNotAllLoadedInTime() {
+ mHandler.postDelayed(() -> {
+ if (mLoadedApps.size() < mAppsToLoad.size()) {
+ LOG.w("Expected to load " + mAppsToLoad.size() + " apps but only loaded "
+ + mLoadedApps.size());
+
+ // Creating a copy to avoid state inconsistency.
+ Set appsToLoadCopy = new HashSet(mAppsToLoad);
+ for (ApplicationsState.AppEntry loadedApp : mLoadedApps) {
+ appsToLoadCopy.remove(loadedApp);
+ }
+
+ for (ApplicationsState.AppEntry appEntry : appsToLoadCopy) {
+ String appName = appEntry.label == null ? APP_NAME_UNKNOWN : appEntry.label;
+ LOG.w("App failed to load: " + appName);
+ }
+ }
+ }, mMaxAppLoadWaitInterval);
+ }
+
+ ApplicationsState.AppFilter getCompositeFilter(String volumeUuid) {
+ if (mAppFilter == null) {
+ return null;
+ }
+ ApplicationsState.AppFilter filter = new ApplicationsState.VolumeFilter(volumeUuid);
+ filter = new ApplicationsState.CompoundFilter(mAppFilter, filter);
+ return filter;
+ }
+
+ private void rebuild() {
+ ApplicationsState.AppFilter filterObj = ApplicationsState.FILTER_EVERYTHING;
+
+ filterObj = new ApplicationsState.CompoundFilter(filterObj,
+ ApplicationsState.FILTER_NOT_HIDE);
+ ApplicationsState.AppFilter compositeFilter = getCompositeFilter(mVolumeInfo.getFsUuid());
+ if (compositeFilter != null) {
+ filterObj = new ApplicationsState.CompoundFilter(filterObj, compositeFilter);
+ }
+ ApplicationsState.AppFilter finalFilterObj = filterObj;
+ mSession.rebuild(finalFilterObj, mAppEntryComparator, /* foreground= */ false);
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/ApplicationPreferenceController.java b/CarSettings/src/com/android/car/settings/applications/ApplicationPreferenceController.java
new file mode 100644
index 00000000..63e0227d
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/ApplicationPreferenceController.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.car.settings.applications;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+
+import androidx.preference.Preference;
+
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.PreferenceController;
+import com.android.settingslib.applications.ApplicationsState;
+import com.android.settingslib.applications.ApplicationsState.AppEntry;
+
+/** Business logic for the Application field in the application details page. */
+public class ApplicationPreferenceController extends PreferenceController {
+
+ private AppEntry mAppEntry;
+ private ApplicationsState mApplicationsState;
+
+ public ApplicationPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ }
+
+ @Override
+ protected Class getPreferenceType() {
+ return Preference.class;
+ }
+
+ /** Sets the {@link AppEntry} which is used to load the app name and icon. */
+ public ApplicationPreferenceController setAppEntry(AppEntry appEntry) {
+ mAppEntry = appEntry;
+ return this;
+ }
+
+ /** Sets the {@link ApplicationsState} which is used to load the app name and icon. */
+ public ApplicationPreferenceController setAppState(ApplicationsState applicationsState) {
+ mApplicationsState = applicationsState;
+ return this;
+ }
+
+ @Override
+ protected void checkInitialized() {
+ if (mAppEntry == null || mApplicationsState == null) {
+ throw new IllegalStateException(
+ "AppEntry and AppState should be set before calling this function");
+ }
+ }
+
+ @Override
+ protected void updateState(Preference preference) {
+ preference.setTitle(getAppName());
+ preference.setIcon(getAppIcon());
+ }
+
+ protected String getAppName() {
+ mAppEntry.ensureLabel(getContext());
+ return mAppEntry.label;
+ }
+
+ protected Drawable getAppIcon() {
+ mApplicationsState.ensureIcon(mAppEntry);
+ return mAppEntry.icon;
+ }
+
+ protected String getAppVersion() {
+ return mAppEntry.getVersion(getContext());
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/ApplicationsSettingsFragment.java b/CarSettings/src/com/android/car/settings/applications/ApplicationsSettingsFragment.java
new file mode 100644
index 00000000..b74ac4aa
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/ApplicationsSettingsFragment.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.car.settings.applications;
+
+import static com.android.car.settings.storage.StorageUtils.maybeInitializeVolume;
+
+import android.app.Application;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.storage.StorageManager;
+import android.os.storage.VolumeInfo;
+
+import com.android.car.settings.R;
+import com.android.settingslib.applications.ApplicationsState;
+
+/**
+ * Lists all installed applications and their summary.
+ */
+public class ApplicationsSettingsFragment extends AppListFragment {
+
+ private ApplicationListItemManager mAppListItemManager;
+
+ @Override
+ protected int getPreferenceScreenResId() {
+ return R.xml.applications_settings_fragment;
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+
+ Application application = requireActivity().getApplication();
+ StorageManager sm = context.getSystemService(StorageManager.class);
+ VolumeInfo volume = maybeInitializeVolume(sm, getArguments());
+ mAppListItemManager = new ApplicationListItemManager(volume, getLifecycle(),
+ ApplicationsState.getInstance(application),
+ getContext().getResources().getInteger(
+ R.integer.millisecond_app_data_update_interval),
+ getContext().getResources().getInteger(
+ R.integer.millisecond_max_app_load_wait_interval));
+ mAppListItemManager.registerListener(
+ use(ApplicationsSettingsPreferenceController.class,
+ R.string.pk_all_applications_settings_list));
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mAppListItemManager.startLoading(getAppFilter(), ApplicationsState.ALPHA_COMPARATOR);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ mAppListItemManager.onFragmentStart();
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ mAppListItemManager.onFragmentStop();
+ }
+
+ @Override
+ protected void onToggleShowSystemApps(boolean showSystem) {
+ mAppListItemManager.rebuildWithFilter(getAppFilter());
+ }
+
+ private ApplicationsState.AppFilter getAppFilter() {
+ return shouldShowSystemApps() ? null
+ : ApplicationsState.FILTER_DOWNLOADED_AND_LAUNCHER_AND_INSTANT;
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/ApplicationsSettingsPreferenceController.java b/CarSettings/src/com/android/car/settings/applications/ApplicationsSettingsPreferenceController.java
new file mode 100644
index 00000000..a0aff7b2
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/ApplicationsSettingsPreferenceController.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.car.settings.applications;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+
+import androidx.preference.Preference;
+import androidx.preference.PreferenceGroup;
+
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.PreferenceController;
+import com.android.car.ui.preference.CarUiPreference;
+import com.android.settingslib.applications.ApplicationsState;
+
+import java.util.ArrayList;
+
+/** Business logic which populates the applications in this setting. */
+public class ApplicationsSettingsPreferenceController extends
+ PreferenceController implements
+ ApplicationListItemManager.AppListItemListener {
+
+ public ApplicationsSettingsPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ }
+
+ @Override
+ protected Class getPreferenceType() {
+ return PreferenceGroup.class;
+ }
+
+ @Override
+ public void onDataLoaded(ArrayList apps) {
+ getPreference().removeAll();
+ for (ApplicationsState.AppEntry appEntry : apps) {
+ getPreference().addPreference(
+ createPreference(appEntry.label, appEntry.sizeStr, appEntry.icon,
+ appEntry.info.packageName));
+ }
+ }
+
+ private Preference createPreference(String title, String summary, Drawable icon,
+ String packageName) {
+ CarUiPreference preference = new CarUiPreference(getContext());
+ preference.setTitle(title);
+ preference.setSummary(summary);
+ preference.setIcon(icon);
+ preference.setKey(packageName);
+ preference.setOnPreferenceClickListener(p -> {
+ getFragmentController().launchFragment(
+ ApplicationDetailsFragment.getInstance(packageName));
+ return true;
+ });
+ return preference;
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/ApplicationsUtils.java b/CarSettings/src/com/android/car/settings/applications/ApplicationsUtils.java
new file mode 100644
index 00000000..b4c29e86
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/ApplicationsUtils.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 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.car.settings.applications;
+
+import static android.provider.DeviceConfig.NAMESPACE_APP_HIBERNATION;
+
+import android.app.admin.DevicePolicyManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.UserInfo;
+import android.provider.DeviceConfig;
+import android.telecom.DefaultDialerManager;
+import android.text.TextUtils;
+import android.util.ArraySet;
+
+import com.android.car.settings.profiles.ProfileHelper;
+import com.android.internal.telephony.SmsApplication;
+
+import java.util.List;
+import java.util.Set;
+
+/** Utility functions for use in applications settings. */
+public class ApplicationsUtils {
+
+ /** Whether or not app hibernation is enabled on the device **/
+ public static final String PROPERTY_APP_HIBERNATION_ENABLED = "app_hibernation_enabled";
+
+ private ApplicationsUtils() {
+ }
+
+ /**
+ * Returns {@code true} if the package should always remain enabled.
+ */
+ // TODO: investigate making this behavior configurable via a feature factory with the current
+ // contents as the default.
+ public static boolean isKeepEnabledPackage(Context context, String packageName) {
+ // Find current default phone/sms app. We should keep them enabled.
+ Set keepEnabledPackages = new ArraySet<>();
+ String defaultDialer = DefaultDialerManager.getDefaultDialerApplication(context);
+ if (!TextUtils.isEmpty(defaultDialer)) {
+ keepEnabledPackages.add(defaultDialer);
+ }
+ ComponentName defaultSms = SmsApplication.getDefaultSmsApplication(
+ context, /* updateIfNeeded= */ true);
+ if (defaultSms != null) {
+ keepEnabledPackages.add(defaultSms.getPackageName());
+ }
+ return keepEnabledPackages.contains(packageName);
+ }
+
+ /**
+ * Returns {@code true} if the given {@code packageName} is device owner or profile owner of at
+ * least one user.
+ */
+ public static boolean isProfileOrDeviceOwner(String packageName, DevicePolicyManager dpm,
+ ProfileHelper profileHelper) {
+ if (dpm.isDeviceOwnerAppOnAnyUser(packageName)) {
+ return true;
+ }
+ List userInfos = profileHelper.getAllProfiles();
+ for (int i = 0; i < userInfos.size(); i++) {
+ ComponentName cn = dpm.getProfileOwnerAsUser(userInfos.get(i).id);
+ if (cn != null && cn.getPackageName().equals(packageName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns {@code true} if the hibernation feature is enabled, as configured through {@link
+ * DeviceConfig}, which can be overridden remotely with a flag or through adb.
+ */
+ public static boolean isHibernationEnabled() {
+ return DeviceConfig.getBoolean(
+ NAMESPACE_APP_HIBERNATION, PROPERTY_APP_HIBERNATION_ENABLED, true);
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/AppsFragment.java b/CarSettings/src/com/android/car/settings/applications/AppsFragment.java
new file mode 100644
index 00000000..000a9f54
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/AppsFragment.java
@@ -0,0 +1,94 @@
+/*
+ * 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.car.settings.applications;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.provider.Settings;
+
+import androidx.annotation.XmlRes;
+
+import com.android.car.settings.R;
+import com.android.car.settings.applications.performance.PerfImpactingAppsItemManager;
+import com.android.car.settings.common.SettingsFragment;
+import com.android.car.settings.search.CarBaseSearchIndexProvider;
+import com.android.settingslib.search.SearchIndexable;
+
+/** Shows subsettings related to apps. */
+@SearchIndexable
+public class AppsFragment extends SettingsFragment {
+
+ private RecentAppsItemManager mRecentAppsItemManager;
+ private InstalledAppCountItemManager mInstalledAppCountItemManager;
+ private HibernatedAppsItemManager mHibernatedAppsItemManager;
+ private PerfImpactingAppsItemManager mPerfImpactingAppsItemManager;
+
+ @Override
+ @XmlRes
+ protected int getPreferenceScreenResId() {
+ return R.xml.apps_fragment;
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ mRecentAppsItemManager = new RecentAppsItemManager(context,
+ context.getResources().getInteger(R.integer.recent_apps_max_count));
+ mRecentAppsItemManager.addListener(use(AllAppsPreferenceController.class,
+ R.string.pk_applications_settings_screen_entry));
+ mRecentAppsItemManager.addListener(use(RecentAppsGroupPreferenceController.class,
+ R.string.pk_recent_apps_group));
+ mRecentAppsItemManager.addListener(use(RecentAppsListPreferenceController.class,
+ R.string.pk_recent_apps_list));
+
+ mInstalledAppCountItemManager = new InstalledAppCountItemManager(context);
+ mInstalledAppCountItemManager.addListener(use(AllAppsPreferenceController.class,
+ R.string.pk_applications_settings_screen_entry));
+ mInstalledAppCountItemManager.addListener(use(RecentAppsViewAllPreferenceController.class,
+ R.string.pk_recent_apps_view_all));
+
+ mHibernatedAppsItemManager = new HibernatedAppsItemManager(context);
+ mHibernatedAppsItemManager.setListener(use(HibernatedAppsPreferenceController.class,
+ R.string.pk_hibernated_apps));
+
+ mPerfImpactingAppsItemManager = new PerfImpactingAppsItemManager(context);
+ mPerfImpactingAppsItemManager.addListener(
+ use(PerfImpactingAppsEntryPreferenceController.class,
+ R.string.pk_performance_impacting_apps_entry));
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mRecentAppsItemManager.startLoading();
+ mInstalledAppCountItemManager.startLoading();
+ mHibernatedAppsItemManager.startLoading();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mPerfImpactingAppsItemManager.startLoading();
+ }
+
+ /**
+ * Data provider for Settings Search.
+ */
+ public static final CarBaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
+ new CarBaseSearchIndexProvider(R.xml.apps_fragment,
+ Settings.ACTION_APPLICATION_SETTINGS);
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/HibernatedAppsItemManager.java b/CarSettings/src/com/android/car/settings/applications/HibernatedAppsItemManager.java
new file mode 100644
index 00000000..faf4c85d
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/HibernatedAppsItemManager.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.settings.applications;
+
+import android.content.Context;
+import android.permission.PermissionControllerManager;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Class for fetching and returning the number of hibernated apps. Largely derived from
+ * {@link com.android.settings.applications.HibernatedAppsPreferenceController}.
+ */
+public class HibernatedAppsItemManager {
+ private final Context mContext;
+ private HibernatedAppsCountListener mListener;
+
+ public HibernatedAppsItemManager(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * Starts fetching recently used apps
+ */
+ public void startLoading() {
+ PermissionControllerManager permController =
+ mContext.getSystemService(PermissionControllerManager.class);
+ if (mListener != null && permController != null) {
+ // This executor is only used for returning the value
+ // The main logic happens on a background thread
+ permController.getUnusedAppCount(mContext.getMainExecutor(),
+ mListener::onHibernatedAppsCountLoaded);
+ }
+ }
+
+ /**
+ * Registers a listener that will be notified once the data is loaded.
+ */
+ public void setListener(@NonNull HibernatedAppsCountListener listener) {
+ mListener = listener;
+ }
+
+ /**
+ * Callback that is called once the count of hibernated apps has been fetched.
+ */
+ public interface HibernatedAppsCountListener {
+ /**
+ * Called when the count of hibernated apps has loaded.
+ */
+ void onHibernatedAppsCountLoaded(int hibernatedAppsCount);
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/HibernatedAppsPreferenceController.java b/CarSettings/src/com/android/car/settings/applications/HibernatedAppsPreferenceController.java
new file mode 100644
index 00000000..d69c3741
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/HibernatedAppsPreferenceController.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.settings.applications;
+
+import static com.android.car.settings.applications.ApplicationsUtils.isHibernationEnabled;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+
+import androidx.preference.Preference;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.PreferenceController;
+import com.android.settingslib.utils.StringUtil;
+
+/**
+ * A preference controller handling the logic for updating summary of hibernated apps.
+ */
+public final class HibernatedAppsPreferenceController extends PreferenceController
+ implements HibernatedAppsItemManager.HibernatedAppsCountListener {
+ private static final String TAG = "HibernatedAppsPrefController";
+
+ public HibernatedAppsPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController,
+ CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ }
+
+ @Override
+ protected Class getPreferenceType() {
+ return Preference.class;
+ }
+
+ @Override
+ public int getDefaultAvailabilityStatus() {
+ return isHibernationEnabled() ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
+ }
+
+ @Override
+ public void onHibernatedAppsCountLoaded(int hibernatedAppsCount) {
+ getPreference().setSummary(StringUtil.getIcuPluralsString(getContext(), hibernatedAppsCount,
+ R.string.unused_apps_summary));
+ refreshUi();
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/HideSystemSwitchPreferenceController.java b/CarSettings/src/com/android/car/settings/applications/HideSystemSwitchPreferenceController.java
new file mode 100644
index 00000000..6e68d57d
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/HideSystemSwitchPreferenceController.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.settings.applications;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import com.android.car.settings.common.ColoredSwitchPreference;
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.PreferenceController;
+
+/**
+ * Hides/shows system apps from Application listings. Intended to be used inside instances of
+ * {@link AppListFragment}.
+ */
+public class HideSystemSwitchPreferenceController
+ extends PreferenceController {
+
+ private final SharedPreferences mSharedPreferences = getContext().getSharedPreferences(
+ AppListFragment.SHARED_PREFERENCE_PATH, Context.MODE_PRIVATE);
+
+ public HideSystemSwitchPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController,
+ CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ }
+
+ @Override
+ protected Class getPreferenceType() {
+ return ColoredSwitchPreference.class;
+ }
+
+ @Override
+ protected boolean handlePreferenceChanged(ColoredSwitchPreference preference, Object newValue) {
+ boolean checked = (Boolean) newValue;
+ mSharedPreferences.edit().putBoolean(AppListFragment.KEY_HIDE_SYSTEM, checked).apply();
+ return true;
+ }
+
+ @Override
+ protected void onStartInternal() {
+ getPreference().setChecked(getSharedPreferenceHidden());
+ }
+
+ private boolean getSharedPreferenceHidden() {
+ return mSharedPreferences.getBoolean(AppListFragment.KEY_HIDE_SYSTEM,
+ /* defaultValue= */ true);
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/InstalledAppCountItemManager.java b/CarSettings/src/com/android/car/settings/applications/InstalledAppCountItemManager.java
new file mode 100644
index 00000000..bcde68e2
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/InstalledAppCountItemManager.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.settings.applications;
+
+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.UserHandle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.settingslib.utils.ThreadUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Class used to count the number of non-system apps. Largely derived from
+ * {@link com.android.settings.applications.InstalledAppCounter}.
+ */
+public class InstalledAppCountItemManager {
+
+ private Context mContext;
+ private final List mInstalledAppCountListeners;
+
+ public InstalledAppCountItemManager(Context context) {
+ mContext = context;
+ mInstalledAppCountListeners = new ArrayList<>();
+ }
+
+ /**
+ * Registers a listener that will be notified once the data is loaded.
+ */
+ public void addListener(@NonNull InstalledAppCountListener listener) {
+ mInstalledAppCountListeners.add(listener);
+ }
+
+ /**
+ * Starts fetching installed apps and counting the non-system apps
+ */
+ public void startLoading() {
+ ThreadUtils.postOnBackgroundThread(() -> {
+ List appList = mContext.getPackageManager()
+ .getInstalledApplications(PackageManager.MATCH_DISABLED_COMPONENTS
+ | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS);
+
+ int appCount = 0;
+ for (ApplicationInfo applicationInfo : appList) {
+ if (shouldCountApp(applicationInfo)) {
+ appCount++;
+ }
+ }
+ int finalAppCount = appCount;
+ for (InstalledAppCountListener listener : mInstalledAppCountListeners) {
+ ThreadUtils.postOnMainThread(() -> listener
+ .onInstalledAppCountLoaded(finalAppCount));
+ }
+ });
+ }
+
+ @VisibleForTesting
+ boolean shouldCountApp(ApplicationInfo applicationInfo) {
+ if ((applicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0) {
+ return true;
+ }
+ if ((applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
+ return true;
+ }
+ int userId = UserHandle.getUserId(applicationInfo.uid);
+ Intent launchIntent = new Intent(Intent.ACTION_MAIN, null)
+ .addCategory(Intent.CATEGORY_LAUNCHER)
+ .setPackage(applicationInfo.packageName);
+ List intents = mContext.getPackageManager().queryIntentActivitiesAsUser(
+ launchIntent,
+ PackageManager.MATCH_DISABLED_COMPONENTS
+ | PackageManager.MATCH_DIRECT_BOOT_AWARE
+ | PackageManager.MATCH_DIRECT_BOOT_UNAWARE,
+ userId);
+ return intents != null && !intents.isEmpty();
+ }
+
+ /**
+ * Callback that is called once the number of installed applications is counted.
+ */
+ public interface InstalledAppCountListener {
+ /**
+ * Called when the apps are successfully loaded from PackageManager and non-system apps are
+ * counted.
+ */
+ void onInstalledAppCountLoaded(int appCount);
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/NotificationsPreferenceController.java b/CarSettings/src/com/android/car/settings/applications/NotificationsPreferenceController.java
new file mode 100644
index 00000000..c89ae63c
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/NotificationsPreferenceController.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.car.settings.applications;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+
+import androidx.preference.TwoStatePreference;
+
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.Logger;
+import com.android.car.settings.notifications.BaseNotificationsPreferenceController;
+
+/**
+ * Controller for preference which enables / disables showing notifications for an application.
+ */
+public class NotificationsPreferenceController extends
+ BaseNotificationsPreferenceController {
+
+ private static final Logger LOG = new Logger(NotificationsPreferenceController.class);
+
+ private String mPackageName;
+ private int mUid;
+ private ApplicationInfo mAppInfo;
+
+ public NotificationsPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ }
+
+ /**
+ * Set the package info of the application.
+ */
+ public void setPackageInfo(PackageInfo packageInfo) {
+ mPackageName = packageInfo.packageName;
+ mUid = packageInfo.applicationInfo.uid;
+ mAppInfo = packageInfo.applicationInfo;
+ }
+
+ @Override
+ protected Class getPreferenceType() {
+ return TwoStatePreference.class;
+ }
+
+ @Override
+ protected void updateState(TwoStatePreference preference) {
+ preference.setChecked(areNotificationsEnabled(mPackageName, mUid));
+ preference.setEnabled(areNotificationsChangeable(mAppInfo));
+ }
+
+ @Override
+ protected boolean handlePreferenceChanged(TwoStatePreference preference, Object newValue) {
+ boolean enabled = (boolean) newValue;
+ return toggleNotificationsSetting(mPackageName, mUid, enabled);
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/PerfImpactingAppsEntryPreferenceController.java b/CarSettings/src/com/android/car/settings/applications/PerfImpactingAppsEntryPreferenceController.java
new file mode 100644
index 00000000..91777134
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/PerfImpactingAppsEntryPreferenceController.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.settings.applications;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+
+import androidx.preference.Preference;
+
+import com.android.car.settings.R;
+import com.android.car.settings.applications.performance.PerfImpactingAppsItemManager;
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.PreferenceController;
+import com.android.settingslib.utils.StringUtil;
+
+/**
+ * Controller for the entry point to performance-impacting apps settings. It fetches the number of
+ * resource overuse packages and updates the entry point preference's summary.
+ */
+public final class PerfImpactingAppsEntryPreferenceController extends
+ PreferenceController implements
+ PerfImpactingAppsItemManager.PerfImpactingAppsListener {
+
+ public PerfImpactingAppsEntryPreferenceController(Context context,
+ String preferenceKey,
+ FragmentController fragmentController,
+ CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ }
+
+ @Override
+ protected Class getPreferenceType() {
+ return Preference.class;
+ }
+
+ @Override
+ public void onPerfImpactingAppsLoaded(int disabledPackagesCount) {
+ getPreference().setSummary(StringUtil.getIcuPluralsString(getContext(),
+ disabledPackagesCount, R.string.performance_impacting_apps_summary));
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/PermissionsPreferenceController.java b/CarSettings/src/com/android/car/settings/applications/PermissionsPreferenceController.java
new file mode 100644
index 00000000..98cf1edb
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/PermissionsPreferenceController.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.settings.applications;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.icu.text.ListFormatter;
+import android.text.TextUtils;
+
+import androidx.preference.Preference;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.Logger;
+import com.android.car.settings.common.PreferenceController;
+import com.android.settingslib.applications.PermissionsSummaryHelper;
+import com.android.settingslib.utils.StringUtil;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Business logic for the permissions entry in the application details settings. */
+public class PermissionsPreferenceController extends PreferenceController {
+
+ private static final Logger LOG = new Logger(PermissionsPreferenceController.class);
+
+ private String mPackageName;
+ private String mSummary;
+
+ public PermissionsPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ }
+
+ @Override
+ protected Class getPreferenceType() {
+ return Preference.class;
+ }
+
+ /**
+ * Set the packageName, which is used on the intent to open the permissions
+ * selection screen.
+ */
+ public void setPackageName(String packageName) {
+ mPackageName = packageName;
+ }
+
+ @Override
+ protected void checkInitialized() {
+ if (mPackageName == null) {
+ throw new IllegalStateException(
+ "PackageName should be set before calling this function");
+ }
+ }
+
+ @Override
+ protected void onStartInternal() {
+ PermissionsSummaryHelper.getPermissionSummary(getContext(), mPackageName,
+ mPermissionCallback);
+ }
+
+ @Override
+ protected void updateState(Preference preference) {
+ preference.setSummary(getSummary());
+ }
+
+ @Override
+ protected boolean handlePreferenceClicked(Preference preference) {
+ Intent intent = new Intent(Intent.ACTION_MANAGE_APP_PERMISSIONS);
+ intent.putExtra(Intent.EXTRA_PACKAGE_NAME, mPackageName);
+ try {
+ getContext().startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ LOG.w("No app can handle android.intent.action.MANAGE_APP_PERMISSIONS");
+ }
+ return true;
+ }
+
+ private CharSequence getSummary() {
+ if (TextUtils.isEmpty(mSummary)) {
+ return getContext().getString(R.string.computing_size);
+ }
+ return mSummary;
+ }
+
+ private final PermissionsSummaryHelper.PermissionsResultCallback mPermissionCallback =
+ new PermissionsSummaryHelper.PermissionsResultCallback() {
+ @Override
+ public void onPermissionSummaryResult(
+ int requestedPermissionCount, int additionalGrantedPermissionCount,
+ List grantedGroupLabels) {
+ Resources res = getContext().getResources();
+
+ if (requestedPermissionCount == 0) {
+ mSummary = res.getString(
+ R.string.runtime_permissions_summary_no_permissions_requested);
+ } else {
+ ArrayList list = new ArrayList<>(grantedGroupLabels);
+ if (additionalGrantedPermissionCount > 0) {
+ // N additional permissions.
+ list.add(StringUtil.getIcuPluralsString(getContext(),
+ additionalGrantedPermissionCount,
+ R.string.runtime_permissions_additional_count));
+ }
+ if (list.isEmpty()) {
+ mSummary = res.getString(
+ R.string.runtime_permissions_summary_no_permissions_granted);
+ } else {
+ mSummary = ListFormatter.getInstance().format(list);
+ }
+ }
+ refreshUi();
+ }
+ };
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/PrioritizeAppPerformancePreferenceController.java b/CarSettings/src/com/android/car/settings/applications/PrioritizeAppPerformancePreferenceController.java
new file mode 100644
index 00000000..8d482042
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/PrioritizeAppPerformancePreferenceController.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.settings.applications;
+
+import android.car.Car;
+import android.car.drivingstate.CarUxRestrictions;
+import android.car.watchdog.CarWatchdogManager;
+import android.car.watchdog.PackageKillableState;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.os.UserHandle;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.TwoStatePreference;
+
+import com.android.car.settings.applications.performance.PerfImpactingAppsUtils;
+import com.android.car.settings.common.ConfirmationDialogFragment;
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.Logger;
+import com.android.car.settings.common.PreferenceController;
+
+/** Controller for preference which turns on / off prioritize app performance setting. */
+public class PrioritizeAppPerformancePreferenceController
+ extends PreferenceController {
+ private static final Logger LOG =
+ new Logger(PrioritizeAppPerformancePreferenceController.class);
+
+ @VisibleForTesting
+ static final String TURN_ON_PRIORITIZE_APP_PERFORMANCE_DIALOG_TAG =
+ "com.android.car.settings.applications.TurnOnPrioritizeAppPerformanceDialogTag";
+
+ @Nullable
+ private Car mCar;
+ @Nullable
+ private CarWatchdogManager mCarWatchdogManager;
+ private String mPackageName;
+ private UserHandle mUserHandle;
+
+ private final ConfirmationDialogFragment.ConfirmListener mConfirmListener = arguments -> {
+ if (!isCarConnected()) {
+ return;
+ }
+ setKillableState(false);
+ getPreference().setChecked(true);
+ };
+
+ public PrioritizeAppPerformancePreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController,
+ CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ }
+
+ @Override
+ protected void onCreateInternal() {
+ connectToCar();
+
+ ConfirmationDialogFragment dialogFragment =
+ (ConfirmationDialogFragment) getFragmentController().findDialogByTag(
+ TURN_ON_PRIORITIZE_APP_PERFORMANCE_DIALOG_TAG);
+ ConfirmationDialogFragment.resetListeners(
+ dialogFragment, mConfirmListener, /* rejectListener= */ null,
+ /* neutralListener= */ null);
+ }
+
+ @Override
+ protected void onDestroyInternal() {
+ if (mCar != null) {
+ mCar.disconnect();
+ mCar = null;
+ }
+ }
+
+ /**
+ * Set the package info of the application.
+ */
+ public void setPackageInfo(PackageInfo packageInfo) {
+ mPackageName = packageInfo.packageName;
+ mUserHandle = UserHandle.getUserHandleForUid(packageInfo.applicationInfo.uid);
+ }
+
+ @Override
+ protected Class getPreferenceType() {
+ return TwoStatePreference.class;
+ }
+
+ @Override
+ protected void updateState(TwoStatePreference preference) {
+ if (!isCarConnected()) {
+ return;
+ }
+ int killableState = PerfImpactingAppsUtils.getKillableState(mPackageName, mUserHandle,
+ mCarWatchdogManager);
+ preference.setChecked(killableState == PackageKillableState.KILLABLE_STATE_NO);
+ preference.setEnabled(killableState != PackageKillableState.KILLABLE_STATE_NEVER);
+ }
+
+ @Override
+ protected boolean handlePreferenceChanged(TwoStatePreference preference, Object newValue) {
+ boolean isToggledOn = (boolean) newValue;
+ if (isToggledOn) {
+ PerfImpactingAppsUtils.showPrioritizeAppConfirmationDialog(getContext(),
+ getFragmentController(), mConfirmListener,
+ TURN_ON_PRIORITIZE_APP_PERFORMANCE_DIALOG_TAG);
+ return false;
+ }
+ if (!isCarConnected()) {
+ return false;
+ }
+ setKillableState(true);
+ return true;
+ }
+
+ private void setKillableState(boolean isKillable) {
+ mCarWatchdogManager.setKillablePackageAsUser(mPackageName, mUserHandle, isKillable);
+ }
+
+ private boolean isCarConnected() {
+ if (mCarWatchdogManager == null) {
+ LOG.e("CarWatchdogManager is null. Could not set killable state for '" + mPackageName
+ + "'.");
+ connectToCar();
+ return false;
+ }
+ return true;
+ }
+
+ private void connectToCar() {
+ if (mCar != null && mCar.isConnected()) {
+ mCar.disconnect();
+ mCar = null;
+ }
+ mCar = Car.createCar(getContext(), null, Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER,
+ (car, isReady) -> {
+ mCarWatchdogManager = isReady
+ ? (CarWatchdogManager) car.getCarManager(Car.CAR_WATCHDOG_SERVICE)
+ : null;
+ });
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/RecentAppsGroupPreferenceController.java b/CarSettings/src/com/android/car/settings/applications/RecentAppsGroupPreferenceController.java
new file mode 100644
index 00000000..30b21442
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/RecentAppsGroupPreferenceController.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.settings.applications;
+
+import android.app.usage.UsageStats;
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+
+import androidx.preference.PreferenceGroup;
+
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.PreferenceController;
+
+import java.util.List;
+
+/**
+ * Class that controls visibility based on whether there have been recently used apps.
+ * Hidden if there have are no recently used apps.
+ */
+public class RecentAppsGroupPreferenceController extends PreferenceController
+ implements RecentAppsItemManager.RecentAppStatsListener {
+
+ // In most cases, device has recently opened apps. So, assume true by default.
+ private boolean mAreThereRecentlyUsedApps = true;
+
+ public RecentAppsGroupPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ }
+
+ @Override
+ protected Class getPreferenceType() {
+ return PreferenceGroup.class;
+ }
+
+ @Override
+ public int getDefaultAvailabilityStatus() {
+ return mAreThereRecentlyUsedApps ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
+ }
+
+ @Override
+ public void onRecentAppStatsLoaded(List recentAppStats) {
+ mAreThereRecentlyUsedApps = !recentAppStats.isEmpty();
+ refreshUi();
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/RecentAppsItemManager.java b/CarSettings/src/com/android/car/settings/applications/RecentAppsItemManager.java
new file mode 100644
index 00000000..37181fbb
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/RecentAppsItemManager.java
@@ -0,0 +1,199 @@
+/*
+ * 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.car.settings.applications;
+
+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.UserHandle;
+import android.util.ArrayMap;
+import android.util.SparseArray;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.Logger;
+import com.android.settingslib.applications.AppUtils;
+import com.android.settingslib.applications.ApplicationsState;
+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;
+
+/**
+ * Class for fetching and returning recently used apps. Largely derived from
+ * {@link com.android.settings.applications.RecentAppStatsMixin}.
+ */
+public class RecentAppsItemManager implements Comparator {
+
+ private static final Logger LOG = new Logger(RecentAppsItemManager.class);
+
+ @VisibleForTesting
+ final List mRecentApps;
+ private final int mUserId;
+ private final int mMaximumApps;
+ private final Context mContext;
+ private final PackageManager mPm;
+ private final UsageStatsManager mUsageStatsManager;
+ private final ApplicationsState mApplicationsState;
+ private final SparseArray mAppStatsListeners;
+ private final int mDaysThreshold;
+ private final List mIgnoredPackages;
+ private Calendar mCalendar;
+
+ public RecentAppsItemManager(Context context, int maximumApps) {
+ this(context, maximumApps, ApplicationsState.getInstance(
+ (Application) context.getApplicationContext()));
+ }
+
+ @VisibleForTesting
+ RecentAppsItemManager(Context context, int maximumApps, ApplicationsState applicationsState) {
+ mContext = context;
+ mMaximumApps = maximumApps;
+ mUserId = UserHandle.myUserId();
+ mPm = mContext.getPackageManager();
+ mUsageStatsManager = mContext.getSystemService(UsageStatsManager.class);
+ mApplicationsState = applicationsState;
+ mRecentApps = new ArrayList<>();
+ mAppStatsListeners = new SparseArray<>();
+ mDaysThreshold = mContext.getResources()
+ .getInteger(R.integer.recent_apps_days_threshold);
+ mIgnoredPackages = Arrays.asList(mContext.getResources()
+ .getStringArray(R.array.recent_apps_ignored_packages));
+ }
+
+ /**
+ * Starts fetching recently used apps
+ */
+ public void startLoading() {
+ ThreadUtils.postOnBackgroundThread(() -> {
+ loadDisplayableRecentApps(mMaximumApps);
+ for (int i = 0; i < mAppStatsListeners.size(); i++) {
+ int finalIndex = i;
+ ThreadUtils.postOnMainThread(() -> mAppStatsListeners.valueAt(finalIndex)
+ .onRecentAppStatsLoaded(mRecentApps));
+ }
+ });
+ }
+
+ @Override
+ public final int compare(UsageStats a, UsageStats b) {
+ // return by descending order
+ return Long.compare(b.getLastTimeUsed(), a.getLastTimeUsed());
+ }
+
+ /**
+ * Registers a listener that will be notified once the data is loaded.
+ */
+ public void addListener(@NonNull RecentAppStatsListener listener) {
+ mAppStatsListeners.append(mAppStatsListeners.size(), listener);
+ }
+
+ @VisibleForTesting
+ void loadDisplayableRecentApps(int number) {
+ mRecentApps.clear();
+ mCalendar = Calendar.getInstance();
+ mCalendar.add(Calendar.DAY_OF_YEAR, -mDaysThreshold);
+ List mStats = mUsageStatsManager.queryUsageStats(
+ UsageStatsManager.INTERVAL_BEST, mCalendar.getTimeInMillis(),
+ System.currentTimeMillis());
+
+ Map map = new ArrayMap<>();
+ for (UsageStats pkgStats : mStats) {
+ if (!shouldIncludePkgInRecents(pkgStats)) {
+ continue;
+ }
+ String pkgName = pkgStats.getPackageName();
+ UsageStats existingStats = map.get(pkgName);
+ if (existingStats == null) {
+ map.put(pkgName, pkgStats);
+ } else {
+ existingStats.add(pkgStats);
+ }
+ }
+ List packageStats = new ArrayList<>();
+ packageStats.addAll(map.values());
+ Collections.sort(packageStats, /* comparator= */ this);
+ int count = 0;
+ for (UsageStats stat : packageStats) {
+ 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) {
+ String pkgName = stat.getPackageName();
+ if (stat.getLastTimeUsed() < mCalendar.getTimeInMillis()) {
+ LOG.d("Invalid timestamp (usage time is more than 24 hours ago), skipping "
+ + pkgName);
+ return false;
+ }
+
+ if (mIgnoredPackages.contains(pkgName)) {
+ LOG.d("System package, skipping " + pkgName);
+ return false;
+ }
+
+ if (AppUtils.isHiddenSystemModule(mContext, pkgName)) {
+ return false;
+ }
+
+ 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.
+ ApplicationsState.AppEntry appEntry =
+ mApplicationsState.getEntry(pkgName, mUserId);
+ if (appEntry == null || appEntry.info == null || !AppUtils.isInstant(appEntry.info)) {
+ LOG.d("Not a user visible or instant app, skipping " + pkgName);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Callback that is called once the recently used apps have been fetched.
+ */
+ public interface RecentAppStatsListener {
+ /**
+ * Called when the recently used apps are successfully loaded
+ */
+ void onRecentAppStatsLoaded(List recentAppStats);
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/RecentAppsListPreferenceController.java b/CarSettings/src/com/android/car/settings/applications/RecentAppsListPreferenceController.java
new file mode 100644
index 00000000..6b417d90
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/RecentAppsListPreferenceController.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.settings.applications;
+
+import android.app.Application;
+import android.app.usage.UsageStats;
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+import android.os.UserHandle;
+import android.text.format.DateUtils;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceCategory;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.PreferenceController;
+import com.android.car.ui.preference.CarUiPreference;
+import com.android.settingslib.Utils;
+import com.android.settingslib.applications.ApplicationsState;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Class responsible for displaying recently used apps.
+ */
+public class RecentAppsListPreferenceController extends PreferenceController
+ implements RecentAppsItemManager.RecentAppStatsListener {
+
+ private ApplicationsState mApplicationsState;
+ private int mUserId;
+ private List mRecentAppStats;
+ private int mMaxRecentAppsCount;
+
+ public RecentAppsListPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ this(context, preferenceKey, fragmentController, uxRestrictions, ApplicationsState
+ .getInstance((Application) context.getApplicationContext()));
+ }
+
+ @VisibleForTesting
+ RecentAppsListPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions,
+ ApplicationsState applicationsState) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ mApplicationsState = applicationsState;
+ mUserId = UserHandle.myUserId();
+ mRecentAppStats = new ArrayList<>();
+ mMaxRecentAppsCount = getContext().getResources().getInteger(
+ R.integer.recent_apps_max_count);
+ }
+
+ @Override
+ public void onRecentAppStatsLoaded(List recentAppStats) {
+ mRecentAppStats = recentAppStats;
+ refreshUi();
+ }
+
+ @Override
+ protected Class getPreferenceType() {
+ return PreferenceCategory.class;
+ }
+
+ @Override
+ protected void updateState(PreferenceCategory preferenceCategory) {
+ preferenceCategory.setVisible(!mRecentAppStats.isEmpty());
+ preferenceCategory.removeAll();
+
+ int prefCount = 0;
+ for (UsageStats usageStats : mRecentAppStats) {
+ Preference pref = createPreference(getContext(), usageStats);
+
+ if (pref != null) {
+ getPreference().addPreference(pref);
+ prefCount++;
+ if (prefCount == mMaxRecentAppsCount) {
+ break;
+ }
+ }
+ }
+ }
+
+ private Preference createPreference(Context context, UsageStats usageStats) {
+ String pkgName = usageStats.getPackageName();
+ ApplicationsState.AppEntry appEntry = mApplicationsState.getEntry(pkgName, mUserId);
+
+ if (appEntry == null) {
+ return null;
+ }
+
+ Preference pref = new CarUiPreference(context);
+ pref.setTitle(appEntry.label);
+ if (appEntry.icon == null) {
+ pref.setIcon(Utils.getBadgedIcon(context, appEntry.info));
+ } else {
+ pref.setIcon(appEntry.icon);
+ }
+ pref.setSummary(DateUtils.getRelativeTimeSpanString(usageStats.getLastTimeStamp(),
+ System.currentTimeMillis(), DateUtils.SECOND_IN_MILLIS));
+ pref.setOnPreferenceClickListener(p -> {
+ getFragmentController().launchFragment(
+ ApplicationDetailsFragment.getInstance(pkgName));
+ return true;
+ });
+
+ return pref;
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/RecentAppsViewAllPreferenceController.java b/CarSettings/src/com/android/car/settings/applications/RecentAppsViewAllPreferenceController.java
new file mode 100644
index 00000000..4f2ca8d9
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/RecentAppsViewAllPreferenceController.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.settings.applications;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+
+import androidx.preference.Preference;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.PreferenceController;
+
+/**
+ * Class responsible for directing users to view the apps list page.
+ * Sets title based on number of installed non-system apps.
+ */
+public class RecentAppsViewAllPreferenceController extends PreferenceController
+ implements InstalledAppCountItemManager.InstalledAppCountListener {
+
+ public RecentAppsViewAllPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ }
+
+ @Override
+ protected Class getPreferenceType() {
+ return Preference.class;
+ }
+
+ @Override
+ public void onInstalledAppCountLoaded(int appCount) {
+ getPreference().setTitle(getContext().getResources().getString(
+ R.string.apps_view_all_apps_title, appCount));
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/StoragePreferenceController.java b/CarSettings/src/com/android/car/settings/applications/StoragePreferenceController.java
new file mode 100644
index 00000000..1d269e4d
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/StoragePreferenceController.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.settings.applications;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+import android.os.UserHandle;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.Preference;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.PreferenceController;
+import com.android.car.settings.storage.AppStorageSettingsDetailsFragment;
+import com.android.settingslib.applications.ApplicationsState;
+
+import java.util.ArrayList;
+
+/** Business logic for the storage entry in the application details settings. */
+public class StoragePreferenceController extends PreferenceController {
+
+ private ApplicationsState mApplicationsState;
+ private ApplicationsState.AppEntry mAppEntry;
+ private ApplicationsState.Session mSession;
+ private String mPackageName;
+
+ @VisibleForTesting
+ final ApplicationsState.Callbacks mApplicationStateCallbacks =
+ new ApplicationsState.Callbacks() {
+ @Override
+ public void onRunningStateChanged(boolean running) {
+ }
+
+ @Override
+ public void onPackageListChanged() {
+ }
+
+ @Override
+ public void onRebuildComplete(ArrayList apps) {
+ }
+
+ @Override
+ public void onPackageIconChanged() {
+ }
+
+ @Override
+ public void onPackageSizeChanged(String packageName) {
+ if (packageName.equals(mPackageName)) {
+ refreshUi();
+ }
+ }
+
+ @Override
+ public void onAllSizesComputed() {
+ refreshUi();
+ }
+
+ @Override
+ public void onLauncherInfoChanged() {
+ }
+
+ @Override
+ public void onLoadEntriesCompleted() {
+ }
+ };
+
+ public StoragePreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ }
+
+ @Override
+ protected Class getPreferenceType() {
+ return Preference.class;
+ }
+
+
+ /** Sets the {@link ApplicationsState.AppEntry} which is used to load the app size. */
+ public StoragePreferenceController setAppEntry(ApplicationsState.AppEntry appEntry) {
+ mAppEntry = appEntry;
+ return this;
+ }
+
+ /** Sets the {@link ApplicationsState} which is used to load the app size. */
+ public StoragePreferenceController setAppState(ApplicationsState applicationsState) {
+ mApplicationsState = applicationsState;
+ return this;
+ }
+
+ /**
+ * Set the packageName, which is used to open the AppStorageSettingsDetailsFragment
+ */
+ public StoragePreferenceController setPackageName(String packageName) {
+ mPackageName = packageName;
+ return this;
+ }
+
+ @Override
+ protected void checkInitialized() {
+ if (mAppEntry == null || mApplicationsState == null || mPackageName == null) {
+ throw new IllegalStateException(
+ "AppEntry, ApplicationsState and PackageName should be set before calling this "
+ + "function");
+ }
+ }
+
+ @Override
+ protected void onCreateInternal() {
+ mSession = mApplicationsState.newSession(mApplicationStateCallbacks);
+ }
+
+ @Override
+ protected void onStartInternal() {
+ mSession.onResume();
+ }
+
+ @Override
+ protected void onStopInternal() {
+ mSession.onPause();
+ }
+
+ @Override
+ protected void updateState(Preference preference) {
+ refreshAppEntry();
+ if (mAppEntry == null) {
+ getFragmentController().goBack();
+ } else if (mAppEntry.sizeStr == null) {
+ preference.setSummary(
+ getContext().getString(R.string.memory_calculating_size));
+ } else {
+ preference.setSummary(
+ getContext().getString(R.string.storage_type_internal, mAppEntry.sizeStr));
+ }
+ }
+
+ @Override
+ protected boolean handlePreferenceClicked(Preference preference) {
+ getFragmentController().launchFragment(
+ AppStorageSettingsDetailsFragment.getInstance(mPackageName));
+ return true;
+ }
+
+ // TODO(b/201351382): Remove after SettingsLib investigation
+ private void refreshAppEntry() {
+ mAppEntry = mApplicationsState.getEntry(mPackageName, UserHandle.myUserId());
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/VersionPreferenceController.java b/CarSettings/src/com/android/car/settings/applications/VersionPreferenceController.java
new file mode 100644
index 00000000..c7602956
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/VersionPreferenceController.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.car.settings.applications;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+
+import androidx.preference.Preference;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.PreferenceController;
+
+/** Business logic for the Version field in the application details page. */
+public class VersionPreferenceController extends PreferenceController {
+
+ private PackageInfo mPackageInfo;
+
+ public VersionPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ }
+
+ @Override
+ protected Class getPreferenceType() {
+ return Preference.class;
+ }
+
+ /** Set the package info which is used to get the version name. */
+ public void setPackageInfo(PackageInfo packageInfo) {
+ mPackageInfo = packageInfo;
+ }
+
+ @Override
+ protected void checkInitialized() {
+ if (mPackageInfo == null) {
+ throw new IllegalStateException(
+ "PackageInfo should be set before calling this function");
+ }
+ }
+
+ @Override
+ protected void updateState(Preference preference) {
+ preference.setTitle(getContext().getString(
+ R.string.application_version_label, mPackageInfo.versionName));
+ }
+
+ @Override
+ protected int getDefaultAvailabilityStatus() {
+ return AVAILABLE_FOR_VIEWING;
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/appinfo/AppAllServicesPreferenceController.kt b/CarSettings/src/com/android/car/settings/applications/appinfo/AppAllServicesPreferenceController.kt
new file mode 100644
index 00000000..d1b71051
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/appinfo/AppAllServicesPreferenceController.kt
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.settings.applications.appinfo
+
+import android.car.drivingstate.CarUxRestrictions
+import android.content.ActivityNotFoundException
+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.res.Resources
+import android.location.LocationManager
+import androidx.preference.Preference
+import com.android.car.settings.common.FragmentController
+import com.android.car.settings.common.Logger
+import com.android.car.settings.common.PreferenceController
+
+/** Preference Controller for the "All Services" preference in the "App Info" page. */
+class AppAllServicesPreferenceController(
+ context: Context,
+ preferenceKey: String,
+ fragmentController: FragmentController,
+ uxRestrictions: CarUxRestrictions
+) : PreferenceController(context, preferenceKey, fragmentController, uxRestrictions) {
+ private val packageManager = context.packageManager
+ private var packageName: String? = null
+
+ override fun onStartInternal() {
+ getStorageSummary()?.let { preference.summary = it }
+ }
+
+ override fun getPreferenceType(): Class = Preference::class.java
+
+ override fun getDefaultAvailabilityStatus(): Int {
+ return if (canPackageHandleIntent() && isLocationProvider()) {
+ AVAILABLE
+ } else {
+ CONDITIONALLY_UNAVAILABLE
+ }
+ }
+
+ override fun handlePreferenceClicked(preference: Preference): Boolean {
+ startAllServicesActivity()
+ return true
+ }
+
+ /**
+ * Set the package name of the package for which the "All Services" activity needs to be shown.
+ *
+ * @param packageName Name of the package for which the services need to be shown.
+ */
+ fun setPackageName(packageName: String?) {
+ this.packageName = packageName
+ }
+
+ private fun getStorageSummary(): CharSequence? {
+ val resolveInfo = getResolveInfo(PackageManager.GET_META_DATA)
+ if (resolveInfo == null) {
+ LOG.d("mResolveInfo is null.")
+ return null
+ }
+ val metaData = resolveInfo.activityInfo.metaData
+ try {
+ val pkgRes = packageManager.getResourcesForActivity(
+ ComponentName(resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name))
+ return pkgRes.getString(metaData.getInt(SUMMARY_METADATA_KEY))
+ } catch (exception: Resources.NotFoundException) {
+ LOG.d("Resource not found for summary string.")
+ } catch (exception: PackageManager.NameNotFoundException) {
+ LOG.d("Name of resource not found for summary string.")
+ }
+ return null
+ }
+
+ private fun isLocationProvider(): Boolean {
+ val locationManagerService = context.getSystemService(LocationManager::class.java)
+ return packageName?.let {
+ locationManagerService?.isProviderPackage(
+ /* provider = */ null,
+ /* packageName = */ it,
+ /* attributionTag = */ null)
+ } ?: false
+ }
+
+ private fun canPackageHandleIntent(): Boolean = getResolveInfo(/* flags = */ 0) != null
+
+ private fun startAllServicesActivity() {
+ val featuresIntent = Intent(Intent.ACTION_VIEW_APP_FEATURES)
+ // Resolve info won't be null as it is only shown for packages that can
+ // handle the intent
+ val resolveInfo = getResolveInfo(/* flags = */ 0)
+ if (resolveInfo == null) {
+ LOG.e("Resolve info is null, package unable to handle all services intent")
+ return
+ }
+ featuresIntent.component =
+ ComponentName(resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name)
+ LOG.v("Starting the All Services activity with intent:" +
+ featuresIntent.toUri(/* flags = */ 0))
+ try {
+ context.startActivity(featuresIntent)
+ } catch (e: ActivityNotFoundException) {
+ LOG.e("The app cannot handle android.intent.action.VIEW_APP_FEATURES")
+ }
+ }
+
+ private fun getResolveInfo(flags: Int): ResolveInfo? {
+ if (packageName == null) {
+ return null
+ }
+ val featuresIntent = Intent(Intent.ACTION_VIEW_APP_FEATURES).apply {
+ `package` = packageName
+ }
+ return packageManager.resolveActivity(featuresIntent, flags)
+ }
+
+ private companion object {
+ val LOG = Logger(AppAllServicesPreferenceController::class.java)
+ const val SUMMARY_METADATA_KEY = "app_features_preference_summary"
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/appinfo/HibernationSwitchPreferenceController.java b/CarSettings/src/com/android/car/settings/applications/appinfo/HibernationSwitchPreferenceController.java
new file mode 100644
index 00000000..385efcbb
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/appinfo/HibernationSwitchPreferenceController.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.settings.applications.appinfo;
+
+import static android.app.AppOpsManager.MODE_ALLOWED;
+import static android.app.AppOpsManager.MODE_DEFAULT;
+import static android.app.AppOpsManager.MODE_IGNORED;
+import static android.app.AppOpsManager.OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED;
+
+import static com.android.car.settings.applications.ApplicationsUtils.isHibernationEnabled;
+
+import android.app.AppOpsManager;
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.text.TextUtils;
+import android.util.Slog;
+
+import androidx.preference.TwoStatePreference;
+
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.PreferenceController;
+
+/**
+ * A PreferenceController handling the logic for exempting hibernation of app
+ */
+public final class HibernationSwitchPreferenceController
+ extends PreferenceController
+ implements AppOpsManager.OnOpChangedListener {
+ private static final String TAG = "HibernationSwitchPrefController";
+ private String mPackageName;
+ private final AppOpsManager mAppOpsManager;
+ private int mPackageUid;
+ private boolean mIsPackageSet;
+ private boolean mIsPackageExemptByDefault;
+
+ public HibernationSwitchPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController,
+ CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ mAppOpsManager = context.getSystemService(AppOpsManager.class);
+ }
+
+ @Override
+ protected Class getPreferenceType() {
+ return TwoStatePreference.class;
+ }
+
+ @Override
+ public int getDefaultAvailabilityStatus() {
+ return isHibernationEnabled() && mIsPackageSet ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
+ }
+
+ @Override
+ protected void onStartInternal() {
+ if (mIsPackageSet) {
+ mAppOpsManager.startWatchingMode(
+ OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED, mPackageName, this);
+ }
+ }
+
+ @Override
+ protected void onStopInternal() {
+ mAppOpsManager.stopWatchingMode(this);
+ }
+
+ /**
+ * Set the package. And also retrieve details from package manager. Some packages may be
+ * exempted from hibernation by default. This method should only be called to initialize the
+ * controller.
+ * @param packageName The name of the package whose hibernation state to be managed.
+ */
+ public void setPackageName(String packageName) {
+ mPackageName = packageName;
+ PackageManager packageManager = getContext().getPackageManager();
+
+ // Q- packages exempt by default, except R- on Auto since Auto-Revoke was skipped in R
+ int maxTargetSdkVersionForExemptApps = android.os.Build.VERSION_CODES.R;
+ try {
+ mPackageUid = packageManager.getPackageUid(packageName, /* flags= */ 0);
+ mIsPackageExemptByDefault = packageManager.getTargetSdkVersion(packageName)
+ <= maxTargetSdkVersionForExemptApps;
+ mIsPackageSet = true;
+ } catch (PackageManager.NameNotFoundException e) {
+ Slog.w(TAG, "Package [" + mPackageName + "] is not found!");
+ mIsPackageSet = false;
+ }
+ }
+
+ @Override
+ protected void updateState(TwoStatePreference preference) {
+ super.updateState(preference);
+ preference.setChecked(!isPackageHibernationExemptByUser());
+ }
+
+ private boolean isPackageHibernationExemptByUser() {
+ if (!mIsPackageSet) return true;
+ int mode = mAppOpsManager.unsafeCheckOpNoThrow(
+ OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED, mPackageUid, mPackageName);
+
+ return mode == MODE_DEFAULT ? mIsPackageExemptByDefault : mode != MODE_ALLOWED;
+ }
+
+ @Override
+ public void onOpChanged(String op, String packageName) {
+ if (OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED.equals(op)
+ && TextUtils.equals(mPackageName, packageName)) {
+ refreshUi();
+ }
+ }
+
+ @Override
+ protected boolean handlePreferenceChanged(TwoStatePreference preference, Object newValue) {
+ try {
+ mAppOpsManager.setUidMode(OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED, mPackageUid,
+ (boolean) newValue ? MODE_ALLOWED : MODE_IGNORED);
+ } catch (RuntimeException e) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/assist/AssistConfigBasePreferenceController.java b/CarSettings/src/com/android/car/settings/applications/assist/AssistConfigBasePreferenceController.java
new file mode 100644
index 00000000..5f3e7f50
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/assist/AssistConfigBasePreferenceController.java
@@ -0,0 +1,128 @@
+/*
+ * 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.car.settings.applications.assist;
+
+import android.car.drivingstate.CarUxRestrictions;
+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;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.TwoStatePreference;
+
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.PreferenceController;
+import com.android.internal.app.AssistUtils;
+
+import java.util.List;
+
+/** Common logic for preference controllers that configure the assistant's behavior. */
+public abstract class AssistConfigBasePreferenceController extends
+ PreferenceController {
+
+ final SettingObserver mSettingObserver;
+ private final AssistUtils mAssistUtils;
+
+ public AssistConfigBasePreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ this(context, preferenceKey, fragmentController, uxRestrictions, new AssistUtils(context));
+ }
+
+ @VisibleForTesting
+ AssistConfigBasePreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions,
+ AssistUtils assistUtils) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ mAssistUtils = assistUtils;
+ mSettingObserver = new SettingObserver(getSettingUris(), this::refreshUi);
+ }
+
+ @Override
+ protected Class getPreferenceType() {
+ return TwoStatePreference.class;
+ }
+
+ @Override
+ protected int getDefaultAvailabilityStatus() {
+ return mAssistUtils.getAssistComponentForUser(
+ UserHandle.myUserId()) != null ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
+ }
+
+ @Override
+ protected void onStartInternal() {
+ mSettingObserver.register(getContext().getContentResolver(), true);
+ }
+
+ @Override
+ protected void onStopInternal() {
+ mSettingObserver.register(getContext().getContentResolver(), false);
+ }
+
+ /** Gets the Setting Uris that should be observed */
+ protected abstract List getSettingUris();
+
+ /**
+ * Creates an observer that listens for changes to {@link Settings.Secure#ASSISTANT} as well as
+ * any other URI defined by {@link #getSettingUris()}.
+ */
+ @VisibleForTesting
+ static class SettingObserver extends ContentObserver {
+ private static final Uri ASSIST_URI = Settings.Secure.getUriFor(Settings.Secure.ASSISTANT);
+ private final List mUriList;
+ private final Runnable mSettingChangeListener;
+
+ SettingObserver(List uriList, Runnable settingChangeListener) {
+ super(new Handler(Looper.getMainLooper()));
+ mUriList = uriList;
+ mSettingChangeListener = settingChangeListener;
+ }
+
+ /** Registers or unregisters this observer to the given content resolver. */
+ void register(ContentResolver cr, boolean register) {
+ if (register) {
+ cr.registerContentObserver(ASSIST_URI, /* notifyForDescendants= */ false,
+ /* observer= */ this);
+ if (mUriList != null) {
+ for (Uri uri : mUriList) {
+ cr.registerContentObserver(uri, /* notifyForDescendants= */ false,
+ /* observer= */ this);
+ }
+ }
+ } else {
+ cr.unregisterContentObserver(this);
+ }
+ }
+
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ super.onChange(selfChange, uri);
+
+ if (shouldUpdatePreference(uri)) {
+ mSettingChangeListener.run();
+ }
+ }
+
+ private boolean shouldUpdatePreference(Uri uri) {
+ return ASSIST_URI.equals(uri) || (mUriList != null && mUriList.contains(uri));
+ }
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/assist/AssistantAndVoiceEntryPreferenceController.java b/CarSettings/src/com/android/car/settings/applications/assist/AssistantAndVoiceEntryPreferenceController.java
new file mode 100644
index 00000000..faa0ccad
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/assist/AssistantAndVoiceEntryPreferenceController.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.settings.applications.assist;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.ComponentName;
+import android.content.Context;
+
+import androidx.annotation.Nullable;
+import androidx.preference.Preference;
+
+import com.android.car.settings.applications.defaultapps.DefaultAppEntryBasePreferenceController;
+import com.android.car.settings.common.FragmentController;
+import com.android.internal.app.AssistUtils;
+import com.android.settingslib.applications.DefaultAppInfo;
+
+/**
+ * Business logic to show the currently selected default assist.
+ */
+public class AssistantAndVoiceEntryPreferenceController extends
+ DefaultAppEntryBasePreferenceController {
+
+ private final AssistUtils mAssistUtils;
+
+ public AssistantAndVoiceEntryPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ mAssistUtils = new AssistUtils(context);
+ }
+
+ @Override
+ protected Class getPreferenceType() {
+ return Preference.class;
+ }
+
+ @Nullable
+ protected DefaultAppInfo getCurrentDefaultAppInfo() {
+ ComponentName cn = mAssistUtils.getAssistComponentForUser(getCurrentProcessUserId());
+ if (cn == null) {
+ return null;
+ }
+ return new DefaultAppInfo(getContext(), getContext().getPackageManager(),
+ getCurrentProcessUserId(), cn);
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/assist/AssistantAndVoiceFragment.java b/CarSettings/src/com/android/car/settings/applications/assist/AssistantAndVoiceFragment.java
new file mode 100644
index 00000000..a509cf7a
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/assist/AssistantAndVoiceFragment.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.settings.applications.assist;
+
+import android.provider.Settings;
+
+import androidx.annotation.XmlRes;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.SettingsFragment;
+import com.android.car.settings.search.CarBaseSearchIndexProvider;
+import com.android.settingslib.search.SearchIndexable;
+
+/** Assistant management settings screen. */
+@SearchIndexable
+public class AssistantAndVoiceFragment extends SettingsFragment {
+
+ @Override
+ @XmlRes
+ protected int getPreferenceScreenResId() {
+ return R.xml.assistant_and_voice_fragment;
+ }
+
+ /**
+ * Data provider for Settings Search.
+ */
+ public static final CarBaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
+ new CarBaseSearchIndexProvider(R.xml.assistant_and_voice_fragment,
+ Settings.ACTION_VOICE_INPUT_SETTINGS);
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/assist/DefaultVoiceInputPickerEntryPreferenceController.java b/CarSettings/src/com/android/car/settings/applications/assist/DefaultVoiceInputPickerEntryPreferenceController.java
new file mode 100644
index 00000000..aa4c4fbe
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/assist/DefaultVoiceInputPickerEntryPreferenceController.java
@@ -0,0 +1,118 @@
+/*
+ * 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.car.settings.applications.assist;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.Settings;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.car.settings.applications.defaultapps.DefaultAppsPickerEntryBasePreferenceController;
+import com.android.car.settings.common.FragmentController;
+import com.android.internal.app.AssistUtils;
+import com.android.settingslib.applications.DefaultAppInfo;
+
+import java.util.Objects;
+
+/**
+ * Business logic to show the currently selected default voice input service and also link to the
+ * service settings, if it exists.
+ */
+public class DefaultVoiceInputPickerEntryPreferenceController extends
+ DefaultAppsPickerEntryBasePreferenceController {
+
+ private static final Uri ASSIST_URI = Settings.Secure.getUriFor(Settings.Secure.ASSISTANT);
+
+ @VisibleForTesting
+ final ContentObserver mSettingObserver = new ContentObserver(
+ new Handler(Looper.getMainLooper())) {
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ super.onChange(selfChange, uri);
+
+ if (ASSIST_URI.equals(uri)) {
+ refreshUi();
+ }
+ }
+ };
+
+ private final VoiceInputInfoProvider mVoiceInputInfoProvider;
+ private final AssistUtils mAssistUtils;
+
+ public DefaultVoiceInputPickerEntryPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ this(context, preferenceKey, fragmentController, uxRestrictions,
+ new VoiceInputInfoProvider(context), new AssistUtils(context));
+ }
+
+ @VisibleForTesting
+ DefaultVoiceInputPickerEntryPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions,
+ VoiceInputInfoProvider voiceInputInfoProvider, AssistUtils assistUtils) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ mVoiceInputInfoProvider = voiceInputInfoProvider;
+ mAssistUtils = assistUtils;
+ }
+
+ @Override
+ protected int getDefaultAvailabilityStatus() {
+ ComponentName currentVoiceService = VoiceInputUtils.getCurrentService(getContext());
+ ComponentName currentAssist = mAssistUtils.getAssistComponentForUser(
+ getCurrentProcessUserId());
+
+ return Objects.equals(currentAssist, currentVoiceService) ? CONDITIONALLY_UNAVAILABLE
+ : AVAILABLE;
+ }
+
+ @Override
+ protected void onStartInternal() {
+ getContext().getContentResolver().registerContentObserver(ASSIST_URI,
+ /* notifyForDescendants= */ false, mSettingObserver);
+ }
+
+ @Override
+ protected void onStopInternal() {
+ getContext().getContentResolver().unregisterContentObserver(mSettingObserver);
+ }
+
+ @Nullable
+ @Override
+ protected DefaultAppInfo getCurrentDefaultAppInfo() {
+ VoiceInputInfoProvider.VoiceInputInfo info = mVoiceInputInfoProvider.getInfoForComponent(
+ VoiceInputUtils.getCurrentService(getContext()));
+ return (info == null) ? null : new DefaultVoiceInputServiceInfo(getContext(),
+ getContext().getPackageManager(), getCurrentProcessUserId(), info,
+ /* enabled= */ true);
+ }
+
+ @Nullable
+ @Override
+ protected Intent getSettingIntent(@Nullable DefaultAppInfo info) {
+ if (info instanceof DefaultVoiceInputServiceInfo) {
+ return ((DefaultVoiceInputServiceInfo) info).getSettingIntent();
+ }
+ return null;
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/assist/DefaultVoiceInputPickerFragment.java b/CarSettings/src/com/android/car/settings/applications/assist/DefaultVoiceInputPickerFragment.java
new file mode 100644
index 00000000..1460ade6
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/assist/DefaultVoiceInputPickerFragment.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.car.settings.applications.assist;
+
+import androidx.annotation.XmlRes;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.SettingsFragment;
+
+/** Shows the screen to pick the default voice input service. */
+public class DefaultVoiceInputPickerFragment extends SettingsFragment {
+
+ @Override
+ @XmlRes
+ protected int getPreferenceScreenResId() {
+ return R.xml.default_voice_input_picker_fragment;
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/assist/DefaultVoiceInputPickerPreferenceController.java b/CarSettings/src/com/android/car/settings/applications/assist/DefaultVoiceInputPickerPreferenceController.java
new file mode 100644
index 00000000..6b1b9326
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/assist/DefaultVoiceInputPickerPreferenceController.java
@@ -0,0 +1,128 @@
+/*
+ * 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.car.settings.applications.assist;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.ComponentName;
+import android.content.Context;
+import android.provider.Settings;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.car.settings.applications.defaultapps.DefaultAppsPickerBasePreferenceController;
+import com.android.car.settings.common.FragmentController;
+import com.android.internal.app.AssistUtils;
+import com.android.settingslib.applications.DefaultAppInfo;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/** Business logic for displaying and choosing the default voice input service. */
+public class DefaultVoiceInputPickerPreferenceController extends
+ DefaultAppsPickerBasePreferenceController {
+
+ private final AssistUtils mAssistUtils;
+ private final VoiceInputInfoProvider mVoiceInputInfoProvider;
+
+ // Current assistant component name, used to restrict available voice inputs.
+ private String mAssistComponentName = null;
+
+ public DefaultVoiceInputPickerPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ this(context, preferenceKey, fragmentController, uxRestrictions, new AssistUtils(context),
+ new VoiceInputInfoProvider(context));
+ }
+
+ @VisibleForTesting
+ DefaultVoiceInputPickerPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions,
+ AssistUtils assistUtils, VoiceInputInfoProvider voiceInputInfoProvider) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ mAssistUtils = assistUtils;
+ mVoiceInputInfoProvider = voiceInputInfoProvider;
+ if (Objects.equals(mAssistUtils.getAssistComponentForUser(getCurrentProcessUserId()),
+ VoiceInputUtils.getCurrentService(getContext()))) {
+ ComponentName cn = mAssistUtils.getAssistComponentForUser(getCurrentProcessUserId());
+ if (cn != null) {
+ mAssistComponentName = cn.flattenToString();
+ }
+ }
+ }
+
+ @NonNull
+ @Override
+ protected List getCandidates() {
+ List candidates = new ArrayList<>();
+ for (VoiceInputInfoProvider.VoiceInteractionInfo info :
+ mVoiceInputInfoProvider.getVoiceInteractionInfoList()) {
+ boolean enabled = TextUtils.equals(info.getComponentName().flattenToString(),
+ mAssistComponentName);
+ candidates.add(
+ new DefaultVoiceInputServiceInfo(getContext(), getContext().getPackageManager(),
+ getCurrentProcessUserId(), info, enabled));
+ }
+
+ for (VoiceInputInfoProvider.VoiceRecognitionInfo info :
+ mVoiceInputInfoProvider.getVoiceRecognitionInfoList()) {
+ candidates.add(
+ new DefaultVoiceInputServiceInfo(getContext(), getContext().getPackageManager(),
+ getCurrentProcessUserId(), info, /* enabled= */ true));
+ }
+ return candidates;
+ }
+
+ @Override
+ protected String getCurrentDefaultKey() {
+ ComponentName cn = VoiceInputUtils.getCurrentService(getContext());
+ if (cn == null) {
+ return null;
+ }
+ return cn.flattenToString();
+ }
+
+ @Override
+ protected void setCurrentDefault(String key) {
+ ComponentName cn = ComponentName.unflattenFromString(key);
+ VoiceInputInfoProvider.VoiceInputInfo info = mVoiceInputInfoProvider.getInfoForComponent(
+ cn);
+
+ if (info instanceof VoiceInputInfoProvider.VoiceInteractionInfo) {
+ VoiceInputInfoProvider.VoiceInteractionInfo interactionInfo =
+ (VoiceInputInfoProvider.VoiceInteractionInfo) info;
+ Settings.Secure.putString(getContext().getContentResolver(),
+ Settings.Secure.VOICE_INTERACTION_SERVICE, key);
+ Settings.Secure.putString(getContext().getContentResolver(),
+ Settings.Secure.VOICE_RECOGNITION_SERVICE,
+ new ComponentName(interactionInfo.getPackageName(),
+ interactionInfo.getRecognitionService())
+ .flattenToString());
+ } else if (info instanceof VoiceInputInfoProvider.VoiceRecognitionInfo) {
+ Settings.Secure.putString(getContext().getContentResolver(),
+ Settings.Secure.VOICE_INTERACTION_SERVICE, "");
+ Settings.Secure.putString(getContext().getContentResolver(),
+ Settings.Secure.VOICE_RECOGNITION_SERVICE, key);
+ }
+ }
+
+ @Override
+ protected boolean includeNonePreference() {
+ return false;
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/assist/DefaultVoiceInputServiceInfo.java b/CarSettings/src/com/android/car/settings/applications/assist/DefaultVoiceInputServiceInfo.java
new file mode 100644
index 00000000..369281ec
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/assist/DefaultVoiceInputServiceInfo.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.car.settings.applications.assist;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+
+import androidx.annotation.Nullable;
+
+import com.android.car.settings.applications.assist.VoiceInputInfoProvider.VoiceInteractionInfo;
+import com.android.car.settings.applications.assist.VoiceInputInfoProvider.VoiceRecognitionInfo;
+import com.android.settingslib.applications.DefaultAppInfo;
+
+/** An extension of {@link DefaultAppInfo} to help represent voice input services. */
+public class DefaultVoiceInputServiceInfo extends DefaultAppInfo {
+
+ private VoiceInputInfoProvider.VoiceInputInfo mInfo;
+
+ /**
+ * Constructs a {@link DefaultVoiceInputServiceInfo}
+ *
+ * @param info a {@link VoiceInteractionInfo} or {@link VoiceRecognitionInfo} that describes
+ * the Voice Input Service.
+ * @param enabled determines whether the service should be selectable or not.
+ */
+ public DefaultVoiceInputServiceInfo(Context context, PackageManager pm, int userId,
+ VoiceInputInfoProvider.VoiceInputInfo info, boolean enabled) {
+ super(context, pm, userId, info.getComponentName(), /* summary= */ null, enabled);
+ mInfo = info;
+ }
+
+ @Override
+ public CharSequence loadLabel() {
+ return mInfo.getLabel();
+ }
+
+ /** Gets the intent to open the related settings component if it exists. */
+ @Nullable
+ public Intent getSettingIntent() {
+ if (mInfo.getSettingsActivityComponentName() == null) {
+ return null;
+ }
+ return new Intent(Intent.ACTION_MAIN).setComponent(
+ mInfo.getSettingsActivityComponentName());
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/assist/ScreenshotContextPreferenceController.java b/CarSettings/src/com/android/car/settings/applications/assist/ScreenshotContextPreferenceController.java
new file mode 100644
index 00000000..eadad84c
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/assist/ScreenshotContextPreferenceController.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.car.settings.applications.assist;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+import android.net.Uri;
+import android.provider.Settings;
+
+import androidx.preference.TwoStatePreference;
+
+import com.android.car.settings.common.FragmentController;
+
+import java.util.Arrays;
+import java.util.List;
+
+/** Toggles the assistant's ability to use a screenshot of the screen for context. */
+public class ScreenshotContextPreferenceController extends AssistConfigBasePreferenceController {
+
+ public ScreenshotContextPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ }
+
+ @Override
+ protected void updateState(TwoStatePreference preference) {
+ boolean checked = Settings.Secure.getInt(getContext().getContentResolver(),
+ Settings.Secure.ASSIST_SCREENSHOT_ENABLED, 1) != 0;
+ preference.setChecked(checked);
+
+ boolean contextChecked = Settings.Secure.getInt(getContext().getContentResolver(),
+ Settings.Secure.ASSIST_STRUCTURE_ENABLED, 1) != 0;
+ preference.setEnabled(contextChecked);
+ }
+
+ @Override
+ protected boolean handlePreferenceChanged(TwoStatePreference preference, Object newValue) {
+ Settings.Secure.putInt(getContext().getContentResolver(),
+ Settings.Secure.ASSIST_SCREENSHOT_ENABLED, (boolean) newValue ? 1 : 0);
+ return true;
+ }
+
+ @Override
+ protected List getSettingUris() {
+ return Arrays.asList(
+ Settings.Secure.getUriFor(Settings.Secure.ASSIST_SCREENSHOT_ENABLED),
+ Settings.Secure.getUriFor(Settings.Secure.ASSIST_STRUCTURE_ENABLED));
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/assist/TextContextPreferenceController.java b/CarSettings/src/com/android/car/settings/applications/assist/TextContextPreferenceController.java
new file mode 100644
index 00000000..336d5f7c
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/assist/TextContextPreferenceController.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.car.settings.applications.assist;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+import android.net.Uri;
+import android.provider.Settings;
+
+import androidx.preference.TwoStatePreference;
+
+import com.android.car.settings.common.FragmentController;
+
+import java.util.Collections;
+import java.util.List;
+
+/** Toggles the assistant's ability to use the text on the screen for context. */
+public class TextContextPreferenceController extends AssistConfigBasePreferenceController {
+
+ public TextContextPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ }
+
+ @Override
+ protected void updateState(TwoStatePreference preference) {
+ preference.setChecked(isAssistContextEnabled());
+ }
+
+ @Override
+ protected boolean handlePreferenceChanged(TwoStatePreference preference, Object newValue) {
+ Settings.Secure.putInt(getContext().getContentResolver(),
+ Settings.Secure.ASSIST_STRUCTURE_ENABLED, (boolean) newValue ? 1 : 0);
+ return true;
+ }
+
+ @Override
+ protected List getSettingUris() {
+ return Collections.singletonList(
+ Settings.Secure.getUriFor(Settings.Secure.ASSIST_STRUCTURE_ENABLED));
+ }
+
+ private boolean isAssistContextEnabled() {
+ return Settings.Secure.getInt(getContext().getContentResolver(),
+ Settings.Secure.ASSIST_STRUCTURE_ENABLED, 1) != 0;
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/assist/VoiceInputInfoProvider.java b/CarSettings/src/com/android/car/settings/applications/assist/VoiceInputInfoProvider.java
new file mode 100644
index 00000000..7c79b30b
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/assist/VoiceInputInfoProvider.java
@@ -0,0 +1,310 @@
+/*
+ * 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.car.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.content.pm.ServiceInfo;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.service.voice.VoiceInteractionService;
+import android.service.voice.VoiceInteractionServiceInfo;
+import android.speech.RecognitionService;
+import android.util.AttributeSet;
+import android.util.Xml;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.collection.ArrayMap;
+import androidx.collection.ArraySet;
+
+import com.android.car.settings.common.Logger;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Extracts the voice interaction services and voice recognition services and converts them into
+ * {@link VoiceInteractionInfo} instances and {@link VoiceRecognitionInfo} instances.
+ */
+public class VoiceInputInfoProvider {
+
+ private static final Logger LOG = new Logger(VoiceInputInfoProvider.class);
+ @VisibleForTesting
+ static final Intent VOICE_INTERACTION_SERVICE_TAG = new Intent(
+ VoiceInteractionService.SERVICE_INTERFACE);
+ @VisibleForTesting
+ static final Intent VOICE_RECOGNITION_SERVICE_TAG = new Intent(
+ RecognitionService.SERVICE_INTERFACE);
+
+ private final Context mContext;
+ private final Map mComponentToInfoMap = new ArrayMap<>();
+ private final List mVoiceInteractionInfoList = new ArrayList<>();
+ private final List mVoiceRecognitionInfoList = new ArrayList<>();
+ private final Set mRecognitionServiceNames = new ArraySet<>();
+
+ public VoiceInputInfoProvider(Context context) {
+ mContext = context;
+
+ loadVoiceInteractionServices();
+ loadVoiceRecognitionServices();
+ }
+
+ /**
+ * Gets the list of voice interaction services represented as {@link VoiceInteractionInfo}
+ * instances.
+ */
+ public List getVoiceInteractionInfoList() {
+ return mVoiceInteractionInfoList;
+ }
+
+ /**
+ * Gets the list of voice recognition services represented as {@link VoiceRecognitionInfo}
+ * instances.
+ */
+ public List getVoiceRecognitionInfoList() {
+ return mVoiceRecognitionInfoList;
+ }
+
+ /**
+ * Returns the appropriate {@link VoiceInteractionInfo} or {@link VoiceRecognitionInfo} based on
+ * the provided {@link ComponentName}.
+ *
+ * @return {@link VoiceInputInfo} if it exists for the component name, null otherwise.
+ */
+ @Nullable
+ public VoiceInputInfo getInfoForComponent(ComponentName key) {
+ return mComponentToInfoMap.getOrDefault(key, null);
+ }
+
+ private void loadVoiceInteractionServices() {
+ List mAvailableVoiceInteractionServices =
+ mContext.getPackageManager().queryIntentServices(VOICE_INTERACTION_SERVICE_TAG,
+ PackageManager.GET_META_DATA);
+
+ for (ResolveInfo resolveInfo : mAvailableVoiceInteractionServices) {
+ VoiceInteractionServiceInfo interactionServiceInfo = new VoiceInteractionServiceInfo(
+ mContext.getPackageManager(), resolveInfo.serviceInfo);
+ if (hasParseError(interactionServiceInfo)) {
+ LOG.w("Error in VoiceInteractionService " + resolveInfo.serviceInfo.packageName
+ + "/" + resolveInfo.serviceInfo.name + ": "
+ + interactionServiceInfo.getParseError());
+ continue;
+ }
+ VoiceInteractionInfo voiceInteractionInfo = new VoiceInteractionInfo(mContext,
+ interactionServiceInfo);
+ mVoiceInteractionInfoList.add(voiceInteractionInfo);
+ if (interactionServiceInfo.getRecognitionService() != null) {
+ mRecognitionServiceNames.add(new ComponentName(resolveInfo.serviceInfo.packageName,
+ interactionServiceInfo.getRecognitionService()));
+ }
+ mComponentToInfoMap.put(new ComponentName(resolveInfo.serviceInfo.packageName,
+ resolveInfo.serviceInfo.name), voiceInteractionInfo);
+ }
+ Collections.sort(mVoiceInteractionInfoList);
+ }
+
+ private void loadVoiceRecognitionServices() {
+ List mAvailableRecognitionServices =
+ mContext.getPackageManager().queryIntentServices(VOICE_RECOGNITION_SERVICE_TAG,
+ PackageManager.GET_META_DATA);
+ for (ResolveInfo resolveInfo : mAvailableRecognitionServices) {
+ ComponentName componentName = new ComponentName(resolveInfo.serviceInfo.packageName,
+ resolveInfo.serviceInfo.name);
+
+ VoiceRecognitionInfo voiceRecognitionInfo = new VoiceRecognitionInfo(mContext,
+ resolveInfo.serviceInfo);
+ mVoiceRecognitionInfoList.add(voiceRecognitionInfo);
+ mRecognitionServiceNames.add(componentName);
+ mComponentToInfoMap.put(componentName, voiceRecognitionInfo);
+ }
+ Collections.sort(mVoiceRecognitionInfoList);
+ }
+
+ @VisibleForTesting
+ boolean hasParseError(VoiceInteractionServiceInfo voiceInteractionServiceInfo) {
+ return voiceInteractionServiceInfo.getParseError() != null;
+ }
+
+ /**
+ * Base object used to represent {@link VoiceInteractionInfo} and {@link VoiceRecognitionInfo}.
+ */
+ abstract static class VoiceInputInfo implements Comparable {
+ private final Context mContext;
+ private final ServiceInfo mServiceInfo;
+
+ VoiceInputInfo(Context context, ServiceInfo serviceInfo) {
+ mContext = context;
+ mServiceInfo = serviceInfo;
+ }
+
+ protected Context getContext() {
+ return mContext;
+ }
+
+ protected ServiceInfo getServiceInfo() {
+ return mServiceInfo;
+ }
+
+ @Override
+ public int compareTo(VoiceInputInfo o) {
+ return getTag().toString().compareTo(o.getTag().toString());
+ }
+
+ /**
+ * Returns the {@link ComponentName} which represents the settings activity, if it exists.
+ */
+ @Nullable
+ ComponentName getSettingsActivityComponentName() {
+ String activity = getSettingsActivity();
+ return (activity != null) ? new ComponentName(mServiceInfo.packageName, activity)
+ : null;
+ }
+
+ /** Returns the package name for the service represented by this {@link VoiceInputInfo}. */
+ String getPackageName() {
+ return mServiceInfo.packageName;
+ }
+
+ /**
+ * Returns the component name for the service represented by this {@link VoiceInputInfo}.
+ */
+ ComponentName getComponentName() {
+ return new ComponentName(mServiceInfo.packageName, mServiceInfo.name);
+ }
+
+ /**
+ * Returns the label to describe the service represented by this {@link VoiceInputInfo}.
+ */
+ abstract CharSequence getLabel();
+
+ /**
+ * The string representation of the settings activity for the service represented by this
+ * {@link VoiceInputInfo}.
+ */
+ protected abstract String getSettingsActivity();
+
+ /**
+ * Returns a tag used to determine the sort order of the {@link VoiceInputInfo} instances.
+ */
+ protected CharSequence getTag() {
+ return mServiceInfo.loadLabel(mContext.getPackageManager());
+ }
+ }
+
+ /** An object to represent {@link VoiceInteractionService} instances. */
+ static class VoiceInteractionInfo extends VoiceInputInfo {
+ private final VoiceInteractionServiceInfo mInteractionServiceInfo;
+
+ VoiceInteractionInfo(Context context, VoiceInteractionServiceInfo info) {
+ super(context, info.getServiceInfo());
+
+ mInteractionServiceInfo = info;
+ }
+
+ /** Returns the recognition service associated with this {@link VoiceInteractionService}. */
+ String getRecognitionService() {
+ return mInteractionServiceInfo.getRecognitionService();
+ }
+
+ @Override
+ protected String getSettingsActivity() {
+ return mInteractionServiceInfo.getSettingsActivity();
+ }
+
+ @Override
+ CharSequence getLabel() {
+ return getServiceInfo().applicationInfo.loadLabel(getContext().getPackageManager());
+ }
+ }
+
+ /** An object to represent {@link RecognitionService} instances. */
+ static class VoiceRecognitionInfo extends VoiceInputInfo {
+
+ VoiceRecognitionInfo(Context context, ServiceInfo serviceInfo) {
+ super(context, serviceInfo);
+ }
+
+ @Override
+ protected String getSettingsActivity() {
+ return getServiceSettingsActivity(getServiceInfo());
+ }
+
+ @Override
+ CharSequence getLabel() {
+ return getTag();
+ }
+
+ private String getServiceSettingsActivity(ServiceInfo serviceInfo) {
+ XmlResourceParser parser = null;
+ String settingActivity = null;
+ try {
+ parser = serviceInfo.loadXmlMetaData(getContext().getPackageManager(),
+ RecognitionService.SERVICE_META_DATA);
+ if (parser == null) {
+ throw new XmlPullParserException(
+ "No " + RecognitionService.SERVICE_META_DATA + " meta-data for "
+ + serviceInfo.packageName);
+ }
+
+ Resources res = getContext().getPackageManager().getResourcesForApplication(
+ serviceInfo.applicationInfo);
+
+ AttributeSet attrs = Xml.asAttributeSet(parser);
+
+ int type;
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && type != XmlPullParser.START_TAG) {
+ continue;
+ }
+
+ String nodeName = parser.getName();
+ if (!"recognition-service".equals(nodeName)) {
+ throw new XmlPullParserException(
+ "Meta-data does not start with recognition-service tag");
+ }
+
+ TypedArray array = res.obtainAttributes(attrs,
+ com.android.internal.R.styleable.RecognitionService);
+ settingActivity = array.getString(
+ com.android.internal.R.styleable.RecognitionService_settingsActivity);
+ array.recycle();
+ } catch (XmlPullParserException e) {
+ LOG.e("error parsing recognition service meta-data", e);
+ } catch (IOException e) {
+ LOG.e("error parsing recognition service meta-data", e);
+ } catch (PackageManager.NameNotFoundException e) {
+ LOG.e("error parsing recognition service meta-data", e);
+ } finally {
+ if (parser != null) parser.close();
+ }
+
+ return settingActivity;
+ }
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/assist/VoiceInputUtils.java b/CarSettings/src/com/android/car/settings/applications/assist/VoiceInputUtils.java
new file mode 100644
index 00000000..0098fd89
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/assist/VoiceInputUtils.java
@@ -0,0 +1,57 @@
+/*
+ * 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.car.settings.applications.assist;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.provider.Settings;
+import android.text.TextUtils;
+
+/** Utilities to help interact with voice input services. */
+final class VoiceInputUtils {
+
+ private VoiceInputUtils() {
+ }
+
+ /**
+ * Chooses the current service based on the current voice interaction service and current
+ * recognizer service.
+ */
+ static ComponentName getCurrentService(Context context) {
+ ComponentName currentVoiceInteraction = getComponentNameOrNull(context,
+ Settings.Secure.VOICE_INTERACTION_SERVICE);
+ ComponentName currentVoiceRecognizer = getComponentNameOrNull(context,
+ Settings.Secure.VOICE_RECOGNITION_SERVICE);
+
+ if (currentVoiceInteraction != null) {
+ return currentVoiceInteraction;
+ } else if (currentVoiceRecognizer != null) {
+ return currentVoiceRecognizer;
+ } else {
+ return null;
+ }
+ }
+
+ private static ComponentName getComponentNameOrNull(Context context, String secureSettingKey) {
+ String currentSetting = Settings.Secure.getString(context.getContentResolver(),
+ secureSettingKey);
+ if (!TextUtils.isEmpty(currentSetting)) {
+ return ComponentName.unflattenFromString(currentSetting);
+ }
+ return null;
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/defaultapps/DefaultAppEntryBasePreferenceController.java b/CarSettings/src/com/android/car/settings/applications/defaultapps/DefaultAppEntryBasePreferenceController.java
new file mode 100644
index 00000000..82a72d00
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/defaultapps/DefaultAppEntryBasePreferenceController.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.car.settings.applications.defaultapps;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.UserHandle;
+import android.text.TextUtils;
+
+import androidx.annotation.Nullable;
+import androidx.preference.Preference;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.Logger;
+import com.android.car.settings.common.PreferenceController;
+import com.android.car.ui.preference.CarUiPreference;
+import com.android.settingslib.applications.DefaultAppInfo;
+
+/**
+ * Base preference which handles the logic to display the currently selected default app.
+ *
+ * @param the upper bound on the type of {@link Preference} on which the controller expects to
+ * operate.
+ */
+public abstract class DefaultAppEntryBasePreferenceController extends
+ PreferenceController {
+
+ private static final Logger LOG = new Logger(
+ DefaultAppEntryBasePreferenceController.class);
+
+ public DefaultAppEntryBasePreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ }
+
+ @Override
+ protected void updateState(V preference) {
+ CharSequence defaultAppLabel = getDefaultAppLabel();
+ if (!TextUtils.isEmpty(defaultAppLabel)) {
+ preference.setSummary(defaultAppLabel);
+ preference.setIcon(DefaultAppUtils.getSafeIcon(getDefaultAppIcon(),
+ getContext().getResources().getInteger(R.integer.default_app_safe_icon_size)));
+ } else {
+ LOG.d("No default app");
+ preference.setSummary(R.string.app_list_preference_none);
+ preference.setIcon(null);
+ if (preference instanceof CarUiPreference) {
+ ((CarUiPreference) preference).setShowChevron(false);
+ }
+ }
+ }
+
+ /** Specifies the currently selected default app. */
+ @Nullable
+ protected abstract DefaultAppInfo getCurrentDefaultAppInfo();
+
+ /** Gets the current process user id. */
+ protected int getCurrentProcessUserId() {
+ return UserHandle.myUserId();
+ }
+
+ private Drawable getDefaultAppIcon() {
+ DefaultAppInfo app = getCurrentDefaultAppInfo();
+ if (app != null) {
+ return app.loadIcon();
+ }
+ return null;
+ }
+
+ private CharSequence getDefaultAppLabel() {
+ DefaultAppInfo app = getCurrentDefaultAppInfo();
+ if (app != null) {
+ return app.loadLabel();
+ }
+ return null;
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/defaultapps/DefaultAppUtils.java b/CarSettings/src/com/android/car/settings/applications/defaultapps/DefaultAppUtils.java
new file mode 100644
index 00000000..9cfb3218
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/defaultapps/DefaultAppUtils.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.car.settings.applications.defaultapps;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.VectorDrawable;
+
+/** Utilities related to default apps. */
+public class DefaultAppUtils {
+
+ private DefaultAppUtils() {
+ }
+
+ /** Scales the icon to a maximum size to avoid crashing Settings if it is too big. */
+ public static Drawable getSafeIcon(Drawable icon, int maxDimension) {
+ Drawable safeIcon = icon;
+ if ((icon != null) && !(icon instanceof VectorDrawable)) {
+ safeIcon = getSafeDrawable(icon, maxDimension);
+ }
+ return safeIcon;
+ }
+
+ /**
+ * Gets a drawable with a limited size to avoid crashing Settings if it's too big.
+ *
+ * @param original original drawable, typically an app icon.
+ * @param maxDimension maximum width/height, in pixels.
+ */
+ private static Drawable getSafeDrawable(Drawable original, int maxDimension) {
+ int actualWidth = original.getMinimumWidth();
+ int actualHeight = original.getMinimumHeight();
+
+ if (actualWidth <= maxDimension && actualHeight <= maxDimension) {
+ return original;
+ }
+
+ float scaleWidth = ((float) maxDimension) / actualWidth;
+ float scaleHeight = ((float) maxDimension) / actualHeight;
+ float scale = Math.min(scaleWidth, scaleHeight);
+ int width = (int) (actualWidth * scale);
+ int height = (int) (actualHeight * scale);
+
+ Bitmap bitmap;
+ if (original instanceof BitmapDrawable) {
+ Bitmap originalBitmap = ((BitmapDrawable) original).getBitmap();
+ bitmap = Bitmap.createScaledBitmap(originalBitmap, width, height, false);
+
+ // When a new BitmapDrawable is created, it defaults to DisplayMetrics.DENSITY_DEFAULT
+ // density. Depending on the original bitmap density, this could increase the
+ // intrinsic height/width of the new BitmapDrawable.
+ BitmapDrawable scaledBitmap = new BitmapDrawable(/* res= */ null, bitmap);
+ scaledBitmap.setTargetDensity(originalBitmap.getDensity());
+ return scaledBitmap;
+ } else {
+ bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ original.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+ original.draw(canvas);
+ }
+ return new BitmapDrawable(/* res= */ null, bitmap);
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/defaultapps/DefaultAppsPickerBasePreferenceController.java b/CarSettings/src/com/android/car/settings/applications/defaultapps/DefaultAppsPickerBasePreferenceController.java
new file mode 100644
index 00000000..4cc06a8e
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/defaultapps/DefaultAppsPickerBasePreferenceController.java
@@ -0,0 +1,172 @@
+/*
+ * 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.car.settings.applications.defaultapps;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+import android.os.UserHandle;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+import androidx.preference.TwoStatePreference;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.ConfirmationDialogFragment;
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.GroupSelectionPreferenceController;
+import com.android.car.settings.common.Logger;
+import com.android.car.ui.preference.CarUiRadioButtonPreference;
+import com.android.settingslib.applications.DefaultAppInfo;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** Defines the shared logic in picking a default application. */
+public abstract class DefaultAppsPickerBasePreferenceController extends
+ GroupSelectionPreferenceController {
+
+ private static final Logger LOG = new Logger(DefaultAppsPickerBasePreferenceController.class);
+ private static final String DIALOG_KEY_ARG = "key_arg";
+ protected static final String NONE_PREFERENCE_KEY = "";
+
+ private final Map mDefaultAppInfoMap = new HashMap<>();
+ private final ConfirmationDialogFragment.ConfirmListener mConfirmListener = arguments -> {
+ setCurrentDefault(arguments.getString(DIALOG_KEY_ARG));
+ notifyCheckedKeyChanged();
+ };
+
+ public DefaultAppsPickerBasePreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ }
+
+ @Override
+ protected void onCreateInternal() {
+ ConfirmationDialogFragment.resetListeners(
+ (ConfirmationDialogFragment) getFragmentController().findDialogByTag(
+ ConfirmationDialogFragment.TAG),
+ mConfirmListener,
+ /* rejectListener= */ null,
+ /* neutralListener= */ null);
+ }
+
+ @Override
+ @NonNull
+ protected List getGroupPreferences() {
+ List entries = new ArrayList<>();
+ if (includeNonePreference()) {
+ entries.add(createNoneOption());
+ }
+
+ List currentCandidates = getCandidates();
+ if (currentCandidates != null) {
+ for (DefaultAppInfo info : currentCandidates) {
+ mDefaultAppInfoMap.put(info.getKey(), info);
+ entries.add(createOption(info));
+ }
+ } else {
+ LOG.i("no candidate provided");
+ }
+
+ return entries;
+ }
+
+ @Override
+ protected final boolean handleGroupItemSelected(TwoStatePreference preference) {
+ String selectedKey = preference.getKey();
+ if (TextUtils.equals(selectedKey, getCurrentCheckedKey())) {
+ return false;
+ }
+
+ CharSequence message = getConfirmationMessage(mDefaultAppInfoMap.get(selectedKey));
+ if (!TextUtils.isEmpty(message)) {
+ ConfirmationDialogFragment dialogFragment =
+ new ConfirmationDialogFragment.Builder(getContext())
+ .setMessage(message.toString())
+ .setPositiveButton(android.R.string.ok, mConfirmListener)
+ .setNegativeButton(android.R.string.cancel, /* rejectListener= */ null)
+ .addArgumentString(DIALOG_KEY_ARG, selectedKey)
+ .build();
+ getFragmentController().showDialog(dialogFragment, ConfirmationDialogFragment.TAG);
+ return false;
+ }
+
+ setCurrentDefault(selectedKey);
+ return true;
+ }
+
+ @Override
+ protected final String getCurrentCheckedKey() {
+ return getCurrentDefaultKey();
+ }
+
+ protected TwoStatePreference createOption(DefaultAppInfo info) {
+ CarUiRadioButtonPreference preference = new CarUiRadioButtonPreference(getContext());
+ preference.setKey(info.getKey());
+ preference.setTitle(info.loadLabel());
+ preference.setIcon(DefaultAppUtils.getSafeIcon(info.loadIcon(),
+ getContext().getResources().getInteger(R.integer.default_app_safe_icon_size)));
+ preference.setEnabled(info.enabled);
+ return preference;
+ }
+
+ /** Gets all of the candidates that should be considered when choosing a default application. */
+ @NonNull
+ protected abstract List getCandidates();
+
+ /** Gets the key of the currently selected candidate. */
+ protected abstract String getCurrentDefaultKey();
+
+ /**
+ * Sets the key of the currently selected candidate. The implementation of this method should
+ * modify the value returned by {@link #getCurrentDefaultKey()}}.
+ *
+ * @param key represents the key from {@link DefaultAppInfo} which should mark the default
+ * application.
+ */
+ protected abstract void setCurrentDefault(String key);
+
+ /**
+ * Defines the warning dialog message to be shown when a default app is selected.
+ */
+ protected CharSequence getConfirmationMessage(DefaultAppInfo info) {
+ return null;
+ }
+
+ /** Gets the current process user id. */
+ protected int getCurrentProcessUserId() {
+ return UserHandle.myUserId();
+ }
+
+ /**
+ * Determines whether the list of default apps should include "none". Implementation classes can
+ * override this value to {@code false} in order to remove the "none" preference.
+ */
+ protected boolean includeNonePreference() {
+ return true;
+ }
+
+ private CarUiRadioButtonPreference createNoneOption() {
+ CarUiRadioButtonPreference preference = new CarUiRadioButtonPreference(getContext());
+ preference.setKey(NONE_PREFERENCE_KEY);
+ preference.setTitle(R.string.app_list_preference_none);
+ preference.setIcon(R.drawable.ic_remove_circle);
+ return preference;
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/defaultapps/DefaultAppsPickerEntryBasePreferenceController.java b/CarSettings/src/com/android/car/settings/applications/defaultapps/DefaultAppsPickerEntryBasePreferenceController.java
new file mode 100644
index 00000000..fda215f6
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/defaultapps/DefaultAppsPickerEntryBasePreferenceController.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.car.settings.applications.defaultapps;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.annotation.Nullable;
+
+import com.android.car.settings.common.FragmentController;
+import com.android.car.ui.preference.CarUiTwoActionIconPreference;
+import com.android.settingslib.applications.DefaultAppInfo;
+
+/**
+ * Base preference which handles the logic to display the currently selected default app as well as
+ * an option to navigate to the settings of the selected default app.
+ */
+public abstract class DefaultAppsPickerEntryBasePreferenceController extends
+ DefaultAppEntryBasePreferenceController {
+
+ public DefaultAppsPickerEntryBasePreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ }
+
+ @Override
+ protected Class getPreferenceType() {
+ return CarUiTwoActionIconPreference.class;
+ }
+
+ @Override
+ protected void updateState(CarUiTwoActionIconPreference preference) {
+ super.updateState(preference);
+
+ // If activity does not exist, return. Otherwise allow intenting to the activity.
+ Intent intent = getSettingIntent(getCurrentDefaultAppInfo());
+ if (intent == null || intent.resolveActivityInfo(
+ getContext().getPackageManager(), intent.getFlags()) == null) {
+ preference.setSecondaryActionVisible(false);
+ return;
+ }
+
+ // Use startActivityForResult because some apps need to check the identity of the caller.
+ preference.setOnSecondaryActionClickListener(() -> {
+ getContext().startActivityForResult(
+ getContext().getBasePackageName(),
+ intent,
+ /* requestCode= */ 0,
+ /* options= */ null);
+ });
+ preference.setSecondaryActionVisible(true);
+ }
+
+ /**
+ * Returns an optional intent that will be launched when clicking the secondary action icon.
+ */
+ @Nullable
+ protected Intent getSettingIntent(@Nullable DefaultAppInfo info) {
+ return null;
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/defaultapps/DefaultAssistantPickerEntryPreferenceController.java b/CarSettings/src/com/android/car/settings/applications/defaultapps/DefaultAssistantPickerEntryPreferenceController.java
new file mode 100644
index 00000000..8f1b6092
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/defaultapps/DefaultAssistantPickerEntryPreferenceController.java
@@ -0,0 +1,124 @@
+/*
+ * 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.car.settings.applications.defaultapps;
+
+import android.app.role.RoleManager;
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.service.voice.VoiceInteractionService;
+import android.service.voice.VoiceInteractionServiceInfo;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.car.settings.common.FragmentController;
+import com.android.car.ui.preference.CarUiTwoActionIconPreference;
+import com.android.internal.util.CollectionUtils;
+import com.android.settingslib.applications.DefaultAppInfo;
+
+import java.util.List;
+
+/**
+ * Business logic to show the currently selected default assistant and also show the assistant
+ * settings, if it exists.
+ */
+public class DefaultAssistantPickerEntryPreferenceController extends
+ DefaultAppsPickerEntryBasePreferenceController {
+
+ private static final Intent ASSISTANT_SERVICE = new Intent(
+ VoiceInteractionService.SERVICE_INTERFACE);
+
+ private final RoleManager mRoleManager;
+
+ public DefaultAssistantPickerEntryPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ mRoleManager = getContext().getSystemService(RoleManager.class);
+ }
+
+ @Nullable
+ @Override
+ protected DefaultAppInfo getCurrentDefaultAppInfo() {
+ ComponentName cn = getComponentName();
+ if (cn == null) {
+ return null;
+ }
+
+ return new DefaultAppInfo(getContext(), getContext().getPackageManager(),
+ getCurrentProcessUserId(), cn);
+ }
+
+ @Override
+ protected boolean handlePreferenceClicked(CarUiTwoActionIconPreference preference) {
+ String packageName = getContext().getPackageManager().getPermissionControllerPackageName();
+ if (packageName != null) {
+ Intent intent = new Intent(Intent.ACTION_MANAGE_DEFAULT_APP)
+ .setPackage(packageName)
+ .putExtra(Intent.EXTRA_ROLE_NAME, RoleManager.ROLE_ASSISTANT);
+ getContext().startActivity(intent);
+ }
+ return true;
+ }
+
+ @Nullable
+ @Override
+ protected Intent getSettingIntent(@Nullable DefaultAppInfo info) {
+ ComponentName cn = getComponentName();
+ if (cn == null) {
+ return null;
+ }
+
+ return new Intent(Intent.ACTION_MAIN).setComponent(cn);
+ }
+
+ private ComponentName getComponentName() {
+ String assistantPkgName = CollectionUtils.firstOrNull(
+ mRoleManager.getRoleHolders(RoleManager.ROLE_ASSISTANT));
+
+ if (assistantPkgName == null) {
+ return null;
+ }
+
+ Intent probe = ASSISTANT_SERVICE.setPackage(assistantPkgName);
+ PackageManager pm = getContext().getPackageManager();
+ List services = pm.queryIntentServices(probe, PackageManager.GET_META_DATA);
+ if (services == null || services.isEmpty()) {
+ return null;
+ }
+
+ String activity = getAssistSettingsActivity(pm, services.get(0));
+ if (activity == null) {
+ return null;
+ }
+
+ return new ComponentName(assistantPkgName, activity);
+ }
+
+ @VisibleForTesting
+ String getAssistSettingsActivity(PackageManager pm, ResolveInfo resolveInfo) {
+ VoiceInteractionServiceInfo voiceInfo = new VoiceInteractionServiceInfo(pm,
+ resolveInfo.serviceInfo);
+ if (!voiceInfo.getSupportsAssist()) {
+ return null;
+ }
+ return voiceInfo.getSettingsActivity();
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/defaultapps/DefaultAutofillPickerEntryPreferenceController.java b/CarSettings/src/com/android/car/settings/applications/defaultapps/DefaultAutofillPickerEntryPreferenceController.java
new file mode 100644
index 00000000..2f8d12c4
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/defaultapps/DefaultAutofillPickerEntryPreferenceController.java
@@ -0,0 +1,111 @@
+/*
+ * 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.car.settings.applications.defaultapps;
+
+import android.car.drivingstate.CarUxRestrictions;
+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.ServiceInfo;
+import android.provider.Settings;
+import android.service.autofill.AutofillService;
+import android.service.autofill.AutofillServiceInfo;
+import android.text.TextUtils;
+import android.view.autofill.AutofillManager;
+
+import androidx.annotation.Nullable;
+
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.Logger;
+import com.android.settingslib.applications.DefaultAppInfo;
+
+import java.util.List;
+
+/** Business logic for displaying the currently selected autofill app. */
+public class DefaultAutofillPickerEntryPreferenceController extends
+ DefaultAppsPickerEntryBasePreferenceController {
+
+ private static final Logger LOG = new Logger(
+ DefaultAutofillPickerEntryPreferenceController.class);
+ private final AutofillManager mAutofillManager;
+
+ public DefaultAutofillPickerEntryPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ mAutofillManager = context.getSystemService(AutofillManager.class);
+ }
+
+ @Override
+ protected int getDefaultAvailabilityStatus() {
+ if (mAutofillManager != null && mAutofillManager.isAutofillSupported()) {
+ return AVAILABLE;
+ }
+ return UNSUPPORTED_ON_DEVICE;
+ }
+
+ @Nullable
+ @Override
+ protected DefaultAppInfo getCurrentDefaultAppInfo() {
+ String flattenComponent = Settings.Secure.getString(getContext().getContentResolver(),
+ Settings.Secure.AUTOFILL_SERVICE);
+ if (!TextUtils.isEmpty(flattenComponent)) {
+ DefaultAppInfo appInfo = new DefaultAppInfo(getContext(),
+ getContext().getPackageManager(), getCurrentProcessUserId(),
+ ComponentName.unflattenFromString(flattenComponent));
+ return appInfo;
+ }
+ return null;
+ }
+
+ @Nullable
+ @Override
+ protected Intent getSettingIntent(@Nullable DefaultAppInfo info) {
+ if (info == null) {
+ return null;
+ }
+
+ Intent intent = new Intent(AutofillService.SERVICE_INTERFACE);
+ List resolveInfos = getContext().getPackageManager().queryIntentServices(
+ intent, PackageManager.GET_META_DATA);
+
+ for (ResolveInfo resolveInfo : resolveInfos) {
+ ServiceInfo serviceInfo = resolveInfo.serviceInfo;
+ String flattenKey = new ComponentName(serviceInfo.packageName,
+ serviceInfo.name).flattenToString();
+ if (TextUtils.equals(info.getKey(), flattenKey)) {
+ String settingsActivity;
+ try {
+ settingsActivity = new AutofillServiceInfo(getContext(), serviceInfo)
+ .getSettingsActivity();
+ } catch (SecurityException e) {
+ // Service does not declare the proper permission, ignore it.
+ LOG.w("Error getting info for " + serviceInfo + ": " + e);
+ continue;
+ }
+ if (TextUtils.isEmpty(settingsActivity)) {
+ continue;
+ }
+ return new Intent(Intent.ACTION_MAIN).setComponent(
+ new ComponentName(serviceInfo.packageName, settingsActivity));
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/defaultapps/DefaultAutofillPickerFragment.java b/CarSettings/src/com/android/car/settings/applications/defaultapps/DefaultAutofillPickerFragment.java
new file mode 100644
index 00000000..5a97fea4
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/defaultapps/DefaultAutofillPickerFragment.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.car.settings.applications.defaultapps;
+
+import androidx.annotation.XmlRes;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.SettingsFragment;
+
+/** Shows the option to choose the default autofill service. */
+public class DefaultAutofillPickerFragment extends SettingsFragment {
+
+ @Override
+ @XmlRes
+ protected int getPreferenceScreenResId() {
+ return R.xml.default_autofill_picker_fragment;
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/defaultapps/DefaultAutofillPickerPreferenceController.java b/CarSettings/src/com/android/car/settings/applications/defaultapps/DefaultAutofillPickerPreferenceController.java
new file mode 100644
index 00000000..777da77e
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/defaultapps/DefaultAutofillPickerPreferenceController.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.car.settings.applications.defaultapps;
+
+import android.Manifest;
+import android.car.drivingstate.CarUxRestrictions;
+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.autofill.AutofillService;
+import android.text.Html;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.FragmentController;
+import com.android.settingslib.applications.DefaultAppInfo;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Business logic for displaying and choosing the default autofill service. */
+public class DefaultAutofillPickerPreferenceController extends
+ DefaultAppsPickerBasePreferenceController {
+
+ public DefaultAutofillPickerPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ }
+
+ @NonNull
+ @Override
+ protected List getCandidates() {
+ List candidates = new ArrayList<>();
+ List resolveInfos = getContext().getPackageManager().queryIntentServices(
+ new Intent(AutofillService.SERVICE_INTERFACE), PackageManager.GET_META_DATA);
+ for (ResolveInfo info : resolveInfos) {
+ String permission = info.serviceInfo.permission;
+ if (Manifest.permission.BIND_AUTOFILL_SERVICE.equals(permission)) {
+ candidates.add(new DefaultAppInfo(getContext(), getContext().getPackageManager(),
+ getCurrentProcessUserId(),
+ new ComponentName(info.serviceInfo.packageName, info.serviceInfo.name)));
+ }
+ }
+ return candidates;
+ }
+
+ @Override
+ protected String getCurrentDefaultKey() {
+ String setting = Settings.Secure.getString(getContext().getContentResolver(),
+ Settings.Secure.AUTOFILL_SERVICE);
+ if (setting != null) {
+ ComponentName componentName = ComponentName.unflattenFromString(setting);
+ if (componentName != null) {
+ return componentName.flattenToString();
+ }
+ }
+ return DefaultAppsPickerBasePreferenceController.NONE_PREFERENCE_KEY;
+ }
+
+ @Override
+ protected void setCurrentDefault(String key) {
+ Settings.Secure.putString(getContext().getContentResolver(),
+ Settings.Secure.AUTOFILL_SERVICE, key);
+ }
+
+ @Override
+ @Nullable
+ protected CharSequence getConfirmationMessage(DefaultAppInfo info) {
+ if (info == null) {
+ return null;
+ }
+
+ CharSequence appName = info.loadLabel();
+ String message = getContext().getString(R.string.autofill_confirmation_message,
+ Html.escapeHtml(appName));
+ return Html.fromHtml(message, Html.FROM_HTML_MODE_LEGACY);
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/managedomainurls/AppLaunchSettingsBasePreferenceController.java b/CarSettings/src/com/android/car/settings/applications/managedomainurls/AppLaunchSettingsBasePreferenceController.java
new file mode 100644
index 00000000..2cedc08e
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/managedomainurls/AppLaunchSettingsBasePreferenceController.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.car.settings.applications.managedomainurls;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.UserHandle;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.Preference;
+
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.PreferenceController;
+import com.android.settingslib.applications.ApplicationsState;
+
+import java.util.List;
+
+/**
+ * Shared logic for preference controllers related to app launch settings.
+ *
+ * @param the upper bound on the type of {@link Preference} on which the controller
+ * expects to operate.
+ */
+public abstract class AppLaunchSettingsBasePreferenceController extends
+ PreferenceController {
+
+ @VisibleForTesting
+ static final Intent sBrowserIntent = new Intent()
+ .setAction(Intent.ACTION_VIEW)
+ .addCategory(Intent.CATEGORY_BROWSABLE)
+ .setData(Uri.parse("http:"));
+
+ protected final PackageManager mPm;
+ private ApplicationsState.AppEntry mAppEntry;
+
+ public AppLaunchSettingsBasePreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ this(context, preferenceKey, fragmentController, uxRestrictions,
+ context.getPackageManager());
+ }
+
+ @VisibleForTesting
+ AppLaunchSettingsBasePreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions,
+ PackageManager packageManager) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ mPm = packageManager;
+ }
+
+ /** Sets the app entry associated with this settings screen. */
+ public void setAppEntry(ApplicationsState.AppEntry entry) {
+ mAppEntry = entry;
+ }
+
+ /** Returns the app entry. */
+ public ApplicationsState.AppEntry getAppEntry() {
+ return mAppEntry;
+ }
+
+ /** Returns the package name. */
+ public String getPackageName() {
+ return mAppEntry.info.packageName;
+ }
+
+ /** Returns the current user id. */
+ protected int getCurrentUserId() {
+ return UserHandle.myUserId();
+ }
+
+ /** Returns {@code true} if the current package is a browser app. */
+ protected boolean isBrowserApp() {
+ sBrowserIntent.setPackage(getPackageName());
+ List list = mPm.queryIntentActivitiesAsUser(sBrowserIntent,
+ PackageManager.MATCH_ALL, getCurrentUserId());
+ for (ResolveInfo info : list) {
+ if (info.activityInfo != null && info.handleAllWebDataURI) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/managedomainurls/AppLinkStatePreferenceController.java b/CarSettings/src/com/android/car/settings/applications/managedomainurls/AppLinkStatePreferenceController.java
new file mode 100644
index 00000000..54fa191a
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/managedomainurls/AppLinkStatePreferenceController.java
@@ -0,0 +1,138 @@
+/*
+ * 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.car.settings.applications.managedomainurls;
+
+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.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.ListPreference;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.Logger;
+
+/**
+ * Business logic to define how the app should handle related domain links (whether related domain
+ * links should be opened always, never, or after asking).
+ */
+public class AppLinkStatePreferenceController extends
+ AppLaunchSettingsBasePreferenceController {
+
+ private static final Logger LOG = new Logger(AppLinkStatePreferenceController.class);
+
+ private boolean mHasDomainUrls;
+
+ public AppLinkStatePreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ }
+
+ @VisibleForTesting
+ AppLinkStatePreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions,
+ PackageManager packageManager) {
+ super(context, preferenceKey, fragmentController, uxRestrictions, packageManager);
+ }
+
+ @Override
+ protected Class getPreferenceType() {
+ return ListPreference.class;
+ }
+
+ @Override
+ protected void onCreateInternal() {
+ mHasDomainUrls =
+ (getAppEntry().info.privateFlags & ApplicationInfo.PRIVATE_FLAG_HAS_DOMAIN_URLS)
+ != 0;
+ }
+
+ @Override
+ protected void updateState(ListPreference preference) {
+ if (isBrowserApp()) {
+ preference.setEnabled(false);
+ } else {
+ preference.setEnabled(mHasDomainUrls);
+
+ preference.setEntries(new CharSequence[]{
+ getContext().getString(R.string.app_link_open_always),
+ getContext().getString(R.string.app_link_open_ask),
+ getContext().getString(R.string.app_link_open_never),
+ });
+ preference.setEntryValues(new CharSequence[]{
+ Integer.toString(INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS),
+ Integer.toString(INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS_ASK),
+ Integer.toString(INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_NEVER),
+ });
+
+ if (mHasDomainUrls) {
+ int state = mPm.getIntentVerificationStatusAsUser(getPackageName(),
+ getCurrentUserId());
+ preference.setValueIndex(linkStateToIndex(state));
+ }
+ }
+ }
+
+ @Override
+ protected boolean handlePreferenceChanged(ListPreference preference, Object newValue) {
+ if (isBrowserApp()) {
+ // We shouldn't get into this state, but if we do make sure
+ // not to cause any permanent mayhem.
+ return false;
+ }
+
+ int newState = Integer.parseInt((String) newValue);
+ int priorState = mPm.getIntentVerificationStatusAsUser(getPackageName(),
+ getCurrentUserId());
+ if (priorState == newState) {
+ return false;
+ }
+
+ boolean success = mPm.updateIntentVerificationStatusAsUser(getPackageName(), newState,
+ getCurrentUserId());
+ if (success) {
+ // Read back the state to see if the change worked.
+ int updatedState = mPm.getIntentVerificationStatusAsUser(getPackageName(),
+ getCurrentUserId());
+ success = (newState == updatedState);
+ } else {
+ LOG.e("Couldn't update intent verification status!");
+ }
+ return success;
+ }
+
+ private int linkStateToIndex(int state) {
+ switch (state) {
+ case INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS:
+ return getPreference().findIndexOfValue(
+ Integer.toString(INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS));
+ case INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_NEVER:
+ return getPreference().findIndexOfValue(
+ Integer.toString(INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_NEVER));
+ case INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS_ASK:
+ default:
+ return getPreference().findIndexOfValue(
+ Integer.toString(INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS_ASK));
+ }
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/managedomainurls/ApplicationLaunchSettingsFragment.java b/CarSettings/src/com/android/car/settings/applications/managedomainurls/ApplicationLaunchSettingsFragment.java
new file mode 100644
index 00000000..16e71cfd
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/managedomainurls/ApplicationLaunchSettingsFragment.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.car.settings.applications.managedomainurls;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.UserHandle;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.annotation.XmlRes;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.SettingsFragment;
+import com.android.settingslib.applications.ApplicationsState;
+
+import java.util.Arrays;
+import java.util.List;
+
+/** Settings screen to show details about launching a specific app. */
+public class ApplicationLaunchSettingsFragment extends SettingsFragment {
+
+ @VisibleForTesting
+ static final String ARG_PACKAGE_NAME = "arg_package_name";
+
+ private ApplicationsState mState;
+ private ApplicationsState.AppEntry mAppEntry;
+
+ /** Creates a new instance of this fragment for the package specified in the arguments. */
+ public static ApplicationLaunchSettingsFragment newInstance(String pkg) {
+ ApplicationLaunchSettingsFragment fragment = new ApplicationLaunchSettingsFragment();
+ Bundle args = new Bundle();
+ args.putString(ARG_PACKAGE_NAME, pkg);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ @XmlRes
+ protected int getPreferenceScreenResId() {
+ return R.xml.application_launch_settings_fragment;
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+
+ mState = ApplicationsState.getInstance(requireActivity().getApplication());
+
+ String pkgName = getArguments().getString(ARG_PACKAGE_NAME);
+ mAppEntry = mState.getEntry(pkgName, UserHandle.myUserId());
+
+ ApplicationWithVersionPreferenceController appController = use(
+ ApplicationWithVersionPreferenceController.class,
+ R.string.pk_opening_links_app_details);
+ appController.setAppState(mState);
+ appController.setAppEntry(mAppEntry);
+
+ List preferenceControllers = Arrays.asList(
+ use(AppLinkStatePreferenceController.class,
+ R.string.pk_opening_links_app_details_state),
+ use(DomainUrlsPreferenceController.class,
+ R.string.pk_opening_links_app_details_urls),
+ use(ClearDefaultsPreferenceController.class,
+ R.string.pk_opening_links_app_details_reset));
+
+ for (AppLaunchSettingsBasePreferenceController controller : preferenceControllers) {
+ controller.setAppEntry(mAppEntry);
+ }
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/managedomainurls/ApplicationWithVersionPreferenceController.java b/CarSettings/src/com/android/car/settings/applications/managedomainurls/ApplicationWithVersionPreferenceController.java
new file mode 100644
index 00000000..5915c07c
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/managedomainurls/ApplicationWithVersionPreferenceController.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.car.settings.applications.managedomainurls;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+
+import androidx.preference.Preference;
+
+import com.android.car.settings.applications.ApplicationPreferenceController;
+import com.android.car.settings.common.FragmentController;
+
+/** In addition to showing the app name and icon, shows the app version in the summary. */
+public class ApplicationWithVersionPreferenceController extends ApplicationPreferenceController {
+
+ public ApplicationWithVersionPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ }
+
+ @Override
+ protected void updateState(Preference preference) {
+ super.updateState(preference);
+ preference.setSummary(getAppVersion());
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/managedomainurls/ClearDefaultsPreferenceController.java b/CarSettings/src/com/android/car/settings/applications/managedomainurls/ClearDefaultsPreferenceController.java
new file mode 100644
index 00000000..a1b2fe2d
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/managedomainurls/ClearDefaultsPreferenceController.java
@@ -0,0 +1,118 @@
+/*
+ * 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.car.settings.applications.managedomainurls;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.hardware.usb.IUsbManager;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.Preference;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.Logger;
+import com.android.settingslib.applications.AppUtils;
+
+/**
+ * Business logic to reset a user preference for auto launching an app.
+ *
+ * i.e. if a user has both NavigationAppA and NavigationAppB installed and NavigationAppA is set
+ * as the default navigation app, the user can reset that preference to pick a different default
+ * navigation app.
+ */
+public class ClearDefaultsPreferenceController extends
+ AppLaunchSettingsBasePreferenceController {
+
+ private static final Logger LOG = new Logger(ClearDefaultsPreferenceController.class);
+
+ private final IUsbManager mUsbManager;
+
+ public ClearDefaultsPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ IBinder b = ServiceManager.getService(Context.USB_SERVICE);
+ mUsbManager = IUsbManager.Stub.asInterface(b);
+ }
+
+ @VisibleForTesting
+ ClearDefaultsPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions,
+ PackageManager packageManager, IUsbManager iUsbManager) {
+ super(context, preferenceKey, fragmentController, uxRestrictions, packageManager);
+ mUsbManager = iUsbManager;
+ }
+
+ @Override
+ protected Class getPreferenceType() {
+ return Preference.class;
+ }
+
+ @Override
+ protected void updateState(Preference preference) {
+ boolean autoLaunchEnabled = AppUtils.hasPreferredActivities(mPm, getPackageName())
+ || isDefaultBrowser(getPackageName())
+ || hasUsbDefaults(mUsbManager, getPackageName());
+
+ preference.setEnabled(autoLaunchEnabled);
+ if (autoLaunchEnabled) {
+ preference.setTitle(R.string.auto_launch_reset_text);
+ preference.setSummary(R.string.auto_launch_enable_text);
+ } else {
+ preference.setTitle(R.string.auto_launch_disable_text);
+ preference.setSummary(null);
+ }
+ }
+
+ @Override
+ protected boolean handlePreferenceClicked(Preference preference) {
+ if (mUsbManager != null) {
+ int userId = getCurrentUserId();
+ mPm.clearPackagePreferredActivities(getPackageName());
+ if (isDefaultBrowser(getPackageName())) {
+ mPm.setDefaultBrowserPackageNameAsUser(/* packageName= */ null, userId);
+ }
+ try {
+ mUsbManager.clearDefaults(getPackageName(), userId);
+ } catch (RemoteException e) {
+ LOG.e("mUsbManager.clearDefaults", e);
+ }
+ refreshUi();
+ }
+ return true;
+ }
+
+ private boolean isDefaultBrowser(String packageName) {
+ String defaultBrowser = mPm.getDefaultBrowserPackageNameAsUser(getCurrentUserId());
+ return packageName.equals(defaultBrowser);
+ }
+
+ private boolean hasUsbDefaults(IUsbManager usbManager, String packageName) {
+ try {
+ if (usbManager != null) {
+ return usbManager.hasDefaults(packageName, getCurrentUserId());
+ }
+ } catch (RemoteException e) {
+ LOG.e("mUsbManager.hasDefaults", e);
+ }
+ return false;
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/managedomainurls/DomainAppPreferenceController.java b/CarSettings/src/com/android/car/settings/applications/managedomainurls/DomainAppPreferenceController.java
new file mode 100644
index 00000000..2416196e
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/managedomainurls/DomainAppPreferenceController.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.car.settings.applications.managedomainurls;
+
+import android.app.Application;
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.UserHandle;
+import android.util.IconDrawableFactory;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.lifecycle.Lifecycle;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceGroup;
+
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.PreferenceController;
+import com.android.car.ui.preference.CarUiPreference;
+import com.android.settingslib.applications.ApplicationsState;
+
+import java.util.ArrayList;
+
+/** Business logic to populate the list of apps that deal with domain urls. */
+public class DomainAppPreferenceController extends PreferenceController {
+
+ private final ApplicationsState mApplicationsState;
+ private final PackageManager mPm;
+
+ @VisibleForTesting
+ final ApplicationsState.Callbacks mApplicationStateCallbacks =
+ new ApplicationsState.Callbacks() {
+ @Override
+ public void onRunningStateChanged(boolean running) {
+ }
+
+ @Override
+ public void onPackageListChanged() {
+ }
+
+ @Override
+ public void onRebuildComplete(ArrayList apps) {
+ rebuildAppList(apps);
+ }
+
+ @Override
+ public void onPackageIconChanged() {
+ }
+
+ @Override
+ public void onPackageSizeChanged(String packageName) {
+ }
+
+ @Override
+ public void onAllSizesComputed() {
+ }
+
+ @Override
+ public void onLauncherInfoChanged() {
+ }
+
+ @Override
+ public void onLoadEntriesCompleted() {
+ mSession.rebuild(ApplicationsState.FILTER_WITH_DOMAIN_URLS,
+ ApplicationsState.ALPHA_COMPARATOR);
+ }
+ };
+
+ private ApplicationsState.Session mSession;
+
+ public DomainAppPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ this(context, preferenceKey, fragmentController, uxRestrictions,
+ ApplicationsState.getInstance((Application) context.getApplicationContext()),
+ context.getPackageManager());
+ }
+
+ @VisibleForTesting
+ DomainAppPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions,
+ ApplicationsState applicationsState, PackageManager packageManager) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ mApplicationsState = applicationsState;
+ mPm = packageManager;
+ }
+
+ @Override
+ protected Class getPreferenceType() {
+ return PreferenceGroup.class;
+ }
+
+ @Override
+ protected void checkInitialized() {
+ if (mSession == null) {
+ throw new IllegalStateException("session should be non null by this point");
+ }
+ }
+
+ /** Sets the lifecycle to create a new session. */
+ public void setLifecycle(Lifecycle lifecycle) {
+ mSession = mApplicationsState.newSession(mApplicationStateCallbacks, lifecycle);
+ }
+
+ @Override
+ protected void onStartInternal() {
+ // Resume the session earlier than the lifecycle so that cached information is updated
+ // even if settings is not resumed (for example in multi-display).
+ mSession.onResume();
+ }
+
+ @Override
+ protected void onStopInternal() {
+ // Since we resume early in onStart, make sure we clean up even if we don't receive onPause.
+ mSession.onPause();
+ }
+
+ private void rebuildAppList(ArrayList apps) {
+ PreferenceGroup preferenceGroup = getPreference();
+ preferenceGroup.removeAll();
+ for (int i = 0; i < apps.size(); i++) {
+ ApplicationsState.AppEntry entry = apps.get(i);
+ preferenceGroup.addPreference(createPreference(entry));
+ }
+ }
+
+ private Preference createPreference(ApplicationsState.AppEntry entry) {
+ String key = entry.info.packageName + "|" + entry.info.uid;
+ IconDrawableFactory iconDrawableFactory = IconDrawableFactory.newInstance(getContext());
+ CarUiPreference preference = new CarUiPreference(getContext());
+ preference.setKey(key);
+ preference.setTitle(entry.label);
+ preference.setSummary(
+ DomainUrlsUtils.getDomainsSummary(getContext(), entry.info.packageName,
+ UserHandle.myUserId(),
+ DomainUrlsUtils.getHandledDomains(mPm, entry.info.packageName)));
+ preference.setIcon(iconDrawableFactory.getBadgedIcon(entry.info));
+ preference.setOnPreferenceClickListener(pref -> {
+ getFragmentController().launchFragment(
+ ApplicationLaunchSettingsFragment.newInstance(entry.info.packageName));
+ return true;
+ });
+ return preference;
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/managedomainurls/DomainUrlsPreferenceController.java b/CarSettings/src/com/android/car/settings/applications/managedomainurls/DomainUrlsPreferenceController.java
new file mode 100644
index 00000000..a6539254
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/managedomainurls/DomainUrlsPreferenceController.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.car.settings.applications.managedomainurls;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+import android.util.ArraySet;
+
+import androidx.preference.Preference;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.ConfirmationDialogFragment;
+import com.android.car.settings.common.FragmentController;
+
+/** Business logic to generate and see the list of supported domain urls. */
+public class DomainUrlsPreferenceController extends
+ AppLaunchSettingsBasePreferenceController {
+
+ private ArraySet mDomains;
+
+ public DomainUrlsPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ }
+
+ @Override
+ protected Class getPreferenceType() {
+ return Preference.class;
+ }
+
+ @Override
+ protected void onCreateInternal() {
+ mDomains = DomainUrlsUtils.getHandledDomains(getContext().getPackageManager(),
+ getPackageName());
+ }
+
+ @Override
+ protected void updateState(Preference preference) {
+ boolean hasDomains = mDomains != null && mDomains.size() > 0;
+ preference.setEnabled(!isBrowserApp() && hasDomains);
+ preference.setSummary(
+ DomainUrlsUtils.getDomainsSummary(getContext(), getPackageName(),
+ getCurrentUserId(), mDomains));
+ }
+
+ @Override
+ protected boolean handlePreferenceClicked(Preference preference) {
+ String newLines = System.lineSeparator() + System.lineSeparator();
+ String message = String.join(newLines, mDomains);
+
+ // Not exactly a "confirmation" dialog, but reusing to remove the need for a custom
+ // dialog fragment.
+ ConfirmationDialogFragment dialogFragment = new ConfirmationDialogFragment.Builder(
+ getContext())
+ .setTitle(R.string.app_launch_supported_domain_urls_title)
+ .setMessage(message)
+ .setNegativeButton(android.R.string.cancel, /* rejectListener= */ null)
+ .build();
+ getFragmentController().showDialog(dialogFragment, ConfirmationDialogFragment.TAG);
+ return true;
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/managedomainurls/DomainUrlsUtils.java b/CarSettings/src/com/android/car/settings/applications/managedomainurls/DomainUrlsUtils.java
new file mode 100644
index 00000000..9769196b
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/managedomainurls/DomainUrlsUtils.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.car.settings.applications.managedomainurls;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.IntentFilterVerificationInfo;
+import android.content.pm.PackageManager;
+import android.util.ArraySet;
+
+import com.android.car.settings.R;
+
+import java.util.List;
+
+/** Utility functions related to handling application domain urls. */
+public final class DomainUrlsUtils {
+ private DomainUrlsUtils() {
+ }
+
+ /** Get a summary text based on the number of handled domains. */
+ public static CharSequence getDomainsSummary(Context context, String packageName, int userId,
+ ArraySet domains) {
+ PackageManager pm = context.getPackageManager();
+
+ // If the user has explicitly said "no" for this package, that's the string we should show.
+ int domainStatus = pm.getIntentVerificationStatusAsUser(packageName, userId);
+ if (domainStatus == PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_NEVER) {
+ return context.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).
+ if (domains.isEmpty()) {
+ return context.getText(R.string.domain_urls_summary_none);
+ } else if (domains.size() == 1) {
+ return context.getString(R.string.domain_urls_summary_one, domains.valueAt(0));
+ } else {
+ return context.getString(R.string.domain_urls_summary_some, domains.valueAt(0));
+ }
+ }
+
+ /** Get the list of domains handled by the given package. */
+ public static ArraySet getHandledDomains(PackageManager pm, String packageName) {
+ List iviList = pm.getIntentFilterVerifications(packageName);
+ List filters = pm.getAllIntentFilters(packageName);
+
+ ArraySet result = new ArraySet<>();
+ if (iviList != null && iviList.size() > 0) {
+ for (IntentFilterVerificationInfo ivi : iviList) {
+ for (String host : ivi.getDomains()) {
+ result.add(host);
+ }
+ }
+ }
+ if (filters != null && filters.size() > 0) {
+ for (IntentFilter filter : filters) {
+ if (filter.hasCategory(Intent.CATEGORY_BROWSABLE)
+ && (filter.hasDataScheme(IntentFilter.SCHEME_HTTP)
+ || filter.hasDataScheme(IntentFilter.SCHEME_HTTPS))) {
+ result.addAll(filter.getHostsList());
+ }
+ }
+ }
+ return result;
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/managedomainurls/ManageDomainUrlsActivity.java b/CarSettings/src/com/android/car/settings/applications/managedomainurls/ManageDomainUrlsActivity.java
new file mode 100644
index 00000000..6920892f
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/managedomainurls/ManageDomainUrlsActivity.java
@@ -0,0 +1,36 @@
+/*
+ * 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.car.settings.applications.managedomainurls;
+
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+import com.android.car.settings.common.BaseCarSettingsActivity;
+
+/**
+ * Starts {@link ManageDomainUrlsFragment} in a separate activity to help with the back navigation
+ * flow. This setting differs from the other settings in that the user arrives here from the
+ * PermissionController rather than from within the Settings app itself.
+ */
+public class ManageDomainUrlsActivity extends BaseCarSettingsActivity {
+
+ @Nullable
+ @Override
+ protected Fragment getInitialFragment() {
+ return new ManageDomainUrlsFragment();
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/managedomainurls/ManageDomainUrlsFragment.java b/CarSettings/src/com/android/car/settings/applications/managedomainurls/ManageDomainUrlsFragment.java
new file mode 100644
index 00000000..7aacc161
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/managedomainurls/ManageDomainUrlsFragment.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.car.settings.applications.managedomainurls;
+
+import android.content.Context;
+
+import androidx.annotation.XmlRes;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.SettingsFragment;
+
+/** Fragment which shows a list of applications to manage handled domain urls. */
+public class ManageDomainUrlsFragment extends SettingsFragment {
+
+ @Override
+ @XmlRes
+ protected int getPreferenceScreenResId() {
+ return R.xml.manage_domain_urls_fragment;
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+
+ use(DomainAppPreferenceController.class, R.string.pk_opening_links_options).setLifecycle(
+ getLifecycle());
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/performance/PerfImpactingAppsItemManager.java b/CarSettings/src/com/android/car/settings/applications/performance/PerfImpactingAppsItemManager.java
new file mode 100644
index 00000000..be3dfa10
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/performance/PerfImpactingAppsItemManager.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.settings.applications.performance;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+
+import com.android.settingslib.utils.ThreadUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Class used to get the disabled packages due to resource overuse.
+ */
+public class PerfImpactingAppsItemManager {
+
+ private Context mContext;
+ private final List mPerfImpactingAppsListeners;
+
+ public PerfImpactingAppsItemManager(Context context) {
+ mContext = context;
+ mPerfImpactingAppsListeners = new ArrayList<>();
+ }
+
+ /**
+ * Registers a listener that will be notified once the data is loaded.
+ */
+ public void addListener(@NonNull PerfImpactingAppsListener listener) {
+ mPerfImpactingAppsListeners.add(listener);
+ }
+
+ /**
+ * Starts fetching installed apps and counting the non-system apps
+ */
+ public void startLoading() {
+ ThreadUtils.postOnBackgroundThread(() -> {
+ int disabledPackagesCount = PerfImpactingAppsUtils.getDisabledPackages(mContext).size();
+ for (PerfImpactingAppsListener listener : mPerfImpactingAppsListeners) {
+ ThreadUtils.postOnMainThread(() -> listener
+ .onPerfImpactingAppsLoaded(disabledPackagesCount));
+ }
+ });
+ }
+
+ /**
+ * Callback that is called once the performance-impacting apps are loaded.
+ */
+ public interface PerfImpactingAppsListener {
+ /**
+ * Called when the apps are successfully loaded from secure settings strings and
+ * PackageManager.
+ */
+ void onPerfImpactingAppsLoaded(int disabledPackagesCount);
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/performance/PerfImpactingAppsPreferenceController.java b/CarSettings/src/com/android/car/settings/applications/performance/PerfImpactingAppsPreferenceController.java
new file mode 100644
index 00000000..732e53f6
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/performance/PerfImpactingAppsPreferenceController.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.settings.applications.performance;
+
+import static android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS;
+
+import android.car.Car;
+import android.car.drivingstate.CarUxRestrictions;
+import android.car.watchdog.CarWatchdogManager;
+import android.car.watchdog.PackageKillableState;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.util.AttributeSet;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.PreferenceGroup;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.Logger;
+import com.android.car.settings.common.PreferenceController;
+import com.android.car.ui.preference.CarUiTwoActionTextPreference;
+import com.android.settingslib.Utils;
+
+import java.io.File;
+import java.util.List;
+
+/**
+ * Displays the list of apps which have been disabled due to resource overuse by CarWatchdogService.
+ *
+ * When a user taps the app, the app's detail setting page is shown. On the other hand, if a
+ * user presses the "Prioritize app" button, they are shown a dialog which allows them to prioritize
+ * the app, meaning the app can run in the background once again.
+ */
+public final class PerfImpactingAppsPreferenceController extends
+ PreferenceController {
+ private static final Logger LOG = new Logger(PerfImpactingAppsPreferenceController.class);
+
+ @VisibleForTesting
+ static final String TURN_ON_PRIORITIZE_APP_PERFORMANCE_DIALOG_TAG =
+ "com.android.car.settings.applications.performance.PrioritizeAppPerformanceDialogTag";
+
+ @Nullable
+ private Car mCar;
+ @Nullable
+ private CarWatchdogManager mCarWatchdogManager;
+ @Nullable
+ private List mEntries;
+
+ public PerfImpactingAppsPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ }
+
+ @Override
+ protected Class getPreferenceType() {
+ return PreferenceGroup.class;
+ }
+
+ @Override
+ protected void onCreateInternal() {
+ connectToCar();
+ updateEntries();
+ }
+
+ @Override
+ protected void onDestroyInternal() {
+ if (mCar != null) {
+ mCar.disconnect();
+ mCar = null;
+ }
+ }
+
+ @Override
+ protected void updateState(PreferenceGroup preference) {
+ if (mEntries == null) {
+ return;
+ }
+ preference.removeAll();
+ for (int i = 0; i < mEntries.size(); i++) {
+ ApplicationInfo entry = mEntries.get(i);
+ PerformanceImpactingAppPreference appPreference =
+ new PerformanceImpactingAppPreference(getContext(), entry);
+ setOnPreferenceClickListeners(appPreference, entry);
+ preference.addPreference(appPreference);
+ }
+ }
+
+ private void setOnPreferenceClickListeners(PerformanceImpactingAppPreference preference,
+ ApplicationInfo info) {
+ String packageName = info.packageName;
+ UserHandle userHandle = UserHandle.getUserHandleForUid(info.uid);
+ preference.setOnPreferenceClickListener(p -> {
+ Intent settingsIntent = new Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
+ .setData(Uri.parse("package:" + packageName));
+ getContext().startActivity(settingsIntent);
+ return true;
+ });
+ preference.setOnSecondaryActionClickListener(
+ () -> PerfImpactingAppsUtils.showPrioritizeAppConfirmationDialog(getContext(),
+ getFragmentController(),
+ args -> prioritizeApp(packageName, userHandle),
+ TURN_ON_PRIORITIZE_APP_PERFORMANCE_DIALOG_TAG));
+ }
+
+ private void prioritizeApp(String packageName, UserHandle userHandle) {
+ if (mCarWatchdogManager == null) {
+ LOG.e("CarWatchdogManager is null. Could not prioritize '" + packageName + "'.");
+ connectToCar();
+ return;
+ }
+ int killableState = PerfImpactingAppsUtils.getKillableState(packageName, userHandle,
+ mCarWatchdogManager);
+ if (killableState == PackageKillableState.KILLABLE_STATE_NEVER) {
+ LOG.wtf("Package '" + packageName + "' for user " + userHandle.getIdentifier()
+ + " is disabled for resource overuse but has KILLABLE_STATE_NEVER");
+ // Given wtf might not kill the process, we enable package in order to
+ // remove from resource overuse disabled package list.
+ PackageManager packageManager = getContext().getPackageManager();
+ packageManager.setApplicationEnabledSetting(packageName,
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED, /* flags= */ 0);
+ new Handler(Looper.getMainLooper()).postDelayed(this::updateEntries,
+ /* delayMillis= */ 1000);
+ } else {
+ mCarWatchdogManager.setKillablePackageAsUser(packageName, userHandle,
+ /* isKillable= */ false);
+ }
+ updateEntries();
+ }
+
+ private void updateEntries() {
+ mEntries = PerfImpactingAppsUtils.getDisabledAppInfos(getContext());
+ refreshUi();
+ }
+
+ private void connectToCar() {
+ if (mCar != null && mCar.isConnected()) {
+ mCar.disconnect();
+ mCar = null;
+ }
+ mCar = Car.createCar(getContext(), /* handler= */ null, Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER,
+ /* statusChangeListener= */ (car, isReady) -> mCarWatchdogManager = isReady
+ ? (CarWatchdogManager) car.getCarManager(Car.CAR_WATCHDOG_SERVICE)
+ : null);
+ }
+
+ static class PerformanceImpactingAppPreference extends CarUiTwoActionTextPreference {
+
+ PerformanceImpactingAppPreference(Context context, ApplicationInfo info) {
+ super(context);
+
+ boolean apkExists = new File(info.sourceDir).exists();
+ setKey(info.packageName + "|" + info.uid);
+ setTitle(getLabel(context, info, apkExists));
+ setIcon(getIconDrawable(context, info, apkExists));
+ setPersistent(false);
+ setSecondaryActionText(R.string.performance_impacting_apps_button_label);
+ }
+
+ @Override
+ protected void init(@Nullable AttributeSet attrs) {
+ super.init(attrs);
+
+ setLayoutResourceInternal(R.layout.car_ui_preference_two_action_text_borderless);
+ }
+
+ private String getLabel(Context context, ApplicationInfo info, boolean apkExists) {
+ if (!apkExists) {
+ return info.packageName;
+ }
+ CharSequence label = info.loadLabel(context.getPackageManager());
+ return label != null ? label.toString() : info.packageName;
+ }
+
+ private Drawable getIconDrawable(Context context, ApplicationInfo info,
+ boolean apkExists) {
+ if (apkExists) {
+ return Utils.getBadgedIcon(context, info);
+ }
+ return context.getDrawable(
+ com.android.internal.R.drawable.sym_app_on_sd_unavailable_icon);
+ }
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/performance/PerfImpactingAppsUtils.java b/CarSettings/src/com/android/car/settings/applications/performance/PerfImpactingAppsUtils.java
new file mode 100644
index 00000000..430d8b4c
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/performance/PerfImpactingAppsUtils.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.settings.applications.performance;
+
+import static android.car.settings.CarSettings.Secure.KEY_PACKAGES_DISABLED_ON_RESOURCE_OVERUSE;
+
+import android.car.watchdog.CarWatchdogManager;
+import android.car.watchdog.PackageKillableState;
+import android.content.ContentResolver;
+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.Process;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.ArraySet;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.ConfirmationDialogFragment;
+import com.android.car.settings.common.FragmentController;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Utility functions for use in Performance-impacting apps settings and Prioritize app settings.
+ */
+public final class PerfImpactingAppsUtils {
+
+ private static final String PACKAGES_DISABLED_ON_RESOURCE_OVERUSE_SEPARATOR = ";";
+
+ private PerfImpactingAppsUtils() {}
+
+ /**
+ * Returns the {@link android.car.watchdog.PackageKillableState.KillableState} for the package
+ * and user provided.
+ */
+ public static int getKillableState(String packageName, UserHandle userHandle,
+ CarWatchdogManager manager) {
+ return Objects.requireNonNull(manager)
+ .getPackageKillableStatesAsUser(userHandle).stream()
+ .filter(pks -> pks.getPackageName().equals(packageName))
+ .findFirst().map(PackageKillableState::getKillableState).orElse(-1);
+ }
+
+ /**
+ * Shows confirmation dialog when user chooses to prioritize an app disabled because of resource
+ * overuse.
+ */
+ public static void showPrioritizeAppConfirmationDialog(Context context,
+ FragmentController fragmentController,
+ ConfirmationDialogFragment.ConfirmListener listener, String dialogTag) {
+ ConfirmationDialogFragment dialogFragment =
+ new ConfirmationDialogFragment.Builder(context)
+ .setTitle(R.string.prioritize_app_performance_dialog_title)
+ .setMessage(R.string.prioritize_app_performance_dialog_text)
+ .setPositiveButton(R.string.prioritize_app_performance_dialog_action_on,
+ listener)
+ .setNegativeButton(R.string.prioritize_app_performance_dialog_action_off,
+ /* rejectListener= */ null)
+ .build();
+ fragmentController.showDialog(dialogFragment, dialogTag);
+ }
+
+ /**
+ * Returns the set of package names disabled due to resource overuse.
+ */
+ public static Set getDisabledPackages(Context context) {
+ ContentResolver contentResolverForUser = context.createContextAsUser(
+ UserHandle.getUserHandleForUid(Process.myUid()), /* flags= */ 0)
+ .getContentResolver();
+ return extractPackages(Settings.Secure.getString(contentResolverForUser,
+ KEY_PACKAGES_DISABLED_ON_RESOURCE_OVERUSE));
+ }
+
+ /**
+ * Returns a list of application infos disabled due to resource overuse.
+ */
+ public static List getDisabledAppInfos(Context context) {
+ Set disabledPackageNames = getDisabledPackages(context);
+ if (disabledPackageNames.isEmpty()) {
+ return new ArrayList<>(0);
+ }
+ PackageManager packageManager = context.getPackageManager();
+ List allPackages = packageManager.queryIntentActivities(
+ new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER),
+ PackageManager.ResolveInfoFlags.of(PackageManager.GET_RESOLVED_FILTER
+ | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS));
+ List disabledAppInfos = new ArrayList<>(allPackages.size());
+ for (int idx = 0; idx < allPackages.size(); idx++) {
+ ApplicationInfo applicationInfo = allPackages.get(idx).activityInfo.applicationInfo;
+ if (disabledPackageNames.contains(applicationInfo.packageName)) {
+ disabledAppInfos.add(applicationInfo);
+ // Match only the first occurrence of a package.
+ // |PackageManager#queryIntentActivities| can return duplicate packages.
+ disabledPackageNames.remove(applicationInfo.packageName);
+ }
+ }
+ return disabledAppInfos;
+ }
+
+ private static ArraySet extractPackages(String settingsString) {
+ return TextUtils.isEmpty(settingsString) ? new ArraySet<>()
+ : new ArraySet<>(Arrays.asList(settingsString.split(
+ PACKAGES_DISABLED_ON_RESOURCE_OVERUSE_SEPARATOR)));
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/performance/PerformanceImpactingAppSettingsFragment.java b/CarSettings/src/com/android/car/settings/applications/performance/PerformanceImpactingAppSettingsFragment.java
new file mode 100644
index 00000000..1160f34f
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/performance/PerformanceImpactingAppSettingsFragment.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.settings.applications.performance;
+
+import androidx.annotation.XmlRes;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.SettingsFragment;
+
+/**
+ * Fragment which lists the apps which are impacting the system's performance adversely.
+ *
+ * Selecting an application will open the application detail fragment.
+ */
+public final class PerformanceImpactingAppSettingsFragment extends SettingsFragment {
+
+ @Override
+ @XmlRes
+ protected int getPreferenceScreenResId() {
+ return R.xml.performance_impact_apps_fragment;
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/specialaccess/AppEntryListManager.java b/CarSettings/src/com/android/car/settings/applications/specialaccess/AppEntryListManager.java
new file mode 100644
index 00000000..368f1e58
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/specialaccess/AppEntryListManager.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright 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.car.settings.applications.specialaccess;
+
+import android.app.Application;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.settingslib.applications.ApplicationsState;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Manages a list of {@link ApplicationsState.AppEntry} instances by syncing in the background and
+ * providing updates via a {@link Callback}. Clients may provide an {@link ExtraInfoBridge} to
+ * populate the {@link ApplicationsState.AppEntry#extraInfo} field with use case sepecific data.
+ * Clients may also provide an {@link ApplicationsState.AppFilter} via an {@link AppFilterProvider}
+ * to determine which entries will appear in the list updates.
+ *
+ *
Clients should call {@link #init(ExtraInfoBridge, AppFilterProvider, Callback)} to specify
+ * behavior and then {@link #start()} to begin loading. {@link #stop()} will cancel loading, and
+ * {@link #destroy()} will clean up resources when this class will no longer be used.
+ */
+public class AppEntryListManager {
+
+ /** Callback for receiving events from {@link AppEntryListManager}. */
+ public interface Callback {
+ /**
+ * Called when the list of {@link ApplicationsState.AppEntry} instances or the {@link
+ * ApplicationsState.AppEntry#extraInfo} fields have changed.
+ */
+ void onAppEntryListChanged(List entries);
+ }
+
+ /**
+ * Provides an {@link ApplicationsState.AppFilter} to tailor the entries in the list updates.
+ */
+ public interface AppFilterProvider {
+ /**
+ * Returns the filter that should be used to trim the entries list before callback delivery.
+ */
+ ApplicationsState.AppFilter getAppFilter();
+ }
+
+ /** Bridges extra information to {@link ApplicationsState.AppEntry#extraInfo}. */
+ public interface ExtraInfoBridge {
+ /**
+ * Populates the {@link ApplicationsState.AppEntry#extraInfo} field on the {@code enrties}
+ * with the relevant data for the implementation.
+ */
+ void loadExtraInfo(List entries);
+ }
+
+ @VisibleForTesting
+ final ApplicationsState.Callbacks mSessionCallbacks =
+ new ApplicationsState.Callbacks() {
+ @Override
+ public void onRunningStateChanged(boolean running) {
+ // No op.
+ }
+
+ @Override
+ public void onPackageListChanged() {
+ forceUpdate();
+ }
+
+ @Override
+ public void onRebuildComplete(ArrayList apps) {
+ if (mCallback != null) {
+ mCallback.onAppEntryListChanged(apps);
+ }
+ }
+
+ @Override
+ public void onPackageIconChanged() {
+ // No op.
+ }
+
+ @Override
+ public void onPackageSizeChanged(String packageName) {
+ // No op.
+ }
+
+ @Override
+ public void onAllSizesComputed() {
+ // No op.
+ }
+
+ @Override
+ public void onLauncherInfoChanged() {
+ // No op.
+ }
+
+ @Override
+ public void onLoadEntriesCompleted() {
+ mHasReceivedLoadEntries = true;
+ forceUpdate();
+ }
+ };
+
+ private final ApplicationsState mApplicationsState;
+ private final BackgroundHandler mBackgroundHandler;
+ private final MainHandler mMainHandler;
+
+ private ExtraInfoBridge mExtraInfoBridge;
+ private AppFilterProvider mFilterProvider;
+ private Callback mCallback;
+ private ApplicationsState.Session mSession;
+
+ private boolean mHasReceivedLoadEntries;
+ private boolean mHasReceivedExtraInfo;
+
+ public AppEntryListManager(Context context) {
+ this(context, ApplicationsState.getInstance((Application) context.getApplicationContext()));
+ }
+
+ @VisibleForTesting
+ AppEntryListManager(Context context, ApplicationsState applicationsState) {
+ mApplicationsState = applicationsState;
+ // Run on the same background thread as the ApplicationsState to make sure updates don't
+ // conflict.
+ mBackgroundHandler = new BackgroundHandler(new WeakReference<>(this),
+ mApplicationsState.getBackgroundLooper());
+ mMainHandler = new MainHandler(new WeakReference<>(this));
+ }
+
+ /**
+ * Specifies the behavior of this manager.
+ *
+ * @param extraInfoBridge an optional bridge to load information into the entries.
+ * @param filterProvider provides a filter to tailor the contents of the list updates.
+ * @param callback callback to which updated lists are delivered.
+ */
+ public void init(@Nullable ExtraInfoBridge extraInfoBridge,
+ @Nullable AppFilterProvider filterProvider,
+ Callback callback) {
+ if (mSession != null) {
+ destroy();
+ }
+ mExtraInfoBridge = extraInfoBridge;
+ mFilterProvider = filterProvider;
+ mCallback = callback;
+ mSession = mApplicationsState.newSession(mSessionCallbacks);
+ }
+
+ /**
+ * Starts loading the information in the background. When loading is finished, the {@link
+ * Callback} will be notified on the main thread.
+ */
+ public void start() {
+ mSession.onResume();
+ }
+
+ /**
+ * Stops any pending loading.
+ */
+ public void stop() {
+ mSession.onPause();
+ clearHandlers();
+ }
+
+ /**
+ * Cleans up internal state when this will no longer be used.
+ */
+ public void destroy() {
+ mSession.onDestroy();
+ clearHandlers();
+ mExtraInfoBridge = null;
+ mFilterProvider = null;
+ mCallback = null;
+ }
+
+ /**
+ * Schedules updates for all {@link ApplicationsState.AppEntry} instances. When loading is
+ * finished, the {@link Callback} will be notified on the main thread.
+ */
+ public void forceUpdate() {
+ mBackgroundHandler.sendEmptyMessage(BackgroundHandler.MSG_LOAD_ALL);
+ }
+
+ /**
+ * Schedules an update for the given {@code entry}. When loading is finished, the {@link
+ * Callback} will be notified on the main thread.
+ */
+ public void forceUpdate(ApplicationsState.AppEntry entry) {
+ mBackgroundHandler.obtainMessage(BackgroundHandler.MSG_LOAD_PKG,
+ entry).sendToTarget();
+ }
+
+ private void rebuild() {
+ if (!mHasReceivedLoadEntries || !mHasReceivedExtraInfo) {
+ // Don't rebuild the list until all the app entries are loaded.
+ return;
+ }
+ mSession.rebuild((mFilterProvider != null) ? mFilterProvider.getAppFilter()
+ : ApplicationsState.FILTER_EVERYTHING,
+ ApplicationsState.ALPHA_COMPARATOR, /* foreground= */ false);
+ }
+
+ private void clearHandlers() {
+ mBackgroundHandler.removeMessages(BackgroundHandler.MSG_LOAD_ALL);
+ mBackgroundHandler.removeMessages(BackgroundHandler.MSG_LOAD_PKG);
+ mMainHandler.removeMessages(MainHandler.MSG_INFO_UPDATED);
+ }
+
+ private void loadInfo(List entries) {
+ if (mExtraInfoBridge != null) {
+ mExtraInfoBridge.loadExtraInfo(entries);
+ }
+ for (ApplicationsState.AppEntry entry : entries) {
+ mApplicationsState.ensureIcon(entry);
+ }
+ }
+
+ private static class BackgroundHandler extends Handler {
+ private static final int MSG_LOAD_ALL = 1;
+ private static final int MSG_LOAD_PKG = 2;
+
+ private final WeakReference mOuter;
+
+ BackgroundHandler(WeakReference outer, Looper looper) {
+ super(looper);
+ mOuter = outer;
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ AppEntryListManager outer = mOuter.get();
+ if (outer == null) {
+ return;
+ }
+ switch (msg.what) {
+ case MSG_LOAD_ALL:
+ outer.loadInfo(outer.mSession.getAllApps());
+ outer.mMainHandler.sendEmptyMessage(MainHandler.MSG_INFO_UPDATED);
+ break;
+ case MSG_LOAD_PKG:
+ ApplicationsState.AppEntry entry = (ApplicationsState.AppEntry) msg.obj;
+ outer.loadInfo(Collections.singletonList(entry));
+ outer.mMainHandler.sendEmptyMessage(MainHandler.MSG_INFO_UPDATED);
+ break;
+ }
+ }
+ }
+
+ private static class MainHandler extends Handler {
+ private static final int MSG_INFO_UPDATED = 1;
+
+ private final WeakReference mOuter;
+
+ MainHandler(WeakReference outer) {
+ mOuter = outer;
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ AppEntryListManager outer = mOuter.get();
+ if (outer == null) {
+ return;
+ }
+ switch (msg.what) {
+ case MSG_INFO_UPDATED:
+ outer.mHasReceivedExtraInfo = true;
+ outer.rebuild();
+ break;
+ }
+ }
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/specialaccess/AppOpsPreferenceController.java b/CarSettings/src/com/android/car/settings/applications/specialaccess/AppOpsPreferenceController.java
new file mode 100644
index 00000000..1b59b9e2
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/specialaccess/AppOpsPreferenceController.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright 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.car.settings.applications.specialaccess;
+
+import android.app.AppOpsManager;
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceGroup;
+
+import com.android.car.settings.R;
+import com.android.car.settings.applications.specialaccess.AppStateAppOpsBridge.PermissionState;
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.PreferenceController;
+import com.android.car.ui.preference.CarUiSwitchPreference;
+import com.android.settingslib.applications.ApplicationsState;
+import com.android.settingslib.applications.ApplicationsState.AppEntry;
+import com.android.settingslib.applications.ApplicationsState.AppFilter;
+import com.android.settingslib.applications.ApplicationsState.CompoundFilter;
+
+import java.util.List;
+
+/**
+ * Displays a list of toggles for applications requesting permission to perform the operation with
+ * which this controller was initialized. {@link #init(int, String, int)} should be called when
+ * this controller is instantiated to specify the {@link AppOpsManager} operation code to control
+ * access for.
+ */
+public class AppOpsPreferenceController extends PreferenceController {
+
+ private static final AppFilter FILTER_HAS_INFO = new AppFilter() {
+ @Override
+ public void init() {
+ // No op.
+ }
+
+ @Override
+ public boolean filterApp(AppEntry info) {
+ return info.extraInfo != null;
+ }
+ };
+
+ private final AppOpsManager mAppOpsManager;
+
+ private final Preference.OnPreferenceChangeListener mOnPreferenceChangeListener =
+ new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ AppOpPreference appOpPreference = (AppOpPreference) preference;
+ AppEntry entry = appOpPreference.mEntry;
+ PermissionState extraInfo = (PermissionState) entry.extraInfo;
+ boolean allowOp = (Boolean) newValue;
+ if (allowOp != extraInfo.isPermissible()) {
+ mAppOpsManager.setMode(mAppOpsOpCode, entry.info.uid,
+ entry.info.packageName,
+ allowOp ? AppOpsManager.MODE_ALLOWED : mNegativeOpMode);
+ // Update the extra info of this entry so that it reflects the new mode.
+ mAppEntryListManager.forceUpdate(entry);
+ return true;
+ }
+ return false;
+ }
+ };
+
+ private final AppEntryListManager.Callback mCallback = new AppEntryListManager.Callback() {
+ @Override
+ public void onAppEntryListChanged(List entries) {
+ mEntries = entries;
+ refreshUi();
+ }
+ };
+
+ private int mAppOpsOpCode = AppOpsManager.OP_NONE;
+ private String mPermission;
+ private int mNegativeOpMode = -1;
+
+ @VisibleForTesting
+ AppEntryListManager mAppEntryListManager;
+ private List mEntries;
+
+ private boolean mShowSystem;
+
+ public AppOpsPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ this(context, preferenceKey, fragmentController, uxRestrictions,
+ context.getSystemService(AppOpsManager.class));
+ }
+
+ @VisibleForTesting
+ AppOpsPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions,
+ AppOpsManager appOpsManager) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ mAppOpsManager = appOpsManager;
+ mAppEntryListManager = new AppEntryListManager(context);
+ }
+
+ @Override
+ protected Class getPreferenceType() {
+ return PreferenceGroup.class;
+ }
+
+ /**
+ * Initializes this controller with the {@code appOpsOpCode} (selected from the operations in
+ * {@link AppOpsManager}) to control access for.
+ *
+ * @param permission the {@link android.Manifest.permission} apps must hold to perform the
+ * operation.
+ * @param negativeOpMode the operation mode that will be passed to {@link
+ * AppOpsManager#setMode(int, int, String, int)} when access for a app is
+ * revoked.
+ */
+ public void init(int appOpsOpCode, String permission, int negativeOpMode) {
+ mAppOpsOpCode = appOpsOpCode;
+ mPermission = permission;
+ mNegativeOpMode = negativeOpMode;
+ }
+
+ /**
+ * Rebuilds the preference list to show system applications if {@code showSystem} is true.
+ * System applications will be hidden otherwise.
+ */
+ public void setShowSystem(boolean showSystem) {
+ if (mShowSystem != showSystem) {
+ mShowSystem = showSystem;
+ mAppEntryListManager.forceUpdate();
+ }
+ }
+
+ @Override
+ protected void checkInitialized() {
+ if (mAppOpsOpCode == AppOpsManager.OP_NONE) {
+ throw new IllegalStateException("App operation code must be initialized");
+ }
+ if (mPermission == null) {
+ throw new IllegalStateException("Manifest permission must be initialized");
+ }
+ if (mNegativeOpMode == -1) {
+ throw new IllegalStateException("Negative case app operation mode must be initialized");
+ }
+ }
+
+ @Override
+ protected void onCreateInternal() {
+ AppStateAppOpsBridge extraInfoBridge = new AppStateAppOpsBridge(getContext(), mAppOpsOpCode,
+ mPermission);
+ mAppEntryListManager.init(extraInfoBridge, this::getAppFilter, mCallback);
+ }
+
+ @Override
+ protected void onStartInternal() {
+ mAppEntryListManager.start();
+ }
+
+ @Override
+ protected void onStopInternal() {
+ mAppEntryListManager.stop();
+ }
+
+ @Override
+ protected void onDestroyInternal() {
+ mAppEntryListManager.destroy();
+ }
+
+ @Override
+ protected void updateState(PreferenceGroup preference) {
+ if (mEntries == null) {
+ // Still loading.
+ return;
+ }
+ preference.removeAll();
+ for (AppEntry entry : mEntries) {
+ Preference appOpPreference = new AppOpPreference(getContext(), entry);
+ appOpPreference.setOnPreferenceChangeListener(mOnPreferenceChangeListener);
+ preference.addPreference(appOpPreference);
+ }
+ }
+
+ @CallSuper
+ protected AppFilter getAppFilter() {
+ AppFilter filterObj = new CompoundFilter(FILTER_HAS_INFO,
+ ApplicationsState.FILTER_NOT_HIDE);
+ if (!mShowSystem) {
+ filterObj = new CompoundFilter(filterObj,
+ ApplicationsState.FILTER_DOWNLOADED_AND_LAUNCHER);
+ }
+ return filterObj;
+ }
+
+ private static class AppOpPreference extends CarUiSwitchPreference {
+
+ private final AppEntry mEntry;
+
+ AppOpPreference(Context context, AppEntry entry) {
+ super(context);
+ String key = entry.info.packageName + "|" + entry.info.uid;
+ setKey(key);
+ setTitle(entry.label);
+ setIcon(entry.icon);
+ setSummary(getAppStateText(entry.info));
+ setPersistent(false);
+ PermissionState extraInfo = (PermissionState) entry.extraInfo;
+ setChecked(extraInfo.isPermissible());
+ mEntry = entry;
+ }
+
+ private String getAppStateText(ApplicationInfo info) {
+ if ((info.flags & ApplicationInfo.FLAG_INSTALLED) == 0) {
+ return getContext().getString(R.string.not_installed);
+ } else if (!info.enabled || info.enabledSetting
+ == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED) {
+ return getContext().getString(R.string.disabled);
+ }
+ return null;
+ }
+ }
+}
diff --git a/CarSettings/src/com/android/car/settings/applications/specialaccess/AppStateAppOpsBridge.java b/CarSettings/src/com/android/car/settings/applications/specialaccess/AppStateAppOpsBridge.java
new file mode 100644
index 00000000..9fcd94ee
--- /dev/null
+++ b/CarSettings/src/com/android/car/settings/applications/specialaccess/AppStateAppOpsBridge.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright 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.car.settings.applications.specialaccess;
+
+import android.app.AppGlobals;
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.ArrayMap;
+import android.util.SparseArray;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.car.settings.common.Logger;
+import com.android.internal.util.ArrayUtils;
+import com.android.settingslib.applications.ApplicationsState.AppEntry;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Bridges {@link AppOpsManager} app operation permission information into {@link
+ * AppEntry#extraInfo} as {@link PermissionState} objects.
+ */
+public class AppStateAppOpsBridge implements AppEntryListManager.ExtraInfoBridge {
+
+ private static final Logger LOG = new Logger(AppStateAppOpsBridge.class);
+
+ private final Context mContext;
+ private final IPackageManager mIPackageManager;
+ private final List