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