diff options
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 { |