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