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