1 /*
2  * Copyright (C) 2018 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.handheld;
18 
19 import static android.Manifest.permission_group.STORAGE;
20 import static android.app.Activity.RESULT_OK;
21 
22 import static com.android.permissioncontroller.Constants.EXTRA_SESSION_ID;
23 import static com.android.permissioncontroller.Constants.INVALID_SESSION_ID;
24 import static com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__ALLOW;
25 import static com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__ALLOW_ALWAYS;
26 import static com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__ALLOW_FOREGROUND;
27 import static com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__ASK_EVERY_TIME;
28 import static com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__DENY;
29 import static com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__DENY_FOREGROUND;
30 import static com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__GRANT_FINE_LOCATION;
31 import static com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__PHOTOS_SELECTED;
32 import static com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__REVOKE_FINE_LOCATION;
33 import static com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandler.DENIED;
34 import static com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandler.DENIED_DO_NOT_ASK_AGAIN;
35 import static com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandler.GRANTED_ALWAYS;
36 import static com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandler.GRANTED_FOREGROUND_ONLY;
37 import static com.android.permissioncontroller.permission.ui.ManagePermissionsActivity.EXTRA_CALLER_NAME;
38 import static com.android.permissioncontroller.permission.ui.ManagePermissionsActivity.EXTRA_RESULT_PERMISSION_INTERACTED;
39 import static com.android.permissioncontroller.permission.ui.ManagePermissionsActivity.EXTRA_RESULT_PERMISSION_RESULT;
40 import static com.android.permissioncontroller.permission.ui.handheld.UtilsKt.pressBack;
41 
42 import android.app.ActionBar;
43 import android.app.AlertDialog;
44 import android.app.Dialog;
45 import android.app.role.RoleManager;
46 import android.content.Context;
47 import android.content.DialogInterface;
48 import android.content.Intent;
49 import android.graphics.drawable.Drawable;
50 import android.os.Bundle;
51 import android.os.Handler;
52 import android.os.Looper;
53 import android.os.UserHandle;
54 import android.text.BidiFormatter;
55 import android.util.Log;
56 import android.view.LayoutInflater;
57 import android.view.MenuItem;
58 import android.view.View;
59 import android.view.ViewGroup;
60 import android.widget.CompoundButton;
61 import android.widget.FrameLayout;
62 import android.widget.ImageView;
63 import android.widget.RadioButton;
64 import android.widget.Switch;
65 import android.widget.TextView;
66 import android.widget.Toast;
67 
68 import androidx.annotation.NonNull;
69 import androidx.annotation.Nullable;
70 import androidx.annotation.StringRes;
71 import androidx.core.widget.NestedScrollView;
72 import androidx.fragment.app.DialogFragment;
73 import androidx.lifecycle.ViewModelProvider;
74 
75 import com.android.modules.utils.build.SdkLevel;
76 import com.android.permissioncontroller.R;
77 import com.android.permissioncontroller.permission.data.FullStoragePermissionAppsLiveData.FullStoragePackageState;
78 import com.android.permissioncontroller.permission.ui.GrantPermissionsViewHandler;
79 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel;
80 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonState;
81 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ButtonType;
82 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModel.ChangeRequest;
83 import com.android.permissioncontroller.permission.ui.model.AppPermissionViewModelFactory;
84 import com.android.permissioncontroller.permission.ui.v33.AdvancedConfirmDialogArgs;
85 import com.android.permissioncontroller.permission.utils.KotlinUtils;
86 import com.android.permissioncontroller.permission.utils.Utils;
87 import com.android.permissioncontroller.permission.utils.v35.MultiDeviceUtils;
88 import com.android.settingslib.RestrictedLockUtils;
89 import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
90 import com.android.settingslib.widget.ActionBarShadowController;
91 
92 import kotlin.Pair;
93 
94 import java.util.Map;
95 import java.util.Objects;
96 import java.util.Optional;
97 import java.util.Set;
98 
99 /**
100  * Show and manage a single permission group for an app.
101  *
102  * <p>Allows the user to control whether the app is granted the permission.
103  */
104 public class AppPermissionFragment extends SettingsWithLargeHeader
105         implements AppPermissionViewModel.ConfirmDialogShowingFragment {
106     private static final String LOG_TAG = "AppPermissionFragment";
107     private static final long POST_DELAY_MS = 20;
108     private static final long EDIT_PHOTOS_BUTTON_ANIMATION_LENGTH_MS = 200L;
109 
110     static final String GRANT_CATEGORY = "grant_category";
111     static final String PERSISTENT_DEVICE_ID = "persistent_device_id";
112 
113     private @NonNull AppPermissionViewModel mViewModel;
114     private @NonNull ViewGroup mAppPermissionRationaleContainer;
115     private @NonNull ViewGroup mAppPermissionRationaleContent;
116     private @NonNull FrameLayout mAllowButtonFrame;
117     private @NonNull RadioButton mAllowButton;
118     private @NonNull RadioButton mAllowAlwaysButton;
119     private @NonNull RadioButton mAllowForegroundButton;
120     private @NonNull RadioButton mAskOneTimeButton;
121     private @NonNull RadioButton mAskButton;
122     private @NonNull RadioButton mAllowLimitedButton;
123     private @NonNull RadioButton mDenyButton;
124     private @NonNull RadioButton mDenyForegroundButton;
125     private @NonNull ImageView mSelectPhotosButton;
126     private @NonNull View mAllowLimitedPhotosLayout;
127     private @NonNull View mSelectPhotosDivider;
128     private @NonNull View mLocationAccuracy;
129     private @NonNull Switch mLocationAccuracySwitch;
130     private @NonNull View mDivider;
131     private @NonNull ViewGroup mWidgetFrame;
132     private @NonNull TextView mPermissionDetails;
133     private @NonNull NestedScrollView mNestedScrollView;
134     private @NonNull String mPackageName;
135     private @NonNull String mPermGroupName;
136     private @NonNull UserHandle mUser;
137     private boolean mIsStorageGroup;
138     private boolean mIsInitialLoad;
139     // This prevents the user from clicking the photo picker button multiple times in succession
140     private boolean mPhotoPickerTriggered;
141     private long mSessionId;
142     private String mPersistentDeviceId;
143 
144     private @NonNull String mPackageLabel;
145     private @NonNull String mPermGroupLabel;
146     private Drawable mPackageIcon;
147     private @NonNull RoleManager mRoleManager;
148 
149     /**
150      * Create a bundle with the arguments needed by this fragment
151      *
152      * @param packageName   The name of the package
153      * @param permName      The name of the permission whose group this fragment is for (optional)
154      * @param groupName     The name of the permission group (required if permName not specified)
155      * @param userHandle    The user of the app permission group
156      * @param caller        The name of the fragment we called from
157      * @param sessionId     The current session ID
158      * @param grantCategory The grant status of this app permission group. Used to initially set
159      *                      the button state
160      * @return A bundle with all of the args placed
161      */
createArgs(@onNull String packageName, @Nullable String permName, @Nullable String groupName, @NonNull UserHandle userHandle, @Nullable String caller, long sessionId, @Nullable String grantCategory)162     public static Bundle createArgs(@NonNull String packageName,
163             @Nullable String permName, @Nullable String groupName,
164             @NonNull UserHandle userHandle, @Nullable String caller, long sessionId, @Nullable
165             String grantCategory) {
166         Bundle arguments = new Bundle();
167         arguments.putString(Intent.EXTRA_PACKAGE_NAME, packageName);
168         if (groupName == null) {
169             arguments.putString(Intent.EXTRA_PERMISSION_NAME, permName);
170         } else {
171             arguments.putString(Intent.EXTRA_PERMISSION_GROUP_NAME, groupName);
172         }
173         arguments.putParcelable(Intent.EXTRA_USER, userHandle);
174         arguments.putString(EXTRA_CALLER_NAME, caller);
175         arguments.putLong(EXTRA_SESSION_ID, sessionId);
176         arguments.putString(GRANT_CATEGORY, grantCategory);
177         return arguments;
178     }
179 
180     @Override
onCreate(Bundle savedInstanceState)181     public void onCreate(Bundle savedInstanceState) {
182         super.onCreate(savedInstanceState);
183 
184         setHasOptionsMenu(true);
185         ActionBar ab = getActivity().getActionBar();
186         if (ab != null) {
187             ab.setDisplayHomeAsUpEnabled(true);
188         }
189 
190         mPackageName = getArguments().getString(Intent.EXTRA_PACKAGE_NAME);
191         mPermGroupName = getArguments().getString(Intent.EXTRA_PERMISSION_GROUP_NAME);
192         if (mPermGroupName == null) {
193             mPermGroupName = getArguments().getString(Intent.EXTRA_PERMISSION_NAME);
194         }
195         mIsStorageGroup = Objects.equals(mPermGroupName, STORAGE);
196         mUser = getArguments().getParcelable(Intent.EXTRA_USER);
197         mPackageLabel = BidiFormatter.getInstance().unicodeWrap(
198                 KotlinUtils.INSTANCE.getPackageLabel(getActivity().getApplication(), mPackageName,
199                         mUser));
200         mPermGroupLabel = KotlinUtils.INSTANCE.getPermGroupLabel(getContext(),
201                 mPermGroupName).toString();
202         mPackageIcon = KotlinUtils.INSTANCE.getBadgedPackageIcon(getActivity().getApplication(),
203                 mPackageName, mUser);
204         mSessionId = getArguments().getLong(EXTRA_SESSION_ID, INVALID_SESSION_ID);
205 
206         mPersistentDeviceId = getArguments().getString(PERSISTENT_DEVICE_ID,
207                 MultiDeviceUtils.getDefaultDevicePersistentDeviceId());
208 
209         AppPermissionViewModelFactory factory = new AppPermissionViewModelFactory(
210                 getActivity().getApplication(), mPackageName, mPermGroupName, mUser, mSessionId,
211                 mPersistentDeviceId);
212         mViewModel = new ViewModelProvider(this, factory).get(AppPermissionViewModel.class);
213         Handler delayHandler = new Handler(Looper.getMainLooper());
214         mViewModel.getButtonStateLiveData().observe(this, buttonState -> {
215             if (mIsInitialLoad) {
216                 setRadioButtonsState(buttonState);
217             } else {
218                 delayHandler.removeCallbacksAndMessages(null);
219                 delayHandler.postDelayed(() -> setRadioButtonsState(buttonState), POST_DELAY_MS);
220             }
221         });
222         mViewModel.getDetailResIdLiveData().observe(this, this::setDetail);
223         mViewModel.getShowAdminSupportLiveData().observe(this, this::setAdminSupportDetail);
224         if (mIsStorageGroup) {
225             mViewModel.getFullStorageStateLiveData().observe(this, this::setSpecialStorageState);
226         }
227 
228         mRoleManager = Utils.getSystemServiceSafe(getContext(), RoleManager.class);
229     }
230 
231     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)232     public View onCreateView(LayoutInflater inflater, ViewGroup container,
233             Bundle savedInstanceState) {
234         Context context = getContext();
235         ViewGroup root = (ViewGroup) inflater.inflate(R.layout.app_permission, container, false);
236 
237         mIsInitialLoad = true;
238 
239         setHeader(mPackageIcon, mPackageLabel, null, null, false);
240         updateHeader(root.requireViewById(R.id.large_header));
241 
242         String text = null;
243         if (MultiDeviceUtils.isDefaultDeviceId(mPersistentDeviceId)) {
244             text = context.getString(R.string.app_permission_header, mPermGroupLabel);
245         } else {
246             final String deviceName = MultiDeviceUtils.getDeviceName(context, mPersistentDeviceId);
247             text = context.getString(R.string.app_permission_header_with_device_name,
248                     mPermGroupLabel, deviceName);
249         }
250         ((TextView) root.requireViewById(R.id.permission_message)).setText(text);
251 
252         String caller = getArguments().getString(EXTRA_CALLER_NAME);
253 
254         TextView footer1Link = root.requireViewById(R.id.footer_link_1);
255         footer1Link.setText(context.getString(R.string.app_permission_footer_app_permissions_link,
256                 mPackageLabel));
257         setBottomLinkState(footer1Link, caller, Intent.ACTION_MANAGE_APP_PERMISSIONS);
258 
259         TextView footer2Link = root.requireViewById(R.id.footer_link_2);
260         footer2Link.setText(context.getString(R.string.app_permission_footer_permission_apps_link));
261         setBottomLinkState(footer2Link, caller, Intent.ACTION_MANAGE_PERMISSION_APPS);
262 
263         Set<String> exemptedPackages = Utils.getExemptedPackages(mRoleManager);
264         ImageView footerInfoIcon = root.requireViewById(R.id.app_additional_info_icon);
265         TextView footerInfoText = root.requireViewById(R.id.app_additional_info_text);
266         if (exemptedPackages.contains(mPackageName)) {
267             int additional_info_label = Utils.isStatusBarIndicatorPermission(mPermGroupName)
268                     ? R.string.exempt_mic_camera_info_label : R.string.exempt_info_label;
269             footerInfoText.setText(context.getString(additional_info_label, mPackageLabel));
270             footerInfoIcon.setVisibility(View.VISIBLE);
271             footerInfoText.setVisibility(View.VISIBLE);
272         } else {
273             footerInfoIcon.setVisibility(View.GONE);
274             footerInfoText.setVisibility(View.GONE);
275         }
276 
277         mAllowButtonFrame = root.requireViewById(R.id.allow_radio_button_frame);
278         mAllowButton = root.requireViewById(R.id.allow_radio_button);
279         mAllowAlwaysButton = root.requireViewById(R.id.allow_always_radio_button);
280         mAllowForegroundButton = root.requireViewById(R.id.allow_foreground_only_radio_button);
281         mAskOneTimeButton = root.requireViewById(R.id.ask_one_time_radio_button);
282         mAskButton = root.requireViewById(R.id.ask_radio_button);
283         mAllowLimitedButton = root.requireViewById(R.id.select_radio_button);
284         mDenyButton = root.requireViewById(R.id.deny_radio_button);
285         mDenyForegroundButton = root.requireViewById(R.id.deny_foreground_radio_button);
286 
287         mDivider = root.requireViewById(R.id.two_target_divider);
288         mWidgetFrame = root.requireViewById(R.id.widget_frame);
289         mPermissionDetails = root.requireViewById(R.id.permission_details);
290         mLocationAccuracy = root.requireViewById(R.id.location_accuracy);
291         mLocationAccuracySwitch = root.requireViewById(R.id.location_accuracy_switch);
292         mAllowLimitedPhotosLayout = root.requireViewById(R.id.radio_select_layout);
293         mSelectPhotosButton = root.requireViewById(R.id.edit_selected_button);
294         mSelectPhotosDivider = root.requireViewById(R.id.edit_photos_divider);
295         mNestedScrollView = root.requireViewById(R.id.nested_scroll_view);
296 
297         if (mViewModel.getButtonStateLiveData().getValue() != null) {
298             setRadioButtonsState(mViewModel.getButtonStateLiveData().getValue());
299         } else {
300             mAllowButton.setVisibility(View.GONE);
301             mAllowAlwaysButton.setVisibility(View.GONE);
302             mAllowForegroundButton.setVisibility(View.GONE);
303             mAskOneTimeButton.setVisibility(View.GONE);
304             mAskButton.setVisibility(View.GONE);
305             mDenyButton.setVisibility(View.GONE);
306             mDenyForegroundButton.setVisibility(View.GONE);
307             mLocationAccuracy.setVisibility(View.GONE);
308             mAllowLimitedPhotosLayout.setVisibility(View.GONE);
309             mSelectPhotosDivider.setAlpha(0f);
310             mSelectPhotosButton.setAlpha(0f);
311         }
312 
313         if (mViewModel.getFullStorageStateLiveData().isInitialized() && mIsStorageGroup) {
314             setSpecialStorageState(mViewModel.getFullStorageStateLiveData().getValue(), root);
315         } else {
316             TextView storageFooter = root.requireViewById(R.id.footer_storage_special_app_access);
317             storageFooter.setVisibility(View.GONE);
318         }
319         mAppPermissionRationaleContainer =
320                 root.requireViewById(R.id.app_permission_rationale_container);
321         mAppPermissionRationaleContent =
322                 root.requireViewById(R.id.app_permission_rationale_content);
323         mViewModel.getShowPermissionRationaleLiveData().observe(this, show -> {
324             showPermissionRationaleDialog(Optional.ofNullable(show).orElse(false));
325         });
326 
327         getActivity().setTitle(
328                 getPreferenceManager().getContext().getString(R.string.app_permission_title,
329                         mPermGroupLabel));
330         return root;
331     }
332 
onResume()333     public void onResume() {
334         super.onResume();
335         // If we're returning to the fragment, photo picker hasn't been triggered
336         mPhotoPickerTriggered = false;
337     }
338 
showPermissionRationaleDialog(boolean showPermissionRationale)339     private void showPermissionRationaleDialog(boolean showPermissionRationale) {
340         if (!showPermissionRationale) {
341             mAppPermissionRationaleContainer.setVisibility(View.GONE);
342         } else {
343             mAppPermissionRationaleContainer.setVisibility(View.VISIBLE);
344             mAppPermissionRationaleContent.setOnClickListener((v) -> {
345                 if (!SdkLevel.isAtLeastU()) {
346                     return;
347                 }
348                 mViewModel.showPermissionRationaleActivity(getActivity(), mPermGroupName);
349             });
350         }
351     }
352 
setBottomLinkState(TextView view, String caller, String action)353     private void setBottomLinkState(TextView view, String caller, String action) {
354         if ((Objects.equals(caller, AppPermissionGroupsFragment.class.getName())
355                 && action.equals(Intent.ACTION_MANAGE_APP_PERMISSIONS))
356                 || (Objects.equals(caller, PermissionAppsFragment.class.getName())
357                 && action.equals(Intent.ACTION_MANAGE_PERMISSION_APPS))) {
358             view.setVisibility(View.GONE);
359         } else {
360             view.setOnClickListener((v) -> {
361                 Bundle args;
362                 if (action.equals(Intent.ACTION_MANAGE_APP_PERMISSIONS)) {
363                     args = AppPermissionGroupsFragment.createArgs(mPackageName, mUser,
364                             mSessionId, true);
365                 } else {
366                     args = PermissionAppsFragment.createArgs(mPermGroupName, mSessionId);
367                 }
368                 mViewModel.showBottomLinkPage(this, action, args);
369             });
370         }
371     }
372 
setSpecialStorageState(FullStoragePackageState storageState)373     private void setSpecialStorageState(FullStoragePackageState storageState) {
374         setSpecialStorageState(storageState, getView());
375     }
376 
377     @Override
onStart()378     public void onStart() {
379         super.onStart();
380 
381         ActionBar ab = getActivity().getActionBar();
382         if (ab != null) {
383             ab.setElevation(0);
384         }
385 
386         ActionBarShadowController.attachToView(getActivity(), getLifecycle(), mNestedScrollView);
387     }
388 
389     @Override
onOptionsItemSelected(MenuItem item)390     public boolean onOptionsItemSelected(MenuItem item) {
391         if (item.getItemId() == android.R.id.home) {
392             pressBack(this);
393             return true;
394         }
395         return super.onOptionsItemSelected(item);
396     }
397 
setRadioButtonsState(Map<ButtonType, ButtonState> states)398     private void setRadioButtonsState(Map<ButtonType, ButtonState> states) {
399         if (states == null && !mViewModel.getButtonStateLiveData().isStale()) {
400             pressBack(this);
401             Log.w(LOG_TAG, "invalid package " + mPackageName + " or perm group "
402                     + mPermGroupName);
403             Toast.makeText(
404                     getActivity(), R.string.app_not_found_dlg_title, Toast.LENGTH_LONG).show();
405             return;
406         } else if (states == null) {
407             return;
408         }
409         mAllowButtonFrame.setOnClickListener((v) -> allowButtonFrameClickListener());
410         mAllowAlwaysButton.setOnClickListener((v) -> {
411             if (mIsStorageGroup) {
412                 showConfirmDialog(ChangeRequest.GRANT_ALL_FILE_ACCESS,
413                         R.string.special_file_access_dialog, -1, false);
414             } else {
415                 mViewModel.requestChange(false, this, this, ChangeRequest.GRANT_BOTH,
416                         APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__ALLOW_ALWAYS);
417             }
418             setResult(GRANTED_ALWAYS);
419         });
420         mAllowForegroundButton.setOnClickListener((v) -> {
421             if (mIsStorageGroup) {
422                 mViewModel.setAllFilesAccess(false);
423                 mViewModel.requestChange(false, this, this, ChangeRequest.GRANT_BOTH,
424                         APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__ALLOW);
425                 setResult(GRANTED_ALWAYS);
426             } else {
427                 mViewModel.requestChange(false, this, this, ChangeRequest.GRANT_FOREGROUND_ONLY,
428                         APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__ALLOW_FOREGROUND);
429                 setResult(GRANTED_FOREGROUND_ONLY);
430             }
431         });
432         // mAskOneTimeButton only shows if checked hence should do nothing
433         mAskButton.setOnClickListener((v) -> {
434             mViewModel.requestChange(true, this, this, ChangeRequest.REVOKE_BOTH,
435                     APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__ASK_EVERY_TIME);
436             setResult(DENIED);
437         });
438         mAllowLimitedButton.setOnClickListener((v) -> {
439             int buttonPressed =
440                     APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__PHOTOS_SELECTED;
441             mViewModel.requestChange(false, this, this, ChangeRequest.PHOTOS_SELECTED,
442                     buttonPressed);
443         });
444         mSelectPhotosButton.setOnClickListener((v) -> {
445             ButtonState selectState = states.get(ButtonType.SELECT_PHOTOS);
446             if (selectState != null && selectState.isChecked() && !mPhotoPickerTriggered) {
447                 mPhotoPickerTriggered = true;
448                 mViewModel.openPhotoPicker(this);
449             }
450         });
451         mDenyButton.setOnClickListener((v) -> {
452             if (mViewModel.getFullStorageStateLiveData().getValue() != null
453                     && !mViewModel.getFullStorageStateLiveData().getValue().isLegacy()) {
454                 mViewModel.setAllFilesAccess(false);
455             }
456             mViewModel.requestChange(false, this, this, ChangeRequest.REVOKE_BOTH,
457                     APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__DENY);
458             setResult(DENIED_DO_NOT_ASK_AGAIN);
459         });
460         mDenyForegroundButton.setOnClickListener((v) -> {
461             mViewModel.requestChange(false, this, this, ChangeRequest.REVOKE_FOREGROUND,
462                     APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__DENY_FOREGROUND);
463             setResult(DENIED_DO_NOT_ASK_AGAIN);
464         });
465         // Set long variable names to new variables to bypass linter errors.
466         int grantFineLocation =
467                 APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__GRANT_FINE_LOCATION;
468         int revokeFineLocation =
469                 APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__REVOKE_FINE_LOCATION;
470         mLocationAccuracy.setOnClickListener((v) -> {
471             mLocationAccuracySwitch.performClick();
472             if (mLocationAccuracySwitch.isChecked()) {
473                 mViewModel.requestChange(false, this, this, ChangeRequest.GRANT_FINE_LOCATION,
474                         grantFineLocation);
475             } else {
476                 mViewModel.requestChange(false, this, this, ChangeRequest.REVOKE_FINE_LOCATION,
477                         revokeFineLocation);
478             }
479         });
480 
481         setButtonState(mAllowButton, states.get(ButtonType.ALLOW));
482         setButtonState(mAllowAlwaysButton, states.get(ButtonType.ALLOW_ALWAYS));
483         setButtonState(mAllowForegroundButton, states.get(ButtonType.ALLOW_FOREGROUND));
484         setButtonState(mAskOneTimeButton, states.get(ButtonType.ASK_ONCE));
485         setButtonState(mAskButton, states.get(ButtonType.ASK));
486         setButtonState(mDenyButton, states.get(ButtonType.DENY));
487         setButtonState(mDenyForegroundButton, states.get(ButtonType.DENY_FOREGROUND));
488         setButtonState(mAllowLimitedButton, states.get(ButtonType.SELECT_PHOTOS));
489         if (mAllowLimitedButton.getVisibility() == View.VISIBLE) {
490             mAllowButton.setText(R.string.app_permission_button_always_allow_all);
491         } else {
492             mAllowButton.setText(R.string.app_permission_button_allow);
493         }
494 
495         ButtonState locationAccuracyState = states.get(ButtonType.LOCATION_ACCURACY);
496         if (!locationAccuracyState.isShown()) {
497             mLocationAccuracy.setVisibility(View.GONE);
498         } else {
499             mLocationAccuracy.setVisibility(View.VISIBLE);
500         }
501         mLocationAccuracySwitch.setChecked(locationAccuracyState.isChecked());
502         if (!locationAccuracyState.isEnabled()) {
503             mLocationAccuracy.setEnabled(false);
504             mLocationAccuracySwitch.setEnabled(false);
505         }
506 
507         mIsInitialLoad = false;
508 
509         if (mViewModel.getFullStorageStateLiveData().isInitialized()) {
510             setSpecialStorageState(mViewModel.getFullStorageStateLiveData().getValue());
511         }
512     }
513 
allowButtonFrameClickListener()514     private void allowButtonFrameClickListener() {
515         if (!mAllowButton.isEnabled()) {
516             mViewModel.handleDisabledAllowButton(this);
517         } else {
518             mAllowButton.setChecked(true);
519             mViewModel.requestChange(false, this, this, ChangeRequest.GRANT_FOREGROUND,
520                     APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__ALLOW);
521             setResult(GRANTED_ALWAYS);
522         }
523     }
524 
setButtonState(CompoundButton button, AppPermissionViewModel.ButtonState state)525     private void setButtonState(CompoundButton button, AppPermissionViewModel.ButtonState state) {
526         int visible = state.isShown() ? View.VISIBLE : View.GONE;
527         button.setVisibility(visible);
528         if (state.isShown()) {
529             button.setChecked(state.isChecked());
530             button.setEnabled(state.isEnabled());
531         }
532         if (mIsInitialLoad) {
533             button.jumpDrawablesToCurrentState();
534         }
535 
536         if (button == mAllowLimitedButton) {
537             mAllowLimitedPhotosLayout.setVisibility(visible);
538             float endOpacity = state.isChecked() ? 1f : 0f;
539             // On initial load, do not show the fade in/out animation
540             if (mIsInitialLoad) {
541                 mSelectPhotosDivider.setAlpha(endOpacity);
542                 mSelectPhotosButton.setAlpha(endOpacity);
543                 return;
544             }
545             mSelectPhotosButton.animate().alpha(endOpacity)
546                     .setDuration(EDIT_PHOTOS_BUTTON_ANIMATION_LENGTH_MS);
547             mSelectPhotosDivider.animate().alpha(endOpacity)
548                     .setDuration(EDIT_PHOTOS_BUTTON_ANIMATION_LENGTH_MS);
549         }
550     }
551 
setSpecialStorageState(FullStoragePackageState storageState, View v)552     private void setSpecialStorageState(FullStoragePackageState storageState, View v) {
553         if (v == null) {
554             return;
555         }
556 
557         TextView textView = v.requireViewById(R.id.footer_storage_special_app_access);
558         if (mAllowButton == null || !mIsStorageGroup) {
559             textView.setVisibility(View.GONE);
560             return;
561         }
562 
563         mAllowAlwaysButton.setText(R.string.app_permission_button_allow_all_files);
564         mAllowForegroundButton.setText(R.string.app_permission_button_allow_media_only);
565 
566         if (storageState == null) {
567             textView.setVisibility(View.GONE);
568             return;
569         }
570 
571         if (storageState.isLegacy()) {
572             mAllowButton.setText(R.string.app_permission_button_allow_all_files);
573             textView.setVisibility(View.GONE);
574             return;
575         }
576 
577         textView.setText(R.string.app_permission_footer_special_file_access);
578         textView.setVisibility(View.VISIBLE);
579     }
580 
setResult(@rantPermissionsViewHandler.Result int result)581     private void setResult(@GrantPermissionsViewHandler.Result int result) {
582         if (!mPackageName.equals(
583                 getActivity().getIntent().getStringExtra(Intent.EXTRA_PACKAGE_NAME))) {
584             return;
585         }
586         Intent intent = new Intent()
587                 .putExtra(EXTRA_RESULT_PERMISSION_INTERACTED, mPermGroupName)
588                 .putExtra(EXTRA_RESULT_PERMISSION_RESULT, result);
589         getActivity().setResult(RESULT_OK, intent);
590     }
591 
setDetail(Pair<Integer, Integer> detailResIds)592     private void setDetail(Pair<Integer, Integer> detailResIds) {
593         if (detailResIds == null) {
594             mWidgetFrame.setVisibility(View.GONE);
595             mDivider.setVisibility(View.GONE);
596             return;
597         }
598         mWidgetFrame.setVisibility(View.VISIBLE);
599         if (detailResIds.getSecond() != null) {
600             // If the permissions are individually controlled, also show a link to the page that
601             // lets you control them.
602             mDivider.setVisibility(View.VISIBLE);
603             showRightIcon(R.drawable.ic_settings);
604             Bundle args = AllAppPermissionsFragment.createArgs(mPackageName, mPermGroupName, mUser);
605             mWidgetFrame.setOnClickListener(v -> mViewModel.showAllPermissions(this, args));
606             mPermissionDetails.setText(getPreferenceManager().getContext().getString(
607                     detailResIds.getFirst(), detailResIds.getSecond()));
608         } else {
609             mPermissionDetails.setText(getPreferenceManager().getContext().getString(
610                     detailResIds.getFirst()));
611         }
612         mPermissionDetails.setVisibility(View.VISIBLE);
613 
614     }
615 
setAdminSupportDetail(EnforcedAdmin admin)616     private void setAdminSupportDetail(EnforcedAdmin admin) {
617         if (admin != null) {
618             showRightIcon(R.drawable.ic_info);
619             mWidgetFrame.setOnClickListener(v ->
620                     RestrictedLockUtils.sendShowAdminSupportDetailsIntent(getContext(), admin)
621             );
622         } else {
623             mWidgetFrame.removeAllViews();
624         }
625     }
626 
627     /**
628      * Show the given icon on the right of the first radio button.
629      *
630      * @param iconId the resourceId of the drawable to use.
631      */
showRightIcon(int iconId)632     private void showRightIcon(int iconId) {
633         mWidgetFrame.removeAllViews();
634         ImageView imageView = new ImageView(getPreferenceManager().getContext());
635         imageView.setImageResource(iconId);
636         mWidgetFrame.addView(imageView);
637         mWidgetFrame.setVisibility(View.VISIBLE);
638     }
639 
640     /**
641      * Show a dialog that warns the users that they are about to revoke permissions that were
642      * granted by default, or that they are about to grant full file access to an app.
643      *
644      *
645      * The order of operation to revoke a permission granted by default is:
646      * 1. `showConfirmDialog`
647      * 1. [ConfirmDialog.onCreateDialog]
648      * 1. [AppPermissionViewModel.onDenyAnyWay] or [AppPermissionViewModel.onConfirmFileAccess]
649      * TODO: Remove once data can be passed between dialogs and fragments with nav component
650      *
651      * @param changeRequest Whether background or foreground should be changed
652      * @param messageId     The Id of the string message to show
653      * @param buttonPressed Button which was pressed to initiate the dialog, one of
654      *                      AppPermissionFragmentActionReported.button_pressed constants
655      * @param oneTime       Whether the one-time (ask) button was clicked rather than the deny
656      *                      button
657      */
658     @Override
showConfirmDialog(ChangeRequest changeRequest, @StringRes int messageId, int buttonPressed, boolean oneTime)659     public void showConfirmDialog(ChangeRequest changeRequest, @StringRes int messageId,
660             int buttonPressed, boolean oneTime) {
661         Bundle args = getArguments().deepCopy();
662         args.putInt(ConfirmDialog.MSG, messageId);
663         args.putSerializable(ConfirmDialog.CHANGE_REQUEST, changeRequest);
664         args.putInt(ConfirmDialog.BUTTON, buttonPressed);
665         args.putBoolean(ConfirmDialog.ONE_TIME, oneTime);
666         ConfirmDialog defaultDenyDialog = new ConfirmDialog();
667         defaultDenyDialog.setCancelable(true);
668         defaultDenyDialog.setArguments(args);
669         defaultDenyDialog.show(getChildFragmentManager().beginTransaction(),
670                 ConfirmDialog.class.getName());
671     }
672 
673     /**
674      * A dialog warning the user that they are about to deny a permission that was granted by
675      * default, or that they are denying a permission on a Pre-M app
676      *
677      * @see AppPermissionViewModel.ConfirmDialogShowingFragment#showConfirmDialog(ChangeRequest,
678      * int, int, boolean)
679      * @see #showConfirmDialog(ChangeRequest, int, int)
680      */
681     public static class ConfirmDialog extends DialogFragment {
682         static final String MSG = ConfirmDialog.class.getName() + ".arg.msg";
683         static final String CHANGE_REQUEST = ConfirmDialog.class.getName()
684                 + ".arg.changeRequest";
685         private static final String KEY = ConfirmDialog.class.getName() + ".arg.key";
686         private static final String BUTTON = ConfirmDialog.class.getName() + ".arg.button";
687         private static final String ONE_TIME = ConfirmDialog.class.getName() + ".arg.onetime";
688         private static int sCode =  APP_PERMISSION_FRAGMENT_ACTION_REPORTED__BUTTON_PRESSED__ALLOW;
689         @Override
onCreateDialog(Bundle savedInstanceState)690         public Dialog onCreateDialog(Bundle savedInstanceState) {
691             // TODO(b/229024576): This code is duplicated, refactor ConfirmDialog for easier
692             // NFF sharing
693             AppPermissionFragment fragment = (AppPermissionFragment) getParentFragment();
694             boolean isGrantFileAccess = getArguments().getSerializable(CHANGE_REQUEST)
695                     == ChangeRequest.GRANT_ALL_FILE_ACCESS;
696             int positiveButtonStringResId = R.string.grant_dialog_button_deny_anyway;
697             if (isGrantFileAccess) {
698                 positiveButtonStringResId = R.string.grant_dialog_button_allow;
699             }
700             AlertDialog.Builder b = new AlertDialog.Builder(getContext())
701                     .setMessage(getArguments().getInt(MSG))
702                     .setNegativeButton(R.string.cancel,
703                             (DialogInterface dialog, int which) -> dialog.cancel())
704                     .setPositiveButton(positiveButtonStringResId,
705                             (DialogInterface dialog, int which) -> {
706                                 if (isGrantFileAccess) {
707                                     fragment.mViewModel.setAllFilesAccess(true);
708                                     fragment.mViewModel.requestChange(false, fragment,
709                                             fragment, ChangeRequest.GRANT_BOTH, sCode);
710                                 } else {
711                                     fragment.mViewModel.onDenyAnyWay((ChangeRequest)
712                                                     getArguments().getSerializable(CHANGE_REQUEST),
713                                             getArguments().getInt(BUTTON),
714                                             getArguments().getBoolean(ONE_TIME));
715                                 }
716                             });
717             Dialog d = b.create();
718             d.setCanceledOnTouchOutside(true);
719             return d;
720         }
721 
722         @Override
onCancel(DialogInterface dialog)723         public void onCancel(DialogInterface dialog) {
724             AppPermissionFragment fragment = (AppPermissionFragment) getParentFragment();
725             fragment.setRadioButtonsState(fragment.mViewModel.getButtonStateLiveData().getValue());
726         }
727     }
728 
729     @Override
showAdvancedConfirmDialog(AdvancedConfirmDialogArgs args)730     public void showAdvancedConfirmDialog(AdvancedConfirmDialogArgs args) {
731         AlertDialog.Builder b = new AlertDialog.Builder(getContext())
732                 .setIcon(args.getIconId())
733                 .setMessage(args.getMessageId())
734                 .setOnCancelListener((DialogInterface dialog) -> {
735                     setRadioButtonsState(mViewModel.getButtonStateLiveData().getValue());
736                 })
737                 .setNegativeButton(args.getNegativeButtonTextId(),
738                         (DialogInterface dialog, int which) -> {
739                             setRadioButtonsState(mViewModel.getButtonStateLiveData().getValue());
740                         })
741                 .setPositiveButton(args.getPositiveButtonTextId(),
742                         (DialogInterface dialog, int which) -> {
743                             mViewModel.requestChange(args.getSetOneTime(),
744                                     AppPermissionFragment.this, AppPermissionFragment.this,
745                                     args.getChangeRequest(), args.getButtonClicked());
746                         });
747         if (args.getTitleId() != 0) {
748             b.setTitle(args.getTitleId());
749         }
750         b.show();
751     }
752 }
753