1 /*
2  * Copyright (C) 2020 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 package com.google.android.car.kitchensink.admin;
17 
18 import static android.app.admin.DeviceAdminReceiver.ACTION_DEVICE_ADMIN_ENABLED;
19 
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.app.AlertDialog;
23 import android.app.admin.DevicePolicyManager;
24 import android.car.Car;
25 import android.car.admin.CarDevicePolicyManager;
26 import android.car.admin.CreateUserResult;
27 import android.car.admin.RemoveUserResult;
28 import android.car.admin.StartUserInBackgroundResult;
29 import android.car.admin.StopUserResult;
30 import android.content.ComponentName;
31 import android.content.Context;
32 import android.content.Intent;
33 import android.content.pm.PackageManager;
34 import android.content.pm.ResolveInfo;
35 import android.content.pm.UserInfo;
36 import android.os.Bundle;
37 import android.os.UserHandle;
38 import android.os.UserManager;
39 import android.text.TextUtils;
40 import android.util.DebugUtils;
41 import android.util.Log;
42 import android.view.LayoutInflater;
43 import android.view.View;
44 import android.view.ViewGroup;
45 import android.widget.ArrayAdapter;
46 import android.widget.Button;
47 import android.widget.CheckBox;
48 import android.widget.EditText;
49 import android.widget.Spinner;
50 
51 import androidx.fragment.app.Fragment;
52 
53 import com.google.android.car.kitchensink.KitchenSinkHelper;
54 import com.google.android.car.kitchensink.R;
55 import com.google.android.car.kitchensink.users.ExistingUsersView;
56 import com.google.android.car.kitchensink.users.UserInfoView;
57 
58 import java.util.ArrayList;
59 import java.util.List;
60 
61 /**
62  * Test UI for {@link CarDevicePolicyManager}.
63  */
64 public final class DevicePolicyFragment extends Fragment {
65 
66     private static final String TAG = DevicePolicyFragment.class.getSimpleName();
67 
68     private UserManager mUserManager;
69     private DevicePolicyManager mDevicePolicyManager;
70     private CarDevicePolicyManager mCarDevicePolicyManager;
71 
72     // Current user
73     private UserInfoView mCurrentUser;
74 
75     // Existing users
76     private ExistingUsersView mCurrentUsers;
77 
78     // New user
79     private EditText mNewUserNameText;
80     private CheckBox mNewUserIsAdminCheckBox;
81     private CheckBox mNewUserIsGuestCheckBox;
82     private Button mCreateUserButton;
83 
84     // Reset password
85     private EditText mPasswordText;
86     private Button mResetPasswordButton;
87 
88     // Other actions
89     private Button mRemoveUserButton;
90     private Button mStartUserInBackgroundButton;
91     private Button mStopUserButton;
92     private Button mLockNowButton;
93     private EditText mWipeDataFlagsText;
94     private Button mWipeDataButton;
95 
96     // Lock tasks
97     private Button mCheckLockTasksButton;
98     private Button mStartLockTasksButton;
99     private Button mStopLockTasksButton;
100 
101     // Set device admin
102     private final List<DeviceAdminApp> mDeviceAdminApps = new ArrayList<>();
103     private Spinner mDeviceAdminAppsSpinner;
104     private Button mSetDeviceAdminAppButton;
105     private KitchenSinkHelper mKitchenSinkHelper;
106 
107     @Override
onAttach(@onNull Context context)108     public void onAttach(@NonNull Context context) {
109         super.onAttach(context);
110         if (!(context instanceof KitchenSinkHelper)) {
111             throw new IllegalStateException(
112                     "Attached activity does not implement "
113                             + KitchenSinkHelper.class.getSimpleName());
114         }
115         mKitchenSinkHelper = (KitchenSinkHelper) context;
116     }
117 
118     @Nullable
119     @Override
onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)120     public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
121             @Nullable Bundle savedInstanceState) {
122         return inflater.inflate(R.layout.device_policy, container, false);
123     }
124 
125     @Override
onViewCreated(View view, Bundle savedInstanceState)126     public void onViewCreated(View view, Bundle savedInstanceState) {
127         mUserManager = UserManager.get(getContext());
128         mDevicePolicyManager = getContext().getSystemService(DevicePolicyManager.class);
129         Car car = mKitchenSinkHelper.getCar();
130         mCarDevicePolicyManager = (CarDevicePolicyManager) car
131                 .getCarManager(Car.CAR_DEVICE_POLICY_SERVICE);
132 
133         mCurrentUser = view.findViewById(R.id.current_user);
134         mCurrentUsers = view.findViewById(R.id.current_users);
135         mRemoveUserButton = view.findViewById(R.id.remove_user);
136         mStartUserInBackgroundButton = view.findViewById(R.id.start_user_in_background);
137         mStopUserButton = view.findViewById(R.id.stop_user);
138 
139         mNewUserNameText = view.findViewById(R.id.new_user_name);
140         mNewUserIsAdminCheckBox = view.findViewById(R.id.new_user_is_admin);
141         mNewUserIsGuestCheckBox = view.findViewById(R.id.new_user_is_guest);
142         mCreateUserButton = view.findViewById(R.id.create_user);
143 
144         mRemoveUserButton.setOnClickListener((v) -> removeUser());
145         mCreateUserButton.setOnClickListener((v) -> createUser());
146         mStartUserInBackgroundButton.setOnClickListener((v) -> startUserInBackground());
147         mStopUserButton.setOnClickListener((v) -> stopUser());
148 
149         mPasswordText = view.findViewById(R.id.password);
150         mResetPasswordButton = view.findViewById(R.id.reset_password);
151         mResetPasswordButton.setOnClickListener((v) -> resetPassword());
152 
153         mLockNowButton = view.findViewById(R.id.lock_now);
154         mLockNowButton.setOnClickListener((v) -> lockNow());
155 
156         mWipeDataFlagsText = view.findViewById(R.id.wipe_data_flags);
157         mWipeDataButton = view.findViewById(R.id.wipe_data);
158         mWipeDataButton.setOnClickListener((v) -> wipeData());
159 
160         mCheckLockTasksButton = view.findViewById(R.id.check_lock_tasks);
161         mCheckLockTasksButton.setOnClickListener((v) -> checkLockTasks());
162 
163         mStartLockTasksButton = view.findViewById(R.id.start_lock_tasks);
164         mStartLockTasksButton.setOnClickListener((v) -> startLockTask());
165 
166         mStopLockTasksButton = view.findViewById(R.id.stop_lock_tasks);
167         mStopLockTasksButton.setOnClickListener((v) -> stopLockTasks());
168 
169         mDeviceAdminAppsSpinner = view.findViewById(R.id.device_admin_apps);
170         mSetDeviceAdminAppButton = view.findViewById(R.id.set_device_admin_app);
171         mSetDeviceAdminAppButton.setOnClickListener((v) -> launchSetDeviceAdminIntent());
172 
173         updateState();
174     }
175 
updateState()176     private void updateState() {
177         // Current user
178         int userId = UserHandle.myUserId();
179         UserInfo user = mUserManager.getUserInfo(userId);
180         Log.v(TAG, "updateState(): currentUser= " + user);
181         mCurrentUser.update(user);
182 
183         // Existing users
184         mCurrentUsers.updateState();
185 
186         setAdminApps();
187     }
188 
setAdminApps()189     private void setAdminApps() {
190         mDeviceAdminApps.clear();
191 
192         PackageManager pm = getContext().getPackageManager();
193 
194         List<ResolveInfo> receivers = pm.queryBroadcastReceivers(
195                 new Intent(ACTION_DEVICE_ADMIN_ENABLED), /* flags= */ 0);
196         if (receivers.isEmpty()) {
197             Log.i(TAG, "setDeviceAdminApps(): no receivers for " + ACTION_DEVICE_ADMIN_ENABLED);
198             return;
199         }
200         Log.i(TAG, receivers.size() + " receivers for " + ACTION_DEVICE_ADMIN_ENABLED);
201 
202         String[] entries = new String[receivers.size()];
203         int i = 0;
204         for (ResolveInfo receiver : receivers) {
205             DeviceAdminApp adminApp = new DeviceAdminApp(receiver, pm);
206             Log.d(TAG, "Adding " + adminApp);
207             mDeviceAdminApps.add(adminApp);
208             entries[i++] = adminApp.name;
209         }
210         mDeviceAdminAppsSpinner.setAdapter(
211                 new ArrayAdapter<String>(getContext(), android.R.layout.simple_spinner_item,
212                         entries));
213     }
214 
removeUser()215     private void removeUser() {
216         int userId = mCurrentUsers.getSelectedUserId();
217         Log.i(TAG, "Remove user: " + userId);
218         RemoveUserResult result = mCarDevicePolicyManager.removeUser(UserHandle.of(userId));
219         if (result.isSuccess()) {
220             updateState();
221             showMessage("User %d removed", userId);
222         } else {
223             showMessage("Failed to remove user %d: %s", userId, result);
224         }
225     }
226 
createUser()227     private void createUser() {
228         String name = mNewUserNameText.getText().toString();
229         if (TextUtils.isEmpty(name)) {
230             name = null;
231         }
232         // Type is treated as a flag here so we can emulate an invalid value by selecting both.
233         int type = CarDevicePolicyManager.USER_TYPE_REGULAR;
234         boolean isAdmin = mNewUserIsAdminCheckBox.isChecked();
235         if (isAdmin) {
236             type |= CarDevicePolicyManager.USER_TYPE_ADMIN;
237         }
238         boolean isGuest = mNewUserIsGuestCheckBox.isChecked();
239         if (isGuest) {
240             type |= CarDevicePolicyManager.USER_TYPE_GUEST;
241         }
242         CreateUserResult result = mCarDevicePolicyManager.createUser(name, type);
243         if (result.isSuccess()) {
244             showMessage("User created: %s", result.getUserHandle().getIdentifier());
245             updateState();
246         } else {
247             showMessage("Failed to create user with type %d: %s", type, result);
248         }
249     }
250 
startUserInBackground()251     private void startUserInBackground() {
252         int userId = mCurrentUsers.getSelectedUserId();
253         Log.i(TAG, "Start user in background: " + userId);
254         StartUserInBackgroundResult result =
255                 mCarDevicePolicyManager.startUserInBackground(UserHandle.of(userId));
256         if (result.isSuccess()) {
257             updateState();
258             showMessage("User %d started", userId);
259         } else {
260             showMessage("Failed to start user %d in background: %s", userId, result);
261         }
262     }
263 
stopUser()264     private void stopUser() {
265         int userId = mCurrentUsers.getSelectedUserId();
266         Log.i(TAG, "Stop user: " + userId);
267         StopUserResult result = mCarDevicePolicyManager.stopUser(UserHandle.of(userId));
268         if (result.isSuccess()) {
269             updateState();
270             showMessage("User %d stopped", userId);
271         } else {
272             showMessage("Failed to stop user %d: %s", userId, result);
273         }
274     }
275 
resetPassword()276     private void resetPassword() {
277         String password = mPasswordText.getText().toString();
278         // NOTE: on "real" code the password should NEVER be logged in plain text, but it's fine
279         // here (as it's used for testing / development purposes)
280         Log.i(TAG, "Calling resetPassword('" + password + "')...");
281         run(() -> mDevicePolicyManager.resetPassword(password, /* flags= */ 0), "Password reset!");
282     }
283 
lockNow()284     private void lockNow() {
285         Log.i(TAG, "Calling lockNow()...");
286         run(() -> mDevicePolicyManager.lockNow(), "Locked!");
287     }
288 
wipeData()289     private void wipeData() {
290         new AlertDialog.Builder(getContext())
291             .setMessage("Wiping data is irreversible, are you sure you want to self-destruct?")
292             .setPositiveButton("Yes", (d, w) -> selfDestruct())
293             .show();
294     }
295 
isAllowedToCheckLockTasks()296     private boolean isAllowedToCheckLockTasks() {
297         return mDevicePolicyManager.isLockTaskPermitted(getContext().getPackageName());
298     }
299 
checkLockTasks()300     private void checkLockTasks() {
301         boolean isAllowed = isAllowedToCheckLockTasks();
302         showMessage("KitchenSink %s allowed to lock tasks", isAllowed ? "IS" : "is NOT");
303     }
304 
startLockTask()305     private void startLockTask() {
306         Log.v(TAG, "startLockTask()");
307         if (!isAllowedToCheckLockTasks()) {
308             showMessage("KitchenSink is not allowed to lock tasks, "
309                     + "you must use the DPC app to allow it");
310             return;
311         }
312 
313         try {
314             getActivity().startLockTask();
315         } catch (IllegalStateException e) {
316             showError(e, "No lock task present");
317         }
318     }
319 
stopLockTasks()320     private void stopLockTasks() {
321         Log.v(TAG, "stopLockTasks()");
322         try {
323             getActivity().stopLockTask();
324         } catch (IllegalStateException e) {
325             showError(e, "No lock task present");
326         }
327     }
328 
launchSetDeviceAdminIntent()329     private void launchSetDeviceAdminIntent() {
330         if (mDeviceAdminApps.isEmpty()) {
331             // Should be disabled
332             Log.e(TAG, "setAdminApp(): no admin");
333             return;
334         }
335         int index = mDeviceAdminAppsSpinner.getSelectedItemPosition();
336         DeviceAdminApp app;
337         try {
338             app = mDeviceAdminApps.get(index);
339         } catch (Exception e) {
340             Log.e(TAG, "Could not get app at index " + index, e);
341             return;
342         }
343         Log.v(TAG, "setAdminApp(): index=" + index + ",size=" + mDeviceAdminApps.size() + ",app="
344                 + app);
345         Intent intent = new Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN)
346                 .putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, app.admin);
347         Log.i(TAG, "launching intent " + intent + " for " + app);
348         getActivity().startActivity(intent);
349     }
350 
selfDestruct()351     private void selfDestruct() {
352         int flags = 0;
353         String flagsText = mWipeDataFlagsText.getText().toString();
354         if (!TextUtils.isEmpty(flagsText)) {
355             try {
356                 flags = Integer.parseInt(flagsText);
357             } catch (Exception e) {
358                 Log.e(TAG, "Invalid wipeData flags: " + flagsText);
359             }
360         }
361 
362         String flagsDesc = flags == 0 ? "0" : flags + "("
363                 + DebugUtils.flagsToString(DevicePolicyManager.class, "WIPE_", flags) + ")";
364 
365         Log.i(TAG, "Calling wipeData(" + flagsDesc + ")...");
366         try {
367             mDevicePolicyManager.wipeData(flags, "SelfDestruct");
368         } catch (Exception e) {
369             Log.e(TAG, "wipeData(" + flagsDesc + ") failed", e);
370             showMessage("wipeData(%s) failed: %s", flagsDesc, e);
371         }
372     }
373 
run(@onNull Runnable runnable, @NonNull String successMessage)374     private void run(@NonNull Runnable runnable, @NonNull String successMessage) {
375         try {
376             runnable.run();
377             showMessage(successMessage);
378         } catch (RuntimeException e) {
379             Log.e(TAG, "Failed", e);
380             showMessage("failed: " + e);
381         }
382     }
383 
showMessage(@onNull String pattern, @Nullable Object... args)384     private void showMessage(@NonNull String pattern, @Nullable Object... args) {
385         String message = String.format(pattern, args);
386         Log.v(TAG, "showMessage(): " + message);
387         new AlertDialog.Builder(getContext()).setMessage(message).show();
388     }
389 
showError(@onNull Exception e, @NonNull String pattern, @Nullable Object... args)390     private void showError(@NonNull Exception e, @NonNull String pattern,
391             @Nullable Object... args) {
392         String message = String.format(pattern, args);
393         Log.e(TAG, "showError(): " + message, e);
394         new AlertDialog.Builder(getContext()).setMessage(message).show();
395     }
396 
397     private static final class DeviceAdminApp {
398         public final ComponentName admin;
399         public final String name;
400 
DeviceAdminApp(ResolveInfo resolveInfo, PackageManager pm)401         DeviceAdminApp(ResolveInfo resolveInfo, PackageManager pm) {
402             admin = resolveInfo.getComponentInfo().getComponentName();
403             CharSequence label = resolveInfo.loadLabel(pm);
404             if (TextUtils.isEmpty(label)) {
405                 name = resolveInfo.getComponentInfo().name;
406                 Log.v(TAG, "no label for " + admin.flattenToShortString() + "; using " + name);
407             } else {
408                 name = label.toString();
409             }
410         }
411 
412         @Override
toString()413         public String toString() {
414             return "AdminApp[name=" + name + ", admin=" + admin.flattenToShortString() + ']';
415         }
416     }
417 }
418