chromium/components/embedder_support/android/java/src/org/chromium/components/embedder_support/application/ClassLoaderContextWrapperFactory.java

// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.components.embedder_support.application;

import android.content.ComponentCallbacks;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;

import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.WrappedClassLoader;

import java.lang.ref.WeakReference;
import java.util.WeakHashMap;

/**
 * This class allows us to wrap the application context so that a Chromium implementation loaded
 * from a separate APK can correctly reference both org.chromium.* and application classes which is
 * necessary to properly inflate UI.
 */
public class ClassLoaderContextWrapperFactory {
    private static final String TAG = "ClsLdrContextWrapper";

    private ClassLoaderContextWrapperFactory() {}

    // Note WeakHashMap only guarantees that keys are weakly held, and ContextWrapper holds a strong
    // reference to the wrapped context. So need a WeakReference here to ensure the Context does not
    // leak.
    private static final WeakHashMap<Context, WeakReference<ClassLoaderContextWrapper>>
            sCtxToWrapper = new WeakHashMap<>();
    private static final Object sLock = new Object();

    // Data needed to create a new package context as a resource override context for a wrapper.
    private static final class OverrideInfo {
        // WebView package name.
        public final String mPackageName;
        // Theme to apply to the resource override context.
        public final int mTheme;
        // Flags to pass through to createPackageContext.
        public final int mFlags;

        public OverrideInfo(String packageName, int theme, int flags) {
            mPackageName = packageName;
            mTheme = theme;
            mFlags = flags;
        }
    }

    private static OverrideInfo sOverrideInfo;

    /**
     * Sets necessary info for calling createPackageContext to create resource override contexts.
     */
    public static void setOverrideInfo(String packageName, int theme, int flags) {
        synchronized (sLock) {
            sOverrideInfo = new OverrideInfo(packageName, theme, flags);
        }
    }

    public static Context get(Context ctx) {
        // Avoid double-wrapping a context.
        if (ctx instanceof ClassLoaderContextWrapper) {
            return ctx;
        }
        ClassLoaderContextWrapper wrapper = null;
        synchronized (sLock) {
            WeakReference<ClassLoaderContextWrapper> ref = sCtxToWrapper.get(ctx);
            wrapper = (ref == null) ? null : ref.get();
            if (wrapper == null) {
                wrapper = new ClassLoaderContextWrapper(ctx);
                sCtxToWrapper.put(ctx, new WeakReference<>(wrapper));
                try {
                    if (sOverrideInfo != null) {
                        Context override =
                                wrapper.createPackageContext(
                                        sOverrideInfo.mPackageName, sOverrideInfo.mFlags);
                        wrapper.setResourceOverrideContext(
                                new ContextThemeWrapper(override, sOverrideInfo.mTheme));
                    }
                } catch (PackageManager.NameNotFoundException e) {
                    Log.e(TAG, "Could not get resource override context.");
                }
            }
        }
        return wrapper;
    }

    /**
     * Should be used by WebView code only to lookup files in the Resources/Assets folder or for
     * algorithmic darkening code.
     *
     * @param context the Context. If this is not a ClassLoaderContextWrapper, it is returned.
     * @return the Context for the embedding application or {@code context}.
     */
    public static Context getOriginalApplicationContext(Context context) {
        if (context instanceof ClassLoaderContextWrapper wrapper) {
            return wrapper.getBaseContext();
        }
        return context;
    }

    private static class ClassLoaderContextWrapper extends ContextWrapper {
        private Context mApplicationContext;

        private Context mResourceOverrideContext;

        public ClassLoaderContextWrapper(Context base) {
            super(base);
        }

        @Override
        public ClassLoader getClassLoader() {
            final ClassLoader appCl = getBaseContext().getClassLoader();
            final ClassLoader chromiumCl = ClassLoaderContextWrapper.class.getClassLoader();

            return new WrappedClassLoader(chromiumCl, appCl);
        }

        @Override
        public Object getSystemService(String name) {
            if (Context.LAYOUT_INFLATER_SERVICE.equals(name)) {
                if (mResourceOverrideContext != null) {
                    return LayoutInflater.from(mResourceOverrideContext);
                }
                LayoutInflater i = (LayoutInflater) getBaseContext().getSystemService(name);
                return i.cloneInContext(this);
            } else {
                return getBaseContext().getSystemService(name);
            }
        }

        @Override
        @SuppressWarnings("NoContextGetApplicationContext")
        public Context getApplicationContext() {
            if (mApplicationContext == null) {
                Context appCtx = getBaseContext().getApplicationContext();
                if (appCtx == getBaseContext()) {
                    mApplicationContext = this;
                } else {
                    mApplicationContext = get(appCtx);
                }
            }
            return mApplicationContext;
        }

        @Override
        public void registerComponentCallbacks(ComponentCallbacks callback) {
            // We have to override registerComponentCallbacks and unregisterComponentCallbacks
            // since they call getApplicationContext().[un]registerComponentCallbacks()
            // which causes us to go into a loop.
            getBaseContext().registerComponentCallbacks(callback);
        }

        @Override
        public void unregisterComponentCallbacks(ComponentCallbacks callback) {
            getBaseContext().unregisterComponentCallbacks(callback);
        }

        @Override
        public void startActivity(Intent intent) {
            if (ContextUtils.activityFromContext(this) == null) {
                // FLAG_ACTIVITY_NEW_TASK is needed to start activities from a non-activity
                // context.
                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            }

            super.startActivity(intent);
        }

        @Override
        public AssetManager getAssets() {
            return getResourceContext().getAssets();
        }

        @Override
        public Resources getResources() {
            return getResourceContext().getResources();
        }

        @Override
        public Resources.Theme getTheme() {
            return getResourceContext().getTheme();
        }

        void setResourceOverrideContext(Context context) {
            mResourceOverrideContext = context;
        }

        private Context getResourceContext() {
            return (mResourceOverrideContext != null) ? mResourceOverrideContext : getBaseContext();
        }
    }
}