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

Commit 86af4462 authored by Willie Koomson's avatar Willie Koomson
Browse files

Log impression and resize events

This change adds functionality to InteractionLogger for logging
impression and resize events.

Bug: 364655296
Test: AppWidgetEventsTest.interactionLogger_{position,impression}
Flag: android.appwidget.flags.engagement_metrics
Change-Id: Iaf55b3132225ca5879f1ab4fb492d2d86a983508
parent 4f871ab5
Loading
Loading
Loading
Loading
+127 −3
Original line number Diff line number Diff line
@@ -43,6 +43,7 @@ import android.os.Build;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.Parcelable;
import android.os.SystemClock;
import android.util.ArraySet;
import android.util.AttributeSet;
import android.util.Log;
@@ -53,6 +54,7 @@ import android.util.SparseIntArray;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewParent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.AbsListView;
import android.widget.Adapter;
@@ -351,6 +353,9 @@ public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppW
                            0 /* heightUsed */);
                }
            }
            if (changed) {
                post(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);
@@ -358,6 +363,12 @@ public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppW
        }
    }

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

    /**
     * Remove bad view and replace with error message view
     */
@@ -1022,6 +1033,7 @@ public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppW
    protected void dispatchDraw(@NonNull Canvas canvas) {
        try {
            super.dispatchDraw(canvas);
            mInteractionLogger.onDraw();
        } catch (Exception e) {
            // Catch draw exceptions that may be caused by RemoteViews
            Log.e(TAG, "Drawing view failed: " + e);
@@ -1036,6 +1048,8 @@ public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppW
    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 Set<Integer> mClickedIds = new ArraySet<>(MAX_NUM_ITEMS);
@@ -1044,6 +1058,15 @@ public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppW
        private final Set<Integer> mScrolledIds = new ArraySet<>(MAX_NUM_ITEMS);
        @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;
        // Last time the widget became visible in SystemClock.uptimeMillis()
        private long mVisibilityChangeMs = 0L;
        private boolean mIsVisible = false;
        private boolean mUpdateVisibilityScheduled = false;

        InteractionLogger() {
        }
@@ -1064,6 +1087,17 @@ public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppW
            return mScrolledIds;
        }

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

        @VisibleForTesting
        @Nullable
        public Rect getPosition() {
            return mPosition;
        }

        @Override
        public boolean onInteraction(View view, PendingIntent pendingIntent,
                RemoteViews.RemoteResponse response) {
@@ -1098,12 +1132,102 @@ public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppW

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

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

        /**
         * 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;
            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;
                }
            }
            mPosition.offset(dx, dy);
        }
            return viewId;

        private void onDraw() {
            if (!engagementMetrics()) return;
            if (getParent() instanceof View view && view.isDirty()) {
                scheduleUpdateVisibility();
            }
        }

        private void onWindowFocusChanged(boolean hasWindowFocus) {
            if (!engagementMetrics()) return;
            updateVisibility(hasWindowFocus);
        }

        /**
         * Schedule a delayed call to updateVisibility. Will skip if a call is already scheduled.
         */
        private void scheduleUpdateVisibility() {
            if (mUpdateVisibilityScheduled) {
                return;
            }

            postDelayed(() -> updateVisibility(hasWindowFocus()), UPDATE_VISIBILITY_DELAY_MS);
            mUpdateVisibilityScheduled = true;
        }

        /**
         * Check if this view is currently visible, and update the duration if an impression has
         * finished.
         */
        private void updateVisibility(boolean hasWindowFocus) {
            boolean wasVisible = mIsVisible;
            boolean isVisible = hasWindowFocus && testVisibility(AppWidgetHostView.this);
            if (isVisible) {
                // Test parent visibility.
                for (ViewParent parent = getParent(); parent != null && isVisible;
                        parent = parent.getParent()) {
                    if (parent instanceof View view) {
                        isVisible = testVisibility(view);
                    } else {
                        break;
                    }
                }
            }

            if (!wasVisible && isVisible) {
                // View has become visible, start the tracker.
                mVisibilityChangeMs = SystemClock.uptimeMillis();
            } else if (wasVisible && !isVisible) {
                // View is no longer visible, add duration.
                mDurationMs += SystemClock.uptimeMillis() - mVisibilityChangeMs;
            }

            mIsVisible = isVisible;
            mUpdateVisibilityScheduled = false;
        }

        private boolean testVisibility(View view) {
            return view.isAggregatedVisible() && view.getGlobalVisibleRect(new Rect())
                    && view.getAlpha() != 0;
        }
    }
}
+44 −0
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package android.appwidget

import android.app.Activity
import android.app.EmptyActivity
import android.app.PendingIntent
import android.appwidget.AppWidgetHostView.InteractionLogger.MAX_NUM_ITEMS
import android.content.Intent
@@ -23,10 +25,12 @@ import android.graphics.Rect
import android.view.View
import android.widget.ListView
import android.widget.RemoteViews
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.android.frameworks.coretests.R
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.CountDownLatch
import org.junit.Test
import org.junit.runner.RunWith

@@ -186,4 +190,44 @@ class AppWidgetEventsTest {
        assertThat(hostView.interactionLogger.scrolledIds)
            .containsExactlyElementsIn(0..itemCount.minus(2))
    }

    @Test
    fun interactionLogger_impression() {
        val remoteViews = RemoteViews(context.packageName, R.layout.remote_views_test)
        hostView.updateAppWidget(remoteViews)
        assertThat(hostView.interactionLogger.durationMs).isEqualTo(0)

        ActivityScenario<Activity>.launch(EmptyActivity::class.java).use { scenario ->
            scenario.onActivity { activity ->
                activity.setContentView(hostView)
                hostView.layout(0, 0, 500, 500)
                hostView.dispatchWindowFocusChanged(true)
            }
            Thread.sleep(2000L)
            hostView.dispatchWindowFocusChanged(false)
            assertThat(hostView.interactionLogger.durationMs).isGreaterThan(2000L)
        }
    }

    @Test
    fun interactionLogger_position() {
        val remoteViews = RemoteViews(context.packageName, R.layout.remote_views_test)
        hostView.updateAppWidget(remoteViews)
        assertThat(hostView.interactionLogger.position).isNull()

        ActivityScenario<Activity>.launch(EmptyActivity::class.java).use { scenario ->
            val latch = CountDownLatch(1)
            scenario.onActivity { activity ->
                activity.setContentView(hostView)
                hostView.layout(0, 0, 500, 500)
                hostView.post {
                    val rect = Rect()
                    assertThat(hostView.getGlobalVisibleRect(rect)).isTrue()
                    assertThat(hostView.interactionLogger.position).isEqualTo(rect)
                    latch.countDown()
                }
            }
            latch.await()
        }
    }
}