diff options
| author | TreeHugger Robot <treehugger-gerrit@google.com> | 2018-10-15 21:51:52 +0000 |
|---|---|---|
| committer | Android (Google) Code Review <android-gerrit@google.com> | 2018-10-15 21:51:52 +0000 |
| commit | bc9ebba7e50ca88bec4a4f68bfe35289ab0acfef (patch) | |
| tree | 456abb614ada323f4aac7cb39e59304e8f893edb /core/java/android | |
| parent | f1246180a553a20675a9f3c2ad05118de86e739a (diff) | |
| parent | 389cb6f54a5a5bb8dea540f57a3a8ac3c3c1c758 (diff) | |
Merge "Suspending app can customize intercepting dialog"
Diffstat (limited to 'core/java/android')
7 files changed, 487 insertions, 13 deletions
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 733fbe566b09..dfb8128e37ee 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -5664,7 +5664,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 @@ -5676,15 +5676,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 7b4c6fc64a69..4f58321da2f4 100644 --- a/core/java/android/content/pm/PackageManagerInternal.java +++ b/core/java/android/content/pm/PackageManagerInternal.java @@ -267,14 +267,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); + } + } +} |
