All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.robolectric.shadows.ShadowPendingIntent Maven / Gradle / Ivy

package org.robolectric.shadows;

import static android.app.PendingIntent.FLAG_CANCEL_CURRENT;
import static android.app.PendingIntent.FLAG_IMMUTABLE;
import static android.app.PendingIntent.FLAG_NO_CREATE;
import static android.app.PendingIntent.FLAG_ONE_SHOT;
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
import static android.os.Build.VERSION_CODES.M;
import static android.os.Build.VERSION_CODES.N;
import static android.os.Build.VERSION_CODES.O;
import static android.os.Build.VERSION_CODES.S;
import static org.robolectric.util.reflector.Reflector.reflector;

import android.annotation.NonNull;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.ActivityThread;
import android.app.PendingIntent;
import android.app.PendingIntent.CanceledException;
import android.app.PendingIntent.OnMarshaledListener;
import android.content.Context;
import android.content.IIntentSender;
import android.content.Intent;
import android.content.IntentSender;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Parcel;
import android.os.Parcelable.Creator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.Resetter;
import org.robolectric.fakes.RoboIntentSender;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.reflector.Accessor;
import org.robolectric.util.reflector.ForType;

@Implements(PendingIntent.class)
@SuppressLint("NewApi")
public class ShadowPendingIntent {

  private enum Type {
    ACTIVITY,
    BROADCAST,
    SERVICE,
    FOREGROUND_SERVICE
  }

  private static final int NULL_PENDING_INTENT_VALUE = -1;

  @GuardedBy("lock")
  private static final List createdIntents = new ArrayList<>();

  private static final Object lock = new Object();

  private static final List parceledPendingIntents = new ArrayList<>();

  @RealObject private PendingIntent realPendingIntent;

  @NonNull private Intent[] savedIntents;
  private Context savedContext;
  private Type type;
  private int requestCode;
  private int flags;
  @Nullable private Bundle options;
  private String creatorPackage;
  private int creatorUid;
  private boolean canceled;
  @Nullable private PendingIntent.OnFinished lastOnFinished;

  @Implementation
  protected static void __staticInitializer__() {
    Shadow.directInitialize(PendingIntent.class);
    ReflectionHelpers.setStaticField(PendingIntent.class, "CREATOR", ShadowPendingIntent.CREATOR);
  }

  @Implementation
  protected static PendingIntent getActivity(
      Context context, int requestCode, @NonNull Intent intent, int flags) {
    return create(context, new Intent[] {intent}, Type.ACTIVITY, requestCode, flags, null);
  }

  @Implementation
  protected static PendingIntent getActivity(
      Context context, int requestCode, @NonNull Intent intent, int flags, Bundle options) {
    return create(context, new Intent[] {intent}, Type.ACTIVITY, requestCode, flags, options);
  }

  @Implementation
  protected static PendingIntent getActivities(
      Context context, int requestCode, @NonNull Intent[] intents, int flags) {
    return create(context, intents, Type.ACTIVITY, requestCode, flags, null);
  }

  @Implementation
  protected static PendingIntent getActivities(
      Context context, int requestCode, @NonNull Intent[] intents, int flags, Bundle options) {
    return create(context, intents, Type.ACTIVITY, requestCode, flags, options);
  }

  @Implementation
  protected static PendingIntent getBroadcast(
      Context context, int requestCode, @NonNull Intent intent, int flags) {
    return create(context, new Intent[] {intent}, Type.BROADCAST, requestCode, flags, null);
  }

  @Implementation
  protected static PendingIntent getService(
      Context context, int requestCode, @NonNull Intent intent, int flags) {
    return create(context, new Intent[] {intent}, Type.SERVICE, requestCode, flags, null);
  }

  @Implementation(minSdk = O)
  protected static PendingIntent getForegroundService(
      Context context, int requestCode, @NonNull Intent intent, int flags) {
    return create(
        context, new Intent[] {intent}, Type.FOREGROUND_SERVICE, requestCode, flags, null);
  }

  @Implementation
  @SuppressWarnings("ReferenceEquality")
  protected void cancel() {
    synchronized (lock) {
      for (Iterator i = createdIntents.iterator(); i.hasNext(); ) {
        PendingIntent pendingIntent = i.next();
        if (pendingIntent == realPendingIntent) {
          canceled = true;
          i.remove();
          break;
        }
      }
    }
  }

  @Implementation
  protected void send() throws CanceledException {
    send(savedContext, 0, null);
  }

  @Implementation
  protected void send(int code) throws CanceledException {
    send(savedContext, code, null);
  }

  @Implementation
  protected void send(int code, PendingIntent.OnFinished onFinished, Handler handler)
      throws CanceledException {
    send(savedContext, code, null, onFinished, handler);
  }

  @Implementation
  protected void send(Context context, int code, Intent intent) throws CanceledException {
    send(context, code, intent, null, null);
  }

  @Implementation
  protected void send(
      Context context,
      int code,
      Intent intent,
      PendingIntent.OnFinished onFinished,
      Handler handler)
      throws CanceledException {
    send(context, code, intent, onFinished, handler, null);
  }

  @Implementation
  protected void send(
      Context context,
      int code,
      Intent intent,
      PendingIntent.OnFinished onFinished,
      Handler handler,
      String requiredPermission)
      throws CanceledException {
    // Manually propagating to keep only one implementation regardless of SDK
    send(context, code, intent, onFinished, handler, requiredPermission, null);
  }

  @Implementation(minSdk = M)
  protected void send(
      Context context,
      int code,
      Intent intent,
      PendingIntent.OnFinished onFinished,
      Handler handler,
      String requiredPermission,
      Bundle options)
      throws CanceledException {
    send(context, code, intent, onFinished, handler, requiredPermission, options, 0);
  }

  void send(
      Context context,
      int code,
      Intent intent,
      PendingIntent.OnFinished onFinished,
      Handler handler,
      String requiredPermission,
      Bundle options,
      int requestCode)
      throws CanceledException {
    this.lastOnFinished =
        handler == null
            ? onFinished
            : (pendingIntent, intent1, resultCode, resultData, resultExtras) ->
                handler.post(
                    () ->
                        onFinished.onSendFinished(
                            pendingIntent, intent1, resultCode, resultData, resultExtras));

    if (canceled) {
      throw new CanceledException();
    }

    // Fill in the last Intent, if it is mutable, with information now available at send-time.
    Intent[] intentsToSend;
    if (intent != null && isMutable(flags)) {
      // Copy the last intent before filling it in to avoid modifying this PendingIntent.
      intentsToSend = Arrays.copyOf(savedIntents, savedIntents.length);
      Intent lastIntentCopy = new Intent(intentsToSend[intentsToSend.length - 1]);
      lastIntentCopy.fillIn(intent, 0);
      intentsToSend[intentsToSend.length - 1] = lastIntentCopy;
    } else {
      intentsToSend = savedIntents;
    }

    ActivityThread activityThread = (ActivityThread) RuntimeEnvironment.getActivityThread();
    ShadowInstrumentation shadowInstrumentation =
        Shadow.extract(activityThread.getInstrumentation());
    if (isActivity()) {
      for (Intent intentToSend : intentsToSend) {
        shadowInstrumentation.execStartActivity(
            context,
            (IBinder) null,
            (IBinder) null,
            (Activity) null,
            intentToSend,
            requestCode,
            (Bundle) null);
      }
    } else if (isBroadcast()) {
      for (Intent intentToSend : intentsToSend) {
        shadowInstrumentation.sendBroadcastWithPermission(
            intentToSend, requiredPermission, context, options, code);
      }
    } else if (isService()) {
      for (Intent intentToSend : intentsToSend) {
        context.startService(intentToSend);
      }
    } else if (isForegroundService()) {
      for (Intent intentToSend : intentsToSend) {
        context.startForegroundService(intentToSend);
      }
    }

    if (isOneShot(flags)) {
      cancel();
    }
  }

  @Implementation
  protected IntentSender getIntentSender() {
    return new RoboIntentSender(realPendingIntent);
  }

  /**
   * Returns {@code true} if this {@code PendingIntent} was created with {@link #getActivity} or
   * {@link #getActivities}.
   *
   * 

This method is intentionally left {@code public} rather than {@code protected} because it * serves a secondary purpose as a utility shadow method for API levels < 31. */ @Implementation(minSdk = S) public boolean isActivity() { return type == Type.ACTIVITY; } /** * Returns {@code true} if this {@code PendingIntent} was created with {@link #getBroadcast}. * *

This method is intentionally left {@code public} rather than {@code protected} because it * serves a secondary purpose as a utility shadow method for API levels < 31. */ @Implementation(minSdk = S) public boolean isBroadcast() { return type == Type.BROADCAST; } /** * Returns {@code true} if this {@code PendingIntent} was created with {@link * #getForegroundService}. * *

This method is intentionally left {@code public} rather than {@code protected} because it * serves a secondary purpose as a utility shadow method for API levels < 31. */ @Implementation(minSdk = S) public boolean isForegroundService() { return type == Type.FOREGROUND_SERVICE; } /** * Returns {@code true} if this {@code PendingIntent} was created with {@link #getService}. * *

This method is intentionally left {@code public} rather than {@code protected} because it * serves a secondary purpose as a utility shadow method for API levels < 31. */ @Implementation(minSdk = S) public boolean isService() { return type == Type.SERVICE; } /** * Returns {@code true} if this {@code PendingIntent} is marked with {@link * PendingIntent#FLAG_IMMUTABLE}. * *

This method is intentionally left {@code public} rather than {@code protected} because it * serves a secondary purpose as a utility shadow method for API levels < 31. */ @Implementation(minSdk = S) public boolean isImmutable() { return (flags & FLAG_IMMUTABLE) > 0; } /** * @return {@code true} iff sending this PendingIntent will start an activity * @deprecated prefer {@link #isActivity} which was added to {@link PendingIntent} in API 31 * (Android S). */ @Deprecated public boolean isActivityIntent() { return type == Type.ACTIVITY; } /** * @return {@code true} iff sending this PendingIntent will broadcast an Intent * @deprecated prefer {@link #isBroadcast} which was added to {@link PendingIntent} in API 31 * (Android S). */ @Deprecated public boolean isBroadcastIntent() { return type == Type.BROADCAST; } /** * @return {@code true} iff sending this PendingIntent will start a service * @deprecated prefer {@link #isService} which was added to {@link PendingIntent} in API 31 * (Android S). */ @Deprecated public boolean isServiceIntent() { return type == Type.SERVICE; } /** * @return {@code true} iff sending this PendingIntent will start a foreground service * @deprecated prefer {@link #isForegroundService} which was added to {@link PendingIntent} in API * 31 (Android S). */ @Deprecated public boolean isForegroundServiceIntent() { return type == Type.FOREGROUND_SERVICE; } /** * @return the context in which this PendingIntent was created */ public Context getSavedContext() { return savedContext; } /** * This returns the last Intent in the Intent[] to be delivered when the PendingIntent is sent. * This method is particularly useful for PendingIntents created with a single Intent: * *

    *
  • {@link #getActivity(Context, int, Intent, int)} *
  • {@link #getActivity(Context, int, Intent, int, Bundle)} *
  • {@link #getBroadcast(Context, int, Intent, int)} *
  • {@link #getService(Context, int, Intent, int)} *
* * @return the final Intent to be delivered when the PendingIntent is sent */ public Intent getSavedIntent() { return savedIntents[savedIntents.length - 1]; } /** * This method is particularly useful for PendingIntents created with multiple Intents: * *
    *
  • {@link #getActivities(Context, int, Intent[], int)} *
  • {@link #getActivities(Context, int, Intent[], int, Bundle)} *
* * @return all Intents to be delivered when the PendingIntent is sent */ public Intent[] getSavedIntents() { return savedIntents; } /** * @return {@true} iff this PendingIntent has been canceled */ public boolean isCanceled() { return canceled; } /** * @return the request code with which this PendingIntent was created */ public int getRequestCode() { return requestCode; } /** * @return the flags with which this PendingIntent was created */ public int getFlags() { return flags; } /** * @return the flags with which this PendingIntent was created */ public @Nullable Bundle getOptions() { return options; } /** * Calls {@link PendingIntent.OnFinished#onSendFinished} on the last {@link * PendingIntent.OnFinished} passed with {@link #send()}. * *

{@link PendingIntent.OnFinished#onSendFinished} is called on the {@link Handler} passed with * {@link #send()} (if any). If no {@link Handler} was provided it's invoked on the calling * thread. * * @return false if no {@link PendingIntent.OnFinished} callback was passed with the last {@link * #send()} call, true otherwise. */ public boolean callLastOnFinished( Intent intent, int resultCode, String resultData, Bundle resultExtras) { if (lastOnFinished == null) { return false; } lastOnFinished.onSendFinished(realPendingIntent, intent, resultCode, resultData, resultExtras); return true; } @Implementation protected String getTargetPackage() { return getCreatorPackage(); } @Implementation(minSdk = JELLY_BEAN_MR1) protected String getCreatorPackage() { return (creatorPackage == null) ? RuntimeEnvironment.getApplication().getPackageName() : creatorPackage; } public void setCreatorPackage(String creatorPackage) { this.creatorPackage = creatorPackage; } @Implementation(minSdk = JELLY_BEAN_MR1) protected int getCreatorUid() { return creatorUid; } public void setCreatorUid(int uid) { this.creatorUid = uid; } @Override @Implementation public boolean equals(Object o) { if (this == o) return true; if (o == null || realPendingIntent.getClass() != o.getClass()) return false; ShadowPendingIntent that = Shadow.extract((PendingIntent) o); String packageName = savedContext == null ? null : savedContext.getPackageName(); String thatPackageName = that.savedContext == null ? null : that.savedContext.getPackageName(); if (!Objects.equals(packageName, thatPackageName)) { return false; } if (this.savedIntents.length != that.savedIntents.length) { return false; } for (int i = 0; i < this.savedIntents.length; i++) { if (!this.savedIntents[i].filterEquals(that.savedIntents[i])) { return false; } } if (this.requestCode != that.requestCode) { return false; } return true; } @Override @Implementation public int hashCode() { int result = savedIntents != null ? Arrays.hashCode(savedIntents) : 0; if (savedContext != null) { String packageName = savedContext.getPackageName(); result = 31 * result + (packageName != null ? packageName.hashCode() : 0); } result = 31 * result + requestCode; return result; } @Implementation @Nullable public static PendingIntent readPendingIntentOrNullFromParcel(@NonNull Parcel in) { int intentIndex = in.readInt(); if (intentIndex == NULL_PENDING_INTENT_VALUE) { return null; } return parceledPendingIntents.get(intentIndex); } @Implementation public static void writePendingIntentOrNullToParcel( @Nullable PendingIntent sender, @NonNull Parcel out) { if (sender == null) { out.writeInt(NULL_PENDING_INTENT_VALUE); return; } int index = parceledPendingIntents.size(); parceledPendingIntents.add(sender); out.writeInt(index); if (RuntimeEnvironment.getApiLevel() >= N) { ThreadLocal sOnMarshaledListener = ReflectionHelpers.getStaticField(PendingIntent.class, "sOnMarshaledListener"); OnMarshaledListener listener = sOnMarshaledListener.get(); if (listener != null) { listener.onMarshaled(sender, out, 0); } } } static final Creator CREATOR = new Creator() { @Override public PendingIntent createFromParcel(Parcel in) { return readPendingIntentOrNullFromParcel(in); } @Override public PendingIntent[] newArray(int size) { return new PendingIntent[size]; } }; @Implementation protected void writeToParcel(Parcel out, int flags) { writePendingIntentOrNullToParcel(realPendingIntent, out); } private static PendingIntent create( Context context, Intent[] intents, Type type, int requestCode, int flags, @Nullable Bundle options) { synchronized (lock) { Objects.requireNonNull(intents, "intents may not be null"); // Search for a matching PendingIntent. PendingIntent pendingIntent = getCreatedIntentFor(type, intents, requestCode, flags); if ((flags & FLAG_NO_CREATE) != 0) { return pendingIntent; } // If requested, update the existing PendingIntent if one exists. if (pendingIntent != null && (flags & FLAG_UPDATE_CURRENT) != 0) { ShadowPendingIntent shadowPendingIntent = Shadow.extract(pendingIntent); Intent intent = shadowPendingIntent.getSavedIntent(); Bundle extras = intent.getExtras(); if (extras != null) { extras.clear(); } intent.putExtras(intents[intents.length - 1]); return pendingIntent; } // If requested, cancel the existing PendingIntent if one exists. if (pendingIntent != null && (flags & FLAG_CANCEL_CURRENT) != 0) { ShadowPendingIntent shadowPendingIntent = Shadow.extract(pendingIntent); shadowPendingIntent.cancel(); pendingIntent = null; } // Build the PendingIntent if it does not exist. if (pendingIntent == null) { pendingIntent = ReflectionHelpers.callConstructor(PendingIntent.class); // Some methods (e.g. toString) may NPE if 'mTarget' is null. reflector(PendingIntentReflector.class, pendingIntent) .setTarget(ReflectionHelpers.createNullProxy(IIntentSender.class)); ShadowPendingIntent shadowPendingIntent = Shadow.extract(pendingIntent); shadowPendingIntent.savedIntents = intents; shadowPendingIntent.type = type; shadowPendingIntent.savedContext = context; shadowPendingIntent.requestCode = requestCode; shadowPendingIntent.flags = flags; shadowPendingIntent.options = options; createdIntents.add(pendingIntent); } return pendingIntent; } } private static PendingIntent getCreatedIntentFor( Type type, Intent[] intents, int requestCode, int flags) { synchronized (lock) { for (PendingIntent createdIntent : createdIntents) { ShadowPendingIntent shadowPendingIntent = Shadow.extract(createdIntent); if (isOneShot(shadowPendingIntent.flags) != isOneShot(flags)) { continue; } if (isMutable(shadowPendingIntent.flags) != isMutable(flags)) { continue; } if (shadowPendingIntent.type != type) { continue; } if (shadowPendingIntent.requestCode != requestCode) { continue; } // The last Intent in the array acts as the "significant element" for matching as per // {@link #getActivities(Context, int, Intent[], int)}. Intent savedIntent = shadowPendingIntent.getSavedIntent(); Intent targetIntent = intents[intents.length - 1]; if (savedIntent == null ? targetIntent == null : savedIntent.filterEquals(targetIntent)) { return createdIntent; } } return null; } } private static boolean isOneShot(int flags) { return (flags & FLAG_ONE_SHOT) != 0; } private static boolean isMutable(int flags) { return (flags & FLAG_IMMUTABLE) == 0; } @Resetter public static void reset() { synchronized (lock) { createdIntents.clear(); } } @ForType(PendingIntent.class) interface PendingIntentReflector { @Accessor("mTarget") void setTarget(IIntentSender target); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy