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.permission.ui.Category.ALLOWED;
22 import static com.android.permissioncontroller.permission.ui.Category.ALLOWED_FOREGROUND;
23 import static com.android.permissioncontroller.permission.ui.Category.ASK;
24 import static com.android.permissioncontroller.permission.ui.Category.DENIED;
25 import static com.android.permissioncontroller.permission.ui.Category.STORAGE_FOOTER;
26 import static com.android.permissioncontroller.permission.ui.ManagePermissionsActivity.EXTRA_CALLER_NAME;
27 
28 import android.content.Context;
29 import android.content.Intent;
30 import android.graphics.drawable.Drawable;
31 import android.os.Build;
32 import android.os.Bundle;
33 import android.os.UserHandle;
34 import android.util.ArrayMap;
35 
36 import androidx.annotation.Nullable;
37 import androidx.annotation.RequiresApi;
38 import androidx.fragment.app.Fragment;
39 import androidx.lifecycle.ViewModelProvider;
40 import androidx.preference.Preference;
41 import androidx.preference.PreferenceCategory;
42 
43 import com.android.modules.utils.build.SdkLevel;
44 import com.android.permissioncontroller.R;
45 import com.android.permissioncontroller.auto.AutoSettingsFrameFragment;
46 import com.android.permissioncontroller.permission.model.v31.AppPermissionUsage;
47 import com.android.permissioncontroller.permission.model.v31.PermissionUsages;
48 import com.android.permissioncontroller.permission.ui.Category;
49 import com.android.permissioncontroller.permission.ui.handheld.SmartIconLoadPackagePermissionPreference;
50 import com.android.permissioncontroller.permission.ui.model.PermissionAppsViewModel;
51 import com.android.permissioncontroller.permission.ui.model.PermissionAppsViewModelFactory;
52 import com.android.permissioncontroller.permission.utils.KotlinUtils;
53 import com.android.permissioncontroller.permission.utils.Utils;
54 import com.android.settingslib.utils.applications.AppUtils;
55 
56 import kotlin.Pair;
57 import kotlin.Triple;
58 
59 import java.text.Collator;
60 import java.util.ArrayList;
61 import java.util.List;
62 import java.util.Map;
63 import java.util.Random;
64 
65 /** Shows the list of applications which have (or do not have) the given permission. */
66 public class AutoPermissionAppsFragment extends AutoSettingsFrameFragment implements
67         PermissionUsages.PermissionsUsagesChangeCallback {
68 
69     private static final String LOG_TAG = "AutoPermissionAppsFragment";
70     private static final String KEY_EMPTY = "_empty";
71 
72     /** Creates a new instance of {@link AutoPermissionAppsFragment} for the given permission. */
newInstance(String permissionGroupName, long sessionId)73     public static AutoPermissionAppsFragment newInstance(String permissionGroupName,
74             long sessionId) {
75         return setPermissionGroupName(new AutoPermissionAppsFragment(), permissionGroupName,
76                 sessionId);
77     }
78 
setPermissionGroupName( T fragment, String permissionGroupName, long sessionId)79     private static <T extends Fragment> T setPermissionGroupName(
80             T fragment, String permissionGroupName, long sessionId) {
81         Bundle arguments = new Bundle();
82         arguments.putString(Intent.EXTRA_PERMISSION_GROUP_NAME, permissionGroupName);
83         arguments.putLong(EXTRA_SESSION_ID, sessionId);
84         fragment.setArguments(arguments);
85         return fragment;
86     }
87 
88     private PermissionAppsViewModel mViewModel;
89     private PermissionUsages mPermissionUsages;
90     private List<AppPermissionUsage> mAppPermissionUsages = new ArrayList<>();
91     private String mPermGroupName;
92 
93     private Collator mCollator;
94 
95     @Override
onCreate(@ullable Bundle savedInstanceState)96     public void onCreate(@Nullable Bundle savedInstanceState) {
97         super.onCreate(savedInstanceState);
98         setLoading(true);
99 
100         mPermGroupName = getArguments().getString(Intent.EXTRA_PERMISSION_GROUP_NAME);
101         if (mPermGroupName == null) {
102             mPermGroupName = getArguments().getString(Intent.EXTRA_PERMISSION_NAME);
103         }
104 
105         Drawable icon = KotlinUtils.INSTANCE.getPermGroupIcon(getContext(), mPermGroupName);
106         CharSequence label = KotlinUtils.INSTANCE.getPermGroupLabel(getContext(), mPermGroupName);
107         CharSequence description = KotlinUtils.INSTANCE.getPermGroupDescription(getContext(),
108                 mPermGroupName);
109 
110         setHeaderLabel(label);
111         Preference header = new Preference(getContext());
112         header.setTitle(label);
113         header.setIcon(icon);
114         header.setSummary(Utils.getPermissionGroupDescriptionString(getContext(), mPermGroupName,
115                 description));
116         getPreferenceScreen().addPreference(header);
117 
118         mCollator = Collator.getInstance(
119                 getContext().getResources().getConfiguration().getLocales().get(0));
120 
121         PermissionAppsViewModelFactory factory =
122                 new PermissionAppsViewModelFactory(getActivity().getApplication(), mPermGroupName,
123                         this, new Bundle());
124         mViewModel = new ViewModelProvider(this, factory).get(PermissionAppsViewModel.class);
125 
126         mViewModel.getCategorizedAppsLiveData().observe(this, this::onPackagesLoaded);
127         mViewModel.getShouldShowSystemLiveData().observe(this, this::updateMenu);
128         mViewModel.getHasSystemAppsLiveData().observe(this, this::hideSystemAppToggleIfNecessary);
129 
130         // If the build type is below S, the app ops for permission usage can't be found. Thus, we
131         // shouldn't load permission usages, for them.
132         if (SdkLevel.isAtLeastS()) {
133             Context context = getPreferenceManager().getContext();
134             mPermissionUsages = new PermissionUsages(context);
135 
136             long filterTimeBeginMillis = mViewModel.getFilterTimeBeginMillis();
137             mPermissionUsages.load(null, null, filterTimeBeginMillis, Long.MAX_VALUE,
138                     PermissionUsages.USAGE_FLAG_LAST, getActivity().getLoaderManager(),
139                     false, false, this, false);
140         }
141     }
142 
updateMenu(Boolean showSystem)143     private void updateMenu(Boolean showSystem) {
144         if (showSystem == null) {
145             showSystem = false;
146         }
147         // Show the opposite label from the current state.
148         String label;
149         if (showSystem) {
150             label = getString(R.string.menu_hide_system);
151         } else {
152             label = getString(R.string.menu_show_system);
153         }
154 
155         boolean showSystemFinal = showSystem;
156         setAction(label, v -> mViewModel.updateShowSystem(!showSystemFinal));
157     }
158 
159     /**
160      * Main differences between this phone implementation and this one are:
161      * <ul>
162      *     <li>No special handling for scoped storage</li>
163      * </ul>
164      */
onPackagesLoaded(Map<Category, List<Pair<String, UserHandle>>> categories)165     private void onPackagesLoaded(Map<Category, List<Pair<String, UserHandle>>> categories) {
166         Preference additionalPermissionsPreference = getPreferenceScreen().findPreference(
167                 ALLOWED_FOREGROUND.getCategoryName());
168         if (additionalPermissionsPreference == null) {
169             // This preference resources includes the "Ask" permission group. That's okay for Auto
170             // even though Auto doesn't support the one-time permission because the code later in
171             // this method will hide unused permission groups.
172             addPreferencesFromResource(R.xml.allowed_denied);
173         }
174         // Hide allowed foreground label by default, to avoid briefly showing it before updating
175         findPreference(ALLOWED_FOREGROUND.getCategoryName()).setVisible(false);
176 
177         // Hide storage footer category
178         findPreference(STORAGE_FOOTER.getCategoryName()).setVisible(false);
179 
180         Context context = getPreferenceManager().getContext();
181 
182         if (context == null || getActivity() == null || categories == null) {
183             return;
184         }
185 
186         Map<String, Preference> existingPrefs = new ArrayMap<>();
187 
188         // Start at 1 since the header preference will always be in the 0th index
189         for (int i = 1; i < getPreferenceScreen().getPreferenceCount(); i++) {
190             PreferenceCategory category = (PreferenceCategory)
191                     getPreferenceScreen().getPreference(i);
192             category.setOrderingAsAdded(true);
193             int numPreferences = category.getPreferenceCount();
194             for (int j = 0; j < numPreferences; j++) {
195                 Preference preference = category.getPreference(j);
196                 existingPrefs.put(preference.getKey(), preference);
197             }
198             category.removeAll();
199         }
200 
201         long viewIdForLogging = new Random().nextLong();
202         long sessionId = getArguments().getLong(EXTRA_SESSION_ID, INVALID_SESSION_ID);
203 
204         Boolean showAlways = mViewModel.getShowAllowAlwaysStringLiveData().getValue();
205         if (showAlways != null && showAlways) {
206             findPreference(ALLOWED.getCategoryName()).setTitle(R.string.allowed_always_header);
207         } else {
208             findPreference(ALLOWED.getCategoryName()).setTitle(R.string.allowed_header);
209         }
210 
211         // A mapping of user + packageName to their last access timestamps for the permission group.
212         Map<String, Long> groupUsageLastAccessTime =
213                 mViewModel.extractGroupUsageLastAccessTime(mAppPermissionUsages);
214 
215         for (Category grantCategory : categories.keySet()) {
216             List<Pair<String, UserHandle>> packages = categories.get(grantCategory);
217             PreferenceCategory category = findPreference(grantCategory.getCategoryName());
218 
219             // If this category is empty set up the empty preference.
220             if (packages.size() == 0) {
221                 Preference empty = new Preference(context);
222                 empty.setSelectable(false);
223                 empty.setKey(category.getKey() + KEY_EMPTY);
224                 if (grantCategory.equals(ALLOWED)) {
225                     empty.setTitle(getString(R.string.no_apps_allowed));
226                 } else if (grantCategory.equals(ALLOWED_FOREGROUND)) {
227                     category.setVisible(false);
228                 } else if (grantCategory.equals(ASK)) {
229                     category.setVisible(false);
230                 } else {
231                     empty.setTitle(getString(R.string.no_apps_denied));
232                 }
233                 category.addPreference(empty);
234                 continue;
235             } else if (grantCategory.equals(ALLOWED_FOREGROUND)) {
236                 category.setVisible(true);
237             } else if (grantCategory.equals(ASK)) {
238                 category.setVisible(true);
239             }
240 
241             for (Pair<String, UserHandle> packageUserLabel : packages) {
242                 String packageName = packageUserLabel.getFirst();
243                 UserHandle user = packageUserLabel.getSecond();
244 
245                 String key = user + packageName;
246 
247                 Long lastAccessTime = groupUsageLastAccessTime.get(key);
248                 Triple<String, Integer, String> summaryTimestamp = Utils
249                         .getPermissionLastAccessSummaryTimestamp(
250                                 lastAccessTime, context, mPermGroupName);
251 
252                 Preference existingPref = existingPrefs.get(key);
253                 if (existingPref != null) {
254                     updatePreferenceSummary(existingPref, summaryTimestamp);
255                     category.addPreference(existingPref);
256                     continue;
257                 }
258 
259                 SmartIconLoadPackagePermissionPreference pref =
260                         new SmartIconLoadPackagePermissionPreference(getActivity().getApplication(),
261                                 packageName, user, context);
262                 pref.setKey(key);
263                 pref.setTitle(KotlinUtils.INSTANCE.getPackageLabel(getActivity().getApplication(),
264                         packageName, user));
265                 pref.setOnPreferenceClickListener((Preference p) -> {
266                     Intent intent = new Intent(Intent.ACTION_MANAGE_APP_PERMISSION);
267                     intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName);
268                     intent.putExtra(Intent.EXTRA_PERMISSION_GROUP_NAME, mPermGroupName);
269                     intent.putExtra(Intent.EXTRA_USER, user);
270                     intent.putExtra(EXTRA_CALLER_NAME, getClass().getName());
271                     intent.putExtra(EXTRA_SESSION_ID, sessionId);
272                     startActivity(intent);
273                     return true;
274                 });
275                 pref.setTitleContentDescription(AppUtils.getAppContentDescription(context,
276                         packageName, user.getIdentifier()));
277 
278                 updatePreferenceSummary(pref, summaryTimestamp);
279 
280                 category.addPreference(pref);
281                 if (!mViewModel.getCreationLogged()) {
282                     logPermissionAppsFragmentCreated(packageName, user, viewIdForLogging,
283                             grantCategory.equals(ALLOWED), grantCategory.equals(ALLOWED_FOREGROUND),
284                             grantCategory.equals(DENIED));
285                 }
286             }
287             KotlinUtils.INSTANCE.sortPreferenceGroup(category, this::comparePreference, false);
288         }
289 
290         mViewModel.setCreationLogged(true);
291 
292         setLoading(false);
293     }
294 
295     @Override
onCreatePreferences(Bundle bundle, String s)296     public void onCreatePreferences(Bundle bundle, String s) {
297         setPreferenceScreen(getPreferenceManager().createPreferenceScreen(getContext()));
298     }
299 
hideSystemAppToggleIfNecessary(Boolean hasSystemApps)300     private void hideSystemAppToggleIfNecessary(Boolean hasSystemApps) {
301         if (hasSystemApps == null || !hasSystemApps) {
302             setAction(/* label= */ null, /* onClickListener= */ null);
303         }
304     }
305 
updatePreferenceSummary(Preference preference, Triple<String, Integer, String> summaryTimestamp)306     private void updatePreferenceSummary(Preference preference,
307             Triple<String, Integer, String> summaryTimestamp) {
308         String summary = mViewModel.getPreferenceSummary(getResources(), summaryTimestamp);
309         if (!summary.isEmpty()) {
310             preference.setSummary(summary);
311         }
312     }
313 
314     @Override
315     @RequiresApi(Build.VERSION_CODES.S)
onPermissionUsagesChanged()316     public void onPermissionUsagesChanged() {
317         if (mPermissionUsages.getUsages().isEmpty()) {
318             return;
319         }
320         if (getContext() == null) {
321             // Async result has come in after our context is gone.
322             return;
323         }
324 
325         mAppPermissionUsages = new ArrayList<>(mPermissionUsages.getUsages());
326         onPackagesLoaded(mViewModel.getCategorizedAppsLiveData().getValue());
327     }
328 
comparePreference(Preference lhs, Preference rhs)329     private int comparePreference(Preference lhs, Preference rhs) {
330         return mViewModel.comparePreference(mCollator, lhs, rhs);
331     }
332 
logPermissionAppsFragmentCreated(String packageName, UserHandle user, long viewId, boolean isAllowed, boolean isAllowedForeground, boolean isDenied)333     private void logPermissionAppsFragmentCreated(String packageName, UserHandle user, long viewId,
334             boolean isAllowed, boolean isAllowedForeground, boolean isDenied) {
335         long sessionId = getArguments().getLong(EXTRA_SESSION_ID, 0);
336         mViewModel.logPermissionAppsFragmentCreated(packageName, user, viewId, isAllowed,
337                 isAllowedForeground, isDenied, sessionId, getActivity().getApplication(),
338                 mPermGroupName, LOG_TAG);
339     }
340 }
341