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.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} */
     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      */
     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
     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      */
    103     public CharSequence getUserMessage() {
    104         return mUserMessage;
    105     }
    106 
    107     /**
    108      * Return primary action that will initiate the recovery.
    109      */
    110     public RemoteAction getUserAction() {
    111         return mUserAction;
    112     }
    113 
    114     /** @removed */
    115     @Deprecated
    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      */
    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      */
    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
    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