1 package org.robolectric.shadows;
2 
3 import static android.app.PendingIntent.FLAG_CANCEL_CURRENT;
4 import static android.app.PendingIntent.FLAG_IMMUTABLE;
5 import static android.app.PendingIntent.FLAG_NO_CREATE;
6 import static android.app.PendingIntent.FLAG_ONE_SHOT;
7 import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
8 import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
9 import static android.os.Build.VERSION_CODES.M;
10 import static android.os.Build.VERSION_CODES.O;
11 
12 import android.annotation.NonNull;
13 import android.annotation.SuppressLint;
14 import android.app.Activity;
15 import android.app.ActivityThread;
16 import android.app.PendingIntent;
17 import android.app.PendingIntent.CanceledException;
18 import android.content.Context;
19 import android.content.Intent;
20 import android.content.IntentSender;
21 import android.os.Bundle;
22 import android.os.Handler;
23 import android.os.IBinder;
24 import java.util.ArrayList;
25 import java.util.Arrays;
26 import java.util.Iterator;
27 import java.util.List;
28 import java.util.Objects;
29 import org.robolectric.RuntimeEnvironment;
30 import org.robolectric.annotation.Implementation;
31 import org.robolectric.annotation.Implements;
32 import org.robolectric.annotation.RealObject;
33 import org.robolectric.annotation.Resetter;
34 import org.robolectric.fakes.RoboIntentSender;
35 import org.robolectric.shadow.api.Shadow;
36 import org.robolectric.util.ReflectionHelpers;
37 
38 @Implements(PendingIntent.class)
39 @SuppressLint("NewApi")
40 public class ShadowPendingIntent {
41 
42   private enum Type {
43     ACTIVITY,
44     BROADCAST,
45     SERVICE,
46     FOREGROUND_SERVICE
47   }
48 
49   private static final List<PendingIntent> createdIntents = new ArrayList<>();
50 
51   @RealObject
52   private PendingIntent realPendingIntent;
53 
54   @NonNull private Intent[] savedIntents;
55   private Context savedContext;
56   private Type type;
57   private int requestCode;
58   private int flags;
59   private String creatorPackage;
60   private boolean canceled;
61 
62   @Implementation
getActivity( Context context, int requestCode, @NonNull Intent intent, int flags)63   protected static PendingIntent getActivity(
64       Context context, int requestCode, @NonNull Intent intent, int flags) {
65     return create(context, new Intent[] {intent}, Type.ACTIVITY, requestCode, flags);
66   }
67 
68   @Implementation
getActivity( Context context, int requestCode, @NonNull Intent intent, int flags, Bundle options)69   protected static PendingIntent getActivity(
70       Context context, int requestCode, @NonNull Intent intent, int flags, Bundle options) {
71     return create(context, new Intent[] {intent}, Type.ACTIVITY, requestCode, flags);
72   }
73 
74   @Implementation
getActivities( Context context, int requestCode, @NonNull Intent[] intents, int flags)75   protected static PendingIntent getActivities(
76       Context context, int requestCode, @NonNull Intent[] intents, int flags) {
77     return create(context, intents, Type.ACTIVITY, requestCode, flags);
78   }
79 
80   @Implementation
getActivities( Context context, int requestCode, @NonNull Intent[] intents, int flags, Bundle options)81   protected static PendingIntent getActivities(
82       Context context, int requestCode, @NonNull Intent[] intents, int flags, Bundle options) {
83     return create(context, intents, Type.ACTIVITY, requestCode, flags);
84   }
85 
86   @Implementation
getBroadcast( Context context, int requestCode, @NonNull Intent intent, int flags)87   protected static PendingIntent getBroadcast(
88       Context context, int requestCode, @NonNull Intent intent, int flags) {
89     return create(context, new Intent[] {intent}, Type.BROADCAST, requestCode, flags);
90   }
91 
92   @Implementation
getService( Context context, int requestCode, @NonNull Intent intent, int flags)93   protected static PendingIntent getService(
94       Context context, int requestCode, @NonNull Intent intent, int flags) {
95     return create(context, new Intent[] {intent}, Type.SERVICE, requestCode, flags);
96   }
97 
98   @Implementation(minSdk = O)
getForegroundService( Context context, int requestCode, @NonNull Intent intent, int flags)99   protected static PendingIntent getForegroundService(
100       Context context, int requestCode, @NonNull Intent intent, int flags) {
101     return create(context, new Intent[] {intent}, Type.FOREGROUND_SERVICE, requestCode, flags);
102   }
103 
104   @Implementation
105   @SuppressWarnings("ReferenceEquality")
cancel()106   protected void cancel() {
107     for (Iterator<PendingIntent> i = createdIntents.iterator(); i.hasNext(); ) {
108       PendingIntent pendingIntent = i.next();
109       if (pendingIntent == realPendingIntent) {
110         canceled = true;
111         i.remove();
112         break;
113       }
114     }
115   }
116 
117   @Implementation
send()118   protected void send() throws CanceledException {
119     send(savedContext, 0, null);
120   }
121 
122   @Implementation
send(int code, PendingIntent.OnFinished onFinished, Handler handler)123   protected void send(int code, PendingIntent.OnFinished onFinished, Handler handler)
124       throws CanceledException {
125     send(savedContext, code, null, onFinished, handler);
126   }
127 
128   @Implementation
send(Context context, int code, Intent intent)129   protected void send(Context context, int code, Intent intent) throws CanceledException {
130     send(context, code, intent, null, null);
131   }
132 
133   @Implementation
send(Context context, int code, Intent intent, PendingIntent.OnFinished onFinished, Handler handler)134   protected void send(Context context, int code, Intent intent, PendingIntent.OnFinished onFinished,
135       Handler handler) throws CanceledException {
136     send(context, code, intent, onFinished, handler, null);
137   }
138 
139   @Implementation
send(Context context, int code, Intent intent, PendingIntent.OnFinished onFinished, Handler handler, String requiredPermission)140   protected void send(Context context, int code, Intent intent, PendingIntent.OnFinished onFinished,
141       Handler handler, String requiredPermission) throws CanceledException {
142     // Manually propagating to keep only one implementation regardless of SDK
143     send(context, code, intent, onFinished, handler, requiredPermission, null);
144   }
145 
146   @Implementation(minSdk = M)
send(Context context, int code, Intent intent, PendingIntent.OnFinished onFinished, Handler handler, String requiredPermission, Bundle options)147   protected void send(Context context, int code, Intent intent, PendingIntent.OnFinished onFinished,
148       Handler handler, String requiredPermission, Bundle options) throws CanceledException {
149     if (canceled) {
150       throw new CanceledException();
151     }
152 
153     // Fill in the last Intent, if it is mutable, with information now available at send-time.
154     Intent[] intentsToSend;
155     if (intent != null && isMutable(flags)) {
156       // Copy the last intent before filling it in to avoid modifying this PendingIntent.
157       intentsToSend = Arrays.copyOf(savedIntents, savedIntents.length);
158       Intent lastIntentCopy = new Intent(intentsToSend[intentsToSend.length - 1]);
159       lastIntentCopy.fillIn(intent, 0);
160       intentsToSend[intentsToSend.length - 1] = lastIntentCopy;
161     } else {
162       intentsToSend = savedIntents;
163     }
164 
165     ActivityThread activityThread = (ActivityThread) RuntimeEnvironment.getActivityThread();
166     ShadowInstrumentation shadowInstrumentation = Shadow.extract(activityThread.getInstrumentation());
167     if (isActivityIntent()) {
168       for (Intent intentToSend : intentsToSend) {
169         shadowInstrumentation.execStartActivity(
170             context,
171             (IBinder) null,
172             (IBinder) null,
173             (Activity) null,
174             intentToSend,
175             0,
176             (Bundle) null);
177       }
178     } else if (isBroadcastIntent()) {
179       for (Intent intentToSend : intentsToSend) {
180         context.sendBroadcast(intentToSend);
181       }
182     } else if (isServiceIntent()) {
183       for (Intent intentToSend : intentsToSend) {
184         context.startService(intentToSend);
185       }
186     } else if (isForegroundServiceIntent()) {
187       for (Intent intentToSend : intentsToSend) {
188         context.startForegroundService(intentToSend);
189       }
190     }
191 
192     if (isOneShot(flags)) {
193       cancel();
194     }
195   }
196 
197   @Implementation
getIntentSender()198   protected IntentSender getIntentSender() {
199     return new RoboIntentSender(realPendingIntent);
200   }
201 
202   /**
203    * @return {@code true} iff sending this PendingIntent will start an activity
204    */
isActivityIntent()205   public boolean isActivityIntent() {
206     return type == Type.ACTIVITY;
207   }
208 
209   /**
210    * @return {@code true} iff sending this PendingIntent will broadcast an Intent
211    */
isBroadcastIntent()212   public boolean isBroadcastIntent() {
213     return type == Type.BROADCAST;
214   }
215 
216   /**
217    * @return {@code true} iff sending this PendingIntent will start a service
218    */
isServiceIntent()219   public boolean isServiceIntent() {
220     return type == Type.SERVICE;
221   }
222 
223   /** @return {@code true} iff sending this PendingIntent will start a foreground service */
isForegroundServiceIntent()224   public boolean isForegroundServiceIntent() {
225     return type == Type.FOREGROUND_SERVICE;
226   }
227 
228   /**
229    * @return the context in which this PendingIntent was created
230    */
getSavedContext()231   public Context getSavedContext() {
232     return savedContext;
233   }
234 
235   /**
236    * This returns the last Intent in the Intent[] to be delivered when the PendingIntent is sent.
237    * This method is particularly useful for PendingIntents created with a single Intent:
238    * <ul>
239    *   <li>{@link #getActivity(Context, int, Intent, int)}</li>
240    *   <li>{@link #getActivity(Context, int, Intent, int, Bundle)}</li>
241    *   <li>{@link #getBroadcast(Context, int, Intent, int)}</li>
242    *   <li>{@link #getService(Context, int, Intent, int)}</li>
243    * </ul>
244    *
245    * @return the final Intent to be delivered when the PendingIntent is sent
246    */
getSavedIntent()247   public Intent getSavedIntent() {
248     return savedIntents[savedIntents.length - 1];
249   }
250 
251   /**
252    * This method is particularly useful for PendingIntents created with multiple Intents:
253    * <ul>
254    *   <li>{@link #getActivities(Context, int, Intent[], int)}</li>
255    *   <li>{@link #getActivities(Context, int, Intent[], int, Bundle)}</li>
256    * </ul>
257    *
258    * @return all Intents to be delivered when the PendingIntent is sent
259    */
getSavedIntents()260   public Intent[] getSavedIntents() {
261     return savedIntents;
262   }
263 
264   /**
265    * @return {@true} iff this PendingIntent has been canceled
266    */
isCanceled()267   public boolean isCanceled() {
268     return canceled;
269   }
270 
271   /**
272    * @return the request code with which this PendingIntent was created
273    */
getRequestCode()274   public int getRequestCode() {
275     return requestCode;
276   }
277 
278   /**
279    * @return the flags with which this PendingIntent was created
280    */
getFlags()281   public int getFlags() {
282     return flags;
283   }
284 
285   @Implementation
getTargetPackage()286   protected String getTargetPackage() {
287     return getCreatorPackage();
288   }
289 
290   @Implementation(minSdk = JELLY_BEAN_MR1)
getCreatorPackage()291   protected String getCreatorPackage() {
292     return (creatorPackage == null)
293         ? RuntimeEnvironment.application.getPackageName()
294         : creatorPackage;
295   }
296 
setCreatorPackage(String creatorPackage)297   public void setCreatorPackage(String creatorPackage) {
298     this.creatorPackage = creatorPackage;
299   }
300 
301   @Override
302   @Implementation
equals(Object o)303   public boolean equals(Object o) {
304     if (this == o) return true;
305     if (o == null || realPendingIntent.getClass() != o.getClass()) return false;
306     ShadowPendingIntent that = Shadow.extract((PendingIntent) o);
307 
308     String packageName = savedContext == null ? null : savedContext.getPackageName();
309     String thatPackageName = that.savedContext == null ? null : that.savedContext.getPackageName();
310     if (!Objects.equals(packageName, thatPackageName)) {
311       return false;
312     }
313 
314     if (this.savedIntents.length != that.savedIntents.length) {
315       return false;
316     }
317 
318     for (int i = 0; i < this.savedIntents.length; i++) {
319       if (!this.savedIntents[i].filterEquals(that.savedIntents[i])) {
320         return false;
321       }
322     }
323 
324     if (this.requestCode != that.requestCode) {
325       return false;
326     }
327     return true;
328   }
329 
330   @Override
331   @Implementation
hashCode()332   public int hashCode() {
333     int result = savedIntents != null ? Arrays.hashCode(savedIntents) : 0;
334     if (savedContext != null) {
335       String packageName = savedContext.getPackageName();
336       result = 31 * result + (packageName != null ? packageName.hashCode() : 0);
337     }
338     result = 31 * result + requestCode;
339     return result;
340   }
341 
create(Context context, Intent[] intents, Type type, int requestCode, int flags)342   private static PendingIntent create(Context context, Intent[] intents, Type type, int requestCode,
343       int flags) {
344     Objects.requireNonNull(intents, "intents may not be null");
345 
346     // Search for a matching PendingIntent.
347     PendingIntent pendingIntent = getCreatedIntentFor(type, intents, requestCode, flags);
348     if ((flags & FLAG_NO_CREATE) != 0) {
349       return pendingIntent;
350     }
351 
352     // If requested, update the existing PendingIntent if one exists.
353     if (pendingIntent != null && (flags & FLAG_UPDATE_CURRENT) != 0) {
354       ShadowPendingIntent shadowPendingIntent = Shadow.extract(pendingIntent);
355       Intent intent = shadowPendingIntent.getSavedIntent();
356       Bundle extras = intent.getExtras();
357       if (extras != null) {
358         extras.clear();
359       }
360       intent.putExtras(intents[intents.length - 1]);
361       return pendingIntent;
362     }
363 
364     // If requested, cancel the existing PendingIntent if one exists.
365     if (pendingIntent != null && (flags & FLAG_CANCEL_CURRENT) != 0) {
366       ShadowPendingIntent shadowPendingIntent = Shadow.extract(pendingIntent);
367       shadowPendingIntent.cancel();
368       pendingIntent = null;
369     }
370 
371     // Build the PendingIntent if it does not exist.
372     if (pendingIntent == null) {
373       pendingIntent = ReflectionHelpers.callConstructor(PendingIntent.class);
374       ShadowPendingIntent shadowPendingIntent = Shadow.extract(pendingIntent);
375       shadowPendingIntent.savedIntents = intents;
376       shadowPendingIntent.type = type;
377       shadowPendingIntent.savedContext = context;
378       shadowPendingIntent.requestCode = requestCode;
379       shadowPendingIntent.flags = flags;
380 
381       createdIntents.add(pendingIntent);
382     }
383 
384     return pendingIntent;
385   }
386 
getCreatedIntentFor(Type type, Intent[] intents, int requestCode, int flags)387   private static PendingIntent getCreatedIntentFor(Type type, Intent[] intents, int requestCode,
388       int flags) {
389     for (PendingIntent createdIntent : createdIntents) {
390       ShadowPendingIntent shadowPendingIntent = Shadow.extract(createdIntent);
391 
392       if (isOneShot(shadowPendingIntent.flags) != isOneShot(flags)) {
393         continue;
394       }
395 
396       if (isMutable(shadowPendingIntent.flags) != isMutable(flags)) {
397         continue;
398       }
399 
400       if (shadowPendingIntent.type != type) {
401         continue;
402       }
403 
404       if (shadowPendingIntent.requestCode != requestCode) {
405         continue;
406       }
407 
408       // The last Intent in the array acts as the "significant element" for matching as per
409       // {@link #getActivities(Context, int, Intent[], int)}.
410       Intent savedIntent = shadowPendingIntent.getSavedIntent();
411       Intent targetIntent = intents[intents.length - 1];
412 
413       if (savedIntent == null ? targetIntent == null : savedIntent.filterEquals(targetIntent)) {
414         return createdIntent;
415       }
416     }
417     return null;
418   }
419 
isOneShot(int flags)420   private static boolean isOneShot(int flags) {
421     return (flags & FLAG_ONE_SHOT) != 0;
422   }
423 
isMutable(int flags)424   private static boolean isMutable(int flags) {
425     return (flags & FLAG_IMMUTABLE) == 0;
426   }
427 
428   @Resetter
reset()429   public static void reset() {
430     createdIntents.clear();
431   }
432 }
433