summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--PermissionController/res/drawable-v33/ic_privacy.xml7
-rw-r--r--PermissionController/res/drawable-v33/ic_safety_group_expand.xml12
-rw-r--r--PermissionController/res/layout-v33/preference_collapsed_group_entry.xml47
-rw-r--r--PermissionController/res/layout-v33/preference_expanded_group_entry.xml5
-rw-r--r--PermissionController/res/layout-v33/preference_expanded_group_widget.xml20
-rw-r--r--PermissionController/src/com/android/permissioncontroller/safetycenter/ui/CollapsableGroupCardHelper.kt71
-rw-r--r--PermissionController/src/com/android/permissioncontroller/safetycenter/ui/PositionInCardList.kt4
-rw-r--r--PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterDashboardFragment.java46
-rw-r--r--PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyEntryPreference.java41
-rw-r--r--PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyGroupHeaderEntryPreference.java72
-rw-r--r--PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyStatusPreference.java22
-rw-r--r--PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SeverityIconPicker.java63
-rw-r--r--PermissionController/src/com/android/permissioncontroller/safetycenter/ui/model/LiveSafetyCenterViewModel.kt75
-rw-r--r--service/Android.bp26
-rw-r--r--service/java/com/android/safetycenter/SafetyCenterConfigReader.java5
-rw-r--r--service/java/com/android/safetycenter/SafetyCenterDataTracker.java246
-rw-r--r--service/java/com/android/safetycenter/SafetyCenterService.java76
17 files changed, 683 insertions, 155 deletions
diff --git a/PermissionController/res/drawable-v33/ic_privacy.xml b/PermissionController/res/drawable-v33/ic_privacy.xml
index 60194f4ce..3e79456dc 100644
--- a/PermissionController/res/drawable-v33/ic_privacy.xml
+++ b/PermissionController/res/drawable-v33/ic_privacy.xml
@@ -20,8 +20,7 @@
android:height="20dp"
android:viewportWidth="16"
android:viewportHeight="20">
- <group>
- <clip-path android:pathData="M0 0H16V20H0V0Z" />
- <path android:pathData="M0 0V20H16V0" android:fillColor="?android:attr/textColorSecondary" />
- </group>
+ <path
+ android:pathData="M8,0L0,3V9.09C0,14.14 3.41,18.85 8,20C12.59,18.85 16,14.14 16,9.09V3L8,0ZM14,9.09C14,13.09 11.45,16.79 8,17.92C4.55,16.79 2,13.1 2,9.09V4.39L8,2.14L14,4.39V9.09ZM8.93,9.77L9.5,13H6.5L7.07,9.77C6.43,9.44 6,8.77 6,8C6,6.9 6.9,6 8,6C9.1,6 10,6.9 10,8C10,8.77 9.57,9.44 8.93,9.77Z"
+ android:fillColor="?android:attr/textColorPrimary"/>
</vector>
diff --git a/PermissionController/res/drawable-v33/ic_safety_group_expand.xml b/PermissionController/res/drawable-v33/ic_safety_group_expand.xml
new file mode 100644
index 000000000..bf0606d7a
--- /dev/null
+++ b/PermissionController/res/drawable-v33/ic_safety_group_expand.xml
@@ -0,0 +1,12 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M0,12C0,5.373 5.373,0 12,0C18.627,0 24,5.373 24,12C24,18.627 18.627,24 12,24C5.373,24 0,18.627 0,12Z"
+ android:fillColor="?attr/colorSurfaceVariant"/>
+ <path
+ android:pathData="M7.607,9.059L6.667,9.999L12,15.332L17.333,9.999L16.393,9.059L12,13.445"
+ android:fillColor="?android:attr/textColorPrimary"/>
+</vector>
diff --git a/PermissionController/res/layout-v33/preference_collapsed_group_entry.xml b/PermissionController/res/layout-v33/preference_collapsed_group_entry.xml
new file mode 100644
index 000000000..d4ad271cb
--- /dev/null
+++ b/PermissionController/res/layout-v33/preference_collapsed_group_entry.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@style/SafetyCenterEntry">
+
+ <FrameLayout
+ android:id="@+id/icon_frame"
+ style="@style/SafetyCenterEntryIconFrame">
+ <ImageView
+ android:id="@android:id/icon"
+ style="@style/SafetyCenterEntryIcon"/>
+ </FrameLayout>
+
+ <Space
+ android:id="@+id/empty_space"
+ style="@style/SafetyCenterEntryEmptySpace"/>
+
+ <LinearLayout
+ style="@style/SafetyCenterEntryTextContainer">
+
+ <TextView android:id="@android:id/title"
+ style="@style/SafetyCenterEntryTitle" />
+
+ <TextView android:id="@android:id/summary"
+ style="@style/SafetyCenterEntrySummary" />
+
+ </LinearLayout>
+
+ <ImageView android:id="@+id/chevron_icon"
+ android:layout_height="match_parent"
+ style="@style/SafetyCenterExpandedGroupIcon" />
+</LinearLayout>
diff --git a/PermissionController/res/layout-v33/preference_expanded_group_entry.xml b/PermissionController/res/layout-v33/preference_expanded_group_entry.xml
index 95fea69c0..f34da0d23 100644
--- a/PermissionController/res/layout-v33/preference_expanded_group_entry.xml
+++ b/PermissionController/res/layout-v33/preference_expanded_group_entry.xml
@@ -21,7 +21,6 @@
<TextView android:id="@android:id/title"
style="@style/SafetyCenterGroupTitle"/>
- <!-- Preference could place its optional widget here. -->
- <LinearLayout android:id="@android:id/widget_frame"
- style="@style/SafetyCenterGroupWidgetFrame"/>
+ <ImageView android:id="@+id/chevron_icon"
+ style="@style/SafetyCenterExpandedGroupIcon" />
</LinearLayout>
diff --git a/PermissionController/res/layout-v33/preference_expanded_group_widget.xml b/PermissionController/res/layout-v33/preference_expanded_group_widget.xml
deleted file mode 100644
index e76cc5982..000000000
--- a/PermissionController/res/layout-v33/preference_expanded_group_widget.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2022 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/expanded_icon"
- android:src="@drawable/ic_safety_group_collapse"
- style="@style/SafetyCenterExpandedGroupIcon"/>
diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/CollapsableGroupCardHelper.kt b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/CollapsableGroupCardHelper.kt
new file mode 100644
index 000000000..c2c4b928f
--- /dev/null
+++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/CollapsableGroupCardHelper.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.permissioncontroller.safetycenter.ui
+
+import android.os.Build
+import android.os.Bundle
+import androidx.annotation.RequiresApi
+import androidx.preference.PreferenceGroup
+
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+internal class CollapsableGroupCardHelper {
+
+ private val expandedGroups = mutableSetOf<CharSequence>()
+
+ private companion object {
+ private const val EXPANDED_ENTRY_GROUPS_SAVED_INSTANCE_STATE_KEY =
+ "expanded_entry_groups_saved_instance_state_key"
+ }
+
+ fun restoreState(state: Bundle?) {
+ state?.getCharSequenceArray(EXPANDED_ENTRY_GROUPS_SAVED_INSTANCE_STATE_KEY)?.let {
+ expandedGroups.clear()
+ expandedGroups.addAll(it)
+ }
+ }
+
+ fun saveState(outState: Bundle) {
+ outState.putCharSequenceArray(
+ EXPANDED_ENTRY_GROUPS_SAVED_INSTANCE_STATE_KEY,
+ expandedGroups.toTypedArray()
+ )
+ }
+
+ fun collapseGroup(groupId: String) {
+ expandedGroups.remove(groupId)
+ }
+
+ fun expandGroup(groupId: String) {
+ expandedGroups.add(groupId)
+ }
+
+ fun updatePreferenceVisibility(group: PreferenceGroup) {
+ val preferenceCount = group.preferenceCount
+ for (i in 0 until preferenceCount) {
+ when (val preference = group.getPreference(i)) {
+ is SafetyGroupHeaderEntryPreference -> {
+ val shouldShowExpanded = expandedGroups.contains(preference.groupId)
+ preference.isVisible = preference.isExpanded == shouldShowExpanded
+ }
+ is SafetyEntryPreference -> {
+ preference.isVisible = preference.groupId == null ||
+ expandedGroups.contains(preference.groupId)
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/PositionInCardList.kt b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/PositionInCardList.kt
index fbe9fa3b0..1b70f4517 100644
--- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/PositionInCardList.kt
+++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/PositionInCardList.kt
@@ -50,9 +50,9 @@ internal enum class PositionInCardList(val backgroundDrawableResId: Int) {
fun getTopMargin(context: Context): Int =
when (this) {
- CARD_START, CARD_START_END ->
+ CARD_START, CARD_START_END, CARD_START_LIST_END ->
context.resources.getDimensionPixelSize(R.dimen.safety_center_card_margin)
- LIST_START, LIST_START_CARD_END ->
+ LIST_START, LIST_START_CARD_END, LIST_START_END ->
context.resources.getDimensionPixelSize(R.dimen.safety_center_list_margin)
else -> 0
}
diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterDashboardFragment.java b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterDashboardFragment.java
index 148340fbc..f5dbe0ff1 100644
--- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterDashboardFragment.java
+++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterDashboardFragment.java
@@ -78,6 +78,8 @@ public final class SafetyCenterDashboardFragment extends PreferenceFragmentCompa
private SafetyStatusPreference mSafetyStatusPreference;
private CollapsableIssuesCardHelper mCollapsableIssuesCardHelper;
+ private final CollapsableGroupCardHelper mCollapsableGroupCardHelper =
+ new CollapsableGroupCardHelper();
private PreferenceGroup mIssuesGroup;
private PreferenceGroup mEntriesGroup;
private PreferenceGroup mStaticEntriesGroup;
@@ -160,6 +162,8 @@ public final class SafetyCenterDashboardFragment extends PreferenceFragmentCompa
mIsQuickSettingsFragment, parsedSafetyCenterIntent.getShouldExpandIssuesGroup());
mCollapsableIssuesCardHelper.restoreState(savedInstanceState);
+ mCollapsableGroupCardHelper.restoreState(savedInstanceState);
+
mSafetyStatusPreference =
requireNonNull(getPreferenceScreen().findPreference(SAFETY_STATUS_KEY));
// TODO: Use real strings here, or set more sensible defaults in the layout
@@ -217,6 +221,7 @@ public final class SafetyCenterDashboardFragment extends PreferenceFragmentCompa
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
mCollapsableIssuesCardHelper.saveState(outState);
+ mCollapsableGroupCardHelper.saveState(outState);
}
@Override
@@ -315,6 +320,8 @@ public final class SafetyCenterDashboardFragment extends PreferenceFragmentCompa
addGroupEntries(context, group, isFirstElement, isLastElement);
}
}
+
+ mCollapsableGroupCardHelper.updatePreferenceVisibility(mEntriesGroup);
}
private void addTopLevelEntry(
@@ -327,6 +334,7 @@ public final class SafetyCenterDashboardFragment extends PreferenceFragmentCompa
context,
getTaskIdForEntry(entry),
entry,
+ /* groupId */ null,
PositionInCardList.calculate(isFirstElement, isLastElement),
mViewModel));
}
@@ -336,34 +344,66 @@ public final class SafetyCenterDashboardFragment extends PreferenceFragmentCompa
SafetyCenterEntryGroup group,
boolean isFirstCard,
boolean isLastCard) {
+ // adding collapsed group entry, which will be visible initially
+ mEntriesGroup.addPreference(
+ new SafetyGroupHeaderEntryPreference(
+ context,
+ group,
+ isFirstCard
+ ? isLastCard ? PositionInCardList.LIST_START_END
+ : PositionInCardList.LIST_START_CARD_END
+ : isLastCard ? PositionInCardList.CARD_START_LIST_END
+ : PositionInCardList.CARD_START_END,
+ /* isExpanded */ false,
+ this::expandGroup
+ )
+ );
+
+ // adding expanded group entry, which will be hidden initially
mEntriesGroup.addPreference(
new SafetyGroupHeaderEntryPreference(
context,
group,
isFirstCard
? PositionInCardList.LIST_START
- : PositionInCardList.CARD_START));
+ : PositionInCardList.CARD_START,
+ /* isExpanded */ true,
+ this::collapseGroup
+ )
+ );
+ // adding group entries, but they are will be hidden initially until group is expanded
List<SafetyCenterEntry> entries = group.getEntries();
for (int i = 0, last = entries.size() - 1; i <= last; i++) {
boolean isCardEnd = i == last;
boolean isListEnd = isLastCard && isCardEnd;
PositionInCardList positionInCardList =
PositionInCardList.calculate(
- /* isListStart= */ false,
+ /* isListStart */ false,
isListEnd,
- /* isCardStart= */ false,
+ /* isCardStart */ false,
isCardEnd);
mEntriesGroup.addPreference(
new SafetyEntryPreference(
context,
getTaskIdForEntry(entries.get(i)),
entries.get(i),
+ group.getId(),
positionInCardList,
mViewModel));
}
}
+ private void expandGroup(String groupId) {
+ mCollapsableGroupCardHelper.expandGroup(groupId);
+ mCollapsableGroupCardHelper.updatePreferenceVisibility(mEntriesGroup);
+ }
+
+ private void collapseGroup(String groupId) {
+ mCollapsableGroupCardHelper.collapseGroup(groupId);
+ mCollapsableGroupCardHelper.updatePreferenceVisibility(mEntriesGroup);
+ }
+
private void updateStaticSafetyEntries(
Context context, List<SafetyCenterStaticEntryGroup> staticEntryGroups) {
mStaticEntriesGroup.removeAll();
diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyEntryPreference.java b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyEntryPreference.java
index c52a115d7..320ddcfcf 100644
--- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyEntryPreference.java
+++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyEntryPreference.java
@@ -46,11 +46,13 @@ public final class SafetyEntryPreference extends Preference implements Comparabl
private final PositionInCardList mPosition;
private final SafetyCenterEntry mEntry;
private final SafetyCenterViewModel mViewModel;
+ private final CharSequence mGroupId;
public SafetyEntryPreference(
Context context,
int taskId,
SafetyCenterEntry entry,
+ CharSequence groupId,
PositionInCardList position,
SafetyCenterViewModel viewModel) {
super(context);
@@ -58,12 +60,14 @@ public final class SafetyEntryPreference extends Preference implements Comparabl
mEntry = entry;
mPosition = position;
mViewModel = viewModel;
+ mGroupId = groupId;
setLayoutResource(R.layout.preference_entry);
setTitle(entry.getTitle());
setSummary(entry.getSummary());
- setIcon(selectIconResId(mEntry));
+ setIcon(SeverityIconPicker.selectIconResId(
+ mEntry.getSeverityLevel(), mEntry.getSeverityUnspecifiedIconType()));
PendingIntent pendingIntent = entry.getPendingIntent();
if (pendingIntent != null) {
@@ -116,7 +120,7 @@ public final class SafetyEntryPreference extends Preference implements Comparabl
boolean hideIcon =
mEntry.getSeverityLevel() == SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNSPECIFIED
&& mEntry.getSeverityUnspecifiedIconType()
- == SafetyCenterEntry.SEVERITY_UNSPECIFIED_ICON_TYPE_NO_ICON;
+ == SafetyCenterEntry.SEVERITY_UNSPECIFIED_ICON_TYPE_NO_ICON;
holder.findViewById(R.id.icon_frame).setVisibility(hideIcon ? View.GONE : View.VISIBLE);
holder.findViewById(R.id.empty_space).setVisibility(hideIcon ? View.VISIBLE : View.GONE);
enableOrDisableEntry(holder);
@@ -157,23 +161,6 @@ public final class SafetyEntryPreference extends Preference implements Comparabl
}
}
- private static int selectIconResId(SafetyCenterEntry entry) {
- switch (entry.getSeverityLevel()) {
- case SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNKNOWN:
- return R.drawable.ic_safety_null_state;
- case SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNSPECIFIED:
- return selectSeverityUnspecifiedIconResId(entry);
- case SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_OK:
- return R.drawable.ic_safety_info;
- case SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_RECOMMENDATION:
- return R.drawable.ic_safety_recommendation;
- case SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_CRITICAL_WARNING:
- return R.drawable.ic_safety_warn;
- }
- Log.e(TAG, String.format("Unexpected SafetyCenterEntry.EntrySeverityLevel: %s", entry));
- return R.drawable.ic_safety_null_state;
- }
-
/** We are doing this because we need some entries to look disabled but still be clickable. */
private void enableOrDisableEntry(@NonNull PreferenceViewHolder holder) {
holder.itemView.setEnabled(mEntry.getPendingIntent() != null);
@@ -186,23 +173,15 @@ public final class SafetyEntryPreference extends Preference implements Comparabl
}
}
- private static int selectSeverityUnspecifiedIconResId(SafetyCenterEntry entry) {
- switch (entry.getSeverityUnspecifiedIconType()) {
- case SafetyCenterEntry.SEVERITY_UNSPECIFIED_ICON_TYPE_NO_ICON:
- return R.drawable.ic_safety_empty;
- case SafetyCenterEntry.SEVERITY_UNSPECIFIED_ICON_TYPE_PRIVACY:
- return R.drawable.ic_privacy;
- case SafetyCenterEntry.SEVERITY_UNSPECIFIED_ICON_TYPE_NO_RECOMMENDATION:
- return R.drawable.ic_safety_null_state;
- }
- Log.e(TAG, String.format("Unexpected SafetyCenterEntry.SeverityNoneIconType: %s", entry));
- return R.drawable.ic_safety_null_state;
+ public CharSequence getGroupId() {
+ return mGroupId;
}
@Override
public boolean isSameItem(@NonNull Preference other) {
return other instanceof SafetyEntryPreference
- && TextUtils.equals(mEntry.getId(), ((SafetyEntryPreference) other).mEntry.getId());
+ && TextUtils.equals(mEntry.getId(), ((SafetyEntryPreference) other).mEntry.getId())
+ && TextUtils.equals(mGroupId, ((SafetyEntryPreference) other).mGroupId);
}
@Override
diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyGroupHeaderEntryPreference.java b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyGroupHeaderEntryPreference.java
index e0247d0e6..1fc52f6a6 100644
--- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyGroupHeaderEntryPreference.java
+++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyGroupHeaderEntryPreference.java
@@ -24,6 +24,7 @@ import android.safetycenter.SafetyCenterEntryGroup;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup.MarginLayoutParams;
+import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
@@ -32,33 +33,60 @@ import androidx.preference.PreferenceViewHolder;
import com.android.permissioncontroller.R;
-/** A preference that displays a visual representation of a {@link SafetyCenterEntry}. */
+import java.util.function.Consumer;
+
+/**
+ * A preference that displays a visual representation of a header for
+ * {@link SafetyCenterEntryGroup}.
+ */
@RequiresApi(TIRAMISU)
public class SafetyGroupHeaderEntryPreference extends Preference implements ComparablePreference {
private static final String TAG = SafetyGroupHeaderEntryPreference.class.getSimpleName();
- private final String mId;
+ private final SafetyCenterEntryGroup mGroup;
private final PositionInCardList mPosition;
+ private final boolean mIsExpanded;
public SafetyGroupHeaderEntryPreference(
- Context context, SafetyCenterEntryGroup group, PositionInCardList position) {
+ Context context,
+ SafetyCenterEntryGroup group,
+ PositionInCardList position,
+ boolean isExpanded,
+ Consumer<String> onClick) {
super(context);
- mId = group.getId();
+ mGroup = group;
mPosition = position;
- setLayoutResource(R.layout.preference_expanded_group_entry);
- setWidgetLayoutResource(R.layout.preference_expanded_group_widget);
+ mIsExpanded = isExpanded;
+ setLayoutResource(
+ isExpanded
+ ? R.layout.preference_expanded_group_entry
+ : R.layout.preference_collapsed_group_entry);
+
setTitle(group.getTitle());
- // TODO(b/222126985): make back selectable to return the Ripple effect
- setSelectable(false);
+ if (!isExpanded) {
+ setSummary(group.getSummary());
+ setIcon(
+ SeverityIconPicker.selectIconResId(
+ group.getSeverityLevel(), group.getSeverityUnspecifiedIconType()));
+ }
+
setOnPreferenceClickListener(
unused -> {
- // TODO(b/222126985): implement collapsing UX
+ onClick.accept(group.getId());
return true;
});
}
+ public String getGroupId() {
+ return mGroup != null ? mGroup.getId() : null;
+ }
+
+ public boolean isExpanded() {
+ return mIsExpanded;
+ }
+
@Override
public void onBindViewHolder(@NonNull PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
@@ -71,22 +99,38 @@ public class SafetyGroupHeaderEntryPreference extends Preference implements Comp
holder.itemView.setLayoutParams(params);
}
- // TODO(b/222126985): show a proper icon based on current state
- holder.findViewById(R.id.expanded_icon).setVisibility(View.GONE);
+ if (!mIsExpanded) {
+ boolean hideIcon =
+ mGroup.getSeverityLevel() == SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNSPECIFIED
+ && mGroup.getSeverityUnspecifiedIconType()
+ == SafetyCenterEntry.SEVERITY_UNSPECIFIED_ICON_TYPE_NO_ICON;
+ holder.findViewById(R.id.icon_frame).setVisibility(hideIcon ? View.GONE : View.VISIBLE);
+ holder.findViewById(R.id.empty_space)
+ .setVisibility(hideIcon ? View.VISIBLE : View.GONE);
+ }
+
+ ImageView chevronIcon = (ImageView) holder.findViewById(R.id.chevron_icon);
+ chevronIcon.setImageResource(
+ mIsExpanded
+ ? R.drawable.ic_safety_group_collapse
+ : R.drawable.ic_safety_group_expand);
}
@Override
public boolean isSameItem(@NonNull Preference other) {
- return mId != null
+ return mGroup != null
&& other instanceof SafetyGroupHeaderEntryPreference
- && TextUtils.equals(mId, ((SafetyGroupHeaderEntryPreference) other).mId);
+ && TextUtils.equals(
+ getGroupId(), ((SafetyGroupHeaderEntryPreference) other).getGroupId());
}
@Override
public boolean hasSameContents(@NonNull Preference other) {
if (other instanceof SafetyGroupHeaderEntryPreference) {
SafetyGroupHeaderEntryPreference o = (SafetyGroupHeaderEntryPreference) other;
- return TextUtils.equals(getTitle(), o.getTitle()) && mPosition == o.mPosition;
+ return TextUtils.equals(getTitle(), o.getTitle())
+ && mPosition == o.mPosition
+ && mIsExpanded == o.mIsExpanded;
}
return false;
}
diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyStatusPreference.java b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyStatusPreference.java
index 5070339d2..93d2fda98 100644
--- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyStatusPreference.java
+++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyStatusPreference.java
@@ -109,11 +109,12 @@ public class SafetyStatusPreference extends Preference implements ComparablePref
View safetyProtectionSectionView = holder.findViewById(R.id.safety_protection_section_view);
safetyProtectionSectionView.setVisibility(mHasIssues ? View.GONE : View.VISIBLE);
- rescanButton.setOnClickListener(unused -> {
- SafetyCenterViewModel viewModel = requireViewModel();
- viewModel.rescan();
- viewModel.getInteractionLogger().record(Action.SCAN_INITIATED);
- });
+ rescanButton.setOnClickListener(
+ unused -> {
+ SafetyCenterViewModel viewModel = requireViewModel();
+ viewModel.rescan();
+ viewModel.getInteractionLogger().record(Action.SCAN_INITIATED);
+ });
updateStatusIcon(statusImage, rescanButton);
}
@@ -148,8 +149,7 @@ public class SafetyStatusPreference extends Preference implements ComparablePref
private boolean isRefreshInProgress() {
int refreshStatus = mStatus.getRefreshStatus();
return refreshStatus == SafetyCenterStatus.REFRESH_STATUS_FULL_RESCAN_IN_PROGRESS
- || refreshStatus
- == SafetyCenterStatus.REFRESH_STATUS_DATA_FETCH_IN_PROGRESS;
+ || refreshStatus == SafetyCenterStatus.REFRESH_STATUS_DATA_FETCH_IN_PROGRESS;
}
private void startScanningAnimation(ImageView statusImage) {
@@ -239,8 +239,7 @@ public class SafetyStatusPreference extends Preference implements ComparablePref
private void startIconChangeAnimation(ImageView statusImage) {
int changeAnimationResId =
StatusAnimationResolver.getStatusChangeAnimation(
- mSettledSeverityLevel,
- mStatus.getSeverityLevel());
+ mSettledSeverityLevel, mStatus.getSeverityLevel());
if (changeAnimationResId == 0) {
setSettledStatus(statusImage);
return;
@@ -324,7 +323,7 @@ public class SafetyStatusPreference extends Preference implements ComparablePref
private void setRescanButtonState(View rescanButton) {
rescanButton.setVisibility(
mStatus.getSeverityLevel() != SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_OK
- || mHasIssues
+ || mHasIssues
? View.GONE
: View.VISIBLE);
rescanButton.setEnabled(!isRefreshInProgress());
@@ -365,7 +364,6 @@ public class SafetyStatusPreference extends Preference implements ComparablePref
return false;
}
SafetyStatusPreference other = (SafetyStatusPreference) preference;
- return Objects.equals(mStatus, other.mStatus)
- && mHasIssues == other.mHasIssues;
+ return Objects.equals(mStatus, other.mStatus) && mHasIssues == other.mHasIssues;
}
}
diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SeverityIconPicker.java b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SeverityIconPicker.java
new file mode 100644
index 000000000..3c7530994
--- /dev/null
+++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SeverityIconPicker.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.permissioncontroller.safetycenter.ui;
+
+import android.safetycenter.SafetyCenterEntry;
+import android.util.Log;
+
+import com.android.permissioncontroller.R;
+
+class SeverityIconPicker {
+
+ private static final String TAG = SeverityIconPicker.class.getSimpleName();
+
+ static int selectIconResId(int severityLevel, int severityUnspecifiedIconType) {
+ switch (severityLevel) {
+ case SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNKNOWN:
+ return R.drawable.ic_safety_null_state;
+ case SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNSPECIFIED:
+ return selectSeverityUnspecifiedIconResId(severityUnspecifiedIconType);
+ case SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_OK:
+ return R.drawable.ic_safety_info;
+ case SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_RECOMMENDATION:
+ return R.drawable.ic_safety_recommendation;
+ case SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_CRITICAL_WARNING:
+ return R.drawable.ic_safety_warn;
+ }
+ Log.e(TAG,
+ String.format(
+ "Unexpected SafetyCenterEntry.EntrySeverityLevel: %s", severityLevel));
+ return R.drawable.ic_safety_null_state;
+ }
+
+ private static int selectSeverityUnspecifiedIconResId(int severityUnspecifiedIconType) {
+ switch (severityUnspecifiedIconType) {
+ case SafetyCenterEntry.SEVERITY_UNSPECIFIED_ICON_TYPE_NO_ICON:
+ return R.drawable.ic_safety_empty;
+ case SafetyCenterEntry.SEVERITY_UNSPECIFIED_ICON_TYPE_PRIVACY:
+ return R.drawable.ic_privacy;
+ case SafetyCenterEntry.SEVERITY_UNSPECIFIED_ICON_TYPE_NO_RECOMMENDATION:
+ return R.drawable.ic_safety_null_state;
+ }
+ Log.e(TAG,
+ String.format(
+ "Unexpected SafetyCenterEntry.SeverityNoneIconType: %s",
+ severityUnspecifiedIconType));
+ return R.drawable.ic_safety_null_state;
+ }
+
+}
diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/model/LiveSafetyCenterViewModel.kt b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/model/LiveSafetyCenterViewModel.kt
index 914a3d88b..10cd9b1e7 100644
--- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/model/LiveSafetyCenterViewModel.kt
+++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/model/LiveSafetyCenterViewModel.kt
@@ -24,6 +24,7 @@ import android.safetycenter.SafetyCenterData
import android.safetycenter.SafetyCenterErrorDetails
import android.safetycenter.SafetyCenterIssue
import android.safetycenter.SafetyCenterManager
+import android.safetycenter.SafetyCenterStatus
import android.safetycenter.config.SafetySource
import android.util.Log
import androidx.annotation.MainThread
@@ -36,7 +37,6 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.android.permissioncontroller.safetycenter.ui.InteractionLogger
import com.android.permissioncontroller.safetycenter.ui.NavigationSource
-import java.util.concurrent.atomic.AtomicBoolean
/* A SafetyCenterViewModel that talks to the real backing service for Safety Center. */
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
@@ -77,7 +77,7 @@ class LiveSafetyCenterViewModel(app: Application) : SafetyCenterViewModel(app) {
})
}
- private var changingConfigurations = AtomicBoolean(false)
+ private var changingConfigurations = false
private val safetyCenterManager = app.getSystemService(SafetyCenterManager::class.java)!!
@@ -113,17 +113,19 @@ class LiveSafetyCenterViewModel(app: Application) : SafetyCenterViewModel(app) {
}
override fun pageOpen() {
- if (!changingConfigurations.getAndSet(false)) {
- // Refresh unless this is a config change
+ if (changingConfigurations) {
+ // Don't refresh when changing configurations, but reset for the next pageOpen call
+ changingConfigurations = false
+ } else {
safetyCenterManager.refreshSafetySources(SafetyCenterManager.REFRESH_REASON_PAGE_OPEN)
}
}
override fun changingConfigurations() {
- changingConfigurations.set(true)
+ changingConfigurations = true
}
- inner class SafetyCenterLiveData :
+ private inner class SafetyCenterLiveData :
MutableLiveData<SafetyCenterUiData>(),
SafetyCenterManager.OnSafetyCenterDataChangedListener {
@@ -131,22 +133,24 @@ class LiveSafetyCenterViewModel(app: Application) : SafetyCenterViewModel(app) {
// manipulate it, or the inFlight or resolved issues lists should only be called on the
// main thread, and are marked accordingly.
private val safetyCenterDataQueue = ArrayDeque<SafetyCenterData>()
- private var currentInFlightIssues = mapOf<IssueId, ActionId>()
+ private var activeInFlightIssues = mapOf<IssueId, ActionId>()
private val currentResolvedIssues = mutableMapOf<IssueId, ActionId>()
override fun onActive() {
safetyCenterManager.addOnSafetyCenterDataChangedListener(
- getMainExecutor(app.applicationContext), this)
+ getMainExecutor(app.applicationContext), this)
super.onActive()
}
override fun onInactive() {
safetyCenterManager.removeOnSafetyCenterDataChangedListener(this)
- // Remove all the tracked state and start from scratch when active again.
- currentInFlightIssues = mapOf()
- currentResolvedIssues.clear()
- safetyCenterDataQueue.clear()
+ if (!changingConfigurations) {
+ // Remove all the tracked state and start from scratch when active again.
+ activeInFlightIssues = mapOf()
+ currentResolvedIssues.clear()
+ safetyCenterDataQueue.clear()
+ }
super.onInactive()
}
@@ -173,40 +177,50 @@ class LiveSafetyCenterViewModel(app: Application) : SafetyCenterViewModel(app) {
}
while (safetyCenterDataQueue.isNotEmpty() && currentResolvedIssues.isEmpty()) {
- val nextSafetyCenterData = safetyCenterDataQueue.first()
+ val nextData = safetyCenterDataQueue.first()
// Calculate newly resolved issues by diffing the tracked in-flight issues and the
// current update. Resolved issues are formerly in-flight issues that no longer
// appear in a subsequent SafetyCenterData update.
val nextResolvedIssues: Map<IssueId, ActionId> =
- determineResolvedIssues(nextSafetyCenterData, currentInFlightIssues)
+ determineResolvedIssues(nextData.buildIssueIdSet())
- // Save the set of in-flight issues to diff against the next data update.
- currentInFlightIssues = nextSafetyCenterData.getInFlightIssues()
+ // Save the set of in-flight issues to diff against the next data update, removing
+ // the now-resolved, formerly in-flight issues. If these are not tracked separately
+ // the queue will not progress once the issue resolution animations complete.
+ activeInFlightIssues = nextData.getInFlightIssues()
- if (nextResolvedIssues.isEmpty()) {
- sendNextData()
- } else {
+ if (nextResolvedIssues.isNotEmpty()) {
currentResolvedIssues.putAll(nextResolvedIssues)
sendResolvedIssuesAndCurrentData()
+ } else if (shouldEndScan(nextData) || shouldSendLastDataInQueue()) {
+ sendNextData()
+ } else {
+ skipNextData()
}
}
}
- private fun determineResolvedIssues(
- incomingData: SafetyCenterData,
- inFlightIssues: Map<IssueId, ActionId>
- ): Map<IssueId, ActionId> {
+ private fun determineResolvedIssues(nextIssueIds: Set<IssueId>): Map<IssueId, ActionId> {
// Any previously in-flight issue that does not appear in the incoming SafetyCenterData
// is considered resolved.
- val issueIdSet: Set<IssueId> = incomingData.issues.map { issue -> issue.id }.toSet()
- return inFlightIssues.filterNot { issue -> issueIdSet.contains(issue.key) }
+ return activeInFlightIssues.filterNot { issue -> nextIssueIds.contains(issue.key) }
}
+ private fun shouldEndScan(nextData: SafetyCenterData): Boolean =
+ isCurrentlyScanning() && !nextData.isScanning()
+
+ private fun shouldSendLastDataInQueue(): Boolean =
+ !isCurrentlyScanning() && safetyCenterDataQueue.size == 1
+
+ private fun isCurrentlyScanning(): Boolean = value?.safetyCenterData?.isScanning() ?: false
+
private fun sendNextData() {
value = SafetyCenterUiData(safetyCenterDataQueue.removeFirst())
}
+ private fun skipNextData() = safetyCenterDataQueue.removeFirst()
+
private fun sendResolvedIssuesAndCurrentData() {
val currentData = value?.safetyCenterData
if (currentData == null || currentResolvedIssues.isEmpty()) {
@@ -231,14 +245,15 @@ class LiveSafetyCenterViewModel(app: Application) : SafetyCenterViewModel(app) {
private fun SafetyCenterData.getInFlightIssues(): Map<IssueId, ActionId> =
issues
- .map { issue ->
- issue.actions
- .filter { it.isInFlight }
- .map { issue.id to it.id }
- }
+ .map { issue -> issue.actions.filter { it.isInFlight }.map { issue.id to it.id } }
.flatten()
.toMap()
+private fun SafetyCenterData.isScanning() =
+ status.refreshStatus == SafetyCenterStatus.REFRESH_STATUS_FULL_RESCAN_IN_PROGRESS
+
+private fun SafetyCenterData.buildIssueIdSet(): Set<IssueId> = issues.map { it.id }.toSet()
+
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
class LiveSafetyCenterViewModelFactory(private val app: Application) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
diff --git a/service/Android.bp b/service/Android.bp
index 1746a6104..9846e7e31 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -91,6 +91,7 @@ java_sdk_library {
//"framework-permission",
"framework-permission-s.impl",
"framework-permission-s-shared",
+ "framework-statsd.stubs.module_lib",
"jsr305",
// Soong fails to automatically add this dependency because all the
@@ -108,6 +109,7 @@ java_sdk_library {
"safety-center-persistence",
"safety-center-resources-lib",
"service-permission-shared",
+ "service-permission-statsd",
"service-permission-streaming-proto-java-gen",
],
errorprone: {
@@ -132,3 +134,27 @@ java_sdk_library {
],
installable: true,
}
+
+genrule {
+ name: "statslog-service-permission-java-gen",
+ tools: ["stats-log-api-gen"],
+ cmd: "$(location stats-log-api-gen) --java $(out) --module permissioncontroller" +
+ " --javaPackage com.android.permission" +
+ " --javaClass PermissionStatsLog --minApiLevel 29",
+ out: ["com/android/permission/PermissionStatsLog.java"],
+}
+
+java_library {
+ name: "service-permission-statsd",
+ srcs: [
+ ":statslog-service-permission-java-gen",
+ ],
+ libs: [
+ "framework-statsd.stubs.module_lib",
+ ],
+ apex_available: [
+ "com.android.permission",
+ ],
+ min_sdk_version: "30",
+ sdk_version: "system_server_current",
+}
diff --git a/service/java/com/android/safetycenter/SafetyCenterConfigReader.java b/service/java/com/android/safetycenter/SafetyCenterConfigReader.java
index aeb179486..92bfab05d 100644
--- a/service/java/com/android/safetycenter/SafetyCenterConfigReader.java
+++ b/service/java/com/android/safetycenter/SafetyCenterConfigReader.java
@@ -143,6 +143,11 @@ final class SafetyCenterConfigReader {
return getCurrentConfigInternal().getExternalSafetySources().containsKey(safetySourceId);
}
+ /** Returns whether the {@link SafetyCenterConfig} is currently overridden. */
+ boolean isOverrideForTestsActive() {
+ return mConfigInternalOverrideForTests != null;
+ }
+
/**
* Returns the {@link Broadcast} defined in the {@link SafetyCenterConfig}, with all the sources
* that they should handle and the profile on which they should be dispatched.
diff --git a/service/java/com/android/safetycenter/SafetyCenterDataTracker.java b/service/java/com/android/safetycenter/SafetyCenterDataTracker.java
index 2daf6a378..9734b198b 100644
--- a/service/java/com/android/safetycenter/SafetyCenterDataTracker.java
+++ b/service/java/com/android/safetycenter/SafetyCenterDataTracker.java
@@ -18,6 +18,21 @@ package com.android.safetycenter;
import static android.os.Build.VERSION_CODES.TIRAMISU;
+import static com.android.permission.PermissionStatsLog.SAFETY_SOURCE_STATE_COLLECTED;
+import static com.android.permission.PermissionStatsLog.SAFETY_SOURCE_STATE_COLLECTED__SAFETY_SOURCE_PROFILE_TYPE__PROFILE_TYPE_MANAGED;
+import static com.android.permission.PermissionStatsLog.SAFETY_SOURCE_STATE_COLLECTED__SAFETY_SOURCE_PROFILE_TYPE__PROFILE_TYPE_PERSONAL;
+import static com.android.permission.PermissionStatsLog.SAFETY_SOURCE_STATE_COLLECTED__SEVERITY_LEVEL__SAFETY_SEVERITY_CRITICAL_WARNING;
+import static com.android.permission.PermissionStatsLog.SAFETY_SOURCE_STATE_COLLECTED__SEVERITY_LEVEL__SAFETY_SEVERITY_LEVEL_UNKNOWN;
+import static com.android.permission.PermissionStatsLog.SAFETY_SOURCE_STATE_COLLECTED__SEVERITY_LEVEL__SAFETY_SEVERITY_OK;
+import static com.android.permission.PermissionStatsLog.SAFETY_SOURCE_STATE_COLLECTED__SEVERITY_LEVEL__SAFETY_SEVERITY_RECOMMENDATION;
+import static com.android.permission.PermissionStatsLog.SAFETY_SOURCE_STATE_COLLECTED__SEVERITY_LEVEL__SAFETY_SEVERITY_UNSPECIFIED;
+import static com.android.permission.PermissionStatsLog.SAFETY_STATE;
+import static com.android.permission.PermissionStatsLog.SAFETY_STATE__OVERALL_SEVERITY_LEVEL__SAFETY_SEVERITY_CRITICAL_WARNING;
+import static com.android.permission.PermissionStatsLog.SAFETY_STATE__OVERALL_SEVERITY_LEVEL__SAFETY_SEVERITY_LEVEL_UNKNOWN;
+import static com.android.permission.PermissionStatsLog.SAFETY_STATE__OVERALL_SEVERITY_LEVEL__SAFETY_SEVERITY_OK;
+import static com.android.permission.PermissionStatsLog.SAFETY_STATE__OVERALL_SEVERITY_LEVEL__SAFETY_SEVERITY_RECOMMENDATION;
+import static com.android.permission.PermissionStatsLog.SAFETY_STATE__OVERALL_SEVERITY_LEVEL__SAFETY_SEVERITY_UNSPECIFIED;
+
import static java.util.Collections.emptyList;
import android.annotation.NonNull;
@@ -57,9 +72,11 @@ import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
+import android.util.StatsEvent;
import androidx.annotation.RequiresApi;
+import com.android.permission.PermissionStatsLog;
import com.android.permission.util.UserUtils;
import com.android.safetycenter.SafetyCenterConfigReader.ExternalSafetySource;
import com.android.safetycenter.internaldata.SafetyCenterEntryGroupId;
@@ -72,6 +89,9 @@ import com.android.safetycenter.persistence.PersistedSafetyCenterIssue;
import com.android.safetycenter.resources.SafetyCenterResourcesContext;
import java.io.PrintWriter;
+import java.math.BigInteger;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
@@ -493,6 +513,46 @@ final class SafetyCenterDataTracker {
}
/**
+ * Clears all the {@link SafetySourceData}, metadata associated with {@link
+ * SafetyCenterIssueKey}s, in flight {@link SafetyCenterIssueActionId} and any refresh in
+ * progress so far, for the given user.
+ *
+ * <p>This method may modify the Safety Center issue cache and change the value reported by
+ * {@link #isSafetyCenterIssueCacheDirty} to {@code true}.
+ */
+ void clearForUser(@UserIdInt int userId) {
+ // Loop in reverse index order to be able to remove entries while iterating.
+ for (int i = mSafetySourceDataForKey.size() - 1; i >= 0; i--) {
+ SafetySourceKey sourceKey = mSafetySourceDataForKey.keyAt(i);
+ if (sourceKey.getUserId() == userId) {
+ mSafetySourceDataForKey.removeAt(i);
+ }
+ }
+ // Loop in reverse index order to be able to remove entries while iterating.
+ for (int i = mSafetySourceErrors.size() - 1; i >= 0; i--) {
+ SafetySourceKey sourceKey = mSafetySourceErrors.valueAt(i);
+ if (sourceKey.getUserId() == userId) {
+ mSafetySourceErrors.removeAt(i);
+ }
+ }
+ // Loop in reverse index order to be able to remove entries while iterating.
+ for (int i = mSafetyCenterIssueCache.size() - 1; i >= 0; i--) {
+ SafetyCenterIssueKey issueKey = mSafetyCenterIssueCache.keyAt(i);
+ if (issueKey.getUserId() == userId) {
+ mSafetyCenterIssueCache.removeAt(i);
+ mSafetyCenterIssueCacheDirty = true;
+ }
+ }
+ // Loop in reverse index order to be able to remove entries while iterating.
+ for (int i = mSafetyCenterIssueActionsInFlight.size() - 1; i >= 0; i--) {
+ SafetyCenterIssueActionId issueActionId = mSafetyCenterIssueActionsInFlight.valueAt(i);
+ if (issueActionId.getSafetyCenterIssueKey().getUserId() == userId) {
+ mSafetyCenterIssueActionsInFlight.removeAt(i);
+ }
+ }
+ }
+
+ /**
* Dumps state for debugging purposes.
*
* @param fout {@link PrintWriter} to write to
@@ -539,43 +599,120 @@ final class SafetyCenterDataTracker {
}
/**
- * Clears all the {@link SafetySourceData}, metadata associated with {@link
- * SafetyCenterIssueKey}s, in flight {@link SafetyCenterIssueActionId} and any refresh in
- * progress so far, for the given user.
- *
- * <p>This method may modify the Safety Center issue cache and change the value reported by
- * {@link #isSafetyCenterIssueCacheDirty} to {@code true}.
+ * Pulls the {@link PermissionStatsLog#SAFETY_STATE} atom and writes all relevant {@link
+ * PermissionStatsLog#SAFETY_SOURCE_STATE_COLLECTED} atoms for the given {@link
+ * UserProfileGroup}.
*/
- void clearForUser(@UserIdInt int userId) {
- // Loop in reverse index order to be able to remove entries while iterating.
- for (int i = mSafetySourceDataForKey.size() - 1; i >= 0; i--) {
- SafetySourceKey sourceKey = mSafetySourceDataForKey.keyAt(i);
- if (sourceKey.getUserId() == userId) {
- mSafetySourceDataForKey.removeAt(i);
- }
- }
- // Loop in reverse index order to be able to remove entries while iterating.
- for (int i = mSafetySourceErrors.size() - 1; i >= 0; i--) {
- SafetySourceKey sourceKey = mSafetySourceErrors.valueAt(i);
- if (sourceKey.getUserId() == userId) {
- mSafetySourceErrors.removeAt(i);
+ void pullAndWriteAtoms(
+ @NonNull UserProfileGroup userProfileGroup, @NonNull List<StatsEvent> statsEvents) {
+ pullOverallSafetyStateAtom(userProfileGroup, statsEvents);
+ // The SAFETY_SOURCE_STATE_COLLECTED atoms are written instead of being pulled, as they do
+ // not support pull.
+ writeSafetySourceStateCollectedAtoms(userProfileGroup);
+ }
+
+ private void pullOverallSafetyStateAtom(
+ @NonNull UserProfileGroup userProfileGroup, @NonNull List<StatsEvent> statsEvents) {
+ SafetyCenterData safetyCenterData = getSafetyCenterData("android", userProfileGroup);
+ int safetyStateOverallSeverityLevel =
+ toSafetyStateOverallSeverityLevel(safetyCenterData.getStatus().getSeverityLevel());
+ long openIssuesCount = safetyCenterData.getIssues().size();
+ long dismissedIssuesCount = 0;
+ for (int i = 0; i < mSafetyCenterIssueCache.size(); i++) {
+ SafetyCenterIssueKey issueKey = mSafetyCenterIssueCache.keyAt(i);
+ IssueData issueData = mSafetyCenterIssueCache.valueAt(i);
+
+ if (mSafetyCenterConfigReader.isExternalSafetySourceActive(issueKey.getSafetySourceId())
+ && userProfileGroup.contains(issueKey.getUserId())
+ && issueData.getDismissedAt() != null) {
+ dismissedIssuesCount++;
}
}
- // Loop in reverse index order to be able to remove entries while iterating.
- for (int i = mSafetyCenterIssueCache.size() - 1; i >= 0; i--) {
- SafetyCenterIssueKey issueKey = mSafetyCenterIssueCache.keyAt(i);
- if (issueKey.getUserId() == userId) {
- mSafetyCenterIssueCache.removeAt(i);
- mSafetyCenterIssueCacheDirty = true;
+ statsEvents.add(
+ PermissionStatsLog.buildStatsEvent(
+ SAFETY_STATE,
+ safetyStateOverallSeverityLevel,
+ openIssuesCount,
+ dismissedIssuesCount));
+ }
+
+ private void writeSafetySourceStateCollectedAtoms(@NonNull UserProfileGroup userProfileGroup) {
+ List<SafetySourcesGroup> safetySourcesGroups =
+ mSafetyCenterConfigReader.getSafetyCenterConfig().getSafetySourcesGroups();
+ for (int i = 0; i < safetySourcesGroups.size(); i++) {
+ SafetySourcesGroup safetySourcesGroup = safetySourcesGroups.get(i);
+ List<SafetySource> safetySources = safetySourcesGroup.getSafetySources();
+
+ for (int j = 0; j < safetySources.size(); j++) {
+ SafetySource safetySource = safetySources.get(j);
+
+ if (!SafetySources.isExternal(safetySource)) {
+ continue;
+ }
+
+ writeSafetySourceStateCollectedAtom(
+ safetySource.getId(), userProfileGroup.getProfileParentUserId(), false);
+
+ if (!SafetySources.supportsManagedProfiles(safetySource)) {
+ continue;
+ }
+
+ int[] managedRunningProfilesUserIds =
+ userProfileGroup.getManagedRunningProfilesUserIds();
+ for (int k = 0; k < managedRunningProfilesUserIds.length; k++) {
+ writeSafetySourceStateCollectedAtom(
+ safetySource.getId(), managedRunningProfilesUserIds[k], true);
+ }
}
}
- // Loop in reverse index order to be able to remove entries while iterating.
- for (int i = mSafetyCenterIssueActionsInFlight.size() - 1; i >= 0; i--) {
- SafetyCenterIssueActionId issueActionId = mSafetyCenterIssueActionsInFlight.valueAt(i);
- if (issueActionId.getSafetyCenterIssueKey().getUserId() == userId) {
- mSafetyCenterIssueActionsInFlight.removeAt(i);
+ }
+
+ private void writeSafetySourceStateCollectedAtom(
+ @NonNull String safetySourceId, @UserIdInt int userId, boolean isUserManaged) {
+ SafetySourceKey key = SafetySourceKey.of(safetySourceId, userId);
+ SafetySourceData safetySourceData = mSafetySourceDataForKey.get(key);
+ SafetySourceStatus safetySourceStatus = getSafetySourceStatus(safetySourceData);
+ List<SafetySourceIssue> safetySourceIssues =
+ safetySourceData == null ? emptyList() : safetySourceData.getIssues();
+
+ long encodedSafetySourceId = getEncodedSafetySourceSourceId(safetySourceId);
+ int safetySourceStateCollectedProfileType =
+ isUserManaged
+ ? SAFETY_SOURCE_STATE_COLLECTED__SAFETY_SOURCE_PROFILE_TYPE__PROFILE_TYPE_MANAGED
+ : SAFETY_SOURCE_STATE_COLLECTED__SAFETY_SOURCE_PROFILE_TYPE__PROFILE_TYPE_PERSONAL;
+ // Returns UNKNOWN for issue-only safety sources; since we are only logging the entry's
+ // severity level.
+ int safetySourceStateCollectedSeverityLevel =
+ safetySourceStatus == null
+ ? SAFETY_SOURCE_STATE_COLLECTED__SEVERITY_LEVEL__SAFETY_SEVERITY_LEVEL_UNKNOWN
+ : toSafetySourceStateCollectedSeverityLevel(
+ safetySourceStatus.getSeverityLevel());
+ long openIssuesCount = 0;
+ long dismissedIssuesCount = 0;
+ for (int i = 0; i < safetySourceIssues.size(); i++) {
+ SafetySourceIssue safetySourceIssue = safetySourceIssues.get(i);
+ SafetyCenterIssueKey safetyCenterIssueKey =
+ SafetyCenterIssueKey.newBuilder()
+ .setSafetySourceId(safetySourceId)
+ .setSafetySourceIssueId(safetySourceIssue.getId())
+ .setUserId(userId)
+ .build();
+
+ IssueData issueData = mSafetyCenterIssueCache.get(safetyCenterIssueKey);
+ if (issueData == null || issueData.getDismissedAt() == null) {
+ openIssuesCount++;
+ } else {
+ dismissedIssuesCount++;
}
}
+
+ PermissionStatsLog.write(
+ SAFETY_SOURCE_STATE_COLLECTED,
+ encodedSafetySourceId,
+ safetySourceStateCollectedProfileType,
+ safetySourceStateCollectedSeverityLevel,
+ openIssuesCount,
+ dismissedIssuesCount);
}
private boolean isDismissed(
@@ -1760,6 +1897,55 @@ final class SafetyCenterDataTracker {
return SafetyCenterEntry.IconAction.ICON_ACTION_TYPE_INFO;
}
+ private static int toSafetyStateOverallSeverityLevel(
+ @SafetyCenterStatus.OverallSeverityLevel int safetyCenterStatusOverallSeverityLevel) {
+ switch (safetyCenterStatusOverallSeverityLevel) {
+ case SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_UNKNOWN:
+ // Using UNSPECIFIED for the UNKNOWN case as we technically got the data here.
+ // UNKNOWN should be reserved for cases where the data couldn't be fetched.
+ return SAFETY_STATE__OVERALL_SEVERITY_LEVEL__SAFETY_SEVERITY_UNSPECIFIED;
+ case SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_OK:
+ return SAFETY_STATE__OVERALL_SEVERITY_LEVEL__SAFETY_SEVERITY_OK;
+ case SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_RECOMMENDATION:
+ return SAFETY_STATE__OVERALL_SEVERITY_LEVEL__SAFETY_SEVERITY_RECOMMENDATION;
+ case SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_CRITICAL_WARNING:
+ return SAFETY_STATE__OVERALL_SEVERITY_LEVEL__SAFETY_SEVERITY_CRITICAL_WARNING;
+ }
+ Log.w(
+ TAG,
+ "Unexpected SafetyCenterStatus.OverallSeverityLevel: "
+ + safetyCenterStatusOverallSeverityLevel);
+ return SAFETY_STATE__OVERALL_SEVERITY_LEVEL__SAFETY_SEVERITY_LEVEL_UNKNOWN;
+ }
+
+ private static int toSafetySourceStateCollectedSeverityLevel(
+ @SafetySourceData.SeverityLevel int safetySourceSeverityLevel) {
+ switch (safetySourceSeverityLevel) {
+ case SafetySourceData.SEVERITY_LEVEL_UNSPECIFIED:
+ return SAFETY_SOURCE_STATE_COLLECTED__SEVERITY_LEVEL__SAFETY_SEVERITY_UNSPECIFIED;
+ case SafetySourceData.SEVERITY_LEVEL_INFORMATION:
+ return SAFETY_SOURCE_STATE_COLLECTED__SEVERITY_LEVEL__SAFETY_SEVERITY_OK;
+ case SafetySourceData.SEVERITY_LEVEL_RECOMMENDATION:
+ return SAFETY_SOURCE_STATE_COLLECTED__SEVERITY_LEVEL__SAFETY_SEVERITY_RECOMMENDATION;
+ case SafetySourceData.SEVERITY_LEVEL_CRITICAL_WARNING:
+ return SAFETY_SOURCE_STATE_COLLECTED__SEVERITY_LEVEL__SAFETY_SEVERITY_CRITICAL_WARNING;
+ }
+ Log.w(TAG, "Unexpected SafetySourceData.SeverityLevel: " + safetySourceSeverityLevel);
+ return SAFETY_SOURCE_STATE_COLLECTED__SEVERITY_LEVEL__SAFETY_SEVERITY_LEVEL_UNKNOWN;
+ }
+
+ private static long getEncodedSafetySourceSourceId(@NonNull String safetySourceId) {
+ MessageDigest messageDigest;
+ try {
+ messageDigest = MessageDigest.getInstance("MD5");
+ } catch (NoSuchAlgorithmException e) {
+ Log.w(TAG, "Couldn't encode safety source id: " + safetySourceId, e);
+ return 0;
+ }
+ messageDigest.update(safetySourceId.getBytes());
+ return new BigInteger(messageDigest.digest()).longValue();
+ }
+
private String getSafetyCenterStatusTitle(
@SafetyCenterStatus.OverallSeverityLevel int overallSeverityLevel,
@NonNull List<SafetyCenterIssueWithCategory> safetyCenterIssuesWithCategories,
diff --git a/service/java/com/android/safetycenter/SafetyCenterService.java b/service/java/com/android/safetycenter/SafetyCenterService.java
index 8d201b163..e6d9944c1 100644
--- a/service/java/com/android/safetycenter/SafetyCenterService.java
+++ b/service/java/com/android/safetycenter/SafetyCenterService.java
@@ -24,6 +24,7 @@ import static android.safetycenter.SafetyCenterManager.REFRESH_REASON_OTHER;
import static android.safetycenter.SafetyCenterManager.RefreshReason;
import static android.safetycenter.SafetyEvent.SAFETY_EVENT_TYPE_RESOLVING_ACTION_FAILED;
+import static com.android.permission.PermissionStatsLog.SAFETY_STATE;
import static com.android.safetycenter.SafetyCenterFlags.PROPERTY_SAFETY_CENTER_ENABLED;
import static java.util.Objects.requireNonNull;
@@ -34,6 +35,9 @@ import android.annotation.UserIdInt;
import android.annotation.WorkerThread;
import android.app.AppOpsManager;
import android.app.PendingIntent;
+import android.app.StatsManager;
+import android.app.StatsManager.PullAtomMetadata;
+import android.app.StatsManager.StatsPullAtomCallback;
import android.content.ApexEnvironment;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -59,12 +63,14 @@ import android.safetycenter.SafetySourceIssue;
import android.safetycenter.config.SafetyCenterConfig;
import android.util.ArraySet;
import android.util.Log;
+import android.util.StatsEvent;
import androidx.annotation.Keep;
import androidx.annotation.RequiresApi;
import com.android.internal.annotations.GuardedBy;
import com.android.modules.utils.BackgroundThread;
+import com.android.permission.PermissionStatsLog;
import com.android.permission.util.ForegroundThread;
import com.android.permission.util.UserUtils;
import com.android.safetycenter.SafetyCenterConfigReader.Broadcast;
@@ -188,15 +194,31 @@ public final class SafetyCenterService extends SystemService {
@Override
public void onBootPhase(int phase) {
if (phase == SystemService.PHASE_BOOT_COMPLETED && canUseSafetyCenter()) {
- Executor foregroundThreadExecutor = ForegroundThread.getExecutor();
- SafetyCenterEnabledListener listener = new SafetyCenterEnabledListener();
- // Ensure the listener is called first with the current state on the same thread.
- foregroundThreadExecutor.execute(listener::setInitialState);
- DeviceConfig.addOnPropertiesChangedListener(
- DeviceConfig.NAMESPACE_PRIVACY, foregroundThreadExecutor, listener);
+ registerSafetyCenterEnabledListener();
+ registerSafetyCenterPullAtomCallback();
}
}
+ private void registerSafetyCenterEnabledListener() {
+ Executor foregroundThreadExecutor = ForegroundThread.getExecutor();
+ SafetyCenterEnabledListener listener = new SafetyCenterEnabledListener();
+ // Ensure the listener is called first with the current state on the same thread.
+ foregroundThreadExecutor.execute(listener::setInitialState);
+ DeviceConfig.addOnPropertiesChangedListener(
+ DeviceConfig.NAMESPACE_PRIVACY, foregroundThreadExecutor, listener);
+ }
+
+ private void registerSafetyCenterPullAtomCallback() {
+ StatsManager statsManager =
+ requireNonNull(getContext().getSystemService(StatsManager.class));
+ PullAtomMetadata defaultMetadata = null;
+ statsManager.setPullAtomCallback(
+ SAFETY_STATE,
+ defaultMetadata,
+ BackgroundThread.getExecutor(),
+ new SafetyCenterPullAtomCallback());
+ }
+
/** Service implementation of {@link ISafetyCenterManager.Stub}. */
private final class Stub extends ISafetyCenterManager.Stub {
@Override
@@ -767,6 +789,48 @@ public final class SafetyCenterService extends SystemService {
}
}
+ /**
+ * A {@link StatsPullAtomCallback} that provides data for {@link
+ * PermissionStatsLog#SAFETY_STATE} when called by the {@link StatsManager}.
+ */
+ private final class SafetyCenterPullAtomCallback implements StatsPullAtomCallback {
+
+ private SafetyCenterPullAtomCallback() {}
+
+ @Override
+ public int onPullAtom(int atomTag, @NonNull List<StatsEvent> statsEvents) {
+ if (atomTag != SAFETY_STATE) {
+ Log.w(
+ TAG,
+ "Attempt to pull atom: "
+ + atomTag
+ + ", but only SAFETY_STATE is supported");
+ return StatsManager.PULL_SKIP;
+ }
+ if (!SafetyCenterFlags.getSafetyCenterEnabled()) {
+ Log.w(TAG, "Attempt to pull SAFETY_STATE, but Safety Center is disabled");
+ return StatsManager.PULL_SKIP;
+ }
+ List<UserProfileGroup> userProfileGroups =
+ UserProfileGroup.getAllUserProfileGroups(getContext());
+ synchronized (mApiLock) {
+ if (mSafetyCenterConfigReader.isOverrideForTestsActive()) {
+ Log.i(TAG, "Pulling and writing atoms with a test config overrideā€¦");
+ // We only log this and proceed with the call here, as we still want to be able
+ // to assert the content of the logs in CTS tests. We may want to filter this
+ // out in the future if this turns out to skew the collected events.
+ } else {
+ Log.i(TAG, "Pulling and writing atomsā€¦");
+ }
+ for (int i = 0; i < userProfileGroups.size(); i++) {
+ mSafetyCenterDataTracker.pullAndWriteAtoms(
+ userProfileGroups.get(i), statsEvents);
+ }
+ }
+ return StatsManager.PULL_SUCCESS;
+ }
+ }
+
/** A {@link Runnable} that is called to signal a refresh timeout. */
private final class RefreshTimeout implements Runnable {