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