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