blob: 7c3f9e2d8675738d7b2b3447dd0de795a19f3da9 [file] [log] [blame]
/*
* 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.enterprise;
import static android.app.Activity.RESULT_OK;
import static android.os.Process.myUserHandle;
import static com.android.car.settings.enterprise.EnterpriseUtils.getAdminWithinPackage;
import static com.android.car.settings.enterprise.EnterpriseUtils.getDeviceAdminInfo;
import android.app.Activity;
import android.app.admin.DeviceAdminInfo;
import android.app.admin.DeviceAdminReceiver;
import android.app.admin.DevicePolicyManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.text.TextUtils;
import androidx.annotation.VisibleForTesting;
import androidx.preference.PreferenceScreen;
import com.android.car.settings.R;
import com.android.car.settings.common.Logger;
import com.android.car.settings.common.PreferenceController;
import com.android.car.settings.common.SettingsFragment;
import com.android.car.ui.toolbar.MenuItem;
import com.android.car.ui.toolbar.ToolbarController;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* A screen that shows details about a device administrator.
*/
public final class DeviceAdminAddFragment extends SettingsFragment {
private static final Logger LOG = new Logger(DeviceAdminAddFragment.class);
@VisibleForTesting
static final int UNINSTALL_DEVICE_ADMIN_REQUEST_CODE = 12;
private CharSequence mAppName;
private DevicePolicyManager mDpm;
private String mPackageToUninstall;
private ComponentName mAdminComponentToUninstall;
private boolean mIsActive;
private MenuItem mUninstallButton;
@VisibleForTesting
void setDevicePolicyManager(DevicePolicyManager dpm) {
mDpm = dpm;
}
@Override
protected int getPreferenceScreenResId() {
return R.xml.device_admin_add;
}
@Override
public List<MenuItem> getToolbarMenuItems() {
return Collections.singletonList(mUninstallButton);
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
// Split in 2 as it would be hard to mock requireActivity();
onAttach(context, requireActivity());
}
@VisibleForTesting
void onAttach(Context context, Activity activity) {
setDevicePolicyManager(context.getSystemService(DevicePolicyManager.class));
Intent intent = activity.getIntent();
if (intent == null) {
LOG.e("no intent on " + activity);
activity.finish();
return;
}
ComponentName admin = (ComponentName)
intent.getParcelableExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN);
LOG.d("Admin using " + DevicePolicyManager.EXTRA_DEVICE_ADMIN + ": " + admin);
if (admin == null) {
String adminPackage = intent
.getStringExtra(DeviceAdminAddActivity.EXTRA_DEVICE_ADMIN_PACKAGE_NAME);
LOG.d("Admin package using " + DeviceAdminAddActivity.EXTRA_DEVICE_ADMIN_PACKAGE_NAME
+ ": " + adminPackage);
if (adminPackage == null) {
LOG.w("Finishing " + activity + " as its intent doesn't have "
+ DevicePolicyManager.EXTRA_DEVICE_ADMIN + " or "
+ DeviceAdminAddActivity.EXTRA_DEVICE_ADMIN_PACKAGE_NAME);
activity.finish();
return;
}
admin = getAdminWithinPackage(context, adminPackage);
if (admin == null) {
LOG.w("Finishing " + activity + " as there is no active admin for " + adminPackage);
activity.finish();
return;
}
// DeviceAdminAddActivity.EXTRA_DEVICE_ADMIN_PACKAGE_NAME is set only when DeviceAdmin
// is called via Apps -> Uninstall for an active device admin. Set the package name to
// uninstall, which will enable and show the "Deactivate & uninstall" button.
setPackageToUninstall(adminPackage, admin);
} else {
// When activating, make sure the given component name is actually a valid device admin.
// No need to check this when deactivating, because it is safe to deactivate an active
// invalid device admin.
if (!isValidAdmin(context, admin)) {
LOG.w("Request to add invalid device admin: " + admin.flattenToShortString());
activity.finish();
return;
}
}
// TODO(b/202342351): both this method and isValidAdmin() call PM to get the ActivityInfo;
// they should be refactored so it's called just onces; similarly, isValidAdmin()
// also create a DeviceAdminInfo
DeviceAdminInfo deviceAdminInfo = getDeviceAdminInfo(context, admin);
LOG.d("Admin: " + admin + " DeviceAdminInfo: " + deviceAdminInfo);
if (deviceAdminInfo == null) {
LOG.w("Finishing " + activity + " as it could not get DeviceAdminInfo for "
+ admin.flattenToShortString());
activity.finish();
return;
}
// This admin already exists, and we have two options at this point:
// 1. If new policy bits are set, show the user the new list.
// 2. If nothing has changed, simply return "OK" immediately.
if (isActionAddDeviceAdminActivity(activity)) {
boolean refreshing = false;
if (mDpm.isAdminActive(admin)) {
if (mDpm.isRemovingAdmin(admin, myUserHandle().getIdentifier())) {
LOG.w("Requested admin is already being removed: " + admin);
activity.finish();
return;
}
ArrayList<DeviceAdminInfo.PolicyInfo> policies = deviceAdminInfo.getUsedPolicies();
for (int i = 0, size = policies.size(); i < size; i++) {
DeviceAdminInfo.PolicyInfo pi = policies.get(i);
if (!mDpm.hasGrantedPolicy(admin, pi.ident)) {
refreshing = true;
break;
}
}
LOG.i("Try to add device admin for " + admin + ", refreshing=" + refreshing);
if (!refreshing) {
// Nothing changed (or policies were removed) - return immediately
activity.setResult(Activity.RESULT_OK);
activity.finish();
return;
}
// Update the active admin with the refreshed policies.
mDpm.setActiveAdmin(admin, refreshing);
}
}
mAppName = deviceAdminInfo.loadLabel(context.getPackageManager());
boolean showUninstallButton =
(mPackageToUninstall != null) && (mAdminComponentToUninstall != null);
setUninstallButton(context, showUninstallButton);
((DeviceAdminAddHeaderPreferenceController) use(
DeviceAdminAddHeaderPreferenceController.class,
R.string.pk_device_admin_add_header).setDeviceAdmin(deviceAdminInfo))
.setActivationListener((value) -> onActivation(value));
((DeviceAdminAddExplanationPreferenceController) use(
DeviceAdminAddExplanationPreferenceController.class,
R.string.pk_device_admin_add_explanation).setDeviceAdmin(deviceAdminInfo))
.setExplanation(intent
.getCharSequenceExtra(DevicePolicyManager.EXTRA_ADD_EXPLANATION));
use(DeviceAdminAddWarningPreferenceController.class,
R.string.pk_device_admin_add_warning).setDeviceAdmin(deviceAdminInfo);
use(DeviceAdminAddPoliciesPreferenceController.class,
R.string.pk_device_admin_add_policies).setDeviceAdmin(deviceAdminInfo);
use(DeviceAdminAddSupportPreferenceController.class,
R.string.pk_device_admin_add_support).setDeviceAdmin(deviceAdminInfo);
}
private void onActivation(boolean value) {
Activity activity = requireActivity();
if (!isActionAddDeviceAdminActivity(activity)) {
return;
}
int result = value ? Activity.RESULT_OK : Activity.RESULT_CANCELED;
LOG.d("Setting " + activity + " result to " + result);
activity.setResult(result);
}
@VisibleForTesting
void setPackageToUninstall(String packageName, ComponentName componentName) {
mPackageToUninstall = packageName;
mAdminComponentToUninstall = componentName;
}
@VisibleForTesting
void setUninstallButton(Context context, boolean showButton) {
mUninstallButton = new MenuItem.Builder(context)
.setTitle(R.string.deactivate_and_uninstall_device_admin)
.setEnabled(showButton)
.setVisible(showButton)
.setOnClickListener(i -> startUninstall())
.build();
}
@VisibleForTesting
void startUninstall() {
mIsActive = mDpm.isAdminActive(mAdminComponentToUninstall);
if (mIsActive) {
LOG.i("Deactivating device admin: " + mAdminComponentToUninstall);
mDpm.removeActiveAdmin(mAdminComponentToUninstall);
}
LOG.i("Uninstalling package: " + mPackageToUninstall);
Uri packageUri = Uri.parse("package:" + mPackageToUninstall);
Intent uninstallIntent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri);
uninstallIntent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
startActivityForResult(uninstallIntent, UNINSTALL_DEVICE_ADMIN_REQUEST_CODE);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
// Call super method to handle callback.
super.onActivityResult(requestCode, resultCode, data);
if (requestCode != UNINSTALL_DEVICE_ADMIN_REQUEST_CODE) {
return;
}
if (resultCode == RESULT_OK) {
Activity activity = requireActivity();
// On successful uninstalling, sets the results and finish the activity.
activity.setResult(RESULT_OK);
activity.finish();
} else {
if (mIsActive) {
// Set active admin back when uninstalling was failed/canceled.
mDpm.setActiveAdmin(mAdminComponentToUninstall, /* refreshing= */ false);
}
LOG.e("Uninstall failed with result " + resultCode);
}
}
@Override
public void setPreferenceScreen(PreferenceScreen preferenceScreen) {
super.setPreferenceScreen(preferenceScreen);
// Split for testing, to avoid calling super.setPreferenceScreen() in tests.
setPreferenceScreenTitle(preferenceScreen);
}
@VisibleForTesting
void setPreferenceScreenTitle(PreferenceScreen preferenceScreen) {
if (!TextUtils.isEmpty(mAppName)) {
preferenceScreen.setTitle(mAppName);
}
}
@Override
protected void setupToolbar(ToolbarController toolbar) {
super.setupToolbar(toolbar);
// Must split in 2, otherwise tests would need to mock what's needed by super.setupToolbar()
setToolbarTitle(toolbar);
}
@VisibleForTesting
void setToolbarTitle(ToolbarController toolbar) {
if (isActionAddDeviceAdminActivity(requireActivity())) {
toolbar.setTitle(R.string.add_device_admin_msg);
}
}
private boolean isActionAddDeviceAdminActivity(Activity activity) {
Intent intent = activity.getIntent();
String action = intent == null ? null : intent.getAction();
return DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN.equals(action);
}
// Must override so it can be spied (it's the exact same signature and modifier access, but it
// works now because the test class is in the same package).
@Override
protected <T extends PreferenceController> T use(Class<T> clazz, int preferenceKeyResId) {
return super.use(clazz, preferenceKeyResId);
}
private boolean isValidAdmin(Context context, ComponentName who) {
PackageManager pm = context.getPackageManager();
ActivityInfo ai;
try {
ai = pm.getReceiverInfo(who, PackageManager.GET_META_DATA);
} catch (PackageManager.NameNotFoundException e) {
LOG.w("Unable to retrieve device policy " + who, e);
return false;
}
if (mDpm.isAdminActive(who)) {
return true;
}
List<ResolveInfo> avail = pm.queryBroadcastReceivers(
new Intent(DeviceAdminReceiver.ACTION_DEVICE_ADMIN_ENABLED),
PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS);
int count = avail == null ? 0 : avail.size();
boolean found = false;
for (int i = 0; i < count; i++) {
ResolveInfo ri = avail.get(i);
if (ai.packageName.equals(ri.activityInfo.packageName)
&& ai.name.equals(ri.activityInfo.name)) {
try {
// We didn't retrieve the meta data for all possible matches, so
// need to use the activity info of this specific one that was retrieved.
ri.activityInfo = ai;
new DeviceAdminInfo(context, ri);
found = true;
} catch (Exception e) {
LOG.w("Bad " + ri.activityInfo, e);
}
break;
}
}
if (!found) {
LOG.d("didn't find enabled admin receiver for " + who.flattenToShortString());
}
return found;
}
}