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.Constants.EXTRA_SESSION_ID;
20 import static com.android.permissioncontroller.Constants.INVALID_SESSION_ID;
21 import static com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSIONS_FRAGMENT_VIEWED;
22 import static com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSIONS_FRAGMENT_VIEWED__CATEGORY__ALLOWED;
23 import static com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSIONS_FRAGMENT_VIEWED__CATEGORY__ALLOWED_FOREGROUND;
24 import static com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSIONS_FRAGMENT_VIEWED__CATEGORY__DENIED;
25 import static com.android.permissioncontroller.permission.ui.handheld.UtilsKt.pressBack;
26 
27 import android.app.ActionBar;
28 import android.app.Activity;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.graphics.drawable.Drawable;
32 import android.icu.text.ListFormatter;
33 import android.net.Uri;
34 import android.os.Bundle;
35 import android.os.UserHandle;
36 import android.provider.Settings;
37 import android.util.Log;
38 import android.view.LayoutInflater;
39 import android.view.Menu;
40 import android.view.MenuInflater;
41 import android.view.MenuItem;
42 import android.view.View;
43 import android.view.ViewGroup;
44 import android.widget.Toast;
45 
46 import androidx.annotation.NonNull;
47 import androidx.annotation.StringRes;
48 import androidx.lifecycle.ViewModelProvider;
49 import androidx.preference.Preference;
50 import androidx.preference.PreferenceCategory;
51 import androidx.preference.PreferenceScreen;
52 import androidx.preference.SwitchPreference;
53 
54 import com.android.permissioncontroller.PermissionControllerStatsLog;
55 import com.android.permissioncontroller.R;
56 import com.android.permissioncontroller.permission.model.livedatatypes.AutoRevokeState;
57 import com.android.permissioncontroller.permission.ui.Category;
58 import com.android.permissioncontroller.permission.ui.model.AppPermissionGroupsViewModel;
59 import com.android.permissioncontroller.permission.ui.model.AppPermissionGroupsViewModel.GroupUiInfo;
60 import com.android.permissioncontroller.permission.ui.model.AppPermissionGroupsViewModel.PermSubtitle;
61 import com.android.permissioncontroller.permission.ui.model.AppPermissionGroupsViewModelFactory;
62 import com.android.permissioncontroller.permission.utils.KotlinUtils;
63 import com.android.permissioncontroller.permission.utils.Utils;
64 import com.android.settingslib.HelpUtils;
65 
66 import java.text.Collator;
67 import java.util.ArrayList;
68 import java.util.List;
69 import java.util.Map;
70 import java.util.Random;
71 
72 /**
73  * Show and manage permission groups for an app.
74  *
75  * <p>Shows the list of permission groups the app has requested at one permission for.
76  */
77 public final class AppPermissionGroupsFragment extends SettingsWithLargeHeader {
78 
79     private static final String LOG_TAG = AppPermissionGroupsFragment.class.getSimpleName();
80     private static final String IS_SYSTEM_PERMS_SCREEN = "_is_system_screen";
81     private static final String AUTO_REVOKE_CATEGORY_KEY = "_AUTO_REVOKE_KEY";
82     private static final String AUTO_REVOKE_SWITCH_KEY = "_AUTO_REVOKE_SWITCH_KEY";
83     private static final String AUTO_REVOKE_SUMMARY_KEY = "_AUTO_REVOKE_SUMMARY_KEY";
84 
85     static final String EXTRA_HIDE_INFO_BUTTON = "hideInfoButton";
86 
87     private AppPermissionGroupsViewModel mViewModel;
88     private boolean mIsSystemPermsScreen;
89     private boolean mIsFirstLoad;
90     private String mPackageName;
91     private UserHandle mUser;
92 
93     private Collator mCollator;
94 
95     /**
96      * Create a bundle with the arguments needed by this fragment
97      *
98      * @param packageName The name of the package
99      * @param userHandle The user of this package
100      * @param sessionId The current session ID
101      * @param isSystemPermsScreen Whether or not this screen is the system permission screen, or
102      * the extra permissions screen
103      *
104      * @return A bundle with all of the args placed
105      */
createArgs(@onNull String packageName, @NonNull UserHandle userHandle, long sessionId, boolean isSystemPermsScreen)106     public static Bundle createArgs(@NonNull String packageName, @NonNull UserHandle userHandle,
107             long sessionId, boolean isSystemPermsScreen) {
108         Bundle arguments = new Bundle();
109         arguments.putString(Intent.EXTRA_PACKAGE_NAME, packageName);
110         arguments.putParcelable(Intent.EXTRA_USER, userHandle);
111         arguments.putLong(EXTRA_SESSION_ID, sessionId);
112         arguments.putBoolean(IS_SYSTEM_PERMS_SCREEN, isSystemPermsScreen);
113         return arguments;
114     }
115 
116     /**
117      * Create a bundle for a system permissions fragment
118      *
119      * @param packageName The name of the package
120      * @param userHandle The user of this package
121      * @param sessionId The current session ID
122      *
123      * @return A bundle with all of the args placed
124      */
createArgs(@onNull String packageName, @NonNull UserHandle userHandle, long sessionId)125     public static Bundle createArgs(@NonNull String packageName, @NonNull UserHandle userHandle,
126             long sessionId) {
127         return createArgs(packageName, userHandle, sessionId, true);
128     }
129 
130     @Override
onCreate(Bundle savedInstanceState)131     public void onCreate(Bundle savedInstanceState) {
132         super.onCreate(savedInstanceState);
133         setHasOptionsMenu(true);
134         mIsFirstLoad = true;
135         final ActionBar ab = getActivity().getActionBar();
136         if (ab != null) {
137             ab.setDisplayHomeAsUpEnabled(true);
138         }
139 
140         mPackageName = getArguments().getString(Intent.EXTRA_PACKAGE_NAME);
141         mUser = getArguments().getParcelable(Intent.EXTRA_USER);
142         mIsSystemPermsScreen = getArguments().getBoolean(IS_SYSTEM_PERMS_SCREEN, true);
143 
144         AppPermissionGroupsViewModelFactory factory =
145                 new AppPermissionGroupsViewModelFactory(mPackageName, mUser,
146                         getArguments().getLong(EXTRA_SESSION_ID, 0));
147 
148         mViewModel = new ViewModelProvider(this, factory).get(AppPermissionGroupsViewModel.class);
149         mViewModel.getPackagePermGroupsLiveData().observe(this, this::updatePreferences);
150         mViewModel.getAutoRevokeLiveData().observe(this, this::setAutoRevokeToggleState);
151 
152         mCollator = Collator.getInstance(
153                 getContext().getResources().getConfiguration().getLocales().get(0));
154 
155         if (mViewModel.getPackagePermGroupsLiveData().getValue() != null) {
156             updatePreferences(mViewModel.getPackagePermGroupsLiveData().getValue());
157         }
158     }
159 
160     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)161     public View onCreateView(LayoutInflater inflater, ViewGroup container,
162             Bundle savedInstanceState) {
163         getActivity().setTitle(R.string.app_permissions);
164         return super.onCreateView(inflater, container, savedInstanceState);
165     }
166 
167     @Override
onOptionsItemSelected(MenuItem item)168     public boolean onOptionsItemSelected(MenuItem item) {
169         switch (item.getItemId()) {
170             case android.R.id.home: {
171                 pressBack(this);
172                 return true;
173             }
174 
175             case MENU_ALL_PERMS: {
176                 mViewModel.showAllPermissions(this, AllAppPermissionsFragment.createArgs(
177                         mPackageName, mUser));
178                 return true;
179             }
180         }
181         return super.onOptionsItemSelected(item);
182     }
183 
184     @Override
onCreateOptionsMenu(Menu menu, MenuInflater inflater)185     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
186         super.onCreateOptionsMenu(menu, inflater);
187         if (mIsSystemPermsScreen) {
188             menu.add(Menu.NONE, MENU_ALL_PERMS, Menu.NONE, R.string.all_permissions);
189             HelpUtils.prepareHelpMenuItem(getActivity(), menu, R.string.help_app_permissions,
190                     getClass().getName());
191         }
192     }
193 
bindUi(SettingsWithLargeHeader fragment, String packageName, UserHandle user)194     private static void bindUi(SettingsWithLargeHeader fragment, String packageName,
195             UserHandle user) {
196         Activity activity = fragment.getActivity();
197         Intent infoIntent = null;
198         if (!activity.getIntent().getBooleanExtra(EXTRA_HIDE_INFO_BUTTON, false)) {
199             infoIntent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
200                     .setData(Uri.fromParts("package", packageName, null));
201         }
202 
203         Drawable icon = KotlinUtils.INSTANCE.getBadgedPackageIcon(activity.getApplication(),
204                 packageName, user);
205         fragment.setHeader(icon, KotlinUtils.INSTANCE.getPackageLabel(activity.getApplication(),
206                 packageName, user), infoIntent, user, false);
207 
208     }
209 
createPreferenceScreenIfNeeded()210     private void createPreferenceScreenIfNeeded() {
211         if (getPreferenceScreen() == null) {
212             addPreferencesFromResource(R.xml.allowed_denied);
213             addAutoRevokePreferences(getPreferenceScreen());
214             bindUi(this, mPackageName, mUser);
215         }
216     }
217 
updatePreferences(Map<Category, List<GroupUiInfo>> groupMap)218     private void updatePreferences(Map<Category, List<GroupUiInfo>> groupMap) {
219         createPreferenceScreenIfNeeded();
220 
221         Context context = getPreferenceManager().getContext();
222         if (context == null) {
223             return;
224         }
225 
226         if (groupMap == null && mViewModel.getPackagePermGroupsLiveData().isInitialized()) {
227             Toast.makeText(
228                     getActivity(), R.string.app_not_found_dlg_title, Toast.LENGTH_LONG).show();
229             Log.w(LOG_TAG, "invalid package " + mPackageName);
230 
231             pressBack(this);
232 
233             return;
234         }
235 
236         findPreference(Category.ALLOWED_FOREGROUND.getCategoryName()).setVisible(false);
237 
238         long sessionId = getArguments().getLong(EXTRA_SESSION_ID, INVALID_SESSION_ID);
239 
240         for (Category grantCategory : groupMap.keySet()) {
241             PreferenceCategory category = findPreference(grantCategory.getCategoryName());
242             int numExtraPerms = 0;
243 
244             category.removeAll();
245 
246             if (grantCategory.equals(Category.ALLOWED_FOREGROUND)) {
247                 category.setVisible(false);
248                 category = findPreference(Category.ALLOWED.getCategoryName());
249             }
250 
251             if (grantCategory.equals(Category.ASK)) {
252                 if (groupMap.get(grantCategory).size() == 0) {
253                     category.setVisible(false);
254                 } else {
255                     category.setVisible(true);
256                 }
257             }
258 
259             for (GroupUiInfo groupInfo : groupMap.get(grantCategory)) {
260                 String groupName = groupInfo.getGroupName();
261 
262                 PermissionControlPreference preference = new PermissionControlPreference(context,
263                         mPackageName, groupName, mUser, AppPermissionGroupsFragment.class.getName(),
264                         sessionId, grantCategory.getCategoryName(), true);
265                 preference.setTitle(KotlinUtils.INSTANCE.getPermGroupLabel(context, groupName));
266                 preference.setIcon(KotlinUtils.INSTANCE.getPermGroupIcon(context, groupName));
267                 preference.setKey(groupName);
268                 switch (groupInfo.getSubtitle()) {
269                     case FOREGROUND_ONLY:
270                         preference.setSummary(R.string.permission_subtitle_only_in_foreground);
271                         break;
272                     case MEDIA_ONLY:
273                         preference.setSummary(R.string.permission_subtitle_media_only);
274                         break;
275                     case ALL_FILES:
276                         preference.setSummary(R.string.permission_subtitle_all_files);
277                         break;
278                 }
279                 if (groupInfo.getSubtitle() == PermSubtitle.FOREGROUND_ONLY) {
280                     preference.setSummary(R.string.permission_subtitle_only_in_foreground);
281                 }
282                 if (groupInfo.isSystem() == mIsSystemPermsScreen) {
283                     category.addPreference(preference);
284                 } else if (!groupInfo.isSystem()) {
285                     numExtraPerms++;
286                 }
287             }
288 
289             int noPermsStringRes = grantCategory.equals(Category.DENIED)
290                     ? R.string.no_permissions_denied : R.string.no_permissions_allowed;
291 
292             if (numExtraPerms > 0) {
293                 final Preference extraPerms = setUpCustomPermissionsScreen(context, numExtraPerms,
294                         grantCategory.getCategoryName());
295                 category.addPreference(extraPerms);
296             }
297 
298             if (category.getPreferenceCount() == 0) {
299                 setNoPermissionPreference(category, noPermsStringRes, context);
300             }
301 
302             KotlinUtils.INSTANCE.sortPreferenceGroup(category, this::comparePreferences, false);
303         }
304 
305         setAutoRevokeToggleState(mViewModel.getAutoRevokeLiveData().getValue());
306 
307         if (mIsFirstLoad) {
308             logAppPermissionGroupsFragmentView();
309             mIsFirstLoad = false;
310         }
311     }
312 
addAutoRevokePreferences(PreferenceScreen screen)313     private void addAutoRevokePreferences(PreferenceScreen screen) {
314         Context context = screen.getPreferenceManager().getContext();
315 
316         PreferenceCategory autoRevokeCategory = new PreferenceCategory(context);
317         autoRevokeCategory.setKey(AUTO_REVOKE_CATEGORY_KEY);
318         screen.addPreference(autoRevokeCategory);
319 
320         SwitchPreference autoRevokeSwitch = new SwitchPreference(context);
321         autoRevokeSwitch.setOnPreferenceClickListener((preference) -> {
322             mViewModel.setAutoRevoke(autoRevokeSwitch.isChecked());
323             return true;
324         });
325         autoRevokeSwitch.setTitle(R.string.auto_revoke_label);
326         autoRevokeSwitch.setKey(AUTO_REVOKE_SWITCH_KEY);
327         autoRevokeCategory.addPreference(autoRevokeSwitch);
328 
329         Preference autoRevokeSummary = new Preference(context);
330         autoRevokeSummary.setIcon(Utils.applyTint(getActivity(), R.drawable.ic_info_outline,
331                 android.R.attr.colorControlNormal));
332         autoRevokeSummary.setKey(AUTO_REVOKE_SUMMARY_KEY);
333         autoRevokeCategory.addPreference(autoRevokeSummary);
334     }
335 
setAutoRevokeToggleState(AutoRevokeState state)336     private void setAutoRevokeToggleState(AutoRevokeState state) {
337         if (state == null || !mViewModel.getPackagePermGroupsLiveData().isInitialized()
338                 || getListView() == null || getView() == null) {
339             return;
340         }
341 
342         PreferenceCategory autoRevokeCategory = getPreferenceScreen()
343                 .findPreference(AUTO_REVOKE_CATEGORY_KEY);
344         SwitchPreference autoRevokeSwitch = autoRevokeCategory.findPreference(
345                 AUTO_REVOKE_SWITCH_KEY);
346         Preference autoRevokeSummary = autoRevokeCategory.findPreference(AUTO_REVOKE_SUMMARY_KEY);
347 
348         if (!state.isEnabledGlobal() || !state.getShouldShowSwitch()) {
349             autoRevokeSwitch.setVisible(false);
350             autoRevokeSummary.setVisible(false);
351             return;
352         }
353         autoRevokeSwitch.setVisible(true);
354         autoRevokeSummary.setVisible(true);
355         autoRevokeSwitch.setChecked(state.isEnabledForApp());
356 
357         List<String> groupLabels = new ArrayList<>();
358         for (String groupName : state.getRevocableGroupNames()) {
359             PreferenceCategory category = getPreferenceScreen().findPreference(
360                     Category.ALLOWED.getCategoryName());
361             Preference pref = category.findPreference(groupName);
362             if (pref != null) {
363                 groupLabels.add(pref.getTitle().toString());
364             }
365         }
366 
367         groupLabels.sort(mCollator);
368         if (groupLabels.isEmpty()) {
369             autoRevokeSummary.setSummary(R.string.auto_revoke_summary);
370         } else {
371             autoRevokeSummary.setSummary(getString(R.string.auto_revoke_summary_with_permissions,
372                     ListFormatter.getInstance().format(groupLabels)));
373         }
374     }
375 
comparePreferences(Preference lhs, Preference rhs)376     private int comparePreferences(Preference lhs, Preference rhs) {
377         String additionalTitle = lhs.getContext().getString(R.string.additional_permissions);
378         if (lhs.getTitle().equals(additionalTitle)) {
379             return 1;
380         } else if (rhs.getTitle().equals(additionalTitle)) {
381             return -1;
382         }
383         return mCollator.compare(lhs.getTitle().toString(),
384                 rhs.getTitle().toString());
385     }
386 
setUpCustomPermissionsScreen(Context context, int count, String category)387     private Preference setUpCustomPermissionsScreen(Context context, int count, String category) {
388         final Preference extraPerms = new Preference(context);
389         extraPerms.setIcon(Utils.applyTint(getActivity(), R.drawable.ic_toc,
390                 android.R.attr.colorControlNormal));
391         extraPerms.setTitle(R.string.additional_permissions);
392         extraPerms.setKey(extraPerms.getTitle() + category);
393         extraPerms.setOnPreferenceClickListener(preference -> {
394             mViewModel.showExtraPerms(this, AppPermissionGroupsFragment.createArgs(
395                     mPackageName, mUser, getArguments().getLong(EXTRA_SESSION_ID), false));
396             return true;
397         });
398         extraPerms.setSummary(getResources().getQuantityString(
399                 R.plurals.additional_permissions_more, count, count));
400         return extraPerms;
401     }
402 
setNoPermissionPreference(PreferenceCategory category, @StringRes int stringId, Context context)403     private void setNoPermissionPreference(PreferenceCategory category, @StringRes int stringId,
404             Context context) {
405         Preference empty = new Preference(context);
406         empty.setKey(getString(stringId));
407         empty.setTitle(empty.getKey());
408         empty.setSelectable(false);
409         category.addPreference(empty);
410     }
411 
logAppPermissionGroupsFragmentView()412     private void logAppPermissionGroupsFragmentView() {
413         Context context = getPreferenceManager().getContext();
414         if (context == null) {
415             return;
416         }
417         String permissionSubtitleOnlyInForeground =
418                 context.getString(R.string.permission_subtitle_only_in_foreground);
419 
420 
421         long sessionId = getArguments().getLong(EXTRA_SESSION_ID, INVALID_SESSION_ID);
422         long viewId = new Random().nextLong();
423 
424         PreferenceCategory allowed = findPreference(Category.ALLOWED.getCategoryName());
425 
426         int numAllowed = allowed.getPreferenceCount();
427         for (int i = 0; i < numAllowed; i++) {
428             Preference preference = allowed.getPreference(i);
429 
430             if (preference.getTitle().equals(getString(R.string.no_permissions_allowed))) {
431                 // R.string.no_permission_allowed was added to PreferenceCategory
432                 continue;
433             }
434 
435             int category = APP_PERMISSIONS_FRAGMENT_VIEWED__CATEGORY__ALLOWED;
436             if (preference.getSummary() != null
437                     && permissionSubtitleOnlyInForeground.contentEquals(preference.getSummary())) {
438                 category = APP_PERMISSIONS_FRAGMENT_VIEWED__CATEGORY__ALLOWED_FOREGROUND;
439             }
440 
441             logAppPermissionsFragmentViewEntry(sessionId, viewId, preference.getKey(),
442                     category);
443         }
444 
445         PreferenceCategory denied = findPreference(Category.DENIED.getCategoryName());
446 
447         int numDenied = denied.getPreferenceCount();
448         for (int i = 0; i < numDenied; i++) {
449             Preference preference = denied.getPreference(i);
450             if (preference.getTitle().equals(getString(R.string.no_permissions_denied))) {
451                 // R.string.no_permission_denied was added to PreferenceCategory
452                 continue;
453             }
454             logAppPermissionsFragmentViewEntry(sessionId, viewId, preference.getKey(),
455                     APP_PERMISSIONS_FRAGMENT_VIEWED__CATEGORY__DENIED);
456         }
457     }
458 
logAppPermissionsFragmentViewEntry( long sessionId, long viewId, String permissionGroupName, int category)459     private void logAppPermissionsFragmentViewEntry(
460             long sessionId, long viewId, String permissionGroupName, int category) {
461 
462         Integer uid = KotlinUtils.INSTANCE.getPackageUid(getActivity().getApplication(),
463                 mPackageName, mUser);
464         if (uid == null) {
465             return;
466         }
467         PermissionControllerStatsLog.write(APP_PERMISSIONS_FRAGMENT_VIEWED, sessionId, viewId,
468                 permissionGroupName, uid, mPackageName, category);
469         Log.v(LOG_TAG, "AppPermissionFragment view logged with sessionId=" + sessionId + " viewId="
470                 + viewId + " permissionGroupName=" + permissionGroupName + " uid="
471                 + uid + " packageName="
472                 + mPackageName + " category=" + category);
473     }
474 }
475