From 389cb6f54a5a5bb8dea540f57a3a8ac3c3c1c758 Mon Sep 17 00:00:00 2001 From: Suprabh Shukla Date: Mon, 1 Oct 2018 18:20:39 -0700 Subject: [PATCH] Suspending app can customize intercepting dialog The suspending app has more context about why a particular app was suspended by the user, but we do not want to delegate the interception of the suspended activity out of the system. Hence allowing it further customizations to the dialog to make it clearer. Test: atest com.android.server.pm.SuspendDialogInfoTest \ com.android.server.pm.SuspendPackagesTest \ com.android.server.pm.PackageUserStateTest \ com.android.server.pm.PackageManagerSettingsTests \ com.android.server.am.ActivityStartInterceptorTest atest GtsSuspendAppsPermissionTestCases GtsSuspendAppsTestCases Bug: 112486945 Bug: 113150060 Change-Id: If9f4d14587a2b75bb572e7984a90e300a2c72d16 --- api/system-current.txt | 19 +- .../app/ApplicationPackageManager.java | 14 +- .../android/content/pm/IPackageManager.aidl | 3 +- .../android/content/pm/PackageManager.java | 69 +++- .../content/pm/PackageManagerInternal.java | 9 +- .../android/content/pm/PackageUserState.java | 8 +- .../android/content/pm/SuspendDialogInfo.aidl | 18 + .../android/content/pm/SuspendDialogInfo.java | 379 ++++++++++++++++++ .../internal/app/SuspendedAppActivity.java | 105 ++++- .../appwidget/AppWidgetServiceImpl.java | 7 +- .../server/am/ActivityStartInterceptor.java | 5 +- .../server/pm/PackageManagerService.java | 13 +- .../server/pm/PackageManagerShellCommand.java | 12 +- .../android/server/pm/PackageSettingBase.java | 9 +- .../java/com/android/server/pm/Settings.java | 38 +- .../am/ActivityStartInterceptorTest.java | 14 +- .../pm/PackageManagerSettingsTests.java | 21 +- .../server/pm/PackageUserStateTest.java | 23 +- .../server/pm/SuspendDialogInfoTest.java | 116 ++++++ .../server/pm/SuspendPackagesTest.java | 15 +- test-mock/api/system-current.txt | 1 + 21 files changed, 812 insertions(+), 86 deletions(-) create mode 100644 core/java/android/content/pm/SuspendDialogInfo.aidl create mode 100644 core/java/android/content/pm/SuspendDialogInfo.java create mode 100644 services/tests/servicestests/src/com/android/server/pm/SuspendDialogInfoTest.java diff --git a/api/system-current.txt b/api/system-current.txt index ac8fc9c23cb81..8a522a1c33706 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -1150,7 +1150,8 @@ package android.content.pm { method public abstract void revokeRuntimePermission(java.lang.String, java.lang.String, android.os.UserHandle); method public abstract boolean setDefaultBrowserPackageNameAsUser(java.lang.String, int); method public void setHarmfulAppWarning(java.lang.String, java.lang.CharSequence); - method public java.lang.String[] setPackagesSuspended(java.lang.String[], boolean, android.os.PersistableBundle, android.os.PersistableBundle, java.lang.String); + method public deprecated java.lang.String[] setPackagesSuspended(java.lang.String[], boolean, android.os.PersistableBundle, android.os.PersistableBundle, java.lang.String); + method public java.lang.String[] setPackagesSuspended(java.lang.String[], boolean, android.os.PersistableBundle, android.os.PersistableBundle, android.content.pm.SuspendDialogInfo); method public abstract void setUpdateAvailable(java.lang.String, boolean); method public abstract boolean updateIntentVerificationStatusAsUser(java.lang.String, int, int); method public abstract void updatePermissionFlags(java.lang.String, java.lang.String, int, int, android.os.UserHandle); @@ -1244,6 +1245,22 @@ package android.content.pm { field public int requestRes; } + public final class SuspendDialogInfo implements android.os.Parcelable { + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + } + + public static final class SuspendDialogInfo.Builder { + ctor public SuspendDialogInfo.Builder(); + method public android.content.pm.SuspendDialogInfo build(); + method public android.content.pm.SuspendDialogInfo.Builder setIcon(int); + method public android.content.pm.SuspendDialogInfo.Builder setMessage(java.lang.String); + method public android.content.pm.SuspendDialogInfo.Builder setMessage(int); + method public android.content.pm.SuspendDialogInfo.Builder setNeutralButtonText(int); + method public android.content.pm.SuspendDialogInfo.Builder setTitle(int); + } + } package android.content.pm.dex { diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index 264029b6ace77..fcd9a05112652 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -55,6 +55,7 @@ import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.pm.SharedLibraryInfo; +import android.content.pm.SuspendDialogInfo; import android.content.pm.VerifierDeviceIdentity; import android.content.pm.VersionedPackage; import android.content.pm.dex.ArtManager; @@ -85,6 +86,7 @@ import android.system.Os; import android.system.OsConstants; import android.system.StructStat; +import android.text.TextUtils; import android.util.ArrayMap; import android.util.IconDrawableFactory; import android.util.LauncherIcons; @@ -2255,9 +2257,19 @@ public void freeStorage(String volumeUuid, long freeStorageSize, IntentSender pi public String[] setPackagesSuspended(String[] packageNames, boolean suspended, PersistableBundle appExtras, PersistableBundle launcherExtras, String dialogMessage) { + final SuspendDialogInfo dialogInfo = !TextUtils.isEmpty(dialogMessage) + ? new SuspendDialogInfo.Builder().setMessage(dialogMessage).build() + : null; + return setPackagesSuspended(packageNames, suspended, appExtras, launcherExtras, dialogInfo); + } + + @Override + public String[] setPackagesSuspended(String[] packageNames, boolean suspended, + PersistableBundle appExtras, PersistableBundle launcherExtras, + SuspendDialogInfo dialogInfo) { try { return mPM.setPackagesSuspendedAsUser(packageNames, suspended, appExtras, - launcherExtras, dialogMessage, mContext.getOpPackageName(), + launcherExtras, dialogInfo, mContext.getOpPackageName(), getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl index 6a20c9349e1d5..4a4de5160e807 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -43,6 +43,7 @@ import android.content.pm.PermissionGroupInfo; import android.content.pm.PermissionInfo; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; +import android.content.pm.SuspendDialogInfo; import android.content.pm.UserInfo; import android.content.pm.VerifierDeviceIdentity; import android.content.pm.VersionedPackage; @@ -273,7 +274,7 @@ interface IPackageManager { String[] setPackagesSuspendedAsUser(in String[] packageNames, boolean suspended, in PersistableBundle appExtras, in PersistableBundle launcherExtras, - String dialogMessage, String callingPackage, int userId); + in SuspendDialogInfo dialogInfo, String callingPackage, int userId); boolean isPackageSuspendedForUser(String packageName, int userId); diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 3032d164ef46a..7e61a2504eea0 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -5663,7 +5663,7 @@ public abstract boolean getApplicationHiddenSettingAsUser(String packageName, * {@link Manifest.permission#MANAGE_USERS} to use this api.

* * @param packageNames The names of the packages to set the suspended status. - * @param suspended If set to {@code true} than the packages will be suspended, if set to + * @param suspended If set to {@code true}, the packages will be suspended, if set to * {@code false}, the packages will be unsuspended. * @param appExtras An optional {@link PersistableBundle} that the suspending app can provide * which will be shared with the apps being suspended. Ignored if @@ -5675,15 +5675,76 @@ public abstract boolean getApplicationHiddenSettingAsUser(String packageName, * suspended app. * * @return an array of package names for which the suspended status could not be set as - * requested in this method. + * requested in this method. Returns {@code null} if {@code packageNames} was {@code null}. + * + * @deprecated use {@link #setPackagesSuspended(String[], boolean, PersistableBundle, + * PersistableBundle, android.content.pm.SuspendDialogInfo)} instead. + * + * @hide + */ + @SystemApi + @Deprecated + @RequiresPermission(Manifest.permission.SUSPEND_APPS) + @Nullable + public String[] setPackagesSuspended(@Nullable String[] packageNames, boolean suspended, + @Nullable PersistableBundle appExtras, @Nullable PersistableBundle launcherExtras, + @Nullable String dialogMessage) { + throw new UnsupportedOperationException("setPackagesSuspended not implemented"); + } + + /** + * Puts the given packages in a suspended state, where attempts at starting activities are + * denied. + * + *

The suspended application's notifications and all of its windows will be hidden, any + * of its started activities will be stopped and it won't be able to ring the device. + * It doesn't remove the data or the actual package file. + * + *

When the user tries to launch a suspended app, a system dialog alerting them that the app + * is suspended will be shown instead. + * The caller can optionally customize the dialog by passing a {@link SuspendDialogInfo} object + * to this api. This dialog will have a button that starts the + * {@link Intent#ACTION_SHOW_SUSPENDED_APP_DETAILS} intent if the suspending app declares an + * activity which handles this action. + * + *

The packages being suspended must already be installed. If a package is uninstalled, it + * will no longer be suspended. + * + *

Optionally, the suspending app can provide extra information in the form of + * {@link PersistableBundle} objects to be shared with the apps being suspended and the + * launcher to support customization that they might need to handle the suspended state. + * + *

The caller must hold {@link Manifest.permission#SUSPEND_APPS} to use this api. + * + * @param packageNames The names of the packages to set the suspended status. + * @param suspended If set to {@code true}, the packages will be suspended, if set to + * {@code false}, the packages will be unsuspended. + * @param appExtras An optional {@link PersistableBundle} that the suspending app can provide + * which will be shared with the apps being suspended. Ignored if + * {@code suspended} is false. + * @param launcherExtras An optional {@link PersistableBundle} that the suspending app can + * provide which will be shared with the launcher. Ignored if + * {@code suspended} is false. + * @param dialogInfo An optional {@link SuspendDialogInfo} object describing the dialog that + * should be shown to the user when they try to launch a suspended app. + * Ignored if {@code suspended} is false. + * + * @return an array of package names for which the suspended status could not be set as + * requested in this method. Returns {@code null} if {@code packageNames} was {@code null}. + * + * @see #isPackageSuspended + * @see SuspendDialogInfo + * @see SuspendDialogInfo.Builder + * @see Intent#ACTION_SHOW_SUSPENDED_APP_DETAILS * * @hide */ @SystemApi @RequiresPermission(Manifest.permission.SUSPEND_APPS) - public String[] setPackagesSuspended(String[] packageNames, boolean suspended, + @Nullable + public String[] setPackagesSuspended(@Nullable String[] packageNames, boolean suspended, @Nullable PersistableBundle appExtras, @Nullable PersistableBundle launcherExtras, - String dialogMessage) { + @Nullable SuspendDialogInfo dialogInfo) { throw new UnsupportedOperationException("setPackagesSuspended not implemented"); } diff --git a/core/java/android/content/pm/PackageManagerInternal.java b/core/java/android/content/pm/PackageManagerInternal.java index b5b4432bbdb27..51ff748a97190 100644 --- a/core/java/android/content/pm/PackageManagerInternal.java +++ b/core/java/android/content/pm/PackageManagerInternal.java @@ -243,14 +243,15 @@ public abstract Bundle getSuspendedPackageLauncherExtras(String packageName, public abstract String getSuspendingPackage(String suspendedPackage, int userId); /** - * Get the dialog message to be shown to the user when they try to launch a suspended - * application. + * Get the information describing the dialog to be shown to the user when they try to launch a + * suspended application. * * @param suspendedPackage The package that has been suspended. * @param userId The user for which to check. - * @return The dialog message to be shown to the user. + * @return A {@link SuspendDialogInfo} object describing the dialog to be shown. */ - public abstract String getSuspendedDialogMessage(String suspendedPackage, int userId); + @Nullable + public abstract SuspendDialogInfo getSuspendedDialogInfo(String suspendedPackage, int userId); /** * Do a straight uid lookup for the given package/application in the given user. diff --git a/core/java/android/content/pm/PackageUserState.java b/core/java/android/content/pm/PackageUserState.java index 248d523a78eff..e21c33ad3bc10 100644 --- a/core/java/android/content/pm/PackageUserState.java +++ b/core/java/android/content/pm/PackageUserState.java @@ -33,6 +33,7 @@ import android.os.PersistableBundle; import android.util.ArraySet; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import java.util.Arrays; @@ -50,7 +51,7 @@ public class PackageUserState { public boolean hidden; // Is the app restricted by owner / admin public boolean suspended; public String suspendingPackage; - public String dialogMessage; // Message to show when a suspended package launch attempt is made + public SuspendDialogInfo dialogInfo; public PersistableBundle suspendedAppExtras; public PersistableBundle suspendedLauncherExtras; public boolean instantApp; @@ -79,6 +80,7 @@ public PackageUserState() { installReason = PackageManager.INSTALL_REASON_UNKNOWN; } + @VisibleForTesting public PackageUserState(PackageUserState o) { ceDataInode = o.ceDataInode; installed = o.installed; @@ -87,7 +89,7 @@ public PackageUserState(PackageUserState o) { hidden = o.hidden; suspended = o.suspended; suspendingPackage = o.suspendingPackage; - dialogMessage = o.dialogMessage; + dialogInfo = o.dialogInfo; suspendedAppExtras = o.suspendedAppExtras; suspendedLauncherExtras = o.suspendedLauncherExtras; instantApp = o.instantApp; @@ -217,7 +219,7 @@ final public boolean equals(Object obj) { || !suspendingPackage.equals(oldState.suspendingPackage)) { return false; } - if (!Objects.equals(dialogMessage, oldState.dialogMessage)) { + if (!Objects.equals(dialogInfo, oldState.dialogInfo)) { return false; } if (!BaseBundle.kindofEquals(suspendedAppExtras, diff --git a/core/java/android/content/pm/SuspendDialogInfo.aidl b/core/java/android/content/pm/SuspendDialogInfo.aidl new file mode 100644 index 0000000000000..5e711cfb01c2c --- /dev/null +++ b/core/java/android/content/pm/SuspendDialogInfo.aidl @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2018 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 android.content.pm; + +parcelable SuspendDialogInfo; diff --git a/core/java/android/content/pm/SuspendDialogInfo.java b/core/java/android/content/pm/SuspendDialogInfo.java new file mode 100644 index 0000000000000..c798c99fed90a --- /dev/null +++ b/core/java/android/content/pm/SuspendDialogInfo.java @@ -0,0 +1,379 @@ +/* + * Copyright (C) 2018 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 android.content.pm; + +import static android.content.res.ResourceId.ID_NULL; + +import android.annotation.DrawableRes; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.StringRes; +import android.annotation.SystemApi; +import android.content.res.ResourceId; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.PersistableBundle; +import android.util.Slog; + +import com.android.internal.util.Preconditions; +import com.android.internal.util.XmlUtils; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; +import java.util.Locale; +import java.util.Objects; + +/** + * A container to describe the dialog to be shown when the user tries to launch a suspended + * application. + * The suspending app can customize the dialog's following attributes: + *

+ * System defaults are used whenever any of these are not provided, or any of the provided resource + * ids cannot be resolved at the time of displaying the dialog. + * + * @hide + * @see PackageManager#setPackagesSuspended(String[], boolean, PersistableBundle, PersistableBundle, + * SuspendDialogInfo) + * @see Builder + */ +@SystemApi +public final class SuspendDialogInfo implements Parcelable { + private static final String TAG = SuspendDialogInfo.class.getSimpleName(); + private static final String XML_ATTR_ICON_RES_ID = "iconResId"; + private static final String XML_ATTR_TITLE_RES_ID = "titleResId"; + private static final String XML_ATTR_DIALOG_MESSAGE_RES_ID = "dialogMessageResId"; + private static final String XML_ATTR_DIALOG_MESSAGE = "dialogMessage"; + private static final String XML_ATTR_BUTTON_TEXT_RES_ID = "buttonTextResId"; + + private final int mIconResId; + private final int mTitleResId; + private final int mDialogMessageResId; + private final String mDialogMessage; + private final int mNeutralButtonTextResId; + + /** + * @return the resource id of the icon to be used with the dialog + * @hide + */ + @DrawableRes + public int getIconResId() { + return mIconResId; + } + + /** + * @return the resource id of the title to be used with the dialog + * @hide + */ + @StringRes + public int getTitleResId() { + return mTitleResId; + } + + /** + * @return the resource id of the text to be shown in the dialog's body + * @hide + */ + @StringRes + public int getDialogMessageResId() { + return mDialogMessageResId; + } + + /** + * @return the text to be shown in the dialog's body. Returns {@code null} if + * {@link #getDialogMessageResId()} returns a valid resource id. + * @hide + */ + @Nullable + public String getDialogMessage() { + return mDialogMessage; + } + + /** + * @return the text to be shown + * @hide + */ + @StringRes + public int getNeutralButtonTextResId() { + return mNeutralButtonTextResId; + } + + /** + * @hide + */ + public void saveToXml(XmlSerializer out) throws IOException { + if (mIconResId != ID_NULL) { + XmlUtils.writeIntAttribute(out, XML_ATTR_ICON_RES_ID, mIconResId); + } + if (mTitleResId != ID_NULL) { + XmlUtils.writeIntAttribute(out, XML_ATTR_TITLE_RES_ID, mTitleResId); + } + if (mDialogMessageResId != ID_NULL) { + XmlUtils.writeIntAttribute(out, XML_ATTR_DIALOG_MESSAGE_RES_ID, mDialogMessageResId); + } else { + XmlUtils.writeStringAttribute(out, XML_ATTR_DIALOG_MESSAGE, mDialogMessage); + } + if (mNeutralButtonTextResId != ID_NULL) { + XmlUtils.writeIntAttribute(out, XML_ATTR_BUTTON_TEXT_RES_ID, mNeutralButtonTextResId); + } + } + + /** + * @hide + */ + public static SuspendDialogInfo restoreFromXml(XmlPullParser in) { + final SuspendDialogInfo.Builder dialogInfoBuilder = new SuspendDialogInfo.Builder(); + try { + final int iconId = XmlUtils.readIntAttribute(in, XML_ATTR_ICON_RES_ID, ID_NULL); + final int titleId = XmlUtils.readIntAttribute(in, XML_ATTR_TITLE_RES_ID, ID_NULL); + final int buttonTextId = XmlUtils.readIntAttribute(in, XML_ATTR_BUTTON_TEXT_RES_ID, + ID_NULL); + final int dialogMessageResId = XmlUtils.readIntAttribute( + in, XML_ATTR_DIALOG_MESSAGE_RES_ID, ID_NULL); + final String dialogMessage = XmlUtils.readStringAttribute(in, XML_ATTR_DIALOG_MESSAGE); + + if (iconId != ID_NULL) { + dialogInfoBuilder.setIcon(iconId); + } + if (titleId != ID_NULL) { + dialogInfoBuilder.setTitle(titleId); + } + if (buttonTextId != ID_NULL) { + dialogInfoBuilder.setNeutralButtonText(buttonTextId); + } + if (dialogMessageResId != ID_NULL) { + dialogInfoBuilder.setMessage(dialogMessageResId); + } else if (dialogMessage != null) { + dialogInfoBuilder.setMessage(dialogMessage); + } + } catch (Exception e) { + Slog.e(TAG, "Exception while parsing from xml. Some fields may default", e); + } + return dialogInfoBuilder.build(); + } + + @Override + public int hashCode() { + int hashCode = mIconResId; + hashCode = 31 * hashCode + mTitleResId; + hashCode = 31 * hashCode + mNeutralButtonTextResId; + hashCode = 31 * hashCode + mDialogMessageResId; + hashCode = 31 * hashCode + Objects.hashCode(mDialogMessage); + return hashCode; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof SuspendDialogInfo)) { + return false; + } + final SuspendDialogInfo otherDialogInfo = (SuspendDialogInfo) obj; + return mIconResId == otherDialogInfo.mIconResId + && mTitleResId == otherDialogInfo.mTitleResId + && mDialogMessageResId == otherDialogInfo.mDialogMessageResId + && mNeutralButtonTextResId == otherDialogInfo.mNeutralButtonTextResId + && Objects.equals(mDialogMessage, otherDialogInfo.mDialogMessage); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder("SuspendDialogInfo: {"); + if (mIconResId != ID_NULL) { + builder.append("mIconId = 0x"); + builder.append(Integer.toHexString(mIconResId)); + builder.append(" "); + } + if (mTitleResId != ID_NULL) { + builder.append("mTitleResId = 0x"); + builder.append(Integer.toHexString(mTitleResId)); + builder.append(" "); + } + if (mNeutralButtonTextResId != ID_NULL) { + builder.append("mNeutralButtonTextResId = 0x"); + builder.append(Integer.toHexString(mNeutralButtonTextResId)); + builder.append(" "); + } + if (mDialogMessageResId != ID_NULL) { + builder.append("mDialogMessageResId = 0x"); + builder.append(Integer.toHexString(mDialogMessageResId)); + builder.append(" "); + } else if (mDialogMessage != null) { + builder.append("mDialogMessage = \""); + builder.append(mDialogMessage); + builder.append("\" "); + } + builder.append("}"); + return builder.toString(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int parcelableFlags) { + dest.writeInt(mIconResId); + dest.writeInt(mTitleResId); + dest.writeInt(mDialogMessageResId); + dest.writeString(mDialogMessage); + dest.writeInt(mNeutralButtonTextResId); + } + + private SuspendDialogInfo(Parcel source) { + mIconResId = source.readInt(); + mTitleResId = source.readInt(); + mDialogMessageResId = source.readInt(); + mDialogMessage = source.readString(); + mNeutralButtonTextResId = source.readInt(); + } + + SuspendDialogInfo(Builder b) { + mIconResId = b.mIconResId; + mTitleResId = b.mTitleResId; + mDialogMessageResId = b.mDialogMessageResId; + mDialogMessage = (mDialogMessageResId == ID_NULL) ? b.mDialogMessage : null; + mNeutralButtonTextResId = b.mNeutralButtonTextResId; + } + + public static final Creator CREATOR = new Creator() { + @Override + public SuspendDialogInfo createFromParcel(Parcel source) { + return new SuspendDialogInfo(source); + } + + @Override + public SuspendDialogInfo[] newArray(int size) { + return new SuspendDialogInfo[size]; + } + }; + + /** + * Builder to build a {@link SuspendDialogInfo} object. + */ + public static final class Builder { + private int mDialogMessageResId = ID_NULL; + private String mDialogMessage; + private int mTitleResId = ID_NULL; + private int mIconResId = ID_NULL; + private int mNeutralButtonTextResId = ID_NULL; + + /** + * Set the resource id of the icon to be used. If not provided, no icon will be shown. + * + * @param resId The resource id of the icon. + * @return this builder object. + */ + @NonNull + public Builder setIcon(@DrawableRes int resId) { + Preconditions.checkArgument(ResourceId.isValid(resId), "Invalid resource id provided"); + mIconResId = resId; + return this; + } + + /** + * Set the resource id of the title text to be displayed. If this is not provided, the + * system will use a default title. + * + * @param resId The resource id of the title. + * @return this builder object. + */ + @NonNull + public Builder setTitle(@StringRes int resId) { + Preconditions.checkArgument(ResourceId.isValid(resId), "Invalid resource id provided"); + mTitleResId = resId; + return this; + } + + /** + * Set the text to show in the body of the dialog. Ignored if a resource id is set via + * {@link #setMessage(int)}. + *

+ * The system will use {@link String#format(Locale, String, Object...) String.format} to + * insert the suspended app name into the message, so an example format string could be + * {@code "The app %1$s is currently suspended"}. This is optional - if the string passed in + * {@code message} does not accept an argument, it will be used as is. + * + * @param message The dialog message. + * @return this builder object. + * @see #setMessage(int) + */ + @NonNull + public Builder setMessage(@NonNull String message) { + Preconditions.checkStringNotEmpty(message, "Message cannot be null or empty"); + mDialogMessage = message; + return this; + } + + /** + * Set the resource id of the dialog message to be shown. If no dialog message is provided + * via either this method or {@link #setMessage(String)}, the system will use a + * default message. + *

+ * The system will use {@link android.content.res.Resources#getString(int, Object...) + * getString} to insert the suspended app name into the message, so an example format string + * could be {@code "The app %1$s is currently suspended"}. This is optional - if the string + * referred to by {@code resId} does not accept an argument, it will be used as is. + * + * @param resId The resource id of the dialog message. + * @return this builder object. + * @see #setMessage(String) + */ + @NonNull + public Builder setMessage(@StringRes int resId) { + Preconditions.checkArgument(ResourceId.isValid(resId), "Invalid resource id provided"); + mDialogMessageResId = resId; + return this; + } + + /** + * Set the resource id of text to be shown on the neutral button. Tapping this button starts + * the {@link android.content.Intent#ACTION_SHOW_SUSPENDED_APP_DETAILS} activity. If this is + * not provided, the system will use a default text. + * + * @param resId The resource id of the button text + * @return this builder object. + */ + @NonNull + public Builder setNeutralButtonText(@StringRes int resId) { + Preconditions.checkArgument(ResourceId.isValid(resId), "Invalid resource id provided"); + mNeutralButtonTextResId = resId; + return this; + } + + /** + * Build the final object based on given inputs. + * + * @return The {@link SuspendDialogInfo} object built using this builder. + */ + @NonNull + public SuspendDialogInfo build() { + return new SuspendDialogInfo(this); + } + } +} diff --git a/core/java/com/android/internal/app/SuspendedAppActivity.java b/core/java/com/android/internal/app/SuspendedAppActivity.java index a8edfb6ec9367..498de53b65e93 100644 --- a/core/java/com/android/internal/app/SuspendedAppActivity.java +++ b/core/java/com/android/internal/app/SuspendedAppActivity.java @@ -16,12 +16,17 @@ package com.android.internal.app; +import static android.content.res.ResourceId.ID_NULL; + import android.Manifest; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.content.pm.SuspendDialogInfo; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.UserHandle; import android.util.Slog; @@ -31,16 +36,19 @@ public class SuspendedAppActivity extends AlertActivity implements DialogInterface.OnClickListener { - private static final String TAG = "SuspendedAppActivity"; - public static final String EXTRA_SUSPENDED_PACKAGE = - "SuspendedAppActivity.extra.SUSPENDED_PACKAGE"; + private static final String TAG = SuspendedAppActivity.class.getSimpleName(); + private static final String PACKAGE_NAME = "com.android.internal.app"; + + public static final String EXTRA_SUSPENDED_PACKAGE = PACKAGE_NAME + ".extra.SUSPENDED_PACKAGE"; public static final String EXTRA_SUSPENDING_PACKAGE = - "SuspendedAppActivity.extra.SUSPENDING_PACKAGE"; - public static final String EXTRA_DIALOG_MESSAGE = "SuspendedAppActivity.extra.DIALOG_MESSAGE"; + PACKAGE_NAME + ".extra.SUSPENDING_PACKAGE"; + public static final String EXTRA_DIALOG_INFO = PACKAGE_NAME + ".extra.DIALOG_INFO"; private Intent mMoreDetailsIntent; private int mUserId; private PackageManager mPm; + private Resources mSuspendingAppResources; + private SuspendDialogInfo mSuppliedDialogInfo; private CharSequence getAppLabel(String packageName) { try { @@ -66,6 +74,65 @@ private Intent getMoreDetailsActivity(String suspendingPackage, String suspended return null; } + private Drawable resolveIcon() { + final int iconId = (mSuppliedDialogInfo != null) ? mSuppliedDialogInfo.getIconResId() + : ID_NULL; + if (iconId != ID_NULL && mSuspendingAppResources != null) { + try { + return mSuspendingAppResources.getDrawable(iconId, null); + } catch (Resources.NotFoundException nfe) { + Slog.e(TAG, "Could not resolve drawable resource id " + iconId); + } + } + return null; + } + + private String resolveTitle() { + final int titleId = (mSuppliedDialogInfo != null) ? mSuppliedDialogInfo.getTitleResId() + : ID_NULL; + if (titleId != ID_NULL && mSuspendingAppResources != null) { + try { + return mSuspendingAppResources.getString(titleId); + } catch (Resources.NotFoundException nfe) { + Slog.e(TAG, "Could not resolve string resource id " + titleId); + } + } + return getString(R.string.app_suspended_title); + } + + private String resolveDialogMessage(String suspendingPkg, String suspendedPkg) { + final CharSequence suspendedAppLabel = getAppLabel(suspendedPkg); + if (mSuppliedDialogInfo != null) { + final int messageId = mSuppliedDialogInfo.getDialogMessageResId(); + final String message = mSuppliedDialogInfo.getDialogMessage(); + if (messageId != ID_NULL && mSuspendingAppResources != null) { + try { + return mSuspendingAppResources.getString(messageId, suspendedAppLabel); + } catch (Resources.NotFoundException nfe) { + Slog.e(TAG, "Could not resolve string resource id " + messageId); + } + } else if (message != null) { + return String.format(getResources().getConfiguration().getLocales().get(0), message, + suspendedAppLabel); + } + } + return getString(R.string.app_suspended_default_message, suspendedAppLabel, + getAppLabel(suspendingPkg)); + } + + private String resolveNeutralButtonText() { + final int buttonTextId = (mSuppliedDialogInfo != null) + ? mSuppliedDialogInfo.getNeutralButtonTextResId() : ID_NULL; + if (buttonTextId != ID_NULL && mSuspendingAppResources != null) { + try { + return mSuspendingAppResources.getString(buttonTextId); + } catch (Resources.NotFoundException nfe) { + Slog.e(TAG, "Could not resolve string resource id " + buttonTextId); + } + } + return getString(R.string.app_suspended_more_details); + } + @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); @@ -79,26 +146,26 @@ public void onCreate(Bundle icicle) { finish(); return; } - final String suppliedMessage = intent.getStringExtra(EXTRA_DIALOG_MESSAGE); final String suspendedPackage = intent.getStringExtra(EXTRA_SUSPENDED_PACKAGE); final String suspendingPackage = intent.getStringExtra(EXTRA_SUSPENDING_PACKAGE); - final CharSequence suspendedAppLabel = getAppLabel(suspendedPackage); - final CharSequence dialogMessage; - if (suppliedMessage == null) { - dialogMessage = getString(R.string.app_suspended_default_message, suspendedAppLabel, - getAppLabel(suspendingPackage)); - } else { - dialogMessage = String.format(getResources().getConfiguration().getLocales().get(0), - suppliedMessage, suspendedAppLabel); + mSuppliedDialogInfo = intent.getParcelableExtra(EXTRA_DIALOG_INFO); + if (mSuppliedDialogInfo != null) { + try { + mSuspendingAppResources = mPm.getResourcesForApplicationAsUser(suspendingPackage, + mUserId); + } catch (PackageManager.NameNotFoundException ne) { + Slog.e(TAG, "Could not find resources for " + suspendingPackage, ne); + } } final AlertController.AlertParams ap = mAlertParams; - ap.mTitle = getString(R.string.app_suspended_title); - ap.mMessage = dialogMessage; + ap.mIcon = resolveIcon(); + ap.mTitle = resolveTitle(); + ap.mMessage = resolveDialogMessage(suspendingPackage, suspendedPackage); ap.mPositiveButtonText = getString(android.R.string.ok); mMoreDetailsIntent = getMoreDetailsActivity(suspendingPackage, suspendedPackage, mUserId); if (mMoreDetailsIntent != null) { - ap.mNeutralButtonText = getString(R.string.app_suspended_more_details); + ap.mNeutralButtonText = resolveNeutralButtonText(); } ap.mPositiveButtonListener = ap.mNeutralButtonListener = this; setupAlert(); @@ -116,11 +183,11 @@ public void onClick(DialogInterface dialog, int which) { } public static Intent createSuspendedAppInterceptIntent(String suspendedPackage, - String suspendingPackage, String dialogMessage, int userId) { + String suspendingPackage, SuspendDialogInfo dialogInfo, int userId) { return new Intent() .setClassName("android", SuspendedAppActivity.class.getName()) .putExtra(EXTRA_SUSPENDED_PACKAGE, suspendedPackage) - .putExtra(EXTRA_DIALOG_MESSAGE, dialogMessage) + .putExtra(EXTRA_DIALOG_INFO, dialogInfo) .putExtra(EXTRA_SUSPENDING_PACKAGE, suspendingPackage) .putExtra(Intent.EXTRA_USER_ID, userId) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java index da52d408e125b..39866a72ab98c 100644 --- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java +++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java @@ -56,6 +56,7 @@ import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.pm.ShortcutServiceInternal; +import android.content.pm.SuspendDialogInfo; import android.content.pm.UserInfo; import android.content.res.Resources; import android.content.res.TypedArray; @@ -629,10 +630,10 @@ private void maskWidgetsViewsLocked(Provider provider, Widget targetWidget) { onClickIntent = mDevicePolicyManagerInternal.createShowAdminSupportIntent( providerUserId, true); } else { - final String dialogMessage = mPackageManagerInternal.getSuspendedDialogMessage( - providerPackage, providerUserId); + final SuspendDialogInfo dialogInfo = mPackageManagerInternal + .getSuspendedDialogInfo(providerPackage, providerUserId); onClickIntent = SuspendedAppActivity.createSuspendedAppInterceptIntent( - providerPackage, suspendingPackage, dialogMessage, providerUserId); + providerPackage, suspendingPackage, dialogInfo, providerUserId); } } else if (provider.maskedByQuietProfile) { showBadge = true; diff --git a/services/core/java/com/android/server/am/ActivityStartInterceptor.java b/services/core/java/com/android/server/am/ActivityStartInterceptor.java index 4789ff3343981..e51824f6f7903 100644 --- a/services/core/java/com/android/server/am/ActivityStartInterceptor.java +++ b/services/core/java/com/android/server/am/ActivityStartInterceptor.java @@ -44,6 +44,7 @@ import android.content.pm.ActivityInfo; import android.content.pm.PackageManagerInternal; import android.content.pm.ResolveInfo; +import android.content.pm.SuspendDialogInfo; import android.content.pm.UserInfo; import android.os.Binder; import android.os.Bundle; @@ -246,9 +247,9 @@ private boolean interceptSuspendedPackageIfNeeded() { if (PLATFORM_PACKAGE_NAME.equals(suspendingPackage)) { return interceptSuspendedByAdminPackage(); } - final String dialogMessage = pmi.getSuspendedDialogMessage(suspendedPackage, mUserId); + final SuspendDialogInfo dialogInfo = pmi.getSuspendedDialogInfo(suspendedPackage, mUserId); mIntent = SuspendedAppActivity.createSuspendedAppInterceptIntent(suspendedPackage, - suspendingPackage, dialogMessage, mUserId); + suspendingPackage, dialogInfo, mUserId); mCallingPid = mRealCallingPid; mCallingUid = mRealCallingUid; mResolvedType = null; diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 13f084e674944..a7e18cf6a8a45 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -187,6 +187,7 @@ import android.content.pm.ServiceInfo; import android.content.pm.SharedLibraryInfo; import android.content.pm.Signature; +import android.content.pm.SuspendDialogInfo; import android.content.pm.UserInfo; import android.content.pm.VerifierDeviceIdentity; import android.content.pm.VerifierInfo; @@ -12705,8 +12706,8 @@ boolean isUserRestricted(int userId, String restrictionKey) { @Override public String[] setPackagesSuspendedAsUser(String[] packageNames, boolean suspended, - PersistableBundle appExtras, PersistableBundle launcherExtras, String dialogMessage, - String callingPackage, int userId) { + PersistableBundle appExtras, PersistableBundle launcherExtras, + SuspendDialogInfo dialogInfo, String callingPackage, int userId) { mContext.enforceCallingOrSelfPermission(android.Manifest.permission.SUSPEND_APPS, "setPackagesSuspendedAsUser"); @@ -12751,7 +12752,7 @@ && getPackageUid(callingPackage, 0, userId) != callingUid) { unactionedPackages.add(packageName); continue; } - pkgSetting.setSuspended(suspended, callingPackage, dialogMessage, appExtras, + pkgSetting.setSuspended(suspended, callingPackage, dialogInfo, appExtras, launcherExtras, userId); changedPackagesList.add(packageName); changedUids.add(UserHandle.getUid(userId, pkgSetting.appId)); @@ -17805,7 +17806,7 @@ private void markPackageUninstalledForUserLPw(PackageSetting ps, UserHandle user false /*hidden*/, false /*suspended*/, null /*suspendingPackage*/, - null /*dialogMessage*/, + null /*dialogInfo*/, null /*suspendedAppExtras*/, null /*suspendedLauncherExtras*/, false /*instantApp*/, @@ -22556,10 +22557,10 @@ public String getSuspendingPackage(String suspendedPackage, int userId) { } @Override - public String getSuspendedDialogMessage(String suspendedPackage, int userId) { + public SuspendDialogInfo getSuspendedDialogInfo(String suspendedPackage, int userId) { synchronized (mPackages) { final PackageSetting ps = mSettings.mPackages.get(suspendedPackage); - return (ps != null) ? ps.readUserState(userId).dialogMessage : null; + return (ps != null) ? ps.readUserState(userId).dialogInfo : null; } } diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java index 93729d1949b0c..e25cca43e8da0 100644 --- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java +++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java @@ -51,6 +51,7 @@ import android.content.pm.PermissionGroupInfo; import android.content.pm.PermissionInfo; import android.content.pm.ResolveInfo; +import android.content.pm.SuspendDialogInfo; import android.content.pm.UserInfo; import android.content.pm.VersionedPackage; import android.content.pm.dex.ArtManager; @@ -1701,9 +1702,18 @@ private int runSuspend(boolean suspendedState) { } final String callingPackage = (Binder.getCallingUid() == Process.ROOT_UID) ? "root" : "com.android.shell"; + + final SuspendDialogInfo info; + if (!TextUtils.isEmpty(dialogMessage)) { + info = new SuspendDialogInfo.Builder() + .setMessage(dialogMessage) + .build(); + } else { + info = null; + } try { mInterface.setPackagesSuspendedAsUser(new String[]{packageName}, suspendedState, - appExtras, launcherExtras, dialogMessage, callingPackage, userId); + appExtras, launcherExtras, info, callingPackage, userId); pw.println("Package " + packageName + " new suspended state: " + mInterface.isPackageSuspendedForUser(packageName, userId)); return 0; diff --git a/services/core/java/com/android/server/pm/PackageSettingBase.java b/services/core/java/com/android/server/pm/PackageSettingBase.java index fd6aceb1ce6b1..3c22f07ad1084 100644 --- a/services/core/java/com/android/server/pm/PackageSettingBase.java +++ b/services/core/java/com/android/server/pm/PackageSettingBase.java @@ -26,6 +26,7 @@ import android.content.pm.PackageParser; import android.content.pm.PackageUserState; import android.content.pm.Signature; +import android.content.pm.SuspendDialogInfo; import android.os.PersistableBundle; import android.service.pm.PackageProto; import android.util.ArraySet; @@ -395,12 +396,12 @@ boolean getSuspended(int userId) { return readUserState(userId).suspended; } - void setSuspended(boolean suspended, String suspendingPackage, String dialogMessage, + void setSuspended(boolean suspended, String suspendingPackage, SuspendDialogInfo dialogInfo, PersistableBundle appExtras, PersistableBundle launcherExtras, int userId) { final PackageUserState existingUserState = modifyUserState(userId); existingUserState.suspended = suspended; existingUserState.suspendingPackage = suspended ? suspendingPackage : null; - existingUserState.dialogMessage = suspended ? dialogMessage : null; + existingUserState.dialogInfo = suspended ? dialogInfo : null; existingUserState.suspendedAppExtras = suspended ? appExtras : null; existingUserState.suspendedLauncherExtras = suspended ? launcherExtras : null; } @@ -423,7 +424,7 @@ void setVirtualPreload(boolean virtualPreload, int userId) { void setUserState(int userId, long ceDataInode, int enabled, boolean installed, boolean stopped, boolean notLaunched, boolean hidden, boolean suspended, String suspendingPackage, - String dialogMessage, PersistableBundle suspendedAppExtras, + SuspendDialogInfo dialogInfo, PersistableBundle suspendedAppExtras, PersistableBundle suspendedLauncherExtras, boolean instantApp, boolean virtualPreload, String lastDisableAppCaller, ArraySet enabledComponents, ArraySet disabledComponents, @@ -438,7 +439,7 @@ void setUserState(int userId, long ceDataInode, int enabled, boolean installed, state.hidden = hidden; state.suspended = suspended; state.suspendingPackage = suspendingPackage; - state.dialogMessage = dialogMessage; + state.dialogInfo = dialogInfo; state.suspendedAppExtras = suspendedAppExtras; state.suspendedLauncherExtras = suspendedLauncherExtras; state.lastDisableAppCaller = lastDisableAppCaller; diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java index 5c88e0637092b..6a7e65400fa78 100644 --- a/services/core/java/com/android/server/pm/Settings.java +++ b/services/core/java/com/android/server/pm/Settings.java @@ -49,6 +49,7 @@ import android.content.pm.PermissionInfo; import android.content.pm.ResolveInfo; import android.content.pm.Signature; +import android.content.pm.SuspendDialogInfo; import android.content.pm.UserInfo; import android.content.pm.VerifierDeviceIdentity; import android.net.Uri; @@ -203,6 +204,7 @@ public static class DatabaseVersion { private static final String TAG_DEFAULT_BROWSER = "default-browser"; private static final String TAG_DEFAULT_DIALER = "default-dialer"; private static final String TAG_VERSION = "version"; + private static final String TAG_SUSPENDED_DIALOG_INFO = "suspended-dialog-info"; private static final String TAG_SUSPENDED_APP_EXTRAS = "suspended-app-extras"; private static final String TAG_SUSPENDED_LAUNCHER_EXTRAS = "suspended-launcher-extras"; @@ -222,6 +224,10 @@ public static class DatabaseVersion { private static final String ATTR_HIDDEN = "hidden"; private static final String ATTR_SUSPENDED = "suspended"; private static final String ATTR_SUSPENDING_PACKAGE = "suspending-package"; + /** + * @deprecated Legacy attribute, kept only for upgrading from P builds. + */ + @Deprecated private static final String ATTR_SUSPEND_DIALOG_MESSAGE = "suspend_dialog_message"; // Legacy, uninstall blocks are stored separately. @Deprecated @@ -730,7 +736,7 @@ void pruneSharedUsersLPw() { false /*hidden*/, false /*suspended*/, null /*suspendingPackage*/, - null /*dialogMessage*/, + null /*dialogInfo*/, null /*suspendedAppExtras*/, null /*suspendedLauncherExtras*/, instantApp, @@ -1620,7 +1626,7 @@ void readPackageRestrictionsLPr(int userId) { false /*hidden*/, false /*suspended*/, null /*suspendingPackage*/, - null /*dialogMessage*/, + null /*dialogInfo*/, null /*suspendedAppExtras*/, null /*suspendedLauncherExtras*/, false /*instantApp*/, @@ -1730,6 +1736,7 @@ void readPackageRestrictionsLPr(int userId) { ArraySet disabledComponents = null; PersistableBundle suspendedAppExtras = null; PersistableBundle suspendedLauncherExtras = null; + SuspendDialogInfo suspendDialogInfo = null; int packageDepth = parser.getDepth(); while ((type=parser.next()) != XmlPullParser.END_DOCUMENT @@ -1752,20 +1759,28 @@ void readPackageRestrictionsLPr(int userId) { case TAG_SUSPENDED_LAUNCHER_EXTRAS: suspendedLauncherExtras = PersistableBundle.restoreFromXml(parser); break; + case TAG_SUSPENDED_DIALOG_INFO: + suspendDialogInfo = SuspendDialogInfo.restoreFromXml(parser); + break; default: Slog.wtf(TAG, "Unknown tag " + parser.getName() + " under tag " + TAG_PACKAGE); } } + if (suspendDialogInfo == null && !TextUtils.isEmpty(dialogMessage)) { + suspendDialogInfo = new SuspendDialogInfo.Builder() + .setMessage(dialogMessage) + .build(); + } if (blockUninstall) { setBlockUninstallLPw(userId, name, true); } ps.setUserState(userId, ceDataInode, enabled, installed, stopped, notLaunched, - hidden, suspended, suspendingPackage, dialogMessage, suspendedAppExtras, - suspendedLauncherExtras, instantApp, virtualPreload, enabledCaller, - enabledComponents, disabledComponents, verifState, linkGeneration, - installReason, harmfulAppWarning); + hidden, suspended, suspendingPackage, suspendDialogInfo, + suspendedAppExtras, suspendedLauncherExtras, instantApp, virtualPreload, + enabledCaller, enabledComponents, disabledComponents, verifState, + linkGeneration, installReason, harmfulAppWarning); } else if (tagName.equals("preferred-activities")) { readPreferredActivitiesLPw(parser, userId); } else if (tagName.equals(TAG_PERSISTENT_PREFERRED_ACTIVITIES)) { @@ -2076,9 +2091,10 @@ void writePackageRestrictionsLPr(int userId) { serializer.attribute(null, ATTR_SUSPENDING_PACKAGE, ustate.suspendingPackage); } - if (ustate.dialogMessage != null) { - serializer.attribute(null, ATTR_SUSPEND_DIALOG_MESSAGE, - ustate.dialogMessage); + if (ustate.dialogInfo != null) { + serializer.startTag(null, TAG_SUSPENDED_DIALOG_INFO); + ustate.dialogInfo.saveToXml(serializer); + serializer.endTag(null, TAG_SUSPENDED_DIALOG_INFO); } if (ustate.suspendedAppExtras != null) { serializer.startTag(null, TAG_SUSPENDED_APP_EXTRAS); @@ -4737,8 +4753,8 @@ void dumpPackageLPr(PrintWriter pw, String prefix, String checkinTag, final PackageUserState pus = ps.readUserState(user.id); pw.print(" suspendingPackage="); pw.print(pus.suspendingPackage); - pw.print(" dialogMessage="); - pw.print(pus.dialogMessage); + pw.print(" dialogInfo="); + pw.print(pus.dialogInfo); } pw.print(" stopped="); pw.print(ps.getStopped(user.id)); diff --git a/services/tests/servicestests/src/com/android/server/am/ActivityStartInterceptorTest.java b/services/tests/servicestests/src/com/android/server/am/ActivityStartInterceptorTest.java index 86541b95f395d..65e4fa0f4affb 100644 --- a/services/tests/servicestests/src/com/android/server/am/ActivityStartInterceptorTest.java +++ b/services/tests/servicestests/src/com/android/server/am/ActivityStartInterceptorTest.java @@ -35,6 +35,7 @@ import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManagerInternal; +import android.content.pm.SuspendDialogInfo; import android.content.pm.UserInfo; import android.os.UserHandle; import android.os.UserManager; @@ -165,17 +166,20 @@ public void testSuspendedByAdminPackage() { public void testSuspendedPackage() { mAInfo.applicationInfo.flags = FLAG_SUSPENDED; final String suspendingPackage = "com.test.suspending.package"; - final String dialogMessage = "Test Message"; + final SuspendDialogInfo dialogInfo = new SuspendDialogInfo.Builder() + .setMessage("Test Message") + .setIcon(0x11110001) + .build(); when(mPackageManagerInternal.getSuspendingPackage(TEST_PACKAGE_NAME, TEST_USER_ID)) .thenReturn(suspendingPackage); - when(mPackageManagerInternal.getSuspendedDialogMessage(TEST_PACKAGE_NAME, TEST_USER_ID)) - .thenReturn(dialogMessage); + when(mPackageManagerInternal.getSuspendedDialogInfo(TEST_PACKAGE_NAME, TEST_USER_ID)) + .thenReturn(dialogInfo); // THEN calling intercept returns true assertTrue(mInterceptor.intercept(null, null, mAInfo, null, null, 0, 0, null)); // Check intent parameters - assertEquals(dialogMessage, - mInterceptor.mIntent.getStringExtra(SuspendedAppActivity.EXTRA_DIALOG_MESSAGE)); + assertEquals(dialogInfo, + mInterceptor.mIntent.getParcelableExtra(SuspendedAppActivity.EXTRA_DIALOG_INFO)); assertEquals(suspendingPackage, mInterceptor.mIntent.getStringExtra(SuspendedAppActivity.EXTRA_SUSPENDING_PACKAGE)); assertEquals(TEST_PACKAGE_NAME, diff --git a/services/tests/servicestests/src/com/android/server/pm/PackageManagerSettingsTests.java b/services/tests/servicestests/src/com/android/server/pm/PackageManagerSettingsTests.java index c3c07880f605d..517b5ade44b89 100644 --- a/services/tests/servicestests/src/com/android/server/pm/PackageManagerSettingsTests.java +++ b/services/tests/servicestests/src/com/android/server/pm/PackageManagerSettingsTests.java @@ -37,6 +37,7 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageParser; import android.content.pm.PackageUserState; +import android.content.pm.SuspendDialogInfo; import android.content.pm.UserInfo; import android.os.BaseBundle; import android.os.PersistableBundle; @@ -200,13 +201,21 @@ public void testReadWritePackageRestrictions_newSuspendInfo() { PACKAGE_NAME_1, 1L, 0.01, true, "appString1"); final PersistableBundle launcherExtras1 = getPersistableBundle( PACKAGE_NAME_1, 10L, 0.1, false, "launcherString1"); - ps1.setSuspended(true, "suspendingPackage1", "dialogMsg1", appExtras1, launcherExtras1, 0); + + final SuspendDialogInfo dialogInfo1 = new SuspendDialogInfo.Builder() + .setIcon(0x11220001) + .setTitle(0x11220002) + .setMessage("1st message") + .setNeutralButtonText(0x11220003) + .build(); + + ps1.setSuspended(true, "suspendingPackage1", dialogInfo1, appExtras1, launcherExtras1, 0); settingsUnderTest.mPackages.put(PACKAGE_NAME_1, ps1); - ps2.setSuspended(true, "suspendingPackage2", "dialogMsg2", null, null, 0); + ps2.setSuspended(true, "suspendingPackage2", null, null, null, 0); settingsUnderTest.mPackages.put(PACKAGE_NAME_2, ps2); - ps3.setSuspended(false, "irrelevant", "irrevelant2", null, null, 0); + ps3.setSuspended(false, "irrelevant", dialogInfo1, null, null, 0); settingsUnderTest.mPackages.put(PACKAGE_NAME_3, ps3); settingsUnderTest.writePackageRestrictionsLPr(0); @@ -221,7 +230,7 @@ public void testReadWritePackageRestrictions_newSuspendInfo() { readUserState(0); assertThat(readPus1.suspended, is(true)); assertThat(readPus1.suspendingPackage, equalTo("suspendingPackage1")); - assertThat(readPus1.dialogMessage, equalTo("dialogMsg1")); + assertThat(readPus1.dialogInfo, equalTo(dialogInfo1)); assertThat(BaseBundle.kindofEquals(readPus1.suspendedAppExtras, appExtras1), is(true)); assertThat(BaseBundle.kindofEquals(readPus1.suspendedLauncherExtras, launcherExtras1), is(true)); @@ -230,7 +239,7 @@ public void testReadWritePackageRestrictions_newSuspendInfo() { readUserState(0); assertThat(readPus2.suspended, is(true)); assertThat(readPus2.suspendingPackage, equalTo("suspendingPackage2")); - assertThat(readPus2.dialogMessage, equalTo("dialogMsg2")); + assertThat(readPus2.dialogInfo, is(nullValue())); assertThat(readPus2.suspendedAppExtras, is(nullValue())); assertThat(readPus2.suspendedLauncherExtras, is(nullValue())); @@ -238,7 +247,7 @@ public void testReadWritePackageRestrictions_newSuspendInfo() { readUserState(0); assertThat(readPus3.suspended, is(false)); assertThat(readPus3.suspendingPackage, is(nullValue())); - assertThat(readPus3.dialogMessage, is(nullValue())); + assertThat(readPus3.dialogInfo, is(nullValue())); assertThat(readPus3.suspendedAppExtras, is(nullValue())); assertThat(readPus3.suspendedLauncherExtras, is(nullValue())); } diff --git a/services/tests/servicestests/src/com/android/server/pm/PackageUserStateTest.java b/services/tests/servicestests/src/com/android/server/pm/PackageUserStateTest.java index 4a33ca37f7675..f0ed612400ed7 100644 --- a/services/tests/servicestests/src/com/android/server/pm/PackageUserStateTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/PackageUserStateTest.java @@ -23,6 +23,7 @@ import static org.junit.Assert.assertThat; import android.content.pm.PackageUserState; +import android.content.pm.SuspendDialogInfo; import android.os.PersistableBundle; import android.util.ArraySet; @@ -37,7 +38,7 @@ public class PackageUserStateTest { @Test - public void testPackageUserState01() { + public void testPackageUserState01() { final PackageUserState testUserState = new PackageUserState(); PackageUserState oldUserState; @@ -84,7 +85,7 @@ public void testPackageUserState01() { } @Test - public void testPackageUserState02() { + public void testPackageUserState02() { final PackageUserState testUserState01 = new PackageUserState(); PackageUserState oldUserState; @@ -102,7 +103,7 @@ public void testPackageUserState02() { } @Test - public void testPackageUserState03() { + public void testPackageUserState03() { final PackageUserState oldUserState = new PackageUserState(); // only new user state has array defined; different @@ -138,7 +139,7 @@ public void testPackageUserState03() { } @Test - public void testPackageUserState04() { + public void testPackageUserState04() { final PackageUserState oldUserState = new PackageUserState(); // only new user state has array defined; different @@ -185,15 +186,19 @@ public void testPackageUserState05() { launcherExtras2.putString("name", "launcherExtras2"); final String suspendingPackage1 = "package1"; final String suspendingPackage2 = "package2"; - final String dialogMessage1 = "dialogMessage1"; - final String dialogMessage2 = "dialogMessage2"; + final SuspendDialogInfo dialogInfo1 = new SuspendDialogInfo.Builder() + .setMessage("dialogMessage1") + .build(); + final SuspendDialogInfo dialogInfo2 = new SuspendDialogInfo.Builder() + .setMessage("dialogMessage2") + .build(); final PackageUserState testUserState1 = new PackageUserState(); testUserState1.suspended = true; testUserState1.suspendedAppExtras = appExtras1; testUserState1.suspendedLauncherExtras = launcherExtras1; testUserState1.suspendingPackage = suspendingPackage1; - testUserState1.dialogMessage = dialogMessage1; + testUserState1.dialogInfo = dialogInfo1; PackageUserState testUserState2 = new PackageUserState(testUserState1); assertThat(testUserState1.equals(testUserState2), is(true)); @@ -209,14 +214,14 @@ public void testPackageUserState05() { assertThat(testUserState1.equals(testUserState2), is(false)); testUserState2 = new PackageUserState(testUserState1); - testUserState2.dialogMessage = dialogMessage2; + testUserState2.dialogInfo = dialogInfo2; assertThat(testUserState1.equals(testUserState2), is(false)); testUserState2 = new PackageUserState(testUserState1); testUserState2.suspended = testUserState1.suspended = false; // Everything is different but irrelevant if suspended is false testUserState2.suspendingPackage = suspendingPackage2; - testUserState2.dialogMessage = dialogMessage2; + testUserState2.dialogInfo = dialogInfo2; testUserState2.suspendedAppExtras = appExtras2; testUserState2.suspendedLauncherExtras = launcherExtras2; assertThat(testUserState1.equals(testUserState2), is(true)); diff --git a/services/tests/servicestests/src/com/android/server/pm/SuspendDialogInfoTest.java b/services/tests/servicestests/src/com/android/server/pm/SuspendDialogInfoTest.java new file mode 100644 index 0000000000000..7eccd67285338 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/pm/SuspendDialogInfoTest.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2018 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.server.pm; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; + +import android.content.pm.SuspendDialogInfo; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class SuspendDialogInfoTest { + private static final int VALID_TEST_RES_ID_1 = 0x11110001; + private static final int VALID_TEST_RES_ID_2 = 0x11110002; + + private static SuspendDialogInfo.Builder createDefaultDialogBuilder() { + return new SuspendDialogInfo.Builder() + .setIcon(VALID_TEST_RES_ID_1) + .setTitle(VALID_TEST_RES_ID_1) + .setMessage(VALID_TEST_RES_ID_1) + .setNeutralButtonText(VALID_TEST_RES_ID_1); + } + + @Test + public void equalsComparesIcons() { + final SuspendDialogInfo.Builder dialogBuilder1 = createDefaultDialogBuilder(); + final SuspendDialogInfo.Builder dialogBuilder2 = createDefaultDialogBuilder(); + assertEquals(dialogBuilder1.build(), dialogBuilder2.build()); + // Only icon is different + dialogBuilder2.setIcon(VALID_TEST_RES_ID_2); + assertNotEquals(dialogBuilder1.build(), dialogBuilder2.build()); + } + + @Test + public void equalsComparesTitle() { + final SuspendDialogInfo.Builder dialogBuilder1 = createDefaultDialogBuilder(); + final SuspendDialogInfo.Builder dialogBuilder2 = createDefaultDialogBuilder(); + assertEquals(dialogBuilder1.build(), dialogBuilder2.build()); + // Only title is different + dialogBuilder2.setTitle(VALID_TEST_RES_ID_2); + assertNotEquals(dialogBuilder1.build(), dialogBuilder2.build()); + } + + @Test + public void equalsComparesButtonText() { + final SuspendDialogInfo.Builder dialogBuilder1 = createDefaultDialogBuilder(); + final SuspendDialogInfo.Builder dialogBuilder2 = createDefaultDialogBuilder(); + assertEquals(dialogBuilder1.build(), dialogBuilder2.build()); + // Only button text is different + dialogBuilder2.setNeutralButtonText(VALID_TEST_RES_ID_2); + assertNotEquals(dialogBuilder1.build(), dialogBuilder2.build()); + } + + @Test + public void equalsComparesMessageIds() { + final SuspendDialogInfo.Builder dialogBuilder1 = createDefaultDialogBuilder(); + final SuspendDialogInfo.Builder dialogBuilder2 = createDefaultDialogBuilder(); + assertEquals(dialogBuilder1.build(), dialogBuilder2.build()); + // Only message is different + dialogBuilder2.setMessage(VALID_TEST_RES_ID_2); + assertNotEquals(dialogBuilder1.build(), dialogBuilder2.build()); + } + + @Test + public void equalsIgnoresMessageStringsWhenIdsSet() { + final SuspendDialogInfo.Builder dialogBuilder1 = new SuspendDialogInfo.Builder() + .setMessage(VALID_TEST_RES_ID_1) + .setMessage("1st message"); + final SuspendDialogInfo.Builder dialogBuilder2 = new SuspendDialogInfo.Builder() + .setMessage(VALID_TEST_RES_ID_1) + .setMessage("2nd message"); + // String messages different but should get be ignored when resource ids are set + assertEquals(dialogBuilder1.build(), dialogBuilder2.build()); + } + + @Test + public void equalsComparesMessageStringsWhenNoIdsSet() { + final SuspendDialogInfo.Builder dialogBuilder1 = new SuspendDialogInfo.Builder() + .setMessage("1st message"); + final SuspendDialogInfo.Builder dialogBuilder2 = new SuspendDialogInfo.Builder() + .setMessage("2nd message"); + // Both have different messages, which are not ignored as resource ids aren't set + assertNotEquals(dialogBuilder1.build(), dialogBuilder2.build()); + } + + @Test + public void messageStringClearedWhenResIdSet() { + final SuspendDialogInfo dialogInfo = new SuspendDialogInfo.Builder() + .setMessage(VALID_TEST_RES_ID_2) + .setMessage("Should be cleared on build") + .build(); + assertNull(dialogInfo.getDialogMessage()); + assertEquals(VALID_TEST_RES_ID_2, dialogInfo.getDialogMessageResId()); + } +} diff --git a/services/tests/servicestests/src/com/android/server/pm/SuspendPackagesTest.java b/services/tests/servicestests/src/com/android/server/pm/SuspendPackagesTest.java index f115b9cd0fc5b..553d234adfca8 100644 --- a/services/tests/servicestests/src/com/android/server/pm/SuspendPackagesTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/SuspendPackagesTest.java @@ -33,6 +33,7 @@ import android.content.pm.IPackageManager; import android.content.pm.LauncherApps; import android.content.pm.PackageManager; +import android.content.pm.SuspendDialogInfo; import android.content.res.Resources; import android.os.BaseBundle; import android.os.Bundle; @@ -152,7 +153,7 @@ Intent pollForIntent(long secondsToWait) { } void drainPendingBroadcasts() { - while (pollForIntent(5) != null); + while (pollForIntent(5) != null) ; } Intent receiveIntentFromApp() { @@ -215,15 +216,15 @@ private PersistableBundle getExtras(String keyPrefix, long lval, String sval, do } private void suspendTestPackage(PersistableBundle appExtras, PersistableBundle launcherExtras, - String dialogMessage) { + SuspendDialogInfo dialogInfo) { final String[] unchangedPackages = mPackageManager.setPackagesSuspended( - PACKAGES_TO_SUSPEND, true, appExtras, launcherExtras, dialogMessage); + PACKAGES_TO_SUSPEND, true, appExtras, launcherExtras, dialogInfo); assertTrue("setPackagesSuspended returned non-empty list", unchangedPackages.length == 0); } private void unsuspendTestPackage() { final String[] unchangedPackages = mPackageManager.setPackagesSuspended( - PACKAGES_TO_SUSPEND, false, null, null, null); + PACKAGES_TO_SUSPEND, false, null, null, (SuspendDialogInfo) null); assertTrue("setPackagesSuspended returned non-empty list", unchangedPackages.length == 0); } @@ -318,7 +319,8 @@ public void testUpdatingAppExtras() { @Test public void testCannotSuspendSelf() { final String[] unchangedPkgs = mPackageManager.setPackagesSuspended( - new String[]{mContext.getOpPackageName()}, true, null, null, null); + new String[]{mContext.getOpPackageName()}, true, null, null, + (SuspendDialogInfo) null); assertTrue(unchangedPkgs.length == 1); assertEquals(mContext.getOpPackageName(), unchangedPkgs[0]); } @@ -457,7 +459,8 @@ public void testInterceptorActivity() throws Exception { mAppCommsReceiver.register(mReceiverHandler, ACTION_REPORT_MORE_DETAILS_ACTIVITY_STARTED, ACTION_REPORT_TEST_ACTIVITY_STARTED); final String testMessage = "This is a test message to report suspension of %1$s"; - suspendTestPackage(null, null, testMessage); + suspendTestPackage(null, null, + new SuspendDialogInfo.Builder().setMessage(testMessage).build()); startTestAppActivity(); assertNull("No broadcast was expected from app", mAppCommsReceiver.pollForIntent(2)); assertNotNull("Given dialog message not shown", mUiDevice.wait( diff --git a/test-mock/api/system-current.txt b/test-mock/api/system-current.txt index 3bd3d68ba6cf6..2b968aec1496c 100644 --- a/test-mock/api/system-current.txt +++ b/test-mock/api/system-current.txt @@ -29,6 +29,7 @@ package android.test.mock { method public void removeOnPermissionsChangeListener(android.content.pm.PackageManager.OnPermissionsChangedListener); method public void revokeRuntimePermission(java.lang.String, java.lang.String, android.os.UserHandle); method public boolean setDefaultBrowserPackageNameAsUser(java.lang.String, int); + method public java.lang.String[] setPackagesSuspended(java.lang.String[], boolean, android.os.PersistableBundle, android.os.PersistableBundle, java.lang.String); method public void setUpdateAvailable(java.lang.String, boolean); method public boolean updateIntentVerificationStatusAsUser(java.lang.String, int, int); method public void updatePermissionFlags(java.lang.String, java.lang.String, int, int, android.os.UserHandle);