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.users;
17 
18 import static android.car.user.CarUserManager.USER_IDENTIFICATION_ASSOCIATION_SET_VALUE_ASSOCIATE_CURRENT_USER;
19 import static android.car.user.CarUserManager.USER_IDENTIFICATION_ASSOCIATION_SET_VALUE_DISASSOCIATE_CURRENT_USER;
20 import static android.car.user.CarUserManager.USER_IDENTIFICATION_ASSOCIATION_TYPE_KEY_FOB;
21 import static android.car.user.CarUserManager.USER_IDENTIFICATION_ASSOCIATION_VALUE_ASSOCIATE_CURRENT_USER;
22 
23 import android.annotation.Nullable;
24 import android.app.AlertDialog;
25 import android.car.Car;
26 import android.car.SyncResultCallback;
27 import android.car.user.CarUserManager;
28 import android.car.user.UserCreationResult;
29 import android.car.user.UserIdentificationAssociationResponse;
30 import android.car.user.UserRemovalResult;
31 import android.car.user.UserSwitchRequest;
32 import android.car.user.UserSwitchResult;
33 import android.car.util.concurrent.AsyncFuture;
34 import android.content.pm.UserInfo;
35 import android.os.Bundle;
36 import android.os.UserHandle;
37 import android.os.UserManager;
38 import android.os.storage.StorageManager;
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.Button;
46 import android.widget.CheckBox;
47 import android.widget.EditText;
48 
49 import androidx.fragment.app.Fragment;
50 
51 import com.google.android.car.kitchensink.KitchenSinkActivity;
52 import com.google.android.car.kitchensink.R;
53 
54 import java.util.concurrent.TimeUnit;
55 import java.util.concurrent.TimeoutException;
56 
57 /**
58  * Shows information (and actions) about the current user.
59  *
60  * <p>Could / should be improved to:
61  *
62  * <ul>
63  *   <li>Add more actions like renaming or deleting the user.
64  *   <li>Add actions for other users (switch, create, remove etc).
65  *   <li>Add option on how to execute tasks above (UserManager or CarUserManager).
66  *   <li>Merge with UserRestrictions and ProfileUser fragments.
67  * </ul>
68  */
69 public final class UserFragment extends Fragment {
70 
71     private static final String TAG = UserFragment.class.getSimpleName();
72 
73     private static final long TIMEOUT_MS = 5_000;
74     private static final long SWITCH_USER_TIMEOUT_MS = 20_000;
75 
76     private final int mUserId = UserHandle.myUserId();
77     private UserManager mUserManager;
78     private CarUserManager mCarUserManager;
79 
80     // Current user
81     private UserInfoView mCurrentUser;
82 
83     private CheckBox mIsAdminCheckBox;
84     private CheckBox mIsAssociatedKeyFobCheckBox;
85 
86     // Existing users
87     private ExistingUsersView mCurrentUsers;
88     private Button mSwitchUserButton;
89     private Button mRemoveUserButton;
90     private Button mLockUserDataButton;
91     private EditText mNewUserNameText;
92     private CheckBox mNewUserIsAdminCheckBox;
93     private CheckBox mNewUserIsGuestCheckBox;
94     private EditText mNewUserExtraFlagsText;
95     private Button mCreateUserButton;
96 
97 
98     @Nullable
99     @Override
onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)100     public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
101             @Nullable Bundle savedInstanceState) {
102         return inflater.inflate(R.layout.user, container, false);
103     }
104 
105     @Override
onViewCreated(View view, Bundle savedInstanceState)106     public void onViewCreated(View view, Bundle savedInstanceState) {
107         mUserManager = UserManager.get(getContext());
108         Car car = ((KitchenSinkActivity) getHost()).getCar();
109         mCarUserManager = (CarUserManager) car.getCarManager(Car.CAR_USER_SERVICE);
110 
111         mCurrentUser = view.findViewById(R.id.current_user);
112         mIsAdminCheckBox = view.findViewById(R.id.is_admin);
113         mIsAssociatedKeyFobCheckBox = view.findViewById(R.id.is_associated_key_fob);
114 
115         mCurrentUsers = view.findViewById(R.id.current_users);
116         mSwitchUserButton = view.findViewById(R.id.switch_user);
117         mRemoveUserButton = view.findViewById(R.id.remove_user);
118         mLockUserDataButton = view.findViewById(R.id.lock_user_data);
119         mNewUserNameText = view.findViewById(R.id.new_user_name);
120         mNewUserIsAdminCheckBox = view.findViewById(R.id.new_user_is_admin);
121         mNewUserIsGuestCheckBox = view.findViewById(R.id.new_user_is_guest);
122 
123         mNewUserExtraFlagsText = view.findViewById(R.id.new_user_flags);
124         mCreateUserButton = view.findViewById(R.id.create_user);
125 
126         mIsAdminCheckBox.setOnClickListener((v) -> toggleAdmin());
127         mSwitchUserButton.setOnClickListener((v) -> switchUser());
128         mRemoveUserButton.setOnClickListener((v) -> removeUser());
129         mCreateUserButton.setOnClickListener((v) -> createUser());
130         mLockUserDataButton.setOnClickListener((v) -> lockUserData());
131         mIsAssociatedKeyFobCheckBox.setOnClickListener((v) -> toggleKeyFob());
132 
133         updateState();
134     }
135 
toggleAdmin()136     private void toggleAdmin() {
137         if (mIsAdminCheckBox.isChecked()) {
138             new AlertDialog.Builder(getContext())
139                     .setMessage("Promoting a user as admin is irreversible.\n\n Confirm?")
140                     .setNegativeButton("No", (d, w) -> promoteCurrentUserAsAdmin(false))
141                     .setPositiveButton("Yes", (d, w) -> promoteCurrentUserAsAdmin(true))
142                     .show();
143         } else {
144             // Shouldn't be called
145             Log.w(TAG, "Cannot un-set an admin user");
146         }
147     }
148 
toggleKeyFob()149     private void toggleKeyFob() {
150         associateKeyFob(mIsAssociatedKeyFobCheckBox.isChecked());
151     }
152 
createUser()153     private void createUser() {
154         String name = mNewUserNameText.getText().toString();
155         if (TextUtils.isEmpty(name)) {
156             name = null;
157         }
158         int flags = 0;
159         boolean isGuest = mNewUserIsGuestCheckBox.isChecked();
160         UserCreationResult result;
161         UserInfo userInfo;
162         Log.v(TAG, "Create user: name=" + name + ", flags="
163                 + UserInfo.flagsToString(flags) + ", is guest=" + isGuest);
164         if (isGuest) {
165             result = getResult(mCarUserManager.createGuest(name));
166         } else {
167             if (mNewUserIsAdminCheckBox.isChecked()) {
168                 flags |= UserInfo.FLAG_ADMIN;
169             }
170             String extraFlags = mNewUserExtraFlagsText.getText().toString();
171             if (!TextUtils.isEmpty(extraFlags)) {
172                 try {
173                     flags |= Integer.parseInt(extraFlags);
174                 } catch (RuntimeException e) {
175                     Log.e(TAG, "createUser(): non-numeric flags " + extraFlags);
176                 }
177             }
178             Log.v(TAG, "Create user: name=" + name + ", flags=" + UserInfo.flagsToString(flags));
179             result = getResult(mCarUserManager.createUser(name, flags));
180         }
181         updateState();
182         StringBuilder message = new StringBuilder();
183         if (result == null) {
184             message.append("Timed out creating user");
185         } else {
186             if (result.isSuccess()) {
187                 message.append("User created: ").append(result.getUser().toString());
188             } else {
189                 int status = result.getStatus();
190                 message.append("Failed with code ").append(status).append('(')
191                         .append(UserCreationResult.statusToString(status)).append(')');
192                 message.append("\nFull result: ").append(result);
193             }
194             String error = result.getErrorMessage();
195             if (error != null) {
196                 message.append("\nError message: ").append(error);
197             }
198         }
199         showMessage(message.toString());
200     }
201 
removeUser()202     private void removeUser() {
203         int userId = mCurrentUsers.getSelectedUserId();
204         Log.i(TAG, "Remove user: " + userId);
205         UserRemovalResult result = mCarUserManager.removeUser(userId);
206         updateState();
207 
208         if (result.isSuccess()) {
209             showMessage("User %d removed", userId);
210         } else {
211             showMessage("Failed to remove user %d: %s", userId,
212                     UserRemovalResult.statusToString(result.getStatus()));
213         }
214     }
215 
switchUser()216     private void switchUser() {
217         int userId = mCurrentUsers.getSelectedUserId();
218         Log.i(TAG, "Switch user: " + userId);
219         SyncResultCallback<UserSwitchResult> userSwitchResultCallback =
220                 new SyncResultCallback<>();
221         mCarUserManager.switchUser(new UserSwitchRequest.Builder(UserHandle.of(userId)).build(),
222                 Runnable::run, userSwitchResultCallback);
223         UserSwitchResult result = new UserSwitchResult(UserSwitchResult.STATUS_ANDROID_FAILURE,
224                 null);
225         try {
226             result = userSwitchResultCallback.get(SWITCH_USER_TIMEOUT_MS, TimeUnit.MILLISECONDS);
227         } catch (TimeoutException e) {
228             Log.e(TAG, "switchUser(" + userId + ") : timed out while waiting for result");
229         } catch (InterruptedException e) {
230             Thread.currentThread().interrupt();
231             Log.e(TAG, "switchUser(" + userId + ") : interrupted while waiting for result");
232         }
233         updateState();
234 
235         StringBuilder message = new StringBuilder();
236         if (result == null) {
237             message.append("Timed out switching user");
238         } else {
239             int status = result.getStatus();
240             if (result.isSuccess()) {
241                 message.append("Switched to user ").append(userId).append(" (status=")
242                         .append(UserSwitchResult.statusToString(status)).append(')');
243             } else {
244                 message.append("Failed with code ").append(status).append('(')
245                         .append(UserSwitchResult.statusToString(status)).append(')');
246             }
247             String error = result.getErrorMessage();
248             if (error != null) {
249                 message.append("\nError message: ").append(error);
250             }
251         }
252         showMessage(message.toString());
253     }
254 
lockUserData()255     private void lockUserData() {
256         int userToLock = mCurrentUsers.getSelectedUserId();
257         if (userToLock == UserHandle.USER_NULL) {
258             return;
259         }
260 
261         StorageManager storageManager = getContext().getSystemService(StorageManager.class);
262 
263         try {
264             storageManager.lockCeStorage(userToLock);
265         } catch (Exception e) {
266             showMessage("Error: lock user data: " + e);
267         }
268     }
269 
promoteCurrentUserAsAdmin(boolean promote)270     private void promoteCurrentUserAsAdmin(boolean promote) {
271         if (!promote) {
272             Log.d(TAG, "NOT promoting user " + mUserId + " as admin");
273         } else {
274             Log.d(TAG, "Promoting user " + mUserId + " as admin");
275             mUserManager.setUserAdmin(mUserId);
276         }
277         updateState();
278     }
279 
updateState()280     private void updateState() {
281         // Current user
282         boolean isAdmin = mUserManager.isAdminUser();
283         boolean isAssociatedKeyFob = isAssociatedKeyFob();
284         UserInfo user = mUserManager.getUserInfo(mUserId);
285         Log.v(TAG, "updateState(): user= " + user + ", isAdmin=" + isAdmin
286                 + ", isAssociatedKeyFob=" + isAssociatedKeyFob);
287         mCurrentUser.update(user);
288         mIsAdminCheckBox.setChecked(isAdmin);
289         mIsAdminCheckBox.setEnabled(!isAdmin); // there's no API to "un-admin a user"
290         mIsAssociatedKeyFobCheckBox.setChecked(isAssociatedKeyFob);
291 
292         // Existing users
293         mCurrentUsers.updateState();
294     }
295 
isAssociatedKeyFob()296     private boolean isAssociatedKeyFob() {
297         UserIdentificationAssociationResponse result = mCarUserManager
298                 .getUserIdentificationAssociation(USER_IDENTIFICATION_ASSOCIATION_TYPE_KEY_FOB);
299         if (!result.isSuccess()) {
300             Log.e(TAG, "isAssociatedKeyFob() failed: " + result);
301             return false;
302         }
303         return result.getValues()[0]
304                 == USER_IDENTIFICATION_ASSOCIATION_VALUE_ASSOCIATE_CURRENT_USER;
305     }
306 
associateKeyFob(boolean associate)307     private void associateKeyFob(boolean associate) {
308         int value = associate ? USER_IDENTIFICATION_ASSOCIATION_SET_VALUE_ASSOCIATE_CURRENT_USER :
309                 USER_IDENTIFICATION_ASSOCIATION_SET_VALUE_DISASSOCIATE_CURRENT_USER;
310         Log.d(TAG, "associateKey(" + associate + "): setting to " + DebugUtils.constantToString(
311                 CarUserManager.class, /* prefix= */ "", value));
312 
313         AsyncFuture<UserIdentificationAssociationResponse> future = mCarUserManager
314                 .setUserIdentificationAssociation(
315                         new int[] { USER_IDENTIFICATION_ASSOCIATION_TYPE_KEY_FOB },
316                         new int[] { value });
317         UserIdentificationAssociationResponse result = getResult(future);
318         Log.d(TAG, "Result: " + result);
319 
320         String error = null;
321         boolean associated = associate;
322 
323         if (result == null) {
324             error = "Timed out associating key fob";
325         } else {
326             if (!result.isSuccess()) {
327                 error = "HAL call failed: " + result;
328             } else {
329                 int newValue = result.getValues()[0];
330                 String newValueName = DebugUtils.constantToString(CarUserManager.class,
331                         /* prefix= */ "", newValue);
332                 Log.d(TAG, "New status: " + newValueName);
333                 associated = (
334                         newValue == USER_IDENTIFICATION_ASSOCIATION_VALUE_ASSOCIATE_CURRENT_USER);
335                 if (associated != associate) {
336                     error = "Result doesn't match request: " + newValueName;
337                 }
338             }
339         }
340         if (error != null) {
341             showMessage("associateKeyFob(" + associate + ") failed: " + error);
342         }
343         updateState();
344     }
345 
showMessage(String pattern, Object... args)346     private void showMessage(String pattern, Object... args) {
347         String message = String.format(pattern, args);
348         Log.v(TAG, "showMessage(): " + message);
349         new AlertDialog.Builder(getContext()).setMessage(message).show();
350     }
351 
352     @Nullable
getResult(AsyncFuture<T> future)353     private static <T> T getResult(AsyncFuture<T> future) {
354         future.whenCompleteAsync((r, e) -> {
355             if (e != null) {
356                 Log.e(TAG, "You have no future!", e);
357                 return;
358             }
359             Log.v(TAG, "The future is here: " + r);
360         }, Runnable::run);
361 
362         T result = null;
363         try {
364             result = future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
365             if (result == null) {
366                 Log.e(TAG, "Timeout (" + TIMEOUT_MS + "ms) waiting for future " + future);
367             }
368         } catch (InterruptedException e) {
369             Log.e(TAG, "Interrupted waiting for future " + future, e);
370             Thread.currentThread().interrupt();
371         } catch (Exception e) {
372             Log.e(TAG, "Exception getting future " + future, e);
373         }
374         return result;
375     }
376 }
377