1 /* 2 * Copyright (C) 2022 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.safetycenter.ui; 18 19 import static android.os.Build.VERSION_CODES.TIRAMISU; 20 21 import static com.android.permissioncontroller.Constants.EXTRA_SESSION_ID; 22 import static com.android.permissioncontroller.safetycenter.SafetyCenterConstants.PRIVACY_SOURCES_GROUP_ID; 23 import static com.android.permissioncontroller.safetycenter.SafetyCenterConstants.QUICK_SETTINGS_SAFETY_CENTER_FRAGMENT; 24 25 import static java.util.Collections.emptyList; 26 import static java.util.Objects.requireNonNull; 27 28 import android.content.Context; 29 import android.content.Intent; 30 import android.graphics.Color; 31 import android.graphics.drawable.ColorDrawable; 32 import android.graphics.drawable.Drawable; 33 import android.os.Bundle; 34 import android.safetycenter.SafetyCenterData; 35 import android.safetycenter.SafetyCenterEntry; 36 import android.safetycenter.SafetyCenterEntryGroup; 37 import android.safetycenter.SafetyCenterEntryOrGroup; 38 import android.safetycenter.SafetyCenterIssue; 39 import android.safetycenter.SafetyCenterStaticEntry; 40 import android.safetycenter.SafetyCenterStaticEntryGroup; 41 import android.util.Log; 42 import android.view.LayoutInflater; 43 import android.view.View; 44 import android.view.ViewGroup; 45 46 import androidx.annotation.Nullable; 47 import androidx.annotation.RequiresApi; 48 import androidx.preference.Preference; 49 import androidx.preference.PreferenceCategory; 50 import androidx.preference.PreferenceGroup; 51 import androidx.recyclerview.widget.RecyclerView; 52 53 import com.android.modules.utils.build.SdkLevel; 54 import com.android.permissioncontroller.R; 55 import com.android.permissioncontroller.safetycenter.ui.model.SafetyCenterUiData; 56 import com.android.permissioncontroller.safetycenter.ui.model.StatusUiData; 57 import com.android.safetycenter.internaldata.SafetyCenterBundles; 58 import com.android.safetycenter.resources.SafetyCenterResourcesApk; 59 60 import kotlin.Unit; 61 62 import java.util.List; 63 import java.util.Map; 64 import java.util.Objects; 65 66 /** Dashboard fragment for the Safety Center. */ 67 @RequiresApi(TIRAMISU) 68 public final class SafetyCenterDashboardFragment extends SafetyCenterFragment { 69 70 private static final String TAG = SafetyCenterDashboardFragment.class.getSimpleName(); 71 72 private static final String SAFETY_STATUS_KEY = "safety_status"; 73 private static final String ISSUES_GROUP_KEY = "issues_group"; 74 private static final String ENTRIES_GROUP_KEY = "entries_group"; 75 private static final String STATIC_ENTRIES_GROUP_KEY = "static_entries_group"; 76 private static final String SPACER_KEY = "spacer"; 77 78 private SafetyStatusPreference mSafetyStatusPreference; 79 private final CollapsableGroupCardHelper mCollapsableGroupCardHelper = 80 new CollapsableGroupCardHelper(); 81 private PreferenceGroup mIssuesGroup; 82 private PreferenceGroup mEntriesGroup; 83 private PreferenceGroup mStaticEntriesGroup; 84 private boolean mIsQuickSettingsFragment; 85 SafetyCenterDashboardFragment()86 public SafetyCenterDashboardFragment() {} 87 88 /** 89 * Create instance of SafetyCenterDashboardFragment with the arguments set 90 * 91 * @param isQuickSettingsFragment Denoting if it is the quick settings fragment 92 * @return SafetyCenterDashboardFragment with the arguments set 93 */ newInstance( long sessionId, boolean isQuickSettingsFragment)94 public static SafetyCenterDashboardFragment newInstance( 95 long sessionId, boolean isQuickSettingsFragment) { 96 Bundle args = new Bundle(); 97 args.putLong(EXTRA_SESSION_ID, sessionId); 98 args.putBoolean(QUICK_SETTINGS_SAFETY_CENTER_FRAGMENT, isQuickSettingsFragment); 99 100 SafetyCenterDashboardFragment frag = new SafetyCenterDashboardFragment(); 101 frag.setArguments(args); 102 return frag; 103 } 104 105 @Override onCreatePreferences(Bundle savedInstanceState, String rootKey)106 public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 107 super.onCreatePreferences(savedInstanceState, rootKey); 108 setPreferencesFromResource(R.xml.safety_center_dashboard, rootKey); 109 110 if (getArguments() != null) { 111 mIsQuickSettingsFragment = 112 getArguments().getBoolean(QUICK_SETTINGS_SAFETY_CENTER_FRAGMENT, false); 113 } 114 mCollapsableGroupCardHelper.restoreState(savedInstanceState); 115 116 mSafetyStatusPreference = 117 requireNonNull(getPreferenceScreen().findPreference(SAFETY_STATUS_KEY)); 118 mSafetyStatusPreference.setViewModel(getSafetyCenterViewModel()); 119 120 mIssuesGroup = getPreferenceScreen().findPreference(ISSUES_GROUP_KEY); 121 mEntriesGroup = getPreferenceScreen().findPreference(ENTRIES_GROUP_KEY); 122 mStaticEntriesGroup = getPreferenceScreen().findPreference(STATIC_ENTRIES_GROUP_KEY); 123 124 if (mIsQuickSettingsFragment) { 125 getPreferenceScreen().removePreference(mEntriesGroup); 126 mEntriesGroup = null; 127 getPreferenceScreen().removePreference(mStaticEntriesGroup); 128 mStaticEntriesGroup = null; 129 Preference spacerPreference = getPreferenceScreen().findPreference(SPACER_KEY); 130 getPreferenceScreen().removePreference(spacerPreference); 131 } 132 getSafetyCenterViewModel().getStatusUiLiveData().observe(this, this::updateStatus); 133 134 prerenderCurrentSafetyCenterData(); 135 } 136 137 @Override onCreateRecyclerView( LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState)138 public RecyclerView onCreateRecyclerView( 139 LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { 140 RecyclerView recyclerView = 141 super.onCreateRecyclerView(inflater, parent, savedInstanceState); 142 143 if (mIsQuickSettingsFragment) { 144 recyclerView.setOverScrollMode(View.OVER_SCROLL_NEVER); 145 recyclerView.setVerticalScrollBarEnabled(false); 146 } 147 return recyclerView; 148 } 149 150 // Set the default divider line between preferences to be transparent 151 @Override setDivider(Drawable divider)152 public void setDivider(Drawable divider) { 153 super.setDivider(new ColorDrawable(Color.TRANSPARENT)); 154 } 155 156 @Override setDividerHeight(int height)157 public void setDividerHeight(int height) { 158 super.setDividerHeight(0); 159 } 160 161 @Override onResume()162 public void onResume() { 163 super.onResume(); 164 getSafetyCenterViewModel().pageOpen(); 165 } 166 167 @Override configureInteractionLogger()168 public void configureInteractionLogger() { 169 InteractionLogger logger = getSafetyCenterViewModel().getInteractionLogger(); 170 logger.setSessionId(getSafetyCenterSessionId()); 171 logger.setViewType(mIsQuickSettingsFragment ? ViewType.QUICK_SETTINGS : ViewType.FULL); 172 173 Intent intent = requireActivity().getIntent(); 174 logger.setNavigationSource(NavigationSource.fromIntent(intent)); 175 logger.setNavigationSensor(Sensor.fromIntent(intent)); 176 } 177 178 @Override onSaveInstanceState(Bundle outState)179 public void onSaveInstanceState(Bundle outState) { 180 super.onSaveInstanceState(outState); 181 mCollapsableGroupCardHelper.saveState(outState); 182 } 183 updateStatus(StatusUiData statusUiData)184 private void updateStatus(StatusUiData statusUiData) { 185 if (mIsQuickSettingsFragment) { 186 SafetyCenterResourcesApk safetyCenterResourcesApk = 187 new SafetyCenterResourcesApk(requireContext()); 188 boolean hasPendingActions = 189 safetyCenterResourcesApk 190 .getStringByName("overall_severity_level_ok_review_summary") 191 .equals(statusUiData.getOriginalSummary().toString()); 192 193 statusUiData = statusUiData.copyForPendingActions(hasPendingActions); 194 } 195 196 mSafetyStatusPreference.setData(statusUiData); 197 } 198 199 @Override renderSafetyCenterData(@ullable SafetyCenterUiData uiData)200 public void renderSafetyCenterData(@Nullable SafetyCenterUiData uiData) { 201 if (uiData == null) return; 202 SafetyCenterData data = uiData.getSafetyCenterData(); 203 204 Log.v(TAG, String.format("renderSafetyCenterData called with: %s", data)); 205 Context context = getContext(); 206 if (context == null) { 207 return; 208 } 209 210 // TODO(b/208212820): Only update entries that have changed since last 211 // update, rather than deleting and re-adding all. 212 updateIssues(context, data.getIssues(), uiData.getResolvedIssues()); 213 214 if (!mIsQuickSettingsFragment) { 215 updateSafetyEntries(context, data.getEntriesOrGroups()); 216 updateStaticSafetyEntries(context, data); 217 } 218 } 219 updateIssues( Context context, List<SafetyCenterIssue> issues, Map<String, String> resolvedIssues)220 private void updateIssues( 221 Context context, List<SafetyCenterIssue> issues, Map<String, String> resolvedIssues) { 222 mIssuesGroup.removeAll(); 223 getCollapsableIssuesCardHelper() 224 .addIssues( 225 context, 226 getSafetyCenterViewModel(), 227 getChildFragmentManager(), 228 mIssuesGroup, 229 issues, 230 emptyList(), 231 resolvedIssues, 232 getActivity().getTaskId()); 233 } 234 235 // TODO(b/208212820): Add groups and move to separate controller updateSafetyEntries( Context context, List<SafetyCenterEntryOrGroup> entriesOrGroups)236 private void updateSafetyEntries( 237 Context context, List<SafetyCenterEntryOrGroup> entriesOrGroups) { 238 mEntriesGroup.removeAll(); 239 240 for (int i = 0, size = entriesOrGroups.size(); i < size; i++) { 241 SafetyCenterEntryOrGroup entryOrGroup = entriesOrGroups.get(i); 242 SafetyCenterEntry entry = entryOrGroup.getEntry(); 243 SafetyCenterEntryGroup group = entryOrGroup.getEntryGroup(); 244 245 boolean isFirstElement = i == 0; 246 boolean isLastElement = i == size - 1; 247 248 if (SdkLevel.isAtLeastV() 249 && group != null 250 && Objects.equals(group.getId(), PRIVACY_SOURCES_GROUP_ID)) { 251 // Add an extra header before the privacy sources 252 PreferenceCategory category = new ComparablePreferenceCategory(context); 253 SafetyCenterResourcesApk safetyCenterResourcesApk = 254 new SafetyCenterResourcesApk(requireContext()); 255 category.setTitle(safetyCenterResourcesApk.getStringByName("privacy_title")); 256 mEntriesGroup.addPreference(category); 257 } 258 259 if (SafetyCenterUiFlags.getShowSubpages() && group != null) { 260 mEntriesGroup.addPreference( 261 new SafetyHomepageEntryPreference( 262 context, group, getSafetyCenterSessionId())); 263 } else if (entry != null) { 264 addTopLevelEntry(context, entry, isFirstElement, isLastElement); 265 } else if (group != null) { 266 addGroupEntries(context, group, isFirstElement, isLastElement); 267 } 268 } 269 } 270 addTopLevelEntry( Context context, SafetyCenterEntry entry, boolean isFirstElement, boolean isLastElement)271 private void addTopLevelEntry( 272 Context context, 273 SafetyCenterEntry entry, 274 boolean isFirstElement, 275 boolean isLastElement) { 276 mEntriesGroup.addPreference( 277 new SafetyEntryPreference( 278 context, 279 PendingIntentSender.getTaskIdForEntry( 280 entry.getId(), getSameTaskSourceIds(), requireActivity()), 281 entry, 282 PositionInCardList.calculate(isFirstElement, isLastElement), 283 getSafetyCenterViewModel())); 284 } 285 addGroupEntries( Context context, SafetyCenterEntryGroup group, boolean isFirstCard, boolean isLastCard)286 private void addGroupEntries( 287 Context context, 288 SafetyCenterEntryGroup group, 289 boolean isFirstCard, 290 boolean isLastCard) { 291 mEntriesGroup.addPreference( 292 new SafetyGroupPreference( 293 context, 294 group, 295 mCollapsableGroupCardHelper::isGroupExpanded, 296 isFirstCard, 297 isLastCard, 298 (entryId) -> 299 PendingIntentSender.getTaskIdForEntry( 300 entryId, getSameTaskSourceIds(), requireActivity()), 301 getSafetyCenterViewModel(), 302 (groupId) -> { 303 mCollapsableGroupCardHelper.onGroupExpanded(groupId); 304 return Unit.INSTANCE; 305 }, 306 (groupId) -> { 307 mCollapsableGroupCardHelper.onGroupCollapsed(groupId); 308 return Unit.INSTANCE; 309 })); 310 } 311 updateStaticSafetyEntries(Context context, SafetyCenterData data)312 private void updateStaticSafetyEntries(Context context, SafetyCenterData data) { 313 mStaticEntriesGroup.removeAll(); 314 315 for (SafetyCenterStaticEntryGroup group : data.getStaticEntryGroups()) { 316 if (group.getTitle().toString().isEmpty()) { 317 // Interpret an empty title as signal to not create a titled category 318 addStaticEntriesTo(context, data, mStaticEntriesGroup, group.getStaticEntries()); 319 } else { 320 PreferenceCategory category = new ComparablePreferenceCategory(context); 321 category.setTitle(group.getTitle()); 322 mStaticEntriesGroup.addPreference(category); 323 addStaticEntriesTo(context, data, category, group.getStaticEntries()); 324 } 325 } 326 } 327 addStaticEntriesTo( Context context, SafetyCenterData data, PreferenceGroup prefGroup, List<SafetyCenterStaticEntry> entries)328 private void addStaticEntriesTo( 329 Context context, 330 SafetyCenterData data, 331 PreferenceGroup prefGroup, 332 List<SafetyCenterStaticEntry> entries) { 333 for (SafetyCenterStaticEntry entry : entries) { 334 prefGroup.addPreference( 335 new StaticSafetyEntryPreference( 336 context, 337 requireActivity().getTaskId(), 338 entry, 339 SafetyCenterBundles.getStaticEntryId(data, entry), 340 getSafetyCenterViewModel())); 341 } 342 } 343 } 344