/*
 * Copyright (C) 2008 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.appwidget;

import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY;
import static android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD;
import static android.appwidget.flags.Flags.FLAG_ENGAGEMENT_METRICS;
import static android.appwidget.flags.Flags.engagementMetrics;
import static android.appwidget.flags.Flags.widgetDisplayChanges;
import static android.content.res.Flags.selfTargetingAndroidResourceFrro;

import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityOptions;
import android.app.PendingIntent;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.ComponentName;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.LauncherActivityInfo;
import android.content.pm.LauncherApps;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.PointF;
import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Pair;
import android.util.SizeF;
import android.util.SparseArray;
import android.util.SparseIntArray;
import android.view.Gravity;
import android.view.View;
import android.view.ViewParent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.AbsListView;
import android.widget.Adapter;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.FrameLayout;
import android.widget.RemoteViews;
import android.widget.RemoteViews.InteractionHandler;
import android.widget.RemoteViewsAdapter.RemoteAdapterConnectionCallback;
import android.widget.TextView;

import com.android.internal.annotations.VisibleForTesting;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;

/**
 * Provides the glue to show AppWidget views. This class offers automatic animation
 * between updates, and will try recycling old views for each incoming
 * {@link RemoteViews}.
 */
public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppWidgetHostListener {

    static final String TAG = "AppWidgetHostView";
    private static final String KEY_JAILED_ARRAY = "jail";
    private static final String KEY_INFLATION_ID = "inflation_id";

    static final boolean LOGD = false;

    static final int VIEW_MODE_NOINIT = 0;
    static final int VIEW_MODE_CONTENT = 1;
    static final int VIEW_MODE_ERROR = 2;
    static final int VIEW_MODE_DEFAULT = 3;

    @UnsupportedAppUsage
    int mAppWidgetId;
    @UnsupportedAppUsage
    AppWidgetProviderInfo mInfo;
    View mView;
    int mViewMode = VIEW_MODE_NOINIT;
    // If true, we should not try to re-apply the RemoteViews on the next inflation.
    boolean mColorMappingChanged = false;
    @NonNull
    private InteractionLogger mInteractionLogger = new InteractionLogger();
    private boolean mOnLightBackground;
    private SizeF mCurrentSize = null;
    private RemoteViews.ColorResources mColorResources = null;
    // Stores the last remote views last inflated.
    private RemoteViews mLastInflatedRemoteViews = null;
    private long mLastInflatedRemoteViewsId = -1;

    private Executor mAsyncExecutor;
    private CancellationSignal mLastExecutionSignal;
    private SparseArray<Parcelable> mDelayedRestoredState;
    private long mDelayedRestoredInflationId;

    /**
     * Create a host view.  Uses default fade animations.
     */
    public AppWidgetHostView(Context context) {
        this(context, android.R.anim.fade_in, android.R.anim.fade_out);
    }

    /**
     * @hide
     */
    public AppWidgetHostView(Context context, InteractionHandler handler) {
        this(context, android.R.anim.fade_in, android.R.anim.fade_out);
        setInteractionHandler(handler);
    }

    /**
     * Create a host view. Uses specified animations when pushing
     * {@link #updateAppWidget(RemoteViews)}.
     *
     * @param animationIn  Resource ID of in animation to use
     * @param animationOut Resource ID of out animation to use
     */
    @SuppressWarnings({"UnusedDeclaration"})
    public AppWidgetHostView(Context context, int animationIn, int animationOut) {
        super(context);
        // We want to segregate the view ids within AppWidgets to prevent
        // problems when those ids collide with view ids in the AppWidgetHost.
        setIsRootNamespace(true);
    }

    /**
     * Pass the given handler to RemoteViews when updating this widget. Unless this
     * is done immediately after construction, a call to {@link #updateAppWidget(RemoteViews)}
     * should be made.
     *
     * @hide
     */
    public void setInteractionHandler(InteractionHandler handler) {
        if (handler instanceof InteractionLogger logger) {
            // Nested AppWidgetHostViews should reuse the parent logger instead of wrapping it.
            mInteractionLogger = logger;
        } else {
            mInteractionLogger = new InteractionLogger(handler);
        }
    }

    /**
     * Return the InteractionLogger used by this class.
     *
     * @hide
     */
    @VisibleForTesting
    @NonNull
    public InteractionLogger getInteractionLogger() {
        return mInteractionLogger;
    }

    /**
     * @hide
     */
    public static class AdapterChildHostView extends AppWidgetHostView {

        public AdapterChildHostView(Context context) {
            super(context);
        }

        @Override
        protected boolean isVisibilityTrackingPermitted() {
            // Do not track visibility for individual adapter items
            return false;
        }
    }

    /**
     * Set the AppWidget that will be displayed by this view. This method also adds default padding
     * to widgets, as described in {@link #getDefaultPaddingForWidget(Context, ComponentName, Rect)}
     * and can be overridden in order to add custom padding.
     */
    public void setAppWidget(int appWidgetId, AppWidgetProviderInfo info) {
        mAppWidgetId = appWidgetId;
        mInfo = info;

        // We add padding to the AppWidgetHostView if necessary
        Rect padding = getDefaultPadding();
        setPadding(padding.left, padding.top, padding.right, padding.bottom);

        // Sometimes the AppWidgetManager returns a null AppWidgetProviderInfo object for
        // a widget, eg. for some widgets in safe mode.
        if (info != null) {
            String description = info.loadLabel(getContext().getPackageManager());
            if ((info.providerInfo.applicationInfo.flags & ApplicationInfo.FLAG_SUSPENDED) != 0) {
                description = Resources.getSystem().getString(
                        com.android.internal.R.string.suspended_widget_accessibility, description);
            }
            setContentDescription(description);
        }
    }

    /**
     * As of ICE_CREAM_SANDWICH we are automatically adding padding to widgets targeting
     * ICE_CREAM_SANDWICH and higher. The new widget design guidelines strongly recommend
     * that widget developers do not add extra padding to their widgets. This will help
     * achieve consistency among widgets.
     *
     * Note: this method is only needed by developers of AppWidgetHosts. The method is provided in
     * order for the AppWidgetHost to account for the automatic padding when computing the number
     * of cells to allocate to a particular widget.
     *
     * @param context   the current context
     * @param component the component name of the widget
     * @param padding   Rect in which to place the output, if null, a new Rect will be allocated and
     *                  returned
     * @return default padding for this widget, in pixels
     */
    public static Rect getDefaultPaddingForWidget(Context context, ComponentName component,
            Rect padding) {
        return getDefaultPaddingForWidget(context, padding);
    }

    private static Rect getDefaultPaddingForWidget(Context context, Rect padding) {
        if (padding == null) {
            padding = new Rect(0, 0, 0, 0);
        } else {
            padding.set(0, 0, 0, 0);
        }
        Resources r = context.getResources();
        padding.left = r.getDimensionPixelSize(
                com.android.internal.R.dimen.default_app_widget_padding_left);
        padding.right = r.getDimensionPixelSize(
                com.android.internal.R.dimen.default_app_widget_padding_right);
        padding.top = r.getDimensionPixelSize(
                com.android.internal.R.dimen.default_app_widget_padding_top);
        padding.bottom = r.getDimensionPixelSize(
                com.android.internal.R.dimen.default_app_widget_padding_bottom);
        return padding;
    }

    private Rect getDefaultPadding() {
        return getDefaultPaddingForWidget(mContext, null);
    }

    public int getAppWidgetId() {
        return mAppWidgetId;
    }

    public AppWidgetProviderInfo getAppWidgetInfo() {
        return mInfo;
    }

    @Override
    protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
        final SparseArray<Parcelable> jail = new SparseArray<>();
        super.dispatchSaveInstanceState(jail);

        Bundle bundle = new Bundle();
        bundle.putSparseParcelableArray(KEY_JAILED_ARRAY, jail);
        bundle.putLong(KEY_INFLATION_ID, mLastInflatedRemoteViewsId);
        container.put(generateId(), bundle);
        container.put(generateId(), bundle);
    }

    private int generateId() {
        final int id = getId();
        return id == View.NO_ID ? mAppWidgetId : id;
    }

    @Override
    protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
        final Parcelable parcelable = container.get(generateId());

        SparseArray<Parcelable> jail = null;
        long inflationId = -1;
        if (parcelable instanceof Bundle) {
            Bundle bundle = (Bundle) parcelable;
            jail = bundle.getSparseParcelableArray(KEY_JAILED_ARRAY);
            inflationId = bundle.getLong(KEY_INFLATION_ID, -1);
        }

        if (jail == null) jail = new SparseArray<>();

        mDelayedRestoredState = jail;
        mDelayedRestoredInflationId = inflationId;
        restoreInstanceState();
    }

    void restoreInstanceState() {
        long inflationId = mDelayedRestoredInflationId;
        SparseArray<Parcelable> state = mDelayedRestoredState;
        if (inflationId == -1 || inflationId != mLastInflatedRemoteViewsId) {
            return; // We don't restore.
        }
        mDelayedRestoredInflationId = -1;
        mDelayedRestoredState = null;
        try {
            super.dispatchRestoreInstanceState(state);
        } catch (Exception e) {
            Log.e(TAG, "failed to restoreInstanceState for widget id: " + mAppWidgetId + ", "
                    + (mInfo == null ? "null" : mInfo.provider), e);
        }
    }

    private SizeF computeSizeFromLayout(int left, int top, int right, int bottom) {
        float density = getResources().getDisplayMetrics().density;
        return new SizeF(
                (right - left - getPaddingLeft() - getPaddingRight()) / density,
                (bottom - top - getPaddingTop() - getPaddingBottom()) / density
        );
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        try {
            SizeF oldSize = mCurrentSize;
            SizeF newSize = computeSizeFromLayout(left, top, right, bottom);
            mCurrentSize = newSize;
            if (mLastInflatedRemoteViews != null) {
                RemoteViews toApply = mLastInflatedRemoteViews.getRemoteViewsToApplyIfDifferent(
                        oldSize, newSize);
                if (toApply != null) {
                    applyRemoteViews(toApply, false);
                    measureChildWithMargins(mView,
                            MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
                            0 /* widthUsed */,
                            MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY),
                            0 /* heightUsed */);
                }
            }
            if (changed && isVisibilityTrackingPermitted()) {
                mInteractionLogger.onPositionChanged();
            }
            super.onLayout(changed, left, top, right, bottom);
        } catch (final RuntimeException e) {
            Log.e(TAG, "Remote provider threw runtime exception, using error view instead.", e);
            handleViewError();
        }
    }

    @Override
    public void onWindowFocusChanged(boolean hasWindowFocus) {
        super.onWindowFocusChanged(hasWindowFocus);
        if (isVisibilityTrackingPermitted()) {
            mInteractionLogger.onWindowFocusChanged();
        }
    }

    /**
     * Remove bad view and replace with error message view
     */
    private void handleViewError() {
        removeViewInLayout(mView);
        View child = getErrorView();
        prepareView(child);
        addViewInLayout(child, 0, child.getLayoutParams());
        measureChild(child, MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
                MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
        child.layout(0, 0, child.getMeasuredWidth() + mPaddingLeft + mPaddingRight,
                child.getMeasuredHeight() + mPaddingTop + mPaddingBottom);
        mView = child;
        mViewMode = VIEW_MODE_ERROR;
    }

    /**
     * Provide guidance about the size of this widget to the AppWidgetManager. The widths and
     * heights should correspond to the full area the AppWidgetHostView is given. Padding added by
     * the framework will be accounted for automatically. This information gets embedded into the
     * AppWidget options and causes a callback to the AppWidgetProvider. In addition, the list of
     * sizes is explicitly set to an empty list.
     *
     * @param newOptions The bundle of options, in addition to the size information,
     *                   can be null.
     * @param minWidth   The minimum width in dips that the widget will be displayed at.
     * @param minHeight  The maximum height in dips that the widget will be displayed at.
     * @param maxWidth   The maximum width in dips that the widget will be displayed at.
     * @param maxHeight  The maximum height in dips that the widget will be displayed at.
     * @see AppWidgetProvider#onAppWidgetOptionsChanged(Context, AppWidgetManager, int, Bundle)
     * @deprecated use {@link AppWidgetHostView#updateAppWidgetSize(Bundle, List)} instead.
     */
    @Deprecated
    public void updateAppWidgetSize(Bundle newOptions, int minWidth, int minHeight, int maxWidth,
            int maxHeight) {
        updateAppWidgetSize(newOptions, minWidth, minHeight, maxWidth, maxHeight, false);
    }

    /**
     * Provide guidance about the size of this widget to the AppWidgetManager. The sizes should
     * correspond to the full area the AppWidgetHostView is given. Padding added by the framework
     * will be accounted for automatically.
     *
     * This method will update the option bundle with the list of sizes and the min/max bounds for
     * width and height.
     *
     * @param newOptions The bundle of options, in addition to the size information.
     * @param sizes      Sizes, in dips, the widget may be displayed at without calling the
     *                   provider
     *                   again. Typically, this will be size of the widget in landscape and
     *                   portrait.
     *                   On some foldables, this might include the size on the outer and inner
     *                   screens.
     * @see AppWidgetProvider#onAppWidgetOptionsChanged(Context, AppWidgetManager, int, Bundle)
     */
    public void updateAppWidgetSize(@NonNull Bundle newOptions, @NonNull List<SizeF> sizes) {
        AppWidgetManager widgetManager = AppWidgetManager.getInstance(mContext);

        Rect padding = getDefaultPadding();
        float density = getResources().getDisplayMetrics().density;

        float xPaddingDips = (padding.left + padding.right) / density;
        float yPaddingDips = (padding.top + padding.bottom) / density;

        ArrayList<SizeF> paddedSizes = new ArrayList<>(sizes.size());
        float minWidth = Float.MAX_VALUE;
        float maxWidth = 0;
        float minHeight = Float.MAX_VALUE;
        float maxHeight = 0;
        for (int i = 0; i < sizes.size(); i++) {
            SizeF size = sizes.get(i);
            SizeF paddedSize = new SizeF(Math.max(0.f, size.getWidth() - xPaddingDips),
                    Math.max(0.f, size.getHeight() - yPaddingDips));
            paddedSizes.add(paddedSize);
            minWidth = Math.min(minWidth, paddedSize.getWidth());
            maxWidth = Math.max(maxWidth, paddedSize.getWidth());
            minHeight = Math.min(minHeight, paddedSize.getHeight());
            maxHeight = Math.max(maxHeight, paddedSize.getHeight());
        }
        if (paddedSizes.equals(
                widgetManager.getAppWidgetOptions(mAppWidgetId).getParcelableArrayList(
                        AppWidgetManager.OPTION_APPWIDGET_SIZES, SizeF.class))) {
            return;
        }
        Bundle options = newOptions.deepCopy();
        options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, (int) minWidth);
        options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, (int) minHeight);
        options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, (int) maxWidth);
        options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, (int) maxHeight);
        options.putParcelableArrayList(AppWidgetManager.OPTION_APPWIDGET_SIZES, paddedSizes);
        if (widgetDisplayChanges() && getDisplay() != null) {
            options.putInt(AppWidgetManager.OPTION_APPWIDGET_DISPLAY_ID,
                    getDisplay().getDisplayId());
        }
        updateAppWidgetOptions(options);
    }

    /**
     * @hide
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    public void updateAppWidgetSize(Bundle newOptions, int minWidth, int minHeight, int maxWidth,
            int maxHeight, boolean ignorePadding) {
        if (newOptions == null) {
            newOptions = new Bundle();
        }

        Rect padding = getDefaultPadding();
        float density = getResources().getDisplayMetrics().density;

        int xPaddingDips = (int) ((padding.left + padding.right) / density);
        int yPaddingDips = (int) ((padding.top + padding.bottom) / density);

        int newMinWidth = minWidth - (ignorePadding ? 0 : xPaddingDips);
        int newMinHeight = minHeight - (ignorePadding ? 0 : yPaddingDips);
        int newMaxWidth = maxWidth - (ignorePadding ? 0 : xPaddingDips);
        int newMaxHeight = maxHeight - (ignorePadding ? 0 : yPaddingDips);

        AppWidgetManager widgetManager = AppWidgetManager.getInstance(mContext);

        // We get the old options to see if the sizes have changed
        Bundle oldOptions = widgetManager.getAppWidgetOptions(mAppWidgetId);
        boolean needsUpdate = false;
        if (newMinWidth != oldOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH) ||
                newMinHeight != oldOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT) ||
                newMaxWidth != oldOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH) ||
                newMaxHeight != oldOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT)) {
            needsUpdate = true;
        }

        if (needsUpdate) {
            newOptions.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, newMinWidth);
            newOptions.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, newMinHeight);
            newOptions.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, newMaxWidth);
            newOptions.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, newMaxHeight);
            newOptions.putParcelableArrayList(AppWidgetManager.OPTION_APPWIDGET_SIZES,
                    new ArrayList<PointF>());
            updateAppWidgetOptions(newOptions);
        }
    }

    /**
     * Specify some extra information for the widget provider. Causes a callback to the
     * AppWidgetProvider.
     *
     * @param options The bundle of options information.
     * @see AppWidgetProvider#onAppWidgetOptionsChanged(Context, AppWidgetManager, int, Bundle)
     */
    public void updateAppWidgetOptions(Bundle options) {
        AppWidgetManager.getInstance(mContext).updateAppWidgetOptions(mAppWidgetId, options);
    }

    /** @hide **/
    @Override
    public LayoutParams generateLayoutParams(Context inflationContext, AttributeSet attrs) {
        // Widget's layout parameter should be inflated in widget's context
        return new FrameLayout.LayoutParams(inflationContext, attrs);
    }

    /**
     * Sets an executor which can be used for asynchronously inflating. CPU intensive tasks like
     * view inflation or loading images will be performed on the executor. The updates will still
     * be applied on the UI thread.
     *
     * @param executor the executor to use or null.
     */
    public void setExecutor(Executor executor) {
        if (mLastExecutionSignal != null) {
            mLastExecutionSignal.cancel();
            mLastExecutionSignal = null;
        }

        mAsyncExecutor = executor;
    }

    /**
     * Sets whether the widget is being displayed on a light/white background and use an
     * alternate UI if available.
     *
     * @see RemoteViews#setLightBackgroundLayoutId(int)
     */
    public void setOnLightBackground(boolean onLightBackground) {
        mOnLightBackground = onLightBackground;
    }

    /**
     * Update the AppWidgetProviderInfo for this view, and reset it to the
     * initial layout.
     *
     * @hide
     */
    @Override
    public void onUpdateProviderInfo(@Nullable AppWidgetProviderInfo info) {
        setAppWidget(mAppWidgetId, info);
        mViewMode = VIEW_MODE_NOINIT;
        updateAppWidget(null);
    }

    /**
     * Process a set of {@link RemoteViews} coming in as an update from the
     * AppWidget provider. Will animate into these new views as needed
     */
    @Override
    public void updateAppWidget(RemoteViews remoteViews) {
        mLastInflatedRemoteViews = remoteViews;
        applyRemoteViews(remoteViews, true);
    }

    /**
     * Reapply the last inflated remote views, or the default view is none was inflated.
     */
    private void reapplyLastRemoteViews() {
        SparseArray<Parcelable> savedState = new SparseArray<>();
        saveHierarchyState(savedState);
        applyRemoteViews(mLastInflatedRemoteViews, true);
        restoreHierarchyState(savedState);
    }

    /**
     * @hide
     */
    protected void applyRemoteViews(@Nullable RemoteViews remoteViews, boolean useAsyncIfPossible) {
        boolean recycled = false;
        View content = null;
        Exception exception = null;

        // Block state restore until the end of the apply.
        mLastInflatedRemoteViewsId = -1;

        if (mLastExecutionSignal != null) {
            mLastExecutionSignal.cancel();
            mLastExecutionSignal = null;
        }

        if (remoteViews == null) {
            if (mViewMode == VIEW_MODE_DEFAULT) {
                // We've already done this -- nothing to do.
                return;
            }
            content = getDefaultView();
            mViewMode = VIEW_MODE_DEFAULT;
        } else {
            // Select the remote view we are actually going to apply.
            RemoteViews rvToApply = remoteViews.getRemoteViewsToApply(mContext, mCurrentSize);
            if (mOnLightBackground) {
                rvToApply = rvToApply.getDarkTextViews();
            }

            if (mAsyncExecutor != null && useAsyncIfPossible) {
                inflateAsync(rvToApply);
                return;
            }

            if (!mColorMappingChanged && rvToApply.canRecycleView(mView)) {
                try {
                    rvToApply.reapply(mContext, mView, mInteractionLogger, mCurrentSize,
                            mColorResources);
                    content = mView;
                    mLastInflatedRemoteViewsId = rvToApply.computeUniqueId(remoteViews);
                    recycled = true;
                    if (LOGD) Log.d(TAG, "was able to recycle existing layout");
                } catch (RuntimeException e) {
                    exception = e;
                }
            }

            // Try normal RemoteView inflation
            if (content == null) {
                try {
                    content = rvToApply.apply(mContext, this, mInteractionLogger,
                            mCurrentSize, mColorResources);
                    mLastInflatedRemoteViewsId = rvToApply.computeUniqueId(remoteViews);
                    if (LOGD) Log.d(TAG, "had to inflate new layout");
                } catch (RuntimeException e) {
                    exception = e;
                }
            }

            mViewMode = VIEW_MODE_CONTENT;
        }

        applyContent(content, recycled, exception);
    }

    private void applyContent(View content, boolean recycled, Exception exception) {
        mColorMappingChanged = false;
        if (content == null) {
            if (mViewMode == VIEW_MODE_ERROR) {
                // We've already done this -- nothing to do.
                return;
            }
            if (exception != null) {
                Log.w(TAG, "Error inflating RemoteViews", exception);
            }
            content = getErrorView();
            mViewMode = VIEW_MODE_ERROR;
        }

        if (!recycled) {
            prepareView(content);
            addView(content);
        }

        if (mView != content) {
            removeView(mView);
            mView = content;
        }
    }

    private void inflateAsync(@NonNull RemoteViews remoteViews) {
        // Prepare a local reference to the remote Context so we're ready to
        // inflate any requested LayoutParams.
        int layoutId = remoteViews.getLayoutId();

        if (mLastExecutionSignal != null) {
            mLastExecutionSignal.cancel();
        }

        // If our stale view has been prepared to match active, and the new
        // layout matches, try recycling it
        if (!mColorMappingChanged && remoteViews.canRecycleView(mView)) {
            try {
                mLastExecutionSignal = remoteViews.reapplyAsync(mContext,
                        mView,
                        mAsyncExecutor,
                        new ViewApplyListener(remoteViews, layoutId, true),
                        mInteractionLogger,
                        mCurrentSize,
                        mColorResources);
            } catch (Exception e) {
                // Reapply failed. Try apply
            }
        }
        if (mLastExecutionSignal == null) {
            mLastExecutionSignal = remoteViews.applyAsync(mContext,
                    this,
                    mAsyncExecutor,
                    new ViewApplyListener(remoteViews, layoutId, false),
                    mInteractionLogger,
                    mCurrentSize,
                    mColorResources);
        }
    }

    private class ViewApplyListener implements RemoteViews.OnViewAppliedListener {
        private final RemoteViews mViews;
        private final boolean mIsReapply;
        private final int mLayoutId;

        ViewApplyListener(
                RemoteViews views,
                int layoutId,
                boolean isReapply) {
            mViews = views;
            mLayoutId = layoutId;
            mIsReapply = isReapply;
        }

        @Override
        public void onViewApplied(View v) {
            mViewMode = VIEW_MODE_CONTENT;

            applyContent(v, mIsReapply, null);

            mLastInflatedRemoteViewsId = mViews.computeUniqueId(mLastInflatedRemoteViews);
            restoreInstanceState();
            mLastExecutionSignal = null;
        }

        @Override
        public void onError(Exception e) {
            if (mIsReapply) {
                // Try a fresh replay
                mLastExecutionSignal = mViews.applyAsync(mContext,
                        AppWidgetHostView.this,
                        mAsyncExecutor,
                        new ViewApplyListener(mViews, mLayoutId, false),
                        mInteractionLogger,
                        mCurrentSize);
            } else {
                applyContent(null, false, e);
                mLastExecutionSignal = null;
            }
        }
    }

    /**
     * Process data-changed notifications for the specified view in the specified
     * set of {@link RemoteViews} views.
     *
     * @hide
     */
    @Override
    public void onViewDataChanged(int viewId) {
        View v = findViewById(viewId);
        if ((v != null) && (v instanceof AdapterView<?>)) {
            AdapterView<?> adapterView = (AdapterView<?>) v;
            Adapter adapter = adapterView.getAdapter();
            if (adapter instanceof BaseAdapter) {
                BaseAdapter baseAdapter = (BaseAdapter) adapter;
                baseAdapter.notifyDataSetChanged();
            } else if (adapter == null && adapterView instanceof RemoteAdapterConnectionCallback) {
                // If the adapter is null, it may mean that the RemoteViewsAapter has not yet
                // connected to its associated service, and hence the adapter hasn't been set.
                // In this case, we need to defer the notify call until it has been set.
                ((RemoteAdapterConnectionCallback) adapterView).deferNotifyDataSetChanged();
            }
        }
    }

    /**
     * Prepare the given view to be shown. This might include adjusting
     * {@link FrameLayout.LayoutParams} before inserting.
     */
    protected void prepareView(View view) {
        // Take requested dimensions from child, but apply default gravity.
        FrameLayout.LayoutParams requested = (FrameLayout.LayoutParams) view.getLayoutParams();
        if (requested == null) {
            requested = new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT,
                    LayoutParams.MATCH_PARENT);
        }

        requested.gravity = Gravity.CENTER;
        view.setLayoutParams(requested);
    }

    /**
     * Inflate and return the default layout requested by AppWidget provider.
     */
    protected View getDefaultView() {
        if (LOGD) {
            Log.d(TAG, "getDefaultView");
        }
        View defaultView = null;
        Exception exception = null;

        try {
            if (mInfo != null && mInfo.initialLayout != 0) {
                int layoutId = mInfo.initialLayout;
                int kgLayoutId = mInfo.initialKeyguardLayout;
                if (kgLayoutId != 0 && kgLayoutId != layoutId) {
                    // If this this a keyguard widget, use keyguard layout
                    Bundle options = AppWidgetManager.getInstance(mContext)
                            .getAppWidgetOptions(mAppWidgetId);
                    if (options.containsKey(OPTION_APPWIDGET_HOST_CATEGORY)
                            && options.getInt(OPTION_APPWIDGET_HOST_CATEGORY)
                                    == WIDGET_CATEGORY_KEYGUARD) {
                        layoutId = kgLayoutId;
                    }
                }

                defaultView = new RemoteViewsWrapper(mInfo.providerInfo.applicationInfo, layoutId)
                        .apply(mContext, this, mInteractionLogger, null, mColorResources);
                if (!(defaultView instanceof AdapterView)) {
                    // AdapterView does not support onClickListener
                    defaultView.setOnClickListener(this::onDefaultViewClicked);
                }
            } else {
                Log.w(TAG, "can't inflate defaultView, missing defaultLayout, mInfo: " + mInfo);
            }
        } catch (RuntimeException e) {
            exception = e;
        }

        if (exception != null) {
            Log.w(TAG, "Error inflating AppWidget " + mInfo, exception);
        }

        if (defaultView == null) {
            if (LOGD) Log.d(TAG, "getDefaultView couldn't find any view, so inflating error");
            defaultView = getErrorView();
        }

        return defaultView;
    }

    /**
     * Handles interactions on the default view of the widget. By default does not use the
     * {@link InteractionHandler} used by other interactions. However, this can be overridden
     * in order to customize the click behavior.
     *
     * @hide
     */
    protected void onDefaultViewClicked(@NonNull View view) {
        final AppWidgetManager manager = AppWidgetManager.getInstance(mContext);
        if (manager != null) {
            manager.noteAppWidgetTapped(mAppWidgetId);
        }
        if (mInfo != null) {
            LauncherApps launcherApps = getContext().getSystemService(LauncherApps.class);
            List<LauncherActivityInfo> activities = launcherApps.getActivityList(
                    mInfo.provider.getPackageName(), mInfo.getProfile());
            if (!activities.isEmpty()) {
                LauncherActivityInfo ai = activities.get(0);
                launcherApps.startMainActivity(ai.getComponentName(), ai.getUser(),
                        RemoteViews.getSourceBounds(view), null);
            }
        }
    }

    /**
     * Inflate and return a view that represents an error state.
     */
    protected View getErrorView() {
        TextView tv = new TextView(mContext);
        tv.setText(com.android.internal.R.string.gadget_host_error_inflating);
        // TODO: get this color from somewhere.
        tv.setBackgroundColor(Color.argb(127, 0, 0, 0));
        return tv;
    }

    /** @hide */
    @Override
    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
        super.onInitializeAccessibilityNodeInfoInternal(info);
        info.setClassName(AppWidgetHostView.class.getName());
    }

    /** @hide */
    public ActivityOptions createSharedElementActivityOptions(
            int[] sharedViewIds, String[] sharedViewNames, Intent fillInIntent) {
        Context parentContext = getContext();
        while ((parentContext instanceof ContextWrapper)
                && !(parentContext instanceof Activity)) {
            parentContext = ((ContextWrapper) parentContext).getBaseContext();
        }
        if (!(parentContext instanceof Activity)) {
            return null;
        }

        List<Pair<View, String>> sharedElements = new ArrayList<>();
        Bundle extras = new Bundle();

        for (int i = 0; i < sharedViewIds.length; i++) {
            View view = findViewById(sharedViewIds[i]);
            if (view != null) {
                sharedElements.add(Pair.create(view, sharedViewNames[i]));

                extras.putParcelable(sharedViewNames[i], RemoteViews.getSourceBounds(view));
            }
        }

        if (!sharedElements.isEmpty()) {
            fillInIntent.putExtra(RemoteViews.EXTRA_SHARED_ELEMENT_BOUNDS, extras);
            final ActivityOptions opts = ActivityOptions.makeSceneTransitionAnimation(
                    (Activity) parentContext,
                    sharedElements.toArray(new Pair[sharedElements.size()]));
            opts.setPendingIntentLaunchFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            return opts;
        }
        return null;
    }

    /**
     * Set the dynamically overloaded color resources.
     *
     * {@code colorMapping} maps a predefined set of color resources to their ARGB
     * representation. Any entry not in the predefined set of colors will be ignored.
     *
     * Calling this method will trigger a full re-inflation of the App Widget.
     *
     * If flag {@code android.content.res.self_targeting_android_resource_frro} is set, any colors
     * in {@code colorMapping} will be overloaded, otherwise the color resources that can
     * be overloaded are the ones whose name is prefixed with {@code system_neutral} or
     * {@code system_accent}, for example {@link android.R.color#system_neutral1_500}.
     */
    public void setColorResources(@NonNull SparseIntArray colorMapping) {
        if (mColorResources != null
                && isSameColorMapping(mColorResources.getColorMapping(), colorMapping)) {
            return;
        }
        if (selfTargetingAndroidResourceFrro()) {
            setColorResources(
                    RemoteViews.ColorResources.createWithOverlay(mContext, colorMapping));
        } else {
            setColorResources(RemoteViews.ColorResources.create(mContext, colorMapping));
        }
    }

    private void setColorResourcesStates(RemoteViews.ColorResources colorResources) {
        mColorResources = colorResources;
        mColorMappingChanged = true;
        mViewMode = VIEW_MODE_NOINIT;
    }

    /** @hide **/
    public void setColorResources(RemoteViews.ColorResources colorResources) {
        if (colorResources == mColorResources) {
            return;
        }
        setColorResourcesStates(colorResources);
        reapplyLastRemoteViews();
    }

    /**
     * @hide
     */
    public void setColorResourcesNoReapply(RemoteViews.ColorResources colorResources) {
        if (colorResources == mColorResources) {
            return;
        }
        setColorResourcesStates(colorResources);
    }

    /** Check if, in the current context, the two color mappings are equivalent. */
    private boolean isSameColorMapping(SparseIntArray oldColors, SparseIntArray newColors) {
        if (oldColors.size() != newColors.size()) {
            return false;
        }
        for (int i = 0; i < oldColors.size(); i++) {
            if (oldColors.keyAt(i) != newColors.keyAt(i)
                    || oldColors.valueAt(i) != newColors.valueAt(i)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Reset the dynamically overloaded resources, reverting to the default values for
     * all the colors.
     *
     * If colors were defined before, calling this method will trigger a full re-inflation of the
     * App Widget.
     */
    public void resetColorResources() {
        if (mColorResources != null) {
            mColorResources = null;
            mColorMappingChanged = true;
            mViewMode = VIEW_MODE_NOINIT;
            reapplyLastRemoteViews();
        }
    }

    @Override
    protected void dispatchDraw(@NonNull Canvas canvas) {
        try {
            super.dispatchDraw(canvas);
        } catch (Exception e) {
            // Catch draw exceptions that may be caused by RemoteViews
            Log.e(TAG, "Drawing view failed: " + e);
            post(this::handleViewError);
        }
    }

    @Override
    public void onVisibilityAggregated(boolean isVisible) {
        super.onVisibilityAggregated(isVisible);
        if (isVisibilityTrackingPermitted()) {
            mInteractionLogger.onVisibilityAggregated();
        }
    }

    /**
     * Start visibility tracking for this widget. This should be called when this view has become
     * visible on screen to the user. This view will mark the start of a duration of visibility.
     */
    @FlaggedApi(FLAG_ENGAGEMENT_METRICS)
    public void startVisibilityTracking()  {
        mInteractionLogger.onTrackingChanged(true);
    }

    /**
     * Stop visibility tracking for this widget. This should be called when this view is no longer
     * visible on screen to the user. This view will mark the end of a duration of visibility and
     * add it to the total visibility duration tracked by this view.
     *
     * <p>Once the {@link AppWidgetHost} stops listening or this widget is deleted, the duration of
     * visibility will be recorded into an {@link AppWidgetEvent} and reported to the widget
     * service.
     */
    @FlaggedApi(FLAG_ENGAGEMENT_METRICS)
    public void stopVisibilityTracking() {
        mInteractionLogger.onTrackingChanged(false);
    }

    /**
     * Override this to return false for AppWidgetHostViews that should never allow visibility
     * tracking.
     *
     * @hide
     */
    protected boolean isVisibilityTrackingPermitted() {
        return true;
    }

    /**
     * This function returns the current set of widget event data being tracked by this widget. The
     * tracked data is cleared is returned here.
     *
     * @hide
     */
    @FlaggedApi(FLAG_ENGAGEMENT_METRICS)
    @Override
    public AppWidgetEvent collectWidgetEvent() {
        return mInteractionLogger.collectWidgetEvent();
    }

    /**
     * This class is used to track user interactions with this widget.
     * @hide
     */
    public class InteractionLogger implements RemoteViews.InteractionHandler {
        @NonNull
        private final AppWidgetEvent.Builder mEvent = new AppWidgetEvent.Builder();
        @Nullable
        private RemoteViews.InteractionHandler mInteractionHandler = null;
        private boolean mIsVisible = false;
        private boolean mIsTracking = false;

        InteractionLogger() {
        }

        InteractionLogger(@Nullable InteractionHandler handler) {
            mInteractionHandler = handler;
        }

        /**
         * Return the current AppWidgetEvent without clearing the tracked data.
         */
        @VisibleForTesting
        public AppWidgetEvent getEvent() {
            synchronized (this) {
                return mEvent.build();
            }
        }

        @Override
        public boolean onInteraction(View view, PendingIntent pendingIntent,
                RemoteViews.RemoteResponse response) {
            if (engagementMetrics()) {
                synchronized (this) {
                    mEvent.addClickedId(getMetricsId(view));
                }
            }
            AppWidgetManager manager = AppWidgetManager.getInstance(mContext);
            if (manager != null) {
                manager.noteAppWidgetTapped(mAppWidgetId);
            }

            if (mInteractionHandler != null) {
                return mInteractionHandler.onInteraction(view, pendingIntent, response);
            } else {
                return RemoteViews.startPendingIntent(view, pendingIntent,
                        response.getLaunchOptions(view));
            }
        }

        @Override
        public void onScroll(@NonNull AbsListView view) {
            if (!engagementMetrics()) return;

            synchronized (this) {
                mEvent.addScrolledId(getMetricsId(view));
            }
            if (mInteractionHandler != null) {
                mInteractionHandler.onScroll(view);
            }
        }

        @FlaggedApi(FLAG_ENGAGEMENT_METRICS)
        private int getMetricsId(@NonNull View view) {
            Object metricsTag = view.getTag(com.android.internal.R.id.remoteViewsMetricsId);
            if (metricsTag instanceof Integer tag) {
                return tag;
            } else {
                return view.getId();
            }
        }

        /**
         * Invoked when the root view is resized or moved.
         */
        private void onPositionChanged() {
            if (!engagementMetrics()) return;
            Rect position = new Rect();
            if (getGlobalVisibleRect(position)) {
                applyScrollOffset(position);
                synchronized (this) {
                    mEvent.setPosition(position);
                }
            }
        }

        /**
         * Finds the first parent with a scrollX or scrollY offset and applies it to the current
         * position Rect. This corresponds to the current "page" of this widget on its workspace.
         */
        private void applyScrollOffset(@Nullable Rect position) {
            if (position == null) return;
            int dx = 0;
            int dy = 0;
            for (ViewParent parent = getParent(); parent != null; parent = parent.getParent()) {
                if (parent instanceof View view && (view.getScrollX() != 0
                        || view.getScrollY() != 0)) {
                    dx = view.getScrollX();
                    dy = view.getScrollY();
                    break;
                }
            }
            position.offset(dx, dy);
        }

        private void onWindowFocusChanged() {
            if (!engagementMetrics()) return;
            synchronized (this) {
                updateVisibilityLocked(mIsTracking);
            }
        }

        private void onVisibilityAggregated() {
            if (!engagementMetrics()) return;
            synchronized (this) {
                updateVisibilityLocked(mIsTracking);
            }
        }

        private void onTrackingChanged(boolean isTracking) {
            if (!engagementMetrics()) return;
            synchronized (this) {
                mIsTracking = isTracking;
                updateVisibilityLocked(mIsTracking);
            }
        }

        /**
         * Check if this view is currently visible, and update the duration if an impression has
         * finished.
         */
        private void updateVisibilityLocked(boolean isTracking) {
            boolean wasVisible = mIsVisible;
            boolean isVisible = isTracking && hasWindowFocus() && isVisibleToUser();
            if (!wasVisible && isVisible) {
                // View has become visible, start the tracker.
                mEvent.startVisibility();
                if (LOGD) Log.d(TAG, logName() + " became visible");
            } else if (wasVisible && !isVisible) {
                // View is no longer visible, add duration.
                mEvent.endVisibility();
                if (LOGD) Log.d(TAG, logName() + " lost visibility");
            }

            mIsVisible = isVisible;
        }

        @Nullable
        private AppWidgetEvent collectWidgetEvent() {
            if (!engagementMetrics()) return null;

            synchronized (this) {
                if (mIsVisible) {
                    // If the widget is currently visible, add the current duration to the event
                    // data.
                    updateVisibilityLocked(false);
                }
                mEvent.setAppWidgetId(mAppWidgetId);
                if (mEvent.isEmpty()) {
                    if (LOGD) Log.d(TAG, "Skipping event for " + logName() + ", no event data");
                    return null;
                }
                AppWidgetEvent event = mEvent.build();
                mEvent.clear();
                if (LOGD) Log.d(TAG, "Returning event for " + logName() + ", " + event);
                return event;
            }
        }

        private String logName() {
            return (mInfo == null ? "null" : mInfo.provider.getPackageName()) + "(" + mAppWidgetId
                + ")";
        }
    }

    private static class RemoteViewsWrapper extends RemoteViews {

        RemoteViewsWrapper(ApplicationInfo application, int layoutId) {
            super(application, layoutId);
        }
    }

}

