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

Commit 936b28ea authored by Xin Guan's avatar Xin Guan Committed by Android (Google) Code Review
Browse files

Merge "Avoid transfering a large usage events list in one transaction" into main

parents f76aff7e c1067415
Loading
Loading
Loading
Loading
+19 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.app.usage;

parcelable ParcelableUsageEventList;
+266 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.app.usage;

import android.annotation.NonNull;
import android.app.usage.UsageEvents.Event;
import android.content.res.Configuration;
import android.os.BadParcelableException;
import android.os.Binder;
import android.os.IBinder;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteException;
import android.util.Log;

import java.util.ArrayList;
import java.util.List;

/**
 * This is a copied version of BaseParceledListSlice with specific
 * {@link UsageEvents.Event} instance that used to transfer the large
 * list of {@link UsageEvents.Event} objects across an IPC. Splits
 * into multiple transactions if needed.
 *
 * @see BasedParceledListSlice
 *
 * @hide
 */
public final class ParcelableUsageEventList implements Parcelable {
    private static final String TAG = "ParcelableUsageEventList";
    private static final boolean DEBUG = false;
    private static final boolean DEBUG_ALL = false;

    private static final int MAX_IPC_SIZE = IBinder.getSuggestedMaxIpcSizeBytes();

    private List<Event> mList;

    public ParcelableUsageEventList(List<Event> list) {
        mList = list;
    }

    private ParcelableUsageEventList(Parcel in) {
        final int N = in.readInt();
        mList = new ArrayList<>();
        if (DEBUG) Log.d(TAG, "Retrieving " + N + " items");
        if (N <= 0) {
            return;
        }

        int i = 0;
        while (i < N) {
            if (in.readInt() == 0) {
                break;
            }
            mList.add(readEventFromParcel(in));
            if (DEBUG_ALL) Log.d(TAG, "Read inline #" + i + ": " + mList.get(mList.size() - 1));
            i++;
        }
        if (DEBUG) {
            Log.d(TAG, "Read " + mList.size() + " inline UsageEvents"
                    + ", total N=" + N + " UsageEvents");
        }
        if (i >= N) {
            return;
        }
        final IBinder retriever = in.readStrongBinder();
        while (i < N) {
            if (DEBUG) Log.d(TAG, "Reading more @" + i + " of " + N + ": retriever=" + retriever);
            Parcel data = Parcel.obtain();
            Parcel reply = Parcel.obtain();
            data.writeInt(i);
            try {
                retriever.transact(IBinder.FIRST_CALL_TRANSACTION, data, reply, 0);
                reply.readException();
                int count = 0;
                while (i < N && reply.readInt() != 0) {
                    mList.add(readEventFromParcel(reply));
                    if (DEBUG_ALL) {
                        Log.d(TAG, "Read extra #" + i + ": " + mList.get(mList.size() - 1));
                    }
                    i++;
                    count++;
                }
                if (DEBUG) Log.d(TAG, "Read extra @" + count + " of " + N);
            } catch (RemoteException e) {
                throw new BadParcelableException(
                    "Failure retrieving array; only received " + i + " of " + N, e);
            } finally {
                reply.recycle();
                data.recycle();
            }
        }
        if (DEBUG) Log.d(TAG, "Finish reading total " + i + " UsageEvents");
    }

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

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        final int N = mList.size();
        final int callFlags = flags;
        dest.writeInt(N);
        if (DEBUG) Log.d(TAG, "Writing " + N + " items");
        if (N > 0) {
            int i = 0;
            while (i < N && dest.dataSize() < MAX_IPC_SIZE) {
                dest.writeInt(1);

                final Event event = mList.get(i);
                writeEventToParcel(event, dest, callFlags);

                if (DEBUG_ALL) Log.d(TAG, "Wrote inline #" + i + ": " + mList.get(i));
                i++;
            }
            if (i < N) {
                dest.writeInt(0);
                Binder retriever = new Binder() {
                    @Override
                    protected boolean onTransact(int code, Parcel data, Parcel reply, int flags)
                            throws RemoteException {
                        if (code != FIRST_CALL_TRANSACTION) {
                            return super.onTransact(code, data, reply, flags);
                        } else if (mList == null) {
                            throw new IllegalArgumentException("Attempt to transfer null list, "
                                + "did transfer finish?");
                        }
                        int i = data.readInt();

                        if (DEBUG) {
                            Log.d(TAG, "Writing more @" + i + " of " + N + " to "
                                    + Binder.getCallingPid() + ", sender=" + this);
                        }

                        try {
                            reply.writeNoException();
                            int count = 0;
                            while (i < N && reply.dataSize() < MAX_IPC_SIZE) {
                                reply.writeInt(1);

                                final Event event = mList.get(i);
                                writeEventToParcel(event, reply, callFlags);

                                if (DEBUG_ALL) {
                                    Log.d(TAG, "Wrote extra #" + i + ": " + mList.get(i));
                                }
                                i++;
                                count++;
                            }
                            if (i < N) {
                                if (DEBUG) {
                                    Log.d(TAG, "Breaking @" + i + " of " + N
                                            + "(count = " + count + ")");
                                }
                                reply.writeInt(0);
                            } else {
                                if (DEBUG) Log.d(TAG, "Transfer done, clearing mList reference");
                                mList = null;
                            }
                        } catch (RuntimeException e) {
                            if (DEBUG) Log.d(TAG, "Transfer failed, clearing mList reference");
                            mList = null;
                            throw e;
                        }
                        return true;
                    }
                };
                if (DEBUG) Log.d(TAG, "Breaking @" + i + " of " + N + ": retriever=" + retriever);
                dest.writeStrongBinder(retriever);
            }
        }
    }

    public List<Event> getList() {
        return mList;
    }

    public static final Parcelable.Creator<ParcelableUsageEventList> CREATOR =
            new Parcelable.Creator<ParcelableUsageEventList>() {
                public ParcelableUsageEventList createFromParcel(Parcel in) {
                    return new ParcelableUsageEventList(in);
                }

                @Override
                public ParcelableUsageEventList[] newArray(int size) {
                    return new ParcelableUsageEventList[size];
                }
            };

    private Event readEventFromParcel(Parcel in) {
        final Event event = new Event();
        event.mPackage = in.readString();
        event.mClass = in.readString();
        event.mInstanceId = in.readInt();
        event.mTaskRootPackage = in.readString();
        event.mTaskRootClass = in.readString();
        event.mEventType = in.readInt();
        event.mTimeStamp = in.readLong();

        // Fill out the event-dependant fields.
        event.mConfiguration = null;
        event.mShortcutId = null;
        event.mAction = null;
        event.mContentType = null;
        event.mContentAnnotations = null;
        event.mNotificationChannelId = null;
        event.mLocusId = null;

        switch (event.mEventType) {
            case Event.CONFIGURATION_CHANGE -> {
                event.mConfiguration = Configuration.CREATOR.createFromParcel(in);
            }
            case Event.SHORTCUT_INVOCATION -> event.mShortcutId = in.readString();
            case Event.CHOOSER_ACTION -> {
                event.mAction = in.readString();
                event.mContentType = in.readString();
                event.mContentAnnotations = in.readStringArray();
            }
            case Event.STANDBY_BUCKET_CHANGED -> event.mBucketAndReason = in.readInt();
            case Event.NOTIFICATION_INTERRUPTION -> event.mNotificationChannelId = in.readString();
            case Event.LOCUS_ID_SET -> event.mLocusId = in.readString();
        }
        event.mFlags = in.readInt();

        return event;
    }

    private void writeEventToParcel(@NonNull Event event, @NonNull Parcel dest, int flags) {
        dest.writeString(event.mPackage);
        dest.writeString(event.mClass);
        dest.writeInt(event.mInstanceId);
        dest.writeString(event.mTaskRootPackage);
        dest.writeString(event.mTaskRootClass);
        dest.writeInt(event.mEventType);
        dest.writeLong(event.mTimeStamp);

        switch (event.mEventType) {
            case Event.CONFIGURATION_CHANGE -> event.mConfiguration.writeToParcel(dest, flags);
            case Event.SHORTCUT_INVOCATION -> dest.writeString(event.mShortcutId);
            case Event.CHOOSER_ACTION -> {
                dest.writeString(event.mAction);
                dest.writeString(event.mContentType);
                dest.writeStringArray(event.mContentAnnotations);
            }
            case Event.STANDBY_BUCKET_CHANGED -> dest.writeInt(event.mBucketAndReason);
            case Event.NOTIFICATION_INTERRUPTION -> dest.writeString(event.mNotificationChannelId);
            case Event.LOCUS_ID_SET -> dest.writeString(event.mLocusId);
        }
        dest.writeInt(event.mFlags);
    }
}
+42 −3
Original line number Diff line number Diff line
@@ -714,7 +714,7 @@ public final class UsageEvents implements Parcelable {
    @UnsupportedAppUsage
    private Parcel mParcel = null;
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
    private final int mEventCount;
    private int mEventCount;

    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
    private int mIndex = 0;
@@ -735,6 +735,23 @@ public final class UsageEvents implements Parcelable {
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
    public UsageEvents(Parcel in) {
        if (Flags.useParceledList()) {
            readUsageEventsFromParcelWithParceledList(in);
        } else {
            readUsageEventsFromParcelWithBlob(in);
        }

        mIncludeTaskRoots = true;
    }

    private void readUsageEventsFromParcelWithParceledList(Parcel in) {
        mIndex = in.readInt();
        mEventsToWrite = in.readParcelable(UsageEvents.class.getClassLoader(),
            ParcelableUsageEventList.class).getList();
        mEventCount = mEventsToWrite.size();
    }

    private void readUsageEventsFromParcelWithBlob(Parcel in) {
        byte[] bytes = in.readBlob();
        Parcel data = Parcel.obtain();
        data.unmarshall(bytes, 0, bytes.length);
@@ -752,7 +769,6 @@ public final class UsageEvents implements Parcelable {
            mParcel.setDataSize(mParcel.dataPosition());
            mParcel.setDataPosition(positionInParcel);
        }
        mIncludeTaskRoots = true;
    }

    /**
@@ -810,6 +826,10 @@ public final class UsageEvents implements Parcelable {
            return false;
        }

        if (Flags.useParceledList()) {
            return getNextEventFromParceledList(eventOut);
        }

        if (mParcel != null) {
            readEventFromParcel(mParcel, eventOut);
        } else {
@@ -824,6 +844,12 @@ public final class UsageEvents implements Parcelable {
        return true;
    }

    private boolean getNextEventFromParceledList(Event eventOut) {
        eventOut.copyFrom(mEventsToWrite.get(mIndex));
        mIndex++;
        return true;
    }

    /**
     * Resets the collection so that it can be iterated over from the beginning.
     *
@@ -968,7 +994,7 @@ public final class UsageEvents implements Parcelable {
            case Event.CHOOSER_ACTION:
                eventOut.mAction = p.readString();
                eventOut.mContentType = p.readString();
                eventOut.mContentAnnotations = p.createStringArray();
                eventOut.mContentAnnotations = p.readStringArray();
                break;
            case Event.STANDBY_BUCKET_CHANGED:
                eventOut.mBucketAndReason = p.readInt();
@@ -990,6 +1016,19 @@ public final class UsageEvents implements Parcelable {

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        if (Flags.useParceledList()) {
            writeUsageEventsToParcelWithParceledList(dest, flags);
        } else {
            writeUsageEventsToParcelWithBlob(dest, flags);
        }
    }

    private void writeUsageEventsToParcelWithParceledList(Parcel dest, int flags) {
        dest.writeInt(mIndex);
        dest.writeParcelable(new ParcelableUsageEventList(mEventsToWrite), flags);
    }

    private void writeUsageEventsToParcelWithBlob(Parcel dest, int flags) {
        Parcel data = Parcel.obtain();
        data.writeInt(mEventCount);
        data.writeInt(mIndex);
+7 −0
Original line number Diff line number Diff line
@@ -21,3 +21,10 @@ flag {
    is_fixed_read_only: true
    bug: "299336442"
}

flag {
    name: "use_parceled_list"
    namespace: "backstage_power"
    description: "Flag for parcelable usage event list"
    bug: "301254110"
}
+164 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.app.usage;

import static android.view.Surface.ROTATION_90;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;

import android.app.usage.UsageEvents.Event;
import android.content.res.Configuration;
import android.os.Parcel;
import android.test.suitebuilder.annotation.LargeTest;

import androidx.test.runner.AndroidJUnit4;

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

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;

@RunWith(AndroidJUnit4.class)
@LargeTest
public class ParcelableUsageEventListTest {
    private static final int SMALL_TEST_EVENT_COUNT = 100;
    private static final int LARGE_TEST_EVENT_COUNT = 10000;

    private Random mRandom = new Random();

    @Test
    public void testSmallList() throws Exception {
        testParcelableUsageEventList(SMALL_TEST_EVENT_COUNT);
    }

    @Test
    public void testLargeList() throws Exception {
        testParcelableUsageEventList(LARGE_TEST_EVENT_COUNT);
    }

    private void testParcelableUsageEventList(int eventCount) throws Exception {
        List<Event> smallList = new ArrayList<>();
        for (int i = 0; i < eventCount; i++) {
            smallList.add(generateUsageEvent());
        }

        ParcelableUsageEventList slice;
        Parcel parcel = Parcel.obtain();
        try {
            parcel.writeParcelable(new ParcelableUsageEventList(smallList), 0);
            parcel.setDataPosition(0);
            slice = parcel.readParcelable(getClass().getClassLoader(),
                    ParcelableUsageEventList.class);
        } finally {
            parcel.recycle();
        }

        assertNotNull(slice);
        assertNotNull(slice.getList());
        assertEquals(eventCount, slice.getList().size());

        for (int i = 0; i < eventCount; i++) {
            compareUsageEvent(smallList.get(i), slice.getList().get(i));
        }
    }

    private Event generateUsageEvent() {
        final Event event = new Event();
        event.mEventType = mRandom.nextInt(Event.MAX_EVENT_TYPE + 1);
        event.mPackage = anyString();
        event.mClass = anyString();
        event.mTimeStamp = anyLong();
        event.mInstanceId = anyInt();
        event.mTimeStamp = anyLong();

        switch (event.mEventType) {
            case Event.CONFIGURATION_CHANGE:
                event.mConfiguration = new Configuration();
                event.mConfiguration.seq = anyInt();
                event.mConfiguration.screenLayout = Configuration.SCREENLAYOUT_ROUND_YES;
                event.mConfiguration.smallestScreenWidthDp = 100;
                event.mConfiguration.orientation = Configuration.ORIENTATION_LANDSCAPE;
                event.mConfiguration.windowConfiguration.setRotation(ROTATION_90);
                break;
            case Event.SHORTCUT_INVOCATION:
                event.mShortcutId = anyString();
                break;
            case Event.CHOOSER_ACTION:
                event.mAction = anyString();
                event.mContentType = anyString();
                event.mContentAnnotations = new String[mRandom.nextInt(10)];
                for (int i = 0; i < event.mContentAnnotations.length; i++) {
                    event.mContentAnnotations[i] = anyString();
                }
                break;
            case Event.STANDBY_BUCKET_CHANGED:
                event.mBucketAndReason = anyInt();
                break;
            case Event.NOTIFICATION_INTERRUPTION:
                event.mNotificationChannelId = anyString();
                break;
            case Event.LOCUS_ID_SET:
                event.mLocusId = anyString();
                break;
        }

        event.mFlags = anyInt();
        return event;
    }

    private static void compareUsageEvent(Event ue1, Event ue2) {
        assertEquals(ue1.mPackage, ue2.mPackage);
        assertEquals(ue1.mClass, ue2.mClass);
        assertEquals(ue1.mTaskRootPackage, ue2.mTaskRootPackage);
        assertEquals(ue1.mTaskRootClass, ue2.mTaskRootClass);
        assertEquals(ue1.mInstanceId, ue2.mInstanceId);
        assertEquals(ue1.mEventType, ue2.mEventType);
        assertEquals(ue1.mTimeStamp, ue2.mTimeStamp);

        switch (ue1.mEventType) {
            case Event.CONFIGURATION_CHANGE:
                assertEquals(ue1.mConfiguration, ue2.mConfiguration);
                break;
            case Event.SHORTCUT_INVOCATION:
                assertEquals(ue1.mShortcutId, ue2.mShortcutId);
                break;
            case Event.CHOOSER_ACTION:
                assertEquals(ue1.mAction, ue2.mAction);
                assertEquals(ue1.mContentType, ue2.mContentType);
                assertTrue(Arrays.equals(ue1.mContentAnnotations, ue2.mContentAnnotations));
                break;
            case Event.STANDBY_BUCKET_CHANGED:
                assertEquals(ue1.mBucketAndReason, ue2.mBucketAndReason);
                break;
            case Event.NOTIFICATION_INTERRUPTION:
                assertEquals(ue1.mNotificationChannelId, ue1.mNotificationChannelId);
                break;
            case Event.LOCUS_ID_SET:
                assertEquals(ue1.mLocusId, ue2.mLocusId);
                break;
        }

        assertEquals(ue1.mFlags, ue2.mFlags);
    }
}