• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 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 
17 package com.android.permissioncontroller.permission.ui.auto;
18 
19 import static com.android.permissioncontroller.Constants.EXTRA_SESSION_ID;
20 import static com.android.permissioncontroller.Constants.INVALID_SESSION_ID;
21 import static com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__ALLOW;
22 import static com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__ALLOW_ALWAYS;
23 import static com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__ALLOW_FOREGROUND;
24 import static com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__DENY;
25 import static com.android.permissioncontroller.permission.ui.ManagePermissionsActivity.EXTRA_RESULT_PERMISSION_INTERACTED;
26 import static com.android.permissioncontroller.permission.ui.ManagePermissionsActivity.EXTRA_RESULT_PERMISSION_RESULT;
27 
28 import android.app.Activity;
29 import android.app.AlertDialog;
30 import android.app.Dialog;
31 import android.content.Context;
32 import android.content.DialogInterface;
33 import android.content.Intent;
34 import android.content.pm.PackageManager;
35 import android.graphics.drawable.Drawable;
36 import android.os.Bundle;
37 import android.os.UserHandle;
38 import android.text.BidiFormatter;
39 import android.util.Log;
40 import android.view.View;
41 import android.widget.RadioButton;
42 
43 import androidx.annotation.NonNull;
44 import androidx.annotation.Nullable;
45 import androidx.core.content.res.TypedArrayUtils;
46 import androidx.fragment.app.DialogFragment;
47 import androidx.fragment.app.Fragment;
48 import androidx.lifecycle.ViewModelProvider;
49 import androidx.preference.PreferenceCategory;
50 import androidx.preference.PreferenceGroup;
51 import androidx.preference.PreferenceScreen;
52 import androidx.preference.PreferenceViewHolder;
53 import androidx.preference.TwoStatePreference;
54 
55 import com.android.car.ui.AlertDialogBuilder;
56 import com.android.permissioncontroller.R;
57 import com.android.permissioncontroller.auto.AutoSettingsFrameFragment;
58 import com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandler;
59 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel;
60 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ChangeRequest;
61 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModelFactory;
62 import com.android.permissioncontroller.permission.ui.v33.AdvancedConfirmDialogArgs;
63 import com.android.permissioncontroller.permission.utils.KotlinUtils;
64 import com.android.permissioncontroller.permission.utils.PackageRemovalMonitor;
65 import com.android.settingslib.RestrictedLockUtils;
66 
67 import java.util.Map;
68 
69 import kotlin.Pair;
70 
71 /** Settings related to a particular permission for the given app. */
72 public class AutoAppPermissionFragment extends AutoSettingsFrameFragment
73         implements AppPermissionViewModel.ConfirmDialogShowingFragment {
74 
75     private static final String LOG_TAG = "AppPermissionFragment";
76     private static final long POST_DELAY_MS = 20;
77 
78     @NonNull
79     private TwoStatePreference mAllowPermissionPreference;
80     @NonNull
81     private TwoStatePreference mAlwaysPermissionPreference;
82     @NonNull
83     private TwoStatePreference mForegroundOnlyPermissionPreference;
84     @NonNull
85     private TwoStatePreference mDenyPermissionPreference;
86     @NonNull
87     private AutoTwoTargetPreference mDetailsPreference;
88 
89     @NonNull
90     private AppPermissionViewModel mViewModel;
91     @NonNull
92     private String mPackageName;
93     @NonNull
94     private String mPermGroupName;
95     @NonNull
96     private UserHandle mUser;
97     @NonNull
98     private String mPackageLabel;
99     @NonNull
100     private String mPermGroupLabel;
101     private Drawable mPackageIcon;
102 
103     /**
104      * Listens for changes to the app the permission is currently getting granted to. {@code null}
105      * when unregistered.
106      */
107     @Nullable
108     private PackageRemovalMonitor mPackageRemovalMonitor;
109 
110     /**
111      * Returns a new {@link AutoAppPermissionFragment}.
112      *
113      * @param packageName the package name for which the permission is being changed
114      * @param permName the name of the permission being changed
115      * @param groupName the name of the permission group being changed
116      * @param userHandle the user for which the permission is being changed
117      */
118     @NonNull
newInstance(@onNull String packageName, @NonNull String permName, @Nullable String groupName, @NonNull UserHandle userHandle, @NonNull long sessionId)119     public static AutoAppPermissionFragment newInstance(@NonNull String packageName,
120             @NonNull String permName, @Nullable String groupName, @NonNull UserHandle userHandle,
121             @NonNull long sessionId) {
122         AutoAppPermissionFragment fragment = new AutoAppPermissionFragment();
123         Bundle arguments = new Bundle();
124         arguments.putString(Intent.EXTRA_PACKAGE_NAME, packageName);
125         if (groupName == null) {
126             arguments.putString(Intent.EXTRA_PERMISSION_NAME, permName);
127         } else {
128             arguments.putString(Intent.EXTRA_PERMISSION_GROUP_NAME, groupName);
129         }
130         arguments.putParcelable(Intent.EXTRA_USER, userHandle);
131         arguments.putLong(EXTRA_SESSION_ID, sessionId);
132         fragment.setArguments(arguments);
133         return fragment;
134     }
135 
136     @Override
onCreatePreferences(Bundle bundle, String s)137     public void onCreatePreferences(Bundle bundle, String s) {
138         setPreferenceScreen(getPreferenceManager().createPreferenceScreen(requireContext()));
139     }
140 
141     @Override
onCreate(@ullable Bundle savedInstanceState)142     public void onCreate(@Nullable Bundle savedInstanceState) {
143         super.onCreate(savedInstanceState);
144         mPackageName = getArguments().getString(Intent.EXTRA_PACKAGE_NAME);
145         mPermGroupName = getArguments().getString(Intent.EXTRA_PERMISSION_GROUP_NAME);
146         if (mPermGroupName == null) {
147             mPermGroupName = getArguments().getString(Intent.EXTRA_PERMISSION_NAME);
148         }
149         mUser = getArguments().getParcelable(Intent.EXTRA_USER);
150         mPackageLabel = BidiFormatter.getInstance().unicodeWrap(
151                 KotlinUtils.INSTANCE.getPackageLabel(getActivity().getApplication(), mPackageName,
152                         mUser));
153         mPermGroupLabel = KotlinUtils.INSTANCE.getPermGroupLabel(getContext(),
154                 mPermGroupName).toString();
155         mPackageIcon = KotlinUtils.INSTANCE.getBadgedPackageIcon(getActivity().getApplication(),
156                 mPackageName, mUser);
157         setHeaderLabel(
158                 requireContext().getString(R.string.app_permission_title, mPermGroupLabel));
159     }
160 
161     @Override
onViewCreated(@onNull View view, @Nullable Bundle savedInstanceState)162     public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
163         super.onViewCreated(view, savedInstanceState);
164 
165         PreferenceScreen screen = getPreferenceScreen();
166         screen.addPreference(
167                 AutoPermissionsUtils.createHeaderPreference(requireContext(),
168                         mPackageIcon, mPackageName, mPackageLabel));
169 
170         // Add permissions selector preferences.
171         PreferenceGroup permissionSelector = new PreferenceCategory(requireContext());
172         permissionSelector.setTitle(
173                 getString(R.string.app_permission_header, mPermGroupLabel));
174         screen.addPreference(permissionSelector);
175 
176         mAllowPermissionPreference = new SelectedPermissionPreference(requireContext());
177         mAllowPermissionPreference.setTitle(R.string.app_permission_button_allow);
178         permissionSelector.addPreference(mAllowPermissionPreference);
179 
180         mAlwaysPermissionPreference = new SelectedPermissionPreference(requireContext());
181         mAlwaysPermissionPreference.setTitle(R.string.app_permission_button_allow_always);
182         permissionSelector.addPreference(mAlwaysPermissionPreference);
183 
184         mForegroundOnlyPermissionPreference = new SelectedPermissionPreference(requireContext());
185         mForegroundOnlyPermissionPreference.setTitle(
186                 R.string.app_permission_button_allow_foreground);
187         permissionSelector.addPreference(mForegroundOnlyPermissionPreference);
188 
189         mDenyPermissionPreference = new SelectedPermissionPreference(requireContext());
190         mDenyPermissionPreference.setTitle(R.string.app_permission_button_deny);
191         permissionSelector.addPreference(mDenyPermissionPreference);
192 
193         mAllowPermissionPreference.setOnPreferenceClickListener(v -> {
194             checkOnlyOneButtonOverride(AppPermissionViewModel.ButtonType.ALLOW);
195             setResult(GrantPermissionsViewHandler.GRANTED_ALWAYS);
196             requestChange(ChangeRequest.GRANT_FOREGROUND,
197                     APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__ALLOW);
198             return true;
199         });
200         mAlwaysPermissionPreference.setOnPreferenceClickListener(v -> {
201             checkOnlyOneButtonOverride(AppPermissionViewModel.ButtonType.ALLOW_ALWAYS);
202             setResult(GrantPermissionsViewHandler.GRANTED_ALWAYS);
203             requestChange(ChangeRequest.GRANT_BOTH,
204                     APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__ALLOW_ALWAYS);
205             return true;
206         });
207         mForegroundOnlyPermissionPreference.setOnPreferenceClickListener(v -> {
208             checkOnlyOneButtonOverride(AppPermissionViewModel.ButtonType.ALLOW_FOREGROUND);
209             setResult(GrantPermissionsViewHandler.GRANTED_FOREGROUND_ONLY);
210             requestChange(ChangeRequest.GRANT_FOREGROUND_ONLY,
211                     APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__ALLOW_FOREGROUND);
212             return true;
213         });
214         mDenyPermissionPreference.setOnPreferenceClickListener(v -> {
215             checkOnlyOneButtonOverride(AppPermissionViewModel.ButtonType.DENY);
216             setResult(GrantPermissionsViewHandler.DENIED);
217             requestChange(ChangeRequest.REVOKE_BOTH,
218                     APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__DENY);
219             return true;
220         });
221 
222         mDetailsPreference = new AutoTwoTargetPreference(requireContext());
223         screen.addPreference(mDetailsPreference);
224     }
225 
226     @Override
onStart()227     public void onStart() {
228         super.onStart();
229         Activity activity = requireActivity();
230 
231         // Get notified when the package is removed.
232         mPackageRemovalMonitor = new PackageRemovalMonitor(requireContext(), mPackageName) {
233             @Override
234             public void onPackageRemoved() {
235                 Log.w(LOG_TAG, mPackageName + " was uninstalled");
236                 activity.setResult(Activity.RESULT_CANCELED);
237                 activity.finish();
238             }
239         };
240         mPackageRemovalMonitor.register();
241 
242         // Check if the package was removed while this activity was not started.
243         try {
244             activity.createPackageContextAsUser(mPackageName, /* flags= */ 0,
245                     mUser).getPackageManager().getPackageInfo(mPackageName,
246                     /* flags= */ 0);
247         } catch (PackageManager.NameNotFoundException e) {
248             Log.w(LOG_TAG, mPackageName + " was uninstalled while this activity was stopped", e);
249             activity.setResult(Activity.RESULT_CANCELED);
250             activity.finish();
251         }
252 
253         long sessionId = getArguments().getLong(EXTRA_SESSION_ID, INVALID_SESSION_ID);
254         AppPermissionViewModelFactory factory = new AppPermissionViewModelFactory(
255                 getActivity().getApplication(), mPackageName, mPermGroupName, mUser, sessionId);
256         mViewModel = new ViewModelProvider(this, factory).get(AppPermissionViewModel.class);
257         mViewModel.getButtonStateLiveData().observe(this, this::setRadioButtonsState);
258         mViewModel.getDetailResIdLiveData().observe(this, this::setDetail);
259         mViewModel.getShowAdminSupportLiveData().observe(this, this::setAdminSupportDetail);
260     }
261 
262     @Override
onStop()263     public void onStop() {
264         super.onStop();
265 
266         if (mPackageRemovalMonitor != null) {
267             mPackageRemovalMonitor.unregister();
268             mPackageRemovalMonitor = null;
269         }
270     }
271 
272     @Override
showConfirmDialog(ChangeRequest changeRequest, int messageId, int buttonPressed, boolean oneTime)273     public void showConfirmDialog(ChangeRequest changeRequest, int messageId,
274             int buttonPressed, boolean oneTime) {
275         Bundle args = new Bundle();
276 
277         args.putInt(ConfirmDialog.MSG, messageId);
278         args.putSerializable(ConfirmDialog.CHANGE_REQUEST, changeRequest);
279         args.putSerializable(ConfirmDialog.BUTTON, buttonPressed);
280 
281         ConfirmDialog confirmDialog = new ConfirmDialog();
282         confirmDialog.setArguments(args);
283         confirmDialog.setTargetFragment(this, 0);
284         confirmDialog.show(requireFragmentManager().beginTransaction(),
285                 ConfirmDialog.class.getName());
286     }
287 
setResult(@rantPermissionsViewHandler.Result int result)288     private void setResult(@GrantPermissionsViewHandler.Result int result) {
289         Intent intent = new Intent()
290                 .putExtra(EXTRA_RESULT_PERMISSION_INTERACTED,
291                         requireArguments().getString(Intent.EXTRA_PERMISSION_GROUP_NAME))
292                 .putExtra(EXTRA_RESULT_PERMISSION_RESULT, result);
293         requireActivity().setResult(Activity.RESULT_OK, intent);
294     }
295 
setRadioButtonsState( Map<AppPermissionViewModel.ButtonType, AppPermissionViewModel.ButtonState> states)296     private void setRadioButtonsState(
297             Map<AppPermissionViewModel.ButtonType, AppPermissionViewModel.ButtonState> states) {
298         setButtonState(mAllowPermissionPreference,
299                 states.get(AppPermissionViewModel.ButtonType.ALLOW));
300         setButtonState(mAlwaysPermissionPreference,
301                 states.get(AppPermissionViewModel.ButtonType.ALLOW_ALWAYS));
302         setButtonState(mForegroundOnlyPermissionPreference,
303                 states.get(AppPermissionViewModel.ButtonType.ALLOW_FOREGROUND));
304         setButtonState(mDenyPermissionPreference,
305                 states.get(AppPermissionViewModel.ButtonType.DENY));
306     }
307 
setButtonState(TwoStatePreference button, AppPermissionViewModel.ButtonState state)308     private void setButtonState(TwoStatePreference button,
309             AppPermissionViewModel.ButtonState state) {
310         button.setVisible(state.isShown());
311         if (state.isShown()) {
312             button.setChecked(state.isChecked());
313             button.setEnabled(state.isEnabled());
314         }
315     }
316 
317     /**
318      * Helper method to handle the UX edge case where the confirmation dialog is shown and two
319      * buttons are selected at once. This happens since the Auto UI doesn't use a proper radio
320      * group, so there is nothing that enforces that tapping on a button unchecks a previously
321      * checked button. Apart from this case, this UI is not necessary since the UI is entirely
322      * driven by the ViewModel.
323      */
checkOnlyOneButtonOverride(AppPermissionViewModel.ButtonType buttonType)324     private void checkOnlyOneButtonOverride(AppPermissionViewModel.ButtonType buttonType) {
325         mAllowPermissionPreference.setChecked(
326                 buttonType == AppPermissionViewModel.ButtonType.ALLOW);
327         mAlwaysPermissionPreference.setChecked(
328                 buttonType == AppPermissionViewModel.ButtonType.ALLOW_ALWAYS);
329         mForegroundOnlyPermissionPreference.setChecked(
330                 buttonType == AppPermissionViewModel.ButtonType.ALLOW_FOREGROUND);
331         mDenyPermissionPreference.setChecked(buttonType == AppPermissionViewModel.ButtonType.DENY);
332     }
333 
setDetail(Pair<Integer, Integer> detailResIds)334     private void setDetail(Pair<Integer, Integer> detailResIds) {
335         if (detailResIds == null) {
336             mDetailsPreference.setVisible(false);
337             return;
338         }
339         if (detailResIds.getSecond() != null) {
340             mDetailsPreference.setWidgetLayoutResource(R.layout.settings_preference_widget);
341             mDetailsPreference.setOnSecondTargetClickListener(
342                     v -> showAllPermissions(mPermGroupName));
343             mDetailsPreference.setSummary(
344                     getString(detailResIds.getFirst(), detailResIds.getSecond()));
345         } else {
346             mDetailsPreference.setSummary(detailResIds.getFirst());
347         }
348     }
349 
350     /**
351      * Show all individual permissions in this group in a new fragment.
352      */
showAllPermissions(@onNull String filterGroup)353     private void showAllPermissions(@NonNull String filterGroup) {
354         Fragment frag = AutoAllAppPermissionsFragment.newInstance(mPackageName,
355                 filterGroup, mUser,
356                 getArguments().getLong(EXTRA_SESSION_ID, INVALID_SESSION_ID));
357         requireFragmentManager().beginTransaction()
358                 .replace(android.R.id.content, frag)
359                 .addToBackStack("AllPerms")
360                 .commit();
361     }
362 
setAdminSupportDetail(RestrictedLockUtils.EnforcedAdmin admin)363     private void setAdminSupportDetail(RestrictedLockUtils.EnforcedAdmin admin) {
364         if (admin != null) {
365             mDetailsPreference.setWidgetLayoutResource(R.layout.info_preference_widget);
366             mDetailsPreference.setOnSecondTargetClickListener(v ->
367                     RestrictedLockUtils.sendShowAdminSupportDetailsIntent(getContext(), admin)
368             );
369         }
370     }
371 
372     /**
373      * Request to grant/revoke permissions group.
374      */
requestChange(ChangeRequest changeRequest, int buttonClicked)375     private void requestChange(ChangeRequest changeRequest,
376             int buttonClicked) {
377         mViewModel.requestChange(/* setOneTime= */false, /* fragment= */ this,
378                 /* defaultDeny= */this, changeRequest, buttonClicked);
379     }
380 
381     /** Preference used to represent apps that can be picked as a default app. */
382     private static class SelectedPermissionPreference extends TwoStatePreference {
383 
SelectedPermissionPreference(Context context)384         SelectedPermissionPreference(Context context) {
385             super(context, null, TypedArrayUtils.getAttr(context, R.attr.preferenceStyle,
386                     android.R.attr.preferenceStyle));
387             setPersistent(false);
388             setLayoutResource(R.layout.car_radio_button_preference);
389             setWidgetLayoutResource(R.layout.radio_button_preference_widget);
390         }
391 
392         @Override
onBindViewHolder(PreferenceViewHolder holder)393         public void onBindViewHolder(PreferenceViewHolder holder) {
394             super.onBindViewHolder(holder);
395 
396             RadioButton radioButton = (RadioButton) holder.findViewById(R.id.radio_button);
397             radioButton.setChecked(isChecked());
398         }
399     }
400 
401     /**
402      * A dialog warning the user that they are about to deny a permission that was granted by
403      * default.
404      *
405      * @see #showConfirmDialog(ChangeRequest, int, int, boolean)
406      */
407     public static class ConfirmDialog extends DialogFragment {
408         private static final String MSG = ConfirmDialog.class.getName() + ".arg.msg";
409         private static final String CHANGE_REQUEST = ConfirmDialog.class.getName()
410                 + ".arg.changeRequest";
411         private static final String BUTTON = ConfirmDialog.class.getName()
412                 + ".arg.button";
413         private static int sCode =  APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__ALLOW;
414 
415         @NonNull
416         @Override
onCreateDialog(Bundle savedInstanceState)417         public Dialog onCreateDialog(Bundle savedInstanceState) {
418             // TODO(b/229024576): This code is duplicated, refactor ConfirmDialog for easier
419             // NFF sharing
420             boolean isGrantFileAccess = getArguments().getSerializable(CHANGE_REQUEST)
421                     == ChangeRequest.GRANT_All_FILE_ACCESS;
422             boolean isGrantStorageSupergroup = getArguments().getSerializable(CHANGE_REQUEST)
423                     == ChangeRequest.GRANT_STORAGE_SUPERGROUP;
424             int positiveButtonStringResId = R.string.grant_dialog_button_deny_anyway;
425             if (isGrantFileAccess || isGrantStorageSupergroup) {
426                 positiveButtonStringResId = R.string.grant_dialog_button_allow;
427             }
428             AutoAppPermissionFragment fragment = (AutoAppPermissionFragment) getTargetFragment();
429             return new AlertDialogBuilder(getContext())
430                     .setMessage(requireArguments().getInt(MSG))
431                     .setNegativeButton(R.string.cancel,
432                             (dialog, which) -> dialog.cancel())
433                     .setPositiveButton(positiveButtonStringResId,
434                             (dialog, which) -> {
435                                 if (isGrantFileAccess) {
436                                     fragment.mViewModel.setAllFilesAccess(true);
437                                     fragment.mViewModel.requestChange(false, fragment,
438                                             fragment, ChangeRequest.GRANT_BOTH, sCode);
439                                 } else if (isGrantStorageSupergroup) {
440                                     fragment.mViewModel.requestChange(false, fragment,
441                                             fragment, ChangeRequest.GRANT_BOTH, sCode);
442                                 } else {
443                                     fragment.mViewModel.onDenyAnyWay((ChangeRequest)
444                                                     getArguments().getSerializable(CHANGE_REQUEST),
445                                             getArguments().getInt(BUTTON),
446                                             /* oneTime= */ false);
447                                 }
448                             })
449                     .create();
450         }
451 
452         @Override
onCancel(DialogInterface dialog)453         public void onCancel(DialogInterface dialog) {
454             AutoAppPermissionFragment fragment = (AutoAppPermissionFragment) getTargetFragment();
455             fragment.setRadioButtonsState(fragment.mViewModel.getButtonStateLiveData().getValue());
456         }
457     }
458 
459     @Override
460     public void showAdvancedConfirmDialog(AdvancedConfirmDialogArgs args) {
461         AlertDialog.Builder b = new AlertDialog.Builder(getContext())
462                 .setIcon(args.getIconId())
463                 .setMessage(args.getMessageId())
464                 .setOnCancelListener((DialogInterface dialog) -> {
465                     setRadioButtonsState(mViewModel.getButtonStateLiveData().getValue());
466                 })
467                 .setNegativeButton(args.getNegativeButtonTextId(),
468                         (DialogInterface dialog, int which) -> {
469                             setRadioButtonsState(mViewModel.getButtonStateLiveData().getValue());
470                         })
471                 .setPositiveButton(args.getPositiveButtonTextId(),
472                         (DialogInterface dialog, int which) -> {
473                             mViewModel.requestChange(args.getSetOneTime(),
474                                     AutoAppPermissionFragment.this, AutoAppPermissionFragment.this,
475                                     args.getChangeRequest(), args.getButtonClicked());
476                         });
477         if (args.getTitleId() != 0) {
478             b.setTitle(args.getTitleId());
479         }
480         b.show();
481     }
482 }
483