1 /*
2  * Copyright (C) 2019 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.auto;
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.ManagePermissionsActivity.EXTRA_CALLER_NAME;
26 
27 import static java.util.concurrent.TimeUnit.DAYS;
28 
29 import android.app.Activity;
30 import android.content.Context;
31 import android.content.Intent;
32 import android.content.pm.PackageInfo;
33 import android.os.Build;
34 import android.os.Bundle;
35 import android.os.UserHandle;
36 import android.util.Log;
37 import android.widget.Toast;
38 
39 import androidx.annotation.NonNull;
40 import androidx.annotation.RequiresApi;
41 import androidx.fragment.app.Fragment;
42 import androidx.lifecycle.ViewModelProvider;
43 import androidx.preference.Preference;
44 import androidx.preference.PreferenceCategory;
45 import androidx.preference.PreferenceGroup;
46 
47 import com.android.modules.utils.build.SdkLevel;
48 import com.android.permissioncontroller.PermissionControllerStatsLog;
49 import com.android.permissioncontroller.R;
50 import com.android.permissioncontroller.auto.AutoSettingsFrameFragment;
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.model.AppPermissionGroupsViewModel;
55 import com.android.permissioncontroller.permission.ui.model.AppPermissionGroupsViewModelFactory;
56 import com.android.permissioncontroller.permission.utils.KotlinUtils;
57 
58 import java.text.Collator;
59 import java.time.Instant;
60 import java.util.ArrayList;
61 import java.util.HashMap;
62 import java.util.List;
63 import java.util.Map;
64 import java.util.Random;
65 
66 /** Screen to show the permissions for a specific application. */
67 public class AutoAppPermissionsFragment extends AutoSettingsFrameFragment implements
68         PermissionUsages.PermissionsUsagesChangeCallback {
69     private static final String LOG_TAG = AutoAppPermissionsFragment.class.getSimpleName();
70 
71     private static final String IS_SYSTEM_PERMS_SCREEN = "_is_system_screen";
72     private static final String KEY_ALLOWED_PERMISSIONS_GROUP = Category.ALLOWED.getCategoryName();
73     private static final String KEY_DENIED_PERMISSIONS_GROUP = Category.DENIED.getCategoryName();
74 
75     private AppPermissionGroupsViewModel mViewModel;
76 
77     private String mPackageName;
78     private boolean mIsFirstLoad;
79     private UserHandle mUser;
80     private PermissionUsages mPermissionUsages;
81     private List<AppPermissionUsage> mAppPermissionUsages = new ArrayList<>();
82     private boolean mIsSystemPermsScreen;
83 
84     private Collator mCollator;
85 
86     /**
87      * @return A new fragment
88      */
newInstance(@onNull String packageName, @NonNull UserHandle userHandle, long sessionId, boolean isSystemPermsScreen)89     public static AutoAppPermissionsFragment newInstance(@NonNull String packageName,
90             @NonNull UserHandle userHandle, long sessionId, boolean isSystemPermsScreen) {
91         return setPackageNameAndUserHandle(new AutoAppPermissionsFragment(), packageName,
92                 userHandle, sessionId, isSystemPermsScreen);
93     }
94 
setPackageNameAndUserHandle(@onNull T fragment, @NonNull String packageName, @NonNull UserHandle userHandle, long sessionId, boolean isSystemPermsScreen)95     private static <T extends Fragment> T setPackageNameAndUserHandle(@NonNull T fragment,
96             @NonNull String packageName, @NonNull UserHandle userHandle, long sessionId,
97             boolean isSystemPermsScreen) {
98         Bundle arguments = new Bundle();
99         arguments.putString(Intent.EXTRA_PACKAGE_NAME, packageName);
100         arguments.putParcelable(Intent.EXTRA_USER, userHandle);
101         arguments.putLong(EXTRA_SESSION_ID, sessionId);
102         arguments.putBoolean(IS_SYSTEM_PERMS_SCREEN, isSystemPermsScreen);
103         fragment.setArguments(arguments);
104         return fragment;
105     }
106 
107     @Override
onCreate(Bundle savedInstanceState)108     public void onCreate(Bundle savedInstanceState) {
109         super.onCreate(savedInstanceState);
110         setLoading(true);
111 
112         mPackageName = getArguments().getString(Intent.EXTRA_PACKAGE_NAME);
113         mUser = getArguments().getParcelable(Intent.EXTRA_USER);
114         mIsSystemPermsScreen = getArguments().getBoolean(IS_SYSTEM_PERMS_SCREEN, true);
115         UserHandle userHandle = getArguments().getParcelable(Intent.EXTRA_USER);
116         Activity activity = requireActivity();
117         PackageInfo packageInfo = AutoPermissionsUtils.getPackageInfo(activity, mPackageName,
118                 userHandle);
119         if (packageInfo == null) {
120             Toast.makeText(getContext(), R.string.app_not_found_dlg_title,
121                     Toast.LENGTH_LONG).show();
122             activity.finish();
123             return;
124         }
125 
126         mCollator = Collator.getInstance(
127                 getContext().getResources().getConfiguration().getLocales().get(0));
128         AppPermissionGroupsViewModelFactory factory =
129                 new AppPermissionGroupsViewModelFactory(mPackageName, userHandle,
130                         getArguments().getLong(EXTRA_SESSION_ID, 0));
131         mViewModel = new ViewModelProvider(this, factory).get(AppPermissionGroupsViewModel.class);
132 
133         setHeaderLabel(getContext().getString(R.string.app_permissions));
134         if (mIsSystemPermsScreen) {
135             setAction(getContext().getString(R.string.all_permissions), v -> showAllPermissions());
136         }
137         createPreferenceCategories(packageInfo);
138 
139         mViewModel.getPackagePermGroupsLiveData().observe(this, this::updatePreferences);
140         if (mViewModel.getPackagePermGroupsLiveData().getValue() != null) {
141             updatePreferences(mViewModel.getPackagePermGroupsLiveData().getValue());
142         }
143 
144         if (SdkLevel.isAtLeastS()) {
145             mPermissionUsages = new PermissionUsages(getContext());
146 
147             long filterTimeBeginMillis = Math.max(System.currentTimeMillis()
148                             - DAYS.toMillis(
149                     AppPermissionGroupsViewModel.AGGREGATE_DATA_FILTER_BEGIN_DAYS),
150                     Instant.EPOCH.toEpochMilli());
151             mPermissionUsages.load(null, null, filterTimeBeginMillis, Long.MAX_VALUE,
152                     PermissionUsages.USAGE_FLAG_LAST, getActivity().getLoaderManager(),
153                     false, false, this, false);
154         }
155     }
156 
157     @Override
onCreatePreferences(Bundle bundle, String s)158     public void onCreatePreferences(Bundle bundle, String s) {
159         setPreferenceScreen(getPreferenceManager().createPreferenceScreen(getContext()));
160     }
161 
162 
163     @Override
164     @RequiresApi(Build.VERSION_CODES.S)
onPermissionUsagesChanged()165     public void onPermissionUsagesChanged() {
166         if (mPermissionUsages.getUsages().isEmpty()) {
167             return;
168         }
169         if (getContext() == null) {
170             // Async result has come in after our context is gone.
171             return;
172         }
173 
174         mAppPermissionUsages = new ArrayList<>(mPermissionUsages.getUsages());
175         updatePreferences(mViewModel.getPackagePermGroupsLiveData().getValue());
176     }
177 
showAllPermissions()178     private void showAllPermissions() {
179         Fragment frag = AutoAllAppPermissionsFragment.newInstance(
180                 getArguments().getString(Intent.EXTRA_PACKAGE_NAME),
181                 getArguments().getParcelable(Intent.EXTRA_USER),
182                 getArguments().getLong(EXTRA_SESSION_ID, INVALID_SESSION_ID));
183         getFragmentManager().beginTransaction()
184                 .replace(android.R.id.content, frag)
185                 .addToBackStack("AllPerms")
186                 .commit();
187     }
188 
bindUi(PackageInfo packageInfo)189     protected void bindUi(PackageInfo packageInfo) {
190         getPreferenceScreen().addPreference(
191                 AutoPermissionsUtils.createHeaderPreference(getContext(),
192                         packageInfo.applicationInfo));
193 
194         PreferenceGroup allowed = new PreferenceCategory(getContext());
195         allowed.setKey(KEY_ALLOWED_PERMISSIONS_GROUP);
196         allowed.setTitle(R.string.allowed_header);
197         getPreferenceScreen().addPreference(allowed);
198 
199         PreferenceGroup denied = new PreferenceCategory(getContext());
200         denied.setKey(KEY_DENIED_PERMISSIONS_GROUP);
201         denied.setTitle(R.string.denied_header);
202         getPreferenceScreen().addPreference(denied);
203     }
204 
createPreferenceCategories(PackageInfo packageInfo)205     private void createPreferenceCategories(PackageInfo packageInfo) {
206         bindUi(packageInfo);
207     }
208 
updatePreferences( Map<Category, List<AppPermissionGroupsViewModel.GroupUiInfo>> groupMap)209     private void updatePreferences(
210             Map<Category, List<AppPermissionGroupsViewModel.GroupUiInfo>> groupMap) {
211         Context context = getPreferenceManager().getContext();
212         if (context == null) {
213             return;
214         }
215 
216         if (groupMap == null && mViewModel.getPackagePermGroupsLiveData().isInitialized()) {
217             Toast.makeText(
218                     getActivity(), R.string.app_not_found_dlg_title, Toast.LENGTH_LONG).show();
219             Log.w(LOG_TAG, "invalid package " + mPackageName);
220 
221             getActivity().finish();
222             return;
223         }
224 
225         Map<String, Long> groupUsageLastAccessTime = new HashMap<>();
226         mViewModel.extractGroupUsageLastAccessTime(groupUsageLastAccessTime, mAppPermissionUsages,
227                 mPackageName);
228 
229         for (Category grantCategory : groupMap.keySet()) {
230             if (Category.ASK.equals(grantCategory)) {
231                 // skip ask category for auto
232                 continue;
233             }
234             PreferenceCategory category = getPreferenceScreen().findPreference(
235                     grantCategory.getCategoryName());
236             if (grantCategory.equals(Category.ALLOWED_FOREGROUND)) {
237                 category = findPreference(Category.ALLOWED.getCategoryName());
238             }
239             int numExtraPerms = 0;
240 
241             category.removeAll();
242 
243 
244             for (AppPermissionGroupsViewModel.GroupUiInfo groupInfo : groupMap.get(grantCategory)) {
245                 if (groupInfo.isSystem() == mIsSystemPermsScreen) {
246                     Preference preference = createPermissionPreference(getContext(), groupInfo,
247                             groupUsageLastAccessTime);
248                     category.addPreference(preference);
249                 } else if (!groupInfo.isSystem()) {
250                     numExtraPerms++;
251                 }
252             }
253 
254 
255             if (numExtraPerms > 0) {
256                 setAdditionalPermissionsPreference(category, numExtraPerms, context);
257             }
258 
259             if (category.getPreferenceCount() == 0) {
260                 setNoPermissionPreference(category, grantCategory, context);
261             }
262 
263             KotlinUtils.INSTANCE.sortPreferenceGroup(category, this::comparePreferences, false);
264         }
265 
266 
267         if (mIsFirstLoad) {
268             logAppPermissionsFragmentView();
269             mIsFirstLoad = false;
270         }
271         setLoading(false);
272     }
273 
createPermissionPreference(Context context, AppPermissionGroupsViewModel.GroupUiInfo groupInfo, Map<String, Long> groupUsageLastAccessTime)274     private Preference createPermissionPreference(Context context,
275             AppPermissionGroupsViewModel.GroupUiInfo groupInfo,
276             Map<String, Long> groupUsageLastAccessTime) {
277         String groupName = groupInfo.getGroupName();
278         Preference preference = new Preference(context);
279         preference.setTitle(KotlinUtils.INSTANCE.getPermGroupLabel(context, groupName));
280         preference.setIcon(KotlinUtils.INSTANCE.getPermGroupIcon(context, groupName));
281         preference.setKey(groupName);
282         String summary = mViewModel.getPreferenceSummary(groupInfo, context,
283                 groupUsageLastAccessTime.get(groupName));
284         if (!summary.isEmpty()) {
285             preference.setSummary(summary);
286         }
287         preference.setOnPreferenceClickListener(pref -> {
288             Intent intent = new Intent(Intent.ACTION_MANAGE_APP_PERMISSION);
289             intent.putExtra(Intent.EXTRA_PACKAGE_NAME, mPackageName);
290             intent.putExtra(Intent.EXTRA_PERMISSION_NAME, groupName);
291             intent.putExtra(Intent.EXTRA_USER, mUser);
292             intent.putExtra(EXTRA_CALLER_NAME, AutoAppPermissionsFragment.class.getName());
293             context.startActivity(intent);
294             return true;
295         });
296         return preference;
297     }
298 
setAdditionalPermissionsPreference(PreferenceCategory category, int numExtraPerms, Context context)299     private void setAdditionalPermissionsPreference(PreferenceCategory category, int numExtraPerms,
300             Context context) {
301         Preference extraPerms = new Preference(context);
302         extraPerms.setIcon(R.drawable.ic_toc);
303         extraPerms.setTitle(R.string.additional_permissions);
304         extraPerms.setOnPreferenceClickListener(preference -> {
305             AutoAppPermissionsFragment
306                     frag = AutoAppPermissionsFragment.newInstance(mPackageName, mUser,
307                     getArguments().getLong(EXTRA_SESSION_ID), false);
308             frag.setTargetFragment(AutoAppPermissionsFragment.this, 0);
309             getFragmentManager().beginTransaction()
310                     .replace(android.R.id.content, frag)
311                     .addToBackStack(null)
312                     .commit();
313             return true;
314         });
315         extraPerms.setSummary(getResources().getQuantityString(
316                 R.plurals.additional_permissions_more, numExtraPerms,
317                 numExtraPerms));
318         category.addPreference(extraPerms);
319     }
320 
setNoPermissionPreference(PreferenceCategory category, Category grantCategory, Context context)321     private void setNoPermissionPreference(PreferenceCategory category, Category grantCategory,
322             Context context) {
323         Preference empty = new Preference(context);
324         empty.setKey(getString(grantCategory.equals(Category.DENIED)
325                 ? R.string.no_permissions_denied : R.string.no_permissions_allowed));
326         empty.setTitle(empty.getKey());
327         empty.setSelectable(false);
328         category.addPreference(empty);
329     }
330 
comparePreferences(Preference lhs, Preference rhs)331     private int comparePreferences(Preference lhs, Preference rhs) {
332         String additionalTitle = lhs.getContext().getString(R.string.additional_permissions);
333         if (lhs.getTitle().equals(additionalTitle)) {
334             return 1;
335         } else if (rhs.getTitle().equals(additionalTitle)) {
336             return -1;
337         }
338         return mCollator.compare(lhs.getTitle().toString(),
339                 rhs.getTitle().toString());
340     }
341 
logAppPermissionsFragmentView()342     private void logAppPermissionsFragmentView() {
343         Context context = getPreferenceManager().getContext();
344         if (context == null) {
345             return;
346         }
347         String permissionSubtitleOnlyInForeground =
348                 context.getString(R.string.permission_subtitle_only_in_foreground);
349 
350 
351         long sessionId = getArguments().getLong(EXTRA_SESSION_ID, INVALID_SESSION_ID);
352         long viewId = new Random().nextLong();
353 
354         PreferenceCategory allowed = findPreference(KEY_ALLOWED_PERMISSIONS_GROUP);
355 
356         int numAllowed = allowed.getPreferenceCount();
357         for (int i = 0; i < numAllowed; i++) {
358             Preference preference = allowed.getPreference(i);
359             if (preference.getTitle().equals(getString(R.string.no_permissions_allowed))) {
360                 // R.string.no_permission_allowed was added to PreferenceCategory
361                 continue;
362             }
363 
364             int category = APP_PERMISSIONS_FRAGMENT_VIEWED__CATEGORY__ALLOWED;
365             if (preference.getSummary() != null
366                     && permissionSubtitleOnlyInForeground.contentEquals(preference.getSummary())) {
367                 category = APP_PERMISSIONS_FRAGMENT_VIEWED__CATEGORY__ALLOWED_FOREGROUND;
368             }
369 
370             logAppPermissionsFragmentViewEntry(sessionId, viewId, preference.getKey(), category);
371         }
372 
373         PreferenceCategory denied = findPreference(KEY_DENIED_PERMISSIONS_GROUP);
374 
375         int numDenied = denied.getPreferenceCount();
376         for (int i = 0; i < numDenied; i++) {
377             Preference preference = denied.getPreference(i);
378             if (preference.getTitle().equals(getString(R.string.no_permissions_denied))) {
379                 // R.string.no_permission_denied was added to PreferenceCategory
380                 continue;
381             }
382             logAppPermissionsFragmentViewEntry(sessionId, viewId, preference.getKey(),
383                     APP_PERMISSIONS_FRAGMENT_VIEWED__CATEGORY__DENIED);
384         }
385     }
386 
logAppPermissionsFragmentViewEntry( long sessionId, long viewId, String permissionGroupName, int category)387     private void logAppPermissionsFragmentViewEntry(
388             long sessionId, long viewId, String permissionGroupName, int category) {
389         Integer uid = KotlinUtils.INSTANCE.getPackageUid(getActivity().getApplication(),
390                 mPackageName, mUser);
391         if (uid == null) {
392             return;
393         }
394         PermissionControllerStatsLog.write(APP_PERMISSIONS_FRAGMENT_VIEWED, sessionId, viewId,
395                 permissionGroupName, uid, mPackageName, category);
396         Log.v(LOG_TAG, "AutoAppPermissionFragment view logged with sessionId=" + sessionId
397                 + " viewId=" + viewId + " permissionGroupName=" + permissionGroupName + " uid="
398                 + uid + " packageName="
399                 + mPackageName + " category=" + category);
400     }
401 }
402 
403