content: /*
   * Copyright (C) 2017 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.test.services.speakeasy.client;
  
  import android.app.ActivityManager;
  import android.app.ActivityManagerNative;
  import android.os.Binder;
  import android.os.Build;
  import android.os.Bundle;
  import android.os.IBinder;
  import android.os.RemoteException;
  import android.util.Log;
  import androidx.test.services.speakeasy.SpeakEasyProtocol;
  import java.lang.reflect.Constructor;
  import java.lang.reflect.Field;
  import java.lang.reflect.InvocationTargetException;
  import java.lang.reflect.Method;
  import java.security.SecureRandom;
  import java.util.Random;
  
  /** Creates a connection to SpeakEasy without requiring a context. */
  public abstract class ToolConnection implements Connection {
    private static final String PACKAGE_NAME = "androidx.test.services";
    private static final String CONTENT_PROVIDER = "androidx_test_services.speak_easy";
    private static final String ATTRIBUTON_SOURCE_CLASS_NAME = "android.content.AttributionSource";
    private static final String TAG = "ToolConnection";
  
    private final Random random = new SecureRandom();
    protected final String packageName;
    protected final String contentProvider;
  
    /** Creates a connection to speakeasy. */
    public static Connection makeConnection() {
      return makeConnection(PACKAGE_NAME, CONTENT_PROVIDER);
    }
  
    static Connection makeConnection(String packageName, String contentProvider) {
      if (Build.VERSION.SDK_INT <= 25) {
        return new ToolConnectionJBToN(packageName, contentProvider);
      } else {
        return new ToolConnectionO(packageName, contentProvider);
      }
    }
  
    private ToolConnection(String packageName, String contentProvider) {
      this.packageName = checkNotNull(packageName);
      this.contentProvider = checkNotNull(contentProvider);
    }
  
    @Override
    public void publish(IBinder binder, PublishResultReceiver rr) {
      publish(Long.toHexString(random.nextLong()), binder, rr);
    }
  
    // VisibleForTesting.
    void publish(String key, IBinder binder, PublishResultReceiver rr) {
      checkNotNull(binder);
      checkNotNull(rr);
      // consider timing out  (could happen when speakeasy is not installed)
      Bundle msg = SpeakEasyProtocol.Publish.asBundle(key, binder, rr);
      try {
        doCall(msg);
      } catch (RemoteException re) {
        throw new RuntimeException(re);
      }
    }
  
    @Override
    public void find(String key, FindResultReceiver rr) {
      checkNotNull(key);
      checkNotNull(rr);
      // consider timing out  (could happen when speakeasy is not installed)
      try {
        doCall(SpeakEasyProtocol.Find.asBundle(key, rr));
      } catch (RemoteException re) {
        throw new RuntimeException(re);
      }
    }
  
    protected abstract void doCall(Bundle b) throws RemoteException;
  
    private abstract static class ToolConnectionPostIcs extends ToolConnection {
  
      protected abstract Object getActivityManager();
  
      ToolConnectionPostIcs(String packageName, String contentProvider) {
        super(packageName, contentProvider);
      }
  
      @Override
      protected final void doCall(Bundle b) throws RemoteException {
        try {
          Log.i(TAG, "looking up IActivityManager");
          Class<?> iam = Class.forName("android.app.IActivityManager");
          Log.i(TAG, "looking up getContentProviderExternal");
  
          Method getCPEMethod = null;
          boolean getCPEPostP;
          try {
            getCPEMethod =
                iam.getMethod(
                    "getContentProviderExternal", String.class, Integer.TYPE, IBinder.class);
            getCPEPostP = false;
          } catch (NoSuchMethodException nsm) {
            // API Level 29 and above have changed the API Signature.
            getCPEMethod =
                iam.getMethod(
                    "getContentProviderExternal",
                    String.class,
                    Integer.TYPE,
                    IBinder.class,
                    String.class);
            getCPEPostP = true;
          }
          Log.i(TAG, "looking up removeContentProviderExternal");
          Method removeCPE =
              iam.getMethod("removeContentProviderExternal", String.class, IBinder.class);
          IBinder token = new Binder();
          try {
            Log.i(TAG, "Getting a content provider holder for: " + contentProvider);
            Object cph;
            int userId = getCurrentUserOrUserZero();
            Log.d(TAG, "Starting contentProvider as user: " + userId);
            if (getCPEPostP) {
              cph = getCPEMethod.invoke(getActivityManager(), contentProvider, userId, token, null);
            } else {
              cph = getCPEMethod.invoke(getActivityManager(), contentProvider, userId, token);
            }
            if (null == cph) {
              throw new IllegalStateException(
                  String.format(
                      "Call to getContentProviderExternal for: %s returns null!", contentProvider));
            }
            Log.i(TAG, "Getting the provider field");
            Field f = cph.getClass().getDeclaredField("provider");
            f.setAccessible(true);
            Object provider = f.get(cph);
  
            if (null == provider) {
              throw new IllegalStateException(
                  String.format(
                      "Call to getContentProviderExternal for: %s returns null provider!",
                      contentProvider));
            }
            Log.i(TAG, "Finding the call method");
            Method call = null;
            for (Method m : provider.getClass().getDeclaredMethods()) {
              if ("call".equals(m.getName())) {
                call = m;
              }
            }
            if (call == null) {
              Log.e(TAG, "No call method!");
              throw new RuntimeException("Could not find call method on content provider!");
            }
            if (call.getParameterTypes().length == 4) {
              Log.i(TAG, "Invoking modern call method");
              call.invoke(provider, null, null, null, b);
            } else if (call.getParameterTypes().length == 5
                && ATTRIBUTON_SOURCE_CLASS_NAME.equals(call.getParameterTypes()[0].getName())) {
              Log.i(TAG, "Invoking Android S call method");
              Class<?> attrSrcClass = Class.forName(ATTRIBUTON_SOURCE_CLASS_NAME);
              Constructor<?> attrSrcCons =
                  attrSrcClass.getConstructor(Integer.TYPE, String.class, String.class);
              Log.i(TAG, "Using uid " + Binder.getCallingUid());
              Object attrSrcObj = attrSrcCons.newInstance(Binder.getCallingUid(), null, null);
              call.invoke(provider, attrSrcObj, CONTENT_PROVIDER, null, null, b);
            } else if (call.getParameterTypes().length == 5) {
              Log.i(TAG, "Invoking Android Q call method");
              call.invoke(provider, null, CONTENT_PROVIDER, null, null, b);
            } else if (call.getParameterTypes().length == 6) {
              Log.i(TAG, "Invoking Android R call method");
              call.invoke(provider, null, null, CONTENT_PROVIDER, null, null, b);
            } else {
              Log.i(TAG, "Invoking legacy call method");
              call.invoke(provider, null, null, b);
            }
            Log.i(TAG, "Intent sent!");
          } finally {
            Log.i(TAG, "Releasing content provider");
            removeCPE.invoke(getActivityManager(), contentProvider, token);
            Log.i(TAG, "Released content provider");
          }
        } catch (IllegalAccessException
            | ClassNotFoundException
            | NoSuchMethodException
            | InvocationTargetException
            | NoSuchFieldException
            | InstantiationException ex) {
          Log.e(TAG, "Connecting to content providers has failed!", ex);
          throw new RuntimeException(ex);
        }
      }
  
      private static int getCurrentUserOrUserZero() {
        try {
          Log.d(TAG, "looking up getCurrentUser");
          Method method = ActivityManager.class.getMethod("getCurrentUser");
          return (int) method.invoke(null);
        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
          Log.e(TAG, "looking up getCurrentUser error ", e);
          return 0;
        }
      }
    }
  
    private static class ToolConnectionJBToN extends ToolConnectionPostIcs {
  
      ToolConnectionJBToN(String packageName, String contentProvider) {
        super(packageName, contentProvider);
      }
  
      @Override
      protected Object getActivityManager() {
        Log.i(TAG, "Invoking ActivityManagerNative.getDefault");
        return ActivityManagerNative.getDefault();
      }
    }
  
    private static class ToolConnectionO extends ToolConnectionPostIcs {
  
      ToolConnectionO(String packageName, String contentProvider) {
        super(packageName, contentProvider);
      }
  
      @Override
      protected Object getActivityManager() {
        try {
          Log.i(TAG, "Invoking getService");
          Method getServiceMethod = ActivityManager.class.getMethod("getService");
          return getServiceMethod.invoke(null);
        } catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException ex) {
          Log.e(TAG, "Could not find / invoke get service", ex);
          throw new RuntimeException(ex);
        }
      }
    }
  
    private static <T> T checkNotNull(T val) {
      if (null == val) {
        throw new NullPointerException();
      }
      return val;
    }
  }
  