chromium/chrome/browser/ui/android/night_mode/java/src/org/chromium/chrome/browser/night_mode/RemoteViewsWithNightModeInflater.java

// Copyright 2019 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.chrome.browser.night_mode;

import android.content.Context;
import android.content.ContextWrapper;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RemoteViews;

import androidx.annotation.Nullable;

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

/**
 * Performs inflation of {@link RemoteViews} taking into account the local night mode.
 * {@link RemoteViews#apply} always uses resource configuration corresponding to system
 *  settings, see https://buganizer.corp.google.com/issues/133424086, http://crbug.com/1626864.
 */
public class RemoteViewsWithNightModeInflater {
    private static final String TAG = "RemoteViewsInflater";

    /**
     * Inflates the RemoteViews.
     *
     * @param remoteViews {@link RemoteViews} to inflate.
     * @param parent Parent {@link ViewGroup} to use for inflation
     * @param isInLocalNightMode Whether night mode is enabled for the current Activity.
     * @param isInSystemNightMode Whether night mode is enabled in system settings.
     * @return Inflated View or null in case of failure.
     */
    public static @Nullable View inflate(
            RemoteViews remoteViews,
            @Nullable ViewGroup parent,
            boolean isInLocalNightMode,
            boolean isInSystemNightMode) {
        if (isInLocalNightMode == isInSystemNightMode) {
            // RemoteViews#apply will use the resource configuration corresponding to system
            // settings.
            return inflateNormally(remoteViews, parent);
        }
        View view = inflateWithEnforcedDarkMode(remoteViews, parent, isInLocalNightMode);
        if (view == null) {
            view = inflateNormally(remoteViews, parent);
            assert view == null : "Failed to inflate valid RemoteViews with enforced dark mode";
        }
        return view;
    }

    private static @Nullable View inflateNormally(RemoteViews remoteViews, ViewGroup parent) {
        try {
            return remoteViews.apply(ContextUtils.getApplicationContext(), parent);
        } catch (RuntimeException e) {
            // Catching a general RuntimeException is ugly, but RemoteViews are passed in by the
            // client app, so can contain all sorts of problems, eg. b/205503898.
            Log.e(TAG, "Failed to inflate the RemoteViews", e);
            return null;
        }
    }

    private static @Nullable View inflateWithEnforcedDarkMode(
            RemoteViews remoteViews, ViewGroup parent, boolean isInLocalNightMode) {
        // This is a modified version of RemoteViews#apply. RemoteViews#apply performs two steps:
        // 1. Inflate the View using the context of the remote app.
        // 2. Apply the Actions to the inflated View (actions are requested by remote app using
        // various setters, such as RemoteViews#setTextViewText).
        //
        // The context used at step 1 does not respect the Configuration override of the Context
        // we pass as an argument of apply().
        //
        // Here we perform step 1 manually, overriding the Configuration just before inflating.
        // Then we perform step 2 using RemoteViews#reapply on an already inflated View.

        try {
            final Context contextForResources =
                    getContextForResources(remoteViews, isInLocalNightMode);
            // App context must be used instead of activity context to avoid the support library
            // bug, see https://crbug.com/783834
            Context appContext = ContextUtils.getApplicationContext();
            Context contextForRemoteViews =
                    new RemoteViewsContextWrapper(appContext, contextForResources);

            LayoutInflater inflater =
                    LayoutInflater.from(appContext).cloneInContext(contextForRemoteViews);
            View view = inflater.inflate(remoteViews.getLayoutId(), parent, false);

            remoteViews.reapply(appContext, view);
            return view;
        } catch (PackageManager.NameNotFoundException | RuntimeException e) {
            // Catching a general RuntimeException is ugly, but RemoteViews are passed in by the
            // client app, so can contain all sorts of problems, eg b/205503898.
            Log.e(TAG, "Failed to inflate the RemoteViews", e);
            return null;
        }
    }

    private static Context getContextForResources(
            RemoteViews remoteViews, boolean isInLocalNightMode)
            throws PackageManager.NameNotFoundException {
        Context appContext = ContextUtils.getApplicationContext();
        String remotePackage = remoteViews.getPackage();
        if (appContext.getPackageName().equals(remotePackage)) return appContext;

        Context remoteContext =
                appContext.createPackageContext(remotePackage, Context.CONTEXT_RESTRICTED);

        // This line is what makes the difference with RemoteViews#apply.
        Context contextWithEnforcedNightMode =
                NightModeUtils.wrapContextWithNightModeConfig(
                        remoteContext, /* themeResId= */ 0, isInLocalNightMode);

        return contextWithEnforcedNightMode;
    }

    // Copied from RemoteViews
    private static class RemoteViewsContextWrapper extends ContextWrapper {
        private final Context mContextForResources;

        RemoteViewsContextWrapper(Context context, Context contextForResources) {
            super(context);
            mContextForResources = contextForResources;
        }

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

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

        @Override
        public String getPackageName() {
            return mContextForResources.getPackageName();
        }
    }
}