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