diff options
author | 2024-07-31 16:59:48 +0000 | |
---|---|---|
committer | 2024-10-08 19:46:23 +0000 | |
commit | 6f5a0ad1921813d5df1b17795b738cb556be2f12 (patch) | |
tree | f0595915e500530c318f103129debb08fc454d24 | |
parent | beb79048744267afcb24be8a26017e2d9a74d24a (diff) |
[W]SelfManaged Association device icon
Introduce custom self managed associaiton device icon
API that allow a system app to set the device icon that
displayed in the self managed association dialog and
this device icon will be stored in AssociationInfo
Test: CTS
Bug: 359341630
Flag: android.companion.association_device_icon
Change-Id: Ifa8f3f4016ff748a827e500824c86b95af12b1df
15 files changed, 233 insertions, 19 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index 56852212ab1e..9d0f92a488b1 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -9769,6 +9769,7 @@ package android.companion { public final class AssociationInfo implements android.os.Parcelable { method public int describeContents(); method @Nullable public android.companion.AssociatedDevice getAssociatedDevice(); + method @FlaggedApi("android.companion.association_device_icon") @Nullable public android.graphics.drawable.Icon getDeviceIcon(); method @Nullable public android.net.MacAddress getDeviceMacAddress(); method @Nullable public String getDeviceProfile(); method @Nullable public CharSequence getDisplayName(); @@ -9782,6 +9783,7 @@ package android.companion { public final class AssociationRequest implements android.os.Parcelable { method public int describeContents(); + method @FlaggedApi("android.companion.association_device_icon") @Nullable public android.graphics.drawable.Icon getDeviceIcon(); method @Nullable public String getDeviceProfile(); method @Nullable public CharSequence getDisplayName(); method public boolean isForceConfirmation(); @@ -9801,6 +9803,7 @@ package android.companion { ctor public AssociationRequest.Builder(); method @NonNull public android.companion.AssociationRequest.Builder addDeviceFilter(@Nullable android.companion.DeviceFilter<?>); method @NonNull public android.companion.AssociationRequest build(); + method @FlaggedApi("android.companion.association_device_icon") @NonNull @RequiresPermission(android.Manifest.permission.REQUEST_COMPANION_SELF_MANAGED) public android.companion.AssociationRequest.Builder setDeviceIcon(@NonNull android.graphics.drawable.Icon); method @NonNull public android.companion.AssociationRequest.Builder setDeviceProfile(@NonNull String); method @NonNull public android.companion.AssociationRequest.Builder setDisplayName(@NonNull CharSequence); method @NonNull @RequiresPermission(android.Manifest.permission.REQUEST_COMPANION_SELF_MANAGED) public android.companion.AssociationRequest.Builder setForceConfirmation(boolean); diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 4fc70769a3b1..aa6ed36ac6a5 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -912,6 +912,7 @@ package android.companion { ctor public AssociationInfo.Builder(@NonNull android.companion.AssociationInfo); method @NonNull public android.companion.AssociationInfo build(); method @NonNull public android.companion.AssociationInfo.Builder setAssociatedDevice(@Nullable android.companion.AssociatedDevice); + method @FlaggedApi("android.companion.association_device_icon") @NonNull public android.companion.AssociationInfo.Builder setDeviceIcon(@Nullable android.graphics.drawable.Icon); method @NonNull public android.companion.AssociationInfo.Builder setDeviceMacAddress(@Nullable android.net.MacAddress); method @NonNull public android.companion.AssociationInfo.Builder setDeviceProfile(@Nullable String); method @NonNull public android.companion.AssociationInfo.Builder setDisplayName(@Nullable CharSequence); diff --git a/core/java/android/companion/AssociationInfo.java b/core/java/android/companion/AssociationInfo.java index b4b96e2c69d6..7f30d7cccb57 100644 --- a/core/java/android/companion/AssociationInfo.java +++ b/core/java/android/companion/AssociationInfo.java @@ -22,6 +22,7 @@ import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.annotation.TestApi; import android.annotation.UserIdInt; +import android.graphics.drawable.Icon; import android.net.MacAddress; import android.os.Parcel; import android.os.Parcelable; @@ -86,6 +87,11 @@ public final class AssociationInfo implements Parcelable { private final int mSystemDataSyncFlags; /** + * A device icon displayed on a selfManaged association dialog. + */ + private final Icon mDeviceIcon; + + /** * Creates a new Association. * * @hide @@ -95,7 +101,7 @@ public final class AssociationInfo implements Parcelable { @Nullable CharSequence displayName, @Nullable String deviceProfile, @Nullable AssociatedDevice associatedDevice, boolean selfManaged, boolean notifyOnDeviceNearby, boolean revoked, boolean pending, long timeApprovedMs, - long lastTimeConnectedMs, int systemDataSyncFlags) { + long lastTimeConnectedMs, int systemDataSyncFlags, @Nullable Icon deviceIcon) { if (id <= 0) { throw new IllegalArgumentException("Association ID should be greater than 0"); } @@ -119,6 +125,7 @@ public final class AssociationInfo implements Parcelable { mTimeApprovedMs = timeApprovedMs; mLastTimeConnectedMs = lastTimeConnectedMs; mSystemDataSyncFlags = systemDataSyncFlags; + mDeviceIcon = deviceIcon; } /** @@ -278,6 +285,20 @@ public final class AssociationInfo implements Parcelable { } /** + * Get the device icon of the associated device. The device icon represents the device type. + * + * @return the device icon, or {@code null} if no device icon is has been set for the + * associated device. + * + * @see AssociationRequest.Builder#setDeviceIcon(Icon) + */ + @FlaggedApi(Flags.FLAG_ASSOCIATION_DEVICE_ICON) + @Nullable + public Icon getDeviceIcon() { + return mDeviceIcon; + } + + /** * Utility method for checking if the association represents a device with the given MAC * address. * @@ -370,14 +391,16 @@ public final class AssociationInfo implements Parcelable { && Objects.equals(mDisplayName, that.mDisplayName) && Objects.equals(mDeviceProfile, that.mDeviceProfile) && Objects.equals(mAssociatedDevice, that.mAssociatedDevice) - && mSystemDataSyncFlags == that.mSystemDataSyncFlags; + && mSystemDataSyncFlags == that.mSystemDataSyncFlags + && (mDeviceIcon == null ? that.mDeviceIcon == null + : mDeviceIcon.sameAs(that.mDeviceIcon)); } @Override public int hashCode() { return Objects.hash(mId, mUserId, mPackageName, mTag, mDeviceMacAddress, mDisplayName, mDeviceProfile, mAssociatedDevice, mSelfManaged, mNotifyOnDeviceNearby, mRevoked, - mPending, mTimeApprovedMs, mLastTimeConnectedMs, mSystemDataSyncFlags); + mPending, mTimeApprovedMs, mLastTimeConnectedMs, mSystemDataSyncFlags, mDeviceIcon); } @Override @@ -402,6 +425,12 @@ public final class AssociationInfo implements Parcelable { dest.writeLong(mTimeApprovedMs); dest.writeLong(mLastTimeConnectedMs); dest.writeInt(mSystemDataSyncFlags); + if (mDeviceIcon != null) { + dest.writeInt(1); + mDeviceIcon.writeToParcel(dest, flags); + } else { + dest.writeInt(0); + } } private AssociationInfo(@NonNull Parcel in) { @@ -420,6 +449,11 @@ public final class AssociationInfo implements Parcelable { mTimeApprovedMs = in.readLong(); mLastTimeConnectedMs = in.readLong(); mSystemDataSyncFlags = in.readInt(); + if (in.readInt() == 1) { + mDeviceIcon = Icon.CREATOR.createFromParcel(in); + } else { + mDeviceIcon = null; + } } @NonNull @@ -459,6 +493,7 @@ public final class AssociationInfo implements Parcelable { private long mTimeApprovedMs; private long mLastTimeConnectedMs; private int mSystemDataSyncFlags; + private Icon mDeviceIcon; /** @hide */ @TestApi @@ -486,6 +521,7 @@ public final class AssociationInfo implements Parcelable { mTimeApprovedMs = info.mTimeApprovedMs; mLastTimeConnectedMs = info.mLastTimeConnectedMs; mSystemDataSyncFlags = info.mSystemDataSyncFlags; + mDeviceIcon = info.mDeviceIcon; } /** @@ -510,6 +546,7 @@ public final class AssociationInfo implements Parcelable { mTimeApprovedMs = info.mTimeApprovedMs; mLastTimeConnectedMs = info.mLastTimeConnectedMs; mSystemDataSyncFlags = info.mSystemDataSyncFlags; + mDeviceIcon = info.mDeviceIcon; } /** @hide */ @@ -625,6 +662,16 @@ public final class AssociationInfo implements Parcelable { /** @hide */ @TestApi @NonNull + @SuppressLint("MissingGetterMatchingBuilder") + @FlaggedApi(Flags.FLAG_ASSOCIATION_DEVICE_ICON) + public Builder setDeviceIcon(@Nullable Icon deviceIcon) { + mDeviceIcon = deviceIcon; + return this; + } + + /** @hide */ + @TestApi + @NonNull public AssociationInfo build() { if (mId <= 0) { throw new IllegalArgumentException("Association ID should be greater than 0"); @@ -648,7 +695,8 @@ public final class AssociationInfo implements Parcelable { mPending, mTimeApprovedMs, mLastTimeConnectedMs, - mSystemDataSyncFlags + mSystemDataSyncFlags, + mDeviceIcon ); } } diff --git a/core/java/android/companion/AssociationRequest.java b/core/java/android/companion/AssociationRequest.java index 2e969f83f836..41a6791d8a7b 100644 --- a/core/java/android/companion/AssociationRequest.java +++ b/core/java/android/companion/AssociationRequest.java @@ -23,12 +23,14 @@ import static com.android.internal.util.CollectionUtils.emptyIfNull; import static java.util.Objects.requireNonNull; import android.Manifest; +import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.StringDef; import android.annotation.UserIdInt; import android.compat.annotation.UnsupportedAppUsage; +import android.graphics.drawable.Icon; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; @@ -234,6 +236,13 @@ public final class AssociationRequest implements Parcelable { private boolean mSkipPrompt; /** + * The device icon displayed in selfManaged association dialog. + * @hide + */ + @Nullable + private Icon mDeviceIcon; + + /** * Creates a new AssociationRequest. * * @param singleDevice @@ -258,15 +267,16 @@ public final class AssociationRequest implements Parcelable { @Nullable @DeviceProfile String deviceProfile, @Nullable CharSequence displayName, boolean selfManaged, - boolean forceConfirmation) { + boolean forceConfirmation, + @Nullable Icon deviceIcon) { mSingleDevice = singleDevice; mDeviceFilters = requireNonNull(deviceFilters); mDeviceProfile = deviceProfile; mDisplayName = displayName; mSelfManaged = selfManaged; mForceConfirmation = forceConfirmation; - mCreationTime = System.currentTimeMillis(); + mDeviceIcon = deviceIcon; } /** @@ -318,6 +328,19 @@ public final class AssociationRequest implements Parcelable { return mSingleDevice; } + /** + * Get the device icon of the self-managed association request. + * + * @return the device icon, or {@code null} if no device icon has been set. + * + * @see Builder#setDeviceIcon(Icon) + */ + @FlaggedApi(Flags.FLAG_ASSOCIATION_DEVICE_ICON) + @Nullable + public Icon getDeviceIcon() { + return mDeviceIcon; + } + /** @hide */ public void setPackageName(@NonNull String packageName) { mPackageName = packageName; @@ -365,6 +388,7 @@ public final class AssociationRequest implements Parcelable { private CharSequence mDisplayName; private boolean mSelfManaged = false; private boolean mForceConfirmation = false; + private Icon mDeviceIcon = null; public Builder() {} @@ -450,6 +474,23 @@ public final class AssociationRequest implements Parcelable { return this; } + /** + * Set the device icon for the self-managed device and this icon will be + * displayed in the self-managed association dialog. + * + * @throws IllegalArgumentException if the icon is not exactly 24dp by 24dp + * or if it is {@link Icon#TYPE_URI} or {@link Icon#TYPE_URI_ADAPTIVE_BITMAP}. + * @see #setSelfManaged(boolean) + */ + @NonNull + @RequiresPermission(REQUEST_COMPANION_SELF_MANAGED) + @FlaggedApi(Flags.FLAG_ASSOCIATION_DEVICE_ICON) + public Builder setDeviceIcon(@NonNull Icon deviceIcon) { + checkNotUsed(); + mDeviceIcon = requireNonNull(deviceIcon); + return this; + } + /** @inheritDoc */ @NonNull @Override @@ -460,7 +501,7 @@ public final class AssociationRequest implements Parcelable { + "provide the display name of the device"); } return new AssociationRequest(mSingleDevice, emptyIfNull(mDeviceFilters), - mDeviceProfile, mDisplayName, mSelfManaged, mForceConfirmation); + mDeviceProfile, mDisplayName, mSelfManaged, mForceConfirmation, mDeviceIcon); } } @@ -561,7 +602,9 @@ public final class AssociationRequest implements Parcelable { && Objects.equals(mDeviceProfilePrivilegesDescription, that.mDeviceProfilePrivilegesDescription) && mCreationTime == that.mCreationTime - && mSkipPrompt == that.mSkipPrompt; + && mSkipPrompt == that.mSkipPrompt + && (mDeviceIcon == null ? that.mDeviceIcon == null + : mDeviceIcon.sameAs(that.mDeviceIcon)); } @Override @@ -579,6 +622,8 @@ public final class AssociationRequest implements Parcelable { _hash = 31 * _hash + Objects.hashCode(mDeviceProfilePrivilegesDescription); _hash = 31 * _hash + Long.hashCode(mCreationTime); _hash = 31 * _hash + Boolean.hashCode(mSkipPrompt); + _hash = 31 * _hash + Objects.hashCode(mDeviceIcon); + return _hash; } @@ -606,6 +651,12 @@ public final class AssociationRequest implements Parcelable { dest.writeString8(mDeviceProfilePrivilegesDescription); } dest.writeLong(mCreationTime); + if (mDeviceIcon != null) { + dest.writeInt(1); + mDeviceIcon.writeToParcel(dest, flags); + } else { + dest.writeInt(0); + } } @Override @@ -650,6 +701,11 @@ public final class AssociationRequest implements Parcelable { this.mDeviceProfilePrivilegesDescription = deviceProfilePrivilegesDescription; this.mCreationTime = creationTime; this.mSkipPrompt = skipPrompt; + if (in.readInt() == 1) { + mDeviceIcon = Icon.CREATOR.createFromParcel(in); + } else { + mDeviceIcon = null; + } } @NonNull diff --git a/core/java/android/companion/CompanionDeviceManager.java b/core/java/android/companion/CompanionDeviceManager.java index 1cdf3b11f247..dfad6de4ba16 100644 --- a/core/java/android/companion/CompanionDeviceManager.java +++ b/core/java/android/companion/CompanionDeviceManager.java @@ -20,6 +20,8 @@ import static android.Manifest.permission.REQUEST_COMPANION_PROFILE_APP_STREAMIN import static android.Manifest.permission.REQUEST_COMPANION_PROFILE_AUTOMOTIVE_PROJECTION; import static android.Manifest.permission.REQUEST_COMPANION_PROFILE_COMPUTER; import static android.Manifest.permission.REQUEST_COMPANION_PROFILE_WATCH; +import static android.graphics.drawable.Icon.TYPE_URI; +import static android.graphics.drawable.Icon.TYPE_URI_ADAPTIVE_BITMAP; import android.annotation.CallbackExecutor; @@ -49,6 +51,11 @@ import android.content.Context; import android.content.Intent; import android.content.IntentSender; import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.graphics.drawable.VectorDrawable; import android.net.MacAddress; import android.os.Binder; import android.os.Handler; @@ -535,6 +542,13 @@ public final class CompanionDeviceManager { Objects.requireNonNull(executor, "Executor cannot be null"); Objects.requireNonNull(callback, "Callback cannot be null"); + final Icon deviceIcon = request.getDeviceIcon(); + + if (deviceIcon != null && !isValidIcon(deviceIcon, mContext)) { + throw new IllegalArgumentException("The size of the device icon must be 24dp x 24dp to" + + "ensure proper display"); + } + try { mService.associate(request, new AssociationRequestCallbackProxy(executor, callback), mContext.getOpPackageName(), mContext.getUserId()); @@ -2027,4 +2041,34 @@ public final class CompanionDeviceManager { } } } + + private boolean isValidIcon(Icon icon, Context context) { + if (icon.getType() == TYPE_URI_ADAPTIVE_BITMAP || icon.getType() == TYPE_URI) { + throw new IllegalArgumentException("The URI based Icon is not supported."); + } + Drawable drawable = icon.loadDrawable(context); + float density = context.getResources().getDisplayMetrics().density; + + if (drawable instanceof BitmapDrawable) { + Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap(); + + float widthDp = bitmap.getWidth() / density; + float heightDp = bitmap.getHeight() / density; + + if (widthDp != 24 || heightDp != 24) { + return false; + } + } else if (drawable instanceof VectorDrawable) { + VectorDrawable vectorDrawable = (VectorDrawable) drawable; + float widthDp = vectorDrawable.getIntrinsicWidth() / density; + float heightDp = vectorDrawable.getIntrinsicHeight() / density; + + if (widthDp != 24 || heightDp != 24) { + return false; + } + } else { + throw new IllegalArgumentException("The format of the device icon is unsupported."); + } + return true; + } } diff --git a/core/java/android/companion/flags.aconfig b/core/java/android/companion/flags.aconfig index 93d62cfeb537..2539a12a2a14 100644 --- a/core/java/android/companion/flags.aconfig +++ b/core/java/android/companion/flags.aconfig @@ -55,3 +55,11 @@ flag { description: "Enable association failure code API" bug: "331459560" } + +flag { + name: "association_device_icon" + is_exported: true + namespace: "companion" + description: "Enable set device icon API" + bug: "341057668" +} diff --git a/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml b/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml index 08155dd3e09a..5805332418d0 100644 --- a/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml +++ b/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml @@ -31,6 +31,12 @@ <!-- A header for selfManaged devices only. --> <include layout="@layout/vendor_header" /> + <!-- A device icon for selfManaged devices only. --> + <ImageView + android:id="@+id/device_icon" + android:visibility="gone" + android:contentDescription="@null" + style="@style/DeviceIcon" /> <!-- Do NOT change the ID of the root LinearLayout above: it's referenced in CTS tests. --> diff --git a/packages/CompanionDeviceManager/res/values/styles.xml b/packages/CompanionDeviceManager/res/values/styles.xml index fe7cfc64603a..a161a505a0ac 100644 --- a/packages/CompanionDeviceManager/res/values/styles.xml +++ b/packages/CompanionDeviceManager/res/values/styles.xml @@ -137,4 +137,10 @@ <item name="android:textAppearance">@android:style/TextAppearance.DeviceDefault.Medium</item> </style> + <style name="DeviceIcon"> + <item name="android:layout_width">24dp</item> + <item name="android:layout_height">24dp</item> + <item name="android:layout_gravity">center</item> + </style> + </resources>
\ No newline at end of file diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java index 7974a37aea57..39bbc25c9384 100644 --- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java @@ -61,6 +61,7 @@ import android.graphics.BlendMode; import android.graphics.BlendModeColorFilter; import android.graphics.Color; import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; import android.net.MacAddress; import android.os.Bundle; import android.os.Handler; @@ -130,6 +131,8 @@ public class CompanionAssociationActivity extends FragmentActivity implements // Present for single device and multiple device only. private ImageView mProfileIcon; + // Present for self managed association only; + private ImageView mDeviceIcon; // Only present for selfManaged devices. private ImageView mVendorHeaderImage; @@ -306,6 +309,8 @@ public class CompanionAssociationActivity extends FragmentActivity implements mVendorHeaderName = findViewById(R.id.vendor_header_name); mVendorHeaderButton = findViewById(R.id.vendor_header_button); + mDeviceIcon = findViewById(R.id.device_icon); + mDeviceListRecyclerView = findViewById(R.id.device_list); mMultipleDeviceSpinner = findViewById(R.id.spinner_multiple_device); @@ -430,6 +435,7 @@ public class CompanionAssociationActivity extends FragmentActivity implements final Drawable vendorIcon; final CharSequence vendorName; final Spanned title; + final Icon deviceIcon = mRequest.getDeviceIcon(); if (!SUPPORTED_SELF_MANAGED_PROFILES.contains(deviceProfile)) { throw new RuntimeException("Unsupported profile " + deviceProfile); @@ -452,6 +458,11 @@ public class CompanionAssociationActivity extends FragmentActivity implements title = getHtmlFromResources(this, PROFILE_TITLES.get(deviceProfile), mAppLabel, getString(R.string.device_type), deviceName); + if (deviceIcon != null) { + mDeviceIcon.setImageIcon(deviceIcon); + mDeviceIcon.setVisibility(View.VISIBLE); + } + if (PROFILE_SUMMARIES.containsKey(deviceProfile)) { final int summaryResourceId = PROFILE_SUMMARIES.get(deviceProfile); final Spanned summary = getHtmlFromResources(this, summaryResourceId, diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index 42f69e9ae02f..95281c81fc33 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -676,7 +676,7 @@ public class CompanionDeviceManagerService extends SystemService { final MacAddress macAddressObj = MacAddress.fromString(macAddress); mAssociationRequestsProcessor.createAssociation(userId, packageName, macAddressObj, - null, null, null, false, null, null); + null, null, null, false, null, null, null); } private void checkCanCallNotificationApi(String callingPackage, int userId) { diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java index 4fc9d55d3e17..280494544e3b 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java @@ -106,8 +106,8 @@ class CompanionDeviceShellCommand extends ShellCommand { boolean selfManaged = getNextBooleanArg(); final MacAddress macAddress = MacAddress.fromString(address); mAssociationRequestsProcessor.createAssociation(userId, packageName, macAddress, - deviceProfile, deviceProfile, /* associatedDevice */ null, selfManaged, - /* callback */ null, /* resultReceiver */ null); + deviceProfile, deviceProfile, /* associatedDevice */ null, false, + /* callback */ null, /* resultReceiver */ null, /* deviceIcon */ null); } break; diff --git a/services/companion/java/com/android/server/companion/association/AssociationDiskStore.java b/services/companion/java/com/android/server/companion/association/AssociationDiskStore.java index 46d60f9c8504..0d57b40f7da4 100644 --- a/services/companion/java/com/android/server/companion/association/AssociationDiskStore.java +++ b/services/companion/java/com/android/server/companion/association/AssociationDiskStore.java @@ -17,10 +17,12 @@ package com.android.server.companion.association; import static com.android.internal.util.XmlUtils.readBooleanAttribute; +import static com.android.internal.util.XmlUtils.readByteArrayAttribute; import static com.android.internal.util.XmlUtils.readIntAttribute; import static com.android.internal.util.XmlUtils.readLongAttribute; import static com.android.internal.util.XmlUtils.readStringAttribute; import static com.android.internal.util.XmlUtils.writeBooleanAttribute; +import static com.android.internal.util.XmlUtils.writeByteArrayAttribute; import static com.android.internal.util.XmlUtils.writeIntAttribute; import static com.android.internal.util.XmlUtils.writeLongAttribute; import static com.android.internal.util.XmlUtils.writeStringAttribute; @@ -36,6 +38,7 @@ import android.annotation.Nullable; import android.annotation.SuppressLint; import android.annotation.UserIdInt; import android.companion.AssociationInfo; +import android.graphics.drawable.Icon; import android.net.MacAddress; import android.os.Environment; import android.util.AtomicFile; @@ -51,6 +54,7 @@ import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlSerializer; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -172,6 +176,7 @@ public final class AssociationDiskStore { private static final String XML_ATTR_TIME_APPROVED = "time_approved"; private static final String XML_ATTR_LAST_TIME_CONNECTED = "last_time_connected"; private static final String XML_ATTR_SYSTEM_DATA_SYNC_FLAGS = "system_data_sync_flags"; + private static final String XML_ATTR_DEVICE_ICON = "device_icon"; private static final String LEGACY_XML_ATTR_DEVICE = "device"; @@ -393,7 +398,7 @@ public final class AssociationDiskStore { return new AssociationInfo(associationId, userId, appPackage, tag, MacAddress.fromString(deviceAddress), null, profile, null, /* managedByCompanionApp */ false, notify, /* revoked */ false, /* pending */ false, - timeApproved, Long.MAX_VALUE, /* systemDataSyncFlags */ 0); + timeApproved, Long.MAX_VALUE, /* systemDataSyncFlags */ 0, null); } private static Associations readAssociationsV1(@NonNull TypedXmlPullParser parser, @@ -444,10 +449,12 @@ public final class AssociationDiskStore { parser, XML_ATTR_LAST_TIME_CONNECTED, Long.MAX_VALUE); final int systemDataSyncFlags = readIntAttribute(parser, XML_ATTR_SYSTEM_DATA_SYNC_FLAGS, 0); + final Icon deviceIcon = byteArrayToIcon( + readByteArrayAttribute(parser, XML_ATTR_DEVICE_ICON)); return new AssociationInfo(associationId, userId, appPackage, tag, macAddress, displayName, profile, null, selfManaged, notify, revoked, pending, timeApproved, - lastTimeConnected, systemDataSyncFlags); + lastTimeConnected, systemDataSyncFlags, deviceIcon); } private static void writeAssociations(@NonNull XmlSerializer parent, @@ -480,6 +487,8 @@ public final class AssociationDiskStore { writeLongAttribute( serializer, XML_ATTR_LAST_TIME_CONNECTED, a.getLastTimeConnectedMs()); writeIntAttribute(serializer, XML_ATTR_SYSTEM_DATA_SYNC_FLAGS, a.getSystemDataSyncFlags()); + writeByteArrayAttribute( + serializer, XML_ATTR_DEVICE_ICON, iconToByteArray(a.getDeviceIcon())); serializer.endTag(null, XML_TAG_ASSOCIATION); } @@ -494,4 +503,24 @@ public final class AssociationDiskStore { private static @Nullable MacAddress stringToMacAddress(@Nullable String address) { return address != null ? MacAddress.fromString(address) : null; } + + private static byte[] iconToByteArray(Icon deviceIcon) + throws IOException { + if (deviceIcon == null) { + return null; + } + + ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); + deviceIcon.writeToStream(byteStream); + return byteStream.toByteArray(); + } + + private static Icon byteArrayToIcon(byte[] bytes) throws IOException { + if (bytes == null) { + return null; + } + + ByteArrayInputStream byteStream = new ByteArrayInputStream(bytes); + return Icon.createFromStream(byteStream); + } } diff --git a/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java b/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java index d56f17bf5edd..aebd11a7f582 100644 --- a/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java +++ b/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java @@ -48,6 +48,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentSender; import android.content.pm.PackageManagerInternal; +import android.graphics.drawable.Icon; import android.net.MacAddress; import android.os.Binder; import android.os.Bundle; @@ -281,7 +282,7 @@ public class AssociationRequestsProcessor { createAssociation(userId, packageName, macAddress, request.getDisplayName(), request.getDeviceProfile(), request.getAssociatedDevice(), request.isSelfManaged(), - callback, resultReceiver); + callback, resultReceiver, request.getDeviceIcon()); }); } @@ -292,15 +293,15 @@ public class AssociationRequestsProcessor { @Nullable MacAddress macAddress, @Nullable CharSequence displayName, @Nullable String deviceProfile, @Nullable AssociatedDevice associatedDevice, boolean selfManaged, @Nullable IAssociationRequestCallback callback, - @Nullable ResultReceiver resultReceiver) { + @Nullable ResultReceiver resultReceiver, @Nullable Icon deviceIcon) { final int id = mAssociationStore.getNextId(); final long timestamp = System.currentTimeMillis(); final AssociationInfo association = new AssociationInfo(id, userId, packageName, /* tag */ null, macAddress, displayName, deviceProfile, associatedDevice, selfManaged, /* notifyOnDeviceNearby */ false, /* revoked */ false, - /* pending */ false, timestamp, Long.MAX_VALUE, /* systemDataSyncFlags */ 0); - + /* pending */ false, timestamp, Long.MAX_VALUE, /* systemDataSyncFlags */ 0, + deviceIcon); // Add role holder for association (if specified) and add new association to store. maybeGrantRoleAndStoreAssociation(association, callback, resultReceiver); } diff --git a/services/companion/java/com/android/server/companion/utils/PermissionsUtils.java b/services/companion/java/com/android/server/companion/utils/PermissionsUtils.java index c927cd04cf64..f37e0c94caca 100644 --- a/services/companion/java/com/android/server/companion/utils/PermissionsUtils.java +++ b/services/companion/java/com/android/server/companion/utils/PermissionsUtils.java @@ -86,7 +86,7 @@ public final class PermissionsUtils { @NonNull AssociationRequest request, int packageUid) { enforcePermissionForRequestingProfile(context, request.getDeviceProfile(), packageUid); - if (request.isSelfManaged()) { + if (request.isSelfManaged() || request.getDeviceIcon() != null) { enforcePermissionForRequestingSelfManaged(context, packageUid); } } diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java index 4d067f6bfc62..9cdc229c1138 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java @@ -2000,7 +2000,8 @@ public class VirtualDeviceManagerServiceTest { /* tag= */ null, MacAddress.BROADCAST_ADDRESS, displayName, deviceProfile, /* associatedDevice= */ null, /* selfManaged= */ true, /* notifyOnDeviceNearby= */ false, /* revoked= */ false, /* pending= */ false, - /* timeApprovedMs= */0, /* lastTimeConnectedMs= */0, /* systemDataSyncFlags= */ -1); + /* timeApprovedMs= */0, /* lastTimeConnectedMs= */0, + /* systemDataSyncFlags= */ -1, /* deviceIcon= */ null); } /** Helper class to drop permissions temporarily and restore them at the end of a test. */ |