1 /*
2  * Copyright (C) 2021 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.v31;
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.PERMISSION_USAGE_FRAGMENT_INTERACTION;
22 import static com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_USAGE_FRAGMENT_INTERACTION__ACTION__SEE_OTHER_PERMISSIONS_CLICKED;
23 import static com.android.permissioncontroller.PermissionControllerStatsLog.PERMISSION_USAGE_FRAGMENT_INTERACTION__ACTION__SHOW_SYSTEM_CLICKED;
24 import static com.android.permissioncontroller.PermissionControllerStatsLog.write;
25 
26 import android.Manifest;
27 import android.app.ActionBar;
28 import android.content.Context;
29 import android.os.Build;
30 import android.os.Bundle;
31 import android.util.Log;
32 import android.view.Menu;
33 import android.view.MenuInflater;
34 import android.view.MenuItem;
35 
36 import androidx.annotation.RequiresApi;
37 import androidx.lifecycle.ViewModelProvider;
38 import androidx.preference.Preference;
39 import androidx.preference.PreferenceCategory;
40 import androidx.preference.PreferenceGroupAdapter;
41 import androidx.preference.PreferenceScreen;
42 import androidx.recyclerview.widget.RecyclerView;
43 
44 import com.android.permissioncontroller.R;
45 import com.android.permissioncontroller.permission.ui.handheld.SettingsWithLargeHeader;
46 import com.android.permissioncontroller.permission.ui.viewmodel.v31.PermissionUsageViewModel;
47 import com.android.permissioncontroller.permission.ui.viewmodel.v31.PermissionUsageViewModelFactory;
48 import com.android.permissioncontroller.permission.ui.viewmodel.v31.PermissionUsagesUiState;
49 import com.android.permissioncontroller.permission.utils.KotlinUtils;
50 import com.android.settingslib.HelpUtils;
51 
52 import java.util.ArrayList;
53 import java.util.Comparator;
54 import java.util.List;
55 import java.util.Map;
56 
57 /** The main page for the privacy dashboard. */
58 @RequiresApi(Build.VERSION_CODES.S)
59 public class PermissionUsageFragment extends SettingsWithLargeHeader {
60     private static final String LOG_TAG = PermissionUsageFragment.class.getSimpleName();
61     private static final Map<String, Integer> PERMISSION_GROUP_ORDER =
62             Map.of(
63                     Manifest.permission_group.LOCATION, 0,
64                     Manifest.permission_group.CAMERA, 1,
65                     Manifest.permission_group.MICROPHONE, 2);
66     private static final int DEFAULT_ORDER = 3;
67 
68     public static final boolean DEBUG = true;
69 
70     // Pie chart in this screen will be the first child.
71     // Hence we use PERMISSION_GROUP_ORDER + 1 here.
72     private static final int PERMISSION_USAGE_INITIAL_EXPANDED_CHILDREN_COUNT =
73             PERMISSION_GROUP_ORDER.size() + 1;
74     private static final int EXPAND_BUTTON_ORDER = 999;
75     /** Map to represent ordering for permission groups in the permissions usage UI. */
76     private static final String KEY_SESSION_ID = "_session_id";
77 
78     private static final String SESSION_ID_KEY =
79             PermissionUsageFragment.class.getName() + KEY_SESSION_ID;
80 
81     private static final int MENU_SHOW_7_DAYS_DATA = Menu.FIRST + 4;
82     private static final int MENU_SHOW_24_HOURS_DATA = Menu.FIRST + 5;
83 
84     private PermissionUsageViewModel mViewModel;
85 
86     private boolean mHasSystemApps;
87     private MenuItem mShowSystemMenu;
88     private MenuItem mHideSystemMenu;
89     private MenuItem mShow7DaysDataMenu;
90     private MenuItem mShow24HoursDataMenu;
91     private boolean mOtherExpanded;
92 
93     private PermissionUsageGraphicPreference mGraphic;
94 
95     /** Unique Id of a request */
96     private long mSessionId;
97 
98     @Override
onCreate(Bundle savedInstanceState)99     public void onCreate(Bundle savedInstanceState) {
100         super.onCreate(savedInstanceState);
101         if (savedInstanceState != null) {
102             mSessionId = savedInstanceState.getLong(SESSION_ID_KEY);
103         } else {
104             mSessionId = getArguments().getLong(EXTRA_SESSION_ID, INVALID_SESSION_ID);
105         }
106 
107         PermissionUsageViewModelFactory factory = new PermissionUsageViewModelFactory(
108                         getActivity().getApplication(), this, new Bundle());
109         mViewModel = new ViewModelProvider(this, factory)
110                 .get(PermissionUsageViewModel.class);
111 
112         // Start out with 'other' permissions not expanded.
113         mOtherExpanded = false;
114 
115         setHasOptionsMenu(true);
116         ActionBar ab = getActivity().getActionBar();
117         if (ab != null) {
118             ab.setDisplayHomeAsUpEnabled(true);
119         }
120         setLoading(true, false);
121 
122         mViewModel.getPermissionUsagesUiLiveData().observe(this, this::updateAllUI);
123     }
124 
125     @Override
onCreateAdapter(PreferenceScreen preferenceScreen)126     public RecyclerView.Adapter onCreateAdapter(PreferenceScreen preferenceScreen) {
127         PreferenceGroupAdapter adapter =
128                 (PreferenceGroupAdapter) super.onCreateAdapter(preferenceScreen);
129 
130         adapter.registerAdapterDataObserver(
131                 new RecyclerView.AdapterDataObserver() {
132                     @Override
133                     public void onChanged() {
134                         updatePreferenceScreenAdvancedTitleAndSummary(preferenceScreen, adapter);
135                     }
136 
137                     @Override
138                     public void onItemRangeInserted(int positionStart, int itemCount) {
139                         onChanged();
140                     }
141 
142                     @Override
143                     public void onItemRangeRemoved(int positionStart, int itemCount) {
144                         onChanged();
145                     }
146 
147                     @Override
148                     public void onItemRangeChanged(int positionStart, int itemCount) {
149                         onChanged();
150                     }
151 
152                     @Override
153                     public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
154                         onChanged();
155                     }
156                 });
157 
158         updatePreferenceScreenAdvancedTitleAndSummary(preferenceScreen, adapter);
159         return adapter;
160     }
161 
updatePreferenceScreenAdvancedTitleAndSummary( PreferenceScreen preferenceScreen, PreferenceGroupAdapter adapter)162     private void updatePreferenceScreenAdvancedTitleAndSummary(
163             PreferenceScreen preferenceScreen, PreferenceGroupAdapter adapter) {
164         int count = adapter.getItemCount();
165         if (count == 0) {
166             return;
167         }
168 
169         Preference preference = adapter.getItem(count - 1);
170 
171         // This is a hacky way of getting the expand button preference for advanced info
172         if (preference.getOrder() == EXPAND_BUTTON_ORDER) {
173             mOtherExpanded = false;
174             preference.setTitle(R.string.perm_usage_adv_info_title);
175             preference.setSummary(preferenceScreen.getSummary());
176             preference.setLayoutResource(R.layout.expand_button_with_large_title);
177             if (mGraphic != null) {
178                 mGraphic.setShowOtherCategory(false);
179             }
180         } else {
181             mOtherExpanded = true;
182             if (mGraphic != null) {
183                 mGraphic.setShowOtherCategory(true);
184             }
185         }
186     }
187 
188     @Override
onStart()189     public void onStart() {
190         super.onStart();
191         getActivity().setTitle(R.string.permission_usage_title);
192     }
193 
194     @Override
onCreateOptionsMenu(Menu menu, MenuInflater inflater)195     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
196         super.onCreateOptionsMenu(menu, inflater);
197         mShowSystemMenu =
198                 menu.add(Menu.NONE, MENU_SHOW_SYSTEM, Menu.NONE, R.string.menu_show_system);
199         mHideSystemMenu =
200                 menu.add(Menu.NONE, MENU_HIDE_SYSTEM, Menu.NONE, R.string.menu_hide_system);
201         updateShowSystemToggle(mViewModel.getShowSystemApps());
202 
203         if (KotlinUtils.INSTANCE.is7DayToggleEnabled()) {
204             mShow7DaysDataMenu =
205                     menu.add(
206                             Menu.NONE,
207                             MENU_SHOW_7_DAYS_DATA,
208                             Menu.NONE,
209                             R.string.menu_show_7_days_data);
210             mShow24HoursDataMenu =
211                     menu.add(
212                             Menu.NONE,
213                             MENU_SHOW_24_HOURS_DATA,
214                             Menu.NONE,
215                             R.string.menu_show_24_hours_data);
216             updateShow7DaysToggle(mViewModel.getShow7DaysData());
217         }
218 
219         HelpUtils.prepareHelpMenuItem(
220                 getActivity(), menu, R.string.help_permission_usage, getClass().getName());
221     }
222 
223     @Override
onOptionsItemSelected(MenuItem item)224     public boolean onOptionsItemSelected(MenuItem item) {
225         int itemId = item.getItemId();
226         switch (itemId) {
227             case android.R.id.home:
228                 getActivity().finishAfterTransition();
229                 return true;
230             case MENU_SHOW_SYSTEM:
231                 write(
232                         PERMISSION_USAGE_FRAGMENT_INTERACTION,
233                         mSessionId,
234                         PERMISSION_USAGE_FRAGMENT_INTERACTION__ACTION__SHOW_SYSTEM_CLICKED);
235                 updateAllUI(mViewModel.updateShowSystem(true));
236                 break;
237             case MENU_HIDE_SYSTEM:
238                 updateAllUI(mViewModel.updateShowSystem(false));
239                 break;
240             case MENU_SHOW_7_DAYS_DATA:
241                 updateAllUI(mViewModel.updateShow7Days(KotlinUtils.INSTANCE.is7DayToggleEnabled()));
242                 break;
243             case MENU_SHOW_24_HOURS_DATA:
244                 updateAllUI(mViewModel.updateShow7Days(false));
245                 break;
246         }
247         return super.onOptionsItemSelected(item);
248     }
249 
250     @Override
getEmptyViewString()251     public int getEmptyViewString() {
252         return R.string.no_permission_usages;
253     }
254 
255     @Override
onSaveInstanceState(Bundle outState)256     public void onSaveInstanceState(Bundle outState) {
257         super.onSaveInstanceState(outState);
258         if (outState != null) {
259             outState.putLong(SESSION_ID_KEY, mSessionId);
260         }
261     }
262 
updateShowSystemToggle(boolean showSystem)263     private void updateShowSystemToggle(boolean showSystem) {
264         if (mHasSystemApps) {
265             if (mShowSystemMenu != null) {
266                 mShowSystemMenu.setVisible(!showSystem);
267             }
268             if (mHideSystemMenu != null) {
269                 mHideSystemMenu.setVisible(showSystem);
270             }
271         } else {
272             if (mShowSystemMenu != null) {
273                 mShowSystemMenu.setVisible(false);
274             }
275             if (mHideSystemMenu != null) {
276                 mHideSystemMenu.setVisible(false);
277             }
278         }
279     }
280 
updateShow7DaysToggle(boolean show7Days)281     private void updateShow7DaysToggle(boolean show7Days) {
282         if (mShow7DaysDataMenu != null) {
283             mShow7DaysDataMenu.setVisible(!show7Days);
284         }
285 
286         if (mShow24HoursDataMenu != null) {
287             mShow24HoursDataMenu.setVisible(show7Days);
288         }
289     }
290 
291     /** Updates page content and menu items. */
updateAllUI(PermissionUsagesUiState uiData)292     private void updateAllUI(PermissionUsagesUiState uiData) {
293         Log.v(LOG_TAG, "Privacy dashboard data = " + uiData);
294         if (getActivity() == null || uiData instanceof PermissionUsagesUiState.Loading) {
295             return;
296         }
297 
298         PermissionUsagesUiState.Success permissionUsagesUiData =
299                 (PermissionUsagesUiState.Success) uiData;
300         Context context = getActivity();
301 
302         PreferenceScreen screen = getPreferenceScreen();
303         if (screen == null) {
304             screen = getPreferenceManager().createPreferenceScreen(context);
305             setPreferenceScreen(screen);
306         }
307         screen.removeAll();
308 
309         if (mOtherExpanded) {
310             screen.setInitialExpandedChildrenCount(Integer.MAX_VALUE);
311         } else {
312             screen.setInitialExpandedChildrenCount(
313                     PERMISSION_USAGE_INITIAL_EXPANDED_CHILDREN_COUNT);
314         }
315         screen.setOnExpandButtonClickListener(() -> {
316             write(
317                     PERMISSION_USAGE_FRAGMENT_INTERACTION,
318                     mSessionId,
319                     PERMISSION_USAGE_FRAGMENT_INTERACTION__ACTION__SEE_OTHER_PERMISSIONS_CLICKED);
320         });
321         boolean containsSystemAppUsages = permissionUsagesUiData.getShouldShowSystemToggle();
322         Map<String, Integer> permissionGroupWithUsageCounts =
323                 permissionUsagesUiData.getPermissionGroupUsageCount();
324         List<Map.Entry<String, Integer>> permissionGroupWithUsageCountsEntries =
325                 new ArrayList(permissionGroupWithUsageCounts.entrySet());
326 
327         permissionGroupWithUsageCountsEntries.sort(Comparator.comparing(
328                 (Map.Entry<String, Integer> permissionGroupWithUsageCount) ->
329                         PERMISSION_GROUP_ORDER.getOrDefault(
330                                 permissionGroupWithUsageCount.getKey(), DEFAULT_ORDER))
331                 .thenComparing((Map.Entry<String, Integer> permissionGroupWithUsageCount) ->
332                         mViewModel.getPermissionGroupLabel(
333                                 context, permissionGroupWithUsageCount.getKey())));
334 
335         if (mHasSystemApps != containsSystemAppUsages) {
336             mHasSystemApps = containsSystemAppUsages;
337         }
338 
339         boolean show7Days = mViewModel.getShow7DaysData();
340         updateShow7DaysToggle(show7Days);
341         updateShowSystemToggle(mViewModel.getShowSystemApps());
342 
343         mGraphic = new PermissionUsageGraphicPreference(context, show7Days);
344         screen.addPreference(mGraphic);
345         mGraphic.setUsages(permissionGroupWithUsageCounts);
346 
347         // Add the preference header.
348         PreferenceCategory category = new PreferenceCategory(context);
349         screen.addPreference(category);
350         CharSequence advancedInfoSummary =
351                 getAdvancedInfoSummaryString(context, permissionGroupWithUsageCountsEntries);
352         screen.setSummary(advancedInfoSummary);
353 
354         addUIContent(context, permissionGroupWithUsageCountsEntries, category);
355     }
356 
getAdvancedInfoSummaryString( Context context, List<Map.Entry<String, Integer>> permissionGroupWithUsageCounts)357     private CharSequence getAdvancedInfoSummaryString(
358             Context context, List<Map.Entry<String, Integer>> permissionGroupWithUsageCounts) {
359         int size = permissionGroupWithUsageCounts.size();
360         if (size <= PERMISSION_USAGE_INITIAL_EXPANDED_CHILDREN_COUNT - 1) {
361             return "";
362         }
363 
364         // case for 1 extra item in the advanced info
365         if (size == PERMISSION_USAGE_INITIAL_EXPANDED_CHILDREN_COUNT) {
366             String permGroupName =
367                     permissionGroupWithUsageCounts
368                             .get(PERMISSION_USAGE_INITIAL_EXPANDED_CHILDREN_COUNT - 1)
369                             .getKey();
370             return mViewModel.getPermissionGroupLabel(context, permGroupName);
371         }
372 
373         String permGroupName1 =
374                 permissionGroupWithUsageCounts
375                         .get(PERMISSION_USAGE_INITIAL_EXPANDED_CHILDREN_COUNT - 1)
376                         .getKey();
377         String permGroupName2 =
378                 permissionGroupWithUsageCounts
379                         .get(PERMISSION_USAGE_INITIAL_EXPANDED_CHILDREN_COUNT)
380                         .getKey();
381         CharSequence permGroupLabel1 = mViewModel.getPermissionGroupLabel(context, permGroupName1);
382         CharSequence permGroupLabel2 = mViewModel.getPermissionGroupLabel(context, permGroupName2);
383 
384         // case for 2 extra items in the advanced info
385         if (size == PERMISSION_USAGE_INITIAL_EXPANDED_CHILDREN_COUNT + 1) {
386             return context.getResources()
387                     .getString(
388                             R.string.perm_usage_adv_info_summary_2_items,
389                             permGroupLabel1,
390                             permGroupLabel2);
391         }
392 
393         // case for 3 or more extra items in the advanced info
394         int numExtraItems = size - PERMISSION_USAGE_INITIAL_EXPANDED_CHILDREN_COUNT - 1;
395         return context.getResources()
396                 .getString(
397                         R.string.perm_usage_adv_info_summary_more_items,
398                         permGroupLabel1,
399                         permGroupLabel2,
400                         numExtraItems);
401     }
402 
403     /** Add preferences for permission usages. */
addUIContent( Context context, List<Map.Entry<String, Integer>> permissionGroupWithUsageCounts, PreferenceCategory category)404     private void addUIContent(
405             Context context,
406             List<Map.Entry<String, Integer>> permissionGroupWithUsageCounts,
407             PreferenceCategory category) {
408         boolean showSystem = mViewModel.getShowSystemApps();
409         boolean show7Days = mViewModel.getShow7DaysData();
410 
411         for (int i = 0; i < permissionGroupWithUsageCounts.size(); i++) {
412             Map.Entry<String, Integer> permissionGroupWithUsageCount =
413                     permissionGroupWithUsageCounts.get(i);
414             PermissionUsageControlPreference permissionUsagePreference =
415                     new PermissionUsageControlPreference(
416                             context,
417                             permissionGroupWithUsageCount.getKey(),
418                             permissionGroupWithUsageCount.getValue(),
419                             showSystem,
420                             mSessionId,
421                             show7Days);
422             category.addPreference(permissionUsagePreference);
423         }
424 
425         setLoading(false, true);
426     }
427 }
428