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

Commit cfb1ee9e authored by Willie Koomson's avatar Willie Koomson
Browse files

Write tokenized user interaction extras to usage stats files

Adds support in IntervalStats obfuscation for writing arbitrary user
interaction extras. The bundle keys are tokenized, and then the bundle
is flattened to string and stored in the proto.

Bug: 440630000
Test: manual, checking widget event extras after a reboot
Test: FrameworksServicesTests:IntervalStatsTest
Flag: android.appwidget.flags.engagement_metrics
Change-Id: Id853bd5a7812fdefaa7e16dec8d2493edfd577cb
parent 48df9172
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -15,6 +15,8 @@
 */
package android.app.usage;

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

import android.annotation.CurrentTimeMillisLong;
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
@@ -559,6 +561,8 @@ public final class UsageEvents implements Parcelable {
        public static class UserInteractionEventExtrasToken {
            public int mCategoryToken = UNASSIGNED_TOKEN;
            public int mActionToken = UNASSIGNED_TOKEN;
            @FlaggedApi(FLAG_ENGAGEMENT_METRICS)
            public byte[] mTokenizedExtras = null;

            public UserInteractionEventExtrasToken() {
                // Do nothing.
+1 −0
Original line number Diff line number Diff line
@@ -154,4 +154,5 @@ message ObfuscatedPackagesProto {
message ObfuscatedUserInteractionExtrasProto {
  optional int32 category_token = 1;
  optional int32 action_token = 2;
  optional bytes tokenized_extras = 3;
}
+101 −2
Original line number Diff line number Diff line
@@ -16,6 +16,10 @@
package com.android.server.usage;

import static android.app.usage.UsageEvents.Event.MAX_EVENT_TYPE;
import static android.app.usage.UsageEvents.Event.USER_INTERACTION;
import static android.appwidget.flags.Flags.FLAG_ENGAGEMENT_METRICS;

import static com.android.server.usage.PackagesTokenData.UNASSIGNED_TOKEN;

import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
@@ -25,24 +29,36 @@ import android.app.usage.UsageEvents;
import android.app.usage.UsageStatsManager;
import android.content.res.Configuration;
import android.os.PersistableBundle;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;

import com.android.internal.util.ArrayUtils;

import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.io.ByteArrayInputStream;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Locale;

@RunWith(AndroidJUnit4.class)
@SmallTest
public final class IntervalStatsTests {
    @Rule
    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();

    private static final int NUMBER_OF_PACKAGES = 7;
    private static final int NUMBER_OF_EVENTS_PER_PACKAGE = 200;
    private static final int NUMBER_OF_EVENTS = NUMBER_OF_PACKAGES * NUMBER_OF_EVENTS_PER_PACKAGE;
    private static final String EXTRA_KEY_INT_ARRAY = "com.example.extra.int_array";
    private static final String EXTRA_KEY_STRING = "com.example.extra.string";
    private static final String EXTRA_KEY_BUNDLE = "com.example.extra.bundle";

    private void populateIntervalStats(IntervalStats intervalStats) {
        final int timeProgression = 23;
@@ -101,11 +117,17 @@ public final class IntervalStatsTests {
                case UsageEvents.Event.USER_INTERACTION:
                    if (Flags.userInteractionTypeApi()) {
                        // "random" user interaction extras.
                        PersistableBundle extras = new PersistableBundle();
                        final PersistableBundle extras = new PersistableBundle();
                        extras.putString(UsageStatsManager.EXTRA_EVENT_CATEGORY,
                                "fake.namespace.category" + (i % 13));
                        extras.putString(UsageStatsManager.EXTRA_EVENT_ACTION,
                                "fakeaction" + (i % 13));
                        extras.putIntArray(EXTRA_KEY_INT_ARRAY,
                            new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10});
                        final char[] longString = new char[256];
                        Arrays.fill(longString, 'a');
                        extras.putString(EXTRA_KEY_STRING, new String(longString));
                        extras.putPersistableBundle(EXTRA_KEY_BUNDLE, new PersistableBundle());
                        event.mExtras = extras;
                    }
                    break;
@@ -167,6 +189,82 @@ public final class IntervalStatsTests {
        assertThat(intervalStats.packageStats.size()).isEqualTo(NUMBER_OF_PACKAGES);
    }

    @RequiresFlagsEnabled(FLAG_ENGAGEMENT_METRICS)
    @Test
    public void testObfuscation_userInteractionExtras() throws Exception {
        final IntervalStats intervalStats = new IntervalStats();
        populateIntervalStats(intervalStats);

        final PackagesTokenData packagesTokenData = new PackagesTokenData();
        intervalStats.obfuscateData(packagesTokenData);

        // validate user interaction extras
        for (int i = 0; i < intervalStats.events.size(); i++) {
            UsageEvents.Event event = intervalStats.events.get(i);
            if (event.mEventType != USER_INTERACTION) continue;
            assertThat(event.mUserInteractionExtrasToken).isNotNull();
            assertThat(event.mUserInteractionExtrasToken.mActionToken).isNotEqualTo(
                UNASSIGNED_TOKEN);
            assertThat(event.mUserInteractionExtrasToken.mCategoryToken).isNotEqualTo(
                UNASSIGNED_TOKEN);
            assertThat(event.mUserInteractionExtrasToken.mTokenizedExtras).isNotNull();
            PersistableBundle tokenizedExtras = PersistableBundle.readFromStream(
                new ByteArrayInputStream(event.mUserInteractionExtrasToken.mTokenizedExtras));
            // Bundle key should be omitted from tokenized extras.
            assertThat(tokenizedExtras.size()).isEqualTo(2);
            int packageToken = packagesTokenData.getPackageTokenOrAdd(event.getPackageName(),
                System.currentTimeMillis());
            for (String keyToken : tokenizedExtras.keySet()) {
                String key = packagesTokenData.getString(packageToken, Integer.parseInt(keyToken));
                assertThat(key).isNotNull();
                switch (key) {
                    case EXTRA_KEY_INT_ARRAY:
                        // int array is truncated
                        assertThat(tokenizedExtras.getIntArray(keyToken)).hasLength(10);
                        break;
                    case EXTRA_KEY_STRING:
                        // String is truncated
                        assertThat(tokenizedExtras.getString(keyToken)).hasLength(127);
                        break;
                    default:
                        throw new Exception("Unexpected key " + key + " in tokenized extras");
                }
            }
        }
    }

    @RequiresFlagsEnabled(FLAG_ENGAGEMENT_METRICS)
    @Test
    public void testDeobfuscation_userInteractionExtras() throws Exception {
        final IntervalStats intervalStats = new IntervalStats();
        populateIntervalStats(intervalStats);

        final PackagesTokenData packagesTokenData = new PackagesTokenData();
        intervalStats.obfuscateData(packagesTokenData);
        intervalStats.deobfuscateData(packagesTokenData);

        // verify user interaction extras are present after deobfuscation
        for (int i = 0; i < intervalStats.events.size(); i++) {
            UsageEvents.Event event = intervalStats.events.get(i);
            if (event.mEventType != USER_INTERACTION) continue;
            assertThat(event.mExtras).isNotNull();
            PersistableBundle extras = event.mExtras;
            // Bundle key is omitted
            assertThat(extras.keySet()).containsExactly(UsageStatsManager.EXTRA_EVENT_CATEGORY,
                UsageStatsManager.EXTRA_EVENT_ACTION, EXTRA_KEY_INT_ARRAY, EXTRA_KEY_STRING);
            int[] array = extras.getIntArray(EXTRA_KEY_INT_ARRAY);
            assertThat(array).hasLength(10);
            for (int j : array) {
                assertThat(array[j]).isEqualTo(j);
            }
            String string = extras.getString(EXTRA_KEY_STRING);
            assertThat(string).hasLength(127);
            for (char c : string.toCharArray()) {
                assertThat(c).isEqualTo('a');
            }
        }
    }

    @Test
    public void testBadDataOnDeobfuscation() {
        final IntervalStats intervalStats = new IntervalStats();
@@ -214,7 +312,8 @@ public final class IntervalStatsTests {
            "packageStats", "configurations", "activeConfiguration", "events"};
    // All fields in this list are defined in IntervalStats but not persisted
    private static final String[] INTERVALSTATS_IGNORED_FIELDS = {"lastTimeSaved",
            "packageStatsObfuscated", "CURRENT_MAJOR_VERSION", "CURRENT_MINOR_VERSION", "TAG"};
            "packageStatsObfuscated", "CURRENT_MAJOR_VERSION", "CURRENT_MINOR_VERSION", "TAG",
            "MAX_EXTRA_ARRAY_LENGTH"};

    @Test
    public void testIntervalStatsFieldsAreKnown() {
+91 −1
Original line number Diff line number Diff line
@@ -31,12 +31,13 @@ import static android.app.usage.UsageEvents.Event.KEYGUARD_SHOWN;
import static android.app.usage.UsageEvents.Event.LOCUS_ID_SET;
import static android.app.usage.UsageEvents.Event.NOTIFICATION_INTERRUPTION;
import static android.app.usage.UsageEvents.Event.ROLLOVER_FOREGROUND_SERVICE;
import static android.app.usage.UsageEvents.Event.USER_INTERACTION;
import static android.app.usage.UsageEvents.Event.SCREEN_INTERACTIVE;
import static android.app.usage.UsageEvents.Event.SCREEN_NON_INTERACTIVE;
import static android.app.usage.UsageEvents.Event.SHORTCUT_INVOCATION;
import static android.app.usage.UsageEvents.Event.STANDBY_BUCKET_CHANGED;
import static android.app.usage.UsageEvents.Event.SYSTEM_INTERACTION;
import static android.app.usage.UsageEvents.Event.USER_INTERACTION;
import static android.appwidget.flags.Flags.engagementMetrics;

import android.app.usage.ConfigurationStats;
import android.app.usage.EventList;
@@ -56,12 +57,15 @@ import android.util.proto.ProtoInputStream;

import com.android.internal.annotations.VisibleForTesting;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;

public class IntervalStats {
    private static final String TAG = "IntervalStats";
    private static final int MAX_EXTRA_ARRAY_LENGTH = 10;

    public static final int CURRENT_MAJOR_VERSION = 1;
    public static final int CURRENT_MINOR_VERSION = 1;
@@ -592,6 +596,7 @@ public class IntervalStats {
                        event.mExtras = new PersistableBundle();
                        event.mExtras.putString(UsageStatsManager.EXTRA_EVENT_CATEGORY, category);
                        event.mExtras.putString(UsageStatsManager.EXTRA_EVENT_ACTION, action);
                        deobfuscateEventExtras(event, packagesTokenData, packageToken);
                        event.mUserInteractionExtrasToken = null;
                    }
                    break;
@@ -604,6 +609,27 @@ public class IntervalStats {
        return dataOmitted;
    }

    private static void deobfuscateEventExtras(Event event, PackagesTokenData packagesTokenData,
        int packageToken) {
        if (!engagementMetrics() || event.mUserInteractionExtrasToken.mTokenizedExtras == null) {
            return;
        }
        try {
            final PersistableBundle tokenizedExtras =
                PersistableBundle.readFromStream(new ByteArrayInputStream(
                    event.mUserInteractionExtrasToken.mTokenizedExtras));
            for (String keyToken : tokenizedExtras.keySet()) {
                final String key = packagesTokenData.getString(packageToken,
                    Integer.parseInt(keyToken));
                if (key == null) continue;
                event.mExtras.putObject(key, tokenizedExtras.get(keyToken));
            }
        } catch (IOException e) {
            Slog.e(TAG, "Failed to parse tokenized extras for USER_INTERACTION"
                + " event for " + event.mPackage, e);
        }
    }

    /**
     * Parses the obfuscated tokenized data held in this interval stats object.
     *
@@ -727,6 +753,7 @@ public class IntervalStats {
                            event.mUserInteractionExtrasToken.mActionToken =
                                    packagesTokenData.getTokenOrAdd(packageToken, event.mPackage,
                                            action);
                            obfuscateEventsExtras(event, packagesTokenData, packageToken);
                        }
                    }
                    break;
@@ -734,6 +761,69 @@ public class IntervalStats {
        }
    }

    private static void obfuscateEventsExtras(Event event, PackagesTokenData packagesTokenData,
        int packageToken) {
        if (!engagementMetrics()) return;
        // Tokenize other keys in the bundle if present.
        final PersistableBundle tokenizedExtras = new PersistableBundle();
        for (String key : event.mExtras.keySet()) {
            if (key.equals(UsageStatsManager.EXTRA_EVENT_CATEGORY)
                || key.equals(UsageStatsManager.EXTRA_EVENT_ACTION)) {
                continue;
            }
            final int keyToken = packagesTokenData.getTokenOrAdd(
                packageToken, event.mPackage, key);
            Object value = event.mExtras.get(key);
            // Skip nested bundles, trim strings, and cap array length at 10.
            if (value == null || value instanceof PersistableBundle) {
                continue;
            } else if (value instanceof String string) {
                value = UsageStatsService.getTrimmedString(string);
            } else if (value.getClass().isArray()) {
                switch (value) {
                    case short[] arr:
                        value = Arrays.copyOf(arr, Integer.min(MAX_EXTRA_ARRAY_LENGTH, arr.length));
                        break;
                    case int[] arr:
                        value = Arrays.copyOf(arr, Integer.min(MAX_EXTRA_ARRAY_LENGTH, arr.length));
                        break;
                    case long[] arr:
                        value = Arrays.copyOf(arr, Integer.min(MAX_EXTRA_ARRAY_LENGTH, arr.length));
                        break;
                    case float[] arr:
                        value = Arrays.copyOf(arr, Integer.min(MAX_EXTRA_ARRAY_LENGTH, arr.length));
                        break;
                    case double[] arr:
                        value = Arrays.copyOf(arr, Integer.min(MAX_EXTRA_ARRAY_LENGTH, arr.length));
                        break;
                    case byte[] arr:
                        value = Arrays.copyOf(arr, Integer.min(MAX_EXTRA_ARRAY_LENGTH, arr.length));
                        break;
                    case char[] arr:
                        value = Arrays.copyOf(arr, Integer.min(MAX_EXTRA_ARRAY_LENGTH, arr.length));
                        break;
                    case String[] arr:
                        value = Arrays.copyOf(arr, Integer.min(MAX_EXTRA_ARRAY_LENGTH, arr.length));
                        break;
                    default:
                        continue;
                }
            }
            tokenizedExtras.putObject(String.valueOf(keyToken), value);
        }
        if (!tokenizedExtras.isEmpty()) {
            final ByteArrayOutputStream out = new ByteArrayOutputStream();
            try {
                tokenizedExtras.writeToStream(out);
                event.mUserInteractionExtrasToken.mTokenizedExtras =
                    out.toByteArray();
            }  catch (IOException e) {
                Slog.e(TAG, "Failed to write tokenized extras for USER_INTERACTION event for "
                    + event.mPackage);
            }
        }
    }

    /**
     * Obfuscates the data in this instance of interval stats.
     * @hide
+12 −0
Original line number Diff line number Diff line
@@ -15,6 +15,8 @@
 */
package com.android.server.usage;

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

import android.app.usage.ConfigurationStats;
import android.app.usage.UsageEvents;
import android.app.usage.UsageEvents.Event.UserInteractionEventExtrasToken;
@@ -931,6 +933,12 @@ final class UsageStatsProtoV2 {
                    interactionExtrasToken.mActionToken = proto.readInt(
                            ObfuscatedUserInteractionExtrasProto.ACTION_TOKEN) - 1;
                    break;
                case (int) ObfuscatedUserInteractionExtrasProto.TOKENIZED_EXTRAS:
                    if (engagementMetrics()) {
                        interactionExtrasToken.mTokenizedExtras = proto.readBytes(
                            ObfuscatedUserInteractionExtrasProto.TOKENIZED_EXTRAS);
                    }
                    break;
                case ProtoInputStream.NO_MORE_FIELDS:
                    return interactionExtrasToken;
            }
@@ -944,6 +952,10 @@ final class UsageStatsProtoV2 {
                interactionExtras.mCategoryToken + 1);
        proto.write(ObfuscatedUserInteractionExtrasProto.ACTION_TOKEN,
                interactionExtras.mActionToken + 1);
        if (engagementMetrics()) {
            proto.write(ObfuscatedUserInteractionExtrasProto.TOKENIZED_EXTRAS,
                interactionExtras.mTokenizedExtras);
        }
        proto.end(token);
    }

Loading