/*
* Copyright (C) 2022 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 androidx.core.app;
import static android.app.PendingIntent.FLAG_IMMUTABLE;
import static android.app.PendingIntent.FLAG_MUTABLE;
import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.Handler;
import androidx.annotation.DoNotInline;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import java.io.Closeable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.concurrent.CountDownLatch;
/** Helper for accessing features in {@link PendingIntent}. */
public final class PendingIntentCompat {
@IntDef(
flag = true,
value = {
PendingIntent.FLAG_ONE_SHOT,
PendingIntent.FLAG_NO_CREATE,
PendingIntent.FLAG_CANCEL_CURRENT,
PendingIntent.FLAG_UPDATE_CURRENT,
Intent.FILL_IN_ACTION,
Intent.FILL_IN_DATA,
Intent.FILL_IN_CATEGORIES,
Intent.FILL_IN_COMPONENT,
Intent.FILL_IN_PACKAGE,
Intent.FILL_IN_SOURCE_BOUNDS,
Intent.FILL_IN_SELECTOR,
Intent.FILL_IN_CLIP_DATA
})
@Retention(RetentionPolicy.SOURCE)
@RestrictTo(RestrictTo.Scope.LIBRARY)
public @interface Flags {}
/**
* Retrieves a {@link PendingIntent} with mandatory mutability flag set on supported platform
* versions. The caller provides the flag as combination of all the other values except
* mutability flag. This method combines mutability flag when necessary. See {@link
* PendingIntent#getActivities(Context, int, Intent[], int, Bundle)}.
*/
public static @NonNull PendingIntent getActivities(
@NonNull Context context,
int requestCode,
@NonNull @SuppressLint("ArrayReturn") Intent[] intents,
@Flags int flags,
@Nullable Bundle options,
boolean isMutable) {
return PendingIntent.getActivities(context, requestCode, intents,
addMutabilityFlags(isMutable, flags), options);
}
/**
* Retrieves a {@link PendingIntent} with mandatory mutability flag set on supported platform
* versions. The caller provides the flag as combination of all the other values except
* mutability flag. This method combines mutability flag when necessary. See {@link
* PendingIntent#getActivities(Context, int, Intent[], int, Bundle)}.
*/
public static @NonNull PendingIntent getActivities(
@NonNull Context context,
int requestCode,
@NonNull @SuppressLint("ArrayReturn") Intent[] intents,
@Flags int flags,
boolean isMutable) {
return PendingIntent.getActivities(
context, requestCode, intents, addMutabilityFlags(isMutable, flags));
}
/**
* Retrieves a {@link PendingIntent} with mandatory mutability flag set on supported platform
* versions. The caller provides the flag as combination of all the other values except
* mutability flag. This method combines mutability flag when necessary.
*
* @return Returns an existing or new PendingIntent matching the given parameters. May return
* {@code null} only if {@link PendingIntent#FLAG_NO_CREATE} has been supplied.
* @see PendingIntent#getActivity(Context, int, Intent, int)
*/
public static @Nullable PendingIntent getActivity(
@NonNull Context context,
int requestCode,
@NonNull Intent intent,
@Flags int flags,
boolean isMutable) {
return PendingIntent.getActivity(
context, requestCode, intent, addMutabilityFlags(isMutable, flags));
}
/**
* Retrieves a {@link PendingIntent} with mandatory mutability flag set on supported platform
* versions. The caller provides the flag as combination of all the other values except
* mutability flag. This method combines mutability flag when necessary.
*
* @return Returns an existing or new PendingIntent matching the given parameters. May return
* {@code null} only if {@link PendingIntent#FLAG_NO_CREATE} has been supplied.
* @see PendingIntent#getActivity(Context, int, Intent, int, Bundle)
*/
public static @Nullable PendingIntent getActivity(
@NonNull Context context,
int requestCode,
@NonNull Intent intent,
@Flags int flags,
@Nullable Bundle options,
boolean isMutable) {
return PendingIntent.getActivity(context, requestCode, intent,
addMutabilityFlags(isMutable, flags), options);
}
/**
* Retrieves a {@link PendingIntent} with mandatory mutability flag set on supported platform
* versions. The caller provides the flag as combination of all the other values except
* mutability flag. This method combines mutability flag when necessary.
*
* @return Returns an existing or new PendingIntent matching the given parameters. May return
* {@code null} only if {@link PendingIntent#FLAG_NO_CREATE} has been supplied.
* @see PendingIntent#getBroadcast(Context, int, Intent, int)
*/
public static @Nullable PendingIntent getBroadcast(
@NonNull Context context,
int requestCode,
@NonNull Intent intent,
@Flags int flags,
boolean isMutable) {
return PendingIntent.getBroadcast(
context, requestCode, intent, addMutabilityFlags(isMutable, flags));
}
/**
* Retrieves a {@link PendingIntent} with mandatory mutability flag set on supported platform
* versions. The caller provides the flag as combination of all the other values except
* mutability flag. This method combines mutability flag when necessary. See {@link
* PendingIntent#getForegroundService(Context, int, Intent, int)} .
*/
@RequiresApi(26)
public static @NonNull PendingIntent getForegroundService(
@NonNull Context context,
int requestCode,
@NonNull Intent intent,
@Flags int flags,
boolean isMutable) {
return Api26Impl.getForegroundService(
context, requestCode, intent, addMutabilityFlags(isMutable, flags));
}
/**
* Retrieves a {@link PendingIntent} with mandatory mutability flag set on supported platform
* versions. The caller provides the flag as combination of all the other values except
* mutability flag. This method combines mutability flag when necessary.
*
* @return Returns an existing or new PendingIntent matching the given parameters. May return
* {@code null} only if {@link PendingIntent#FLAG_NO_CREATE} has been supplied.
* @see PendingIntent#getService(Context, int, Intent, int)
*/
public static @Nullable PendingIntent getService(
@NonNull Context context,
int requestCode,
@NonNull Intent intent,
@Flags int flags,
boolean isMutable) {
return PendingIntent.getService(
context, requestCode, intent, addMutabilityFlags(isMutable, flags));
}
/**
* {@link PendingIntent#send()} variants that support {@link PendingIntent.OnFinished} callbacks
* have a bug on many API levels that the callback may be invoked even if the PendingIntent was
* never sent (ie, such as if the PendingIntent was canceled, and the send() invocation threw a
* {@link PendingIntent.CanceledException}). Using this compatibility method fixes that bug and
* guarantees that {@link PendingIntent.OnFinished} callbacks will only be invoked if send()
* completed successfully.
*
* <p>See {@link PendingIntent#send(int, PendingIntent.OnFinished, Handler)}.
*/
@SuppressLint("LambdaLast") // compat shim so arguments should be in the same order
public static void send(
@NonNull PendingIntent pendingIntent,
int code,
@Nullable PendingIntent.OnFinished onFinished,
@Nullable Handler handler) throws PendingIntent.CanceledException {
try (GatedCallback gatedCallback = new GatedCallback(onFinished)) {
pendingIntent.send(code, gatedCallback.getCallback(), handler);
gatedCallback.complete();
}
}
/**
* {@link PendingIntent#send()} variants that support {@link PendingIntent.OnFinished} callbacks
* have a bug on many API levels that the callback may be invoked even if the PendingIntent was
* never sent (ie, such as if the PendingIntent was canceled, and the send() invocation threw a
* {@link PendingIntent.CanceledException}). Using this compatibility method fixes that bug and
* guarantees that {@link PendingIntent.OnFinished} callbacks will only be invoked if send()
* completed successfully.
*
* <p>See {@link PendingIntent#send(Context, int, Intent, PendingIntent.OnFinished, Handler)}.
*/
@SuppressLint("LambdaLast") // compat shim so arguments must be in the same order
public static void send(
@NonNull PendingIntent pendingIntent,
// compat shim so arguments must be in the same order
@SuppressLint("ContextFirst") @NonNull Context context,
int code,
@NonNull Intent intent,
@Nullable PendingIntent.OnFinished onFinished,
@Nullable Handler handler) throws PendingIntent.CanceledException {
send(pendingIntent, context, code, intent, onFinished, handler, null, null);
}
/**
* {@link PendingIntent#send()} variants that support {@link PendingIntent.OnFinished} callbacks
* have a bug on many API levels that the callback may be invoked even if the PendingIntent was
* never sent (ie, such as if the PendingIntent was canceled, and the send() invocation threw a
* {@link PendingIntent.CanceledException}). Using this compatibility method fixes that bug and
* guarantees that {@link PendingIntent.OnFinished} callbacks will only be invoked if send()
* completed successfully.
*
* <p>See {@link
* PendingIntent#send(Context, int, Intent, PendingIntent.OnFinished, Handler, String, Bundle)}
*/
@SuppressLint("LambdaLast") // compat shim so arguments must be in the same order
public static void send(
@NonNull PendingIntent pendingIntent,
// compat shim so arguments must be in the same order
@SuppressLint("ContextFirst") @NonNull Context context,
int code,
@NonNull Intent intent,
@Nullable PendingIntent.OnFinished onFinished,
@Nullable Handler handler,
@Nullable String requiredPermissions,
@Nullable Bundle options) throws PendingIntent.CanceledException {
try (GatedCallback gatedCallback = new GatedCallback(onFinished)) {
if (VERSION.SDK_INT >= VERSION_CODES.M) {
Api23Impl.send(
pendingIntent,
context,
code,
intent,
onFinished,
handler,
requiredPermissions,
options);
} else {
pendingIntent.send(context, code, intent, gatedCallback.getCallback(), handler,
requiredPermissions);
}
gatedCallback.complete();
}
}
private static int addMutabilityFlags(boolean isMutable, int flags) {
if (isMutable) {
if (VERSION.SDK_INT >= 31) {
flags |= FLAG_MUTABLE;
}
} else {
if (VERSION.SDK_INT >= 23) {
flags |= FLAG_IMMUTABLE;
}
}
return flags;
}
private PendingIntentCompat() {}
@RequiresApi(23)
private static class Api23Impl {
private Api23Impl() {}
@DoNotInline
public static void send(
@NonNull PendingIntent pendingIntent,
@NonNull Context context,
int code,
@NonNull Intent intent,
@Nullable PendingIntent.OnFinished onFinished,
@Nullable Handler handler,
@Nullable String requiredPermission,
@Nullable Bundle options) throws PendingIntent.CanceledException {
pendingIntent.send(
context,
code,
intent,
onFinished,
handler,
requiredPermission,
options);
}
}
@RequiresApi(26)
private static class Api26Impl {
private Api26Impl() {}
@DoNotInline
public static PendingIntent getForegroundService(
Context context, int requestCode, Intent intent, int flags) {
return PendingIntent.getForegroundService(context, requestCode, intent, flags);
}
}
// see b/201299281 for more info and context
private static class GatedCallback implements Closeable {
private final CountDownLatch mComplete = new CountDownLatch(1);
@Nullable
private PendingIntent.OnFinished mCallback;
private boolean mSuccess;
GatedCallback(@Nullable PendingIntent.OnFinished callback) {
this.mCallback = callback;
mSuccess = false;
}
@Nullable
public PendingIntent.OnFinished getCallback() {
if (mCallback == null) {
return null;
} else {
return this::onSendFinished;
}
}
public void complete() {
mSuccess = true;
}
@Override
public void close() {
if (!mSuccess) {
mCallback = null;
}
mComplete.countDown();
}
private void onSendFinished(
PendingIntent pendingIntent,
Intent intent,
int resultCode,
String resultData,
Bundle resultExtras) {
boolean interrupted = false;
try {
while (true) {
try {
mComplete.await();
break;
} catch (InterruptedException e) {
interrupted = true;
}
}
} finally {
if (interrupted) {
Thread.currentThread().interrupt();
}
}
if (mCallback != null) {
mCallback.onSendFinished(
pendingIntent,
intent,
resultCode,
resultData,
resultExtras);
mCallback = null;
}
}
}
}