• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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