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 
22 import android.app.ActionBar;
23 import android.app.Activity;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.res.ColorStateList;
27 import android.content.res.Configuration;
28 import android.content.res.TypedArray;
29 import android.os.Build;
30 import android.os.Bundle;
31 import android.text.format.DateFormat;
32 import android.util.Log;
33 import android.view.LayoutInflater;
34 import android.view.Menu;
35 import android.view.MenuInflater;
36 import android.view.MenuItem;
37 import android.view.View;
38 import android.view.ViewGroup;
39 
40 import androidx.annotation.Nullable;
41 import androidx.annotation.RequiresApi;
42 import androidx.coordinatorlayout.widget.CoordinatorLayout;
43 import androidx.lifecycle.ViewModelProvider;
44 import androidx.preference.Preference;
45 import androidx.preference.PreferenceCategory;
46 import androidx.preference.PreferenceScreen;
47 import androidx.recyclerview.widget.RecyclerView;
48 
49 import com.android.permissioncontroller.PermissionControllerApplication;
50 import com.android.permissioncontroller.R;
51 import com.android.permissioncontroller.permission.ui.ManagePermissionsActivity;
52 import com.android.permissioncontroller.permission.ui.handheld.SettingsWithLargeHeader;
53 import com.android.permissioncontroller.permission.ui.model.v31.PermissionUsageDetailsViewModel;
54 import com.android.permissioncontroller.permission.ui.model.v31.PermissionUsageDetailsViewModel.AppPermissionAccessUiInfo;
55 import com.android.permissioncontroller.permission.ui.model.v31.PermissionUsageDetailsViewModel.PermissionUsageDetailsUiInfo;
56 import com.android.permissioncontroller.permission.ui.model.v31.PermissionUsageDetailsViewModel.PermissionUsageDetailsViewModelFactory;
57 import com.android.permissioncontroller.permission.utils.KotlinUtils;
58 
59 import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;
60 
61 import java.time.Clock;
62 import java.time.Instant;
63 import java.time.ZoneId;
64 import java.time.ZonedDateTime;
65 import java.time.temporal.ChronoUnit;
66 import java.util.List;
67 import java.util.concurrent.atomic.AtomicReference;
68 
69 /** The permission details page showing the history/timeline of a permission */
70 @RequiresApi(Build.VERSION_CODES.S)
71 public class PermissionUsageDetailsFragment extends SettingsWithLargeHeader {
72     private static final String KEY_SESSION_ID = "_session_id";
73     private static final String SESSION_ID_KEY =
74             PermissionUsageDetailsFragment.class.getName() + KEY_SESSION_ID;
75     private static final String TAG = PermissionUsageDetailsFragment.class.getName();
76 
77     private static final int MENU_SHOW_7_DAYS_DATA = Menu.FIRST + 4;
78     private static final int MENU_SHOW_24_HOURS_DATA = Menu.FIRST + 5;
79     private @Nullable String mPermissionGroup;
80     private int mUsageSubtitle;
81     private boolean mHasSystemApps;
82 
83     private MenuItem mShowSystemMenu;
84     private MenuItem mHideSystemMenu;
85     private MenuItem mShow7DaysDataMenu;
86     private MenuItem mShow24HoursDataMenu;
87 
88     private PermissionUsageDetailsViewModel mViewModel;
89 
90     private long mSessionId;
91 
92     @Override
onCreate(Bundle savedInstanceState)93     public void onCreate(Bundle savedInstanceState) {
94         super.onCreate(savedInstanceState);
95         mPermissionGroup = getArguments().getString(Intent.EXTRA_PERMISSION_GROUP_NAME);
96 
97         if (mPermissionGroup == null) {
98             Log.e(TAG, "No permission group was provided for PermissionDetailsFragment");
99             return;
100         }
101 
102         PermissionUsageDetailsViewModelFactory factory =
103                 new PermissionUsageDetailsViewModelFactory(
104                         PermissionControllerApplication.get(), this, mPermissionGroup);
105         mViewModel =
106                 new ViewModelProvider(this, factory).get(PermissionUsageDetailsViewModel.class);
107 
108         if (savedInstanceState != null) {
109             mSessionId = savedInstanceState.getLong(SESSION_ID_KEY);
110         } else {
111             mSessionId = getArguments().getLong(EXTRA_SESSION_ID, INVALID_SESSION_ID);
112         }
113 
114         mViewModel.updateShowSystemAppsToggle(
115                 getArguments().getBoolean(ManagePermissionsActivity.EXTRA_SHOW_SYSTEM, false));
116         mViewModel.updateShow7DaysToggle(
117                 KotlinUtils.INSTANCE.is7DayToggleEnabled()
118                         && getArguments()
119                                 .getBoolean(ManagePermissionsActivity.EXTRA_SHOW_7_DAYS, false));
120 
121         setHasOptionsMenu(true);
122         ActionBar ab = getActivity().getActionBar();
123         if (ab != null) {
124             ab.setDisplayHomeAsUpEnabled(true);
125         }
126         setLoading(true, false);
127 
128         mViewModel.getPermissionUsagesDetailsInfoUiLiveData().observe(this, this::updateAllUI);
129     }
130 
131     @Override
onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)132     public View onCreateView(
133             LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
134         ViewGroup rootView =
135                 (ViewGroup) super.onCreateView(inflater, container, savedInstanceState);
136 
137         PermissionDetailsWrapperFragment parentFragment =
138                 (PermissionDetailsWrapperFragment) requireParentFragment();
139         CoordinatorLayout coordinatorLayout = parentFragment.getCoordinatorLayout();
140         inflater.inflate(R.layout.permission_details_extended_fab, coordinatorLayout);
141         ExtendedFloatingActionButton extendedFab =
142                 coordinatorLayout.requireViewById(R.id.extended_fab);
143         // Load the background tint color from the application theme
144         // rather than the Material Design theme
145         Activity activity = getActivity();
146         ColorStateList backgroundColor =
147                 activity.getColorStateList(android.R.color.system_accent3_100);
148         extendedFab.setBackgroundTintList(backgroundColor);
149         extendedFab.setText(R.string.manage_permission);
150         boolean isUiModeNight =
151                 (activity.getResources().getConfiguration().uiMode
152                                 & Configuration.UI_MODE_NIGHT_MASK)
153                         == Configuration.UI_MODE_NIGHT_YES;
154         int textColorAttr =
155                 isUiModeNight
156                         ? android.R.attr.textColorPrimaryInverse
157                         : android.R.attr.textColorPrimary;
158         TypedArray typedArray = activity.obtainStyledAttributes(new int[] {textColorAttr});
159         ColorStateList textColor = typedArray.getColorStateList(0);
160         typedArray.recycle();
161         extendedFab.setTextColor(textColor);
162         extendedFab.setIcon(activity.getDrawable(R.drawable.ic_settings_outline));
163         extendedFab.setVisibility(View.VISIBLE);
164         extendedFab.setOnClickListener(
165                 view -> {
166                     Intent intent =
167                             new Intent(Intent.ACTION_MANAGE_PERMISSION_APPS)
168                                     .putExtra(Intent.EXTRA_PERMISSION_NAME, mPermissionGroup);
169                     startActivity(intent);
170                 });
171         RecyclerView recyclerView = getListView();
172         int bottomPadding =
173                 getResources()
174                         .getDimensionPixelSize(
175                                 R.dimen.privhub_details_recycler_view_bottom_padding);
176         recyclerView.setPadding(0, 0, 0, bottomPadding);
177         recyclerView.setClipToPadding(false);
178         recyclerView.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY);
179 
180         return rootView;
181     }
182 
183     @Override
onStart()184     public void onStart() {
185         super.onStart();
186         CharSequence title = getString(R.string.permission_history_title);
187         if (mPermissionGroup != null) {
188             title =
189                     getResources()
190                             .getString(
191                                     R.string.permission_group_usage_title,
192                                     KotlinUtils.INSTANCE.getPermGroupLabel(
193                                             getActivity(), mPermissionGroup));
194         }
195         getActivity().setTitle(title);
196     }
197 
198     @Override
onSaveInstanceState(Bundle outState)199     public void onSaveInstanceState(Bundle outState) {
200         super.onSaveInstanceState(outState);
201         outState.putLong(SESSION_ID_KEY, mSessionId);
202     }
203 
204     @Override
onCreateOptionsMenu(Menu menu, MenuInflater inflater)205     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
206         mShowSystemMenu =
207                 menu.add(Menu.NONE, MENU_SHOW_SYSTEM, Menu.NONE, R.string.menu_show_system);
208         mHideSystemMenu =
209                 menu.add(Menu.NONE, MENU_HIDE_SYSTEM, Menu.NONE, R.string.menu_hide_system);
210         boolean showSystem = false;
211         if (mViewModel.getShowSystemLiveData().getValue() != null) {
212             showSystem = mViewModel.getShowSystemLiveData().getValue();
213         }
214         updateShowSystemToggle(showSystem);
215 
216         if (KotlinUtils.INSTANCE.is7DayToggleEnabled()) {
217             mShow7DaysDataMenu =
218                     menu.add(
219                             Menu.NONE,
220                             MENU_SHOW_7_DAYS_DATA,
221                             Menu.NONE,
222                             R.string.menu_show_7_days_data);
223             mShow24HoursDataMenu =
224                     menu.add(
225                             Menu.NONE,
226                             MENU_SHOW_24_HOURS_DATA,
227                             Menu.NONE,
228                             R.string.menu_show_24_hours_data);
229             boolean show7Days = false;
230             if (mViewModel.getShow7DaysLiveData().getValue() != null) {
231                 show7Days = mViewModel.getShow7DaysLiveData().getValue();
232             }
233             updateShow7DaysToggle(show7Days);
234         }
235     }
236 
237     @Override
onOptionsItemSelected(MenuItem item)238     public boolean onOptionsItemSelected(MenuItem item) {
239         int itemId = item.getItemId();
240         switch (itemId) {
241             case android.R.id.home:
242                 getActivity().finishAfterTransition();
243                 return true;
244             case MENU_SHOW_SYSTEM:
245                 mViewModel.updateShowSystemAppsToggle(true);
246                 break;
247             case MENU_HIDE_SYSTEM:
248                 mViewModel.updateShowSystemAppsToggle(false);
249                 break;
250             case MENU_SHOW_7_DAYS_DATA:
251                 mViewModel.updateShow7DaysToggle(KotlinUtils.INSTANCE.is7DayToggleEnabled());
252                 break;
253             case MENU_SHOW_24_HOURS_DATA:
254                 mViewModel.updateShow7DaysToggle(false);
255                 break;
256         }
257 
258         return super.onOptionsItemSelected(item);
259     }
260 
261     /** Updates page content and menu items. */
updateAllUI(PermissionUsageDetailsUiInfo uiData)262     private void updateAllUI(PermissionUsageDetailsUiInfo uiData) {
263         if (getActivity() == null) {
264             return;
265         }
266         Context context = getActivity();
267         PreferenceScreen screen = getPreferenceScreen();
268         if (screen == null) {
269             screen = getPreferenceManager().createPreferenceScreen(context);
270             setPreferenceScreen(screen);
271         }
272         screen.removeAll();
273         boolean show7Days =
274                 mViewModel.getShow7DaysLiveData().getValue() != null
275                         ? mViewModel.getShow7DaysLiveData().getValue()
276                         : false;
277 
278         Preference subtitlePreference = new Preference(context);
279         updateShow7DaysToggle(show7Days);
280         mUsageSubtitle =
281                 show7Days
282                         ? R.string.permission_group_usage_subtitle_7d
283                         : R.string.permission_group_usage_subtitle_24h;
284 
285         subtitlePreference.setSummary(
286                 getResources()
287                         .getString(
288                                 mUsageSubtitle,
289                                 KotlinUtils.INSTANCE.getPermGroupLabel(
290                                         getActivity(), mPermissionGroup)));
291         subtitlePreference.setSelectable(false);
292         screen.addPreference(subtitlePreference);
293 
294         boolean containsSystemAppAccesses = uiData.getContainsSystemAppAccesses();
295         if (mHasSystemApps != containsSystemAppAccesses) {
296             mHasSystemApps = containsSystemAppAccesses;
297         }
298         boolean showSystem =
299                 mViewModel.getShowSystemLiveData().getValue() != null
300                         ? mViewModel.getShowSystemLiveData().getValue()
301                         : false;
302         updateShowSystemToggle(showSystem);
303 
304         // Make these variables effectively final so that
305         // we can use these captured variables in the below lambda expression
306         AtomicReference<PreferenceCategory> category =
307                 new AtomicReference<>(createDayCategoryPreference());
308         screen.addPreference(category.get());
309         PreferenceScreen finalScreen = screen;
310 
311         if (getActivity() == null) {
312             // Fragment has no Activity, return.
313             return;
314         }
315         renderHistoryPreferences(uiData.getAppPermissionAccessUiInfoList(), category, finalScreen);
316 
317         setLoading(false, true);
318         setProgressBarVisible(false);
319     }
320 
321     /** Render the provided appPermissionAccessUiInfoList into the [preferenceScreen] UI. */
renderHistoryPreferences( List<AppPermissionAccessUiInfo> appPermissionAccessUiInfoList, AtomicReference<PreferenceCategory> category, PreferenceScreen preferenceScreen)322     private void renderHistoryPreferences(
323             List<AppPermissionAccessUiInfo> appPermissionAccessUiInfoList,
324             AtomicReference<PreferenceCategory> category,
325             PreferenceScreen preferenceScreen) {
326         Context context = getContext();
327         long previousDateMs = 0L;
328         long midnightToday =
329                 ZonedDateTime.now(ZoneId.systemDefault())
330                                 .truncatedTo(ChronoUnit.DAYS)
331                                 .toEpochSecond()
332                         * 1000L;
333         long midnightYesterday =
334                 ZonedDateTime.now(ZoneId.systemDefault())
335                                 .minusDays(1)
336                                 .truncatedTo(ChronoUnit.DAYS)
337                                 .toEpochSecond()
338                         * 1000L;
339 
340         for (int i = 0; i < appPermissionAccessUiInfoList.size(); i++) {
341             AppPermissionAccessUiInfo appPermissionAccessUiInfo =
342                     appPermissionAccessUiInfoList.get(i);
343             long accessEndTime = appPermissionAccessUiInfo.getAccessEndTime();
344             long currentDateMs =
345                     ZonedDateTime.ofInstant(
346                                             Instant.ofEpochMilli(accessEndTime),
347                                             Clock.system(ZoneId.systemDefault()).getZone())
348                                     .truncatedTo(ChronoUnit.DAYS)
349                                     .toEpochSecond()
350                             * 1000L;
351             if (currentDateMs != previousDateMs) {
352                 if (previousDateMs != 0L) {
353                     category.set(createDayCategoryPreference());
354                     preferenceScreen.addPreference(category.get());
355                 }
356                 if (accessEndTime > midnightToday) {
357                     category.get().setTitle(R.string.permission_history_category_today);
358                 } else if (accessEndTime > midnightYesterday) {
359                     category.get().setTitle(R.string.permission_history_category_yesterday);
360                 } else {
361                     category.get()
362                             .setTitle(DateFormat.getLongDateFormat(context).format(currentDateMs));
363                 }
364                 previousDateMs = currentDateMs;
365             }
366 
367             Preference permissionUsagePreference =
368                     new PermissionHistoryPreference(
369                             getContext(),
370                             appPermissionAccessUiInfo.getUserHandle(),
371                             appPermissionAccessUiInfo.getPackageName(),
372                             appPermissionAccessUiInfo.getBadgedPackageIcon(),
373                             appPermissionAccessUiInfo.getPackageLabel(),
374                             appPermissionAccessUiInfo.getPermissionGroup(),
375                             appPermissionAccessUiInfo.getAccessStartTime(),
376                             appPermissionAccessUiInfo.getAccessEndTime(),
377                             appPermissionAccessUiInfo.getSummaryText(),
378                             appPermissionAccessUiInfo.getShowingAttribution(),
379                             appPermissionAccessUiInfo.getAttributionTags(),
380                             i == appPermissionAccessUiInfoList.size() - 1,
381                             mSessionId);
382 
383             category.get().addPreference(permissionUsagePreference);
384         }
385     }
386 
updateShowSystemToggle(boolean showSystem)387     private void updateShowSystemToggle(boolean showSystem) {
388         if (mHasSystemApps) {
389             if (mShowSystemMenu != null) {
390                 mShowSystemMenu.setVisible(!showSystem);
391                 mShowSystemMenu.setEnabled(true);
392             }
393 
394             if (mHideSystemMenu != null) {
395                 mHideSystemMenu.setVisible(showSystem);
396                 mHideSystemMenu.setEnabled(true);
397             }
398         } else {
399             if (mShowSystemMenu != null) {
400                 mShowSystemMenu.setVisible(true);
401                 mShowSystemMenu.setEnabled(false);
402             }
403 
404             if (mHideSystemMenu != null) {
405                 mHideSystemMenu.setVisible(false);
406                 mHideSystemMenu.setEnabled(false);
407             }
408         }
409     }
410 
updateShow7DaysToggle(boolean show7Days)411     private void updateShow7DaysToggle(boolean show7Days) {
412         if (mShow7DaysDataMenu != null) {
413             mShow7DaysDataMenu.setVisible(!show7Days);
414         }
415 
416         if (mShow24HoursDataMenu != null) {
417             mShow24HoursDataMenu.setVisible(show7Days);
418         }
419     }
420 
createDayCategoryPreference()421     private PreferenceCategory createDayCategoryPreference() {
422         PreferenceCategory category = new PreferenceCategory(getContext());
423         // Do not reserve icon space, so that the text moves all the way left.
424         category.setIconSpaceReserved(false);
425         return category;
426     }
427 }
428