Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit cce841ad authored by Makoto Onuki's avatar Makoto Onuki
Browse files

Experimental implementation of Instrumentation.startActivity

Bug: 421246454
Flag: TEST_ONLY
Test: $ANDROID_BUILD_TOP/frameworks/base/ravenwood/scripts/run-ravenwood-tests.sh -s
Change-Id: Ide6e97ab7a7339781904b8729bd070dd43c69f53
parent 0e8b0e70
Loading
Loading
Loading
Loading
+18 −1
Original line number Diff line number Diff line
@@ -15,8 +15,12 @@
 */
package android.app;

import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.platform.test.ravenwood.RavenwoodExperimentalApiChecker.onExperimentalApiCalled;

import android.content.pm.PackageManager;
import android.os.FileUtils;
import android.os.IBinder;
import android.platform.test.ravenwood.RavenwoodPackageManager;

import java.io.File;
@@ -25,7 +29,7 @@ public class ContextImpl_ravenwood {
    private static final String TAG = "ContextImpl_ravenwood";

    static PackageManager getPackageManagerInner(ContextImpl contextImpl) {
        return new RavenwoodPackageManager(contextImpl);
        return RavenwoodPackageManager.create(contextImpl);
    }

    static File ensurePrivateDirExists(File file, int mode, int gid, String xattr) {
@@ -40,4 +44,17 @@ public class ContextImpl_ravenwood {
        }
        return file;
    }

    /** Experimental implementation */
    static int checkPermission(ContextImpl self, String permission, int pid, int uid) {
        onExperimentalApiCalled(2);
        return PERMISSION_GRANTED;
    }

    /** Experimental implementation */
    static int checkPermission(ContextImpl self, String permission, int pid, int uid,
            IBinder callerToken) {
        onExperimentalApiCalled(2);
        return PERMISSION_GRANTED;
    }
}
+31 −0
Original line number Diff line number Diff line
@@ -15,7 +15,15 @@
 */
package android.app;

import static android.platform.test.ravenwood.RavenwoodExperimentalApiChecker.onExperimentalApiCalled;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UiThread;
import android.content.Intent;
import android.os.Bundle;
import android.platform.test.ravenwood.RavenwoodUtils;
import android.util.Log;

public class Instrumentation_ravenwood {
    private static final String TAG = "Instrumentation_ravenwood";
@@ -26,4 +34,27 @@ public class Instrumentation_ravenwood {
    static void checkPendingExceptionOnRavenwood() {
        RavenwoodUtils.getMainHandler().post(() -> {});
    }

    static Activity startActivitySync(@NonNull Instrumentation inst,
            @NonNull Intent intent,
            @Nullable Bundle options) {
        onExperimentalApiCalled(2);
        return RavenwoodUtils.runOnMainThreadSync(
                () -> startActivitySyncOnMain(inst, intent, options));
    }

    @UiThread
    private static Activity startActivitySyncOnMain(
            @NonNull Instrumentation inst,
            @NonNull Intent intent,
            @Nullable Bundle options) {
        Log.logStackTrace(Log.LOG_ID_MAIN, Log.INFO, TAG,
                "startActivity: intent=" + intent + " options=" + options, 2);

        try {
            return RavenwoodActivityDriver.getInstance().createResumedActivity(intent, options);
        } catch (Exception e) {
            throw new RuntimeException("Failed to start activity. Intent=" + intent, e);
        }
    }
}
+75 −0
Original line number Diff line number Diff line
@@ -15,16 +15,31 @@
 */
package android.app;

import static android.platform.test.ravenwood.RavenwoodExperimentalApiChecker.isExperimentalApiEnabled;

import android.annotation.NonNull;
import android.app.SystemServiceRegistry.CachedServiceFetcher;
import android.app.SystemServiceRegistry.ServiceFetcher;
import android.content.ClipboardManager;
import android.content.Context;
import android.hardware.input.InputManager;
import android.os.IBinder;
import android.os.IUserManager;
import android.os.PermissionEnforcer;
import android.os.ServiceManager;
import android.os.ServiceManager.ServiceNotFoundException;
import android.os.UserManager;
import android.platform.test.ravenwood.RavenwoodPermissionEnforcer;
import android.ravenwood.example.BlueManager;
import android.ravenwood.example.RedManager;
import android.view.LayoutInflater;
import android.view.WindowManager;
import android.view.WindowManagerImpl;
import android.view.autofill.AutofillManager;
import android.view.autofill.IAutoFillManager;
import android.view.inputmethod.InputMethodManager;

import com.android.internal.policy.PhoneLayoutInflater;

public class SystemServiceRegistry_ravenwood {
    private SystemServiceRegistry_ravenwood() {
@@ -67,6 +82,8 @@ public class SystemServiceRegistry_ravenwood {
                        return new RavenwoodPermissionEnforcer();
                    }});

        maybeRegisterExperimentalServices();

        registerRavenwoodSpecificServices();
    }

@@ -87,4 +104,62 @@ public class SystemServiceRegistry_ravenwood {
                    }
                });
    }

    /**
     * Register "experimental" system services, which are _not_ supported. They're used only for
     * Ravenwood internal development.
     */
    private static void maybeRegisterExperimentalServices() {

        if (!isExperimentalApiEnabled()) {
            return;
        }

        registerService(Context.INPUT_SERVICE, InputManager.class,
                new CachedServiceFetcher<InputManager>() {
            @Override
            public InputManager createService(ContextImpl ctx) {
                return new InputManager(ctx.getOuterContext());
            }});

        registerService(Context.INPUT_METHOD_SERVICE, InputMethodManager.class,
                new ServiceFetcher<InputMethodManager>() {
            @Override
            public InputMethodManager getService(ContextImpl ctx) {
                return InputMethodManager.forContext(ctx.getOuterContext());
            }});

        registerService(Context.WINDOW_SERVICE, WindowManager.class,
                new CachedServiceFetcher<WindowManager>() {
            @Override
            public WindowManager createService(ContextImpl ctx) {
                return new WindowManagerImpl(ctx);
            }});

        registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
                new CachedServiceFetcher<LayoutInflater>() {
            @Override
            public LayoutInflater createService(ContextImpl ctx) {
                return new PhoneLayoutInflater(ctx.getOuterContext());
            }});

        registerService(Context.USER_SERVICE, UserManager.class,
                new CachedServiceFetcher<UserManager>() {
            @Override
            public UserManager createService(ContextImpl ctx) throws ServiceNotFoundException {
                IBinder b = ServiceManager.getServiceOrThrow(Context.USER_SERVICE);
                IUserManager service = IUserManager.Stub.asInterface(b);
                return new UserManager(ctx, service);
            }});

        registerService(Context.AUTOFILL_MANAGER_SERVICE, AutofillManager.class,
                new CachedServiceFetcher<AutofillManager>() {
            @Override
            public AutofillManager createService(ContextImpl ctx) throws ServiceNotFoundException {
                // Get the services without throwing as this is an optional feature
                IBinder b = ServiceManager.getService(Context.AUTOFILL_MANAGER_SERVICE);
                IAutoFillManager service = IAutoFillManager.Stub.asInterface(b);
                return new AutofillManager(ctx.getOuterContext(), service);
            }});
    }
}
+152 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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 android.app;

import android.annotation.NonNull;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.os.Binder;
import android.os.Bundle;
import android.platform.test.ravenwood.RavenwoodExperimentalApiChecker;
import android.platform.test.ravenwood.RavenwoodUtils;
import android.view.Display;
import android.view.WindowManager;

/**
 * Experimental implementation of launching activities.
 */
@SuppressWarnings("unchecked")
public class RavenwoodActivityDriver {
    private static final RavenwoodActivityDriver sInstance = new RavenwoodActivityDriver();

    private RavenwoodActivityDriver() {
    }

    public static RavenwoodActivityDriver getInstance() {
        return sInstance;
    }

    @SuppressWarnings("unchecked")
    private static <T extends Activity> Class<T> resolveActivityClass(Intent intent) {
        try {
            // TODO: Handle the case where the component name is null.
            return (Class<T>) Class.forName(intent.getComponent().getClassName());
        } catch (Exception e) {
            throw new RuntimeException("Failed to start an activity", e);
        }
    }

    private static ActivityInfo makeActivityInfo(
            @NonNull ApplicationInfo appInfo, @NonNull Intent intent) {
        var clazz = resolveActivityClass(intent);

        // Here's an example ActivityInfo from the launcher:
        // http://screen/Beq9oSdYSntJr5k
        ActivityInfo acti = new ActivityInfo();
        acti.applicationInfo = appInfo;
        acti.enabled = true;
        acti.exported = true;
        acti.packageName = appInfo.packageName;

        acti.labelRes = 0; // TODO
        acti.launchMode = 2; // from the example
        acti.resizeMode = 2; // from the example
        acti.flags = 0; // TODO
        acti.name = clazz.getName();

        acti.processName = appInfo.packageName;

        return acti;
    }

    /**
     * Instantiate an activity and driver it to the "RESUMED" state.
     */
    public <T extends Activity> T createResumedActivity(
            Intent intent,
            Bundle activityOptions // ignored for now.
    ) throws Exception {
        RavenwoodExperimentalApiChecker.onExperimentalApiCalled(0);

        Application app = RavenwoodAppDriver.getInstance().getApplication();
        ContextImpl appImpl = ContextImpl.getImpl(app);
        ApplicationInfo appInfo = appImpl.getApplicationInfo();
        ActivityInfo activityInfo = makeActivityInfo(appInfo, intent);
        Binder token = new Binder();
        int displayId = Display.DEFAULT_DISPLAY;

        ContextImpl activityContextImpl = ContextImpl.createActivityContext(
                appImpl.mMainThread,
                appImpl.mPackageInfo,
                activityInfo,
                token,
                displayId,
                null // overrideConfiguration
        );

        T activity = (T) resolveActivityClass(intent).getConstructor().newInstance();
        activity.attach(
                activityContextImpl,
                appImpl.mMainThread,
                appImpl.mMainThread.mInstrumentation,
                token,
                0, // mIdent -- ??
                app,
                intent,
                activityInfo,
                "Activity title",
                null, // parent
                "embeddedID", // mEmbeddedID -- ??
                null, // lastNonConfigurationInstances
                null, // config
                "referrer",
                null, // IVoiceInteractor,
                null, // window
                null, // activityConfigCallback
                null, // assistToken
                null, // shareableActivityToken
                null // initialCallerInfoAccessToken
        );

        // ActivityController has this.
        // return create().start().postCreate(null).resume().visible().topActivityResumed(true);

        Bundle savedInstanceState = null;
        activity.performCreate(savedInstanceState);
        activity.performStart("Called by Ravenwood");
        activity.onPostCreate(savedInstanceState);
        activity.performResume(false, // Followed by pause
                "Called by Ravenwood");
        // ActivityController has
        // "emulate logic of ActivityThread#handleResumeActivity"

        // Make visible -- this requires some setup, which is copied from ActivityController.
        activity.getWindow().getAttributes().type =
                WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
        activity.mDecor = activity.getWindow().getDecorView();
        activity.makeVisible();

        activity.performTopResumedActivityChanged(
                true, // isTop
                "Called by Ravenwood");

        RavenwoodUtils.getMainHandler().post(() ->
                activity.getWindow().getDecorView().getViewRootImpl().windowFocusChanged(true));

        return activity;
    }
}
+4 −0
Original line number Diff line number Diff line
@@ -75,6 +75,8 @@ public final class RavenwoodAppDriver {

    private final Instrumentation mInstrumentation;

    private final RavenwoodActivityDriver mActivityDriver = RavenwoodActivityDriver.getInstance();

    /**
     * Constructor. It essentially simulates the start of an app lifecycle.
     *
@@ -174,6 +176,8 @@ public final class RavenwoodAppDriver {
        ai.sourceDir = env.getResourcesApkFile(packageName).getAbsolutePath();
        ai.publicSourceDir = ai.sourceDir;

        ai.processName = packageName;

        // TODO: Set CE/DE data dirs too.

        return ai;
Loading