206 lines
8.4 KiB
Java
206 lines
8.4 KiB
Java
/*
|
|
* 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.annotation.NonNull;
|
|
import android.content.Context;
|
|
import android.os.Bundle;
|
|
import android.os.Parcel;
|
|
import android.os.Parcelable;
|
|
|
|
import java.util.Objects;
|
|
|
|
/**
|
|
* 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.
|
|
* <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.
|
|
*/
|
|
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(@NonNull Throwable cause, @NonNull CharSequence userMessage,
|
|
@NonNull RemoteAction userAction) {
|
|
super(cause.getMessage());
|
|
mUserMessage = Objects.requireNonNull(userMessage);
|
|
mUserAction = Objects.requireNonNull(userAction);
|
|
}
|
|
|
|
/**
|
|
* Return short message describing the issue for end user audiences, which
|
|
* may be shown in a notification or dialog.
|
|
*/
|
|
public @NonNull CharSequence getUserMessage() {
|
|
return mUserMessage;
|
|
}
|
|
|
|
/**
|
|
* Return primary action that will initiate the recovery.
|
|
*/
|
|
public @NonNull RemoteAction getUserAction() {
|
|
return mUserAction;
|
|
}
|
|
|
|
/**
|
|
* 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}.
|
|
* @hide
|
|
*/
|
|
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.
|
|
*
|
|
* @hide
|
|
*/
|
|
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, android.app.RecoverableSecurityException.class);
|
|
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 @android.annotation.NonNull 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];
|
|
}
|
|
};
|
|
}
|