diff options
12 files changed, 345 insertions, 88 deletions
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/data/LauncherPackagesLiveData.kt b/PermissionController/src/com/android/permissioncontroller/permission/data/LauncherPackagesLiveData.kt index da0f26000..f0d811821 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/data/LauncherPackagesLiveData.kt +++ b/PermissionController/src/com/android/permissioncontroller/permission/data/LauncherPackagesLiveData.kt @@ -19,6 +19,7 @@ package com.android.permissioncontroller.permission.data import android.content.Intent import android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE import android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE +import android.content.pm.PackageManager.FEATURE_LEANBACK import com.android.permissioncontroller.PermissionControllerApplication import kotlinx.coroutines.Job @@ -31,14 +32,26 @@ object LauncherPackagesLiveData : SmartAsyncMediatorLiveData<Set<String>>(), private val LAUNCHER_INTENT = Intent(Intent.ACTION_MAIN, null) .addCategory(Intent.CATEGORY_LAUNCHER) + // On ATV some apps may have a leanback launcher icon but no regular launcher icon + private val LEANBACK_LAUNCHER_INTENT = Intent(Intent.ACTION_MAIN, null) + .addCategory(Intent.CATEGORY_LEANBACK_LAUNCHER) + override suspend fun loadDataAndPostValue(job: Job) { val launcherPkgs = mutableSetOf<String>() + + loadPkgsFromIntent(launcherPkgs, LAUNCHER_INTENT) + if (PermissionControllerApplication.get().packageManager + .hasSystemFeature(FEATURE_LEANBACK)) { + loadPkgsFromIntent(launcherPkgs, LEANBACK_LAUNCHER_INTENT) + } + postValue(launcherPkgs) + } + + private fun loadPkgsFromIntent(launcherPkgs: MutableSet<String>, intent: Intent) { for (info in PermissionControllerApplication.get().packageManager.queryIntentActivities( - LAUNCHER_INTENT, MATCH_DIRECT_BOOT_AWARE or MATCH_DIRECT_BOOT_UNAWARE)) { + intent, MATCH_DIRECT_BOOT_AWARE or MATCH_DIRECT_BOOT_UNAWARE)) { launcherPkgs.add(info.activityInfo.packageName) } - - postValue(launcherPkgs) } override fun onPackageUpdate(packageName: String) { diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/ManagePermissionsActivity.java b/PermissionController/src/com/android/permissioncontroller/permission/ui/ManagePermissionsActivity.java index ecf9c00ea..d83714422 100644 --- a/PermissionController/src/com/android/permissioncontroller/permission/ui/ManagePermissionsActivity.java +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/ManagePermissionsActivity.java @@ -66,6 +66,7 @@ import com.android.permissioncontroller.permission.ui.handheld.PermissionAppsFra import com.android.permissioncontroller.permission.ui.handheld.dashboard.PermissionDetailsWrapperFragment; import com.android.permissioncontroller.permission.ui.handheld.dashboard.PermissionUsageV2WrapperFragment; import com.android.permissioncontroller.permission.ui.legacy.AppPermissionActivity; +import com.android.permissioncontroller.permission.ui.television.TvUnusedAppsFragment; import com.android.permissioncontroller.permission.ui.wear.AppPermissionsFragmentWear; import com.android.permissioncontroller.permission.utils.KotlinUtils; import com.android.permissioncontroller.permission.utils.Utils; @@ -375,7 +376,10 @@ public final class ManagePermissionsActivity extends SettingsActivity { if (DeviceUtils.isAuto(this)) { androidXFragment = AutoUnusedAppsFragment.newInstance(); androidXFragment.setArguments(UnusedAppsFragment.createArgs(sessionId)); - } else if (DeviceUtils.isWear(this) || DeviceUtils.isTelevision(this)) { + } else if (DeviceUtils.isTelevision(this)) { + androidXFragment = TvUnusedAppsFragment.newInstance(); + androidXFragment.setArguments(UnusedAppsFragment.createArgs(sessionId)); + } else if (DeviceUtils.isWear(this)) { androidXFragment = HandheldUnusedAppsWrapperFragment.newInstance(); androidXFragment.setArguments(UnusedAppsFragment.createArgs(sessionId)); } else { diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/television/TvUnusedAppsFragment.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/television/TvUnusedAppsFragment.kt new file mode 100644 index 000000000..6caaa7420 --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/television/TvUnusedAppsFragment.kt @@ -0,0 +1,112 @@ +/* + * 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.permission.ui.television + +import android.app.Application +import android.os.Bundle +import android.os.UserHandle +import androidx.preference.Preference +import androidx.preference.PreferenceCategory +import com.android.permissioncontroller.R +import com.android.permissioncontroller.hibernation.isHibernationEnabled +import com.android.permissioncontroller.permission.ui.UnusedAppsFragment +import com.android.permissioncontroller.permission.ui.UnusedAppsFragment.Companion.INFO_MSG_CATEGORY + +/** + * TV wrapper, with customizations, around [UnusedAppsFragment]. + */ +class TvUnusedAppsFragment : SettingsWithHeader(), + UnusedAppsFragment.Parent<TvUnusedAppsPreference> { + + companion object { + private const val UNUSED_PREFERENCE_KEY = "unused_pref_row_key" + + /** Create a new instance of this fragment. */ + @JvmStatic + fun newInstance(): TvUnusedAppsFragment { + return TvUnusedAppsFragment() + } + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + // Preferences will be added via shared logic in [UnusedAppsFragment]. + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + if (savedInstanceState == null) { + val fragment: + UnusedAppsFragment<TvUnusedAppsFragment, TvUnusedAppsPreference> = + UnusedAppsFragment.newInstance() + fragment.arguments = arguments + // child fragment does not have its own UI - it will add to the preferences of this + // parent fragment + childFragmentManager.beginTransaction() + .add(fragment, null) + .commit() + } + } + + override fun createFooterPreference(): Preference { + val preference = Preference(context) + if (isHibernationEnabled()) { + preference.summary = getString(R.string.unused_apps_page_summary) + } else { + preference.summary = + getString(R.string.auto_revoked_apps_page_summary) + } + preference.setIcon(R.drawable.ic_info_outline) + preference.isSelectable = false + return preference + } + + override fun setLoadingState(loading: Boolean, animate: Boolean) { + setLoading(loading, animate) + } + + override fun createUnusedAppPref( + app: Application, + packageName: String, + user: UserHandle + ): TvUnusedAppsPreference { + return TvUnusedAppsPreference(app, packageName, user, requireContext()) + } + + override fun setTitle(title: CharSequence) { + setHeader(null, null, null, title) + } + + override fun setEmptyState(empty: Boolean) { + val infoMsgCategory = + preferenceScreen.findPreference<PreferenceCategory>(INFO_MSG_CATEGORY)!! + val noUnusedAppsPreference: Preference? = + infoMsgCategory.findPreference<Preference>(UNUSED_PREFERENCE_KEY) + if (empty && noUnusedAppsPreference == null) { + infoMsgCategory.addPreference(createNoUnusedAppsPreference()) + } else if (noUnusedAppsPreference != null) { + noUnusedAppsPreference.setVisible(empty) + } + } + + private fun createNoUnusedAppsPreference(): Preference { + val preference = Preference(context) + preference.title = getString(R.string.zero_unused_apps) + preference.key = UNUSED_PREFERENCE_KEY + preference.isSelectable = false + preference.order = 0 + return preference + } +} diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/television/TvUnusedAppsPreference.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/television/TvUnusedAppsPreference.kt new file mode 100644 index 000000000..23f4a4589 --- /dev/null +++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/television/TvUnusedAppsPreference.kt @@ -0,0 +1,49 @@ +/* + * 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.permission.ui.television + +import androidx.preference.Preference +import android.app.Application +import android.content.Context +import android.os.UserHandle +import com.android.permissioncontroller.permission.ui.RemovablePref +import com.android.permissioncontroller.permission.utils.KotlinUtils + +/** + * A TV-styled preference which represents an app that has been auto revoked. Has the app icon and + * label, as well as a button to open the app. + * + * @param app The current application + * @param packageName The name of the package whose icon this preference will retrieve + * @param user The user whose package icon will be retrieved + * @param context The current context + */ +class TvUnusedAppsPreference( + app: Application, + packageName: String, + user: UserHandle, + context: Context +) : Preference(context), RemovablePref { + + init { + icon = KotlinUtils.getBadgedPackageIcon(app, packageName, user) + } + + override fun setRemoveClickRunnable(runnable: Runnable) { + // TV Settings don't have secondary icons and actions + } +} diff --git a/PermissionController/src/com/android/permissioncontroller/role/RolePermissionProtection.md b/PermissionController/src/com/android/permissioncontroller/role/RolePermissionProtection.md index 975840aaa..f632332f3 100644 --- a/PermissionController/src/com/android/permissioncontroller/role/RolePermissionProtection.md +++ b/PermissionController/src/com/android/permissioncontroller/role/RolePermissionProtection.md @@ -93,8 +93,8 @@ overlay. Since role is modularized, we also need to make this config resource a system API for access by role. -Edit `frameworks/base/core/res/res/values/public.xml` to expose the new config resource as a system -API: +Edit `frameworks/base/core/res/res/values/public-staging.xml` to expose the new config resource as +a system API: ```xml <staging-public-group type="string" first-id="0xXXXXXXXX"> diff --git a/framework-s/api/system-current.txt b/framework-s/api/system-current.txt index 42091733e..ecf36e9c0 100644 --- a/framework-s/api/system-current.txt +++ b/framework-s/api/system-current.txt @@ -170,7 +170,7 @@ package android.safetycenter { method @NonNull public android.app.PendingIntent getPendingIntent(); method @Nullable public CharSequence getSuccessMessage(); method public boolean isInFlight(); - method public boolean isResolving(); + method public boolean willResolve(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.safetycenter.SafetyCenterIssue.Action> CREATOR; } @@ -178,11 +178,11 @@ package android.safetycenter { public static final class SafetyCenterIssue.Action.Builder { ctor public SafetyCenterIssue.Action.Builder(@NonNull String); method @NonNull public android.safetycenter.SafetyCenterIssue.Action build(); - method @NonNull public android.safetycenter.SafetyCenterIssue.Action.Builder setInFlight(boolean); + method @NonNull public android.safetycenter.SafetyCenterIssue.Action.Builder setIsInFlight(boolean); method @NonNull public android.safetycenter.SafetyCenterIssue.Action.Builder setLabel(@NonNull CharSequence); method @NonNull public android.safetycenter.SafetyCenterIssue.Action.Builder setPendingIntent(@NonNull android.app.PendingIntent); - method @NonNull public android.safetycenter.SafetyCenterIssue.Action.Builder setResolving(boolean); method @NonNull public android.safetycenter.SafetyCenterIssue.Action.Builder setSuccessMessage(@Nullable CharSequence); + method @NonNull public android.safetycenter.SafetyCenterIssue.Action.Builder setWillResolve(boolean); } public static final class SafetyCenterIssue.Builder { diff --git a/framework-s/java/android/safetycenter/SafetyCenterIssue.java b/framework-s/java/android/safetycenter/SafetyCenterIssue.java index 9f5b83c55..3371bc852 100644 --- a/framework-s/java/android/safetycenter/SafetyCenterIssue.java +++ b/framework-s/java/android/safetycenter/SafetyCenterIssue.java @@ -23,6 +23,7 @@ import static java.util.Objects.requireNonNull; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.app.PendingIntent; import android.os.Parcel; @@ -370,7 +371,7 @@ public final class SafetyCenterIssue implements Parcelable { private final CharSequence mLabel; @NonNull private final PendingIntent mPendingIntent; - private final boolean mResolving; + private final boolean mWillResolve; private final boolean mInFlight; @Nullable private final CharSequence mSuccessMessage; @@ -379,13 +380,13 @@ public final class SafetyCenterIssue implements Parcelable { @NonNull String id, @NonNull CharSequence label, @NonNull PendingIntent pendingIntent, - boolean resolving, + boolean willResolve, boolean inFlight, @Nullable CharSequence successMessage) { mId = requireNonNull(id); mLabel = requireNonNull(label); mPendingIntent = requireNonNull(pendingIntent); - mResolving = resolving; + mWillResolve = willResolve; mInFlight = inFlight; mSuccessMessage = successMessage; } @@ -410,14 +411,18 @@ public final class SafetyCenterIssue implements Parcelable { /** * Returns whether invoking this action will fix or address the issue sufficiently for it - * to be considered resolved i.e. the issue will no longer need to be conveyed to the user - * in the UI. + * to be considered resolved (i.e. the issue will no longer need to be conveyed to the user + * in the UI). */ - public boolean isResolving() { - return mResolving; + public boolean willResolve() { + return mWillResolve; } - /** Returns whether or not this action is currently being executed. */ + /** + * Returns whether or not this action is currently being executed (i.e. the user clicked + * on a button that triggered this action, and now the Safety Center is waiting for the + * action's result). + */ public boolean isInFlight() { return mInFlight; } @@ -439,7 +444,7 @@ public final class SafetyCenterIssue implements Parcelable { return Objects.equals(mId, action.mId) && TextUtils.equals(mLabel, action.mLabel) && Objects.equals(mPendingIntent, action.mPendingIntent) - && mResolving == action.mResolving + && mWillResolve == action.mWillResolve && mInFlight == action.mInFlight && TextUtils.equals(mSuccessMessage, action.mSuccessMessage); } @@ -447,7 +452,7 @@ public final class SafetyCenterIssue implements Parcelable { @Override public int hashCode() { return Objects.hash( - mId, mLabel, mSuccessMessage, mResolving, mInFlight, mPendingIntent); + mId, mLabel, mSuccessMessage, mWillResolve, mInFlight, mPendingIntent); } @Override @@ -456,7 +461,7 @@ public final class SafetyCenterIssue implements Parcelable { + "mId=" + mId + ", mLabel=" + mLabel + ", mPendingIntent=" + mPendingIntent - + ", mResolving=" + mResolving + + ", mWillResolve=" + mWillResolve + ", mInFlight=" + mInFlight + ", mSuccessMessage=" + mSuccessMessage + '}'; @@ -472,7 +477,7 @@ public final class SafetyCenterIssue implements Parcelable { dest.writeString(mId); TextUtils.writeToParcel(mLabel, dest, flags); dest.writeParcelable(mPendingIntent, flags); - dest.writeBoolean(mResolving); + dest.writeBoolean(mWillResolve); dest.writeBoolean(mInFlight); TextUtils.writeToParcel(mSuccessMessage, dest, flags); } @@ -486,8 +491,8 @@ public final class SafetyCenterIssue implements Parcelable { .setPendingIntent( in.readParcelable( PendingIntent.class.getClassLoader(), PendingIntent.class)) - .setResolving(in.readBoolean()) - .setInFlight(in.readBoolean()) + .setWillResolve(in.readBoolean()) + .setIsInFlight(in.readBoolean()) .setSuccessMessage(TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in)) .build(); } @@ -503,7 +508,7 @@ public final class SafetyCenterIssue implements Parcelable { private String mId; private CharSequence mLabel; private PendingIntent mPendingIntent; - private boolean mResolving; + private boolean mWillResolve; private boolean mInFlight; private CharSequence mSuccessMessage; @@ -529,23 +534,28 @@ public final class SafetyCenterIssue implements Parcelable { } /** - * Sets whether or not this action is resolving. Defaults to false. + * Sets whether or not this action will resolve the issue when executed. Defaults to + * false. * - * @see #isResolving() + * @see #willResolve() */ + @SuppressLint("MissingGetterMatchingBuilder") @NonNull - public Builder setResolving(boolean resolving) { - mResolving = resolving; + public Builder setWillResolve(boolean willResolve) { + mWillResolve = willResolve; return this; } /** - * Sets whether or not this action is in flight. Defaults to false. + * Sets a boolean that indicates whether or not this action is currently being executed + * (i.e. the user clicked on a button that triggered this action, and now the Safety + * Center is waiting for the action's result). Defaults to false. * * @see #isInFlight() */ + @SuppressLint("MissingGetterMatchingBuilder") @NonNull - public Builder setInFlight(boolean inFlight) { + public Builder setIsInFlight(boolean inFlight) { mInFlight = inFlight; return this; } @@ -564,7 +574,7 @@ public final class SafetyCenterIssue implements Parcelable { @NonNull public Action build() { return new Action( - mId, mLabel, mPendingIntent, mResolving, mInFlight, mSuccessMessage); + mId, mLabel, mPendingIntent, mWillResolve, mInFlight, mSuccessMessage); } } } diff --git a/service/java/com/android/safetycenter/SafetyCenterListeners.java b/service/java/com/android/safetycenter/SafetyCenterListeners.java index 184d23429..f1bb012e3 100644 --- a/service/java/com/android/safetycenter/SafetyCenterListeners.java +++ b/service/java/com/android/safetycenter/SafetyCenterListeners.java @@ -21,11 +21,11 @@ import static android.os.Build.VERSION_CODES.TIRAMISU; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; -import android.os.Binder; import android.os.RemoteCallbackList; import android.os.RemoteException; import android.safetycenter.IOnSafetyCenterDataChangedListener; import android.safetycenter.SafetyCenterData; +import android.safetycenter.SafetyCenterErrorDetails; import android.util.Log; import android.util.SparseArray; @@ -56,13 +56,10 @@ final class SafetyCenterListeners { static void deliverUpdate( @NonNull IOnSafetyCenterDataChangedListener listener, @NonNull SafetyCenterData safetyCenterData) { - final long identity = Binder.clearCallingIdentity(); try { listener.onSafetyCenterDataChanged(safetyCenterData); } catch (RemoteException e) { - Log.e(TAG, "Error delivering SafetyCenterData update to listener", e); - } finally { - Binder.restoreCallingIdentity(identity); + Log.e(TAG, "Error delivering SafetyCenterData to listener", e); } } @@ -85,6 +82,39 @@ final class SafetyCenterListeners { listeners.finishBroadcast(); } + /** + * Delivers a {@link SafetyCenterErrorDetails} update to a single {@link + * IOnSafetyCenterDataChangedListener}. + */ + private static void deliverError( + @NonNull IOnSafetyCenterDataChangedListener listener, + @NonNull SafetyCenterErrorDetails safetyCenterErrorDetails) { + try { + listener.onError(safetyCenterErrorDetails); + } catch (RemoteException e) { + Log.e(TAG, "Error delivering SafetyCenterErrorDetails to listener", e); + } + } + + /** + * Delivers a {@link SafetyCenterErrorDetails} update to a {@link RemoteCallbackList} of {@link + * IOnSafetyCenterDataChangedListener}. + * + * <p>Registering or unregistering {@link IOnSafetyCenterDataChangedListener} on the underlying + * {@link RemoteCallbackList} on another thread while an update is happening is safe as this is + * handled by the {@link RemoteCallbackList} already (as well as listeners death). + */ + static void deliverError( + @NonNull RemoteCallbackList<IOnSafetyCenterDataChangedListener> listeners, + @NonNull SafetyCenterErrorDetails safetyCenterErrorDetails) { + int i = listeners.beginBroadcast(); + while (i > 0) { + i--; + deliverError(listeners.getBroadcastItem(i), safetyCenterErrorDetails); + } + listeners.finishBroadcast(); + } + /** Adds a {@link IOnSafetyCenterDataChangedListener} for the given {@code userId}. */ void addListener( @NonNull IOnSafetyCenterDataChangedListener listener, diff --git a/service/java/com/android/safetycenter/SafetyCenterService.java b/service/java/com/android/safetycenter/SafetyCenterService.java index cea0ae404..6096925b8 100644 --- a/service/java/com/android/safetycenter/SafetyCenterService.java +++ b/service/java/com/android/safetycenter/SafetyCenterService.java @@ -39,6 +39,7 @@ import android.provider.DeviceConfig; import android.safetycenter.IOnSafetyCenterDataChangedListener; import android.safetycenter.ISafetyCenterManager; import android.safetycenter.SafetyCenterData; +import android.safetycenter.SafetyCenterErrorDetails; import android.safetycenter.SafetyEvent; import android.safetycenter.SafetySourceData; import android.safetycenter.SafetySourceErrorDetails; @@ -194,7 +195,14 @@ public final class SafetyCenterService extends SystemService { if (!checkApiEnabled("reportSafetySourceError")) { return; } + // TODO(b/218379298): Add implementation + RemoteCallbackList<IOnSafetyCenterDataChangedListener> listeners; + synchronized (mApiLock) { + listeners = mSafetyCenterListeners.getListeners(userId); + } + + SafetyCenterListeners.deliverError(listeners, new SafetyCenterErrorDetails("Error")); } @Override diff --git a/tests/cts/safetycenter/src/android/safetycenter/cts/SafetyCenterIssueTest.kt b/tests/cts/safetycenter/src/android/safetycenter/cts/SafetyCenterIssueTest.kt index a8428049c..819124611 100644 --- a/tests/cts/safetycenter/src/android/safetycenter/cts/SafetyCenterIssueTest.kt +++ b/tests/cts/safetycenter/src/android/safetycenter/cts/SafetyCenterIssueTest.kt @@ -48,15 +48,15 @@ class SafetyCenterIssueTest { val action1 = SafetyCenterIssue.Action.Builder("action_id_1") .setLabel("an action") .setPendingIntent(pendingIntent1) - .setResolving(true) - .setInFlight(true) + .setWillResolve(true) + .setIsInFlight(true) .setSuccessMessage("a success message") .build() val action2 = SafetyCenterIssue.Action.Builder("action_id_2") .setLabel("another action") .setPendingIntent(pendingIntent2) - .setResolving(false) - .setInFlight(false) + .setWillResolve(false) + .setIsInFlight(false) .build() val issue1 = SafetyCenterIssue.Builder("issue_id") @@ -330,9 +330,9 @@ class SafetyCenterIssueTest { } @Test - fun action_isResolving_returnsIsResolving() { - assertThat(action1.isResolving).isTrue() - assertThat(action2.isResolving).isFalse() + fun action_willResolve_returnsWillResolve() { + assertThat(action1.willResolve()).isTrue() + assertThat(action2.willResolve()).isFalse() } @Test @@ -377,15 +377,15 @@ class SafetyCenterIssueTest { val action = SafetyCenterIssue.Action.Builder("an_id") .setLabel("a label") .setPendingIntent(pendingIntent1) - .setResolving(true) - .setInFlight(true) + .setWillResolve(true) + .setIsInFlight(true) .setSuccessMessage("a success message") .build() val equivalentAction = SafetyCenterIssue.Action.Builder("an_id") .setLabel("a label") .setPendingIntent(pendingIntent1) - .setResolving(true) - .setInFlight(true) + .setWillResolve(true) + .setIsInFlight(true) .setSuccessMessage("a success message") .build() @@ -445,17 +445,17 @@ class SafetyCenterIssueTest { } @Test - fun action_equals_toString_differentResovlingValues_areNotEqual() { + fun action_equals_toString_differentWillResolveValues_areNotEqual() { val action = SafetyCenterIssue.Action.Builder("an_id") .setLabel("a label") .setPendingIntent(pendingIntent1) - .setResolving(true) + .setWillResolve(true) .setSuccessMessage("a success message") .build() val differentAction = SafetyCenterIssue.Action.Builder("an_id") .setLabel("a label") .setPendingIntent(pendingIntent1) - .setResolving(false) + .setWillResolve(false) .setSuccessMessage("a success message") .build() @@ -468,15 +468,15 @@ class SafetyCenterIssueTest { val action = SafetyCenterIssue.Action.Builder("an_id") .setLabel("a label") .setPendingIntent(pendingIntent1) - .setResolving(true) - .setInFlight(true) + .setWillResolve(true) + .setIsInFlight(true) .setSuccessMessage("a success message") .build() val differentAction = SafetyCenterIssue.Action.Builder("an_id") .setLabel("a label") .setPendingIntent(pendingIntent1) - .setResolving(true) - .setInFlight(false) + .setWillResolve(true) + .setIsInFlight(false) .setSuccessMessage("a success message") .build() diff --git a/tests/cts/safetycenter/src/android/safetycenter/cts/SafetyCenterManagerTest.kt b/tests/cts/safetycenter/src/android/safetycenter/cts/SafetyCenterManagerTest.kt index a574a7892..af785eecf 100644 --- a/tests/cts/safetycenter/src/android/safetycenter/cts/SafetyCenterManagerTest.kt +++ b/tests/cts/safetycenter/src/android/safetycenter/cts/SafetyCenterManagerTest.kt @@ -25,6 +25,7 @@ import android.content.Intent import android.content.Intent.ACTION_SAFETY_CENTER import android.os.Build.VERSION_CODES.TIRAMISU import android.safetycenter.SafetyCenterData +import android.safetycenter.SafetyCenterErrorDetails import android.safetycenter.SafetyCenterManager import android.safetycenter.SafetyCenterManager.OnSafetyCenterDataChangedListener import android.safetycenter.SafetyCenterManager.REFRESH_REASON_PAGE_OPEN @@ -56,6 +57,7 @@ import android.safetycenter.testing.getSafetySourceDataWithPermission import android.safetycenter.testing.isSafetyCenterEnabledWithPermission import android.safetycenter.testing.refreshSafetySourcesWithPermission import android.safetycenter.testing.removeOnSafetyCenterDataChangedListenerWithPermission +import android.safetycenter.testing.reportSafetySourceErrorWithPermission import android.safetycenter.testing.setSafetyCenterConfigOverrideWithPermission import android.safetycenter.testing.setSafetySourceDataWithPermission import androidx.test.core.app.ApplicationProvider.getApplicationContext @@ -135,13 +137,28 @@ class SafetyCenterManagerTest { .build() ) .build() - private val listenerChannel = Channel<SafetyCenterData>() + private val listener = object : OnSafetyCenterDataChangedListener { + private val dataChannel = Channel<SafetyCenterData>() + private val errorChannel = Channel<SafetyCenterErrorDetails>() - // The lambda has to be wrapped to the right type because kotlin wraps lambdas in a new Java - // functional interface implementation each time they are referenced/cast to a Java interface: - // b/215569072. - private val listener = OnSafetyCenterDataChangedListener { - runBlockingWithTimeout { listenerChannel.send(it) } + override fun onSafetyCenterDataChanged(data: SafetyCenterData) { + runBlockingWithTimeout { dataChannel.send(data) } + } + + override fun onError(errorDetails: SafetyCenterErrorDetails) { + runBlockingWithTimeout { errorChannel.send(errorDetails) } + } + + fun receiveSafetyCenterData(timeout: Duration = TIMEOUT_LONG) = + runBlockingWithTimeout(timeout) { dataChannel.receive() } + + fun receiveSafetyCenterErrorDetails(timeout: Duration = TIMEOUT_LONG) = + runBlockingWithTimeout(timeout) { errorChannel.receive() } + + fun cancelChannels() { + dataChannel.cancel() + errorChannel.cancel() + } } @Before @@ -160,8 +177,8 @@ class SafetyCenterManagerTest { } @After - fun cancelChannelAfterTest() { - listenerChannel.cancel() + fun cancelChannelsAfterTest() { + listener.cancelChannels() } @Test @@ -284,6 +301,23 @@ class SafetyCenterManagerTest { } @Test + fun reportSafetySourceError_callsErrorListener() { + safetyCenterManager.addOnSafetyCenterDataChangedListenerWithPermission( + directExecutor(), + listener + ) + + safetyCenterManager.reportSafetySourceErrorWithPermission( + CTS_SOURCE_ID, + SafetySourceErrorDetails(EVENT_SOURCE_STATE_CHANGED) + ) + val safetyCenterErrorDetailsFromListener = listener.receiveSafetyCenterErrorDetails() + + assertThat(safetyCenterErrorDetailsFromListener).isEqualTo( + SafetyCenterErrorDetails("Error")) + } + + @Test fun reportSafetySourceError_withoutPermission_throwsSecurityException() { assertFailsWith(SecurityException::class) { safetyCenterManager.reportSafetySourceError( @@ -448,7 +482,7 @@ class SafetyCenterManagerTest { directExecutor(), listener ) - val safetyCenterDataFromListener = receiveListenerUpdate() + val safetyCenterDataFromListener = listener.receiveSafetyCenterData() // TODO(b/218830137): Assert on content. assertThat(safetyCenterDataFromListener).isNotNull() @@ -462,14 +496,14 @@ class SafetyCenterManagerTest { listener ) // Receive initial data. - receiveListenerUpdate() + listener.receiveSafetyCenterData() safetyCenterManager.setSafetySourceDataWithPermission( CTS_SOURCE_ID, safetySourceDataOk, EVENT_SOURCE_STATE_CHANGED ) - val safetyCenterDataFromListener = receiveListenerUpdate() + val safetyCenterDataFromListener = listener.receiveSafetyCenterData() // TODO(b/218830137): Assert on content. assertThat(safetyCenterDataFromListener).isNotNull() @@ -483,21 +517,21 @@ class SafetyCenterManagerTest { listener ) // Receive initial data. - receiveListenerUpdate() + listener.receiveSafetyCenterData() safetyCenterManager.setSafetySourceDataWithPermission( CTS_SOURCE_ID, safetySourceDataOk, EVENT_SOURCE_STATE_CHANGED ) // Receive update from #setSafetySourceData call. - receiveListenerUpdate() + listener.receiveSafetyCenterData() safetyCenterManager.setSafetySourceDataWithPermission( CTS_SOURCE_ID, safetySourceDataCritical, EVENT_SOURCE_STATE_CHANGED ) - val safetyCenterDataFromListener = receiveListenerUpdate() + val safetyCenterDataFromListener = listener.receiveSafetyCenterData() // TODO(b/218830137): Assert on content. assertThat(safetyCenterDataFromListener).isNotNull() @@ -511,21 +545,21 @@ class SafetyCenterManagerTest { listener ) // Receive initial data. - receiveListenerUpdate() + listener.receiveSafetyCenterData() safetyCenterManager.setSafetySourceDataWithPermission( CTS_SOURCE_ID, safetySourceDataOk, EVENT_SOURCE_STATE_CHANGED ) // Receive update from #setSafetySourceData call. - receiveListenerUpdate() + listener.receiveSafetyCenterData() safetyCenterManager.setSafetySourceDataWithPermission( CTS_SOURCE_ID, safetySourceData = null, EVENT_SOURCE_STATE_CHANGED ) - val safetyCenterDataFromListener = receiveListenerUpdate() + val safetyCenterDataFromListener = listener.receiveSafetyCenterData() // TODO(b/218830137): Assert on content. assertThat(safetyCenterDataFromListener).isNotNull() @@ -539,7 +573,7 @@ class SafetyCenterManagerTest { listener ) // Receive initial data. - receiveListenerUpdate() + listener.receiveSafetyCenterData() safetyCenterManager.setSafetySourceDataWithPermission( CTS_SOURCE_ID, @@ -548,7 +582,7 @@ class SafetyCenterManagerTest { ) assertFailsWith(TimeoutCancellationException::class) { - receiveListenerUpdate(TIMEOUT_SHORT) + listener.receiveSafetyCenterData(TIMEOUT_SHORT) } } @@ -560,14 +594,14 @@ class SafetyCenterManagerTest { listener ) // Receive initial data. - receiveListenerUpdate() + listener.receiveSafetyCenterData() safetyCenterManager.setSafetySourceDataWithPermission( CTS_SOURCE_ID, safetySourceDataOk, EVENT_SOURCE_STATE_CHANGED ) // Receive update from #setSafetySourceData call. - receiveListenerUpdate() + listener.receiveSafetyCenterData() safetyCenterManager.setSafetySourceDataWithPermission( CTS_SOURCE_ID, @@ -576,7 +610,7 @@ class SafetyCenterManagerTest { ) assertFailsWith(TimeoutCancellationException::class) { - receiveListenerUpdate(TIMEOUT_SHORT) + listener.receiveSafetyCenterData(TIMEOUT_SHORT) } } @@ -591,7 +625,7 @@ class SafetyCenterManagerTest { ) assertFailsWith(TimeoutCancellationException::class) { - receiveListenerUpdate(TIMEOUT_SHORT) + listener.receiveSafetyCenterData(TIMEOUT_SHORT) } } @@ -608,7 +642,7 @@ class SafetyCenterManagerTest { object : OnSafetyCenterDataChangedListener { override fun onSafetyCenterDataChanged(safetyCenterData: SafetyCenterData) { safetyCenterManager.removeOnSafetyCenterDataChangedListenerWithPermission(this) - runBlockingWithTimeout { listenerChannel.send(safetyCenterData) } + listener.onSafetyCenterDataChanged(safetyCenterData) } } safetyCenterManager.addOnSafetyCenterDataChangedListenerWithPermission( @@ -619,7 +653,7 @@ class SafetyCenterManagerTest { // Check that we don't deadlock when using a one-shot listener: this is because adding the // listener could call the listener while holding a lock on the binder thread-pool; causing // a deadlock when attempting to call the `SafetyCenterManager` from that listener. - receiveListenerUpdate() + listener.receiveSafetyCenterData() } @Test @@ -630,7 +664,7 @@ class SafetyCenterManagerTest { listener ) // Receive initial data. - receiveListenerUpdate() + listener.receiveSafetyCenterData() safetyCenterManager.removeOnSafetyCenterDataChangedListenerWithPermission(listener) safetyCenterManager.setSafetySourceDataWithPermission( @@ -640,7 +674,7 @@ class SafetyCenterManagerTest { ) assertFailsWith(TimeoutCancellationException::class) { - receiveListenerUpdate(TIMEOUT_SHORT) + listener.receiveSafetyCenterData(TIMEOUT_SHORT) } } @@ -703,9 +737,6 @@ class SafetyCenterManagerTest { ) } - private fun receiveListenerUpdate(timeout: Duration = TIMEOUT_LONG): SafetyCenterData = - runBlockingWithTimeout(timeout) { listenerChannel.receive() } - private fun <T> runBlockingWithTimeout( timeout: Duration = TIMEOUT_LONG, block: suspend () -> T diff --git a/tests/cts/safetycenter/src/android/safetycenter/testing/SafetyCenterApisWithShellPermissions.kt b/tests/cts/safetycenter/src/android/safetycenter/testing/SafetyCenterApisWithShellPermissions.kt index 153390492..b874b8219 100644 --- a/tests/cts/safetycenter/src/android/safetycenter/testing/SafetyCenterApisWithShellPermissions.kt +++ b/tests/cts/safetycenter/src/android/safetycenter/testing/SafetyCenterApisWithShellPermissions.kt @@ -57,7 +57,7 @@ fun SafetyCenterManager.getSafetySourceDataWithPermission(id: String) = callWithShellPermissionIdentity({ getSafetySourceData(id) }, SEND_SAFETY_CENTER_UPDATE) /** - * Calls [SafetyCenterManager.reportSafetySourceError] adopting Shell's [MANAGE_SAFETY_CENTER] + * Calls [SafetyCenterManager.reportSafetySourceError] adopting Shell's [SEND_SAFETY_CENTER_UPDATE] * permission. */ fun SafetyCenterManager.reportSafetySourceErrorWithPermission( @@ -66,7 +66,7 @@ fun SafetyCenterManager.reportSafetySourceErrorWithPermission( ) = callWithShellPermissionIdentity( { reportSafetySourceError(safetySourceId, safetySourceErrorDetails) }, - MANAGE_SAFETY_CENTER + SEND_SAFETY_CENTER_UPDATE ) /** @@ -112,7 +112,7 @@ fun SafetyCenterManager.removeOnSafetyCenterDataChangedListenerWithPermission( * Calls [SafetyCenterManager.dismissSafetyCenterIssue] adopting Shell's [MANAGE_SAFETY_CENTER] * permission. */ -fun SafetyCenterManager.dismissSafetyIssueWithPermission(safetyCenterIssueId: String) = +fun SafetyCenterManager.dismissSafetyCenterIssueWithPermission(safetyCenterIssueId: String) = callWithShellPermissionIdentity({ dismissSafetyCenterIssue(safetyCenterIssueId) }, MANAGE_SAFETY_CENTER) @@ -120,7 +120,7 @@ fun SafetyCenterManager.dismissSafetyIssueWithPermission(safetyCenterIssueId: St * Calls [SafetyCenterManager.executeSafetyCenterIssueAction] adopting Shell's * [MANAGE_SAFETY_CENTER] permission. */ -fun SafetyCenterManager.executeSafetyCenterActionWithPermission( +fun SafetyCenterManager.executeSafetyCenterIssueActionWithPermission( safetyCenterIssueId: String, safetyCenterIssueActionId: String ) = |