1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.content.pm; 18 19 import static android.content.res.Resources.ID_NULL; 20 21 import android.annotation.DrawableRes; 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.annotation.StringRes; 25 import android.annotation.SystemApi; 26 import android.content.res.ResourceId; 27 import android.os.Parcel; 28 import android.os.Parcelable; 29 import android.os.PersistableBundle; 30 import android.util.Slog; 31 32 import com.android.internal.util.Preconditions; 33 import com.android.internal.util.XmlUtils; 34 35 import org.xmlpull.v1.XmlPullParser; 36 import org.xmlpull.v1.XmlSerializer; 37 38 import java.io.IOException; 39 import java.util.Locale; 40 import java.util.Objects; 41 42 /** 43 * A container to describe the dialog to be shown when the user tries to launch a suspended 44 * application. 45 * The suspending app can customize the dialog's following attributes: 46 * <ul> 47 * <li>The dialog icon, by providing a resource id. 48 * <li>The title text, by providing a resource id. 49 * <li>The text of the dialog's body, by providing a resource id or a string. 50 * <li>The text on the neutral button which starts the 51 * {@link android.content.Intent#ACTION_SHOW_SUSPENDED_APP_DETAILS SHOW_SUSPENDED_APP_DETAILS} 52 * activity, by providing a resource id. 53 * </ul> 54 * System defaults are used whenever any of these are not provided, or any of the provided resource 55 * ids cannot be resolved at the time of displaying the dialog. 56 * 57 * @hide 58 * @see PackageManager#setPackagesSuspended(String[], boolean, PersistableBundle, PersistableBundle, 59 * SuspendDialogInfo) 60 * @see Builder 61 */ 62 @SystemApi 63 public final class SuspendDialogInfo implements Parcelable { 64 private static final String TAG = SuspendDialogInfo.class.getSimpleName(); 65 private static final String XML_ATTR_ICON_RES_ID = "iconResId"; 66 private static final String XML_ATTR_TITLE_RES_ID = "titleResId"; 67 private static final String XML_ATTR_DIALOG_MESSAGE_RES_ID = "dialogMessageResId"; 68 private static final String XML_ATTR_DIALOG_MESSAGE = "dialogMessage"; 69 private static final String XML_ATTR_BUTTON_TEXT_RES_ID = "buttonTextResId"; 70 71 private final int mIconResId; 72 private final int mTitleResId; 73 private final int mDialogMessageResId; 74 private final String mDialogMessage; 75 private final int mNeutralButtonTextResId; 76 77 /** 78 * @return the resource id of the icon to be used with the dialog 79 * @hide 80 */ 81 @DrawableRes getIconResId()82 public int getIconResId() { 83 return mIconResId; 84 } 85 86 /** 87 * @return the resource id of the title to be used with the dialog 88 * @hide 89 */ 90 @StringRes getTitleResId()91 public int getTitleResId() { 92 return mTitleResId; 93 } 94 95 /** 96 * @return the resource id of the text to be shown in the dialog's body 97 * @hide 98 */ 99 @StringRes getDialogMessageResId()100 public int getDialogMessageResId() { 101 return mDialogMessageResId; 102 } 103 104 /** 105 * @return the text to be shown in the dialog's body. Returns {@code null} if 106 * {@link #getDialogMessageResId()} returns a valid resource id. 107 * @hide 108 */ 109 @Nullable getDialogMessage()110 public String getDialogMessage() { 111 return mDialogMessage; 112 } 113 114 /** 115 * @return the text to be shown 116 * @hide 117 */ 118 @StringRes getNeutralButtonTextResId()119 public int getNeutralButtonTextResId() { 120 return mNeutralButtonTextResId; 121 } 122 123 /** 124 * @hide 125 */ saveToXml(XmlSerializer out)126 public void saveToXml(XmlSerializer out) throws IOException { 127 if (mIconResId != ID_NULL) { 128 XmlUtils.writeIntAttribute(out, XML_ATTR_ICON_RES_ID, mIconResId); 129 } 130 if (mTitleResId != ID_NULL) { 131 XmlUtils.writeIntAttribute(out, XML_ATTR_TITLE_RES_ID, mTitleResId); 132 } 133 if (mDialogMessageResId != ID_NULL) { 134 XmlUtils.writeIntAttribute(out, XML_ATTR_DIALOG_MESSAGE_RES_ID, mDialogMessageResId); 135 } else { 136 XmlUtils.writeStringAttribute(out, XML_ATTR_DIALOG_MESSAGE, mDialogMessage); 137 } 138 if (mNeutralButtonTextResId != ID_NULL) { 139 XmlUtils.writeIntAttribute(out, XML_ATTR_BUTTON_TEXT_RES_ID, mNeutralButtonTextResId); 140 } 141 } 142 143 /** 144 * @hide 145 */ restoreFromXml(XmlPullParser in)146 public static SuspendDialogInfo restoreFromXml(XmlPullParser in) { 147 final SuspendDialogInfo.Builder dialogInfoBuilder = new SuspendDialogInfo.Builder(); 148 try { 149 final int iconId = XmlUtils.readIntAttribute(in, XML_ATTR_ICON_RES_ID, ID_NULL); 150 final int titleId = XmlUtils.readIntAttribute(in, XML_ATTR_TITLE_RES_ID, ID_NULL); 151 final int buttonTextId = XmlUtils.readIntAttribute(in, XML_ATTR_BUTTON_TEXT_RES_ID, 152 ID_NULL); 153 final int dialogMessageResId = XmlUtils.readIntAttribute( 154 in, XML_ATTR_DIALOG_MESSAGE_RES_ID, ID_NULL); 155 final String dialogMessage = XmlUtils.readStringAttribute(in, XML_ATTR_DIALOG_MESSAGE); 156 157 if (iconId != ID_NULL) { 158 dialogInfoBuilder.setIcon(iconId); 159 } 160 if (titleId != ID_NULL) { 161 dialogInfoBuilder.setTitle(titleId); 162 } 163 if (buttonTextId != ID_NULL) { 164 dialogInfoBuilder.setNeutralButtonText(buttonTextId); 165 } 166 if (dialogMessageResId != ID_NULL) { 167 dialogInfoBuilder.setMessage(dialogMessageResId); 168 } else if (dialogMessage != null) { 169 dialogInfoBuilder.setMessage(dialogMessage); 170 } 171 } catch (Exception e) { 172 Slog.e(TAG, "Exception while parsing from xml. Some fields may default", e); 173 } 174 return dialogInfoBuilder.build(); 175 } 176 177 @Override hashCode()178 public int hashCode() { 179 int hashCode = mIconResId; 180 hashCode = 31 * hashCode + mTitleResId; 181 hashCode = 31 * hashCode + mNeutralButtonTextResId; 182 hashCode = 31 * hashCode + mDialogMessageResId; 183 hashCode = 31 * hashCode + Objects.hashCode(mDialogMessage); 184 return hashCode; 185 } 186 187 @Override equals(Object obj)188 public boolean equals(Object obj) { 189 if (this == obj) { 190 return true; 191 } 192 if (!(obj instanceof SuspendDialogInfo)) { 193 return false; 194 } 195 final SuspendDialogInfo otherDialogInfo = (SuspendDialogInfo) obj; 196 return mIconResId == otherDialogInfo.mIconResId 197 && mTitleResId == otherDialogInfo.mTitleResId 198 && mDialogMessageResId == otherDialogInfo.mDialogMessageResId 199 && mNeutralButtonTextResId == otherDialogInfo.mNeutralButtonTextResId 200 && Objects.equals(mDialogMessage, otherDialogInfo.mDialogMessage); 201 } 202 203 @Override toString()204 public String toString() { 205 final StringBuilder builder = new StringBuilder("SuspendDialogInfo: {"); 206 if (mIconResId != ID_NULL) { 207 builder.append("mIconId = 0x"); 208 builder.append(Integer.toHexString(mIconResId)); 209 builder.append(" "); 210 } 211 if (mTitleResId != ID_NULL) { 212 builder.append("mTitleResId = 0x"); 213 builder.append(Integer.toHexString(mTitleResId)); 214 builder.append(" "); 215 } 216 if (mNeutralButtonTextResId != ID_NULL) { 217 builder.append("mNeutralButtonTextResId = 0x"); 218 builder.append(Integer.toHexString(mNeutralButtonTextResId)); 219 builder.append(" "); 220 } 221 if (mDialogMessageResId != ID_NULL) { 222 builder.append("mDialogMessageResId = 0x"); 223 builder.append(Integer.toHexString(mDialogMessageResId)); 224 builder.append(" "); 225 } else if (mDialogMessage != null) { 226 builder.append("mDialogMessage = \""); 227 builder.append(mDialogMessage); 228 builder.append("\" "); 229 } 230 builder.append("}"); 231 return builder.toString(); 232 } 233 234 @Override describeContents()235 public int describeContents() { 236 return 0; 237 } 238 239 @Override writeToParcel(Parcel dest, int parcelableFlags)240 public void writeToParcel(Parcel dest, int parcelableFlags) { 241 dest.writeInt(mIconResId); 242 dest.writeInt(mTitleResId); 243 dest.writeInt(mDialogMessageResId); 244 dest.writeString(mDialogMessage); 245 dest.writeInt(mNeutralButtonTextResId); 246 } 247 SuspendDialogInfo(Parcel source)248 private SuspendDialogInfo(Parcel source) { 249 mIconResId = source.readInt(); 250 mTitleResId = source.readInt(); 251 mDialogMessageResId = source.readInt(); 252 mDialogMessage = source.readString(); 253 mNeutralButtonTextResId = source.readInt(); 254 } 255 SuspendDialogInfo(Builder b)256 SuspendDialogInfo(Builder b) { 257 mIconResId = b.mIconResId; 258 mTitleResId = b.mTitleResId; 259 mDialogMessageResId = b.mDialogMessageResId; 260 mDialogMessage = (mDialogMessageResId == ID_NULL) ? b.mDialogMessage : null; 261 mNeutralButtonTextResId = b.mNeutralButtonTextResId; 262 } 263 264 public static final @android.annotation.NonNull Creator<SuspendDialogInfo> CREATOR = new Creator<SuspendDialogInfo>() { 265 @Override 266 public SuspendDialogInfo createFromParcel(Parcel source) { 267 return new SuspendDialogInfo(source); 268 } 269 270 @Override 271 public SuspendDialogInfo[] newArray(int size) { 272 return new SuspendDialogInfo[size]; 273 } 274 }; 275 276 /** 277 * Builder to build a {@link SuspendDialogInfo} object. 278 */ 279 public static final class Builder { 280 private int mDialogMessageResId = ID_NULL; 281 private String mDialogMessage; 282 private int mTitleResId = ID_NULL; 283 private int mIconResId = ID_NULL; 284 private int mNeutralButtonTextResId = ID_NULL; 285 286 /** 287 * Set the resource id of the icon to be used. If not provided, no icon will be shown. 288 * 289 * @param resId The resource id of the icon. 290 * @return this builder object. 291 */ 292 @NonNull setIcon(@rawableRes int resId)293 public Builder setIcon(@DrawableRes int resId) { 294 Preconditions.checkArgument(ResourceId.isValid(resId), "Invalid resource id provided"); 295 mIconResId = resId; 296 return this; 297 } 298 299 /** 300 * Set the resource id of the title text to be displayed. If this is not provided, the 301 * system will use a default title. 302 * 303 * @param resId The resource id of the title. 304 * @return this builder object. 305 */ 306 @NonNull setTitle(@tringRes int resId)307 public Builder setTitle(@StringRes int resId) { 308 Preconditions.checkArgument(ResourceId.isValid(resId), "Invalid resource id provided"); 309 mTitleResId = resId; 310 return this; 311 } 312 313 /** 314 * Set the text to show in the body of the dialog. Ignored if a resource id is set via 315 * {@link #setMessage(int)}. 316 * <p> 317 * The system will use {@link String#format(Locale, String, Object...) String.format} to 318 * insert the suspended app name into the message, so an example format string could be 319 * {@code "The app %1$s is currently suspended"}. This is optional - if the string passed in 320 * {@code message} does not accept an argument, it will be used as is. 321 * 322 * @param message The dialog message. 323 * @return this builder object. 324 * @see #setMessage(int) 325 */ 326 @NonNull setMessage(@onNull String message)327 public Builder setMessage(@NonNull String message) { 328 Preconditions.checkStringNotEmpty(message, "Message cannot be null or empty"); 329 mDialogMessage = message; 330 return this; 331 } 332 333 /** 334 * Set the resource id of the dialog message to be shown. If no dialog message is provided 335 * via either this method or {@link #setMessage(String)}, the system will use a 336 * default message. 337 * <p> 338 * The system will use {@link android.content.res.Resources#getString(int, Object...) 339 * getString} to insert the suspended app name into the message, so an example format string 340 * could be {@code "The app %1$s is currently suspended"}. This is optional - if the string 341 * referred to by {@code resId} does not accept an argument, it will be used as is. 342 * 343 * @param resId The resource id of the dialog message. 344 * @return this builder object. 345 * @see #setMessage(String) 346 */ 347 @NonNull setMessage(@tringRes int resId)348 public Builder setMessage(@StringRes int resId) { 349 Preconditions.checkArgument(ResourceId.isValid(resId), "Invalid resource id provided"); 350 mDialogMessageResId = resId; 351 return this; 352 } 353 354 /** 355 * Set the resource id of text to be shown on the neutral button. Tapping this button starts 356 * the {@link android.content.Intent#ACTION_SHOW_SUSPENDED_APP_DETAILS} activity. If this is 357 * not provided, the system will use a default text. 358 * 359 * @param resId The resource id of the button text 360 * @return this builder object. 361 */ 362 @NonNull setNeutralButtonText(@tringRes int resId)363 public Builder setNeutralButtonText(@StringRes int resId) { 364 Preconditions.checkArgument(ResourceId.isValid(resId), "Invalid resource id provided"); 365 mNeutralButtonTextResId = resId; 366 return this; 367 } 368 369 /** 370 * Build the final object based on given inputs. 371 * 372 * @return The {@link SuspendDialogInfo} object built using this builder. 373 */ 374 @NonNull build()375 public SuspendDialogInfo build() { 376 return new SuspendDialogInfo(this); 377 } 378 } 379 } 380