diff options
author | Suprabh Shukla <suprabh@google.com> | 2018-10-01 18:20:39 -0700 |
---|---|---|
committer | Suprabh Shukla <suprabh@google.com> | 2018-10-12 16:02:53 -0700 |
commit | 389cb6f54a5a5bb8dea540f57a3a8ac3c3c1c758 (patch) | |
tree | 8e14ff1d81b08098a5a98b700cb5438376928195 | |
parent | 20c6621d823a343734332e6adbd8e0ea129ead75 (diff) |
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
21 files changed, 812 insertions, 86 deletions
diff --git a/api/system-current.txt b/api/system-current.txt index ac8fc9c23cb8..8a522a1c3370 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<android.content.pm.SuspendDialogInfo> 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 264029b6ace7..fcd9a0511265 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -55,6 +55,7 @@ import android.content.pm.ProviderInfo; 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.ErrnoException; 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 class ApplicationPackageManager extends PackageManager { 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 6a20c9349e1d..4a4de5160e80 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 3032d164ef46..7e61a2504eea 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -5663,7 +5663,7 @@ public abstract class PackageManager { * {@link Manifest.permission#MANAGE_USERS} to use this api.</p> * * @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 class PackageManager { * 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. + * + * <p>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. + * + * <p>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. + * + * <p>The packages being suspended must already be installed. If a package is uninstalled, it + * will no longer be suspended. + * + * <p>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. + * + * <p>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 b5b4432bbdb2..51ff748a9719 100644 --- a/core/java/android/content/pm/PackageManagerInternal.java +++ b/core/java/android/content/pm/PackageManagerInternal.java @@ -243,14 +243,15 @@ public abstract class PackageManagerInternal { 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 248d523a78ef..e21c33ad3bc1 100644 --- a/core/java/android/content/pm/PackageUserState.java +++ b/core/java/android/content/pm/PackageUserState.java @@ -33,6 +33,7 @@ import android.os.BaseBundle; 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 class PackageUserState { installReason = PackageManager.INSTALL_REASON_UNKNOWN; } + @VisibleForTesting public PackageUserState(PackageUserState o) { ceDataInode = o.ceDataInode; installed = o.installed; @@ -87,7 +89,7 @@ public class PackageUserState { 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 @@ public class PackageUserState { || !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 000000000000..5e711cfb01c2 --- /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 000000000000..c798c99fed90 --- /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: + * <ul> + * <li>The dialog icon, by providing a resource id. + * <li>The title text, by providing a resource id. + * <li>The text of the dialog's body, by providing a resource id or a string. + * <li>The text on the neutral button which starts the + * {@link android.content.Intent#ACTION_SHOW_SUSPENDED_APP_DETAILS SHOW_SUSPENDED_APP_DETAILS} + * activity, by providing a resource id. + * </ul> + * 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<SuspendDialogInfo> CREATOR = new Creator<SuspendDialogInfo>() { + @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)}. + * <p> + * 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. + * <p> + * 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 a8edfb6ec936..498de53b65e9 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 @@ import com.android.internal.R; 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 @@ public class SuspendedAppActivity extends AlertActivity 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 class SuspendedAppActivity extends AlertActivity 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 class SuspendedAppActivity extends AlertActivity } 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 da52d408e125..39866a72ab98 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.ParceledListSlice; 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 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku 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 4789ff334398..e51824f6f790 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.IntentSender; 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 @@ class ActivityStartInterceptor { 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 13f084e67494..a7e18cf6a8a4 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.SELinuxUtil; 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 @@ public class PackageManagerService extends IPackageManager.Stub @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 @@ public class PackageManagerService extends IPackageManager.Stub 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 @@ public class PackageManagerService extends IPackageManager.Stub false /*hidden*/, false /*suspended*/, null /*suspendingPackage*/, - null /*dialogMessage*/, + null /*dialogInfo*/, null /*suspendedAppExtras*/, null /*suspendedLauncherExtras*/, false /*instantApp*/, @@ -22556,10 +22557,10 @@ public class PackageManagerService extends IPackageManager.Stub } @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 93729d1949b0..e25cca43e8da 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.ParceledListSlice; 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 @@ class PackageManagerShellCommand extends ShellCommand { } 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 fd6aceb1ce6b..3c22f07ad108 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.PackageManager; 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 @@ public abstract class PackageSettingBase extends SettingBase { 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 @@ public abstract class PackageSettingBase extends SettingBase { 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<String> enabledComponents, ArraySet<String> disabledComponents, @@ -438,7 +439,7 @@ public abstract class PackageSettingBase extends SettingBase { 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 5c88e0637092..6a7e65400fa7 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.PackageUserState; 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 final class Settings { 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 final class Settings { 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 @@ public final class Settings { false /*hidden*/, false /*suspended*/, null /*suspendingPackage*/, - null /*dialogMessage*/, + null /*dialogInfo*/, null /*suspendedAppExtras*/, null /*suspendedLauncherExtras*/, instantApp, @@ -1620,7 +1626,7 @@ public final class Settings { false /*hidden*/, false /*suspended*/, null /*suspendingPackage*/, - null /*dialogMessage*/, + null /*dialogInfo*/, null /*suspendedAppExtras*/, null /*suspendedLauncherExtras*/, false /*instantApp*/, @@ -1730,6 +1736,7 @@ public final class Settings { ArraySet<String> 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 @@ public final class Settings { 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 @@ public final class Settings { 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 @@ public final class Settings { 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 86541b95f395..65e4fa0f4aff 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.Intent; 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 class ActivityStartInterceptorTest { 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 c3c07880f605..517b5ade44b8 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.Context; 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 class PackageManagerSettingsTests { 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 class PackageManagerSettingsTests { 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 class PackageManagerSettingsTests { 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 class PackageManagerSettingsTests { 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 4a33ca37f767..f0ed612400ed 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.hamcrest.CoreMatchers.is; 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 @@ import org.junit.runner.RunWith; public class PackageUserStateTest { @Test - public void testPackageUserState01() { + public void testPackageUserState01() { final PackageUserState testUserState = new PackageUserState(); PackageUserState oldUserState; @@ -84,7 +85,7 @@ public class PackageUserStateTest { } @Test - public void testPackageUserState02() { + public void testPackageUserState02() { final PackageUserState testUserState01 = new PackageUserState(); PackageUserState oldUserState; @@ -102,7 +103,7 @@ public class PackageUserStateTest { } @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 class PackageUserStateTest { } @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 class PackageUserStateTest { 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 class PackageUserStateTest { 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 000000000000..7eccd6728533 --- /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 f115b9cd0fc5..553d234adfca 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.IntentFilter; 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 @@ public class SuspendPackagesTest { } void drainPendingBroadcasts() { - while (pollForIntent(5) != null); + while (pollForIntent(5) != null) ; } Intent receiveIntentFromApp() { @@ -215,15 +216,15 @@ public class SuspendPackagesTest { } 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 class SuspendPackagesTest { @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 class SuspendPackagesTest { 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 3bd3d68ba6cf..2b968aec1496 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); |