| /* |
| * Copyright (C) 2017 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.app; |
| |
| import android.content.ContentProvider; |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.graphics.drawable.Icon; |
| import android.os.Bundle; |
| import android.os.Parcel; |
| import android.os.Parcelable; |
| |
| import com.android.internal.util.Preconditions; |
| |
| /** |
| * Specialization of {@link SecurityException} that contains additional |
| * information about how to involve the end user to recover from the exception. |
| * <p> |
| * This exception is only appropriate where there is a concrete action the user |
| * can take to recover and make forward progress, such as confirming or entering |
| * authentication credentials, or granting access. |
| * <p> |
| * If the receiving app is actively involved with the user, it should present |
| * the contained recovery details to help the user make forward progress. The |
| * {@link #showAsDialog(Activity)} and |
| * {@link #showAsNotification(Context, String)} methods are provided as a |
| * convenience, but receiving apps are encouraged to use |
| * {@link #getUserMessage()} and {@link #getUserAction()} to integrate in a more |
| * natural way if relevant. |
| * <p class="note"> |
| * Note: legacy code that receives this exception may treat it as a general |
| * {@link SecurityException}, and thus there is no guarantee that the messages |
| * contained will be shown to the end user. |
| * |
| * @hide |
| */ |
| public final class RecoverableSecurityException extends SecurityException implements Parcelable { |
| private static final String TAG = "RecoverableSecurityException"; |
| |
| private final CharSequence mUserMessage; |
| private final RemoteAction mUserAction; |
| |
| /** {@hide} */ |
| public RecoverableSecurityException(Parcel in) { |
| this(new SecurityException(in.readString()), in.readCharSequence(), |
| RemoteAction.CREATOR.createFromParcel(in)); |
| } |
| |
| /** |
| * Create an instance ready to be thrown. |
| * |
| * @param cause original cause with details designed for engineering |
| * audiences. |
| * @param userMessage short message describing the issue for end user |
| * audiences, which may be shown in a notification or dialog. |
| * This should be localized and less than 64 characters. For |
| * example: <em>PIN required to access Document.pdf</em> |
| * @param userAction primary action that will initiate the recovery. The |
| * title should be localized and less than 24 characters. For |
| * example: <em>Enter PIN</em>. This action must launch an |
| * activity that is expected to set |
| * {@link Activity#setResult(int)} before finishing to |
| * communicate the final status of the recovery. For example, |
| * apps that observe {@link Activity#RESULT_OK} may choose to |
| * immediately retry their operation. |
| */ |
| public RecoverableSecurityException(Throwable cause, CharSequence userMessage, |
| RemoteAction userAction) { |
| super(cause.getMessage()); |
| mUserMessage = Preconditions.checkNotNull(userMessage); |
| mUserAction = Preconditions.checkNotNull(userAction); |
| } |
| |
| /** {@hide} */ |
| @Deprecated |
| public RecoverableSecurityException(Throwable cause, CharSequence userMessage, |
| CharSequence userActionTitle, PendingIntent userAction) { |
| this(cause, userMessage, |
| new RemoteAction( |
| Icon.createWithResource("android", |
| com.android.internal.R.drawable.ic_restart), |
| userActionTitle, userActionTitle, userAction)); |
| } |
| |
| /** |
| * Return short message describing the issue for end user audiences, which |
| * may be shown in a notification or dialog. |
| */ |
| public CharSequence getUserMessage() { |
| return mUserMessage; |
| } |
| |
| /** |
| * Return primary action that will initiate the recovery. |
| */ |
| public RemoteAction getUserAction() { |
| return mUserAction; |
| } |
| |
| /** @removed */ |
| @Deprecated |
| public void showAsNotification(Context context) { |
| final NotificationManager nm = context.getSystemService(NotificationManager.class); |
| |
| // Create a channel per-sender, since we don't want one poorly behaved |
| // remote app to cause all of our notifications to be blocked |
| final String channelId = TAG + "_" + mUserAction.getActionIntent().getCreatorUid(); |
| nm.createNotificationChannel(new NotificationChannel(channelId, TAG, |
| NotificationManager.IMPORTANCE_DEFAULT)); |
| |
| showAsNotification(context, channelId); |
| } |
| |
| /** |
| * Convenience method that will show a very simple notification populated |
| * with the details from this exception. |
| * <p> |
| * If you want more flexibility over retrying your original operation once |
| * the user action has finished, consider presenting your own UI that uses |
| * {@link Activity#startIntentSenderForResult} to launch the |
| * {@link PendingIntent#getIntentSender()} from {@link #getUserAction()} |
| * when requested. If the result of that activity is |
| * {@link Activity#RESULT_OK}, you should consider retrying. |
| * <p> |
| * This method will only display the most recent exception from any single |
| * remote UID; notifications from older exceptions will always be replaced. |
| * |
| * @param channelId the {@link NotificationChannel} to use, which must have |
| * been already created using |
| * {@link NotificationManager#createNotificationChannel}. |
| */ |
| public void showAsNotification(Context context, String channelId) { |
| final NotificationManager nm = context.getSystemService(NotificationManager.class); |
| final Notification.Builder builder = new Notification.Builder(context, channelId) |
| .setSmallIcon(com.android.internal.R.drawable.ic_print_error) |
| .setContentTitle(mUserAction.getTitle()) |
| .setContentText(mUserMessage) |
| .setContentIntent(mUserAction.getActionIntent()) |
| .setCategory(Notification.CATEGORY_ERROR); |
| nm.notify(TAG, mUserAction.getActionIntent().getCreatorUid(), builder.build()); |
| } |
| |
| /** |
| * Convenience method that will show a very simple dialog populated with the |
| * details from this exception. |
| * <p> |
| * If you want more flexibility over retrying your original operation once |
| * the user action has finished, consider presenting your own UI that uses |
| * {@link Activity#startIntentSenderForResult} to launch the |
| * {@link PendingIntent#getIntentSender()} from {@link #getUserAction()} |
| * when requested. If the result of that activity is |
| * {@link Activity#RESULT_OK}, you should consider retrying. |
| * <p> |
| * This method will only display the most recent exception from any single |
| * remote UID; dialogs from older exceptions will always be replaced. |
| */ |
| public void showAsDialog(Activity activity) { |
| final LocalDialog dialog = new LocalDialog(); |
| final Bundle args = new Bundle(); |
| args.putParcelable(TAG, this); |
| dialog.setArguments(args); |
| |
| final String tag = TAG + "_" + mUserAction.getActionIntent().getCreatorUid(); |
| final FragmentManager fm = activity.getFragmentManager(); |
| final FragmentTransaction ft = fm.beginTransaction(); |
| final Fragment old = fm.findFragmentByTag(tag); |
| if (old != null) { |
| ft.remove(old); |
| } |
| ft.add(dialog, tag); |
| ft.commitAllowingStateLoss(); |
| } |
| |
| /** |
| * Implementation detail for |
| * {@link RecoverableSecurityException#showAsDialog(Activity)}; needs to |
| * remain static to be recreated across orientation changes. |
| * |
| * @hide |
| */ |
| public static class LocalDialog extends DialogFragment { |
| @Override |
| public Dialog onCreateDialog(Bundle savedInstanceState) { |
| final RecoverableSecurityException e = getArguments().getParcelable(TAG); |
| return new AlertDialog.Builder(getActivity()) |
| .setMessage(e.mUserMessage) |
| .setPositiveButton(e.mUserAction.getTitle(), (dialog, which) -> { |
| try { |
| e.mUserAction.getActionIntent().send(); |
| } catch (PendingIntent.CanceledException ignored) { |
| } |
| }) |
| .setNegativeButton(android.R.string.cancel, null) |
| .create(); |
| } |
| } |
| |
| @Override |
| public int describeContents() { |
| return 0; |
| } |
| |
| @Override |
| public void writeToParcel(Parcel dest, int flags) { |
| dest.writeString(getMessage()); |
| dest.writeCharSequence(mUserMessage); |
| mUserAction.writeToParcel(dest, flags); |
| } |
| |
| public static final Creator<RecoverableSecurityException> CREATOR = |
| new Creator<RecoverableSecurityException>() { |
| @Override |
| public RecoverableSecurityException createFromParcel(Parcel source) { |
| return new RecoverableSecurityException(source); |
| } |
| |
| @Override |
| public RecoverableSecurityException[] newArray(int size) { |
| return new RecoverableSecurityException[size]; |
| } |
| }; |
| } |