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