1 /*
2  * Copyright (C) 2022 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.android.settings.users;
17 
18 import android.app.Dialog;
19 import android.app.settings.SettingsEnums;
20 import android.content.Context;
21 import android.content.DialogInterface;
22 import android.content.pm.UserInfo;
23 import android.os.Bundle;
24 import android.os.Handler;
25 import android.os.UserManager;
26 import android.provider.Settings;
27 import android.util.Log;
28 
29 import androidx.appcompat.app.AlertDialog;
30 import androidx.fragment.app.Fragment;
31 import androidx.preference.Preference;
32 
33 import com.android.settings.R;
34 import com.android.settings.core.BasePreferenceController;
35 import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
36 import com.android.settingslib.RestrictedSwitchPreference;
37 
38 /**
39  * Controller to control the preference toggle for "remove guest on exit"
40  *
41  * Note, class is not 'final' since we need to mock it for unit tests
42  */
43 public class RemoveGuestOnExitPreferenceController extends BasePreferenceController
44                 implements  Preference.OnPreferenceChangeListener {
45 
46     private static final String TAG = RemoveGuestOnExitPreferenceController.class.getSimpleName();
47     private static final String TAG_CONFIRM_GUEST_REMOVE = "confirmGuestRemove";
48     private static final int REMOVE_GUEST_ON_EXIT_DEFAULT = 1;
49 
50     private final UserCapabilities mUserCaps;
51     private final UserManager mUserManager;
52     private final Fragment mParentFragment;
53     private final Handler mHandler;
54 
RemoveGuestOnExitPreferenceController(Context context, String key, Fragment parent, Handler handler)55     public RemoveGuestOnExitPreferenceController(Context context, String key,
56             Fragment parent, Handler handler) {
57         super(context, key);
58         mUserCaps = UserCapabilities.create(context);
59         mUserManager = context.getSystemService(UserManager.class);
60         mParentFragment = parent;
61         mHandler = handler;
62     }
63 
64     @Override
updateState(Preference preference)65     public void updateState(Preference preference) {
66         mUserCaps.updateAddUserCapabilities(mContext);
67         final RestrictedSwitchPreference restrictedSwitchPreference =
68                 (RestrictedSwitchPreference) preference;
69         restrictedSwitchPreference.setChecked(isChecked());
70         if (!isAvailable()) {
71             restrictedSwitchPreference.setVisible(false);
72         } else {
73             restrictedSwitchPreference.setDisabledByAdmin(
74                     mUserCaps.disallowAddUser() ? mUserCaps.getEnforcedAdmin() : null);
75             restrictedSwitchPreference.setVisible(mUserCaps.mUserSwitcherEnabled);
76         }
77     }
78 
79     @Override
getAvailabilityStatus()80     public int getAvailabilityStatus() {
81         // if guest is forced to be ephemeral via config_guestUserEphemeral
82         // then disable this controller
83         // also disable this controller for non-admin users
84         // also disable when config_guestUserAllowEphemeralStateChange is false
85         if (mUserManager.isGuestUserAlwaysEphemeral()
86                 || !UserManager.isGuestUserAllowEphemeralStateChange()
87                 || !mUserCaps.isAdmin()
88                 || mUserCaps.disallowAddUser()
89                 || mUserCaps.disallowAddUserSetByAdmin()) {
90             return DISABLED_FOR_USER;
91         } else {
92             return mUserCaps.mUserSwitcherEnabled ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
93         }
94     }
95 
isChecked()96     private boolean isChecked() {
97         return Settings.Global.getInt(mContext.getContentResolver(),
98                 Settings.Global.REMOVE_GUEST_ON_EXIT, REMOVE_GUEST_ON_EXIT_DEFAULT) != 0;
99     }
100 
setChecked(Context context, boolean isChecked)101     private static boolean setChecked(Context context, boolean isChecked) {
102         Settings.Global.putInt(context.getContentResolver(),
103                 Settings.Global.REMOVE_GUEST_ON_EXIT, isChecked ? 1 : 0);
104         return true;
105     }
106 
107     @Override
onPreferenceChange(Preference preference, Object newValue)108     public boolean onPreferenceChange(Preference preference, Object newValue) {
109         boolean enable = (boolean) newValue;
110         UserInfo guestInfo = mUserManager.findCurrentGuestUser();
111 
112         // no guest do the setting and return
113         // guest ephemeral state will take effect on guest create
114         if (guestInfo == null) {
115             return setChecked(mContext, enable);
116         }
117         // if guest has never been initialized or started
118         // we can change guest ephemeral state
119         if (!guestInfo.isInitialized()) {
120             boolean isSuccess = mUserManager.setUserEphemeral(guestInfo.id, enable);
121             if (isSuccess) {
122                 return setChecked(mContext, enable);
123             } else {
124                 Log.w(TAG, "Unused guest, id=" + guestInfo.id
125                         + ". Mark ephemeral as " + enable + " failed !!!");
126                 return false;
127             }
128         }
129         // if guest has been used before and is not ephemeral
130         // but now we are making reset guest on exit preference as enabled
131         // then show confirmation dialog box and remove this guest if confirmed by user
132         if (guestInfo.isInitialized() && !guestInfo.isEphemeral() && enable) {
133             ConfirmGuestRemoveFragment.show(mParentFragment,
134                     mHandler,
135                     enable,
136                     guestInfo.id,
137                     (RestrictedSwitchPreference) preference);
138             return false;
139         }
140         // all other cases, there should be none, don't change state
141         return false;
142     }
143 
144 
145     /**
146      * Dialog to confirm guest removal on toggle clicked set to true
147      *
148      * Fragment must be a public static class to be  properly recreated from instance state
149      * else we will get "AndroidRuntime: java.lang.IllegalStateException"
150      */
151     public static final class ConfirmGuestRemoveFragment extends InstrumentedDialogFragment
152             implements DialogInterface.OnClickListener {
153 
154         private static final String TAG = ConfirmGuestRemoveFragment.class.getSimpleName();
155         private static final String SAVE_ENABLING = "enabling";
156         private static final String SAVE_GUEST_USER_ID = "guestUserId";
157 
158         private boolean mEnabling;
159         private int mGuestUserId;
160         private RestrictedSwitchPreference mPreference;
161         private Handler mHandler;
162 
show(Fragment parent, Handler handler, boolean enabling, int guestUserId, RestrictedSwitchPreference preference)163         private static void show(Fragment parent,
164                 Handler handler,
165                 boolean enabling, int guestUserId,
166                 RestrictedSwitchPreference preference) {
167             if (!parent.isAdded()) return;
168 
169             final ConfirmGuestRemoveFragment dialog = new ConfirmGuestRemoveFragment();
170             dialog.mHandler = handler;
171             dialog.mEnabling = enabling;
172             dialog.mGuestUserId = guestUserId;
173             dialog.setTargetFragment(parent, 0);
174             dialog.mPreference = preference;
175             dialog.show(parent.getFragmentManager(), TAG_CONFIRM_GUEST_REMOVE);
176         }
177 
178         @Override
onCreateDialog(Bundle savedInstanceState)179         public Dialog onCreateDialog(Bundle savedInstanceState) {
180             final Context context = getActivity();
181             if (savedInstanceState != null) {
182                 mEnabling = savedInstanceState.getBoolean(SAVE_ENABLING);
183                 mGuestUserId = savedInstanceState.getInt(SAVE_GUEST_USER_ID);
184             }
185 
186             final AlertDialog.Builder builder = new AlertDialog.Builder(context);
187             builder.setTitle(R.string.remove_guest_on_exit_dialog_title);
188             builder.setMessage(R.string.remove_guest_on_exit_dialog_message);
189             builder.setPositiveButton(
190                         com.android.settingslib.R.string.guest_exit_clear_data_button, this);
191             builder.setNegativeButton(android.R.string.cancel, null);
192 
193             return builder.create();
194         }
195 
196         @Override
onSaveInstanceState(Bundle outState)197         public void onSaveInstanceState(Bundle outState) {
198             super.onSaveInstanceState(outState);
199             outState.putBoolean(SAVE_ENABLING, mEnabling);
200             outState.putInt(SAVE_GUEST_USER_ID, mGuestUserId);
201         }
202 
203         @Override
getMetricsCategory()204         public int getMetricsCategory() {
205             return SettingsEnums.DIALOG_USER_REMOVE;
206         }
207 
208         @Override
onClick(DialogInterface dialog, int which)209         public void onClick(DialogInterface dialog, int which) {
210             if (which != DialogInterface.BUTTON_POSITIVE) {
211                 return;
212             }
213             UserManager userManager = getContext().getSystemService(UserManager.class);
214             if (userManager == null) {
215                 Log.e(TAG, "Unable to get user manager service");
216                 return;
217             }
218             UserInfo guestUserInfo = userManager.getUserInfo(mGuestUserId);
219             // only do action for guests and when enabling the preference
220             if (guestUserInfo == null || !guestUserInfo.isGuest() || !mEnabling) {
221                 Log.w(TAG, "Removing guest user ... failed, id=" + mGuestUserId);
222                 return;
223             }
224             if (mPreference != null) {
225                 // Using markGuestForDeletion allows us to create a new guest before this one is
226                 // fully removed.
227                 boolean isSuccess = userManager.markGuestForDeletion(guestUserInfo.id);
228                 if (!isSuccess) {
229                     Log.w(TAG, "Couldn't mark the guest for deletion for user "
230                             + guestUserInfo.id);
231                     return;
232                 }
233                 userManager.removeUser(guestUserInfo.id);
234                 if (setChecked(getContext(), mEnabling)) {
235                     mPreference.setChecked(mEnabling);
236                     mHandler.sendEmptyMessage(
237                                 UserSettings
238                                     .MESSAGE_REMOVE_GUEST_ON_EXIT_CONTROLLER_GUEST_REMOVED);
239                 }
240             }
241         }
242     }
243 }
244