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

Commit 6fe1f853 authored by Joe Antonetti's avatar Joe Antonetti Committed by Android (Google) Code Review
Browse files

Merge "[1/N][Requesting Handoff from App] Create HandoffActivityDataSerializer" into main

parents f282cd7e aa7a7ce4
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -58,3 +58,9 @@ message RemoteTaskInfo {
message RemoteTaskRemovedMessage {
    int32 taskId = 1;
}

message HandoffActivityData {
    string componentName = 1;
    string fallbackUri = 2;
    bytes extras = 3;
}
+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
+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