1 /*
2  * Copyright (C) 2015 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 com.android.permissioncontroller.permission.ui.handheld.UtilsKt.pressBack;
20 
21 import android.app.ActionBar;
22 import android.app.AlertDialog;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.graphics.drawable.Drawable;
26 import android.net.Uri;
27 import android.os.Bundle;
28 import android.os.UserHandle;
29 import android.provider.Settings;
30 import android.util.Log;
31 import android.view.MenuItem;
32 import android.widget.Switch;
33 import android.widget.Toast;
34 
35 import androidx.annotation.NonNull;
36 import androidx.annotation.Nullable;
37 import androidx.lifecycle.ViewModelProvider;
38 import androidx.preference.Preference;
39 import androidx.preference.PreferenceCategory;
40 import androidx.preference.PreferenceGroup;
41 
42 import com.android.permissioncontroller.R;
43 import com.android.permissioncontroller.permission.data.PackagePermissionsLiveData;
44 import com.android.permissioncontroller.permission.model.AppPermissionGroup;
45 import com.android.permissioncontroller.permission.model.Permission;
46 import com.android.permissioncontroller.permission.ui.model.AllAppPermissionsViewModel;
47 import com.android.permissioncontroller.permission.ui.model.AllAppPermissionsViewModelFactory;
48 import com.android.permissioncontroller.permission.utils.ArrayUtils;
49 import com.android.permissioncontroller.permission.utils.KotlinUtils;
50 import com.android.permissioncontroller.permission.utils.Utils;
51 
52 import java.text.Collator;
53 import java.util.List;
54 import java.util.Map;
55 
56 /**
57  * Show and manage individual permissions for an app.
58  *
59  * <p>Shows the list of individual runtime and non-runtime permissions the app has requested.
60  */
61 public final class AllAppPermissionsFragment extends SettingsWithLargeHeader {
62 
63     private static final String LOG_TAG = "AllAppPermissionsFragment";
64 
65     private static final String KEY_OTHER = "other_perms";
66 
67     private AllAppPermissionsViewModel mViewModel;
68     private Collator mCollator;
69     private String mPackageName;
70     private String mFilterGroup;
71     private UserHandle mUser;
72 
73     /**
74      * Create a bundle with the arguments needed by this fragment
75      *
76      * @param packageName The name of the package
77      * @param filterGroup An optional group to filter out permissions not in the group
78      * @param userHandle The user of this package
79      * @return A bundle with all of the args placed
80      */
createArgs(@onNull String packageName, @Nullable String filterGroup, @NonNull UserHandle userHandle)81     public static Bundle createArgs(@NonNull String packageName, @Nullable String filterGroup,
82             @NonNull UserHandle userHandle) {
83         Bundle arguments = new Bundle();
84         arguments.putString(Intent.EXTRA_PACKAGE_NAME, packageName);
85         arguments.putString(Intent.EXTRA_PERMISSION_GROUP_NAME, filterGroup);
86         arguments.putParcelable(Intent.EXTRA_USER, userHandle);
87         return arguments;
88     }
89 
90     /**
91      * Create a bundle with the arguments needed by this fragment
92      *
93      * @param packageName The name of the package
94      * @param userHandle The user of this package
95      * @return A bundle with all of the args placed
96      */
createArgs(@onNull String packageName, @NonNull UserHandle userHandle)97     public static Bundle createArgs(@NonNull String packageName, @NonNull UserHandle userHandle) {
98         return createArgs(packageName, null, userHandle);
99     }
100 
101     @Override
onCreate(Bundle savedInstanceState)102     public void onCreate(Bundle savedInstanceState) {
103         super.onCreate(savedInstanceState);
104         mPackageName = getArguments().getString(Intent.EXTRA_PACKAGE_NAME);
105         mFilterGroup = getArguments().getString(Intent.EXTRA_PERMISSION_GROUP_NAME);
106         mUser = getArguments().getParcelable(Intent.EXTRA_USER);
107         if (mPackageName == null || mUser == null) {
108             Log.e(LOG_TAG, "Missing required argument EXTRA_PACKAGE_NAME or "
109                     + "EXTRA_USER");
110             pressBack(this);
111         }
112 
113         AllAppPermissionsViewModelFactory factory = new AllAppPermissionsViewModelFactory(
114                 mPackageName, mUser, mFilterGroup);
115 
116         mViewModel = new ViewModelProvider(this, factory).get(AllAppPermissionsViewModel.class);
117         mViewModel.getAllPackagePermissionsLiveData().observe(this, this::updateUi);
118 
119         mCollator = Collator.getInstance(
120                 getContext().getResources().getConfiguration().getLocales().get(0));
121     }
122 
123     @Override
onStart()124     public void onStart() {
125         super.onStart();
126 
127         final ActionBar ab = getActivity().getActionBar();
128         if (ab != null) {
129             ab.setDisplayHomeAsUpEnabled(true);
130         }
131 
132         // If we target a group make this look like app permissions.
133         if (getArguments().getString(Intent.EXTRA_PERMISSION_GROUP_NAME) == null) {
134             getActivity().setTitle(R.string.all_permissions);
135         } else {
136             getActivity().setTitle(R.string.app_permissions);
137         }
138 
139         setHasOptionsMenu(true);
140     }
141 
142     @Override
onOptionsItemSelected(MenuItem item)143     public boolean onOptionsItemSelected(MenuItem item) {
144         switch (item.getItemId()) {
145             case android.R.id.home: {
146                 pressBack(this);
147                 return true;
148             }
149         }
150         return super.onOptionsItemSelected(item);
151     }
152 
updateUi(Map<String, List<String>> groupMap)153     private void updateUi(Map<String, List<String>> groupMap) {
154         if (groupMap == null && mViewModel.getAllPackagePermissionsLiveData().isInitialized()) {
155             Toast.makeText(
156                     getActivity(), R.string.app_not_found_dlg_title, Toast.LENGTH_LONG).show();
157             Log.w(LOG_TAG, "invalid package " + mPackageName);
158             pressBack(this);
159             return;
160         }
161 
162         if (getPreferenceScreen() == null) {
163             addPreferencesFromResource(R.xml.all_permissions);
164         }
165 
166         PreferenceGroup otherGroup = findPreference(KEY_OTHER);
167         otherGroup.removeAll();
168         Preference header = findPreference(HEADER_KEY);
169 
170         getPreferenceScreen().removeAll();
171         getPreferenceScreen().addPreference(otherGroup);
172         getPreferenceScreen().addPreference(header);
173 
174         Drawable icon = KotlinUtils.INSTANCE.getBadgedPackageIcon(getActivity().getApplication(),
175                 mPackageName, mUser);
176         CharSequence label = KotlinUtils.INSTANCE.getPackageLabel(getActivity().getApplication(),
177                 mPackageName, mUser);
178         Intent infoIntent = null;
179         if (!getActivity().getIntent().getBooleanExtra(
180                 AppPermissionGroupsFragment.EXTRA_HIDE_INFO_BUTTON, false)) {
181             infoIntent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
182                     .setData(Uri.fromParts("package", mPackageName, null));
183         }
184         setHeader(icon, label, infoIntent, mUser, false);
185         if (groupMap != null) {
186             for (String groupName : groupMap.keySet()) {
187                 List<String> permissions = groupMap.get(groupName);
188                 if (permissions == null || permissions.isEmpty()) {
189                     continue;
190                 }
191 
192                 PreferenceGroup pref = findOrCreatePrefGroup(groupName);
193                 for (String permName : permissions) {
194                     pref.addPreference(getPreference(permName, groupName));
195                 }
196             }
197         }
198         if (otherGroup.getPreferenceCount() == 0) {
199             otherGroup.setVisible(false);
200         } else {
201             otherGroup.setVisible(true);
202         }
203         KotlinUtils.INSTANCE.sortPreferenceGroup(getPreferenceScreen(), this::comparePreferences,
204                 true
205         );
206 
207         setLoading(false, true);
208     }
209 
comparePreferences(Preference lhs, Preference rhs)210     private int comparePreferences(Preference lhs, Preference rhs) {
211         String lKey = lhs.getKey();
212         String rKey = rhs.getKey();
213         if (lKey.equals(KEY_OTHER)) {
214             return 1;
215         } else if (rKey.equals(KEY_OTHER)) {
216             return -1;
217         }
218         if (Utils.isModernPermissionGroup(lKey)
219                 != Utils.isModernPermissionGroup(rKey)) {
220             return Utils.isModernPermissionGroup(lKey) ? -1 : 1;
221         }
222         return mCollator.compare(lhs.getTitle().toString(), rhs.getTitle().toString());
223     }
224 
findOrCreatePrefGroup(String groupName)225     private PreferenceGroup findOrCreatePrefGroup(String groupName) {
226         if (groupName.equals(PackagePermissionsLiveData.NON_RUNTIME_NORMAL_PERMS)) {
227             return findPreference(KEY_OTHER);
228         }
229         PreferenceGroup pref = findPreference(groupName);
230         if (pref == null) {
231             pref = new PreferenceCategory(getPreferenceManager().getContext());
232             pref.setKey(groupName);
233             pref.setTitle(KotlinUtils.INSTANCE.getPermGroupLabel(getContext(), groupName));
234             getPreferenceScreen().addPreference(pref);
235         } else {
236             pref.removeAll();
237         }
238         return pref;
239     }
240 
getPreference(String permName, String groupName)241     private Preference getPreference(String permName, String groupName) {
242         final Preference pref;
243         Context context = getPreferenceManager().getContext();
244 
245         // We allow individual permission control for some permissions if review enabled
246         final boolean mutable = Utils.isPermissionIndividuallyControlled(getContext(),
247                 permName);
248         if (mutable) {
249             AppPermissionGroup appPermGroup = AppPermissionGroup.create(
250                     getActivity().getApplication(), mPackageName, groupName, mUser, false);
251             pref = new MyMultiTargetSwitchPreference(context, permName, appPermGroup);
252         } else {
253             pref = new Preference(context);
254         }
255         pref.setIcon(KotlinUtils.INSTANCE.getPermInfoIcon(context, permName));
256         pref.setTitle(KotlinUtils.INSTANCE.getPermInfoLabel(context, permName));
257         pref.setSingleLineTitle(false);
258         final CharSequence desc = KotlinUtils.INSTANCE.getPermInfoDescription(context,
259                 permName);
260 
261         pref.setOnPreferenceClickListener((Preference preference) -> {
262             new AlertDialog.Builder(getContext())
263                     .setMessage(desc)
264                     .setPositiveButton(android.R.string.ok, null)
265                     .show();
266             return mutable;
267         });
268 
269         return pref;
270     }
271 
272     private static final class MyMultiTargetSwitchPreference extends MultiTargetSwitchPreference {
MyMultiTargetSwitchPreference(Context context, String permission, AppPermissionGroup appPermissionGroup)273         MyMultiTargetSwitchPreference(Context context, String permission,
274                 AppPermissionGroup appPermissionGroup) {
275             super(context);
276 
277             setChecked(appPermissionGroup.areRuntimePermissionsGranted(
278                     new String[]{permission}));
279 
280             setSwitchOnClickListener(v -> {
281                 Switch switchView = (Switch) v;
282                 if (switchView.isChecked()) {
283                     appPermissionGroup.grantRuntimePermissions(true, false,
284                             new String[]{permission});
285                     // We are granting a permission from a group but since this is an
286                     // individual permission control other permissions in the group may
287                     // be revoked, hence we need to mark them user fixed to prevent the
288                     // app from requesting a non-granted permission and it being granted
289                     // because another permission in the group is granted. This applies
290                     // only to apps that support runtime permissions.
291                     if (appPermissionGroup.doesSupportRuntimePermissions()) {
292                         int grantedCount = 0;
293                         String[] revokedPermissionsToFix = null;
294                         final int permissionCount = appPermissionGroup.getPermissions().size();
295                         for (int i = 0; i < permissionCount; i++) {
296                             Permission current = appPermissionGroup.getPermissions().get(i);
297                             if (!current.isGrantedIncludingAppOp()) {
298                                 if (!current.isUserFixed()) {
299                                     revokedPermissionsToFix = ArrayUtils.appendString(
300                                             revokedPermissionsToFix, current.getName());
301                                 }
302                             } else {
303                                 grantedCount++;
304                             }
305                         }
306                         if (revokedPermissionsToFix != null) {
307                             // If some permissions were not granted then they should be fixed.
308                             appPermissionGroup.revokeRuntimePermissions(true,
309                                     revokedPermissionsToFix);
310                         } else if (appPermissionGroup.getPermissions().size() == grantedCount) {
311                             // If all permissions are granted then they should not be fixed.
312                             appPermissionGroup.grantRuntimePermissions(true, false);
313                         }
314                     }
315                 } else {
316                     appPermissionGroup.revokeRuntimePermissions(true,
317                             new String[]{permission});
318                     // If we just revoked the last permission we need to clear
319                     // the user fixed state as now the app should be able to
320                     // request them at runtime if supported.
321                     if (appPermissionGroup.doesSupportRuntimePermissions()
322                             && !appPermissionGroup.areRuntimePermissionsGranted()) {
323                         appPermissionGroup.revokeRuntimePermissions(false);
324                     }
325                 }
326             });
327         }
328     }
329 }
330