1 /*
2  * Copyright (C) 2022 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 androidx.core.app;
18 
19 import static android.app.PendingIntent.FLAG_IMMUTABLE;
20 import static android.app.PendingIntent.FLAG_MUTABLE;
21 
22 import android.annotation.SuppressLint;
23 import android.app.PendingIntent;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.os.Build.VERSION;
27 import android.os.Build.VERSION_CODES;
28 import android.os.Bundle;
29 import android.os.Handler;
30 
31 import androidx.annotation.IntDef;
32 import androidx.annotation.RequiresApi;
33 import androidx.annotation.RestrictTo;
34 
35 import org.jspecify.annotations.NonNull;
36 import org.jspecify.annotations.Nullable;
37 
38 import java.io.Closeable;
39 import java.lang.annotation.Retention;
40 import java.lang.annotation.RetentionPolicy;
41 import java.util.concurrent.CountDownLatch;
42 
43 /** Helper for accessing features in {@link PendingIntent}. */
44 public final class PendingIntentCompat {
45 
46     @IntDef(
47             flag = true,
48             value = {
49                 PendingIntent.FLAG_ONE_SHOT,
50                 PendingIntent.FLAG_NO_CREATE,
51                 PendingIntent.FLAG_CANCEL_CURRENT,
52                 PendingIntent.FLAG_UPDATE_CURRENT,
53                 Intent.FILL_IN_ACTION,
54                 Intent.FILL_IN_DATA,
55                 Intent.FILL_IN_CATEGORIES,
56                 Intent.FILL_IN_COMPONENT,
57                 Intent.FILL_IN_PACKAGE,
58                 Intent.FILL_IN_SOURCE_BOUNDS,
59                 Intent.FILL_IN_SELECTOR,
60                 Intent.FILL_IN_CLIP_DATA
61             })
62     @Retention(RetentionPolicy.SOURCE)
63     @RestrictTo(RestrictTo.Scope.LIBRARY)
64     public @interface Flags {}
65 
66     /**
67      * Retrieves a {@link PendingIntent} with mandatory mutability flag set on supported platform
68      * versions. The caller provides the flag as combination of all the other values except
69      * mutability flag. This method combines mutability flag when necessary. See {@link
70      * PendingIntent#getActivities(Context, int, Intent[], int, Bundle)}.
71      */
getActivities( @onNull Context context, int requestCode, @SuppressLint("ArrayReturn") Intent @NonNull [] intents, @Flags int flags, @Nullable Bundle options, boolean isMutable)72     public static @NonNull PendingIntent getActivities(
73             @NonNull Context context,
74             int requestCode,
75             @SuppressLint("ArrayReturn") Intent @NonNull [] intents,
76             @Flags int flags,
77             @Nullable Bundle options,
78             boolean isMutable) {
79         return PendingIntent.getActivities(context, requestCode, intents,
80                 addMutabilityFlags(isMutable, flags), options);
81     }
82 
83     /**
84      * Retrieves a {@link PendingIntent} with mandatory mutability flag set on supported platform
85      * versions. The caller provides the flag as combination of all the other values except
86      * mutability flag. This method combines mutability flag when necessary. See {@link
87      * PendingIntent#getActivities(Context, int, Intent[], int, Bundle)}.
88      */
getActivities( @onNull Context context, int requestCode, @SuppressLint("ArrayReturn") Intent @NonNull [] intents, @Flags int flags, boolean isMutable)89     public static @NonNull PendingIntent getActivities(
90             @NonNull Context context,
91             int requestCode,
92             @SuppressLint("ArrayReturn") Intent @NonNull [] intents,
93             @Flags int flags,
94             boolean isMutable) {
95         return PendingIntent.getActivities(
96                 context, requestCode, intents, addMutabilityFlags(isMutable, flags));
97     }
98 
99     /**
100      * Retrieves a {@link PendingIntent} with mandatory mutability flag set on supported platform
101      * versions. The caller provides the flag as combination of all the other values except
102      * mutability flag. This method combines mutability flag when necessary.
103      *
104      * @return Returns an existing or new PendingIntent matching the given parameters. May return
105      *         {@code null} only if {@link PendingIntent#FLAG_NO_CREATE} has been supplied.
106      * @see PendingIntent#getActivity(Context, int, Intent, int)
107      */
getActivity( @onNull Context context, int requestCode, @NonNull Intent intent, @Flags int flags, boolean isMutable)108     public static @Nullable PendingIntent getActivity(
109             @NonNull Context context,
110             int requestCode,
111             @NonNull Intent intent,
112             @Flags int flags,
113             boolean isMutable) {
114         return PendingIntent.getActivity(
115                 context, requestCode, intent, addMutabilityFlags(isMutable, flags));
116     }
117 
118     /**
119      * Retrieves a {@link PendingIntent} with mandatory mutability flag set on supported platform
120      * versions. The caller provides the flag as combination of all the other values except
121      * mutability flag. This method combines mutability flag when necessary.
122      *
123      * @return Returns an existing or new PendingIntent matching the given parameters. May return
124      *         {@code null} only if {@link PendingIntent#FLAG_NO_CREATE} has been supplied.
125      * @see PendingIntent#getActivity(Context, int, Intent, int, Bundle)
126      */
getActivity( @onNull Context context, int requestCode, @NonNull Intent intent, @Flags int flags, @Nullable Bundle options, boolean isMutable)127     public static @Nullable PendingIntent getActivity(
128             @NonNull Context context,
129             int requestCode,
130             @NonNull Intent intent,
131             @Flags int flags,
132             @Nullable Bundle options,
133             boolean isMutable) {
134         return PendingIntent.getActivity(context, requestCode, intent,
135                 addMutabilityFlags(isMutable, flags), options);
136     }
137 
138     /**
139      * Retrieves a {@link PendingIntent} with mandatory mutability flag set on supported platform
140      * versions. The caller provides the flag as combination of all the other values except
141      * mutability flag. This method combines mutability flag when necessary.
142      *
143      * @return Returns an existing or new PendingIntent matching the given parameters. May return
144      *         {@code null} only if {@link PendingIntent#FLAG_NO_CREATE} has been supplied.
145      * @see PendingIntent#getBroadcast(Context, int, Intent, int)
146      */
getBroadcast( @onNull Context context, int requestCode, @NonNull Intent intent, @Flags int flags, boolean isMutable)147     public static @Nullable PendingIntent getBroadcast(
148             @NonNull Context context,
149             int requestCode,
150             @NonNull Intent intent,
151             @Flags int flags,
152             boolean isMutable) {
153         return PendingIntent.getBroadcast(
154                 context, requestCode, intent, addMutabilityFlags(isMutable, flags));
155     }
156 
157     /**
158      * Retrieves a {@link PendingIntent} with mandatory mutability flag set on supported platform
159      * versions. The caller provides the flag as combination of all the other values except
160      * mutability flag. This method combines mutability flag when necessary. See {@link
161      * PendingIntent#getForegroundService(Context, int, Intent, int)} .
162      */
163     @RequiresApi(26)
getForegroundService( @onNull Context context, int requestCode, @NonNull Intent intent, @Flags int flags, boolean isMutable)164     public static @NonNull PendingIntent getForegroundService(
165             @NonNull Context context,
166             int requestCode,
167             @NonNull Intent intent,
168             @Flags int flags,
169             boolean isMutable) {
170         return Api26Impl.getForegroundService(
171                 context, requestCode, intent, addMutabilityFlags(isMutable, flags));
172     }
173 
174     /**
175      * Retrieves a {@link PendingIntent} with mandatory mutability flag set on supported platform
176      * versions. The caller provides the flag as combination of all the other values except
177      * mutability flag. This method combines mutability flag when necessary.
178      *
179      * @return Returns an existing or new PendingIntent matching the given parameters. May return
180      *         {@code null} only if {@link PendingIntent#FLAG_NO_CREATE} has been supplied.
181      * @see PendingIntent#getService(Context, int, Intent, int)
182      */
getService( @onNull Context context, int requestCode, @NonNull Intent intent, @Flags int flags, boolean isMutable)183     public static @Nullable PendingIntent getService(
184             @NonNull Context context,
185             int requestCode,
186             @NonNull Intent intent,
187             @Flags int flags,
188             boolean isMutable) {
189         return PendingIntent.getService(
190                 context, requestCode, intent, addMutabilityFlags(isMutable, flags));
191     }
192 
193     /**
194      * {@link PendingIntent#send()} variants that support {@link PendingIntent.OnFinished} callbacks
195      * have a bug on many API levels that the callback may be invoked even if the PendingIntent was
196      * never sent (ie, such as if the PendingIntent was canceled, and the send() invocation threw a
197      * {@link PendingIntent.CanceledException}). Using this compatibility method fixes that bug and
198      * guarantees that {@link PendingIntent.OnFinished} callbacks will only be invoked if send()
199      * completed successfully.
200      *
201      * <p>See {@link PendingIntent#send(int, PendingIntent.OnFinished, Handler)}.
202      */
203     @SuppressLint("LambdaLast")  // compat shim so arguments should be in the same order
send( @onNull PendingIntent pendingIntent, int code, PendingIntent.@Nullable OnFinished onFinished, @Nullable Handler handler)204     public static void send(
205             @NonNull PendingIntent pendingIntent,
206             int code,
207             PendingIntent.@Nullable OnFinished onFinished,
208             @Nullable Handler handler) throws PendingIntent.CanceledException {
209         try (GatedCallback gatedCallback = new GatedCallback(onFinished)) {
210             pendingIntent.send(code, gatedCallback.getCallback(), handler);
211             gatedCallback.complete();
212         }
213     }
214 
215     /**
216      * {@link PendingIntent#send()} variants that support {@link PendingIntent.OnFinished} callbacks
217      * have a bug on many API levels that the callback may be invoked even if the PendingIntent was
218      * never sent (ie, such as if the PendingIntent was canceled, and the send() invocation threw a
219      * {@link PendingIntent.CanceledException}). Using this compatibility method fixes that bug and
220      * guarantees that {@link PendingIntent.OnFinished} callbacks will only be invoked if send()
221      * completed successfully.
222      *
223      * <p>See {@link PendingIntent#send(Context, int, Intent, PendingIntent.OnFinished, Handler)}.
224      */
225     @SuppressLint("LambdaLast")  // compat shim so arguments must be in the same order
send( @onNull PendingIntent pendingIntent, @SuppressLint("ContextFirst") @NonNull Context context, int code, @NonNull Intent intent, PendingIntent.@Nullable OnFinished onFinished, @Nullable Handler handler)226     public static void send(
227             @NonNull PendingIntent pendingIntent,
228             // compat shim so arguments must be in the same order
229             @SuppressLint("ContextFirst") @NonNull Context context,
230             int code,
231             @NonNull Intent intent,
232             PendingIntent.@Nullable OnFinished onFinished,
233             @Nullable Handler handler) throws PendingIntent.CanceledException {
234         send(pendingIntent, context, code, intent, onFinished, handler, null, null);
235     }
236 
237     /**
238      * {@link PendingIntent#send()} variants that support {@link PendingIntent.OnFinished} callbacks
239      * have a bug on many API levels that the callback may be invoked even if the PendingIntent was
240      * never sent (ie, such as if the PendingIntent was canceled, and the send() invocation threw a
241      * {@link PendingIntent.CanceledException}). Using this compatibility method fixes that bug and
242      * guarantees that {@link PendingIntent.OnFinished} callbacks will only be invoked if send()
243      * completed successfully.
244      *
245      * <p>See {@link
246      * PendingIntent#send(Context, int, Intent, PendingIntent.OnFinished, Handler, String, Bundle)}
247      */
248     @SuppressLint("LambdaLast")  // compat shim so arguments must be in the same order
send( @onNull PendingIntent pendingIntent, @SuppressLint("ContextFirst") @NonNull Context context, int code, @NonNull Intent intent, PendingIntent.@Nullable OnFinished onFinished, @Nullable Handler handler, @Nullable String requiredPermissions, @Nullable Bundle options)249     public static void send(
250             @NonNull PendingIntent pendingIntent,
251             // compat shim so arguments must be in the same order
252             @SuppressLint("ContextFirst") @NonNull Context context,
253             int code,
254             @NonNull Intent intent,
255             PendingIntent.@Nullable OnFinished onFinished,
256             @Nullable Handler handler,
257             @Nullable String requiredPermissions,
258             @Nullable Bundle options) throws PendingIntent.CanceledException {
259         try (GatedCallback gatedCallback = new GatedCallback(onFinished)) {
260             if (VERSION.SDK_INT >= VERSION_CODES.M) {
261                 Api23Impl.send(
262                         pendingIntent,
263                         context,
264                         code,
265                         intent,
266                         onFinished,
267                         handler,
268                         requiredPermissions,
269                         options);
270             } else {
271                 pendingIntent.send(context, code, intent, gatedCallback.getCallback(), handler,
272                         requiredPermissions);
273             }
274             gatedCallback.complete();
275         }
276     }
277 
addMutabilityFlags(boolean isMutable, int flags)278     static int addMutabilityFlags(boolean isMutable, int flags) {
279         if (isMutable) {
280             if (VERSION.SDK_INT >= 31) {
281                 flags |= FLAG_MUTABLE;
282             }
283         } else {
284             if (VERSION.SDK_INT >= 23) {
285                 flags |= FLAG_IMMUTABLE;
286             }
287         }
288 
289         return flags;
290     }
291 
PendingIntentCompat()292     private PendingIntentCompat() {}
293 
294     @RequiresApi(23)
295     private static class Api23Impl {
Api23Impl()296         private Api23Impl() {}
297 
send( @onNull PendingIntent pendingIntent, @NonNull Context context, int code, @NonNull Intent intent, PendingIntent.@Nullable OnFinished onFinished, @Nullable Handler handler, @Nullable String requiredPermission, @Nullable Bundle options)298         public static void send(
299                 @NonNull PendingIntent pendingIntent,
300                 @NonNull Context context,
301                 int code,
302                 @NonNull Intent intent,
303                 PendingIntent.@Nullable OnFinished onFinished,
304                 @Nullable Handler handler,
305                 @Nullable String requiredPermission,
306                 @Nullable Bundle options) throws PendingIntent.CanceledException {
307             pendingIntent.send(
308                     context,
309                     code,
310                     intent,
311                     onFinished,
312                     handler,
313                     requiredPermission,
314                     options);
315         }
316     }
317 
318     @RequiresApi(26)
319     private static class Api26Impl {
Api26Impl()320         private Api26Impl() {}
321 
getForegroundService( Context context, int requestCode, Intent intent, int flags)322         public static PendingIntent getForegroundService(
323                 Context context, int requestCode, Intent intent, int flags) {
324             return PendingIntent.getForegroundService(context, requestCode, intent, flags);
325         }
326     }
327 
328     // see b/201299281 for more info and context
329     private static class GatedCallback implements Closeable {
330 
331         private final CountDownLatch mComplete = new CountDownLatch(1);
332 
333         private PendingIntent.@Nullable OnFinished mCallback;
334         private boolean mSuccess;
335 
GatedCallback(PendingIntent.@ullable OnFinished callback)336         GatedCallback(PendingIntent.@Nullable OnFinished callback) {
337             this.mCallback = callback;
338             mSuccess = false;
339         }
340 
getCallback()341         public PendingIntent.@Nullable OnFinished getCallback() {
342             if (mCallback == null) {
343                 return null;
344             } else {
345                 return this::onSendFinished;
346             }
347         }
348 
complete()349         public void complete() {
350             mSuccess = true;
351         }
352 
353         @Override
close()354         public void close() {
355             if (!mSuccess) {
356                 mCallback = null;
357             }
358             mComplete.countDown();
359         }
360 
onSendFinished( PendingIntent pendingIntent, Intent intent, int resultCode, String resultData, Bundle resultExtras)361         private void onSendFinished(
362                 PendingIntent pendingIntent,
363                 Intent intent,
364                 int resultCode,
365                 String resultData,
366                 Bundle resultExtras) {
367             boolean interrupted = false;
368             try {
369                 while (true) {
370                     try {
371                         mComplete.await();
372                         break;
373                     } catch (InterruptedException e) {
374                         interrupted = true;
375                     }
376                 }
377             } finally {
378                 if (interrupted) {
379                     Thread.currentThread().interrupt();
380                 }
381             }
382 
383             if (mCallback != null) {
384                 mCallback.onSendFinished(
385                         pendingIntent,
386                         intent,
387                         resultCode,
388                         resultData,
389                         resultExtras);
390                 mCallback = null;
391             }
392         }
393     }
394 }
395