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