diff options
5 files changed, 498 insertions, 3 deletions
diff --git a/core/java/android/app/usage/ParcelableUsageEventList.aidl b/core/java/android/app/usage/ParcelableUsageEventList.aidl new file mode 100644 index 000000000000..165299651cff --- /dev/null +++ b/core/java/android/app/usage/ParcelableUsageEventList.aidl @@ -0,0 +1,19 @@ +/* + * 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; diff --git a/core/java/android/app/usage/ParcelableUsageEventList.java b/core/java/android/app/usage/ParcelableUsageEventList.java new file mode 100644 index 000000000000..016d97f60e26 --- /dev/null +++ b/core/java/android/app/usage/ParcelableUsageEventList.java @@ -0,0 +1,266 @@ +/* + * 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); + } +} diff --git a/core/java/android/app/usage/UsageEvents.java b/core/java/android/app/usage/UsageEvents.java index 3c256ad97f21..c188686d2d5b 100644 --- a/core/java/android/app/usage/UsageEvents.java +++ b/core/java/android/app/usage/UsageEvents.java @@ -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); diff --git a/core/java/android/app/usage/flags.aconfig b/core/java/android/app/usage/flags.aconfig index 4f1c65b1e395..0b8e29f954a5 100644 --- a/core/java/android/app/usage/flags.aconfig +++ b/core/java/android/app/usage/flags.aconfig @@ -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" +} diff --git a/core/tests/coretests/src/android/app/usage/ParcelableUsageEventListTest.java b/core/tests/coretests/src/android/app/usage/ParcelableUsageEventListTest.java new file mode 100644 index 000000000000..2ec58d43477d --- /dev/null +++ b/core/tests/coretests/src/android/app/usage/ParcelableUsageEventListTest.java @@ -0,0 +1,164 @@ +/* + * 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); + } +} |