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