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 package com.android.permissioncontroller.permission.ui.handheld;
17 
18 import static com.android.permissioncontroller.Constants.EXTRA_SESSION_ID;
19 import static com.android.permissioncontroller.Constants.INVALID_SESSION_ID;
20 import static com.android.permissioncontroller.permission.ui.Category.ALLOWED;
21 import static com.android.permissioncontroller.permission.ui.Category.ALLOWED_FOREGROUND;
22 import static com.android.permissioncontroller.permission.ui.Category.ASK;
23 import static com.android.permissioncontroller.permission.ui.Category.DENIED;
24 import static com.android.permissioncontroller.permission.ui.handheld.UtilsKt.pressBack;
25 import static com.android.permissioncontroller.permission.ui.handheld.dashboard.UtilsKt.shouldShowPermissionsDashboard;
26 
27 import android.Manifest;
28 import android.app.ActionBar;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.graphics.drawable.Drawable;
32 import android.os.Build;
33 import android.os.Bundle;
34 import android.os.Handler;
35 import android.os.Looper;
36 import android.os.UserHandle;
37 import android.util.ArrayMap;
38 import android.view.Menu;
39 import android.view.MenuInflater;
40 import android.view.MenuItem;
41 import android.view.View;
42 
43 import androidx.annotation.NonNull;
44 import androidx.annotation.RequiresApi;
45 import androidx.lifecycle.ViewModelProvider;
46 import androidx.preference.Preference;
47 import androidx.preference.PreferenceCategory;
48 
49 import com.android.modules.utils.build.SdkLevel;
50 import com.android.permissioncontroller.R;
51 import com.android.permissioncontroller.permission.model.AppPermissionUsage;
52 import com.android.permissioncontroller.permission.model.PermissionUsages;
53 import com.android.permissioncontroller.permission.ui.Category;
54 import com.android.permissioncontroller.permission.ui.ManagePermissionsActivity;
55 import com.android.permissioncontroller.permission.ui.model.PermissionAppsViewModel;
56 import com.android.permissioncontroller.permission.ui.model.PermissionAppsViewModelFactory;
57 import com.android.permissioncontroller.permission.utils.KotlinUtils;
58 import com.android.permissioncontroller.permission.utils.Utils;
59 import com.android.settingslib.HelpUtils;
60 import com.android.settingslib.utils.applications.AppUtils;
61 
62 import java.text.Collator;
63 import java.util.ArrayList;
64 import java.util.List;
65 import java.util.Map;
66 import java.util.Random;
67 
68 import kotlin.Pair;
69 
70 /**
71  * Show and manage apps which request a single permission group.
72  *
73  * <p>Shows a list of apps which request at least on permission of this group.
74  */
75 public final class PermissionAppsFragment extends SettingsWithLargeHeader implements
76         PermissionUsages.PermissionsUsagesChangeCallback {
77 
78     private static final String KEY_SHOW_SYSTEM_PREFS = "_showSystem";
79     private static final String CREATION_LOGGED_SYSTEM_PREFS = "_creationLogged";
80     private static final String KEY_FOOTER = "_footer";
81     private static final String KEY_EMPTY = "_empty";
82     private static final String LOG_TAG = "PermissionAppsFragment";
83     private static final String STORAGE_ALLOWED_FULL = "allowed_storage_full";
84     private static final String STORAGE_ALLOWED_SCOPED = "allowed_storage_scoped";
85     private static final int SHOW_LOAD_DELAY_MS = 200;
86 
87     private static final int MENU_PERMISSION_USAGE = MENU_HIDE_SYSTEM + 1;
88 
89     /**
90      * Create a bundle with the arguments needed by this fragment
91      *
92      * @param permGroupName The name of the permission group
93      * @param sessionId     The current session ID
94      * @return A bundle with all of the args placed
95      */
createArgs(String permGroupName, long sessionId)96     public static Bundle createArgs(String permGroupName, long sessionId) {
97         Bundle arguments = new Bundle();
98         arguments.putString(Intent.EXTRA_PERMISSION_GROUP_NAME, permGroupName);
99         arguments.putLong(EXTRA_SESSION_ID, sessionId);
100         return arguments;
101     }
102 
103     private MenuItem mShowSystemMenu;
104     private MenuItem mHideSystemMenu;
105     private String mPermGroupName;
106     private Collator mCollator;
107     private PermissionAppsViewModel mViewModel;
108     private PermissionUsages mPermissionUsages;
109     private List<AppPermissionUsage> mAppPermissionUsages = new ArrayList<>();
110 
111     @Override
onCreate(Bundle savedInstanceState)112     public void onCreate(Bundle savedInstanceState) {
113         super.onCreate(savedInstanceState);
114 
115         mPermGroupName = getArguments().getString(Intent.EXTRA_PERMISSION_GROUP_NAME);
116         if (mPermGroupName == null) {
117             mPermGroupName = getArguments().getString(Intent.EXTRA_PERMISSION_NAME);
118         }
119 
120         mCollator = Collator.getInstance(
121                 getContext().getResources().getConfiguration().getLocales().get(0));
122 
123         PermissionAppsViewModelFactory factory =
124                 new PermissionAppsViewModelFactory(getActivity().getApplication(), mPermGroupName,
125                         this, new Bundle());
126         mViewModel = new ViewModelProvider(this, factory).get(PermissionAppsViewModel.class);
127 
128         mViewModel.getCategorizedAppsLiveData().observe(this, this::onPackagesLoaded);
129         mViewModel.getShouldShowSystemLiveData().observe(this, this::updateMenu);
130         mViewModel.getHasSystemAppsLiveData().observe(this, (Boolean hasSystem) ->
131                 getActivity().invalidateOptionsMenu());
132 
133         if (!mViewModel.arePackagesLoaded()) {
134             Handler handler = new Handler(Looper.getMainLooper());
135             handler.postDelayed(() -> {
136                 if (!mViewModel.arePackagesLoaded()) {
137                     setLoading(true /* loading */, false /* animate */);
138                 }
139             }, SHOW_LOAD_DELAY_MS);
140         }
141 
142         setHasOptionsMenu(true);
143         final ActionBar ab = getActivity().getActionBar();
144         if (ab != null) {
145             ab.setDisplayHomeAsUpEnabled(true);
146         }
147 
148         // If the build type is below S, the app ops for permission usage can't be found. Thus, we
149         // shouldn't load permission usages, for them.
150         if (SdkLevel.isAtLeastS()) {
151             Context context = getPreferenceManager().getContext();
152             mPermissionUsages = new PermissionUsages(context);
153 
154             long filterTimeBeginMillis = mViewModel.getFilterTimeBeginMillis();
155             mPermissionUsages.load(null, null, filterTimeBeginMillis, Long.MAX_VALUE,
156                     PermissionUsages.USAGE_FLAG_LAST, getActivity().getLoaderManager(),
157                     false, false, this, false);
158         }
159     }
160 
161     @Override
162     @RequiresApi(Build.VERSION_CODES.S)
onPermissionUsagesChanged()163     public void onPermissionUsagesChanged() {
164         if (mPermissionUsages.getUsages().isEmpty()) {
165             return;
166         }
167         if (getContext() == null) {
168             // Async result has come in after our context is gone.
169             return;
170         }
171 
172         mAppPermissionUsages = new ArrayList<>(mPermissionUsages.getUsages());
173         onPackagesLoaded(mViewModel.getCategorizedAppsLiveData().getValue());
174     }
175 
176     @Override
onCreateOptionsMenu(Menu menu, MenuInflater inflater)177     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
178         super.onCreateOptionsMenu(menu, inflater);
179 
180         if (mViewModel.getHasSystemAppsLiveData().getValue()) {
181             mShowSystemMenu = menu.add(Menu.NONE, MENU_SHOW_SYSTEM, Menu.NONE,
182                     R.string.menu_show_system);
183             mHideSystemMenu = menu.add(Menu.NONE, MENU_HIDE_SYSTEM, Menu.NONE,
184                     R.string.menu_hide_system);
185             updateMenu(mViewModel.getShouldShowSystemLiveData().getValue());
186         }
187 
188         if (shouldShowPermissionsDashboard()) {
189             menu.add(Menu.NONE, MENU_PERMISSION_USAGE, Menu.NONE, R.string.permission_usage_title);
190         }
191 
192         if (!SdkLevel.isAtLeastS()) {
193             HelpUtils.prepareHelpMenuItem(getActivity(), menu, R.string.help_app_permissions,
194                     getClass().getName());
195         }
196     }
197 
198     @Override
onOptionsItemSelected(MenuItem item)199     public boolean onOptionsItemSelected(MenuItem item) {
200         switch (item.getItemId()) {
201             case android.R.id.home:
202                 mViewModel.updateShowSystem(false);
203                 pressBack(this);
204                 return true;
205             case MENU_SHOW_SYSTEM:
206             case MENU_HIDE_SYSTEM:
207                 mViewModel.updateShowSystem(item.getItemId() == MENU_SHOW_SYSTEM);
208                 break;
209             case MENU_PERMISSION_USAGE:
210                 getActivity().startActivity(new Intent(Intent.ACTION_REVIEW_PERMISSION_USAGE)
211                         .setClass(getContext(), ManagePermissionsActivity.class)
212                         .putExtra(Intent.EXTRA_PERMISSION_GROUP_NAME, mPermGroupName));
213                 return true;
214         }
215         return super.onOptionsItemSelected(item);
216     }
217 
updateMenu(Boolean showSystem)218     private void updateMenu(Boolean showSystem) {
219         if (showSystem == null) {
220             showSystem = false;
221         }
222         if (mShowSystemMenu != null && mHideSystemMenu != null) {
223             mShowSystemMenu.setVisible(!showSystem);
224             mHideSystemMenu.setVisible(showSystem);
225         }
226     }
227 
228     @Override
onViewCreated(View view, Bundle savedInstanceState)229     public void onViewCreated(View view, Bundle savedInstanceState) {
230         super.onViewCreated(view, savedInstanceState);
231         bindUi(this, mPermGroupName);
232     }
233 
bindUi(SettingsWithLargeHeader fragment, @NonNull String groupName)234     private static void bindUi(SettingsWithLargeHeader fragment, @NonNull String groupName) {
235         Context context = fragment.getContext();
236         if (context == null || fragment.getActivity() == null) {
237             return;
238         }
239         Drawable icon = KotlinUtils.INSTANCE.getPermGroupIcon(context, groupName);
240 
241         CharSequence label = KotlinUtils.INSTANCE.getPermGroupLabel(context, groupName);
242         CharSequence description = KotlinUtils.INSTANCE.getPermGroupDescription(context, groupName);
243 
244         fragment.setHeader(icon, label, null, null, true);
245         fragment.setSummary(Utils.getPermissionGroupDescriptionString(fragment.getActivity(),
246                 groupName, description), null);
247         fragment.getActivity().setTitle(label);
248     }
249 
onPackagesLoaded(Map<Category, List<Pair<String, UserHandle>>> categories)250     private void onPackagesLoaded(Map<Category, List<Pair<String, UserHandle>>> categories) {
251         boolean isStorage = mPermGroupName.equals(Manifest.permission_group.STORAGE);
252         if (getPreferenceScreen() == null) {
253             if (isStorage) {
254                 addPreferencesFromResource(R.xml.allowed_denied_storage);
255             } else {
256                 addPreferencesFromResource(R.xml.allowed_denied);
257             }
258             // Hide allowed foreground label by default, to avoid briefly showing it before updating
259             findPreference(ALLOWED_FOREGROUND.getCategoryName()).setVisible(false);
260         }
261         Context context = getPreferenceManager().getContext();
262 
263         if (context == null || getActivity() == null || categories == null) {
264             return;
265         }
266 
267         Map<String, Preference> existingPrefs = new ArrayMap<>();
268 
269         for (int i = 0; i < getPreferenceScreen().getPreferenceCount(); i++) {
270             PreferenceCategory category = (PreferenceCategory)
271                     getPreferenceScreen().getPreference(i);
272             category.setOrderingAsAdded(true);
273             int numPreferences = category.getPreferenceCount();
274             for (int j = 0; j < numPreferences; j++) {
275                 Preference preference = category.getPreference(j);
276                 existingPrefs.put(preference.getKey(), preference);
277             }
278             category.removeAll();
279         }
280 
281         long viewIdForLogging = new Random().nextLong();
282         long sessionId = getArguments().getLong(EXTRA_SESSION_ID, INVALID_SESSION_ID);
283 
284         Boolean showAlways = mViewModel.getShowAllowAlwaysStringLiveData().getValue();
285         if (!isStorage) {
286             if (showAlways != null && showAlways) {
287                 findPreference(ALLOWED.getCategoryName()).setTitle(R.string.allowed_always_header);
288             } else {
289                 findPreference(ALLOWED.getCategoryName()).setTitle(R.string.allowed_header);
290             }
291         }
292 
293         // A mapping of user + packageName to their last access timestamps for the permission group.
294         Map<String, Long> groupUsageLastAccessTime =
295                 mViewModel.extractGroupUsageLastAccessTime(mAppPermissionUsages);
296 
297         for (Category grantCategory : categories.keySet()) {
298             List<Pair<String, UserHandle>> packages = categories.get(grantCategory);
299             PreferenceCategory category = findPreference(grantCategory.getCategoryName());
300 
301 
302             // If this category is empty, and this isn't the "allowed" category of the storage
303             // permission, set up the empty preference.
304             if (packages.size() == 0 && (!isStorage || !grantCategory.equals(ALLOWED))) {
305                 Preference empty = new Preference(context);
306                 empty.setSelectable(false);
307                 empty.setKey(category.getKey() + KEY_EMPTY);
308                 if (grantCategory.equals(ALLOWED)) {
309                     empty.setTitle(getString(R.string.no_apps_allowed));
310                 } else if (grantCategory.equals(ALLOWED_FOREGROUND)) {
311                     category.setVisible(false);
312                 } else if (grantCategory.equals(ASK)) {
313                     category.setVisible(false);
314                 } else {
315                     empty.setTitle(getString(R.string.no_apps_denied));
316                 }
317                 category.addPreference(empty);
318                 continue;
319             } else if (grantCategory.equals(ALLOWED_FOREGROUND)) {
320                 category.setVisible(true);
321             } else if (grantCategory.equals(ASK)) {
322                 category.setVisible(true);
323             }
324 
325             for (Pair<String, UserHandle> packageUserLabel : packages) {
326                 String packageName = packageUserLabel.getFirst();
327                 UserHandle user = packageUserLabel.getSecond();
328 
329                 String key = user + packageName;
330 
331                 Long lastAccessTime = groupUsageLastAccessTime.get(key);
332                 Pair<String, Integer> summaryTimestamp = Utils
333                         .getPermissionLastAccessSummaryTimestamp(
334                                 lastAccessTime, context, mPermGroupName);
335 
336                 if (isStorage && grantCategory.equals(ALLOWED)) {
337                     category = mViewModel.packageHasFullStorage(packageName, user)
338                             ? findPreference(STORAGE_ALLOWED_FULL)
339                             : findPreference(STORAGE_ALLOWED_SCOPED);
340                 }
341 
342                 Preference existingPref = existingPrefs.get(key);
343                 if (existingPref != null) {
344                     updatePreferenceSummary(existingPref, summaryTimestamp);
345                     category.addPreference(existingPref);
346                     continue;
347                 }
348 
349                 SmartIconLoadPackagePermissionPreference pref =
350                         new SmartIconLoadPackagePermissionPreference(getActivity().getApplication(),
351                                 packageName, user, context);
352                 pref.setKey(key);
353                 pref.setTitle(KotlinUtils.INSTANCE.getPackageLabel(getActivity().getApplication(),
354                         packageName, user));
355                 pref.setOnPreferenceClickListener((Preference p) -> {
356                     mViewModel.navigateToAppPermission(this, packageName, user,
357                             AppPermissionFragment.createArgs(packageName, null, mPermGroupName,
358                                     user, getClass().getName(), sessionId,
359                                     grantCategory.getCategoryName()));
360                     return true;
361                 });
362                 pref.setTitleContentDescription(AppUtils.getAppContentDescription(context,
363                         packageName, user.getIdentifier()));
364 
365                 updatePreferenceSummary(pref, summaryTimestamp);
366 
367                 category.addPreference(pref);
368                 if (!mViewModel.getCreationLogged()) {
369                     logPermissionAppsFragmentCreated(packageName, user, viewIdForLogging,
370                             grantCategory.equals(ALLOWED), grantCategory.equals(ALLOWED_FOREGROUND),
371                             grantCategory.equals(DENIED));
372                 }
373             }
374 
375             if (isStorage && grantCategory.equals(ALLOWED)) {
376                 PreferenceCategory full = findPreference(STORAGE_ALLOWED_FULL);
377                 PreferenceCategory scoped = findPreference(STORAGE_ALLOWED_SCOPED);
378                 if (full.getPreferenceCount() == 0) {
379                     Preference empty = new Preference(context);
380                     empty.setSelectable(false);
381                     empty.setKey(STORAGE_ALLOWED_FULL + KEY_EMPTY);
382                     empty.setTitle(getString(R.string.no_apps_allowed_full));
383                     full.addPreference(empty);
384                 }
385 
386                 if (scoped.getPreferenceCount() == 0) {
387                     Preference empty = new Preference(context);
388                     empty.setSelectable(false);
389                     empty.setKey(STORAGE_ALLOWED_FULL + KEY_EMPTY);
390                     empty.setTitle(getString(R.string.no_apps_allowed_scoped));
391                     scoped.addPreference(empty);
392                 }
393                 KotlinUtils.INSTANCE.sortPreferenceGroup(full, this::comparePreference, false);
394                 KotlinUtils.INSTANCE.sortPreferenceGroup(scoped, this::comparePreference, false);
395             } else {
396                 KotlinUtils.INSTANCE.sortPreferenceGroup(category, this::comparePreference, false);
397             }
398         }
399 
400         mViewModel.setCreationLogged(true);
401 
402         setLoading(false /* loading */, true /* animate */);
403     }
404 
updatePreferenceSummary(Preference preference, Pair<String, Integer> summaryTimestamp)405     private void updatePreferenceSummary(Preference preference,
406             Pair<String, Integer> summaryTimestamp) {
407         String summary = mViewModel.getPreferenceSummary(getResources(), summaryTimestamp);
408         if (!summary.isEmpty()) {
409             preference.setSummary(summary);
410         }
411     }
412 
413 
comparePreference(Preference lhs, Preference rhs)414     private int comparePreference(Preference lhs, Preference rhs) {
415         int result = mCollator.compare(lhs.getTitle().toString(),
416                 rhs.getTitle().toString());
417         if (result == 0) {
418             result = lhs.getKey().compareTo(rhs.getKey());
419         }
420         return result;
421     }
422 
logPermissionAppsFragmentCreated(String packageName, UserHandle user, long viewId, boolean isAllowed, boolean isAllowedForeground, boolean isDenied)423     private void logPermissionAppsFragmentCreated(String packageName, UserHandle user, long viewId,
424             boolean isAllowed, boolean isAllowedForeground, boolean isDenied) {
425         long sessionId = getArguments().getLong(EXTRA_SESSION_ID, 0);
426         mViewModel.logPermissionAppsFragmentCreated(packageName, user, viewId, isAllowed,
427                 isAllowedForeground, isDenied, sessionId, getActivity().getApplication(),
428                 mPermGroupName, LOG_TAG);
429     }
430 }
431