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

Commit 78d7940e authored by Willie Koomson's avatar Willie Koomson Committed by Android (Google) Code Review
Browse files

Merge "Refactor widget event data to use dedicated class" into main

parents 700bdc9a 997eab09
Loading
Loading
Loading
Loading
+19 −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.appwidget;

parcelable AppWidgetEvent;
+285 −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.appwidget;

import static android.appwidget.flags.Flags.FLAG_ENGAGEMENT_METRICS;

import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.usage.UsageStatsManager;
import android.graphics.Rect;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.PersistableBundle;
import android.text.TextUtils;
import android.util.ArraySet;

import java.util.Arrays;

/**
 * An immutable class that describes the event data for an app widget interaction event.
 *
 * @hide
 */
@FlaggedApi(FLAG_ENGAGEMENT_METRICS)
public class AppWidgetEvent implements Parcelable {
    /**
     * Max number of clicked and scrolled IDs stored per event.
     */
    public static final int MAX_NUM_ITEMS = 10;

    private final int mAppWidgetId;
    private final long mDurationMs;
    @Nullable
    private final Rect mPosition;
    @Nullable
    private final int[] mClickedIds;
    @Nullable
    private final int[] mScrolledIds;

    /**
     * The app widget ID of the widget that generated this event.
     */
    public int getAppWidgetId() {
        return mAppWidgetId;
    }

    /**
     * This contains a long that represents the duration of time in milliseconds during which the
     * widget was visible.
     */
    public long getDurationMs() {
        return mDurationMs;
    }

    /**
     * This rect with describes the global coordinates of the widget at the end of the interaction
     * event.
     */
    @Nullable
    public Rect getPosition() {
        return mPosition;
    }

    /**
     * This describes which views have been clicked during a single impression of the widget.
     */
    @Nullable
    public int[] getClickedIds() {
        return mClickedIds;
    }

    /**
     * This describes which views have been scrolled during a single impression of the widget.
     */
    @Nullable
    public int[] getScrolledIds() {
        return mScrolledIds;
    }

    private AppWidgetEvent(int appWidgetId, long durationMs,
            @Nullable Rect position, @Nullable int[] clickedIds,
            @Nullable int[] scrolledIds) {
        mAppWidgetId = appWidgetId;
        mDurationMs = durationMs;
        mPosition = position;
        mClickedIds = clickedIds;
        mScrolledIds = scrolledIds;
    }

    /**
     * Unflatten the AppWidgetEvent from a parcel.
     */
    private AppWidgetEvent(Parcel in) {
        mAppWidgetId = in.readInt();
        mDurationMs = in.readLong();
        mPosition = in.readTypedObject(Rect.CREATOR);
        mClickedIds = in.createIntArray();
        mScrolledIds = in.createIntArray();
    }

    @Override
    public void writeToParcel(Parcel out, int flags) {
        out.writeInt(mAppWidgetId);
        out.writeLong(mDurationMs);
        out.writeTypedObject(mPosition, flags);
        out.writeIntArray(mClickedIds);
        out.writeIntArray(mScrolledIds);
    }

    /**
     * Parcelable.Creator that instantiates AppWidgetEvent objects
     */
    public static final @android.annotation.NonNull Parcelable.Creator<AppWidgetEvent> CREATOR =
            new Parcelable.Creator<>() {
                public AppWidgetEvent createFromParcel(Parcel parcel) {
                    return new AppWidgetEvent(parcel);
                }

                public AppWidgetEvent[] newArray(int size) {
                    return new AppWidgetEvent[size];
                }
            };

    @Override
    public int describeContents() {
        return 0;
    }

    /**
     * Create a PersistableBundle that represents this event.
     */
    @NonNull
    public PersistableBundle toBundle() {
        PersistableBundle extras = new PersistableBundle();
        extras.putString(UsageStatsManager.EXTRA_EVENT_ACTION,
                AppWidgetManager.EVENT_TYPE_WIDGET_INTERACTION);
        extras.putString(UsageStatsManager.EXTRA_EVENT_CATEGORY,
                AppWidgetManager.EVENT_CATEGORY_APPWIDGET);
        extras.putInt(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
        extras.putLong(AppWidgetManager.EXTRA_EVENT_DURATION_MS, mDurationMs);
        if (mPosition != null) {
            extras.putIntArray(AppWidgetManager.EXTRA_EVENT_POSITION_RECT,
                new int[]{mPosition.left, mPosition.top, mPosition.right, mPosition.bottom});
        }
        if (mClickedIds != null && mClickedIds.length > 0) {
            extras.putIntArray(AppWidgetManager.EXTRA_EVENT_CLICKED_VIEWS, mClickedIds);
        }
        if (mScrolledIds != null && mScrolledIds.length > 0) {
            extras.putIntArray(AppWidgetManager.EXTRA_EVENT_SCROLLED_VIEWS, mScrolledIds);
        }
        return extras;
    }

    @Override
    public String toString() {
        return TextUtils.formatSimple("AppWidgetEvent(appWidgetId=%d, durationMs=%d, position=%s,"
                + " clickedIds=%s, scrolledIds=%s)", mAppWidgetId, mDurationMs, mPosition,
            Arrays.toString(mClickedIds), Arrays.toString(mScrolledIds));
    }

    /**
     * Builder class to construct AppWidgetEvent objects.
     *
     * @hide
     */
    public static class Builder {
        @NonNull
        private final ArraySet<Integer> mClickedIds = new ArraySet<>(MAX_NUM_ITEMS);
        @NonNull
        private final ArraySet<Integer> mScrolledIds = new ArraySet<>(MAX_NUM_ITEMS);
        private int mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
        private long mDurationMs = 0;
        @Nullable
        private Rect mPosition = null;

        public Builder() {
        }

        public Builder setAppWidgetId(int appWidgetId) {
            mAppWidgetId = appWidgetId;
            return this;
        }

        public Builder addDurationMs(long durationMs) {
            mDurationMs += durationMs;
            return this;
        }

        public Builder setPosition(@Nullable Rect position) {
            mPosition = position;
            return this;
        }

        public Builder addClickedId(int id) {
            if (mClickedIds.size() < MAX_NUM_ITEMS) {
                mClickedIds.add(id);
            }
            return this;
        }

        public Builder addScrolledId(int id) {
            if (mScrolledIds.size() < MAX_NUM_ITEMS) {
                mScrolledIds.add(id);
            }
            return this;
        }

        /**
         * Merge the given event's data into this event's data.
         */
        public void merge(@Nullable AppWidgetEvent event) {
            if (event == null) {
                return;
            }

            if (mAppWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
                setAppWidgetId(event.getAppWidgetId());
            } else if (mAppWidgetId != event.getAppWidgetId()) {
                throw new IllegalArgumentException("Trying to merge events with different app "
                    + "widget IDs: " + mAppWidgetId + " != " + event.getAppWidgetId());
            }
            addDurationMs(event.getDurationMs());
            setPosition(event.getPosition());
            addAllUntilMax(mClickedIds, event.getClickedIds());
            addAllUntilMax(mScrolledIds, event.getScrolledIds());
        }

        /**
         * Returns true if the app widget ID has not been set, or if no data has been added to this
         * event yet.
         */
        public boolean isEmpty() {
            return mAppWidgetId <= 0 || mDurationMs == 0L;
        }

        /**
         * Resets the event data fields.
         */
        public void clear() {
            mDurationMs = 0;
            mPosition = null;
            mClickedIds.clear();
            mScrolledIds.clear();
        }

        public AppWidgetEvent build() {
            return new AppWidgetEvent(mAppWidgetId, mDurationMs, mPosition, toIntArray(mClickedIds),
                toIntArray(mScrolledIds));
        }

        private static void addAllUntilMax(@NonNull ArraySet<Integer> set, @Nullable int[] toAdd) {
            if (toAdd == null) {
                return;
            }
            for (int i = 0; i < toAdd.length && set.size() < MAX_NUM_ITEMS; i++) {
                set.add(toAdd[i]);
            }
        }

        @Nullable
        private static int[] toIntArray(@NonNull ArraySet<Integer> set) {
            if (set.isEmpty()) return null;
            int[] array = new int[set.size()];
            for (int i = 0; i < array.length; i++) {
                array[i] = set.valueAt(i);
            }
            return array;
        }
    }
}
+14 −9
Original line number Diff line number Diff line
@@ -16,6 +16,10 @@

package android.appwidget;

import static android.appwidget.flags.Flags.FLAG_ENGAGEMENT_METRICS;
import static android.appwidget.flags.Flags.engagementMetrics;

import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Activity;
@@ -31,7 +35,6 @@ import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.PersistableBundle;
import android.os.Process;
import android.os.RemoteException;
import android.os.ServiceManager;
@@ -47,6 +50,7 @@ import com.android.internal.appwidget.IAppWidgetService;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
@@ -564,8 +568,9 @@ public class AppWidgetHost {
         *
         * @hide
         */
        @FlaggedApi(FLAG_ENGAGEMENT_METRICS)
        @Nullable
        default PersistableBundle collectWidgetEvent() {
        default AppWidgetEvent collectWidgetEvent() {
            return null;
        }
    }
@@ -663,14 +668,14 @@ public class AppWidgetHost {
     * @hide
     */
    public void reportAllWidgetEvents() {
        if (sService == null) {
        if (sService == null || !engagementMetrics()) {
            return;
        }

        List<PersistableBundle> eventList = new ArrayList<>();
        List<AppWidgetEvent> eventList = new ArrayList<>();
        synchronized (mListeners) {
            for (int i = 0; i < mListeners.size(); i++) {
                PersistableBundle event = mListeners.valueAt(i).collectWidgetEvent();
                AppWidgetEvent event = mListeners.valueAt(i).collectWidgetEvent();
                if (event != null) {
                    eventList.add(event);
                }
@@ -679,7 +684,7 @@ public class AppWidgetHost {
        if (eventList.isEmpty()) {
            return;
        }
        PersistableBundle[] events = new PersistableBundle[eventList.size()];
        AppWidgetEvent[] events = new AppWidgetEvent[eventList.size()];
        for (int i = 0; i < events.length; i++) {
            events[i] = eventList.get(i);
        }
@@ -697,18 +702,18 @@ public class AppWidgetHost {
     * @hide
     */
    public void reportEventForWidget(int appWidgetId) {
        if (sService == null) {
        if (sService == null || !engagementMetrics()) {
            return;
        }
        AppWidgetHostListener listener = getListener(appWidgetId);
        if (listener == null) {
            return;
        }
        PersistableBundle event = listener.collectWidgetEvent();
        AppWidgetEvent event = listener.collectWidgetEvent();
        if (event == null) {
            return;
        }
        PersistableBundle[] events = {event};
        AppWidgetEvent[] events = {event};

        try {
            sService.reportWidgetEvents(mContextOpPackageName, events);
+27 −68
Original line number Diff line number Diff line
@@ -43,9 +43,7 @@ import android.os.Build;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.Parcelable;
import android.os.PersistableBundle;
import android.os.SystemClock;
import android.util.ArraySet;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Pair;
@@ -71,7 +69,6 @@ import com.android.internal.annotations.VisibleForTesting;

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

/**
@@ -1048,8 +1045,9 @@ public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppW
     *
     * @hide
     */
    @FlaggedApi(FLAG_ENGAGEMENT_METRICS)
    @Override
    public PersistableBundle collectWidgetEvent() {
    public AppWidgetEvent collectWidgetEvent() {
        return mInteractionLogger.collectWidgetEvent();
    }

@@ -1058,23 +1056,13 @@ public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppW
     * @hide
     */
    public class InteractionLogger implements RemoteViews.InteractionHandler {
        // Max number of clicked and scrolled IDs stored per impression.
        public static final int MAX_NUM_ITEMS = 10;
        // Determines the minimum time between calls to updateVisibility().
        private static final long UPDATE_VISIBILITY_DELAY_MS = 1000L;
        // Clicked views
        @NonNull
        private final ArraySet<Integer> mClickedIds = new ArraySet<>(MAX_NUM_ITEMS);
        // Scrolled views
        @NonNull
        private final ArraySet<Integer> mScrolledIds = new ArraySet<>(MAX_NUM_ITEMS);
        private final AppWidgetEvent.Builder mEvent = new AppWidgetEvent.Builder();
        @Nullable
        private RemoteViews.InteractionHandler mInteractionHandler = null;
        // Last position this widget was laid out in
        @Nullable
        private Rect mPosition = null;
        // Total duration for the impression
        private long mDurationMs = 0L;
        // Holds event data since last report.
        // Last time the widget became visible in SystemClock.uptimeMillis()
        private long mVisibilityChangeMs = 0L;
        private boolean mIsVisible = false;
@@ -1087,34 +1075,19 @@ public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppW
            mInteractionHandler = handler;
        }

        /**
         * Return the current AppWidgetEvent without clearing the tracked data.
         */
        @VisibleForTesting
        @NonNull
        public Set<Integer> getClickedIds() {
            return mClickedIds;
        }

        @VisibleForTesting
        @NonNull
        public Set<Integer> getScrolledIds() {
            return mScrolledIds;
        }

        @VisibleForTesting
        public long getDurationMs() {
            return mDurationMs;
        }

        @VisibleForTesting
        @Nullable
        public Rect getPosition() {
            return mPosition;
        public AppWidgetEvent getEvent() {
            return mEvent.build();
        }

        @Override
        public boolean onInteraction(View view, PendingIntent pendingIntent,
                RemoteViews.RemoteResponse response) {
            if (engagementMetrics() && mClickedIds.size() < MAX_NUM_ITEMS) {
                mClickedIds.add(getMetricsId(view));
            if (engagementMetrics()) {
                mEvent.addClickedId(getMetricsId(view));
            }
            AppWidgetManager manager = AppWidgetManager.getInstance(mContext);
            if (manager != null) {
@@ -1133,10 +1106,7 @@ public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppW
        public void onScroll(@NonNull AbsListView view) {
            if (!engagementMetrics()) return;

            if (mScrolledIds.size() < MAX_NUM_ITEMS) {
                mScrolledIds.add(getMetricsId(view));
            }

            mEvent.addScrolledId(getMetricsId(view));
            if (mInteractionHandler != null) {
                mInteractionHandler.onScroll(view);
            }
@@ -1157,9 +1127,10 @@ public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppW
         */
        private void onPositionChanged() {
            if (!engagementMetrics()) return;
            mPosition = new Rect();
            if (getGlobalVisibleRect(mPosition)) {
                applyScrollOffset();
            Rect position = new Rect();
            if (getGlobalVisibleRect(position)) {
                applyScrollOffset(position);
                mEvent.setPosition(position);
            }
        }

@@ -1167,8 +1138,8 @@ public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppW
         * 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() {
            if (mPosition == null) return;
        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()) {
@@ -1179,7 +1150,7 @@ public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppW
                    break;
                }
            }
            mPosition.offset(dx, dy);
            position.offset(dx, dy);
        }

        private void onDraw() {
@@ -1230,7 +1201,7 @@ public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppW
                mVisibilityChangeMs = SystemClock.uptimeMillis();
            } else if (wasVisible && !isVisible) {
                // View is no longer visible, add duration.
                mDurationMs += SystemClock.uptimeMillis() - mVisibilityChangeMs;
                mEvent.addDurationMs(SystemClock.uptimeMillis() - mVisibilityChangeMs);
            }

            mIsVisible = isVisible;
@@ -1243,33 +1214,21 @@ public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppW
        }

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

            if (mIsVisible) {
                // If the widget is currently visible, add the current duration to the event data.
                updateVisibility(false);
            }
            if (mAppWidgetId <= 0 || mDurationMs == 0L) {
            mEvent.setAppWidgetId(mAppWidgetId);
            if (mEvent.isEmpty()) {
                return null;
            }
            PersistableBundle event = AppWidgetManager.createWidgetInteractionEvent(mAppWidgetId,
                    mDurationMs, mPosition, toIntArray(mClickedIds), toIntArray(mScrolledIds));

            mClickedIds.clear();
            mScrolledIds.clear();
            mDurationMs = 0;
            mPosition = null;

            AppWidgetEvent event = mEvent.build();
            mEvent.clear();
            return event;
        }

        private static int[] toIntArray(ArraySet<Integer> set) {
            if (set.isEmpty()) return null;
            int[] array = new int[set.size()];
            for (int i = 0; i < array.length; i++) {
                array[i] = set.valueAt(i);
            }
            return array;
        }
    }
}
+0 −35
Original line number Diff line number Diff line
@@ -43,7 +43,6 @@ import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.content.pm.ParceledListSlice;
import android.content.pm.ShortcutInfo;
import android.graphics.Rect;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
@@ -52,7 +51,6 @@ import android.os.HandlerExecutor;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.PersistableBundle;
import android.os.Process;
import android.os.RemoteException;
import android.os.UserHandle;
@@ -1593,39 +1591,6 @@ public class AppWidgetManager {
        }
    }

    /**
     * Create a {@link PersistableBundle} that represents a single widget interaction event.
     *
     * @param appWidgetId App Widget ID of the widget.
     * @param durationMs Duration of the impression in milliseconds
     * @param position Current position of the widget.
     * @param clickedIds IDs of views clicked during this event.
     * @param scrolledIds IDs of views scrolled during this event.
     *
     * @hide
     */
    @FlaggedApi(Flags.FLAG_ENGAGEMENT_METRICS)
    @NonNull
    public static PersistableBundle createWidgetInteractionEvent(int appWidgetId, long durationMs,
            @Nullable Rect position, @Nullable int[] clickedIds, @Nullable int[] scrolledIds) {
        PersistableBundle extras = new PersistableBundle();
        extras.putString(UsageStatsManager.EXTRA_EVENT_ACTION, EVENT_TYPE_WIDGET_INTERACTION);
        extras.putString(UsageStatsManager.EXTRA_EVENT_CATEGORY, EVENT_CATEGORY_APPWIDGET);
        extras.putInt(EXTRA_APPWIDGET_ID, appWidgetId);
        extras.putLong(EXTRA_EVENT_DURATION_MS, durationMs);
        if (position != null) {
            extras.putIntArray(EXTRA_EVENT_POSITION_RECT,
                    new int[]{position.left, position.top, position.right, position.bottom});
        }
        if (clickedIds != null && clickedIds.length > 0) {
            extras.putIntArray(EXTRA_EVENT_CLICKED_VIEWS, clickedIds);
        }
        if (scrolledIds != null && scrolledIds.length > 0) {
            extras.putIntArray(EXTRA_EVENT_SCROLLED_VIEWS, scrolledIds);
        }
        return extras;
    }

    @UiThread
    private static @NonNull Executor createUpdateExecutorIfNull() {
        if (sUpdateExecutor == null) {
Loading