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

Commit a219bc15 authored by Joe Antonetti's avatar Joe Antonetti
Browse files

[Handoff][9/N] Cache PackageMetadata when Serializing

This change adds a cache for package metadata retrieved from PackageManager, so we don't have to serialize each time we send a message.

Flag: android.companion.enable_task_continuity
Bug: 400970610
Test: Added Unit Testing
Change-Id: If4a61c022a61c300a331d69a8eeac1c059aad52a
parent 394a8f3f
Loading
Loading
Loading
Loading
+10 −22
Original line number Diff line number Diff line
@@ -16,17 +16,12 @@

package com.android.server.companion.datatransfer.continuity;

import static com.android.server.companion.datatransfer.contextsync.BitmapUtils.renderDrawableToByteArray;

import android.annotation.NonNull;
import android.app.ActivityManager.RunningTaskInfo;
import android.app.ActivityTaskManager;
import android.app.TaskStackListener;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.os.RemoteException;
import android.util.Slog;

@@ -36,6 +31,8 @@ import com.android.server.companion.datatransfer.continuity.messages.RemoteTaskA
import com.android.server.companion.datatransfer.continuity.messages.RemoteTaskRemovedMessage;
import com.android.server.companion.datatransfer.continuity.messages.RemoteTaskUpdatedMessage;
import com.android.server.companion.datatransfer.continuity.messages.RemoteTaskInfo;
import com.android.server.companion.datatransfer.continuity.tasks.PackageMetadata;
import com.android.server.companion.datatransfer.continuity.tasks.PackageMetadataCache;

import java.io.IOException;
import java.util.List;
@@ -54,7 +51,7 @@ class TaskBroadcaster extends TaskStackListener {
    private final Context mContext;
    private final ActivityTaskManager mActivityTaskManager;
    private final TaskContinuityMessenger mTaskContinuityMessenger;
    private final PackageManager mPackageManager;
    private final PackageMetadataCache mPackageMetadataCache;

    private boolean mIsListeningToActivityTaskManager = false;

@@ -67,7 +64,7 @@ class TaskBroadcaster extends TaskStackListener {

        mContext = context;
        mActivityTaskManager = context.getSystemService(ActivityTaskManager.class);
        mPackageManager = context.getPackageManager();
        mPackageMetadataCache = new PackageMetadataCache(context.getPackageManager());
        mTaskContinuityMessenger = taskContinuityMessenger;
    }

@@ -166,26 +163,17 @@ class TaskBroadcaster extends TaskStackListener {
    }

    private RemoteTaskInfo createRemoteTaskInfo(RunningTaskInfo taskInfo) {
        PackageInfo packageInfo;
        try {
            packageInfo = mPackageManager.getPackageInfo(
                taskInfo.baseActivity.getPackageName(),
                PackageManager.GET_META_DATA);
        } catch (PackageManager.NameNotFoundException e) {
            Slog.e(TAG, "Failed to get package info for task: " + taskInfo.taskId, e);
        PackageMetadata packageMetadata = mPackageMetadataCache.getMetadataForPackage(
            taskInfo.baseActivity.getPackageName());
        if (packageMetadata == null) {
            Slog.w(TAG, "Could not get package metadata for task: " + taskInfo.taskId);
            return null;
        }

        String baseApplicationLabel = mPackageManager.getApplicationLabel(
            packageInfo.applicationInfo).toString();

        Drawable baseApplicationIcon = mPackageManager.getApplicationIcon(
            packageInfo.applicationInfo);

        return new RemoteTaskInfo(
            taskInfo.taskId,
            baseApplicationLabel,
            packageMetadata.label(),
            taskInfo.lastActiveTime,
            renderDrawableToByteArray(baseApplicationIcon));
            packageMetadata.icon());
    }
}
 No newline at end of file
+27 −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.tasks;

import android.annotation.NonNull;

/**
 * Metadata about a package.
 *
 * @param label The label of the package.
 * @param icon The icon of the package.
 */
public record PackageMetadata(@NonNull String label, @NonNull byte[] icon) {}
 No newline at end of file
+88 −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.tasks;

import static com.android.server.companion.datatransfer.contextsync.BitmapUtils.renderDrawableToByteArray;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.pm.PackageManager;
import android.content.pm.PackageInfo;
import android.graphics.drawable.Drawable;
import android.util.Slog;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import javax.annotation.concurrent.GuardedBy;

public class PackageMetadataCache {

    private static final String TAG = "PackageMetadataCache";

    private final PackageManager mPackageManager;

    @GuardedBy("this")
    private final Map<String, PackageMetadata> mPackageMetadataMap = new HashMap<>();

    public PackageMetadataCache(@NonNull PackageManager packageManager) {
        mPackageManager = Objects.requireNonNull(packageManager);
    }

    @Nullable
    public PackageMetadata getMetadataForPackage(@NonNull String packageName) {
        Objects.requireNonNull(packageName);

        synchronized (this) {
            if (mPackageMetadataMap.containsKey(packageName)) {
                return mPackageMetadataMap.get(packageName);
            }

            PackageInfo packageInfo;
            try {
                packageInfo = mPackageManager.getPackageInfo(
                    packageName,
                    PackageManager.GET_META_DATA);
            } catch (PackageManager.NameNotFoundException e) {
                Slog.e(TAG, "Failed to get package info for package: " + packageName, e);
                return null;
            }

            CharSequence label = mPackageManager.getApplicationLabel(packageInfo.applicationInfo);
            if (label == null) {
                Slog.e(TAG, "PackageManager returned null label for package: " + packageName);
                return null;
            }

            Drawable icon = mPackageManager.getApplicationIcon(packageInfo.applicationInfo);
            if (icon == null) {
                Slog.e(TAG, "PackageManager returned null icon for package: " + packageName);
                return null;
            }

            byte[] serializedIcon = renderDrawableToByteArray(icon);
            if (serializedIcon == null) {
                Slog.e(TAG, "Failed to serialize icon for package: " + packageName);
                return null;
            }

            PackageMetadata metadata = new PackageMetadata(label.toString(), serializedIcon);
            mPackageMetadataMap.put(packageName, metadata);
            return metadata;
        }
    }
}
 No newline at end of file
+122 −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.tasks;

import static com.android.server.companion.datatransfer.contextsync.BitmapUtils.renderDrawableToByteArray;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
import static org.mockito.ArgumentMatchers.eq;

import com.android.frameworks.servicestests.R;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.platform.test.annotations.Presubmit;
import android.testing.AndroidTestingRunner;
import androidx.test.InstrumentationRegistry;

@Presubmit
@RunWith(AndroidTestingRunner.class)
public class PackageMetadataCacheTest {

    @Mock private PackageManager mockPackageManager;

    private PackageMetadataCache packageMetadataCache;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        packageMetadataCache = new PackageMetadataCache(mockPackageManager);
    }

    @Test
    public void testGetMetadataForPackage_returnsMetadata() {
        String packageName = "com.example.app";
        String label = "label";
        Drawable icon = createTestDrawable();
        setupMockApplicationInfo(packageName, label, icon);

        PackageMetadata metadata = packageMetadataCache.getMetadataForPackage(packageName);
        assertThat(metadata.label()).isEqualTo(label);
        assertThat(metadata.icon()).isEqualTo(renderDrawableToByteArray(icon));
    }

    @Test
    public void testGetMetadataForPackage_packageManagerReturnsNullLabel_returnsNull() {
        String packageName = "com.example.app";
        Drawable icon = createTestDrawable();
        setupMockApplicationInfo(packageName, null, icon);
        assertThat(packageMetadataCache.getMetadataForPackage(packageName)).isNull();
    }

    @Test
    public void testGetMetadataForPackage_packageManagerReturnsNullIcon_returnsNull() {
        String packageName = "com.example.app";
        String label = "label";
        setupMockApplicationInfo(packageName, label, null);
        assertThat(packageMetadataCache.getMetadataForPackage(packageName)).isNull();
    }

    @Test
    public void testGetMetadataForPackage_packageManagerThrowsException_returnsNull()
        throws PackageManager.NameNotFoundException {

        when(mockPackageManager.getPackageInfo(anyString(), anyInt()))
            .thenThrow(new PackageManager.NameNotFoundException());

        assertThat(packageMetadataCache.getMetadataForPackage("com.example.app")).isNull();
    }

    private void setupMockApplicationInfo(String packageName, String label, Drawable icon) {
        PackageInfo packageInfo = new PackageInfo();
        packageInfo.packageName = packageName;
        packageInfo.applicationInfo = new ApplicationInfo();
        packageInfo.applicationInfo.name = packageName;
        try {
            when(mockPackageManager
                    .getPackageInfo(eq(packageName), eq(PackageManager.GET_META_DATA)))
            .thenReturn(packageInfo);
        } catch (PackageManager.NameNotFoundException e) {
        }
        when(mockPackageManager.getApplicationLabel(eq(packageInfo.applicationInfo)))
            .thenReturn(label);
        when(mockPackageManager.getApplicationIcon(eq(packageInfo.applicationInfo)))
            .thenReturn(icon);
    }

    private Drawable createTestDrawable() {
        Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
        Bitmap bitmap = BitmapFactory.decodeResource(
                context.getResources(), R.drawable.black_32x32);
        return new BitmapDrawable(context.getResources(), bitmap);
    }
}
 No newline at end of file