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
17package android.app;
18
19import android.content.ContentProvider;
20import android.content.ContentResolver;
21import android.content.Context;
22import android.graphics.drawable.Icon;
23import android.os.Bundle;
24import android.os.Parcel;
25import android.os.Parcelable;
26
27import 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 */
51public 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