Loading core/proto/android/companion/task_continuity_message.proto +6 −0 Original line number Diff line number Diff line Loading @@ -58,3 +58,9 @@ message RemoteTaskInfo { message RemoteTaskRemovedMessage { int32 taskId = 1; } message HandoffActivityData { string componentName = 1; string fallbackUri = 2; bytes extras = 3; } services/companion/java/com/android/server/companion/datatransfer/continuity/messages/HandoffActivityDataSerializer.java 0 → 100644 +118 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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 com.android.server.companion.datatransfer.continuity.messages; import android.annotation.NonNull; import android.app.HandoffActivityData; import android.content.ComponentName; import android.net.Uri; import android.os.Parcel; import android.os.PersistableBundle; import android.util.Log; import android.util.proto.ProtoInputStream; import android.util.proto.ProtoOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; public final class HandoffActivityDataSerializer { private static final String TAG = "HandoffActivityDataSerializer"; public static void writeToProto( HandoffActivityData handoffActivityData, ProtoOutputStream pos) throws IOException { String flattenedComponentName = handoffActivityData.getComponentName().flattenToString(); pos.writeString( android.companion.HandoffActivityData.COMPONENT_NAME, flattenedComponentName); Uri fallbackUri = handoffActivityData.getFallbackUri(); if (fallbackUri != null) { pos.writeString( android.companion.HandoffActivityData.FALLBACK_URI, fallbackUri.toString()); } PersistableBundle extras = handoffActivityData.getExtras(); if (!extras.isEmpty()) { ByteArrayOutputStream extrasStream = new ByteArrayOutputStream(); extras.writeToStream(extrasStream); pos.writeBytes( android.companion.HandoffActivityData.EXTRAS, extrasStream.toByteArray()); } } public static HandoffActivityData readFromProto(ProtoInputStream pis) throws IOException { ComponentName componentName = null; Uri fallbackUri = null; PersistableBundle extras = new PersistableBundle(); while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { switch (pis.getFieldNumber()) { case (int) android.companion.HandoffActivityData.COMPONENT_NAME: String flattenedComponentName = pis.readString(android.companion.HandoffActivityData.COMPONENT_NAME); componentName = ComponentName.unflattenFromString(flattenedComponentName); if (componentName == null) { throw new IOException( "Invalid component name in proto: " + flattenedComponentName); } break; case (int) android.companion.HandoffActivityData.FALLBACK_URI: String flattenedFallbackUri = pis.readString(android.companion.HandoffActivityData.FALLBACK_URI); fallbackUri = Uri.parse(flattenedFallbackUri); if (fallbackUri == null) { Log.w(TAG, "Invalid URI received in HandoffActivityData proto. Ignoring."); } break; case (int) android.companion.HandoffActivityData.EXTRAS: byte[] rawExtras = pis.readBytes(android.companion.HandoffActivityData.EXTRAS); ByteArrayInputStream extrasStream = new ByteArrayInputStream(rawExtras); PersistableBundle newExtras = PersistableBundle.readFromStream(extrasStream); if (newExtras != null) { extras = newExtras; } break; default: Log.w(TAG, "Skipping unknown field in HandoffActivityData: " + pis.getFieldNumber()); break; } } if (componentName == null) { throw new IOException("No component name in proto"); } return new HandoffActivityData.Builder(componentName) .setFallbackUri(fallbackUri) .setExtras(extras) .build(); } } No newline at end of file services/tests/servicestests/src/com/android/server/companion/datatransfer/continuity/messages/HandoffActivityDataSerializerTest.java 0 → 100644 +119 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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 com.android.server.companion.datatransfer.continuity.messages; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import static org.testng.Assert.expectThrows; import android.app.HandoffActivityData; import android.content.ComponentName; import android.content.Intent; import android.net.Uri; import android.os.PersistableBundle; import android.platform.test.annotations.Presubmit; import android.testing.AndroidTestingRunner; import android.util.proto.ProtoInputStream; import android.util.proto.ProtoOutputStream; import android.util.proto.ProtoParseException; import com.android.server.companion.datatransfer.continuity.messages.HandoffActivityDataSerializer; import com.android.server.companion.datatransfer.continuity.messages.RemoteTaskInfo; import org.junit.Test; import org.junit.runner.RunWith; import java.io.IOException; @Presubmit @RunWith(AndroidTestingRunner.class) public class HandoffActivityDataSerializerTest { static final ComponentName COMPONENT_NAME = new ComponentName("com.example.package", "com.example.package.MyActivity"); static final Uri FALLBACK_URI = Uri.parse("https://www.google.com"); @Test public void testRoundTripSerializationWorks() throws IOException { HandoffActivityData onlyComponentName = new HandoffActivityData.Builder(COMPONENT_NAME).build(); confirmRoundTripSerializationDoesNotModifyData(onlyComponentName); HandoffActivityData withFallbackUri = new HandoffActivityData.Builder(COMPONENT_NAME) .setFallbackUri(FALLBACK_URI) .build(); confirmRoundTripSerializationDoesNotModifyData(withFallbackUri); PersistableBundle extras = new PersistableBundle(); extras.putString("key", "value"); HandoffActivityData withExtras = new HandoffActivityData.Builder(COMPONENT_NAME) .setExtras(extras) .build(); confirmRoundTripSerializationDoesNotModifyData(withExtras); HandoffActivityData withAllFields = new HandoffActivityData.Builder(COMPONENT_NAME) .setFallbackUri(FALLBACK_URI) .setExtras(extras) .build(); confirmRoundTripSerializationDoesNotModifyData(withAllFields); } @Test public void testReadFromProto_noComponentName_throwsException() { ProtoOutputStream pos = new ProtoOutputStream(); pos.flush(); assertThrows(IOException.class, () -> HandoffActivityDataSerializer.readFromProto( new ProtoInputStream(pos.getBytes()))); } @Test public void testReadFromProto_invalidComponentName_throwsException() { ProtoOutputStream pos = new ProtoOutputStream(); pos.writeString(android.companion.HandoffActivityData.COMPONENT_NAME, "invalid"); pos.flush(); assertThrows(IOException.class, () -> HandoffActivityDataSerializer.readFromProto( new ProtoInputStream(pos.getBytes()))); } private void confirmRoundTripSerializationDoesNotModifyData(HandoffActivityData expected) throws IOException { // Write to a ProtoOutputStream. ProtoOutputStream pos = new ProtoOutputStream(); HandoffActivityDataSerializer.writeToProto(expected, pos); // Read from a ProtoInputStream. ProtoInputStream pis = new ProtoInputStream(pos.getBytes()); HandoffActivityData actual = HandoffActivityDataSerializer.readFromProto(pis); assertThat(actual.getComponentName()).isEqualTo(expected.getComponentName()); assertThat(actual.getFallbackUri()).isEqualTo(expected.getFallbackUri()); if (expected.getExtras() != null) { assertThat(actual.getExtras()).isNotNull(); assertThat(actual.getExtras().size()).isEqualTo(expected.getExtras().size()); for (String key : expected.getExtras().keySet()) { assertThat(actual.getExtras().getString(key)) .isEqualTo(expected.getExtras().getString(key)); } } else { assertThat(actual.getExtras()).isNull(); } } } No newline at end of file Loading
core/proto/android/companion/task_continuity_message.proto +6 −0 Original line number Diff line number Diff line Loading @@ -58,3 +58,9 @@ message RemoteTaskInfo { message RemoteTaskRemovedMessage { int32 taskId = 1; } message HandoffActivityData { string componentName = 1; string fallbackUri = 2; bytes extras = 3; }
services/companion/java/com/android/server/companion/datatransfer/continuity/messages/HandoffActivityDataSerializer.java 0 → 100644 +118 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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 com.android.server.companion.datatransfer.continuity.messages; import android.annotation.NonNull; import android.app.HandoffActivityData; import android.content.ComponentName; import android.net.Uri; import android.os.Parcel; import android.os.PersistableBundle; import android.util.Log; import android.util.proto.ProtoInputStream; import android.util.proto.ProtoOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; public final class HandoffActivityDataSerializer { private static final String TAG = "HandoffActivityDataSerializer"; public static void writeToProto( HandoffActivityData handoffActivityData, ProtoOutputStream pos) throws IOException { String flattenedComponentName = handoffActivityData.getComponentName().flattenToString(); pos.writeString( android.companion.HandoffActivityData.COMPONENT_NAME, flattenedComponentName); Uri fallbackUri = handoffActivityData.getFallbackUri(); if (fallbackUri != null) { pos.writeString( android.companion.HandoffActivityData.FALLBACK_URI, fallbackUri.toString()); } PersistableBundle extras = handoffActivityData.getExtras(); if (!extras.isEmpty()) { ByteArrayOutputStream extrasStream = new ByteArrayOutputStream(); extras.writeToStream(extrasStream); pos.writeBytes( android.companion.HandoffActivityData.EXTRAS, extrasStream.toByteArray()); } } public static HandoffActivityData readFromProto(ProtoInputStream pis) throws IOException { ComponentName componentName = null; Uri fallbackUri = null; PersistableBundle extras = new PersistableBundle(); while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { switch (pis.getFieldNumber()) { case (int) android.companion.HandoffActivityData.COMPONENT_NAME: String flattenedComponentName = pis.readString(android.companion.HandoffActivityData.COMPONENT_NAME); componentName = ComponentName.unflattenFromString(flattenedComponentName); if (componentName == null) { throw new IOException( "Invalid component name in proto: " + flattenedComponentName); } break; case (int) android.companion.HandoffActivityData.FALLBACK_URI: String flattenedFallbackUri = pis.readString(android.companion.HandoffActivityData.FALLBACK_URI); fallbackUri = Uri.parse(flattenedFallbackUri); if (fallbackUri == null) { Log.w(TAG, "Invalid URI received in HandoffActivityData proto. Ignoring."); } break; case (int) android.companion.HandoffActivityData.EXTRAS: byte[] rawExtras = pis.readBytes(android.companion.HandoffActivityData.EXTRAS); ByteArrayInputStream extrasStream = new ByteArrayInputStream(rawExtras); PersistableBundle newExtras = PersistableBundle.readFromStream(extrasStream); if (newExtras != null) { extras = newExtras; } break; default: Log.w(TAG, "Skipping unknown field in HandoffActivityData: " + pis.getFieldNumber()); break; } } if (componentName == null) { throw new IOException("No component name in proto"); } return new HandoffActivityData.Builder(componentName) .setFallbackUri(fallbackUri) .setExtras(extras) .build(); } } No newline at end of file
services/tests/servicestests/src/com/android/server/companion/datatransfer/continuity/messages/HandoffActivityDataSerializerTest.java 0 → 100644 +119 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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 com.android.server.companion.datatransfer.continuity.messages; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import static org.testng.Assert.expectThrows; import android.app.HandoffActivityData; import android.content.ComponentName; import android.content.Intent; import android.net.Uri; import android.os.PersistableBundle; import android.platform.test.annotations.Presubmit; import android.testing.AndroidTestingRunner; import android.util.proto.ProtoInputStream; import android.util.proto.ProtoOutputStream; import android.util.proto.ProtoParseException; import com.android.server.companion.datatransfer.continuity.messages.HandoffActivityDataSerializer; import com.android.server.companion.datatransfer.continuity.messages.RemoteTaskInfo; import org.junit.Test; import org.junit.runner.RunWith; import java.io.IOException; @Presubmit @RunWith(AndroidTestingRunner.class) public class HandoffActivityDataSerializerTest { static final ComponentName COMPONENT_NAME = new ComponentName("com.example.package", "com.example.package.MyActivity"); static final Uri FALLBACK_URI = Uri.parse("https://www.google.com"); @Test public void testRoundTripSerializationWorks() throws IOException { HandoffActivityData onlyComponentName = new HandoffActivityData.Builder(COMPONENT_NAME).build(); confirmRoundTripSerializationDoesNotModifyData(onlyComponentName); HandoffActivityData withFallbackUri = new HandoffActivityData.Builder(COMPONENT_NAME) .setFallbackUri(FALLBACK_URI) .build(); confirmRoundTripSerializationDoesNotModifyData(withFallbackUri); PersistableBundle extras = new PersistableBundle(); extras.putString("key", "value"); HandoffActivityData withExtras = new HandoffActivityData.Builder(COMPONENT_NAME) .setExtras(extras) .build(); confirmRoundTripSerializationDoesNotModifyData(withExtras); HandoffActivityData withAllFields = new HandoffActivityData.Builder(COMPONENT_NAME) .setFallbackUri(FALLBACK_URI) .setExtras(extras) .build(); confirmRoundTripSerializationDoesNotModifyData(withAllFields); } @Test public void testReadFromProto_noComponentName_throwsException() { ProtoOutputStream pos = new ProtoOutputStream(); pos.flush(); assertThrows(IOException.class, () -> HandoffActivityDataSerializer.readFromProto( new ProtoInputStream(pos.getBytes()))); } @Test public void testReadFromProto_invalidComponentName_throwsException() { ProtoOutputStream pos = new ProtoOutputStream(); pos.writeString(android.companion.HandoffActivityData.COMPONENT_NAME, "invalid"); pos.flush(); assertThrows(IOException.class, () -> HandoffActivityDataSerializer.readFromProto( new ProtoInputStream(pos.getBytes()))); } private void confirmRoundTripSerializationDoesNotModifyData(HandoffActivityData expected) throws IOException { // Write to a ProtoOutputStream. ProtoOutputStream pos = new ProtoOutputStream(); HandoffActivityDataSerializer.writeToProto(expected, pos); // Read from a ProtoInputStream. ProtoInputStream pis = new ProtoInputStream(pos.getBytes()); HandoffActivityData actual = HandoffActivityDataSerializer.readFromProto(pis); assertThat(actual.getComponentName()).isEqualTo(expected.getComponentName()); assertThat(actual.getFallbackUri()).isEqualTo(expected.getFallbackUri()); if (expected.getExtras() != null) { assertThat(actual.getExtras()).isNotNull(); assertThat(actual.getExtras().size()).isEqualTo(expected.getExtras().size()); for (String key : expected.getExtras().keySet()) { assertThat(actual.getExtras().getString(key)) .isEqualTo(expected.getExtras().getString(key)); } } else { assertThat(actual.getExtras()).isNull(); } } } No newline at end of file