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);