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