Merge "[GH] Implement PendingIntentCompat to enforce mutability flag." into androidx-main
diff --git a/core/core/api/current.txt b/core/core/api/current.txt
index 4919591..30803b3 100644
--- a/core/core/api/current.txt
+++ b/core/core/api/current.txt
@@ -800,6 +800,16 @@
method public void removeOnPictureInPictureModeChangedListener(androidx.core.util.Consumer<androidx.core.app.PictureInPictureModeChangedInfo!>);
}
+ public final class PendingIntentCompat {
+ method public static android.app.PendingIntent getActivities(android.content.Context, int, android.content.Intent![], int, android.os.Bundle, boolean);
+ method public static android.app.PendingIntent getActivities(android.content.Context, int, android.content.Intent![], int, boolean);
+ method public static android.app.PendingIntent getActivity(android.content.Context, int, android.content.Intent, int, boolean);
+ method public static android.app.PendingIntent getActivity(android.content.Context, int, android.content.Intent, int, android.os.Bundle, boolean);
+ method public static android.app.PendingIntent getBroadcast(android.content.Context, int, android.content.Intent, int, boolean);
+ method @RequiresApi(26) public static android.app.PendingIntent getForegroundService(android.content.Context, int, android.content.Intent, int, boolean);
+ method public static android.app.PendingIntent getService(android.content.Context, int, android.content.Intent, int, boolean);
+ }
+
public class Person {
method public static androidx.core.app.Person fromBundle(android.os.Bundle);
method public androidx.core.graphics.drawable.IconCompat? getIcon();
diff --git a/core/core/api/public_plus_experimental_current.txt b/core/core/api/public_plus_experimental_current.txt
index e539396..7aa1a18 100644
--- a/core/core/api/public_plus_experimental_current.txt
+++ b/core/core/api/public_plus_experimental_current.txt
@@ -800,6 +800,16 @@
method public void removeOnPictureInPictureModeChangedListener(androidx.core.util.Consumer<androidx.core.app.PictureInPictureModeChangedInfo!>);
}
+ public final class PendingIntentCompat {
+ method public static android.app.PendingIntent getActivities(android.content.Context, int, android.content.Intent![], int, android.os.Bundle, boolean);
+ method public static android.app.PendingIntent getActivities(android.content.Context, int, android.content.Intent![], int, boolean);
+ method public static android.app.PendingIntent getActivity(android.content.Context, int, android.content.Intent, int, boolean);
+ method public static android.app.PendingIntent getActivity(android.content.Context, int, android.content.Intent, int, android.os.Bundle, boolean);
+ method public static android.app.PendingIntent getBroadcast(android.content.Context, int, android.content.Intent, int, boolean);
+ method @RequiresApi(26) public static android.app.PendingIntent getForegroundService(android.content.Context, int, android.content.Intent, int, boolean);
+ method public static android.app.PendingIntent getService(android.content.Context, int, android.content.Intent, int, boolean);
+ }
+
public class Person {
method public static androidx.core.app.Person fromBundle(android.os.Bundle);
method public androidx.core.graphics.drawable.IconCompat? getIcon();
diff --git a/core/core/api/restricted_current.txt b/core/core/api/restricted_current.txt
index 86dafe9..cc32575 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -888,6 +888,16 @@
method public void removeOnPictureInPictureModeChangedListener(androidx.core.util.Consumer<androidx.core.app.PictureInPictureModeChangedInfo!>);
}
+ public final class PendingIntentCompat {
+ method public static android.app.PendingIntent getActivities(android.content.Context, int, android.content.Intent![], int, android.os.Bundle, boolean);
+ method public static android.app.PendingIntent getActivities(android.content.Context, int, android.content.Intent![], int, boolean);
+ method public static android.app.PendingIntent getActivity(android.content.Context, int, android.content.Intent, int, boolean);
+ method public static android.app.PendingIntent getActivity(android.content.Context, int, android.content.Intent, int, android.os.Bundle, boolean);
+ method public static android.app.PendingIntent getBroadcast(android.content.Context, int, android.content.Intent, int, boolean);
+ method @RequiresApi(26) public static android.app.PendingIntent getForegroundService(android.content.Context, int, android.content.Intent, int, boolean);
+ method public static android.app.PendingIntent getService(android.content.Context, int, android.content.Intent, int, boolean);
+ }
+
public class Person {
method @RequiresApi(28) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.core.app.Person fromAndroidPerson(android.app.Person);
method public static androidx.core.app.Person fromBundle(android.os.Bundle);
diff --git a/core/core/src/main/java/androidx/core/app/PendingIntentCompat.java b/core/core/src/main/java/androidx/core/app/PendingIntentCompat.java
new file mode 100644
index 0000000..db89748
--- /dev/null
+++ b/core/core/src/main/java/androidx/core/app/PendingIntentCompat.java
@@ -0,0 +1,234 @@
+/*
+ * 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;
+import android.os.Bundle;
+
+import androidx.annotation.DoNotInline;
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Helper for accessing features in {@link PendingIntent}. */
+public final class PendingIntentCompat {
+
+ /** @hide */
+ @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)
+ 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,
+ @NonNull Bundle options,
+ boolean isMutable) {
+ if (Build.VERSION.SDK_INT >= 16) {
+ return Api16Impl.getActivities(
+ context, requestCode, intents, addMutabilityFlags(isMutable, flags), options);
+ } else {
+ return PendingIntent.getActivities(context, requestCode, intents, 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,
+ 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. See {@link
+ * PendingIntent#getActivity(Context, int, Intent, int)}.
+ */
+ public static @NonNull 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. See {@link
+ * PendingIntent#getActivity(Context, int, Intent, int, Bundle)}.
+ */
+ public static @NonNull PendingIntent getActivity(
+ @NonNull Context context,
+ int requestCode,
+ @NonNull Intent intent,
+ @Flags int flags,
+ @NonNull Bundle options,
+ boolean isMutable) {
+ if (Build.VERSION.SDK_INT >= 16) {
+ return Api16Impl.getActivity(
+ context, requestCode, intent, addMutabilityFlags(isMutable, flags), options);
+ } else {
+ return PendingIntent.getActivity(context, requestCode, intent, 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#getBroadcast(Context, int, Intent, int)}.
+ */
+ public static @NonNull 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. See {@link
+ * PendingIntent#getService(Context, int, Intent, int)}.
+ */
+ public static @NonNull PendingIntent getService(
+ @NonNull Context context,
+ int requestCode,
+ @NonNull Intent intent,
+ @Flags int flags,
+ boolean isMutable) {
+ return PendingIntent.getService(
+ context, requestCode, intent, addMutabilityFlags(isMutable, flags));
+ }
+
+ private static int addMutabilityFlags(boolean isMutable, int flags) {
+ if (isMutable) {
+ if (Build.VERSION.SDK_INT >= 31) {
+ flags |= FLAG_MUTABLE;
+ }
+ } else {
+ if (Build.VERSION.SDK_INT >= 23) {
+ flags |= FLAG_IMMUTABLE;
+ }
+ }
+
+ return flags;
+ }
+
+ private PendingIntentCompat() {}
+
+ @RequiresApi(16)
+ private static class Api16Impl {
+ private Api16Impl() {}
+
+ @DoNotInline
+ public static @NonNull PendingIntent getActivities(
+ @NonNull Context context,
+ int requestCode,
+ @NonNull @SuppressLint("ArrayReturn") Intent[] intents,
+ @Flags int flags,
+ @NonNull Bundle options) {
+ return PendingIntent.getActivities(context, requestCode, intents, flags, options);
+ }
+
+ @DoNotInline
+ public static @NonNull PendingIntent getActivity(
+ @NonNull Context context,
+ int requestCode,
+ @NonNull Intent intent,
+ @Flags int flags,
+ @NonNull Bundle options) {
+ return PendingIntent.getActivity(context, requestCode, intent, flags, 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);
+ }
+ }
+}
diff --git a/core/core/src/test/java/androidx/core/app/PendingIntentCompatTest.java b/core/core/src/test/java/androidx/core/app/PendingIntentCompatTest.java
new file mode 100644
index 0000000..22eed6b
--- /dev/null
+++ b/core/core/src/test/java/androidx/core/app/PendingIntentCompatTest.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright 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 com.google.common.truth.Truth.assertThat;
+
+import static org.robolectric.Shadows.shadowOf;
+
+import android.annotation.TargetApi;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowPendingIntent;
+
+/** Unit test for {@link PendingIntentCompat}. */
+@RunWith(RobolectricTestRunner.class)
+public class PendingIntentCompatTest {
+ private final Context context = ApplicationProvider.getApplicationContext();
+
+ @Config(maxSdk = 22)
+ @Test
+ public void addMutabilityFlags_immutableOnPreM() {
+ int requestCode = 7465;
+ Intent intent = new Intent();
+ Bundle options = new Bundle();
+ ShadowPendingIntent shadow =
+ shadowOf(
+ PendingIntentCompat.getActivity(
+ context,
+ requestCode,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT,
+ options,
+ /* isMutable= */ false));
+ assertThat(shadow.isActivityIntent()).isTrue();
+ assertThat(shadow.getFlags()).isEqualTo(PendingIntent.FLAG_UPDATE_CURRENT);
+ assertThat(shadow.getRequestCode()).isEqualTo(requestCode);
+ }
+
+ @Config(minSdk = 23)
+ @Test
+ public void addMutabilityFlags_immutableOnMPlus() {
+ int requestCode = 7465;
+ Intent intent = new Intent();
+ Bundle options = new Bundle();
+ ShadowPendingIntent shadow =
+ shadowOf(
+ PendingIntentCompat.getActivity(
+ context,
+ requestCode,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT,
+ options,
+ /* isMutable= */ false));
+ assertThat(shadow.isActivityIntent()).isTrue();
+ assertThat(shadow.getFlags())
+ .isEqualTo(PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+ assertThat(shadow.getRequestCode()).isEqualTo(requestCode);
+ }
+
+ @Config(maxSdk = 30)
+ @Test
+ public void addMutabilityFlags_mutableOnPreS() {
+ int requestCode = 7465;
+ Intent intent = new Intent();
+ Bundle options = new Bundle();
+ ShadowPendingIntent shadow =
+ shadowOf(
+ PendingIntentCompat.getActivity(
+ context,
+ requestCode,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT,
+ options,
+ /* isMutable= */ true));
+ assertThat(shadow.isActivityIntent()).isTrue();
+ assertThat(shadow.getFlags()).isEqualTo(PendingIntent.FLAG_UPDATE_CURRENT);
+ assertThat(shadow.getRequestCode()).isEqualTo(requestCode);
+ }
+
+ @Config(minSdk = 31)
+ @Test
+ public void addMutabilityFlags_mutableOnSPlus() {
+ int requestCode = 7465;
+ Intent intent = new Intent();
+ Bundle options = new Bundle();
+ ShadowPendingIntent shadow =
+ shadowOf(
+ PendingIntentCompat.getActivity(
+ context,
+ requestCode,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT,
+ options,
+ /* isMutable= */ true));
+ assertThat(shadow.isActivityIntent()).isTrue();
+ assertThat(shadow.getFlags())
+ .isEqualTo(PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
+ assertThat(shadow.getRequestCode()).isEqualTo(requestCode);
+ }
+
+ @Test
+ public void getActivities_withBundle() {
+ int requestCode = 7465;
+ Intent[] intents = new Intent[] {};
+ Bundle options = new Bundle();
+ ShadowPendingIntent shadow =
+ shadowOf(
+ PendingIntentCompat.getActivities(
+ context,
+ requestCode,
+ intents,
+ PendingIntent.FLAG_UPDATE_CURRENT,
+ options,
+ /* isMutable= */ false));
+ assertThat(shadow.isActivityIntent()).isTrue();
+ }
+
+ @Test
+ public void getActivities() {
+ int requestCode = 7465;
+ Intent[] intents = new Intent[] {};
+ Bundle options = new Bundle();
+ ShadowPendingIntent shadow =
+ shadowOf(
+ PendingIntentCompat.getActivities(
+ context,
+ requestCode,
+ intents,
+ PendingIntent.FLAG_UPDATE_CURRENT,
+ /* isMutable= */ false));
+ assertThat(shadow.isActivityIntent()).isTrue();
+ }
+
+ @Test
+ public void getActivity_withBundle() {
+ int requestCode = 7465;
+ Intent intent = new Intent();
+ Bundle options = new Bundle();
+ ShadowPendingIntent shadow =
+ shadowOf(
+ PendingIntentCompat.getActivity(
+ context,
+ requestCode,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT,
+ options,
+ /* isMutable= */ false));
+ assertThat(shadow.isActivityIntent()).isTrue();
+ }
+
+ @Test
+ public void getActivity() {
+ int requestCode = 7465;
+ Intent intent = new Intent();
+ Bundle options = new Bundle();
+ ShadowPendingIntent shadow =
+ shadowOf(
+ PendingIntentCompat.getActivity(
+ context,
+ requestCode,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT,
+ /* isMutable= */ false));
+ assertThat(shadow.isActivityIntent()).isTrue();
+ }
+
+ @Test
+ public void getService() {
+ int requestCode = 7465;
+ Intent intent = new Intent();
+ Bundle options = new Bundle();
+ ShadowPendingIntent shadow =
+ shadowOf(
+ PendingIntentCompat.getService(
+ context,
+ requestCode,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT,
+ /* isMutable= */ false));
+ assertThat(shadow.isService()).isTrue();
+ }
+
+ @Test
+ public void getBroadcast() {
+ int requestCode = 7465;
+ Intent intent = new Intent();
+ Bundle options = new Bundle();
+ ShadowPendingIntent shadow =
+ shadowOf(
+ PendingIntentCompat.getBroadcast(
+ context,
+ requestCode,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT,
+ /* isMutable= */ false));
+ assertThat(shadow.isBroadcast()).isTrue();
+ }
+
+ @TargetApi(26)
+ @Config(minSdk = 26)
+ @Test
+ public void getForegroundService() {
+ int requestCode = 7465;
+ Intent intent = new Intent();
+ Bundle options = new Bundle();
+ ShadowPendingIntent shadow =
+ shadowOf(
+ PendingIntentCompat.getForegroundService(
+ context,
+ requestCode,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT,
+ /* isMutable= */ false));
+ assertThat(shadow.isForegroundService()).isTrue();
+ }
+}