1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.car.settings.enterprise; 18 19 import static android.app.Activity.RESULT_OK; 20 import static android.os.Process.myUserHandle; 21 22 import static com.android.car.settings.enterprise.EnterpriseUtils.getAdminWithinPackage; 23 import static com.android.car.settings.enterprise.EnterpriseUtils.getDeviceAdminInfo; 24 25 import android.app.Activity; 26 import android.app.admin.DeviceAdminInfo; 27 import android.app.admin.DeviceAdminReceiver; 28 import android.app.admin.DevicePolicyManager; 29 import android.content.ComponentName; 30 import android.content.Context; 31 import android.content.Intent; 32 import android.content.pm.ActivityInfo; 33 import android.content.pm.PackageManager; 34 import android.content.pm.ResolveInfo; 35 import android.net.Uri; 36 import android.text.TextUtils; 37 38 import androidx.annotation.VisibleForTesting; 39 import androidx.preference.PreferenceScreen; 40 41 import com.android.car.settings.R; 42 import com.android.car.settings.common.Logger; 43 import com.android.car.settings.common.PreferenceController; 44 import com.android.car.settings.common.SettingsFragment; 45 import com.android.car.ui.toolbar.MenuItem; 46 import com.android.car.ui.toolbar.ToolbarController; 47 48 import java.util.ArrayList; 49 import java.util.Collections; 50 import java.util.List; 51 52 /** 53 * A screen that shows details about a device administrator. 54 */ 55 public final class DeviceAdminAddFragment extends SettingsFragment { 56 57 private static final Logger LOG = new Logger(DeviceAdminAddFragment.class); 58 59 @VisibleForTesting 60 static final int UNINSTALL_DEVICE_ADMIN_REQUEST_CODE = 12; 61 62 private CharSequence mAppName; 63 private DevicePolicyManager mDpm; 64 private String mPackageToUninstall; 65 private ComponentName mAdminComponentToUninstall; 66 private boolean mIsActive; 67 private MenuItem mUninstallButton; 68 69 @VisibleForTesting setDevicePolicyManager(DevicePolicyManager dpm)70 void setDevicePolicyManager(DevicePolicyManager dpm) { 71 mDpm = dpm; 72 } 73 74 @Override getPreferenceScreenResId()75 protected int getPreferenceScreenResId() { 76 return R.xml.device_admin_add; 77 } 78 79 @Override getToolbarMenuItems()80 public List<MenuItem> getToolbarMenuItems() { 81 return Collections.singletonList(mUninstallButton); 82 } 83 84 @Override onAttach(Context context)85 public void onAttach(Context context) { 86 super.onAttach(context); 87 88 // Split in 2 as it would be hard to mock requireActivity(); 89 onAttach(context, requireActivity()); 90 } 91 92 @VisibleForTesting onAttach(Context context, Activity activity)93 void onAttach(Context context, Activity activity) { 94 setDevicePolicyManager(context.getSystemService(DevicePolicyManager.class)); 95 Intent intent = activity.getIntent(); 96 if (intent == null) { 97 LOG.e("no intent on " + activity); 98 activity.finish(); 99 return; 100 } 101 102 ComponentName admin = (ComponentName) 103 intent.getParcelableExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN); 104 LOG.d("Admin using " + DevicePolicyManager.EXTRA_DEVICE_ADMIN + ": " + admin); 105 if (admin == null) { 106 String adminPackage = intent 107 .getStringExtra(DeviceAdminAddActivity.EXTRA_DEVICE_ADMIN_PACKAGE_NAME); 108 LOG.d("Admin package using " + DeviceAdminAddActivity.EXTRA_DEVICE_ADMIN_PACKAGE_NAME 109 + ": " + adminPackage); 110 if (adminPackage == null) { 111 LOG.w("Finishing " + activity + " as its intent doesn't have " 112 + DevicePolicyManager.EXTRA_DEVICE_ADMIN + " or " 113 + DeviceAdminAddActivity.EXTRA_DEVICE_ADMIN_PACKAGE_NAME); 114 activity.finish(); 115 return; 116 } 117 admin = getAdminWithinPackage(context, adminPackage); 118 if (admin == null) { 119 LOG.w("Finishing " + activity + " as there is no active admin for " + adminPackage); 120 activity.finish(); 121 return; 122 } 123 // DeviceAdminAddActivity.EXTRA_DEVICE_ADMIN_PACKAGE_NAME is set only when DeviceAdmin 124 // is called via Apps -> Uninstall for an active device admin. Set the package name to 125 // uninstall, which will enable and show the "Deactivate & uninstall" button. 126 setPackageToUninstall(adminPackage, admin); 127 } else { 128 // When activating, make sure the given component name is actually a valid device admin. 129 // No need to check this when deactivating, because it is safe to deactivate an active 130 // invalid device admin. 131 if (!isValidAdmin(context, admin)) { 132 LOG.w("Request to add invalid device admin: " + admin.flattenToShortString()); 133 activity.finish(); 134 return; 135 } 136 } 137 138 // TODO(b/202342351): both this method and isValidAdmin() call PM to get the ActivityInfo; 139 // they should be refactored so it's called just onces; similarly, isValidAdmin() 140 // also create a DeviceAdminInfo 141 DeviceAdminInfo deviceAdminInfo = getDeviceAdminInfo(context, admin); 142 LOG.d("Admin: " + admin + " DeviceAdminInfo: " + deviceAdminInfo); 143 144 if (deviceAdminInfo == null) { 145 LOG.w("Finishing " + activity + " as it could not get DeviceAdminInfo for " 146 + admin.flattenToShortString()); 147 activity.finish(); 148 return; 149 } 150 151 // This admin already exists, and we have two options at this point: 152 // 1. If new policy bits are set, show the user the new list. 153 // 2. If nothing has changed, simply return "OK" immediately. 154 if (isActionAddDeviceAdminActivity(activity)) { 155 boolean refreshing = false; 156 if (mDpm.isAdminActive(admin)) { 157 if (mDpm.isRemovingAdmin(admin, myUserHandle().getIdentifier())) { 158 LOG.w("Requested admin is already being removed: " + admin); 159 activity.finish(); 160 return; 161 } 162 ArrayList<DeviceAdminInfo.PolicyInfo> policies = deviceAdminInfo.getUsedPolicies(); 163 for (int i = 0, size = policies.size(); i < size; i++) { 164 DeviceAdminInfo.PolicyInfo pi = policies.get(i); 165 if (!mDpm.hasGrantedPolicy(admin, pi.ident)) { 166 refreshing = true; 167 break; 168 } 169 } 170 LOG.i("Try to add device admin for " + admin + ", refreshing=" + refreshing); 171 if (!refreshing) { 172 // Nothing changed (or policies were removed) - return immediately 173 activity.setResult(Activity.RESULT_OK); 174 activity.finish(); 175 return; 176 } 177 // Update the active admin with the refreshed policies. 178 mDpm.setActiveAdmin(admin, refreshing); 179 } 180 } 181 182 mAppName = deviceAdminInfo.loadLabel(context.getPackageManager()); 183 184 boolean showUninstallButton = 185 (mPackageToUninstall != null) && (mAdminComponentToUninstall != null); 186 setUninstallButton(context, showUninstallButton); 187 188 ((DeviceAdminAddHeaderPreferenceController) use( 189 DeviceAdminAddHeaderPreferenceController.class, 190 R.string.pk_device_admin_add_header).setDeviceAdmin(deviceAdminInfo)) 191 .setActivationListener((value) -> onActivation(value)); 192 ((DeviceAdminAddExplanationPreferenceController) use( 193 DeviceAdminAddExplanationPreferenceController.class, 194 R.string.pk_device_admin_add_explanation).setDeviceAdmin(deviceAdminInfo)) 195 .setExplanation(intent 196 .getCharSequenceExtra(DevicePolicyManager.EXTRA_ADD_EXPLANATION)); 197 use(DeviceAdminAddWarningPreferenceController.class, 198 R.string.pk_device_admin_add_warning).setDeviceAdmin(deviceAdminInfo); 199 use(DeviceAdminAddPoliciesPreferenceController.class, 200 R.string.pk_device_admin_add_policies).setDeviceAdmin(deviceAdminInfo); 201 use(DeviceAdminAddSupportPreferenceController.class, 202 R.string.pk_device_admin_add_support).setDeviceAdmin(deviceAdminInfo); 203 } 204 onActivation(boolean value)205 private void onActivation(boolean value) { 206 Activity activity = requireActivity(); 207 if (!isActionAddDeviceAdminActivity(activity)) { 208 return; 209 } 210 211 int result = value ? Activity.RESULT_OK : Activity.RESULT_CANCELED; 212 LOG.d("Setting " + activity + " result to " + result); 213 activity.setResult(result); 214 } 215 216 @VisibleForTesting setPackageToUninstall(String packageName, ComponentName componentName)217 void setPackageToUninstall(String packageName, ComponentName componentName) { 218 mPackageToUninstall = packageName; 219 mAdminComponentToUninstall = componentName; 220 } 221 222 @VisibleForTesting setUninstallButton(Context context, boolean showButton)223 void setUninstallButton(Context context, boolean showButton) { 224 mUninstallButton = new MenuItem.Builder(context) 225 .setTitle(R.string.deactivate_and_uninstall_device_admin) 226 .setEnabled(showButton) 227 .setVisible(showButton) 228 .setOnClickListener(i -> startUninstall()) 229 .build(); 230 } 231 232 @VisibleForTesting startUninstall()233 void startUninstall() { 234 mIsActive = mDpm.isAdminActive(mAdminComponentToUninstall); 235 if (mIsActive) { 236 LOG.i("Deactivating device admin: " + mAdminComponentToUninstall); 237 mDpm.removeActiveAdmin(mAdminComponentToUninstall); 238 } 239 LOG.i("Uninstalling package: " + mPackageToUninstall); 240 Uri packageUri = Uri.parse("package:" + mPackageToUninstall); 241 Intent uninstallIntent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri); 242 uninstallIntent.putExtra(Intent.EXTRA_RETURN_RESULT, true); 243 startActivityForResult(uninstallIntent, UNINSTALL_DEVICE_ADMIN_REQUEST_CODE); 244 } 245 246 @Override onActivityResult(int requestCode, int resultCode, Intent data)247 public void onActivityResult(int requestCode, int resultCode, Intent data) { 248 // Call super method to handle callback. 249 super.onActivityResult(requestCode, resultCode, data); 250 if (requestCode != UNINSTALL_DEVICE_ADMIN_REQUEST_CODE) { 251 return; 252 } 253 if (resultCode == RESULT_OK) { 254 Activity activity = requireActivity(); 255 // On successful uninstalling, sets the results and finish the activity. 256 activity.setResult(RESULT_OK); 257 activity.finish(); 258 } else { 259 if (mIsActive) { 260 // Set active admin back when uninstalling was failed/canceled. 261 mDpm.setActiveAdmin(mAdminComponentToUninstall, /* refreshing= */ false); 262 } 263 LOG.e("Uninstall failed with result " + resultCode); 264 } 265 } 266 267 @Override setPreferenceScreen(PreferenceScreen preferenceScreen)268 public void setPreferenceScreen(PreferenceScreen preferenceScreen) { 269 super.setPreferenceScreen(preferenceScreen); 270 271 // Split for testing, to avoid calling super.setPreferenceScreen() in tests. 272 setPreferenceScreenTitle(preferenceScreen); 273 } 274 275 @VisibleForTesting setPreferenceScreenTitle(PreferenceScreen preferenceScreen)276 void setPreferenceScreenTitle(PreferenceScreen preferenceScreen) { 277 if (!TextUtils.isEmpty(mAppName)) { 278 preferenceScreen.setTitle(mAppName); 279 } 280 } 281 282 @Override setupToolbar(ToolbarController toolbar)283 protected void setupToolbar(ToolbarController toolbar) { 284 super.setupToolbar(toolbar); 285 286 // Must split in 2, otherwise tests would need to mock what's needed by super.setupToolbar() 287 setToolbarTitle(toolbar); 288 } 289 290 @VisibleForTesting setToolbarTitle(ToolbarController toolbar)291 void setToolbarTitle(ToolbarController toolbar) { 292 if (isActionAddDeviceAdminActivity(requireActivity())) { 293 toolbar.setTitle(R.string.add_device_admin_msg); 294 } 295 } 296 isActionAddDeviceAdminActivity(Activity activity)297 private boolean isActionAddDeviceAdminActivity(Activity activity) { 298 Intent intent = activity.getIntent(); 299 String action = intent == null ? null : intent.getAction(); 300 return DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN.equals(action); 301 } 302 303 // Must override so it can be spied (it's the exact same signature and modifier access, but it 304 // works now because the test class is in the same package). 305 @Override use(Class<T> clazz, int preferenceKeyResId)306 protected <T extends PreferenceController> T use(Class<T> clazz, int preferenceKeyResId) { 307 return super.use(clazz, preferenceKeyResId); 308 } 309 isValidAdmin(Context context, ComponentName who)310 private boolean isValidAdmin(Context context, ComponentName who) { 311 PackageManager pm = context.getPackageManager(); 312 ActivityInfo ai; 313 try { 314 ai = pm.getReceiverInfo(who, PackageManager.GET_META_DATA); 315 } catch (PackageManager.NameNotFoundException e) { 316 LOG.w("Unable to retrieve device policy " + who, e); 317 return false; 318 } 319 320 if (mDpm.isAdminActive(who)) { 321 return true; 322 } 323 List<ResolveInfo> avail = pm.queryBroadcastReceivers( 324 new Intent(DeviceAdminReceiver.ACTION_DEVICE_ADMIN_ENABLED), 325 PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS); 326 int count = avail == null ? 0 : avail.size(); 327 boolean found = false; 328 for (int i = 0; i < count; i++) { 329 ResolveInfo ri = avail.get(i); 330 if (ai.packageName.equals(ri.activityInfo.packageName) 331 && ai.name.equals(ri.activityInfo.name)) { 332 try { 333 // We didn't retrieve the meta data for all possible matches, so 334 // need to use the activity info of this specific one that was retrieved. 335 ri.activityInfo = ai; 336 new DeviceAdminInfo(context, ri); 337 found = true; 338 } catch (Exception e) { 339 LOG.w("Bad " + ri.activityInfo, e); 340 } 341 break; 342 } 343 } 344 if (!found) { 345 LOG.d("didn't find enabled admin receiver for " + who.flattenToShortString()); 346 } 347 return found; 348 } 349 } 350