Introduce dedicated PollingFrame class to replace the use of Bundles per
API Council feedback

Bug: b/315131060
Test: Tested with CTS
Change-Id: I9f69dba67d3f4d204da05148f20cbfefc2a8c89a
diff --git a/nfc/api/current.txt b/nfc/api/current.txt
index f111327..0383276 100644
--- a/nfc/api/current.txt
+++ b/nfc/api/current.txt
@@ -228,14 +228,10 @@
     method public final android.os.IBinder onBind(android.content.Intent);
     method public abstract void onDeactivated(int);
     method public abstract byte[] processCommandApdu(byte[], android.os.Bundle);
-    method @FlaggedApi("android.nfc.nfc_read_polling_loop") public void processPollingFrames(@NonNull java.util.List<android.os.Bundle>);
+    method @FlaggedApi("android.nfc.nfc_read_polling_loop") public void processPollingFrames(@NonNull java.util.List<android.nfc.cardemulation.PollingFrame>);
     method public final void sendResponseApdu(byte[]);
     field public static final int DEACTIVATION_DESELECTED = 1; // 0x1
     field public static final int DEACTIVATION_LINK_LOSS = 0; // 0x0
-    field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final String KEY_POLLING_LOOP_DATA = "android.nfc.cardemulation.DATA";
-    field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final String KEY_POLLING_LOOP_GAIN = "android.nfc.cardemulation.GAIN";
-    field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final String KEY_POLLING_LOOP_TIMESTAMP = "android.nfc.cardemulation.TIMESTAMP";
-    field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final String KEY_POLLING_LOOP_TYPE = "android.nfc.cardemulation.TYPE";
     field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final char POLLING_LOOP_TYPE_A = 65; // 0x0041 'A'
     field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final char POLLING_LOOP_TYPE_B = 66; // 0x0042 'B'
     field @FlaggedApi("android.nfc.nfc_read_polling_loop") public static final char POLLING_LOOP_TYPE_F = 70; // 0x0046 'F'
@@ -274,6 +270,17 @@
     field public static final String SERVICE_META_DATA = "android.nfc.cardemulation.off_host_apdu_service";
   }
 
+  @FlaggedApi("android.nfc.nfc_read_polling_loop") public final class PollingFrame implements android.os.Parcelable {
+    ctor public PollingFrame(char, @Nullable byte[], int, int);
+    method public int describeContents();
+    method @NonNull public byte[] getData();
+    method public int getGain();
+    method public int getTimestamp();
+    method public char getType();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.nfc.cardemulation.PollingFrame> CREATOR;
+  }
+
 }
 
 package android.nfc.tech {
diff --git a/nfc/java/android/nfc/NfcAdapter.java b/nfc/java/android/nfc/NfcAdapter.java
index c5b7582..313e6d2 100644
--- a/nfc/java/android/nfc/NfcAdapter.java
+++ b/nfc/java/android/nfc/NfcAdapter.java
@@ -38,6 +38,8 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.net.Uri;
+import android.nfc.cardemulation.HostApduService;
+import android.nfc.cardemulation.PollingFrame;
 import android.nfc.tech.MifareClassic;
 import android.nfc.tech.Ndef;
 import android.nfc.tech.NfcA;
@@ -2799,7 +2801,12 @@
      */
     @TestApi
     @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP)
-    public void notifyPollingLoop(@NonNull Bundle frame) {
+    public void notifyPollingLoop(@NonNull PollingFrame pollingFrame) {
+        Bundle frame = new Bundle();
+        frame.putChar(HostApduService.KEY_POLLING_LOOP_TYPE, pollingFrame.getType());
+        frame.putByte(HostApduService.KEY_POLLING_LOOP_GAIN, (byte) pollingFrame.getGain());
+        frame.putByteArray(HostApduService.KEY_POLLING_LOOP_DATA, pollingFrame.getData());
+        frame.putInt(HostApduService.KEY_POLLING_LOOP_TIMESTAMP, pollingFrame.getTimestamp());
         try {
             if (sService == null) {
                 attemptDeadServiceRecovery(null);
diff --git a/nfc/java/android/nfc/cardemulation/HostApduService.java b/nfc/java/android/nfc/cardemulation/HostApduService.java
index 363788e..a71764d 100644
--- a/nfc/java/android/nfc/cardemulation/HostApduService.java
+++ b/nfc/java/android/nfc/cardemulation/HostApduService.java
@@ -245,7 +245,9 @@
 
     /**
      * KEY_POLLING_LOOP_TYPE is the Bundle key for the type of
-     * polling loop frame in the Bundle passed to {@link #processPollingFrames(List)}
+     * polling loop frame in the Bundle included in MSG_POLLING_LOOP.
+     *
+     * @hide
      */
     @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP)
     public static final String KEY_POLLING_LOOP_TYPE = "android.nfc.cardemulation.TYPE";
@@ -300,24 +302,27 @@
 
     /**
      * KEY_POLLING_LOOP_DATA is the Bundle key for the raw data of captured from
-     * the polling loop frame in the Bundle passed to {@link #processPollingFrames(List)}
-     * when the frame type isn't recognized.
+     * the polling loop frame in the Bundle included in MSG_POLLING_LOOP.
+     *
+     * @hide
      */
     @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP)
     public static final String KEY_POLLING_LOOP_DATA = "android.nfc.cardemulation.DATA";
 
     /**
      * KEY_POLLING_LOOP_GAIN is the Bundle key for the field strength of
-     * the polling loop frame in the Bundle passed to {@link #processPollingFrames(List)}
-     * when the frame type isn't recognized.
+     * the polling loop frame in the Bundle included in MSG_POLLING_LOOP.
+     *
+     * @hide
      */
     @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP)
     public static final String KEY_POLLING_LOOP_GAIN = "android.nfc.cardemulation.GAIN";
 
     /**
      * KEY_POLLING_LOOP_TIMESTAMP is the Bundle key for the timestamp of
-     * the polling loop frame in the Bundle passed to {@link #processPollingFrames(List)}
-     * when the frame type isn't recognized.
+     * the polling loop frame in the Bundle included in MSG_POLLING_LOOP.
+     *
+     * @hide
      */
     @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP)
     public static final String KEY_POLLING_LOOP_TIMESTAMP = "android.nfc.cardemulation.TIMESTAMP";
@@ -407,7 +412,12 @@
                     ArrayList<Bundle> frames =
                             msg.getData().getParcelableArrayList(KEY_POLLING_LOOP_FRAMES_BUNDLE,
                             Bundle.class);
-                    processPollingFrames(frames);
+                    ArrayList<PollingFrame> pollingFrames =
+                            new ArrayList<PollingFrame>(frames.size());
+                    for (Bundle frame : frames) {
+                        pollingFrames.add(new PollingFrame(frame));
+                    }
+                    processPollingFrames(pollingFrames);
                     break;
             default:
                 super.handleMessage(msg);
@@ -482,7 +492,7 @@
      * @param frame A description of the polling frame.
      */
     @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP)
-    public void processPollingFrames(@NonNull List<Bundle> frame) {
+    public void processPollingFrames(@NonNull List<PollingFrame> frame) {
     }
 
     /**
diff --git a/nfc/java/android/nfc/cardemulation/PollingFrame.java b/nfc/java/android/nfc/cardemulation/PollingFrame.java
new file mode 100644
index 0000000..2e7c77c
--- /dev/null
+++ b/nfc/java/android/nfc/cardemulation/PollingFrame.java
@@ -0,0 +1,143 @@
+/*
+ * 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.nfc.cardemulation;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Polling Frames represent data about individual frames of an NFC polling loop. These frames will
+ * be deliverd to subclasses of {@link HostApduService} that have registered filters with
+ * {@link CardEmulation#registerPollingLoopFilterForService(ComponentName, String)} that match a
+ * given frame in a loop and will be delivered through calls to
+ * {@link HostApduService#processPollingFrames(List)}.
+ */
+@FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP)
+public final class PollingFrame implements Parcelable{
+    private char mType;
+    private byte[] mData;
+    private int mGain;
+    private int mTimestamp;
+
+    public static final @NonNull Parcelable.Creator<PollingFrame> CREATOR =
+            new Parcelable.Creator<PollingFrame>() {
+
+                @Override
+                public PollingFrame createFromParcel(Parcel source) {
+                    return new PollingFrame(source.readBundle());
+                }
+
+                @Override
+                public PollingFrame[] newArray(int size) {
+                    return new PollingFrame[0];
+                }
+            };
+
+    PollingFrame(Bundle frame) {
+        mType = frame.getChar(HostApduService.KEY_POLLING_LOOP_TYPE);
+        mData = frame.getByteArray(HostApduService.KEY_POLLING_LOOP_DATA);
+        if (mData == null) {
+            mData = new byte[0];
+        }
+        mGain = frame.getByte(HostApduService.KEY_POLLING_LOOP_GAIN);
+        mTimestamp = frame.getInt(HostApduService.KEY_POLLING_LOOP_TIMESTAMP);
+    }
+
+    public PollingFrame(char type, @Nullable byte[] data, int gain, int timestamp) {
+        mType = type;
+        mData = data == null ? new byte[0] : data;
+        mGain = gain;
+        mTimestamp = timestamp;
+    }
+
+    private PollingFrame(Parcel source) {
+        mType = (char) source.readInt();
+        source.readByteArray(mData);
+        mGain = source.readInt();
+        mTimestamp = source.readInt();
+    }
+
+    /**
+     * Returns the type of frame for this polling loop frame.
+     *
+     * The possible return values are:
+     * <ul>
+     *   <li>{@link HostApduService#POLLING_LOOP_TYPE_ON}</li>
+     *   <li>{@link HostApduService#POLLING_LOOP_TYPE_OFF}</li>
+     *   <li>{@link HostApduService#POLLING_LOOP_TYPE_A}</li>
+     *   <li>{@link HostApduService#POLLING_LOOP_TYPE_B}</li>
+     *   <li>{@link HostApduService#POLLING_LOOP_TYPE_F}</li>
+     * </ul>
+     */
+    public char getType() {
+        return mType;
+    }
+
+    /**
+     * Returns the raw data from the polling type frame.
+     */
+    public @NonNull byte[] getData() {
+        return mData;
+    }
+
+    /**
+     * Returns the gain representing the field strength of the NFC field when this polling loop
+     * frame was observed.
+     */
+    public int getGain() {
+        return mGain;
+    }
+
+    /**
+     * Returns the timestamp of when the polling loop frame was observed in milliseconds. These
+     * timestamps are relative and not absolute and should only be used fro comparing the timing of
+     * frames relative to each other.
+     * @return the timestamp in milliseconds
+     */
+    public int getTimestamp() {
+        return mTimestamp;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeBundle(toBundle());
+    }
+
+    /**
+     *
+     * @hide
+     * @return a Bundle representing this frame
+     */
+    public Bundle toBundle() {
+        Bundle frame = new Bundle();
+        frame.putInt(HostApduService.KEY_POLLING_LOOP_TYPE, getType());
+        frame.putByte(HostApduService.KEY_POLLING_LOOP_GAIN, (byte) getGain());
+        frame.putByteArray(HostApduService.KEY_POLLING_LOOP_DATA, getData());
+        frame.putInt(HostApduService.KEY_POLLING_LOOP_TIMESTAMP, getTimestamp());
+        return frame;
+    }
+}