1 /* 2 * Copyright (C) 2017 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.app; 18 19 import android.content.ContentProvider; 20 import android.content.ContentResolver; 21 import android.content.Context; 22 import android.graphics.drawable.Icon; 23 import android.os.Bundle; 24 import android.os.Parcel; 25 import android.os.Parcelable; 26 27 import com.android.internal.util.Preconditions; 28 29 /** 30 * Specialization of {@link SecurityException} that contains additional 31 * information about how to involve the end user to recover from the exception. 32 * <p> 33 * This exception is only appropriate where there is a concrete action the user 34 * can take to recover and make forward progress, such as confirming or entering 35 * authentication credentials, or granting access. 36 * <p> 37 * If the receiving app is actively involved with the user, it should present 38 * the contained recovery details to help the user make forward progress. The 39 * {@link #showAsDialog(Activity)} and 40 * {@link #showAsNotification(Context, String)} methods are provided as a 41 * convenience, but receiving apps are encouraged to use 42 * {@link #getUserMessage()} and {@link #getUserAction()} to integrate in a more 43 * natural way if relevant. 44 * <p class="note"> 45 * Note: legacy code that receives this exception may treat it as a general 46 * {@link SecurityException}, and thus there is no guarantee that the messages 47 * contained will be shown to the end user. 48 * 49 * @hide 50 */ 51 public final class RecoverableSecurityException extends SecurityException implements Parcelable { 52 private static final String TAG = "RecoverableSecurityException"; 53 54 private final CharSequence mUserMessage; 55 private final RemoteAction mUserAction; 56 57 /** {@hide} */ RecoverableSecurityException(Parcel in)58 public RecoverableSecurityException(Parcel in) { 59 this(new SecurityException(in.readString()), in.readCharSequence(), 60 RemoteAction.CREATOR.createFromParcel(in)); 61 } 62 63 /** 64 * Create an instance ready to be thrown. 65 * 66 * @param cause original cause with details designed for engineering 67 * audiences. 68 * @param userMessage short message describing the issue for end user 69 * audiences, which may be shown in a notification or dialog. 70 * This should be localized and less than 64 characters. For 71 * example: <em>PIN required to access Document.pdf</em> 72 * @param userAction primary action that will initiate the recovery. The 73 * title should be localized and less than 24 characters. For 74 * example: <em>Enter PIN</em>. This action must launch an 75 * activity that is expected to set 76 * {@link Activity#setResult(int)} before finishing to 77 * communicate the final status of the recovery. For example, 78 * apps that observe {@link Activity#RESULT_OK} may choose to 79 * immediately retry their operation. 80 */ RecoverableSecurityException(Throwable cause, CharSequence userMessage, RemoteAction userAction)81 public RecoverableSecurityException(Throwable cause, CharSequence userMessage, 82 RemoteAction userAction) { 83 super(cause.getMessage()); 84 mUserMessage = Preconditions.checkNotNull(userMessage); 85 mUserAction = Preconditions.checkNotNull(userAction); 86 } 87 88 /** {@hide} */ 89 @Deprecated RecoverableSecurityException(Throwable cause, CharSequence userMessage, CharSequence userActionTitle, PendingIntent userAction)90 public RecoverableSecurityException(Throwable cause, CharSequence userMessage, 91 CharSequence userActionTitle, PendingIntent userAction) { 92 this(cause, userMessage, 93 new RemoteAction( 94 Icon.createWithResource("android", 95 com.android.internal.R.drawable.ic_restart), 96 userActionTitle, userActionTitle, userAction)); 97 } 98 99 /** 100 * Return short message describing the issue for end user audiences, which 101 * may be shown in a notification or dialog. 102 */ getUserMessage()103 public CharSequence getUserMessage() { 104 return mUserMessage; 105 } 106 107 /** 108 * Return primary action that will initiate the recovery. 109 */ getUserAction()110 public RemoteAction getUserAction() { 111 return mUserAction; 112 } 113 114 /** @removed */ 115 @Deprecated showAsNotification(Context context)116 public void showAsNotification(Context context) { 117 final NotificationManager nm = context.getSystemService(NotificationManager.class); 118 119 // Create a channel per-sender, since we don't want one poorly behaved 120 // remote app to cause all of our notifications to be blocked 121 final String channelId = TAG + "_" + mUserAction.getActionIntent().getCreatorUid(); 122 nm.createNotificationChannel(new NotificationChannel(channelId, TAG, 123 NotificationManager.IMPORTANCE_DEFAULT)); 124 125 showAsNotification(context, channelId); 126 } 127 128 /** 129 * Convenience method that will show a very simple notification populated 130 * with the details from this exception. 131 * <p> 132 * If you want more flexibility over retrying your original operation once 133 * the user action has finished, consider presenting your own UI that uses 134 * {@link Activity#startIntentSenderForResult} to launch the 135 * {@link PendingIntent#getIntentSender()} from {@link #getUserAction()} 136 * when requested. If the result of that activity is 137 * {@link Activity#RESULT_OK}, you should consider retrying. 138 * <p> 139 * This method will only display the most recent exception from any single 140 * remote UID; notifications from older exceptions will always be replaced. 141 * 142 * @param channelId the {@link NotificationChannel} to use, which must have 143 * been already created using 144 * {@link NotificationManager#createNotificationChannel}. 145 */ showAsNotification(Context context, String channelId)146 public void showAsNotification(Context context, String channelId) { 147 final NotificationManager nm = context.getSystemService(NotificationManager.class); 148 final Notification.Builder builder = new Notification.Builder(context, channelId) 149 .setSmallIcon(com.android.internal.R.drawable.ic_print_error) 150 .setContentTitle(mUserAction.getTitle()) 151 .setContentText(mUserMessage) 152 .setContentIntent(mUserAction.getActionIntent()) 153 .setCategory(Notification.CATEGORY_ERROR); 154 nm.notify(TAG, mUserAction.getActionIntent().getCreatorUid(), builder.build()); 155 } 156 157 /** 158 * Convenience method that will show a very simple dialog populated with the 159 * details from this exception. 160 * <p> 161 * If you want more flexibility over retrying your original operation once 162 * the user action has finished, consider presenting your own UI that uses 163 * {@link Activity#startIntentSenderForResult} to launch the 164 * {@link PendingIntent#getIntentSender()} from {@link #getUserAction()} 165 * when requested. If the result of that activity is 166 * {@link Activity#RESULT_OK}, you should consider retrying. 167 * <p> 168 * This method will only display the most recent exception from any single 169 * remote UID; dialogs from older exceptions will always be replaced. 170 */ showAsDialog(Activity activity)171 public void showAsDialog(Activity activity) { 172 final LocalDialog dialog = new LocalDialog(); 173 final Bundle args = new Bundle(); 174 args.putParcelable(TAG, this); 175 dialog.setArguments(args); 176 177 final String tag = TAG + "_" + mUserAction.getActionIntent().getCreatorUid(); 178 final FragmentManager fm = activity.getFragmentManager(); 179 final FragmentTransaction ft = fm.beginTransaction(); 180 final Fragment old = fm.findFragmentByTag(tag); 181 if (old != null) { 182 ft.remove(old); 183 } 184 ft.add(dialog, tag); 185 ft.commitAllowingStateLoss(); 186 } 187 188 /** 189 * Implementation detail for 190 * {@link RecoverableSecurityException#showAsDialog(Activity)}; needs to 191 * remain static to be recreated across orientation changes. 192 * 193 * @hide 194 */ 195 public static class LocalDialog extends DialogFragment { 196 @Override onCreateDialog(Bundle savedInstanceState)197 public Dialog onCreateDialog(Bundle savedInstanceState) { 198 final RecoverableSecurityException e = getArguments().getParcelable(TAG); 199 return new AlertDialog.Builder(getActivity()) 200 .setMessage(e.mUserMessage) 201 .setPositiveButton(e.mUserAction.getTitle(), (dialog, which) -> { 202 try { 203 e.mUserAction.getActionIntent().send(); 204 } catch (PendingIntent.CanceledException ignored) { 205 } 206 }) 207 .setNegativeButton(android.R.string.cancel, null) 208 .create(); 209 } 210 } 211 212 @Override 213 public int describeContents() { 214 return 0; 215 } 216 217 @Override 218 public void writeToParcel(Parcel dest, int flags) { 219 dest.writeString(getMessage()); 220 dest.writeCharSequence(mUserMessage); 221 mUserAction.writeToParcel(dest, flags); 222 } 223 224 public static final Creator<RecoverableSecurityException> CREATOR = 225 new Creator<RecoverableSecurityException>() { 226 @Override 227 public RecoverableSecurityException createFromParcel(Parcel source) { 228 return new RecoverableSecurityException(source); 229 } 230 231 @Override 232 public RecoverableSecurityException[] newArray(int size) { 233 return new RecoverableSecurityException[size]; 234 } 235 }; 236 } 237