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

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

package org.robolectric.shadows;

import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
import static android.os.Build.VERSION_CODES.KITKAT;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
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.Q;
import static org.robolectric.shadow.api.Shadow.directlyOn;
import static org.robolectric.util.reflector.Reflector.reflector;

import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.app.ActivityThread;
import android.app.LoadedApk;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.IContentProvider;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.IntentSender;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.Environment;
import android.os.FileUtils;
import android.os.Handler;
import android.os.IBinder;
import android.os.UserHandle;
import com.google.common.base.Strings;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import java.io.File;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executor;
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.shadow.api.Shadow;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.reflector.Accessor;
import org.robolectric.util.reflector.Direct;
import org.robolectric.util.reflector.ForType;
import org.robolectric.util.reflector.Static;

@Implements(className = ShadowContextImpl.CLASS_NAME)
@SuppressWarnings("NewApi")
public class ShadowContextImpl {

  public static final String CLASS_NAME = "android.app.ContextImpl";

  @RealObject private Context realContextImpl;

  private Map systemServices = new HashMap();
  private final Set removedSystemServices = new HashSet<>();
  private final Object contentResolverLock = new Object();

  @GuardedBy("contentResolverLock")
  private ContentResolver contentResolver;

  private Integer userId;

  /**
   * Returns the handle to a system-level service by name. If the service is not available in
   * Roboletric, or it is set to unavailable in {@link ShadowServiceManager#setServiceAvailability},
   * {@code null} will be returned.
   */
  @Implementation
  @Nullable
  protected Object getSystemService(String name) {
    if (removedSystemServices.contains(name)) {
      return null;
    }
    if (!systemServices.containsKey(name)) {
      return reflector(_ContextImpl_.class, realContextImpl).getSystemService(name);
    }
    return systemServices.get(name);
  }

  public void setSystemService(String key, Object service) {
    systemServices.put(key, service);
  }

  /**
   * Makes {@link #getSystemService(String)} return {@code null} for the given system service name,
   * mimicking a device that doesn't have that system service.
   */
  public void removeSystemService(String name) {
    removedSystemServices.add(name);
  }

  @Implementation
  protected void startIntentSender(
      IntentSender intent,
      Intent fillInIntent,
      int flagsMask,
      int flagsValues,
      int extraFlags,
      Bundle options)
      throws IntentSender.SendIntentException {
    intent.sendIntent(realContextImpl, 0, fillInIntent, null, null, null);
  }

  @Implementation
  protected ClassLoader getClassLoader() {
    return this.getClass().getClassLoader();
  }

  @Implementation
  protected int checkCallingPermission(String permission) {
    return checkPermission(permission, android.os.Process.myPid(), android.os.Process.myUid());
  }

  @Implementation
  protected int checkCallingOrSelfPermission(String permission) {
    return checkCallingPermission(permission);
  }

  @Implementation
  protected ContentResolver getContentResolver() {
    synchronized (contentResolverLock) {
      if (contentResolver == null) {
        contentResolver =
            new ContentResolver(realContextImpl) {
              @Override
              protected IContentProvider acquireProvider(Context c, String name) {
                return null;
              }

              @Override
              public boolean releaseProvider(IContentProvider icp) {
                return false;
              }

              @Override
              protected IContentProvider acquireUnstableProvider(Context c, String name) {
                return null;
              }

              @Override
              public boolean releaseUnstableProvider(IContentProvider icp) {
                return false;
              }

              @Override
              public void unstableProviderDied(IContentProvider icp) {}
            };
      }
      return contentResolver;
    }
  }

  @Implementation
  protected void sendBroadcast(Intent intent) {
    getShadowInstrumentation()
        .sendBroadcastWithPermission(
            intent, /*userHandle=*/ null, /*receiverPermission=*/ null, realContextImpl);
  }

  @Implementation
  protected void sendBroadcast(Intent intent, String receiverPermission) {
    getShadowInstrumentation()
        .sendBroadcastWithPermission(
            intent, /*userHandle=*/ null, receiverPermission, realContextImpl);
  }

  @Implementation(minSdk = JELLY_BEAN_MR1)
  @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS)
  protected void sendBroadcastAsUser(@RequiresPermission Intent intent, UserHandle user) {
    getShadowInstrumentation()
        .sendBroadcastWithPermission(intent, user, /*receiverPermission=*/ null, realContextImpl);
  }

  @Implementation(minSdk = JELLY_BEAN_MR1)
  @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS)
  protected void sendBroadcastAsUser(
      @RequiresPermission Intent intent, UserHandle user, @Nullable String receiverPermission) {
    getShadowInstrumentation()
        .sendBroadcastWithPermission(intent, user, receiverPermission, realContextImpl);
  }

  @Implementation
  protected void sendOrderedBroadcast(Intent intent, String receiverPermission) {
    getShadowInstrumentation()
        .sendOrderedBroadcastWithPermission(intent, receiverPermission, realContextImpl);
  }

  @Implementation
  protected void sendOrderedBroadcast(
      Intent intent,
      String receiverPermission,
      BroadcastReceiver resultReceiver,
      Handler scheduler,
      int initialCode,
      String initialData,
      Bundle initialExtras) {
    getShadowInstrumentation()
        .sendOrderedBroadcastAsUser(
            intent,
            /*userHandle=*/ null,
            receiverPermission,
            resultReceiver,
            scheduler,
            initialCode,
            initialData,
            initialExtras,
            realContextImpl);
  }

  /**
   * Allows the test to query for the broadcasts for specific users, for everything else behaves as
   * {@link #sendOrderedBroadcastAsUser}.
   */
  @Implementation(minSdk = JELLY_BEAN_MR1)
  protected void sendOrderedBroadcastAsUser(
      Intent intent,
      UserHandle userHandle,
      String receiverPermission,
      BroadcastReceiver resultReceiver,
      Handler scheduler,
      int initialCode,
      String initialData,
      Bundle initialExtras) {
    getShadowInstrumentation()
        .sendOrderedBroadcastAsUser(
            intent,
            userHandle,
            receiverPermission,
            resultReceiver,
            scheduler,
            initialCode,
            initialData,
            initialExtras,
            realContextImpl);
  }

  /** Behaves as {@link #sendOrderedBroadcastAsUser}. Currently ignores appOp and options. */
  @Implementation(minSdk = M)
  protected void sendOrderedBroadcastAsUser(
      Intent intent,
      UserHandle userHandle,
      String receiverPermission,
      int appOp,
      Bundle options,
      BroadcastReceiver resultReceiver,
      Handler scheduler,
      int initialCode,
      String initialData,
      Bundle initialExtras) {
    sendOrderedBroadcastAsUser(
        intent,
        userHandle,
        receiverPermission,
        resultReceiver,
        scheduler,
        initialCode,
        initialData,
        initialExtras);
  }

  @Implementation
  protected void sendStickyBroadcast(Intent intent) {
    getShadowInstrumentation().sendStickyBroadcast(intent, realContextImpl);
  }

  @Implementation
  protected int checkPermission(String permission, int pid, int uid) {
    return getShadowInstrumentation().checkPermission(permission, pid, uid);
  }

  @Implementation
  protected Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
    return getShadowInstrumentation().registerReceiver(receiver, filter, 0, realContextImpl);
  }

  @Implementation(minSdk = O)
  protected Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter, int flags) {
    return getShadowInstrumentation().registerReceiver(receiver, filter, flags, realContextImpl);
  }

  @Implementation
  protected Intent registerReceiver(
      BroadcastReceiver receiver,
      IntentFilter filter,
      String broadcastPermission,
      Handler scheduler) {
    return getShadowInstrumentation()
        .registerReceiver(receiver, filter, broadcastPermission, scheduler, 0, realContextImpl);
  }

  @Implementation(minSdk = O)
  protected Intent registerReceiver(
      BroadcastReceiver receiver,
      IntentFilter filter,
      String broadcastPermission,
      Handler scheduler,
      int flags) {
    return getShadowInstrumentation()
        .registerReceiver(receiver, filter, broadcastPermission, scheduler, flags, realContextImpl);
  }

  @Implementation(minSdk = JELLY_BEAN_MR1)
  protected Intent registerReceiverAsUser(
      BroadcastReceiver receiver,
      UserHandle user,
      IntentFilter filter,
      String broadcastPermission,
      Handler scheduler) {
    return getShadowInstrumentation()
        .registerReceiverWithContext(
            receiver, filter, broadcastPermission, scheduler, 0, realContextImpl);
  }

  @Implementation
  protected void unregisterReceiver(BroadcastReceiver broadcastReceiver) {
    getShadowInstrumentation().unregisterReceiver(broadcastReceiver);
  }

  @Implementation
  protected ComponentName startService(Intent service) {
    validateServiceIntent(service);
    return getShadowInstrumentation().startService(service);
  }

  @Implementation(minSdk = O)
  protected ComponentName startForegroundService(Intent service) {
    return startService(service);
  }

  @Implementation
  protected boolean stopService(Intent name) {
    validateServiceIntent(name);
    return getShadowInstrumentation().stopService(name);
  }

  @Implementation(minSdk = Q)
  protected boolean bindService(
      Intent service, int flags, Executor executor, ServiceConnection conn) {
    return getShadowInstrumentation().bindService(service, flags, executor, conn);
  }

  @Implementation
  protected boolean bindService(Intent intent, final ServiceConnection serviceConnection, int i) {
    validateServiceIntent(intent);
    return getShadowInstrumentation().bindService(intent, serviceConnection, i);
  }

  /** Binds to a service but ignores the given UserHandle. */
  @Implementation(minSdk = LOLLIPOP)
  protected boolean bindServiceAsUser(
      Intent intent, final ServiceConnection serviceConnection, int i, UserHandle userHandle) {
    return bindService(intent, serviceConnection, i);
  }

  @Implementation
  protected void unbindService(final ServiceConnection serviceConnection) {
    getShadowInstrumentation().unbindService(serviceConnection);
  }

  // This is a private method in ContextImpl so we copy the relevant portions of it here.
  @Implementation(minSdk = KITKAT)
  protected void validateServiceIntent(Intent service) {
    if (service.getComponent() == null
        && service.getPackage() == null
        && realContextImpl.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.LOLLIPOP) {
      throw new IllegalArgumentException("Service Intent must be explicit: " + service);
    }
  }

  /**
   * Behaves as {@link android.app.ContextImpl#startActivity(Intent, Bundle)}. The user parameter is
   * ignored.
   */
  @Implementation(minSdk = LOLLIPOP)
  protected void startActivityAsUser(Intent intent, Bundle options, UserHandle user) {
    // TODO: Remove this once {@link com.android.server.wmActivityTaskManagerService} is
    // properly shadowed.
    reflector(_ContextImpl_.class, realContextImpl).startActivity(intent, options);
  }

  /* Set the user id returned by {@link #getUserId()}. */
  public void setUserId(int userId) {
    this.userId = userId;
  }

  @Implementation(minSdk = JELLY_BEAN_MR2)
  protected int getUserId() {
    if (userId != null) {
      return userId;
    } else {
      return directlyOn(realContextImpl, ShadowContextImpl.CLASS_NAME, "getUserId");
    }
  }

  @Implementation(maxSdk = JELLY_BEAN_MR2)
  protected File getExternalFilesDir(String type) {
    return Environment.getExternalStoragePublicDirectory(type);
  }

  @Implementation(minSdk = KITKAT)
  protected File[] getExternalFilesDirs(String type) {
    return new File[] {Environment.getExternalStoragePublicDirectory(type)};
  }

  @Resetter
  public static void reset() {
    String prefsCacheFieldName =
        RuntimeEnvironment.getApiLevel() >= N ? "sSharedPrefsCache" : "sSharedPrefs";
    Object prefsDefaultValue = RuntimeEnvironment.getApiLevel() >= KITKAT ? null : new HashMap<>();
    Class contextImplClass =
        ReflectionHelpers.loadClass(
            ShadowContextImpl.class.getClassLoader(), "android.app.ContextImpl");
    ReflectionHelpers.setStaticField(contextImplClass, prefsCacheFieldName, prefsDefaultValue);

    if (RuntimeEnvironment.getApiLevel() <= VERSION_CODES.LOLLIPOP_MR1) {
      HashMap fetchers =
          ReflectionHelpers.getStaticField(contextImplClass, "SYSTEM_SERVICE_MAP");
      Class staticServiceFetcherClass =
          ReflectionHelpers.loadClass(
              ShadowContextImpl.class.getClassLoader(),
              "android.app.ContextImpl$StaticServiceFetcher");

      for (Object o : fetchers.values()) {
        if (staticServiceFetcherClass.isInstance(o)) {
          ReflectionHelpers.setField(staticServiceFetcherClass, o, "mCachedInstance", null);
        }
      }

      if (RuntimeEnvironment.getApiLevel() >= KITKAT) {

        Object windowServiceFetcher = fetchers.get(Context.WINDOW_SERVICE);
        ReflectionHelpers.setField(
            windowServiceFetcher.getClass(), windowServiceFetcher, "mDefaultDisplay", null);
      }
    }
  }

  private ShadowInstrumentation getShadowInstrumentation() {
    ActivityThread activityThread = (ActivityThread) RuntimeEnvironment.getActivityThread();
    return Shadow.extract(activityThread.getInstrumentation());
  }

  @Implementation
  public File getDatabasePath(String name) {
    // Windows is an abomination.
    if (File.separatorChar == '\\' && Paths.get(name).isAbsolute()) {
      String dirPath = name.substring(0, name.lastIndexOf(File.separatorChar));
      File dir = new File(dirPath);
      name = name.substring(name.lastIndexOf(File.separatorChar));
      File f = new File(dir, name);
      if (!dir.isDirectory() && dir.mkdir()) {
        FileUtils.setPermissions(dir.getPath(), 505, -1, -1);
      }
      return f;
    } else {
      return reflector(_ContextImpl_.class, realContextImpl).getDatabasePath(name);
    }
  }

  @Implementation
  protected SharedPreferences getSharedPreferences(String name, int mode) {
    // Windows does not allow colons in file names, which may be used in shared preference
    // names. URL-encode any colons in Windows.
    if (!Strings.isNullOrEmpty(name) && File.separatorChar == '\\') {
      name = name.replace(":", "%3A");
    }
    return reflector(_ContextImpl_.class, realContextImpl).getSharedPreferences(name, mode);
  }

  /** Reflector interface for {@link android.app.ContextImpl}'s internals. */
  @ForType(className = CLASS_NAME)
  public interface _ContextImpl_ {
    @Static
    Context createSystemContext(ActivityThread activityThread);

    @Static
    Context createAppContext(ActivityThread activityThread, LoadedApk loadedApk);

    @Static
    Context createActivityContext(
        ActivityThread mainThread,
        LoadedApk packageInfo,
        ActivityInfo activityInfo,
        IBinder activityToken,
        int displayId,
        Configuration overrideConfiguration);

    void setOuterContext(Context context);

    @Direct
    Object getSystemService(String name);

    void startActivity(Intent intent, Bundle options);

    @Direct
    File getDatabasePath(String name);

    @Direct
    SharedPreferences getSharedPreferences(String name, int mode);

    @Accessor("mClassLoader")
    void setClassLoader(ClassLoader classLoader);
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy