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