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