blob: db8f8c2387d60746924ad1aa95159ad2d8626d0d [file] [log] [blame]
/*
* 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.Resources.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 @android.annotation.NonNull 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);
}
}
}