Easy app-installing for Guest users

Allows an admin user to selectively install packages into a Guest user
by displaying a list of available packages that are installed in the
admin, but not in the Guest, and letting the admin choose to copy those
apps to the Guest.

Test: atest SettingsLibTests:com.android.settingslib.users.AppCopyingHelperTest

Test: Manual: install some apps in user 0, create a guest, uninstall
some system apps from it. Now, open this panel. Request some of those
apps be installed in the Guest and verify it worked.

Bug: 193281439

Change-Id: I4e6874a4ee93cd7bba96e1f6c8d04ed95873c1a2
This commit is contained in:
Adam Bookatz
2021-07-09 15:52:54 -07:00
parent b9db3d3a04
commit 64c668e7b5
6 changed files with 284 additions and 1 deletions

View File

@@ -17,7 +17,8 @@
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
android:viewportHeight="24.0"
android:tint="?android:attr/colorControlNormal">
<path
android:pathData="M16,6C16,7.1 16.9,8 18,8C19.1,8 20,7.1 20,6C20,4.9 19.1,4 18,4C16.9,4 16,4.9 16,6ZM6,8C7.1,8 8,7.1 8,6C8,4.9 7.1,4 6,4C4.9,4 4,4.9 4,6C4,7.1 4.9,8 6,8ZM12.001,20C13.101,20 14.001,19.1 14.001,18C14.001,16.9 13.101,16 12.001,16C10.901,16 10.001,16.9 10.001,18C10.001,19.1 10.901,20 12.001,20ZM8.001,18C8.001,19.1 7.101,20 6.001,20C4.901,20 4.001,19.1 4.001,18C4.001,16.9 4.901,16 6.001,16C7.101,16 8.001,16.9 8.001,18ZM6.001,14C7.101,14 8.001,13.1 8.001,12C8.001,10.9 7.101,10 6.001,10C4.901,10 4.001,10.9 4.001,12C4.001,13.1 4.901,14 6.001,14ZM14.001,12C14.001,13.1 13.101,14 12.001,14C10.901,14 10.001,13.1 10.001,12C10.001,10.9 10.901,10 12.001,10C13.101,10 14.001,10.9 14.001,12ZM14.001,6C14.001,7.1 13.101,8 12.001,8C10.901,8 10.001,7.1 10.001,6C10.001,4.9 10.901,4 12.001,4C13.101,4 14.001,4.9 14.001,6ZM18,14C19.1,14 20,13.1 20,12C20,10.9 19.1,10 18,10C16.9,10 16,10.9 16,12C16,13.1 16.9,14 18,14ZM20,18C20,19.1 19.1,20 18,20C16.9,20 16,19.1 16,18C16,16.9 16.9,16 18,16C19.1,16 20,16.9 20,18Z"
android:fillType="evenOdd"

View File

@@ -7728,6 +7728,11 @@
<!-- Applicaitons with restrictions - settings button [CHAR LIMIT=30] -->
<string name="apps_with_restrictions_settings_button">Expand settings for application</string>
<!-- Title for copying apps to another user [CHAR LIMIT=45] -->
<string name="user_choose_copy_apps_to_another_user">Choose apps to install</string>
<!-- Menu title for copying apps to another user [CHAR LIMIT=35] -->
<string name="user_copy_apps_menu_title">Install available apps</string>
<!-- NFC payment settings --><skip/>
<string name="nfc_payment_settings_title">Contactless payments</string>
<!-- Caption for button linking to a page explaining how Tap and Pay works-->

20
res/xml/app_copier.xml Normal file
View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:settings="http://schemas.android.com/apk/res-auto"
android:title="@string/user_choose_copy_apps_to_another_user">
</PreferenceScreen>

View File

@@ -29,6 +29,10 @@
android:key="app_and_content_access"
android:icon="@drawable/ic_lock_closed"
android:title="@string/user_restrictions_title" />
<com.android.settingslib.RestrictedPreference
android:key="app_copying"
android:icon="@drawable/ic_apps"
android:title="@string/user_copy_apps_menu_title" />
<com.android.settingslib.RestrictedPreference
android:key="remove_user"
android:icon="@drawable/ic_delete"

View File

@@ -0,0 +1,231 @@
/*
* 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.settings.users;
import android.app.settings.SettingsEnums;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.Log;
import androidx.preference.PreferenceGroup;
import com.android.settings.R;
import com.android.settings.SettingsPreferenceFragment;
import com.android.settings.Utils;
import com.android.settingslib.users.AppCopyHelper;
import com.android.settingslib.widget.AppSwitchPreference;
/**
* Allows an admin user to selectively copy some of their installed packages to a second user.
*/
public class AppCopyFragment extends SettingsPreferenceFragment {
private static final String TAG = AppCopyFragment.class.getSimpleName();
private static final boolean DEBUG = false;
private static final String PKG_PREFIX = "pkg_";
/** Key for extra passed in from calling fragment for the userId of the user being edited */
public static final String EXTRA_USER_ID = "user_id";
protected UserManager mUserManager;
protected UserHandle mUser;
private AppCopyHelper mHelper;
/** List of installable apps presented to the user. */
private PreferenceGroup mAppList;
private boolean mAppListChanged;
private AsyncTask mAppLoadingTask;
private final BroadcastReceiver mUserBackgrounding = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (DEBUG) Log.d(TAG, "mUserBackgrounding onReceive");
// Update the user's app selection right away without waiting for a pause
// onPause() might come in too late, causing apps to disappear after broadcasts
// have been scheduled during user startup.
if (mAppListChanged) {
if (DEBUG) Log.d(TAG, "User backgrounding: installing apps");
mHelper.installSelectedApps();
if (DEBUG) Log.d(TAG, "User backgrounding: done installing apps");
}
}
};
private final BroadcastReceiver mPackageObserver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
onPackageChanged(intent);
}
};
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
init(icicle);
}
protected void init(Bundle icicle) {
if (icicle != null) {
mUser = new UserHandle(icicle.getInt(EXTRA_USER_ID));
} else {
final Bundle args = getArguments();
if (args != null) {
if (args.containsKey(EXTRA_USER_ID)) {
mUser = new UserHandle(args.getInt(EXTRA_USER_ID));
}
}
}
if (mUser == null) {
throw new IllegalStateException("No user specified.");
}
mHelper = new AppCopyHelper(getContext(), mUser);
mUserManager = (UserManager) getActivity().getSystemService(Context.USER_SERVICE);
addPreferencesFromResource(R.xml.app_copier);
mAppList = getPreferenceScreen();
mAppList.setOrderingAsAdded(false);
}
@Override
public int getMetricsCategory() {
return SettingsEnums.USERS_APP_COPYING;
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(EXTRA_USER_ID, mUser.getIdentifier());
}
@Override
public void onResume() {
super.onResume();
getActivity().registerReceiver(mUserBackgrounding,
new IntentFilter(Intent.ACTION_USER_BACKGROUND));
final IntentFilter packageFilter = new IntentFilter();
packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
packageFilter.addDataScheme("package");
getActivity().registerReceiver(mPackageObserver, packageFilter);
mAppListChanged = false;
if (mAppLoadingTask == null || mAppLoadingTask.getStatus() == AsyncTask.Status.FINISHED) {
mAppLoadingTask = new AppLoadingTask().execute();
}
}
@Override
public void onPause() {
super.onPause();
getActivity().unregisterReceiver(mUserBackgrounding);
getActivity().unregisterReceiver(mPackageObserver);
if (mAppListChanged) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
mHelper.installSelectedApps();
return null;
}
}.execute();
}
}
private void onPackageChanged(Intent intent) {
final String action = intent.getAction();
final String packageName = intent.getData().getSchemeSpecificPart();
if (DEBUG) Log.d(TAG, "onPackageChanged (" + action + "): " + packageName);
// Package added/removed, so check if the preference needs to be enabled
final AppSwitchPreference pref = findPreference(getKeyForPackage(packageName));
if (pref == null) return;
if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
pref.setEnabled(false);
pref.setChecked(false);
mHelper.setPackageSelected(packageName, false);
} else if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
pref.setEnabled(true);
}
}
private class AppLoadingTask extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... params) {
mHelper.fetchAndMergeApps();
return null;
}
@Override
protected void onPostExecute(Void result) {
populateApps();
}
}
private void populateApps() {
// Check if the user was removed in the meantime.
if (Utils.getExistingUser(mUserManager, mUser) == null) {
return;
}
mHelper.resetSelectedPackages();
mAppList.removeAll();
for (AppCopyHelper.SelectableAppInfo app : mHelper.getVisibleApps()) {
if (app.packageName == null) continue;
final AppSwitchPreference p = new AppSwitchPreference(getPrefContext());
p.setIcon(app.icon != null ? app.icon.mutate() : null);
p.setChecked(false);
p.setTitle(app.appName);
p.setKey(getKeyForPackage(app.packageName));
p.setPersistent(false);
p.setOnPreferenceChangeListener((preference, newValue) -> {
if (!preference.isEnabled()) {
// This item isn't available anymore (perhaps it was since uninstalled).
if (DEBUG) Log.d(TAG, "onPreferenceChange but not enabled");
return false;
}
final boolean checked = (boolean) newValue;
final String packageName = preference.getKey().substring(PKG_PREFIX.length());
if (DEBUG) Log.d(TAG, "onPreferenceChange: " + packageName + " check=" + newValue);
mHelper.setPackageSelected(packageName, checked);
mAppListChanged = true;
return true;
});
mAppList.addPreference(p);
}
mAppListChanged = true;
}
private String getKeyForPackage(String packageName) {
return PKG_PREFIX + packageName;
}
}

View File

@@ -62,6 +62,7 @@ public class UserDetailsSettings extends SettingsPreferenceFragment
private static final String KEY_ENABLE_TELEPHONY = "enable_calling";
private static final String KEY_REMOVE_USER = "remove_user";
private static final String KEY_APP_AND_CONTENT_ACCESS = "app_and_content_access";
private static final String KEY_APP_COPYING = "app_copying";
/** Integer extra containing the userId to manage */
static final String EXTRA_USER_ID = "user_id";
@@ -84,9 +85,12 @@ public class UserDetailsSettings extends SettingsPreferenceFragment
@VisibleForTesting
Preference mAppAndContentAccessPref;
@VisibleForTesting
Preference mAppCopyingPref;
@VisibleForTesting
Preference mRemoveUserPref;
@VisibleForTesting
/** The user being studied (not the user doing the studying). */
UserInfo mUserInfo;
private Bundle mDefaultGuestRestrictions;
@@ -142,6 +146,9 @@ public class UserDetailsSettings extends SettingsPreferenceFragment
} else if (preference == mAppAndContentAccessPref) {
openAppAndContentAccessScreen(false);
return true;
} else if (preference == mAppCopyingPref) {
openAppCopyingScreen();
return true;
}
return false;
}
@@ -241,6 +248,7 @@ public class UserDetailsSettings extends SettingsPreferenceFragment
mPhonePref = findPreference(KEY_ENABLE_TELEPHONY);
mRemoveUserPref = findPreference(KEY_REMOVE_USER);
mAppAndContentAccessPref = findPreference(KEY_APP_AND_CONTENT_ACCESS);
mAppCopyingPref = findPreference(KEY_APP_COPYING);
mSwitchUserPref.setTitle(
context.getString(com.android.settingslib.R.string.user_switch_to_user,
@@ -258,6 +266,7 @@ public class UserDetailsSettings extends SettingsPreferenceFragment
removePreference(KEY_ENABLE_TELEPHONY);
removePreference(KEY_REMOVE_USER);
removePreference(KEY_APP_AND_CONTENT_ACCESS);
removePreference(KEY_APP_COPYING);
} else {
if (!Utils.isVoiceCapable(context)) { // no telephony
removePreference(KEY_ENABLE_TELEPHONY);
@@ -292,6 +301,7 @@ public class UserDetailsSettings extends SettingsPreferenceFragment
mPhonePref.setChecked(!mUserManager.hasUserRestriction(
UserManager.DISALLOW_OUTGOING_CALLS, new UserHandle(userId)));
mRemoveUserPref.setTitle(R.string.user_remove_user);
removePreference(KEY_APP_COPYING);
}
if (RestrictedLockUtilsInternal.hasBaseUserRestriction(context,
UserManager.DISALLOW_REMOVE_USER, UserHandle.myUserId())) {
@@ -301,6 +311,7 @@ public class UserDetailsSettings extends SettingsPreferenceFragment
mRemoveUserPref.setOnPreferenceClickListener(this);
mPhonePref.setOnPreferenceChangeListener(this);
mAppAndContentAccessPref.setOnPreferenceClickListener(this);
mAppCopyingPref.setOnPreferenceClickListener(this);
}
}
@@ -395,6 +406,17 @@ public class UserDetailsSettings extends SettingsPreferenceFragment
.launch();
}
private void openAppCopyingScreen() {
final Bundle extras = new Bundle();
extras.putInt(AppRestrictionsFragment.EXTRA_USER_ID, mUserInfo.id);
new SubSettingLauncher(getContext())
.setDestination(AppCopyFragment.class.getName())
.setArguments(extras)
.setTitleRes(R.string.user_copy_apps_menu_title)
.setSourceMetricsCategory(getMetricsCategory())
.launch();
}
private boolean isSecondaryUser(UserInfo user) {
return UserManager.USER_TYPE_FULL_SECONDARY.equals(user.userType);
}